CLI Overview
The Ts.ED CLI is no longer only a project scaffolder. The current runtime layers @tsed/cli-core with three specialized packages:
@tsed/cli-promptsto orchestrate conversational flows with the Ts.ED DI container.@tsed/cli-tasksto stream progress, logs, and nested steps inside the terminal.@tsed/cli-mcpto expose any CLI capability through the Model Context Protocol (MCP) so AI assistants can call it.
This page shows how the pieces fit together, which versions you need, and how to bootstrap an interactive workflow.
Architecture at a glance
@tsed/cli-corebootstraps the DI container, loads installed plugins, and resolves commands/tasks/prompts via decorators.@tsed/cli-promptsdeclares reusable prompt providers. Each prompt receives the DI context, so you can reuse services or share state across steps.@tsed/cli-tasksruns ordered task arrays and injects aTaskLoggerinto each step so you can stream progress, logs, and status updates to the terminal.@tsed/cli-mcpwraps the same DI container in an MCP server. External clients (Claude Desktop, VS Code Agents, etc.) can call CLI tools through the MCP spec without shell access.
The runtime ensures tasks and prompts always run inside a DIContext, so your generators, plugins, and MCP tools share the exact same services.
Compatibility matrix
| Package | Minimum version | Node.js recommendation | Notes |
|---|---|---|---|
@tsed/cli-core | 7.0.0 | 3+ | Base runtime for custom CLIs and plugins. |
@tsed/cli-prompts | 7.0.0 | 3+ | Depends on @tsed/di and @clack/prompts. |
@tsed/cli-tasks | 7.0.0 | 3+ | Uses WHATWG streams for log forwarding. |
@tsed/cli-mcp | 7.0.0 | 20+ recommended | MCP transports lean on @modelcontextprotocol/sdk features that benefit from the Node 20 fetch stack. |
TIP
The CLI binaries still support Node 16, but interactive prompts/tasks rely on Intl APIs that work best on Node 18+. Running on Node 20 ensures the bundled MCP server can reuse fetch without polyfills.
@tsed/cli-core provides a bunch of reusable Ts.ED injectable services to handle differents usecases:
Quickstart
- Install the runtime packages inside any Node.js project. Include
@tsed/cli-mcponly if you plan to expose commands over MCP:
npm install @tsed/cli-core @tsed/cli-prompts @tsed/cli-tasks
npm install @tsed/cli-mcp --save-dev # optional MCP serveryarn add @tsed/cli-core @tsed/cli-prompts @tsed/cli-tasks
yarn add -D @tsed/cli-mcp # optionalpnpm add @tsed/cli-core @tsed/cli-prompts @tsed/cli-tasks
pnpm add -D @tsed/cli-mcp # optionalbun add @tsed/cli-core @tsed/cli-prompts @tsed/cli-tasks
bun add -d @tsed/cli-mcp # optional- Adopt the same project layout the official CLI uses so your commands, tools, resources, and templates stay discoverable:
my-cli/
├─ package.json
├─ tsconfig.json
└─ src/
├─ bin/
│ └─ index.ts
├─ commands/
│ └─ InteractiveWelcome.ts
├─ resources/
├─ tools/
├─ prompts/
└─ templates/Peeking at packages/cli/src/bin/tsed.ts in this repo shows the exact layout Ts.ED uses in production.
- Register prompts, tasks, and commands. Pick whichever API fits your style—decorators Command or the command helper:
import {mkdir, writeFile} from "node:fs/promises";
import {join} from "node:path";
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";
type Runtime = "node" | "bun";
export interface WelcomeContext {
projectName: string;
runtime: Runtime;
installDeps: boolean;
}
@Command({
name: "interactive:welcome",
description: "Guide developers through project bootstrap with prompts and tasks"
})
export class InteractiveWelcome implements CommandProvider<WelcomeContext> {
protected cliExeca = inject(CliExeca);
async $prompt(initial: Partial<WelcomeContext>): Promise<PromptQuestion[]> {
return [
{
type: "input",
name: "projectName",
message: "Project name",
default: initial.projectName || "awesome-cli",
validate(value) {
return value?.trim() ? undefined : "Project name is required (letters, numbers, and dashes are allowed)";
}
},
{
type: "list",
name: "runtime",
message: "Choose a runtime",
choices: [
{name: "Node.js", value: "node"},
{name: "Bun", value: "bun"}
],
default: initial.runtime || "node"
},
{
type: "confirm",
name: "installDeps",
message: "Install dependencies after generating files?",
default: true
}
];
}
$mapContext(ctx: Partial<WelcomeContext>): WelcomeContext {
return {
projectName: ctx.projectName?.trim() || "awesome-cli",
runtime: (ctx.runtime as Runtime) || "node",
installDeps: ctx.installDeps ?? true
};
}
async $exec(ctx: WelcomeContext): Promise<Task<WelcomeContext>[]> {
return [
{
title: "Create workspace",
task: async (context, logger) => {
const destination = join(process.cwd(), context.projectName);
await mkdir(destination, {recursive: true});
await writeFile(join(destination, "README.md"), `# ${context.projectName}\n\nGenerated with the Ts.ED CLI runtime.\n`);
logger.message(`Workspace ready at ${destination}`);
}
},
{
title: "Install dependencies",
skip: (context) => (!context.installDeps ? "Skipped by --no-install flag" : false),
task: async (context, logger) => {
logger.message("Installing packages (this may take a minute)...");
const packageManager = context.runtime === "bun" ? "bun" : "npm";
return this.cliExeca.run(packageManager, ["install"], {
cwd: join(process.cwd(), context.projectName)
});
}
}
];
}
}import {mkdir, writeFile} from "node:fs/promises";
import {join} from "node:path";
import {CliExeca, command} from "@tsed/cli-core";
import type {Task} from "@tsed/cli-tasks";
import {inject} from "@tsed/di";
import {s} from "@tsed/schema";
type Runtime = "node" | "bun";
export interface WelcomeContext {
projectName: string;
runtime: Runtime;
installDeps: boolean;
}
const WelcomeSchema = s.object({
projectName: s.string().prompt("Project name").default("awesome-cli"),
runtime: s
.enums<Runtime>(["node", "bun"])
.prompt("Choose a runtime")
.choices([
{label: "Node.js", value: "node"},
{label: "Bun", value: "bun"}
])
.default("node"),
installDeps: s.boolean().prompt("Install dependencies after generating files?").default(true)
});
export const InteractiveWelcome = command<WelcomeContext>({
name: "interactive:welcome",
description: "Guide developers through project bootstrap with prompts and tasks",
inputSchema: WelcomeSchema,
async handler(context): Promise<Task<WelcomeContext>[]> {
const cliExeca = inject(CliExeca);
return [
{
title: "Create workspace",
task: async (ctx, logger) => {
const destination = join(process.cwd(), ctx.projectName);
await mkdir(destination, {recursive: true});
await writeFile(join(destination, "README.md"), `# ${ctx.projectName}\n\nGenerated with the Ts.ED CLI runtime.\n`);
logger.message(`Workspace ready at ${destination}`);
}
},
{
title: "Install dependencies",
skip: (ctx) => (!ctx.installDeps ? "Skipped by --no-install flag" : false),
task: async (ctx, logger) => {
logger.message("Installing packages (this may take a minute)...");
const packageManager = ctx.runtime === "bun" ? "bun" : "npm";
return cliExeca.run(packageManager, ["install"], {
cwd: join(process.cwd(), ctx.projectName)
});
}
}
];
}
}).token();TIP
The functional example shows how inputSchema replaces ad-hoc args/options. See the Commands guide for every helper (.prompt(), .choices(), .when(), etc.).
- Bootstrap the runtime in
src/bin/index.ts(or any entrypoint undersrc/bin). This mirrors the officialpackages/cli/src/bin/tsed.tsentrypoint without the module-alias hook:
#!/usr/bin/env node
import {CliCore, type PackageJson} from "@tsed/cli-core";
import pkg from "../../package.json" assert {type: "json"};
import {InteractiveWelcome} from "../commands/InteractiveWelcome.ts";
CliCore.bootstrap({
name: "awesome",
pkg: pkg as PackageJson,
commands: [InteractiveWelcome],
tools: [],
resources: [],
prompts: [],
updateNotifier: false,
checkPrecondition: false,
logger: {
level: process.env.DEBUG ? "debug" : "info"
}
}).catch((error) => {
console.error(error);
process.exit(1);
});- Execute commands through Node + SWC so both the functional API and decorators work without a build step:
node --import @swc-node/register/esm-register src/bin/index.ts interactive:welcome- guides the user through a
PromptRunner-powered conversation to collect inputs, - hands the data to
@tsed/cli-tasksso eachTask<Context>streams logs through the shared renderer, - and persists the result via any Ts.ED service available in DI.
Where to go next
- Learn how to configure
@Command/command()in Commands. - Build custom generators with Templates.
- Compose rich multi-step conversations in Prompts.
- Stream progress, handle retries, and chain subtasks in Tasks.
- Learn how to expose the CLI over MCP in MCP servers.