Command configuration
@tsed/cli-core exposes two equivalent APIs: the @Command decorator for class-based providers and the command() helper for lightweight functions. Both surface the same option bag defined in packages/cli-core/src/interfaces/CommandOptions.ts.
Two APIs, same surface
import {CliExeca, Command, type CommandProvider} from "@tsed/cli-core";
import type {PromptQuestion} from "@tsed/cli-prompts";
import type {Task} from "@tsed/cli-tasks";
import {inject} from "@tsed/di";
interface BuildContext {
project: string;
install: boolean;
}
@Command({
name: "build:assets",
alias: "ba",
description: "Bundle templates and install deps",
args: {
project: {description: "Project path", type: String, required: true}
},
options: {
"--no-install": {description: "Skip package installation", type: Boolean}
}
})
export class BuildAssetsCmd implements CommandProvider<BuildContext> {
protected cliExeca = inject(CliExeca);
async $prompt(initial: Partial<BuildContext>): Promise<PromptQuestion[]> {
return [
{
type: "confirm",
name: "install",
message: "Install dependencies?",
default: initial.install ?? true
}
];
}
$mapContext(ctx: Partial<BuildContext>): BuildContext {
return {
project: (ctx.project || process.cwd()).trim(),
install: ctx.install ?? true
};
}
async $exec(ctx: BuildContext): Promise<Task<BuildContext>[]> {
return [
{
title: "Install dependencies",
skip: (state) => (!state.install ? "Skipped by prompt" : false),
task: (state) =>
this.cliExeca.run("npm", ["install"], {
cwd: state.project
})
}
];
}
}import {CliExeca, command} from "@tsed/cli-core";
import type {PromptQuestion} from "@tsed/cli-prompts";
import {inject} from "@tsed/di";
interface BuildContext {
project: string;
install: boolean;
}
function prompt(initial: Partial<BuildContext>): PromptQuestion[] {
return [
{
type: "input",
name: "project",
message: "Project directory",
default: initial.project || "./"
},
{
type: "confirm",
name: "install",
message: "Install dependencies?",
default: initial.install ?? true
}
];
}
export const BuildAssetsCmd = command<BuildContext>({
name: "build:assets",
description: "Bundle templates and install deps",
alias: "ba",
prompt,
handler(ctx) {
const cliExeca = inject(CliExeca);
return [
{
title: "Install dependencies",
skip: (state) => (!state.install ? "Skipped by prompt" : false),
task: (state) =>
cliExeca.run("npm", ["install"], {
cwd: state.project || process.cwd()
})
}
];
}
}).token();Input schema Beta
Introduced in v7, Input Schema lets you describe CLI options and prompts once, then reuse that definition everywhere. Attach a @tsed/schema JSON schema (or factory) to drive prompt flows, Commander flags, MCP contracts, and AJV validation in one go. Prefer this over ad-hoc args/options whenever you can.
Base fields
import {s} from "@tsed/schema";
@Command({
inputSchema:
s.object({
project: s.string().description("Absolute path").default("."),
install: s.boolean().default(true)
})
})Prompts + choices + CLI flags
import {PlatformType} from "@tsed/cli-core";
@Command({
inputSchema:
s.object({
platform: s
.enums(PlatformType)
.default(PlatformType.EXPRESS)
.prompt("Choose a platform")
.choices([
{label: "Express.js", value: PlatformType.EXPRESS},
{label: "Fastify.js (beta)", value: PlatformType.FASTIFY}
])
.opt("-p, --platform <platform>")
})
})Conditional prompts with .when()
@Command({
inputSchema:
s.object({
root: s.string().default("."),
projectName: s
.string()
.prompt("Project name")
.when((ctx) => ctx.root !== ".")
.description("Defaults to the folder name when root is '.'")
})
})Nested arrays with grouped choices
import {FeatureType} from "@tsed/cli-core";
@Command({
inputSchema: () =>
s.object({
features: s
.array()
.items(s.enums(FeatureType))
.prompt("Select features")
.choices([
{
label: "ORM",
value: FeatureType.ORM,
items: [
{label: "Prisma", value: FeatureType.PRISMA},
{label: "TypeORM", value: FeatureType.TYPEORM}
]
}
])
})
})TIP
Whenever a field needs CLI flags, prompts, or MCP validation, encode it in inputSchema using helpers like .prompt(), .choices(), .when(), .opt(), and .items()—it keeps every interface in sync and lets Ts.ED run AJV validation for free.
Options
The following keys exist on both APIs via BaseCommandOptions:
name, alias, and description
The command must be uniquely named. alias registers short forms (tsed ba) and description feeds auto-generated help output.
args
Declare positional arguments using the Commander syntax. Each entry inherits the CommandArg structure:
args: {
project: {description: "Target directory", type: String, required: true},
port: {description: "Dev server port", type: Number, defaultValue: 8080}
}options
Flag-like options map to Commander’s .option() configuration and support advanced parsing:
@Commmand({
options: {
"--pkg-manager <name>": {
description: "npm, pnpm, bun, yarn",
type: String,
customParser: (value) => value.toLowerCase()
},
"--feature <items...>": {
description: "Additional generators",
type: Array,
itemType: String
}
}
})Input schema
Introduced in v7, Input Schema lets you describe CLI options and prompts once, then reuse that definition everywhere. Attach a @tsed/schema JSON schema (or factory) to drive prompt flows, Commander flags, MCP contracts, and AJV validation in one go. Prefer this over ad-hoc args/options whenever you can.
Base fields
import {s} from "@tsed/schema";
@Commmand({
inputSchema: () =>
s.object({
project: s.string().description("Absolute path").default("."),
install: s.boolean().default(true)
});
})Prompts + choices + CLI flags
import {PlatformType} from "@tsed/cli-core";
@Commmand({
inputSchema: () =>
s.object({
platform: s
.enums(PlatformType)
.default(PlatformType.EXPRESS)
.prompt("Choose a platform")
.choices([
{label: "Express.js", value: PlatformType.EXPRESS},
{label: "Fastify.js (beta)", value: PlatformType.FASTIFY}
])
.opt("-p, --platform <platform>")
});
})Conditional prompts with .when()
inputSchema: () =>
s.object({
root: s.string().default("."),
projectName: s
.string()
.prompt("Project name")
.when((ctx) => ctx.root !== ".")
.description("Defaults to the folder name when root is '.'")
});Nested arrays with grouped choices
import {FeatureType} from "@tsed/cli-core";
@Commmand({
inputSchema: () =>
s.object({
features: s
.array()
.items(s.enums(FeatureType))
.prompt("Select features")
.choices([
{
label: "ORM",
value: FeatureType.ORM,
items: [
{label: "Prisma", value: FeatureType.PRISMA},
{label: "TypeORM", value: FeatureType.TYPEORM}
]
}
])
});
})TIP
Whenever a field needs CLI flags, prompts, or MCP validation, encode it in inputSchema using helpers like .prompt(), .choices(), .when(), .opt(), and .items()—it keeps every interface in sync and lets Ts.ED run AJV validation for free.
Options
The following keys exist on both APIs via BaseCommandOptions and apply whether you use decorators or the functional helper.
name, alias, and description
The command must be uniquely named. alias registers short forms (tsed ba) and description feeds auto-generated help output.
args
Declare positional arguments using the Commander syntax. Each entry inherits the CommandArg structure:
@Commmand({
args: {
project: {description: "Target directory", type: String, required: true},
port: {description: "Dev server port", type: Number, defaultValue: 8080}
}
})options
Flag-like options map to Commander’s .option() configuration and support advanced parsing:
@Commmand({
options: {
"--pkg-manager <name>": {
description: "npm, pnpm, bun, yarn",
type: String,
customParser: (value) => value.toLowerCase()
},
"--feature <items...>": {
description: "Additional generators",
type: Array,
itemType: String
}
}
})allowUnknownOption
Set to true when you want pass-through flags (useful when piping arguments to scripts or wrangling custom builders). Unknown flags are otherwise rejected with a validation error.
disableReadUpPkg
By default, Ts.ED walks up the directory tree to locate a package.json. Set disableReadUpPkg: true to stop at the current working directory.
renderMode
Propagates to @tsed/cli-tasks so you can opt into "raw" rendering (machine-readable logs) instead of the default interactive renderer:
renderMode: "raw";Class-specific lifecycle hooks
Class-based commands implement the CommandProvider interface:
$prompt(initial)returns aPromptQuestion[](see the Prompts guide). Use it to collect input before tasks begin.$mapContext(data)lets you coerce and normalize user input (coalescing defaults, casting strings, etc.).$exec(ctx)must return aTask[](see the Tasks guide).
Because the class lives in DI, you can inject services via inject(SomeService) or constructor injection.
TIP
Expose additional helpers (e.g., protected cliFs = inject(CliFs)) so your command stays testable with @tsed/cli-testing, which mocks these services automatically.
Functional API extras
The command() helper accepts the same option bag but swaps lifecycle hooks for plain functions:
prompt(initial)mirrors$prompt.handler(data)replaces$execand can returnTask[]orvoid.
Use this style when you want a single-file command without DI bindings. If you later need DI, convert the handler into a class and register it with @Command.
Input orchestration tips
- Prefer
inputSchemawhen you need both prompt generation and MCP integration; the schema covers CLI usage, documentation, and remote calls simultaneously. - Use
args/optionsfor plain Commander parsing, then layerpromptor$promptto ask remaining questions interactively. - Compose
renderMode: "raw"withCliCore.bootstrap({logger: {level: "info"}})when running inside CI so task output stays structured and parsable.