From dedd060b479cc9d3b68bd45f30b1de685cb61fe5 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 2 Dec 2025 08:38:25 +0100 Subject: [PATCH 01/18] chore: remove missing logs --- packages/cli-core/src/packageManagers/PackageManagersModule.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli-core/src/packageManagers/PackageManagersModule.ts b/packages/cli-core/src/packageManagers/PackageManagersModule.ts index 3a2410998..3f921b5d6 100644 --- a/packages/cli-core/src/packageManagers/PackageManagersModule.ts +++ b/packages/cli-core/src/packageManagers/PackageManagersModule.ts @@ -128,7 +128,6 @@ export class PackageManagersModule { } this.projectPackageJson.setPreference("packageManager", selectedPackageManager.name); - console.log("==", name, selectedPackageManager); return selectedPackageManager; } From a04eef41b1b9f7b59a75746f482da0a15b4b68bd Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 10 Dec 2025 07:58:27 +0100 Subject: [PATCH 02/18] refactor(cli): update command definitions to use object token syntax --- packages/cli-core/src/fn/command.ts | 23 +++++++++++++++---- packages/cli/src/commands/add/AddCmd.ts | 3 ++- .../cli/src/commands/generate/GenerateCmd.ts | 3 ++- packages/cli/src/commands/init/InitCmd.ts | 3 ++- packages/cli/src/commands/run/RunCmd.ts | 3 ++- .../template/CreateTemplateCommand.ts | 3 ++- packages/cli/src/commands/update/UpdateCmd.ts | 7 +++--- 7 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/cli-core/src/fn/command.ts b/packages/cli-core/src/fn/command.ts index 89d68a012..9ee9d9aed 100644 --- a/packages/cli-core/src/fn/command.ts +++ b/packages/cli-core/src/fn/command.ts @@ -1,8 +1,23 @@ -import {injectable, type TokenProvider} from "@tsed/di"; +import type {Type} from "@tsed/core"; +import {type FactoryTokenProvider, injectable, type TokenProvider} from "@tsed/di"; import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; -import type {CommandParameters} from "../interfaces/CommandParameters.js"; +import type {CommandOptions} from "../interfaces/CommandOptions.js"; +import type {CommandProvider} from "../interfaces/index.js"; -export function command(token: TokenProvider, options: CommandParameters): ReturnType { - return injectable(token).type("command").set(CommandStoreKeys.COMMAND, options); +export function command(options: CommandOptions) { + if (!options.token) { + return injectable>(Symbol.for(`COMMAND_${options.name}`) as any) + .type("command") + .set(CommandStoreKeys.COMMAND, options) + .factory(() => { + return { + ...options, + $prompt: options.prompt, + $exec: options.handler + }; + }); + } + + return injectable>(options.token).type("command").set(CommandStoreKeys.COMMAND, options); } diff --git a/packages/cli/src/commands/add/AddCmd.ts b/packages/cli/src/commands/add/AddCmd.ts index c61b340f8..e441f0688 100644 --- a/packages/cli/src/commands/add/AddCmd.ts +++ b/packages/cli/src/commands/add/AddCmd.ts @@ -59,7 +59,8 @@ export class AddCmd implements CommandProvider { } } -command(AddCmd, { +command({ + token: AddCmd, name: "add", description: "Add cli plugin to the current project", args: { diff --git a/packages/cli/src/commands/generate/GenerateCmd.ts b/packages/cli/src/commands/generate/GenerateCmd.ts index 54179c531..55c0e92ff 100644 --- a/packages/cli/src/commands/generate/GenerateCmd.ts +++ b/packages/cli/src/commands/generate/GenerateCmd.ts @@ -75,7 +75,8 @@ export class GenerateCmd implements CommandProvider { } } -command(GenerateCmd, { +command({ + token: GenerateCmd, name: "generate", alias: "g", description: "Generate a new provider class", diff --git a/packages/cli/src/commands/init/InitCmd.ts b/packages/cli/src/commands/init/InitCmd.ts index 3f90f802c..c7ae2513c 100644 --- a/packages/cli/src/commands/init/InitCmd.ts +++ b/packages/cli/src/commands/init/InitCmd.ts @@ -446,7 +446,8 @@ export class InitCmd implements CommandProvider { } } -command(InitCmd, { +command({ + token: InitCmd, name: "init", description: "Init a new Ts.ED project", args: { diff --git a/packages/cli/src/commands/run/RunCmd.ts b/packages/cli/src/commands/run/RunCmd.ts index 9d9c5acb2..436f9616c 100644 --- a/packages/cli/src/commands/run/RunCmd.ts +++ b/packages/cli/src/commands/run/RunCmd.ts @@ -26,7 +26,8 @@ export class RunCmd implements CommandProvider { } } -command(RunCmd, { +command({ + token: RunCmd, name: "run", description: "Run a project level command", args: { diff --git a/packages/cli/src/commands/template/CreateTemplateCommand.ts b/packages/cli/src/commands/template/CreateTemplateCommand.ts index 5bf28aeca..eb5c1ced2 100644 --- a/packages/cli/src/commands/template/CreateTemplateCommand.ts +++ b/packages/cli/src/commands/template/CreateTemplateCommand.ts @@ -97,7 +97,8 @@ export class CreateTemplateCommand implements CommandProvider { } } -command(CreateTemplateCommand, { +command({ + token: CreateTemplateCommand, name: "template", description: "Create a custom template that can be selected in tsed generate command", args: { diff --git a/packages/cli/src/commands/update/UpdateCmd.ts b/packages/cli/src/commands/update/UpdateCmd.ts index 4dc5327a5..c5a156223 100644 --- a/packages/cli/src/commands/update/UpdateCmd.ts +++ b/packages/cli/src/commands/update/UpdateCmd.ts @@ -141,9 +141,8 @@ export class UpdateCmd implements CommandProvider { } } -command(UpdateCmd, { +command({ + token: UpdateCmd, name: "update", - description: "Update all Ts.ED packages used by your project", - args: {}, - options: {} + description: "Update all Ts.ED packages used by your project" }); From 2504a8bdb39a92fabf97feadace34ecff1d0890b Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 10 Dec 2025 08:00:22 +0100 Subject: [PATCH 03/18] refactor(cli-core): remove unused hook decorators and simplify command options - Deleted `@On`, `@OnAdd`, `@OnExec`, `@OnPostInstall`, and `@OnPrompt` with associated tests. - Simplified `CommandOptions` design by refactoring `CommandParameters` into specific types. - Updated DI usage in services and functions for clean and efficient dependency management. - Improved command execution flow by streamlining task handling logic. --- packages/cli-core/src/decorators/command.ts | 6 +- packages/cli-core/src/decorators/index.ts | 4 - packages/cli-core/src/decorators/on.ts | 9 -- .../cli-core/src/decorators/onAdd.spec.ts | 18 ---- packages/cli-core/src/decorators/onAdd.ts | 6 -- .../cli-core/src/decorators/onExec.spec.ts | 19 ----- packages/cli-core/src/decorators/onExec.ts | 6 -- .../src/decorators/onPostInstall.spec.ts | 19 ----- .../cli-core/src/decorators/onPostInstall.ts | 6 -- .../cli-core/src/decorators/onPrompt.spec.ts | 19 ----- packages/cli-core/src/decorators/onPrompt.ts | 6 -- .../cli-core/src/domains/CommandStoreKeys.ts | 7 -- packages/cli-core/src/fn/command.ts | 5 +- .../src/interfaces/CommandMetadata.ts | 6 +- ...CommandParameters.ts => CommandOptions.ts} | 22 ++++- .../src/interfaces/CommandProvider.ts | 12 --- packages/cli-core/src/interfaces/index.ts | 5 +- packages/cli-core/src/services/CliPlugins.ts | 5 +- packages/cli-core/src/services/CliService.ts | 82 ++++++------------- .../src/utils/getCommandMetadata.spec.ts | 6 +- .../cli-core/src/utils/getCommandMetadata.ts | 5 +- .../cli-core/src/utils/mapCommanderArgs.ts | 2 +- 22 files changed, 65 insertions(+), 210 deletions(-) delete mode 100644 packages/cli-core/src/decorators/on.ts delete mode 100644 packages/cli-core/src/decorators/onAdd.spec.ts delete mode 100644 packages/cli-core/src/decorators/onAdd.ts delete mode 100644 packages/cli-core/src/decorators/onExec.spec.ts delete mode 100644 packages/cli-core/src/decorators/onExec.ts delete mode 100644 packages/cli-core/src/decorators/onPostInstall.spec.ts delete mode 100644 packages/cli-core/src/decorators/onPostInstall.ts delete mode 100644 packages/cli-core/src/decorators/onPrompt.spec.ts delete mode 100644 packages/cli-core/src/decorators/onPrompt.ts delete mode 100644 packages/cli-core/src/domains/CommandStoreKeys.ts rename packages/cli-core/src/interfaces/{CommandParameters.ts => CommandOptions.ts} (70%) diff --git a/packages/cli-core/src/decorators/command.ts b/packages/cli-core/src/decorators/command.ts index 0ce64b39d..2f3d72591 100644 --- a/packages/cli-core/src/decorators/command.ts +++ b/packages/cli-core/src/decorators/command.ts @@ -1,8 +1,8 @@ import {command} from "../fn/command.js"; -import type {CommandParameters} from "../interfaces/CommandParameters.js"; +import type {CommandOptions} from "../interfaces/CommandOptions.js"; -export function Command(options: CommandParameters): ClassDecorator { +export function Command(options: CommandOptions): ClassDecorator { return (token) => { - command(token, options); + command({...options, token}); }; } diff --git a/packages/cli-core/src/decorators/index.ts b/packages/cli-core/src/decorators/index.ts index 8c69362e8..dcbd99bc9 100644 --- a/packages/cli-core/src/decorators/index.ts +++ b/packages/cli-core/src/decorators/index.ts @@ -1,5 +1 @@ export * from "./command.js"; -export * from "./onAdd.js"; -export * from "./onExec.js"; -export * from "./onPostInstall.js"; -export * from "./onPrompt.js"; diff --git a/packages/cli-core/src/decorators/on.ts b/packages/cli-core/src/decorators/on.ts deleted file mode 100644 index e05dbdcd7..000000000 --- a/packages/cli-core/src/decorators/on.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {StoreMerge} from "@tsed/core"; - -export function On(hookName: string, name: string): MethodDecorator { - return (target, propertyKey) => { - StoreMerge(hookName, { - [name]: [propertyKey] - })(target); - }; -} diff --git a/packages/cli-core/src/decorators/onAdd.spec.ts b/packages/cli-core/src/decorators/onAdd.spec.ts deleted file mode 100644 index 8ea1c0bb3..000000000 --- a/packages/cli-core/src/decorators/onAdd.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {Store} from "@tsed/core"; - -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; -import {OnAdd} from "./onAdd.js"; - -class Test { - @OnAdd("@tsed/cli-plugin") - test() {} -} - -describe("@OnAdd", () => { - it("should store metadata", () => { - const result = Store.from(Test).get(CommandStoreKeys.ADD); - expect(result).toEqual({ - "@tsed/cli-plugin": ["test"] - }); - }); -}); diff --git a/packages/cli-core/src/decorators/onAdd.ts b/packages/cli-core/src/decorators/onAdd.ts deleted file mode 100644 index 3eb693b7b..000000000 --- a/packages/cli-core/src/decorators/onAdd.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; -import {On} from "./on.js"; - -export function OnAdd(cliPlugin: string): MethodDecorator { - return On(CommandStoreKeys.ADD, cliPlugin); -} diff --git a/packages/cli-core/src/decorators/onExec.spec.ts b/packages/cli-core/src/decorators/onExec.spec.ts deleted file mode 100644 index 861571fc3..000000000 --- a/packages/cli-core/src/decorators/onExec.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {Store} from "@tsed/core"; - -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; -import {OnExec} from "./onExec.js"; - -class Test { - @OnExec("cmd") - test() {} -} - -describe("@OnExec", () => { - it("should store metadata", () => { - const result = Store.from(Test).get(CommandStoreKeys.EXEC_HOOKS); - - expect(result).toEqual({ - cmd: ["test"] - }); - }); -}); diff --git a/packages/cli-core/src/decorators/onExec.ts b/packages/cli-core/src/decorators/onExec.ts deleted file mode 100644 index 460c68810..000000000 --- a/packages/cli-core/src/decorators/onExec.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; -import {On} from "./on.js"; - -export function OnExec(cmdName: string): MethodDecorator { - return On(CommandStoreKeys.EXEC_HOOKS, cmdName); -} diff --git a/packages/cli-core/src/decorators/onPostInstall.spec.ts b/packages/cli-core/src/decorators/onPostInstall.spec.ts deleted file mode 100644 index a170d501b..000000000 --- a/packages/cli-core/src/decorators/onPostInstall.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {Store} from "@tsed/core"; - -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; -import {OnPostInstall} from "./onPostInstall.js"; - -class Test { - @OnPostInstall("cmd") - test() {} -} - -describe("@OnPostIntall", () => { - it("should store metadata", () => { - const result = Store.from(Test).get(CommandStoreKeys.POST_INSTALL_HOOKS); - - expect(result).toEqual({ - cmd: ["test"] - }); - }); -}); diff --git a/packages/cli-core/src/decorators/onPostInstall.ts b/packages/cli-core/src/decorators/onPostInstall.ts deleted file mode 100644 index a30facf7f..000000000 --- a/packages/cli-core/src/decorators/onPostInstall.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; -import {On} from "./on.js"; - -export function OnPostInstall(cmdName: string): MethodDecorator { - return On(CommandStoreKeys.POST_INSTALL_HOOKS, cmdName); -} diff --git a/packages/cli-core/src/decorators/onPrompt.spec.ts b/packages/cli-core/src/decorators/onPrompt.spec.ts deleted file mode 100644 index b510e4933..000000000 --- a/packages/cli-core/src/decorators/onPrompt.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {Store} from "@tsed/core"; - -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; -import {OnPrompt} from "./onPrompt.js"; - -class Test { - @OnPrompt("cmd") - test() {} -} - -describe("@OnPrompt", () => { - it("should store metadata", () => { - const result = Store.from(Test).get(CommandStoreKeys.PROMPT_HOOKS); - - expect(result).toEqual({ - cmd: ["test"] - }); - }); -}); diff --git a/packages/cli-core/src/decorators/onPrompt.ts b/packages/cli-core/src/decorators/onPrompt.ts deleted file mode 100644 index c2a656307..000000000 --- a/packages/cli-core/src/decorators/onPrompt.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; -import {On} from "./on.js"; - -export function OnPrompt(cmdName: string): MethodDecorator { - return On(CommandStoreKeys.PROMPT_HOOKS, cmdName); -} diff --git a/packages/cli-core/src/domains/CommandStoreKeys.ts b/packages/cli-core/src/domains/CommandStoreKeys.ts deleted file mode 100644 index 7a0171bc0..000000000 --- a/packages/cli-core/src/domains/CommandStoreKeys.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum CommandStoreKeys { - COMMAND = "command", - ADD = "$onAdd", - EXEC_HOOKS = "$onExec", - POST_INSTALL_HOOKS = "$onPostInstall", - PROMPT_HOOKS = "$onPrompt" -} diff --git a/packages/cli-core/src/fn/command.ts b/packages/cli-core/src/fn/command.ts index 9ee9d9aed..e15220563 100644 --- a/packages/cli-core/src/fn/command.ts +++ b/packages/cli-core/src/fn/command.ts @@ -1,7 +1,6 @@ import type {Type} from "@tsed/core"; import {type FactoryTokenProvider, injectable, type TokenProvider} from "@tsed/di"; -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; import type {CommandOptions} from "../interfaces/CommandOptions.js"; import type {CommandProvider} from "../interfaces/index.js"; @@ -9,7 +8,7 @@ export function command(options: CommandOptions) { if (!options.token) { return injectable>(Symbol.for(`COMMAND_${options.name}`) as any) .type("command") - .set(CommandStoreKeys.COMMAND, options) + .set("command", options) .factory(() => { return { ...options, @@ -19,5 +18,5 @@ export function command(options: CommandOptions) { }); } - return injectable>(options.token).type("command").set(CommandStoreKeys.COMMAND, options); + return injectable>(options.token).type("command").set("command", options); } diff --git a/packages/cli-core/src/interfaces/CommandMetadata.ts b/packages/cli-core/src/interfaces/CommandMetadata.ts index 930920724..4bce221a1 100644 --- a/packages/cli-core/src/interfaces/CommandMetadata.ts +++ b/packages/cli-core/src/interfaces/CommandMetadata.ts @@ -1,6 +1,6 @@ -import type {CommandArg, CommandOptions, CommandParameters} from "./CommandParameters.js"; +import type {BaseCommandOptions, CommandArg, CommandOpts} from "./CommandOptions.js"; -export interface CommandMetadata extends CommandParameters { +export interface CommandMetadata extends BaseCommandOptions { /** * CommandProvider arguments */ @@ -11,7 +11,7 @@ export interface CommandMetadata extends CommandParameters { * CommandProvider options */ options: { - [key: string]: CommandOptions; + [key: string]: CommandOpts; }; allowUnknownOption?: boolean; diff --git a/packages/cli-core/src/interfaces/CommandParameters.ts b/packages/cli-core/src/interfaces/CommandOptions.ts similarity index 70% rename from packages/cli-core/src/interfaces/CommandParameters.ts rename to packages/cli-core/src/interfaces/CommandOptions.ts index 4762b9fe3..96c08e5ab 100644 --- a/packages/cli-core/src/interfaces/CommandParameters.ts +++ b/packages/cli-core/src/interfaces/CommandOptions.ts @@ -1,4 +1,7 @@ import {Type} from "@tsed/core"; +import type {TokenProvider} from "@tsed/di"; + +import type {CommandProvider} from "./CommandProvider.js"; export interface CommandArg { /** @@ -23,7 +26,7 @@ export interface CommandArg { required?: boolean; } -export interface CommandOptions { +export interface CommandOpts { /** * Description of the commander.option() */ @@ -51,7 +54,7 @@ export interface CommandOptions { customParser?: (value: any) => any; } -export interface CommandParameters { +export interface BaseCommandOptions { /** * name commands */ @@ -72,7 +75,7 @@ export interface CommandParameters { * CommandProvider options */ options?: { - [key: string]: CommandOptions; + [key: string]: CommandOpts; }; allowUnknownOption?: boolean; @@ -80,6 +83,19 @@ export interface CommandParameters { enableFeatures?: string[]; disableReadUpPkg?: boolean; +} + +interface FunctionalCommandOptions extends BaseCommandOptions { + prompt?: CommandProvider["$prompt"]; + handler: CommandProvider["$exec"]; + + [key: string]: any; +} + +interface ClassCommandOptions extends BaseCommandOptions { + token: TokenProvider; [key: string]: any; } + +export type CommandOptions = ClassCommandOptions | FunctionalCommandOptions; diff --git a/packages/cli-core/src/interfaces/CommandProvider.ts b/packages/cli-core/src/interfaces/CommandProvider.ts index 6ccffdcbe..c45382261 100644 --- a/packages/cli-core/src/interfaces/CommandProvider.ts +++ b/packages/cli-core/src/interfaces/CommandProvider.ts @@ -14,12 +14,6 @@ declare module "inquirer" { export type QuestionOptions = QuestionCollection; export interface CommandProvider { - /** - * Run a function before the main prompt. Useful for pre-loading data from the file system - * @param initialOptions - */ - $beforePrompt?(initialOptions: Partial): Partial; - /** * Hook to create the main prompt for the command * See https://github.com/enquirer/enquirer for more detail on question configuration. @@ -33,12 +27,6 @@ export interface CommandProvider { */ $mapContext?(ctx: Partial): Ctx; - /** - * Run something before the exec hook - * @param ctx - */ - $beforeExec?(ctx: Ctx): Promise; - /** * Run a command * @param ctx diff --git a/packages/cli-core/src/interfaces/index.ts b/packages/cli-core/src/interfaces/index.ts index 2e02eec0d..97ff8e2a8 100644 --- a/packages/cli-core/src/interfaces/index.ts +++ b/packages/cli-core/src/interfaces/index.ts @@ -1,4 +1,5 @@ import {Type} from "@tsed/core"; +import type {FactoryTokenProvider, TokenProvider} from "@tsed/di"; import type {CommandProvider} from "./CommandProvider.js"; import type {PackageJson} from "./PackageJson.js"; @@ -6,7 +7,7 @@ import type {PackageJson} from "./PackageJson.js"; export * from "./CliDefaultOptions.js"; export * from "./CommandData.js"; export * from "./CommandMetadata.js"; -export * from "./CommandParameters.js"; +export * from "./CommandOptions.js"; export * from "./CommandProvider.js"; export * from "./PackageJson.js"; export * from "./ProjectPreferences.js"; @@ -18,7 +19,7 @@ declare global { /** * Load given commands */ - commands: Type[]; + commands: TokenProvider[]; /** * Init Cli with defined argv */ diff --git a/packages/cli-core/src/services/CliPlugins.ts b/packages/cli-core/src/services/CliPlugins.ts index d5a5b6ca4..6f191f220 100644 --- a/packages/cli-core/src/services/CliPlugins.ts +++ b/packages/cli-core/src/services/CliPlugins.ts @@ -1,7 +1,7 @@ import {constant, inject, injectable} from "@tsed/di"; +import {$asyncEmit} from "@tsed/hooks"; import chalk from "chalk"; -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; import type {Task} from "../interfaces/index.js"; import {PackageManagersModule} from "../packageManagers/PackageManagersModule.js"; import {createSubTasks} from "../utils/createTasksRunner.js"; @@ -39,7 +39,8 @@ export class CliPlugins { return { title: `Run plugin '${chalk.cyan(plugin)}'`, task: () => { - return this.cliHooks.emit(CommandStoreKeys.ADD, plugin, ctx); + return $asyncEmit("$onAddPlugin", plugin, ctx); + // return this.cliHooks.emit(CommandStoreKeys.ADD, plugin, ctx); } }; }); diff --git a/packages/cli-core/src/services/CliService.ts b/packages/cli-core/src/services/CliService.ts index a742fb6cd..22005b3f7 100644 --- a/packages/cli-core/src/services/CliService.ts +++ b/packages/cli-core/src/services/CliService.ts @@ -20,10 +20,9 @@ import Inquirer from "inquirer"; import inquirer_autocomplete_prompt from "inquirer-autocomplete-prompt"; import {v4} from "uuid"; -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; import type {CommandData} from "../interfaces/CommandData.js"; import type {CommandMetadata} from "../interfaces/CommandMetadata.js"; -import type {CommandArg, CommandOptions} from "../interfaces/CommandParameters.js"; +import type {CommandArg, CommandOpts} from "../interfaces/CommandOptions.js"; import type {CommandProvider} from "../interfaces/CommandProvider.js"; import type {Task} from "../interfaces/index.js"; import {PackageManagersModule} from "../packageManagers/index.js"; @@ -72,7 +71,6 @@ export class CliService { await $asyncEmit("$loadPackageJson"); - data = await this.beforePrompt(cmdName, data, $ctx); data = await this.prompt(cmdName, data, $ctx); try { @@ -89,20 +87,20 @@ export class CliService { } public async exec(cmdName: string, data: any, $ctx: DIContext) { - const initialTasks = await this.getTasks(cmdName, data); + const tasks = await this.getTasks(cmdName, data); $ctx.set("data", data); - if (initialTasks.length) { - const tasks = [ - ...initialTasks, - { - title: "Install dependencies", - enabled: () => this.reinstallAfterRun && (this.projectPkg.rewrite || this.projectPkg.reinstall), - task: createSubTasks(() => this.packageManagers.install(data), {...data, concurrent: false}) - }, - ...(await this.getPostInstallTasks(cmdName, data)) - ]; + if (tasks.length) { + if (this.reinstallAfterRun && (this.projectPkg.rewrite || this.projectPkg.reinstall)) { + tasks.push( + { + title: "Install dependencies", + task: createSubTasks(() => this.packageManagers.install(data), {...data, concurrent: false}) + }, + ...(await this.getPostInstallTasks(cmdName, data)) + ); + } data = this.mapData(cmdName, data, $ctx); @@ -112,29 +110,6 @@ export class CliService { } } - /** - * Run prompt for a given command - * @param cmdName - * @param data Initial data - * @param $ctx - */ - public async beforePrompt(cmdName: string, data: CommandData = {}, $ctx: DIContext) { - const provider = this.commands.get(cmdName); - const instance = inject(provider.useClass)!; - const verbose = data.verbose; - - $ctx.set("data", data); - - if (instance.$beforePrompt) { - data = await instance.$beforePrompt(JSON.parse(JSON.stringify(data))); - data.verbose = verbose; - } - - $ctx.set("data", data); - - return data; - } - /** * Run prompt for a given command * @param cmdName @@ -143,15 +118,12 @@ export class CliService { */ public async prompt(cmdName: string, data: CommandData = {}, $ctx: DIContext) { const provider = this.commands.get(cmdName); - const instance = inject(provider.useClass)!; + const instance = inject(provider.token)!; $ctx.set("data", data); if (instance.$prompt) { - const questions = [ - ...((await instance.$prompt(data)) as any[]), - ...(await this.hooks.emit(CommandStoreKeys.PROMPT_HOOKS, cmdName, data)) - ]; + const questions = [...((await instance.$prompt(data)) as any[])]; if (questions.length) { data = { @@ -178,15 +150,12 @@ export class CliService { data = this.mapData(cmdName, data, $ctx); - if (instance.$beforeExec) { - await instance.$beforeExec(data); - } + const tasks = []; - return [ - ...(await instance.$exec(data)), - ...(await this.hooks.emit(CommandStoreKeys.EXEC_HOOKS, cmdName, data)), - ...(await $asyncAlter(`$alter${pascalCase(cmdName)}Tasks`, [], [data])) - ].map((opts) => { + tasks.push(...((await instance.$exec(data)) || [])); + tasks.push(...(await $asyncAlter(`$alter${pascalCase(cmdName)}Tasks`, [], [data]))); + + return tasks.map((opts) => { return { ...opts, task: async (arg, task) => { @@ -204,13 +173,12 @@ export class CliService { public async getPostInstallTasks(cmdName: string, data: any) { const provider = this.commands.get(cmdName); - const instance = inject(provider.useClass)!; + const instance = inject(provider.token)!; data = this.mapData(cmdName, data, getContext()!); return [ ...(instance.$postInstall ? await instance.$postInstall(data) : []), - ...(await this.hooks.emit(CommandStoreKeys.POST_INSTALL_HOOKS, cmdName, data)), ...(await $asyncAlter(`$alter${pascalCase(cmdName)}PostInstallTasks`, [] as Task[], [data])), ...(instance.$afterPostInstall ? await instance.$afterPostInstall(data) : []) ]; @@ -274,7 +242,7 @@ export class CliService { return cmd; } - private load() { + load() { injector() .getProviders("command") .forEach((provider) => this.build(provider)); @@ -282,7 +250,7 @@ export class CliService { private mapData(cmdName: string, data: CommandData, $ctx: DIContext) { const provider = this.commands.get(cmdName); - const instance = inject(provider.useClass)!; + const instance = inject(provider.token)!; const verbose = data.verbose; data.commandName ||= cmdName; @@ -309,8 +277,8 @@ export class CliService { * Build command and sub-commands * @param provider */ - private build(provider: Provider) { - const metadata = getCommandMetadata(provider.useClass); + private build(provider: Provider) { + const metadata = getCommandMetadata(provider.token); if (metadata.name) { if (this.commands.has(metadata.name)) { @@ -331,7 +299,7 @@ export class CliService { * @param options * @param allowUnknownOptions */ - private buildOption(subCommand: Command, options: {[key: string]: CommandOptions}, allowUnknownOptions: boolean) { + private buildOption(subCommand: Command, options: {[key: string]: CommandOpts}, allowUnknownOptions: boolean) { Object.entries(options).reduce((subCommand, [flags, {description, required, customParser, defaultValue, ...options}]) => { const fn = (v: any) => { return parseOption(v, options); diff --git a/packages/cli-core/src/utils/getCommandMetadata.spec.ts b/packages/cli-core/src/utils/getCommandMetadata.spec.ts index c63b7f3bb..391c04d51 100644 --- a/packages/cli-core/src/utils/getCommandMetadata.spec.ts +++ b/packages/cli-core/src/utils/getCommandMetadata.spec.ts @@ -27,7 +27,8 @@ describe("getCommandMetadata", () => { disableReadUpPkg: false, enableFeatures: [], bindLogger: true, - options: {} + options: {}, + token: TestCmd }); expect(getCommandMetadata(TestCmd2)).toEqual({ @@ -39,7 +40,8 @@ describe("getCommandMetadata", () => { disableReadUpPkg: false, enableFeatures: [], bindLogger: false, - options: {} + options: {}, + token: TestCmd2 }); }); }); diff --git a/packages/cli-core/src/utils/getCommandMetadata.ts b/packages/cli-core/src/utils/getCommandMetadata.ts index 972a4b3e6..7041e030b 100644 --- a/packages/cli-core/src/utils/getCommandMetadata.ts +++ b/packages/cli-core/src/utils/getCommandMetadata.ts @@ -1,9 +1,8 @@ import {Store} from "@tsed/core"; import type {TokenProvider} from "@tsed/di"; -import {CommandStoreKeys} from "../domains/CommandStoreKeys.js"; import type {CommandMetadata} from "../interfaces/CommandMetadata.js"; -import type {CommandParameters} from "../interfaces/CommandParameters.js"; +import type {CommandOptions} from "../interfaces/CommandOptions.js"; export function getCommandMetadata(token: TokenProvider): CommandMetadata { const { @@ -17,7 +16,7 @@ export function getCommandMetadata(token: TokenProvider): CommandMetadata { disableReadUpPkg, bindLogger = true, ...opts - } = Store.from(token)?.get(CommandStoreKeys.COMMAND) as CommandParameters; + } = Store.from(token)?.get("command") as CommandOptions; return { name, diff --git a/packages/cli-core/src/utils/mapCommanderArgs.ts b/packages/cli-core/src/utils/mapCommanderArgs.ts index 43a20c887..4b8e800ac 100644 --- a/packages/cli-core/src/utils/mapCommanderArgs.ts +++ b/packages/cli-core/src/utils/mapCommanderArgs.ts @@ -1,6 +1,6 @@ import {isArray, isClass, Type} from "@tsed/core"; -import type {CommandArg} from "../interfaces/CommandParameters.js"; +import type {CommandArg} from "../interfaces/CommandOptions.js"; function mapValue(value: any, {type, itemType}: {type?: Type; itemType?: Type}) { if (!value) { From 9a26397f365990e4b44b61a8d2449751caec6cef Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 10 Dec 2025 08:01:05 +0100 Subject: [PATCH 04/18] refactor(cli): update command definitions to use object shorthand syntax for tokens --- .../src/commands/GenerateHttpClientCmd.ts | 3 ++- .../cli-generate-swagger/src/commands/GenerateSwaggerCmd.ts | 3 ++- packages/cli-plugin-prisma/src/commands/PrismaCmd.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/cli-generate-http-client/src/commands/GenerateHttpClientCmd.ts b/packages/cli-generate-http-client/src/commands/GenerateHttpClientCmd.ts index 6776f11df..8e488e743 100644 --- a/packages/cli-generate-http-client/src/commands/GenerateHttpClientCmd.ts +++ b/packages/cli-generate-http-client/src/commands/GenerateHttpClientCmd.ts @@ -167,7 +167,8 @@ export class GenerateHttpClientCmd implements CommandProvider { } } -command(GenerateHttpClientCmd, { +command({ + token: GenerateHttpClientCmd, name: "generate-http-client", description: "Generate the client API from swagger spec", options: { diff --git a/packages/cli-generate-swagger/src/commands/GenerateSwaggerCmd.ts b/packages/cli-generate-swagger/src/commands/GenerateSwaggerCmd.ts index 25b6b617a..a555a6726 100644 --- a/packages/cli-generate-swagger/src/commands/GenerateSwaggerCmd.ts +++ b/packages/cli-generate-swagger/src/commands/GenerateSwaggerCmd.ts @@ -93,7 +93,8 @@ export class GenerateSwaggerCmd implements CommandProvider { } } -command(GenerateSwaggerCmd, { +command({ + token: GenerateSwaggerCmd, name: "generate-swagger", description: "Generate the client API from swagger spec", options: { diff --git a/packages/cli-plugin-prisma/src/commands/PrismaCmd.ts b/packages/cli-plugin-prisma/src/commands/PrismaCmd.ts index 2b3203058..3ce145e2c 100644 --- a/packages/cli-plugin-prisma/src/commands/PrismaCmd.ts +++ b/packages/cli-plugin-prisma/src/commands/PrismaCmd.ts @@ -19,7 +19,8 @@ export class PrismaCmd implements CommandProvider { } } -command(PrismaCmd, { +command({ + token: PrismaCmd, name: "prisma", description: "Run a prisma command", args: { From ef55543cf3491924b325a30a59fb165113a2beda Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 10 Dec 2025 08:02:36 +0100 Subject: [PATCH 05/18] refactor(cli-plugins): replace `@OnAdd` decorator usage with `$onAddPlugin` method - Updated plugin modules to use `$onAddPlugin` for plugin initialization logic. - Refactored DI usage and simplified dependency imports in plugin modules. - Adjusted test files to remove redundant commands and streamline task lists. --- packages/cli-core/src/services/CliPlugins.ts | 3 +- .../src/hooks/EslintInitHook.ts | 18 ++++--- .../test/init.integration.spec.ts | 5 -- .../src/CliPluginJestModule.ts | 17 +++--- .../src/CliPluginMongooseModule.ts | 27 +++++----- .../src/CliPluginPrismaModule.ts | 13 ++--- .../tests/init.integration.spec.ts | 7 +-- .../src/TypeGraphqlModule.ts | 52 ++++++++++--------- 8 files changed, 71 insertions(+), 71 deletions(-) diff --git a/packages/cli-core/src/services/CliPlugins.ts b/packages/cli-core/src/services/CliPlugins.ts index 6f191f220..b68dc6070 100644 --- a/packages/cli-core/src/services/CliPlugins.ts +++ b/packages/cli-core/src/services/CliPlugins.ts @@ -39,8 +39,7 @@ export class CliPlugins { return { title: `Run plugin '${chalk.cyan(plugin)}'`, task: () => { - return $asyncEmit("$onAddPlugin", plugin, ctx); - // return this.cliHooks.emit(CommandStoreKeys.ADD, plugin, ctx); + return $asyncEmit("$onAddPlugin", [plugin, ctx]); } }; }); diff --git a/packages/cli-plugin-eslint/src/hooks/EslintInitHook.ts b/packages/cli-plugin-eslint/src/hooks/EslintInitHook.ts index 84ee410b8..15ce39318 100644 --- a/packages/cli-plugin-eslint/src/hooks/EslintInitHook.ts +++ b/packages/cli-plugin-eslint/src/hooks/EslintInitHook.ts @@ -89,21 +89,27 @@ export class EslintInitHook implements CliCommandHooks { return packageJson; } - $alterInitPostInstallTasks(tasks: Task[], data: InitCmdContext): Task[] | Promise { - const packageJson = inject(ProjectPackageJson); - const packageManagers = inject(PackageManagersModule); - + $alterInitSubTasks(tasks: Task[], data: InitCmdContext) { return [ ...tasks, { title: "Add eslint configuration", task: () => { - render("eslint.config", { + return render("eslint.config", { ...data, name: "eslint.config" }); } - }, + } + ]; + } + + $alterInitPostInstallTasks(tasks: Task[], data: InitCmdContext): Task[] | Promise { + const packageJson = inject(ProjectPackageJson); + const packageManagers = inject(PackageManagersModule); + + return [ + ...tasks, { title: "Add husky prepare task", skip: !data.lintstaged, diff --git a/packages/cli-plugin-eslint/test/init.integration.spec.ts b/packages/cli-plugin-eslint/test/init.integration.spec.ts index a76e64c4a..f2e344fa3 100644 --- a/packages/cli-plugin-eslint/test/init.integration.spec.ts +++ b/packages/cli-plugin-eslint/test/init.integration.spec.ts @@ -72,9 +72,6 @@ describe("Eslint: Init cmd", () => { "yarn install", "yarn add --ignore-engines @tsed/logger @tsed/logger-std @tsed/engines @tsed/barrels ajv cross-env @swc/core @swc/cli @swc/helpers @swc-node/register typescript body-parser cors compression cookie-parser express method-override", "yarn add -D --ignore-engines @types/node @types/multer tslib nodemon @types/cors @types/express @types/compression @types/cookie-parser @types/method-override", - "yarn run prepare", - "yarn run test:lint:fix", - "yarn run barrels", ] `); @@ -187,8 +184,6 @@ describe("Eslint: Init cmd", () => { "yarn install", "yarn add --ignore-engines @tsed/logger @tsed/logger-std @tsed/engines @tsed/barrels ajv cross-env @swc/core @swc/cli @swc/helpers @swc-node/register typescript body-parser cors compression cookie-parser express method-override", "yarn add -D --ignore-engines @types/node @types/multer tslib nodemon @types/cors @types/express @types/compression @types/cookie-parser @types/method-override", - "yarn run test:lint:fix", - "yarn run barrels", ] `); diff --git a/packages/cli-plugin-jest/src/CliPluginJestModule.ts b/packages/cli-plugin-jest/src/CliPluginJestModule.ts index 5981b7e8e..d265b6bb1 100644 --- a/packages/cli-plugin-jest/src/CliPluginJestModule.ts +++ b/packages/cli-plugin-jest/src/CliPluginJestModule.ts @@ -2,22 +2,21 @@ import "./templates/index.js"; import {RuntimesModule} from "@tsed/cli"; -import {inject, Module, OnAdd, ProjectPackageJson} from "@tsed/cli-core"; +import {inject, ProjectPackageJson} from "@tsed/cli-core"; +import {injectable} from "@tsed/di"; import {JestGenerateHook} from "./hooks/JestGenerateHook.js"; import {JestInitHook} from "./hooks/JestInitHook.js"; -@Module({ - imports: [JestInitHook, JestGenerateHook] -}) export class CliPluginJestModule { protected runtimes = inject(RuntimesModule); protected packageJson = inject(ProjectPackageJson); - @OnAdd("@tsed/cli-plugin-jest") - install() { - this.addScripts(); - this.addDevDependencies(); + $onAddPlugin(plugin: string) { + if (plugin === "@tsed/cli-plugin-jest") { + this.addScripts(); + this.addDevDependencies(); + } } addScripts() { @@ -37,3 +36,5 @@ export class CliPluginJestModule { }); } } + +injectable(CliPluginJestModule).imports([JestInitHook, JestGenerateHook]); diff --git a/packages/cli-plugin-mongoose/src/CliPluginMongooseModule.ts b/packages/cli-plugin-mongoose/src/CliPluginMongooseModule.ts index 439c24141..5f701b5c8 100644 --- a/packages/cli-plugin-mongoose/src/CliPluginMongooseModule.ts +++ b/packages/cli-plugin-mongoose/src/CliPluginMongooseModule.ts @@ -1,26 +1,27 @@ import "./templates/index.js"; -import {inject, Module, OnAdd, ProjectPackageJson} from "@tsed/cli-core"; +import {inject, ProjectPackageJson} from "@tsed/cli-core"; +import {injectable} from "@tsed/di"; import {MongooseGenerateHook} from "./hooks/MongooseGenerateHook.js"; import {MongooseInitHook} from "./hooks/MongooseInitHook.js"; import {CliMongoose} from "./services/CliMongoose.js"; -@Module({ - imports: [MongooseInitHook, MongooseGenerateHook, CliMongoose] -}) export class CliPluginMongooseModule { protected packageJson = inject(ProjectPackageJson); - @OnAdd("@tsed/cli-plugin-mongoose") - install() { - this.packageJson.addDependencies({ - "@tsed/mongoose": this.packageJson.dependencies["@tsed/platform-http"], - mongoose: "latest" - }); + $onAddPlugin(plugin: string) { + if (plugin == "@tsed/cli-plugin-mongoose") { + this.packageJson.addDependencies({ + "@tsed/mongoose": this.packageJson.dependencies["@tsed/platform-http"], + mongoose: "latest" + }); - this.packageJson.addDevDependencies({ - "@tsed/testing-mongoose": this.packageJson.dependencies["@tsed/platform-http"] - }); + this.packageJson.addDevDependencies({ + "@tsed/testing-mongoose": this.packageJson.dependencies["@tsed/platform-http"] + }); + } } } + +injectable(CliPluginMongooseModule).imports([MongooseInitHook, MongooseGenerateHook, CliMongoose]); diff --git a/packages/cli-plugin-prisma/src/CliPluginPrismaModule.ts b/packages/cli-plugin-prisma/src/CliPluginPrismaModule.ts index 9c339475f..fb92258f1 100644 --- a/packages/cli-plugin-prisma/src/CliPluginPrismaModule.ts +++ b/packages/cli-plugin-prisma/src/CliPluginPrismaModule.ts @@ -1,5 +1,5 @@ import type {InitCmdContext} from "@tsed/cli"; -import {inject, OnAdd, ProjectPackageJson} from "@tsed/cli-core"; +import {inject, ProjectPackageJson} from "@tsed/cli-core"; import {injectable} from "@tsed/di"; import {PrismaCmd} from "./commands/PrismaCmd.js"; @@ -8,11 +8,12 @@ import {PrismaInitHook} from "./hooks/PrismaInitHook.js"; export class CliPluginPrismaModule { protected packageJson = inject(ProjectPackageJson); - @OnAdd("@tsed/cli-plugin-prisma") - onAdd(ctx: InitCmdContext) { - this.addScripts(); - this.addDependencies(ctx); - this.addDevDependencies(ctx); + $onAddPlugin(plugin: string, ctx: InitCmdContext) { + if (plugin == "@tsed/cli-plugin-prisma") { + this.addScripts(); + this.addDependencies(ctx); + this.addDevDependencies(ctx); + } } addScripts() { diff --git a/packages/cli-plugin-prisma/tests/init.integration.spec.ts b/packages/cli-plugin-prisma/tests/init.integration.spec.ts index 89ce1cd38..90716410f 100644 --- a/packages/cli-plugin-prisma/tests/init.integration.spec.ts +++ b/packages/cli-plugin-prisma/tests/init.integration.spec.ts @@ -91,10 +91,7 @@ describe("Prisma: Init cmd", () => { "yarn install", "yarn add --ignore-engines @tsed/logger @tsed/logger-std @tsed/engines @tsed/barrels ajv cross-env @swc/core @swc/cli @swc/helpers @swc-node/register typescript body-parser cors compression cookie-parser express method-override", "yarn add -D --ignore-engines @types/node @types/multer tslib nodemon @types/cors @types/express @types/compression @types/cookie-parser @types/method-override", - "yarn add --ignore-engines @tsed/logger @tsed/logger-std @tsed/engines @tsed/barrels ajv cross-env @swc/core @swc/cli @swc/helpers @swc-node/register typescript body-parser cors compression cookie-parser express method-override @tsed/prisma @prisma/client", - "yarn add -D --ignore-engines @types/node @types/multer tslib nodemon @types/cors @types/express @types/compression @types/cookie-parser @types/method-override prisma", "npx prisma init", - "yarn run barrels", ] `); @@ -125,9 +122,7 @@ describe("Prisma: Init cmd", () => { "build": "yarn run barrels && swc src --out-dir dist -s --strip-leading-paths", "barrels": "barrels", "start": "yarn run barrels && nodemon src/index.ts", - "start:prod": "cross-env NODE_ENV=production node --import @swc-node/register/esm-register src/index.js", - "prisma:migrate": "npx prisma migrate dev --name init", - "prisma:generate": "npx prisma generate" + "start:prod": "cross-env NODE_ENV=production node --import @swc-node/register/esm-register src/index.js" }, "dependencies": { "@tsed/ajv": "5.58.1", diff --git a/packages/cli-plugin-typegraphql/src/TypeGraphqlModule.ts b/packages/cli-plugin-typegraphql/src/TypeGraphqlModule.ts index 331627f9e..da57f04ba 100644 --- a/packages/cli-plugin-typegraphql/src/TypeGraphqlModule.ts +++ b/packages/cli-plugin-typegraphql/src/TypeGraphqlModule.ts @@ -1,33 +1,35 @@ -import {inject, Module, OnAdd, ProjectPackageJson} from "@tsed/cli-core"; +import type {InitCmdContext} from "@tsed/cli"; +import {inject, ProjectPackageJson} from "@tsed/cli-core"; +import {injectable} from "@tsed/di"; import {TypeGraphqlInitHook} from "./hooks/TypeGraphqlInitHook.js"; -@Module({ - imports: [TypeGraphqlInitHook] -}) export class TypeGraphqlModule { protected packageJson = inject(ProjectPackageJson); - @OnAdd("@tsed/cli-plugin-typegraphql") - install(ctx: any) { - this.packageJson.addDependencies( - { - "@tsed/typegraphql": "{{tsedVersion}}", - "apollo-datasource": "^3.3.1", - "apollo-datasource-rest": "^3.5.1", - "apollo-server-core": "^3.6.2", - "type-graphql": "^1.1.1", - "class-validator": "^0.13.2", - graphql: "^15.7.2" - }, - ctx - ); - this.packageJson.addDevDependencies( - { - "@types/validator": "latest", - "apollo-server-testing": "latest" - }, - ctx - ); + $onAddPlugin(plugin: string, ctx: InitCmdContext) { + if (plugin === "@tsed/cli-plugin-typegraphql") { + this.packageJson.addDependencies( + { + "@tsed/typegraphql": "{{tsedVersion}}", + "apollo-datasource": "^3.3.1", + "apollo-datasource-rest": "^3.5.1", + "apollo-server-core": "^3.6.2", + "type-graphql": "^1.1.1", + "class-validator": "^0.13.2", + graphql: "^15.7.2" + }, + ctx + ); + this.packageJson.addDevDependencies( + { + "@types/validator": "latest", + "apollo-server-testing": "latest" + }, + ctx + ); + } } } + +injectable(TypeGraphqlModule).imports([TypeGraphqlInitHook]); From d426538f157c6fb72fb7a8a9157a75747316a825 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 10 Dec 2025 08:03:13 +0100 Subject: [PATCH 06/18] refactor(cli): restructure MCP commands and tools directories, remove `tsed-mcp` binary, and enhance schema definitions - Consolidated MCP-related tools and resources under `commands/mcp` for improved organization. - Removed `tsed-mcp` binary and updated `package.json` and integration scripts accordingly. - Enhanced support for dynamic input schemas with arrow function handlers in `defineTool`. - Improved error handling and logging during MCP tool execution. - Added `initProjectTool` and `McpCommand` for streamlined project initialization in MCP mode. --- packages/cli-mcp/src/fn/defineTool.ts | 20 ++++- packages/cli-mcp/src/index.ts | 1 + packages/cli/package.json | 3 +- packages/cli/src/bin/tsed-mcp.ts | 49 ------------ packages/cli/src/bin/tsed.ts | 4 +- packages/cli/src/commands/index.ts | 3 +- packages/cli/src/commands/init/InitCmd.ts | 16 ++-- .../{InitSchema.ts => InitFileSchema.ts} | 4 +- packages/cli/src/commands/mcp/McpCommand.ts | 12 +++ .../src/{ => commands}/mcp/resources/index.ts | 0 .../mcp/resources/projectInfoResource.ts | 0 .../mcp/resources/serverInfoResource.ts | 0 .../src/commands/mcp/schema/InitMCPSchema.ts | 37 +++++++++ .../mcp/schema/ProjectPreferencesSchema.ts | 4 +- .../{ => commands}/mcp/tools/generateTool.ts | 6 +- .../mcp/tools/getTemplateTool.ts | 4 +- .../cli/src/{ => commands}/mcp/tools/index.ts | 3 +- .../src/commands/mcp/tools/initProjectTool.ts | 76 +++++++++++++++++++ .../mcp/tools/listTemplatesTool.ts | 4 +- .../mcp/tools/setWorkspaceTool.ts | 0 packages/cli/src/index.ts | 4 +- tools/integration/mcp.ts | 2 +- tools/integration/package.json | 2 +- 23 files changed, 172 insertions(+), 82 deletions(-) delete mode 100644 packages/cli/src/bin/tsed-mcp.ts rename packages/cli/src/commands/init/config/{InitSchema.ts => InitFileSchema.ts} (90%) create mode 100644 packages/cli/src/commands/mcp/McpCommand.ts rename packages/cli/src/{ => commands}/mcp/resources/index.ts (100%) rename packages/cli/src/{ => commands}/mcp/resources/projectInfoResource.ts (100%) rename packages/cli/src/{ => commands}/mcp/resources/serverInfoResource.ts (100%) create mode 100644 packages/cli/src/commands/mcp/schema/InitMCPSchema.ts rename packages/cli/src/{ => commands}/mcp/schema/ProjectPreferencesSchema.ts (84%) rename packages/cli/src/{ => commands}/mcp/tools/generateTool.ts (94%) rename packages/cli/src/{ => commands}/mcp/tools/getTemplateTool.ts (91%) rename packages/cli/src/{ => commands}/mcp/tools/index.ts (75%) create mode 100644 packages/cli/src/commands/mcp/tools/initProjectTool.ts rename packages/cli/src/{ => commands}/mcp/tools/listTemplatesTool.ts (88%) rename packages/cli/src/{ => commands}/mcp/tools/setWorkspaceTool.ts (100%) diff --git a/packages/cli-mcp/src/fn/defineTool.ts b/packages/cli-mcp/src/fn/defineTool.ts index 963f3abf9..2d96ca8e9 100644 --- a/packages/cli-mcp/src/fn/defineTool.ts +++ b/packages/cli-mcp/src/fn/defineTool.ts @@ -2,6 +2,7 @@ import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js"; import type {RequestHandlerExtra} from "@modelcontextprotocol/sdk/shared/protocol.js"; import type {CallToolResult, ServerNotification, ServerRequest, Tool} from "@modelcontextprotocol/sdk/types.js"; import {injectable} from "@tsed/cli-core"; +import {isArrowFn} from "@tsed/core"; import {DIContext, injector, logger, runInContext, type TokenProvider} from "@tsed/di"; import {JsonSchema} from "@tsed/schema"; import {v4} from "uuid"; @@ -17,7 +18,7 @@ type ToolConfig = Parameters[1]; export type ToolProps = Omit & { token?: TokenProvider; name: string; - inputSchema?: JsonSchema | Tool["inputSchema"]; + inputSchema?: JsonSchema | (() => JsonSchema) | Tool["inputSchema"]; outputSchema?: JsonSchema | Tool["outputSchema"]; handler: ToolCallback; }; @@ -57,7 +58,7 @@ export function defineTool(options: ToolProps ({ ...options, - inputSchema: toZod(options.inputSchema), + inputSchema: toZod(isArrowFn(options.inputSchema) ? options.inputSchema() : options.inputSchema), outputSchema: toZod(options.outputSchema), async handler(args: Input, extra: RequestHandlerExtra) { const $ctx = new DIContext({ @@ -73,6 +74,21 @@ export function defineTool(options: ToolProps { return options.handler(args as Input, extra); }); + } catch (er) { + $ctx.logger.error({ + event: "MCP_TOOL_ERROR", + tool: options.name, + error_message: er.message, + stack: er.stack + }); + + return { + content: [], + structuredContent: { + code: "E_MCP_TOOL_ERROR", + message: er.message + } + }; } finally { // Ensure per-invocation context is destroyed to avoid leaks try { diff --git a/packages/cli-mcp/src/index.ts b/packages/cli-mcp/src/index.ts index 3767b5ed8..1b7014e26 100644 --- a/packages/cli-mcp/src/index.ts +++ b/packages/cli-mcp/src/index.ts @@ -2,3 +2,4 @@ export * from "./fn/definePrompt.js"; export * from "./fn/defineResource.js"; export * from "./fn/defineTool.js"; export * from "./services/CLIMCPServer.js"; +export * from "./services/McpServerFactory.js"; diff --git a/packages/cli/package.json b/packages/cli/package.json index 3cf3108fb..6318eccc5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,8 +22,7 @@ "test:ci": "vitest run --coverage.thresholds.autoUpdate=true" }, "bin": { - "tsed": "lib/esm/bin/tsed.js", - "tsed-mcp": "lib/esm/bin/tsed-mcp.js" + "tsed": "lib/esm/bin/tsed.js" }, "files": [ "lib/esm/bin/tsed.js", diff --git a/packages/cli/src/bin/tsed-mcp.ts b/packages/cli/src/bin/tsed-mcp.ts deleted file mode 100644 index fdcca3c3d..000000000 --- a/packages/cli/src/bin/tsed-mcp.ts +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env node -import "@swc-node/register/esm-register"; - -import {register} from "node:module"; -import {join} from "node:path"; -import {pathToFileURL} from "node:url"; - -import type {PackageJson} from "@tsed/cli-core"; -import {CLIMCPServer} from "@tsed/cli-mcp"; - -const EXT = process.env.CLI_MODE === "ts" ? "ts" : "js"; - -register(pathToFileURL(join(import.meta.dirname, `../loaders/alias.hook.${EXT}`)), { - parentURL: import.meta.dirname, - data: { - "@tsed/core": import.meta.resolve("@tsed/core"), - "@tsed/di": import.meta.resolve("@tsed/di"), - "@tsed/schema": import.meta.resolve("@tsed/schema"), - "@tsed/cli-core": import.meta.resolve("@tsed/cli-core"), - "@tsed/cli": import.meta.resolve("@tsed/cli") - }, - transferList: [] -}); - -const {tools, resources, PKG, TEMPLATE_DIR, ArchitectureConvention, ProjectConvention} = await import("../index.js"); - -CLIMCPServer.bootstrap({ - name: "tsed", - version: PKG.version, - pkg: PKG as PackageJson, - templateDir: TEMPLATE_DIR, - tools, - resources, - mcp: { - mode: process.env.args?.includes("--http") || process.env.USE_MCP_HTTP ? "streamable-http" : "stdio" - }, - defaultProjectPreferences() { - return { - convention: ProjectConvention.DEFAULT, - architecture: ArchitectureConvention.DEFAULT - }; - }, - project: { - reinstallAfterRun: true - } -}).catch((error) => { - console.error(error); - process.exit(-1); -}); diff --git a/packages/cli/src/bin/tsed.ts b/packages/cli/src/bin/tsed.ts index b1db99d2d..6d307245e 100644 --- a/packages/cli/src/bin/tsed.ts +++ b/packages/cli/src/bin/tsed.ts @@ -21,7 +21,7 @@ register(pathToFileURL(join(import.meta.dirname, `../loaders/alias.hook.${EXT}`) transferList: [] }); -const {commands, CliCore, PKG, TEMPLATE_DIR, ArchitectureConvention, ProjectConvention} = await import("../index.js"); +const {tools, commands, resources, CliCore, PKG, TEMPLATE_DIR, ArchitectureConvention, ProjectConvention} = await import("../index.js"); CliCore.bootstrap({ name: "tsed", @@ -31,6 +31,8 @@ CliCore.bootstrap({ updateNotifier: true, checkPrecondition: true, commands, + tools, + resources, defaultProjectPreferences() { return { convention: ProjectConvention.DEFAULT, diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 4b0aaa067..1ab882e00 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -1,8 +1,9 @@ import {AddCmd} from "./add/AddCmd.js"; import {GenerateCmd} from "./generate/GenerateCmd.js"; import {InitCmd} from "./init/InitCmd.js"; +import {McpCommand} from "./mcp/McpCommand.js"; import {RunCmd} from "./run/RunCmd.js"; import {CreateTemplateCommand} from "./template/CreateTemplateCommand.js"; import {UpdateCmd} from "./update/UpdateCmd.js"; -export default [AddCmd, InitCmd, GenerateCmd, UpdateCmd, RunCmd, CreateTemplateCommand]; +export default [AddCmd, InitCmd, GenerateCmd, UpdateCmd, RunCmd, CreateTemplateCommand, McpCommand]; diff --git a/packages/cli/src/commands/init/InitCmd.ts b/packages/cli/src/commands/init/InitCmd.ts index c7ae2513c..25214c55e 100644 --- a/packages/cli/src/commands/init/InitCmd.ts +++ b/packages/cli/src/commands/init/InitCmd.ts @@ -37,7 +37,7 @@ import {BunRuntime} from "../../runtimes/supports/BunRuntime.js"; import {NodeRuntime} from "../../runtimes/supports/NodeRuntime.js"; import {CliProjectService} from "../../services/CliProjectService.js"; import {FeaturesMap, FeatureType} from "./config/FeaturesPrompt.js"; -import {InitSchema} from "./config/InitSchema.js"; +import {InitFileSchema} from "./config/InitFileSchema.js"; import {mapToContext} from "./mappers/mapToContext.js"; import {getFeaturesPrompt} from "./prompts/getFeaturesPrompt.js"; @@ -92,20 +92,16 @@ export class InitCmd implements CommandProvider { } } - async $beforePrompt(initialOptions: Partial) { + async $prompt(initialOptions: Partial): Promise { if (initialOptions.file) { const file = join(this.packageJson.cwd, initialOptions.file); - return { + initialOptions = { ...initialOptions, - ...(await this.cliLoadFile.loadFile(file, InitSchema)) + ...(await this.cliLoadFile.loadFile(file, InitFileSchema)) }; } - return initialOptions; - } - - $prompt(initialOptions: Partial): QuestionOptions { if (initialOptions.skipPrompt) { return []; } @@ -153,7 +149,7 @@ export class InitCmd implements CommandProvider { } as InitOptions; } - async $beforeExec(ctx: InitOptions): Promise { + async $exec(ctx: InitOptions): Promise { this.fs.ensureDirSync(this.packageJson.cwd); ctx.projectName && (this.packageJson.name = ctx.projectName); @@ -199,9 +195,7 @@ export class InitCmd implements CommandProvider { ], ctx ); - } - async $exec(ctx: InitOptions): Promise { this.checkPrecondition(ctx); const runtime = this.runtimes.get(); diff --git a/packages/cli/src/commands/init/config/InitSchema.ts b/packages/cli/src/commands/init/config/InitFileSchema.ts similarity index 90% rename from packages/cli/src/commands/init/config/InitSchema.ts rename to packages/cli/src/commands/init/config/InitFileSchema.ts index b04d5a233..b2a977830 100644 --- a/packages/cli/src/commands/init/config/InitSchema.ts +++ b/packages/cli/src/commands/init/config/InitFileSchema.ts @@ -1,10 +1,10 @@ import {PackageManager} from "@tsed/cli-core"; import {s} from "@tsed/schema"; -import {ArchitectureConvention, PlatformType, ProjectConvention} from "../../../interfaces/index.js"; +import {PlatformType, ProjectConvention} from "../../../interfaces/index.js"; import {FeatureType} from "./FeaturesPrompt.js"; -export const InitSchema = s +export const InitFileSchema = s .object({ tsedVersion: s.string().optional().description("The CLI will use the given tsed version to generate the project"), projectName: s.string().optional().description("The project name"), diff --git a/packages/cli/src/commands/mcp/McpCommand.ts b/packages/cli/src/commands/mcp/McpCommand.ts new file mode 100644 index 000000000..c860d9e1f --- /dev/null +++ b/packages/cli/src/commands/mcp/McpCommand.ts @@ -0,0 +1,12 @@ +import {command} from "@tsed/cli-core"; +import {MCP_SERVER} from "@tsed/cli-mcp"; +import {inject} from "@tsed/di"; + +export const McpCommand = command({ + name: "mcp", + description: "Run a MCP server", + async handler() { + await inject(MCP_SERVER).connect(); + return []; + } +}).token(); diff --git a/packages/cli/src/mcp/resources/index.ts b/packages/cli/src/commands/mcp/resources/index.ts similarity index 100% rename from packages/cli/src/mcp/resources/index.ts rename to packages/cli/src/commands/mcp/resources/index.ts diff --git a/packages/cli/src/mcp/resources/projectInfoResource.ts b/packages/cli/src/commands/mcp/resources/projectInfoResource.ts similarity index 100% rename from packages/cli/src/mcp/resources/projectInfoResource.ts rename to packages/cli/src/commands/mcp/resources/projectInfoResource.ts diff --git a/packages/cli/src/mcp/resources/serverInfoResource.ts b/packages/cli/src/commands/mcp/resources/serverInfoResource.ts similarity index 100% rename from packages/cli/src/mcp/resources/serverInfoResource.ts rename to packages/cli/src/commands/mcp/resources/serverInfoResource.ts diff --git a/packages/cli/src/commands/mcp/schema/InitMCPSchema.ts b/packages/cli/src/commands/mcp/schema/InitMCPSchema.ts new file mode 100644 index 000000000..480da5738 --- /dev/null +++ b/packages/cli/src/commands/mcp/schema/InitMCPSchema.ts @@ -0,0 +1,37 @@ +import {PackageManagersModule} from "@tsed/cli-core"; +import {inject} from "@tsed/di"; +import {s} from "@tsed/schema"; + +import {ArchitectureConvention, PlatformType, ProjectConvention} from "../../../interfaces/index.js"; +import {RuntimesModule} from "../../../runtimes/RuntimesModule.js"; +import {FeatureType} from "../../init/config/FeaturesPrompt.js"; + +export const InitMCPSchema = () => + s.object({ + cwd: s.string().required().description("Current working directory to initialize Ts.ED project"), + projectName: s.string().description("Project name. Defaults to the current folder name."), + platform: s.string().enum(PlatformType).default(PlatformType.EXPRESS).description("Target platform (express, koa, fastify)."), + convention: s + .string() + .enum(ProjectConvention) + .default(ProjectConvention.DEFAULT) + .description("Project convention (default, nest, etc.)."), + runtime: s.string().enum(inject(RuntimesModule).list()).default("node").description("Runtime (node, bun, ...)."), + packageManager: s + .string() + .enum(inject(PackageManagersModule).list()) + .default("npm") + .description("Package manager (npm, pnpm, yarn, bun)."), + architecture: s + .string() + .enum(ArchitectureConvention) + .default(ArchitectureConvention.DEFAULT) + .description("Architecture convention (default, feature, ...)."), + features: s.array().items(s.string().enum(FeatureType)).description("List of features to enable (swagger, graphql, prisma, etc.)."), + GH_TOKEN: s + .string() + .optional() + .description( + "GitHub token to install premium plugins. For example config:aws_secrets:premium or all features endings by `:premium` needs a GH_TOKEN" + ) + }); diff --git a/packages/cli/src/mcp/schema/ProjectPreferencesSchema.ts b/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts similarity index 84% rename from packages/cli/src/mcp/schema/ProjectPreferencesSchema.ts rename to packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts index 98dced5f5..9559af0d0 100644 --- a/packages/cli/src/mcp/schema/ProjectPreferencesSchema.ts +++ b/packages/cli/src/commands/mcp/schema/ProjectPreferencesSchema.ts @@ -1,8 +1,8 @@ import {PackageManager} from "@tsed/cli-core"; import {s} from "@tsed/schema"; -import {PlatformType} from "../../interfaces/PlatformType.js"; -import {ProjectConvention} from "../../interfaces/ProjectConvention.js"; +import {PlatformType} from "../../../interfaces/PlatformType.js"; +import {ProjectConvention} from "../../../interfaces/ProjectConvention.js"; export const ProjectPreferenceSchema = s .object({ diff --git a/packages/cli/src/mcp/tools/generateTool.ts b/packages/cli/src/commands/mcp/tools/generateTool.ts similarity index 94% rename from packages/cli/src/mcp/tools/generateTool.ts rename to packages/cli/src/commands/mcp/tools/generateTool.ts index 7ff68bd7b..022ceb0f6 100644 --- a/packages/cli/src/mcp/tools/generateTool.ts +++ b/packages/cli/src/commands/mcp/tools/generateTool.ts @@ -3,9 +3,9 @@ import {defineTool} from "@tsed/cli-mcp"; import {inject} from "@tsed/di"; import {array, number, object, string} from "@tsed/schema"; -import {CliProjectService} from "../../services/CliProjectService.js"; -import {CliTemplatesService} from "../../services/CliTemplatesService.js"; -import {mapDefaultTemplateOptions} from "../../services/mappers/mapDefaultTemplateOptions.js"; +import {CliProjectService} from "../../../services/CliProjectService.js"; +import {CliTemplatesService} from "../../../services/CliTemplatesService.js"; +import {mapDefaultTemplateOptions} from "../../../services/mappers/mapDefaultTemplateOptions.js"; export const generateTool = defineTool({ name: "generate-file", diff --git a/packages/cli/src/mcp/tools/getTemplateTool.ts b/packages/cli/src/commands/mcp/tools/getTemplateTool.ts similarity index 91% rename from packages/cli/src/mcp/tools/getTemplateTool.ts rename to packages/cli/src/commands/mcp/tools/getTemplateTool.ts index bd6efd603..176d4f545 100644 --- a/packages/cli/src/mcp/tools/getTemplateTool.ts +++ b/packages/cli/src/commands/mcp/tools/getTemplateTool.ts @@ -2,8 +2,8 @@ import {defineTool} from "@tsed/cli-mcp"; import {inject} from "@tsed/di"; import {s} from "@tsed/schema"; -import {CliTemplatesService} from "../../services/CliTemplatesService.js"; -import {resolveSchema} from "../../utils/resolveSchema.js"; +import {CliTemplatesService} from "../../../services/CliTemplatesService.js"; +import {resolveSchema} from "../../../utils/resolveSchema.js"; export const getTemplateTool = defineTool({ name: "get-template", diff --git a/packages/cli/src/mcp/tools/index.ts b/packages/cli/src/commands/mcp/tools/index.ts similarity index 75% rename from packages/cli/src/mcp/tools/index.ts rename to packages/cli/src/commands/mcp/tools/index.ts index a905c5943..b50f1b0e0 100644 --- a/packages/cli/src/mcp/tools/index.ts +++ b/packages/cli/src/commands/mcp/tools/index.ts @@ -1,7 +1,8 @@ import {projectInfoResource} from "../resources/projectInfoResource.js"; import {generateTool} from "./generateTool.js"; import {getTemplateTool} from "./getTemplateTool.js"; +import {initProjectTool} from "./initProjectTool.js"; import {listTemplatesTool} from "./listTemplatesTool.js"; import {setWorkspaceTool} from "./setWorkspaceTool.js"; -export default [setWorkspaceTool, projectInfoResource, listTemplatesTool, getTemplateTool, generateTool]; +export default [setWorkspaceTool, projectInfoResource, listTemplatesTool, getTemplateTool, generateTool, initProjectTool]; diff --git a/packages/cli/src/commands/mcp/tools/initProjectTool.ts b/packages/cli/src/commands/mcp/tools/initProjectTool.ts new file mode 100644 index 000000000..f4fbeaf0c --- /dev/null +++ b/packages/cli/src/commands/mcp/tools/initProjectTool.ts @@ -0,0 +1,76 @@ +import {CliService, ProjectPackageJson} from "@tsed/cli-core"; +import {defineTool} from "@tsed/cli-mcp"; +import {context, inject} from "@tsed/di"; +import {s} from "@tsed/schema"; + +import {CliTemplatesService} from "../../../services/CliTemplatesService.js"; +import {InitMCPSchema} from "../schema/InitMCPSchema.js"; + +export const initProjectTool = defineTool({ + name: "init-project", + title: "Initialize Ts.ED project", + description: "Initialize a new Ts.ED project in the current workspace (sans Listr/logs).", + inputSchema: InitMCPSchema, + outputSchema: s.object({ + files: s.array().items(s.string()).description("List of generated files."), + count: s.number().description("Number of files generated."), + projectName: s.string().optional().description("Resolved project name."), + cwd: s.string().optional().description("Resolved workspace directory in which the project was initialized."), + logs: s.array().items(s.string()).optional().description("Execution logs"), + warnings: s.array().items(s.string()).optional().description("Non blocking warnings") + }), + async handler(input) { + const cliService = inject(CliService); + const projectPackage = inject(ProjectPackageJson); + const templates = inject(CliTemplatesService); + + projectPackage.setCWD(input.cwd); + + // If the current workspace already looks initialized, block to prevent accidental re-init + if (projectPackage.preferences?.packageManager) { + return { + content: [], + isError: true, + structuredContent: { + code: "E_PROJECT_ALREADY_INITIALIZED", + message: "This workspace already appears to be an initialized Ts.ED project.", + suggestion: "Use 'generate-file' to add new files, or call 'set-workspace' with a different folder to initialize a new project." + } + }; + } + + const warnings: string[] = []; + + // Build options and map context (skip prompts by default) + const initialOptions = { + skipPrompt: true, + ...(input as any) + } as any; + + cliService.load(); + + await cliService.exec("init", initialOptions, context()); + + // Collect rendered files from templates service (same pattern as generateTool) + const rendered = templates.getRenderedFiles(); + const files = rendered.map((f) => f.outputPath); + + const logs = [ + `init:root:${projectPackage.cwd ?? "."}`, + `init:platform:${projectPackage.preferences.platform}`, + `files:${files.length}` + ]; + + return { + content: [], + structuredContent: { + files, + count: files.length, + projectName: projectPackage.name, + cwd: projectPackage.cwd, + logs, + warnings: warnings.length ? warnings : undefined + } + }; + } +}); diff --git a/packages/cli/src/mcp/tools/listTemplatesTool.ts b/packages/cli/src/commands/mcp/tools/listTemplatesTool.ts similarity index 88% rename from packages/cli/src/mcp/tools/listTemplatesTool.ts rename to packages/cli/src/commands/mcp/tools/listTemplatesTool.ts index 71b3fd421..726233dea 100644 --- a/packages/cli/src/mcp/tools/listTemplatesTool.ts +++ b/packages/cli/src/commands/mcp/tools/listTemplatesTool.ts @@ -2,8 +2,8 @@ import {defineTool} from "@tsed/cli-mcp"; import {inject} from "@tsed/di"; import {array, object, string} from "@tsed/schema"; -import {CliTemplatesService} from "../../services/CliTemplatesService.js"; -import {summarizeSchema} from "../../utils/summarizeSchema.js"; +import {CliTemplatesService} from "../../../services/CliTemplatesService.js"; +import {summarizeSchema} from "../../../utils/summarizeSchema.js"; export const listTemplatesTool = defineTool({ name: "list-templates", diff --git a/packages/cli/src/mcp/tools/setWorkspaceTool.ts b/packages/cli/src/commands/mcp/tools/setWorkspaceTool.ts similarity index 100% rename from packages/cli/src/mcp/tools/setWorkspaceTool.ts rename to packages/cli/src/commands/mcp/tools/setWorkspaceTool.ts diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ba78cfb3a..0cb659feb 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -9,13 +9,13 @@ export * from "./commands/init/config/FeaturesPrompt.js"; export * from "./commands/init/config/FeaturesPrompt.js"; export * from "./commands/init/InitCmd.js"; export * from "./commands/init/prompts/getFeaturesPrompt.js"; +export {default as resources} from "./commands/mcp/resources/index.js"; +export {default as tools} from "./commands/mcp/tools/index.js"; export * from "./commands/update/UpdateCmd.js"; export * from "./constants/index.js"; export * from "./fn/exec.js"; export * from "./fn/render.js"; export * from "./interfaces/index.js"; -export {default as resources} from "./mcp/resources/index.js"; -export {default as tools} from "./mcp/tools/index.js"; export * from "./pipes/index.js"; export * from "./runtimes/index.js"; export * from "./services/CliProjectService.js"; diff --git a/tools/integration/mcp.ts b/tools/integration/mcp.ts index e9b554e4c..edebad925 100644 --- a/tools/integration/mcp.ts +++ b/tools/integration/mcp.ts @@ -1 +1 @@ -import "@tsed/cli/bin/mcp"; +import "@tsed/cli/bin"; diff --git a/tools/integration/package.json b/tools/integration/package.json index df911b9dc..340cdffe1 100644 --- a/tools/integration/package.json +++ b/tools/integration/package.json @@ -12,7 +12,7 @@ "scripts": { "run-cmd": "cross-env NODE_ENV=development CLI_MODE=ts CI=true node --import @swc-node/register/esm-register index.ts", "run-mcp-stdio": "npx @modelcontextprotocol/inspector node -e NODE_ENV=development -e CLI_MODE=ts -e CI=true -e LOG_SERVER_URL=http://localhost:3838 --import @swc-node/register/esm-register mcp.ts", - "run-mcp-http": "cross-env NODE_ENV=development CLI_MODE=ts CI=true USE_MCP_HTTP=true node --import @swc-node/register/esm-register mcp.ts", + "run-mcp-http": "cross-env NODE_ENV=development CLI_MODE=ts CI=true USE_MCP_HTTP=true node --import @swc-node/register/esm-register index.ts mcp", "mcp:inspector": "npx @modelcontextprotocol/inspector", "start:help": "yarn run-cmd -h", "start:help:g": "yarn run-cmd generate -h", From e44bd12caad77109ce7a69a2d62789159fe518db Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 10 Dec 2025 20:13:06 +0100 Subject: [PATCH 07/18] docs(contributing): add repository guidelines and update contribution instructions --- AGENTS.md | 25 +++++++++++++++++++++++++ CONTRIBUTING.md | 17 +++++++++++------ 2 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..707ef801c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +Yarn 4 + Lerna workspaces organize sources under `packages/*`: `cli` hosts command entrypoints and generator templates, `cli-core` handles DI/prompt infrastructure, `cli-mcp` exposes MCP servers and tools, while `cli-plugin-*` packages bundle framework-specific blueprints plus their own tests. `cli-testing` provides shared harnesses, templates live in `packages/cli/templates`, artifacts land in `dist/`, VuePress + TSDoc files stay in `docs/`, and `tools/*` holds automation scripts (TypeScript references, ESLint, Vitest installers). + +## Build, Test, and Development Commands + +- `yarn build`: runs `monorepo build --verbose` across every workspace. +- `yarn build:references`: refreshes TS project references after renaming or adding packages. +- `yarn test`: executes the Vitest suite once; add `--coverage` for V8 reports. +- `yarn lint` / `yarn lint:fix`: applies the flat ESLint config + Prettier formatting used in CI. +- `yarn docs:serve` / `yarn docs:build`: runs `lerna run build && tsdoc` then serves or builds the VuePress site. + +## Coding Style & Naming Conventions + +Author TypeScript ES modules with 2-space indentation, double quotes, and trailing commas per the shared Prettier rules. Favor named exports and keep command providers in `packages/cli/src/commands/**/NameCmd.ts`, suffixing classes with `Cmd`. Tests and utilities mirror their source paths, ending in `.spec.ts` or `.integration.spec.ts`. Plugin packages follow `cli-plugin-` naming; templates underneath use kebab-case folder names. Keep import blocks sorted (enforced by `eslint-plugin-simple-import-sort`) and reserve default exports for entry aggregators such as `packages/cli/src/index.ts`. + +## Testing Guidelines + +Vitest powers both unit and integration coverage. Co-locate fast tests next to implementation files, while scenario-heavy suites live under `packages/*/test/**`. Use `yarn vitest packages/cli/src/commands/init/InitCmd.spec.ts` to focus a file, prefer `.integration.spec.ts` when exercising generators end-to-end, and ship PRs with `yarn test --coverage` so CI can enforce thresholds. Mock filesystem access via helpers in `cli-testing` instead of touching the real disk. + +## Commit & Pull Request Guidelines + +Commit messages must satisfy Conventional Commits (see `commitlint.config.js`); scope by workspace, e.g., `feat(cli-plugin-prisma): expand seed template`. Keep subjects under 200 characters and describe behavior, not implementation minutiae. Before opening a pull request, run `yarn build`, `yarn test`, and `yarn lint`, update docs/templates when behavior changes, and link the relevant issue. Summaries should highlight user-facing changes, test evidence, and screenshots or logs when generator output shifts. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b0d7e3bd..e1abcaf1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,13 @@ -# Contributing +# Contributing + ## Introduction First, thank you for considering contributing to Ts.ED! It is people like you that make the open source community such a great community! 😊 We welcome any type of contribution, not just code. You can help with: +- Repository-specific build, testing, and PR expectations are summarized in the [Repository Guidelines](AGENTS.md); please skim them before opening a pull request. + - QA: file bug reports, the more details you can give the better (e.g. screenshots with the console open) - Marketing: writing blog posts, how to's, printing stickers, ... - Community: presenting the project at meetups, organizing a dedicated meetup for the local community, ... @@ -23,6 +26,7 @@ Code review process The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? --- + ### WARNING Ts.ED project uses [conventional commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) as format commit message. @@ -30,8 +34,7 @@ Ts.ED project uses [conventional commits](https://www.conventionalcommits.org/en Release note and tagging version are based on the message commits. If you don't follow the format, our CI won't be able to increment the version correctly and your feature won't be released on NPM. -To write your commit message, see [convention page here](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) ---- +## To write your commit message, see [convention page here](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) ## Financial contributions @@ -42,6 +45,7 @@ We also welcome financial contributions in full transparency on our open collect If you have any questions, create an [issue](https://github.com/tsedio/tsed/issues) (protip: do a quick search first to see if someone else didn't ask the same question before!). You can also reach us at https://gitter.im/Tsed-io/community. ## How to work on Ts.ED + ### Setup Clone your fork of the repository @@ -51,6 +55,7 @@ $ git clone https://github.com/YOUR_USERNAME/tsed-cli.git ``` Install npm dependencies with yarn (not with NPM!): + ```bash yarn ``` @@ -59,7 +64,7 @@ Compile TypeScript: ```bash yarn build -// or +// or npm run build ``` @@ -101,6 +106,7 @@ git commit -m "feat(domain): Your message" ``` Then: + ```bash npm run test git fetch @@ -142,18 +148,17 @@ yarn vuepress:serve - Feel free to ask for help from other members of the Ts.ED team. ## Credits + ### Contributors - ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/tsed#backer)] - ### Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/tsed#sponsor)] From 663a64eb56ca42bca4b26df60f3eeb6391cd83d1 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 10 Dec 2025 21:19:49 +0100 Subject: [PATCH 08/18] test(cli-core): add unit tests for CliFs, CliDockerComposeYaml, CliService, and other core services --- packages/cli-core/src/fn/command.spec.ts | 75 ++++++++ packages/cli-core/src/fn/command.ts | 6 +- .../src/interfaces/CommandMetadata.ts | 2 +- .../cli-core/src/interfaces/CommandOptions.ts | 22 ++- .../src/services/CliDockerComposeYaml.spec.ts | 162 +++++++++++++++++ .../cli-core/src/services/CliExeca.spec.ts | 106 +++++++++++ packages/cli-core/src/services/CliFs.spec.ts | 85 +++++++++ .../cli-core/src/services/CliHooks.spec.ts | 53 ++++++ .../src/services/CliHttpLogClient.spec.ts | 148 +++++++++++++++ .../src/services/CliPackageJson.spec.ts | 24 +++ .../cli-core/src/services/CliService.spec.ts | 169 ++++++++++++++++++ .../cli-core/src/services/CliYaml.spec.ts | 43 +++++ .../src/utils/getCommandMetadata.spec.ts | 89 +++++++++ .../cli-core/src/utils/getCommandMetadata.ts | 38 +++- packages/cli/src/commands/mcp/McpCommand.ts | 11 +- 15 files changed, 1015 insertions(+), 18 deletions(-) create mode 100644 packages/cli-core/src/fn/command.spec.ts create mode 100644 packages/cli-core/src/services/CliDockerComposeYaml.spec.ts create mode 100644 packages/cli-core/src/services/CliExeca.spec.ts create mode 100644 packages/cli-core/src/services/CliFs.spec.ts create mode 100644 packages/cli-core/src/services/CliHooks.spec.ts create mode 100644 packages/cli-core/src/services/CliHttpLogClient.spec.ts create mode 100644 packages/cli-core/src/services/CliPackageJson.spec.ts create mode 100644 packages/cli-core/src/services/CliService.spec.ts create mode 100644 packages/cli-core/src/services/CliYaml.spec.ts diff --git a/packages/cli-core/src/fn/command.spec.ts b/packages/cli-core/src/fn/command.spec.ts new file mode 100644 index 000000000..8e262690a --- /dev/null +++ b/packages/cli-core/src/fn/command.spec.ts @@ -0,0 +1,75 @@ +import {injectable} from "@tsed/di"; + +import type {CommandOptions} from "../interfaces/CommandOptions.js"; +import type {CommandProvider} from "../interfaces/CommandProvider.js"; +import {command} from "./command.js"; + +vi.mock("@tsed/di", () => ({ + injectable: vi.fn() +})); + +describe("command()", () => { + const createBuilder = () => + ({ + type: vi.fn().mockReturnThis(), + set: vi.fn().mockReturnThis(), + factory: vi.fn().mockReturnThis() + }) as any; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should register a functional command via factory", () => { + const builder = createBuilder(); + vi.mocked(injectable).mockReturnValue(builder); + + const handler: CommandProvider["$exec"] = vi.fn(); + const prompt: CommandProvider["$prompt"] = vi.fn(); + const options: CommandOptions = { + name: "test", + description: "description", + handler, + prompt + }; + + command(options); + + expect(injectable).toHaveBeenCalledWith(Symbol.for("COMMAND_test")); + expect(builder.type).toHaveBeenCalledWith("command"); + expect(builder.set).toHaveBeenCalledWith("command", options); + expect(builder.factory).toHaveBeenCalledTimes(1); + + const registeredFactory = builder.factory.mock.calls[0][0]; + const result = registeredFactory(); + + expect(result).toMatchObject({ + ...options, + $exec: handler, + $prompt: prompt + }); + expect(result.handler).toBe(handler); + }); + + it("should register a class-based command with provided token", () => { + const builder = createBuilder(); + vi.mocked(injectable).mockReturnValue(builder); + + class TestCommand implements CommandProvider { + $exec(): any {} + } + + const options: CommandOptions = { + name: "test-class", + description: "description", + token: TestCommand + }; + + command(options); + + expect(injectable).toHaveBeenCalledWith(TestCommand); + expect(builder.type).toHaveBeenCalledWith("command"); + expect(builder.set).toHaveBeenCalledWith("command", options); + expect(builder.factory).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli-core/src/fn/command.ts b/packages/cli-core/src/fn/command.ts index e15220563..129a4d576 100644 --- a/packages/cli-core/src/fn/command.ts +++ b/packages/cli-core/src/fn/command.ts @@ -4,9 +4,9 @@ import {type FactoryTokenProvider, injectable, type TokenProvider} from "@tsed/d import type {CommandOptions} from "../interfaces/CommandOptions.js"; import type {CommandProvider} from "../interfaces/index.js"; -export function command(options: CommandOptions) { +export function command(options: CommandOptions) { if (!options.token) { - return injectable>(Symbol.for(`COMMAND_${options.name}`) as any) + return injectable>>(Symbol.for(`COMMAND_${options.name}`) as any) .type("command") .set("command", options) .factory(() => { @@ -18,5 +18,5 @@ export function command(options: CommandOptions) { }); } - return injectable>(options.token).type("command").set("command", options); + return injectable>>(options.token).type("command").set("command", options); } diff --git a/packages/cli-core/src/interfaces/CommandMetadata.ts b/packages/cli-core/src/interfaces/CommandMetadata.ts index 4bce221a1..70212616c 100644 --- a/packages/cli-core/src/interfaces/CommandMetadata.ts +++ b/packages/cli-core/src/interfaces/CommandMetadata.ts @@ -1,6 +1,6 @@ import type {BaseCommandOptions, CommandArg, CommandOpts} from "./CommandOptions.js"; -export interface CommandMetadata extends BaseCommandOptions { +export interface CommandMetadata extends BaseCommandOptions { /** * CommandProvider arguments */ diff --git a/packages/cli-core/src/interfaces/CommandOptions.ts b/packages/cli-core/src/interfaces/CommandOptions.ts index 96c08e5ab..2c1607498 100644 --- a/packages/cli-core/src/interfaces/CommandOptions.ts +++ b/packages/cli-core/src/interfaces/CommandOptions.ts @@ -1,7 +1,10 @@ import {Type} from "@tsed/core"; import type {TokenProvider} from "@tsed/di"; +import type {JsonSchema} from "@tsed/schema"; +import type {Answers} from "inquirer"; -import type {CommandProvider} from "./CommandProvider.js"; +import type {CommandProvider, QuestionOptions} from "./CommandProvider.js"; +import type {Tasks} from "./Tasks.js"; export interface CommandArg { /** @@ -54,7 +57,7 @@ export interface CommandOpts { customParser?: (value: any) => any; } -export interface BaseCommandOptions { +export interface BaseCommandOptions { /** * name commands */ @@ -71,6 +74,9 @@ export interface BaseCommandOptions { args?: { [key: string]: CommandArg; }; + + inputSchema?: JsonSchema | (() => JsonSchema); + /** * CommandProvider options */ @@ -85,17 +91,17 @@ export interface BaseCommandOptions { disableReadUpPkg?: boolean; } -interface FunctionalCommandOptions extends BaseCommandOptions { - prompt?: CommandProvider["$prompt"]; - handler: CommandProvider["$exec"]; +interface FunctionalCommandOptions extends BaseCommandOptions { + prompt?(initialOptions: Partial): QuestionOptions | Promise>; + handler: (data: Input) => Tasks | Promise | any | Promise; [key: string]: any; } -interface ClassCommandOptions extends BaseCommandOptions { - token: TokenProvider; +interface ClassCommandOptions extends BaseCommandOptions { + token: TokenProvider>; [key: string]: any; } -export type CommandOptions = ClassCommandOptions | FunctionalCommandOptions; +export type CommandOptions = ClassCommandOptions | FunctionalCommandOptions; diff --git a/packages/cli-core/src/services/CliDockerComposeYaml.spec.ts b/packages/cli-core/src/services/CliDockerComposeYaml.spec.ts new file mode 100644 index 000000000..439c96c9b --- /dev/null +++ b/packages/cli-core/src/services/CliDockerComposeYaml.spec.ts @@ -0,0 +1,162 @@ +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; + +import {CliDockerComposeYaml} from "./CliDockerComposeYaml.js"; +import {CliFs} from "./CliFs.js"; +import {CliYaml} from "./CliYaml.js"; +import {ProjectPackageJson} from "./ProjectPackageJson.js"; + +describe("CliDockerComposeYaml", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + async function createService(deps: Partial> = {}) { + const cliYaml = deps.cliYaml || { + read: vi.fn().mockResolvedValue({}), + write: vi.fn().mockResolvedValue(undefined) + }; + const fs = deps.fs || { + exists: vi.fn().mockReturnValue(true), + findUpFile: vi.fn().mockReturnValue(undefined) + }; + const projectPkg = deps.projectPkg || ({dir: "/project"} as ProjectPackageJson); + + const service = await CliPlatformTest.invoke(CliDockerComposeYaml, [ + { + token: CliYaml, + use: cliYaml + }, + { + token: CliFs, + use: fs + }, + { + token: ProjectPackageJson, + use: projectPkg + } + ]); + + return {service, cliYaml, fs, projectPkg}; + } + + describe("read()", () => { + it("should read docker-compose.yml from project root when it exists", async () => { + const {service, cliYaml, fs} = await createService(); + + const result = await service.read(); + + expect(fs.exists).toHaveBeenCalledWith("docker-compose.yml"); + expect(cliYaml.read).toHaveBeenCalledWith("docker-compose.yml"); + expect(result).toEqual({}); + }); + + it("should return an empty object when no docker-compose file is found", async () => { + const fs = { + exists: vi.fn().mockReturnValue(false), + findUpFile: vi.fn().mockReturnValue(undefined) + }; + const {service, cliYaml} = await createService({fs}); + + const result = await service.read(); + + expect(fs.findUpFile).toHaveBeenCalled(); + expect(cliYaml.read).not.toHaveBeenCalled(); + expect(result).toEqual({}); + }); + }); + + describe("write()", () => { + it("should write to the discovered docker-compose file", async () => { + const cliYaml = { + read: vi.fn(), + write: vi.fn().mockResolvedValue(undefined) + }; + const fs = { + exists: vi.fn(), + findUpFile: vi.fn().mockReturnValue("/repo/docker-compose.yml") + }; + const {service} = await createService({cliYaml, fs}); + const payload = {services: {}}; + + await service.write(payload); + + expect(cliYaml.write).toHaveBeenCalledWith("/repo/docker-compose.yml", payload); + }); + + it("should fallback to project dir when docker-compose file does not exist yet", async () => { + const cliYaml = { + read: vi.fn(), + write: vi.fn().mockResolvedValue(undefined) + }; + const fs = { + exists: vi.fn(), + findUpFile: vi.fn().mockReturnValue(undefined) + }; + const projectPkg = {dir: "/repo"} as ProjectPackageJson; + const {service} = await createService({cliYaml, fs, projectPkg}); + const payload = {services: {}}; + + await service.write(payload); + + expect(cliYaml.write).toHaveBeenCalledWith("/repo/docker-compose.yml", payload); + }); + }); + + describe("addDatabaseService()", () => { + it("should append a postgres service and persist the file", async () => { + const cliYaml = { + read: vi.fn().mockResolvedValue({services: {}}), + write: vi.fn().mockResolvedValue(undefined) + }; + const fs = { + exists: vi.fn().mockReturnValue(true), + findUpFile: vi.fn() + }; + const {service} = await createService({cliYaml, fs}); + + await service.addDatabaseService("OrdersDb", "postgres"); + + expect(cliYaml.write).toHaveBeenCalledTimes(1); + const [, dockerCompose] = cliYaml.write.mock.calls[0]; + expect(dockerCompose).toEqual({ + services: { + orders_db: { + image: "postgres:9.6.1", + ports: ["5432:5432"], + volumes: ["./pgdata:/var/lib/postgresql/data"], + environment: { + POSTGRES_USER: "test", + POSTGRES_PASSWORD: "test", + POSTGRES_DB: "test" + } + } + } + }); + }); + + it("should append a mongodb service definition when requested", async () => { + const cliYaml = { + read: vi.fn().mockResolvedValue({services: {}}), + write: vi.fn().mockResolvedValue(undefined) + }; + const fs = { + exists: vi.fn().mockReturnValue(true), + findUpFile: vi.fn() + }; + const {service} = await createService({cliYaml, fs}); + + await service.addDatabaseService("Analytics", "mongodb"); + + expect(cliYaml.write).toHaveBeenCalledTimes(1); + const [, dockerCompose] = cliYaml.write.mock.calls[0]; + expect(dockerCompose).toEqual({ + services: { + analytics: { + image: "mongo:5.0.8", + ports: ["27017:27017"] + } + } + }); + }); + }); +}); diff --git a/packages/cli-core/src/services/CliExeca.spec.ts b/packages/cli-core/src/services/CliExeca.spec.ts new file mode 100644 index 000000000..246bb568c --- /dev/null +++ b/packages/cli-core/src/services/CliExeca.spec.ts @@ -0,0 +1,106 @@ +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; +import {Observable} from "rxjs"; + +import {CliExeca} from "./CliExeca.js"; + +vi.mock("../utils/streamToObservable.js", () => { + return { + streamToObservable: vi.fn() + }; +}); + +vi.mock("split", () => ({ + default: vi.fn(() => "SPLIT_STREAM") +})); + +const mockedStreamToObservable = vi.importMock("../utils/streamToObservable.js"); + +describe("CliExeca", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + describe("run()", () => { + it("should merge stdout and stderr observables and filter falsy events", async () => { + const stdout$ = new Observable((observer) => { + observer.next("build:started"); + observer.next(""); + observer.complete(); + }); + const stderr$ = new Observable((observer) => { + observer.next("warning"); + observer.next(undefined as any); + observer.complete(); + }); + + const streamToObservable = await mockedStreamToObservable; + vi.mocked(streamToObservable.streamToObservable).mockReturnValueOnce(stdout$).mockReturnValueOnce(stderr$); + + const service = await CliPlatformTest.invoke(CliExeca); + const stdoutPipe = vi.fn().mockReturnValue("STDOUT"); + const stderrPipe = vi.fn().mockReturnValue("STDERR"); + const cp: any = { + stdout: { + pipe: stdoutPipe + }, + stderr: { + pipe: stderrPipe + } + }; + + (service as any).raw = vi.fn().mockReturnValue(cp) as any; + + const events: string[] = []; + await new Promise((resolve, reject) => { + service.run("node", ["--version"], {cwd: "/repo"}).subscribe({ + next: (value) => events.push(value), + complete: () => resolve(), + error: (err) => reject(err) + }); + }); + + expect(events).toEqual(["build:started", "warning"]); + expect(stdoutPipe).toHaveBeenCalled(); + expect(stderrPipe).toHaveBeenCalled(); + expect(service.raw).toHaveBeenCalledWith("node", ["--version"], {cwd: "/repo"}); + expect(streamToObservable.streamToObservable).toHaveBeenNthCalledWith(1, "STDOUT", {await: cp}); + expect(streamToObservable.streamToObservable).toHaveBeenNthCalledWith(2, "STDERR", {await: cp}); + }); + }); + + describe("runSync()", () => { + it("should delegate to rawSync", async () => { + const service = await CliPlatformTest.invoke(CliExeca); + const result = {stdout: "done"}; + + (service as any).rawSync = vi.fn().mockReturnValue(result) as any; + + expect(service.runSync("npm", ["install"], {} as any)).toEqual(result); + expect(service.rawSync).toHaveBeenCalledWith("npm", ["install"], {}); + }); + }); + + describe("getAsync()", () => { + it("should resolve stdout from the async process", async () => { + const service = await CliPlatformTest.invoke(CliExeca); + (service as any).raw = vi.fn().mockResolvedValue({stdout: "ok"}) as any; + + const result = await service.getAsync("npm", ["--version"]); + + expect(service.raw).toHaveBeenCalledWith("npm", ["--version"], undefined); + expect(result).toEqual("ok"); + }); + }); + + describe("get()", () => { + it("should return stdout from the sync process", async () => { + const service = await CliPlatformTest.invoke(CliExeca); + (service as any).rawSync = vi.fn().mockReturnValue({stdout: "1.0.0"}) as any; + + const result = service.get("npm", ["--version"]); + + expect(service.rawSync).toHaveBeenCalledWith("npm", ["--version"], undefined); + expect(result).toEqual("1.0.0"); + }); + }); +}); diff --git a/packages/cli-core/src/services/CliFs.spec.ts b/packages/cli-core/src/services/CliFs.spec.ts new file mode 100644 index 000000000..4c5a750b2 --- /dev/null +++ b/packages/cli-core/src/services/CliFs.spec.ts @@ -0,0 +1,85 @@ +import {mkdir, mkdtemp, rm, writeFile} from "node:fs/promises"; +import {tmpdir} from "node:os"; +import {join} from "node:path"; + +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; + +import {CliFs} from "./CliFs.js"; + +describe("CliFs", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + it("should delegate exists() to fs-extra", async () => { + const service = await CliPlatformTest.invoke(CliFs); + service.raw.existsSync = vi.fn().mockReturnValue(true); + + expect(service.exists("package.json")).toEqual(true); + expect(service.raw.existsSync).toHaveBeenCalledWith("package.json"); + }); + + it("should normalize joins using forward slashes", async () => { + const service = await CliPlatformTest.invoke(CliFs); + + expect(service.join("src", "cli", "..", "core")).toBe("src/core"); + }); + + it("should read and parse json files", async () => { + const service = await CliPlatformTest.invoke(CliFs); + const spy = vi.spyOn(service, "readFile").mockResolvedValueOnce('{"name":"cli"}'); + + const result = await service.readJson("package.json"); + + expect(spy).toHaveBeenCalledWith("package.json", undefined); + expect(result).toEqual({name: "cli"}); + }); + + it("should stringify payloads when writing json", async () => { + const service = await CliPlatformTest.invoke(CliFs); + (service as any).raw.writeFile = vi.fn().mockResolvedValue(undefined); + + await service.writeJson("package.json", {name: "cli"}); + + expect(service.raw.writeFile).toHaveBeenCalledWith("package.json", JSON.stringify({name: "cli"}, null, 2), {encoding: "utf8"}); + }); + + it("should locate the nearest matching file walking up the directory tree", async () => { + const service = await CliPlatformTest.invoke(CliFs); + const hits = new Set(["/repo/a/file.yml", "/repo/file.yml"]); + + vi.spyOn(service, "fileExistsSync").mockImplementation((path) => hits.has(path)); + + const result = service.findUpFile("/repo/a/b", "file.yml"); + + expect(result).toEqual("/repo/a/file.yml"); + }); + + it("should import a module discovered under node_modules by inspecting exports", async () => { + const service = await CliPlatformTest.invoke(CliFs); + const tempDir = await mkdtemp(join(tmpdir(), "cli-fs-")); + const moduleDir = join(tempDir, "node_modules", "cli-fs-mock"); + await mkdir(moduleDir, {recursive: true}); + await writeFile( + join(moduleDir, "package.json"), + JSON.stringify({ + exports: { + ".": { + import: "./index.mjs" + } + } + }) + ); + await writeFile(join(moduleDir, "index.mjs"), "export const value = 42;"); + const finder = vi.spyOn(service, "findUpFile").mockReturnValue(moduleDir); + + try { + const mod: any = await service.importModule("cli-fs-mock", tempDir); + + expect(mod.value).toEqual(42); + } finally { + finder.mockRestore(); + await rm(tempDir, {recursive: true, force: true}); + } + }); +}); diff --git a/packages/cli-core/src/services/CliHooks.spec.ts b/packages/cli-core/src/services/CliHooks.spec.ts new file mode 100644 index 000000000..042fe5954 --- /dev/null +++ b/packages/cli-core/src/services/CliHooks.spec.ts @@ -0,0 +1,53 @@ +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; +import {injector} from "@tsed/di"; + +import {CliHooks} from "./CliHooks.js"; + +describe("CliHooks", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + it("should execute hook handlers declared on providers", async () => { + class TestHook { + onTestHook(arg: string) { + return [`${arg}-done`]; + } + } + + const inj = injector(); + inj.addProvider(TestHook, { + useClass: TestHook + }); + const provider = inj.getProvider(TestHook)!; + provider.useClass = TestHook; + provider.store.set("$onTest", { + cmd: ["onTestHook"] + }); + + const hooks = await CliPlatformTest.invoke(CliHooks); + const result = await hooks.emit("$onTest", "cmd", "value"); + + expect(result).toEqual(["value-done"]); + }); + + it("should ignore providers that do not declare the hook/cmd combination", async () => { + class EmptyHook { + onOtherHook() { + return ["noop"]; + } + } + + const inj = injector(); + inj.addProvider(EmptyHook, { + useClass: EmptyHook + }); + const provider = inj.getProvider(EmptyHook)!; + provider.useClass = EmptyHook; + + const hooks = await CliPlatformTest.invoke(CliHooks); + const result = await hooks.emit("$onUnknown", "cmd"); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/cli-core/src/services/CliHttpLogClient.spec.ts b/packages/cli-core/src/services/CliHttpLogClient.spec.ts new file mode 100644 index 000000000..45dc167db --- /dev/null +++ b/packages/cli-core/src/services/CliHttpLogClient.spec.ts @@ -0,0 +1,148 @@ +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; +import {Logger} from "@tsed/logger"; + +import {CliHttpLogClient} from "./CliHttpLogClient.js"; + +class TestCliHttpLogClient extends CliHttpLogClient { + public override logToCurl(options: any) { + return "curl --request"; + } + + public callOnSuccess(options: any) { + return this.onSuccess(options); + } + + public callOnError(error: any, options: any) { + return this.onError(error, options); + } + + public callFormatLog(options: any) { + return this.formatLog(options); + } + + public callErrorMapper(error: any) { + return this.errorMapper(error); + } +} + +describe("CliHttpLogClient", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + async function createClient(loggerOverrides: Partial = {}) { + const logger = { + debug: vi.fn(), + warn: vi.fn(), + level: "info", + ...loggerOverrides + } as Logger; + + const client = await CliPlatformTest.invoke(TestCliHttpLogClient, [ + { + token: Logger, + use: logger + } + ]); + + return {client, logger}; + } + + it("should log successful calls with formatted metadata", async () => { + const {client, logger} = await createClient(); + const start = Date.now() - 10; + + client.callOnSuccess({ + startTime: start, + url: "/health", + method: "GET", + params: {foo: "bar"} + }); + + expect(logger.debug).toHaveBeenCalledWith( + expect.objectContaining({ + callee: "http", + method: "GET", + url: "/health", + callee_qs: "foo=bar", + status: "OK" + }) + ); + }); + + it("should map errors, serialize request/response metadata, and log them", async () => { + const {client, logger} = await createClient(); + const error = { + message: "Request failed", + response: { + status: 500, + headers: { + "x-request-id": "req-1" + }, + data: { + message: "boom" + } + } + }; + + await client.callOnError(error, { + startTime: Date.now() - 5, + url: "/health", + method: "POST", + headers: { + accept: "application/json" + }, + params: {filter: "all"}, + data: {payload: true} + }); + + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + status: "KO", + callee_code: 500, + callee_error: "Request failed", + callee_response_request_id: "req-1", + callee_request_body: JSON.stringify({payload: true}), + callee_response_body: JSON.stringify({message: "boom"}), + curl: "curl --request" + }) + ); + }); + + it("should compute duration and omit invalid values when startTime is missing", async () => { + const {client} = await createClient(); + + const formatted = client.callFormatLog({ + startTime: undefined, + url: "/health", + method: "GET" + }); + + expect(formatted).toEqual({ + callee: "http", + method: "GET", + url: "/health", + callee_qs: undefined, + duration: undefined + }); + }); + + it("should map error metadata even when message is missing", async () => { + const {client} = await createClient(); + const mapped = client.callErrorMapper({ + response: { + status: 429, + headers: {"x-request-id": "req-42"}, + data: {error: "too many requests"} + } + }); + + expect(mapped).toEqual({ + message: 429, + code: 429, + headers: {"x-request-id": "req-42"}, + body: {error: "too many requests"}, + x_request_id: "req-42" + }); + }); +}); diff --git a/packages/cli-core/src/services/CliPackageJson.spec.ts b/packages/cli-core/src/services/CliPackageJson.spec.ts new file mode 100644 index 000000000..2e549b0d5 --- /dev/null +++ b/packages/cli-core/src/services/CliPackageJson.spec.ts @@ -0,0 +1,24 @@ +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; +import {constant} from "@tsed/di"; + +import {CliPackageJson as CliPackageJsonToken, cliPackageJson} from "./CliPackageJson.js"; + +describe("CliPackageJson provider", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + it("should expose the current package.json via DI", () => { + const expected = constant("pkg", {}); + const pkg = CliPlatformTest.get(CliPackageJsonToken as any); + + expect(pkg).toEqual(expected); + }); + + it("should resolve through the cliPackageJson helper", () => { + const expected = constant("pkg", {}); + const pkg = cliPackageJson(); + + expect(pkg).toEqual(expected); + }); +}); diff --git a/packages/cli-core/src/services/CliService.spec.ts b/packages/cli-core/src/services/CliService.spec.ts new file mode 100644 index 000000000..0feef57f6 --- /dev/null +++ b/packages/cli-core/src/services/CliService.spec.ts @@ -0,0 +1,169 @@ +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; +import {Store} from "@tsed/core"; +import {DIContext, injector, logger} from "@tsed/di"; +import {Command} from "commander"; + +import {CliService} from "./CliService.js"; + +describe("CliService", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + it("should map command data, honor $mapContext, and toggle logger level", async () => { + const service = await CliPlatformTest.invoke(CliService); + const token = class TestCommand {}; + const instance = { + $mapContext: vi.fn().mockImplementation((payload) => { + payload.flag = true; + return payload; + }) + }; + + injector().addProvider(token, { + useValue: instance + }); + + service["commands"].set("generate", {token} as any); + + const $ctx = new DIContext({ + id: "ctx", + injector: injector(), + logger: logger(), + level: "info", + platform: "CLI" + }); + $ctx.set("command", {bindLogger: vi.fn()}); + + const originalLevel = logger().level; + try { + const result = service["mapData"]("generate", {verbose: true, tsed: "yes"} as any, $ctx); + + expect(instance.$mapContext).toHaveBeenCalledWith( + expect.objectContaining({ + verbose: true, + tsed: "yes", + commandName: "generate" + }) + ); + expect(result).toEqual({ + verbose: true, + tsed: "yes", + flag: true, + commandName: "generate", + bindLogger: $ctx.get("command")!.bindLogger + }); + expect(logger().level.toLowerCase()).toBe("debug"); + } finally { + logger().level = originalLevel; + } + }); + + it("should keep info logging level when verbose is falsy", async () => { + const service = await CliPlatformTest.invoke(CliService); + const token = class OtherCommand {}; + injector().addProvider(token, { + useValue: {} + }); + service["commands"].set("add", {token} as any); + const $ctx = new DIContext({ + id: "ctx", + injector: injector(), + logger: logger(), + level: "info", + platform: "CLI" + }); + + const originalLevel = logger().level; + try { + const result = service["mapData"]("add", {verbose: false}, $ctx); + + expect(result.verbose).toBe(false); + expect(logger().level.toLowerCase()).toBe("info"); + } finally { + logger().level = originalLevel; + } + }); + + it("should build options with defaults, include root-dir/verbose, and tolerate unknown options", async () => { + const service = await CliPlatformTest.invoke(CliService); + const cmd = new Command("generate"); + cmd.exitOverride(); + + const built = service["buildOption"]( + cmd, + { + "-f, --feature ": { + description: "Feature name", + required: true, + type: String + }, + "--toggle": { + description: "Enable toggle", + type: Boolean + } + }, + true + ); + + expect(() => + built.parse(["node", "cli", "--feature", "http", "--toggle", "-r", "./repo", "--verbose", "--custom"], {from: "user"}) + ).not.toThrow(); + + const opts = built.opts(); + expect(opts.feature).toBe("http"); + expect(opts.toggle).toBe(true); + expect(opts.rootDir).toBe("./repo"); + expect(opts.verbose).toBe(true); + expect((built as any)._allowUnknownOption).toBe(true); + }); + + it("should register command arguments with required flags and defaults", async () => { + const service = await CliPlatformTest.invoke(CliService); + const cmd = new Command("generate"); + + service["buildArguments"](cmd, { + project: { + description: "Project name", + required: true + }, + template: { + description: "Template name", + required: false, + defaultValue: "rest" + } + }); + + const args = (cmd as any).registeredArguments; + expect(args[0].name()).toBe("project"); + expect(args[0].required).toBe(true); + expect(args[1].name()).toBe("template"); + expect(args[1].required).toBe(false); + expect(args[1].defaultValue).toBe("rest"); + }); + + it("should register providers via build() and guard against duplicate command names", async () => { + const service = await CliPlatformTest.invoke(CliService); + const token = class First {}; + Store.from(token).set("command", {name: "generate", description: "desc", args: {}, options: {}} as any); + const provider: any = { + token, + useClass: token + }; + const createCommand = vi.spyOn(service as any, "createCommand").mockReturnValue({} as any); + + service["build"](provider); + + expect(createCommand).toHaveBeenCalledWith(expect.objectContaining({name: "generate"})); + expect(service["commands"].get("generate")).toBe(provider); + + const duplicateProvider: any = { + token: class Duplicate {}, + useClass: class Duplicate {}, + command: {} + }; + Store.from(duplicateProvider.token).set("command", {name: "generate", description: "dup"} as any); + + expect(() => service["build"](duplicateProvider)).toThrow("The generate command is already registered"); + }); +}); diff --git a/packages/cli-core/src/services/CliYaml.spec.ts b/packages/cli-core/src/services/CliYaml.spec.ts new file mode 100644 index 000000000..54b1e0f94 --- /dev/null +++ b/packages/cli-core/src/services/CliYaml.spec.ts @@ -0,0 +1,43 @@ +// @ts-ignore +import {CliPlatformTest} from "@tsed/cli-testing"; + +import {CliFs} from "./CliFs.js"; +import {CliYaml} from "./CliYaml.js"; + +describe("CliYaml", () => { + beforeEach(() => CliPlatformTest.create()); + afterEach(() => CliPlatformTest.reset()); + + it("should parse yaml files via CliFs", async () => { + const fs = { + readFile: vi.fn().mockResolvedValue("name: cli") + }; + const service = await CliPlatformTest.invoke(CliYaml, [ + { + token: CliFs, + use: fs + } + ]); + + const result = await service.read("settings.yml"); + + expect(fs.readFile).toHaveBeenCalledWith("settings.yml", {encoding: "utf8"}); + expect(result).toEqual({name: "cli"}); + }); + + it("should serialize objects to yaml before writing", async () => { + const fs = { + writeFile: vi.fn().mockResolvedValue(undefined) + }; + const service = await CliPlatformTest.invoke(CliYaml, [ + { + token: CliFs, + use: fs + } + ]); + + await service.write("settings.yml", {name: "cli"}); + + expect(fs.writeFile).toHaveBeenCalledWith("settings.yml", "name: cli\n", {encoding: "utf8"}); + }); +}); diff --git a/packages/cli-core/src/utils/getCommandMetadata.spec.ts b/packages/cli-core/src/utils/getCommandMetadata.spec.ts index 391c04d51..36ee01e03 100644 --- a/packages/cli-core/src/utils/getCommandMetadata.spec.ts +++ b/packages/cli-core/src/utils/getCommandMetadata.spec.ts @@ -1,3 +1,5 @@ +import {s} from "@tsed/schema"; + import {Command} from "../decorators/index.js"; import {getCommandMetadata} from "./getCommandMetadata.js"; @@ -24,6 +26,7 @@ describe("getCommandMetadata", () => { description: "description", name: "name", alias: "g", + inputSchema: undefined, disableReadUpPkg: false, enableFeatures: [], bindLogger: true, @@ -37,6 +40,7 @@ describe("getCommandMetadata", () => { description: "description", name: "name", alias: "g", + inputSchema: undefined, disableReadUpPkg: false, enableFeatures: [], bindLogger: false, @@ -44,4 +48,89 @@ describe("getCommandMetadata", () => { token: TestCmd2 }); }); + + it("should map inputSchema properties to args and options (direct schema)", () => { + const schema = s + .object({ + filename: s.string().required(), + verbose: s.boolean(), + list: s.array().items(s.string()) + }) + .set("custom-parser", (v: any) => v); + + // enrich properties with metadata used by getCommandMetadata + const props = schema.get("properties") as any; + props.filename.set("arg", "file").set("description", "The file to process").set("default", "index.ts"); + props.verbose.set("opt", "--verbose").set("description", "Enable verbose mode").set("default", false); + props.list.set("opt", "--list").set("description", "List of items"); + + @Command({ + name: "with-schema", + description: "cmd with schema", + alias: "ws", + inputSchema: schema + }) + class Cmd {} + + const metadata = getCommandMetadata(Cmd); + + // inputSchema should be kept + expect(metadata.inputSchema).toBe(schema); + + // args mapped from property with `arg` + expect(metadata.args).toEqual( + expect.objectContaining({ + file: expect.objectContaining({ + description: "The file to process", + defaultValue: "index.ts", + type: String + }) + }) + ); + + // options mapped from properties with `opt`, including customParser from root schema + expect(metadata.options).toEqual( + expect.objectContaining({ + "--verbose": expect.objectContaining({ + description: "Enable verbose mode", + defaultValue: false, + required: false, + type: Boolean, + customParser: schema.get("custom-parser") + }), + "--list": expect.objectContaining({ + description: "List of items", + required: false, + type: Array, + itemType: String + }) + }) + ); + }); + + it("should support inputSchema as a factory function (arrow fn)", () => { + const inner = s.object({count: s.number()}); + inner.get("properties").count.set("opt", "-c").set("description", "Count"); + + @Command({ + name: "with-fn", + description: "cmd with schema fn", + alias: "wf", + inputSchema: () => inner + }) + class Cmd {} + + const metadata = getCommandMetadata(Cmd); + + expect(metadata.inputSchema).toBeTypeOf("function"); + // getCommandMetadata resolves the function and maps options + expect(metadata.options).toEqual( + expect.objectContaining({ + "-c": expect.objectContaining({ + description: "Count", + type: Number + }) + }) + ); + }); }); diff --git a/packages/cli-core/src/utils/getCommandMetadata.ts b/packages/cli-core/src/utils/getCommandMetadata.ts index 7041e030b..77a33940c 100644 --- a/packages/cli-core/src/utils/getCommandMetadata.ts +++ b/packages/cli-core/src/utils/getCommandMetadata.ts @@ -1,8 +1,9 @@ -import {Store} from "@tsed/core"; +import {isArrowFn, Store} from "@tsed/core"; import type {TokenProvider} from "@tsed/di"; +import type {JsonSchema} from "@tsed/schema"; import type {CommandMetadata} from "../interfaces/CommandMetadata.js"; -import type {CommandOptions} from "../interfaces/CommandOptions.js"; +import type {CommandArg, CommandOptions, CommandOpts} from "../interfaces/CommandOptions.js"; export function getCommandMetadata(token: TokenProvider): CommandMetadata { const { @@ -14,12 +15,43 @@ export function getCommandMetadata(token: TokenProvider): CommandMetadata { options = {}, enableFeatures, disableReadUpPkg, + inputSchema, bindLogger = true, ...opts - } = Store.from(token)?.get("command") as CommandOptions; + } = Store.from(token)?.get("command") as CommandOptions; + + if (inputSchema) { + const schema = isArrowFn(inputSchema) ? inputSchema() : inputSchema; + + Object.entries(schema.get("properties") || {})?.forEach(([propertyKey, propertySchema]) => { + const base = { + type: propertySchema.getTarget(), + itemType: propertySchema.isCollection ? propertySchema.get("items").getTarget() : undefined, + description: propertySchema.get("description") || "", + defaultValue: propertySchema.get("default"), + required: schema.isRequired(propertyKey) + }; + + const opt = propertySchema.get("opt"); + + if (opt) { + options[opt] = { + ...base, + customParser: schema.get("custom-parser") + } satisfies CommandOpts; + } + + const arg = propertySchema.get("arg"); + + if (arg) { + args[arg] = base satisfies CommandArg; + } + }); + } return { name, + inputSchema, alias, args, description, diff --git a/packages/cli/src/commands/mcp/McpCommand.ts b/packages/cli/src/commands/mcp/McpCommand.ts index c860d9e1f..dafb387b3 100644 --- a/packages/cli/src/commands/mcp/McpCommand.ts +++ b/packages/cli/src/commands/mcp/McpCommand.ts @@ -1,12 +1,17 @@ import {command} from "@tsed/cli-core"; import {MCP_SERVER} from "@tsed/cli-mcp"; import {inject} from "@tsed/di"; +import {s} from "@tsed/schema"; + +const McpSchema = s.object({ + http: s.boolean().default(false).description("Run MCP using HTTP server").customKey("opt", "--http") +}); export const McpCommand = command({ name: "mcp", description: "Run a MCP server", - async handler() { - await inject(MCP_SERVER).connect(); - return []; + inputSchema: McpSchema, + handler(data) { + return inject(MCP_SERVER).connect(data.http ? "streamable-http" : "stdio"); } }).token(); From 305c26bdf0c1e99f929e4d657bbc01e5a0a08c46 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 10 Dec 2025 21:20:55 +0100 Subject: [PATCH 09/18] refactor(cli-mcp): remove CLIMCPServer and refactor MCP server lifecycle and logging --- packages/cli-mcp/src/index.ts | 1 - .../cli-mcp/src/services/CLIMCPServer.spec.ts | 59 ------------------- packages/cli-mcp/src/services/CLIMCPServer.ts | 47 --------------- .../src/services/McpServerFactory.spec.ts | 18 +----- .../cli-mcp/src/services/McpServerFactory.ts | 10 ++-- .../src/services/McpStdioServer.spec.ts | 2 + .../cli-mcp/src/services/McpStdioServer.ts | 3 + .../src/services/McpStreamableServer.ts | 28 ++++++--- 8 files changed, 31 insertions(+), 137 deletions(-) delete mode 100644 packages/cli-mcp/src/services/CLIMCPServer.spec.ts delete mode 100644 packages/cli-mcp/src/services/CLIMCPServer.ts diff --git a/packages/cli-mcp/src/index.ts b/packages/cli-mcp/src/index.ts index 1b7014e26..f451b8e37 100644 --- a/packages/cli-mcp/src/index.ts +++ b/packages/cli-mcp/src/index.ts @@ -1,5 +1,4 @@ export * from "./fn/definePrompt.js"; export * from "./fn/defineResource.js"; export * from "./fn/defineTool.js"; -export * from "./services/CLIMCPServer.js"; export * from "./services/McpServerFactory.js"; diff --git a/packages/cli-mcp/src/services/CLIMCPServer.spec.ts b/packages/cli-mcp/src/services/CLIMCPServer.spec.ts deleted file mode 100644 index a19f457ec..000000000 --- a/packages/cli-mcp/src/services/CLIMCPServer.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {injector} from "@tsed/di"; -import {$asyncEmit} from "@tsed/hooks"; -import {beforeEach, describe, expect, it, vi} from "vitest"; - -import {CLIMCPServer} from "./CLIMCPServer.js"; - -vi.mock("@tsed/cli-core", async () => { - const actual = await vi.importActual("@tsed/cli-core"); - return { - ...actual, - loadPlugins: vi.fn().mockResolvedValue(undefined) - }; -}); - -vi.mock("@tsed/hooks", async () => { - const actual = await vi.importActual("@tsed/hooks"); - return { - ...actual, - $asyncEmit: vi.fn().mockResolvedValue(undefined) - }; -}); - -describe("CLIMCPServer", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("should have a static bootstrap method", () => { - expect(typeof CLIMCPServer.bootstrap).toBe("function"); - }); - - it("should emit lifecycle hooks", async () => { - const emitSpy = vi.mocked($asyncEmit); - - vi.spyOn(injector(), "load").mockResolvedValue(undefined); - - try { - await CLIMCPServer.bootstrap({name: "test"}); - } catch (e) { - // Ignore connection errors - } - - expect(emitSpy).toHaveBeenCalledWith("$beforeInit"); - expect(emitSpy).toHaveBeenCalledWith("$afterInit"); - expect(emitSpy).toHaveBeenCalledWith("$onReady"); - }); - - it("should load injector", async () => { - const loadSpy = vi.spyOn(injector(), "load").mockResolvedValue(undefined); - - try { - await CLIMCPServer.bootstrap({name: "test"}); - } catch (e) { - // Ignore connection errors - } - - expect(loadSpy).toHaveBeenCalled(); - }); -}); diff --git a/packages/cli-mcp/src/services/CLIMCPServer.ts b/packages/cli-mcp/src/services/CLIMCPServer.ts deleted file mode 100644 index 210d61f54..000000000 --- a/packages/cli-mcp/src/services/CLIMCPServer.ts +++ /dev/null @@ -1,47 +0,0 @@ -import "./McpServerFactory.js"; - -import {createInjector, loadPlugins} from "@tsed/cli-core"; -import {constant, inject, injector} from "@tsed/di"; -import {$asyncEmit} from "@tsed/hooks"; - -import {MCP_SERVER} from "./McpServerFactory.js"; - -export class CLIMCPServer { - protected constructor(settings: Partial) { - createInjector({ - ...settings, - logger: { - level: "info" - } - }); - } - - static async bootstrap(settings: Partial) { - const server = new CLIMCPServer({ - ...settings, - name: settings.name || "tsed", - project: { - srcDir: "src", - scriptsDir: "scripts", - ...(settings.project || {}) - } - }); - - return server.bootstrap(); - } - - async bootstrap() { - constant("plugins") && (await loadPlugins()); - - await $asyncEmit("$beforeInit"); - - await injector().load(); - await $asyncEmit("$afterInit"); - - injector().settings.set("loaded", true); - - await $asyncEmit("$onReady"); - - await inject(MCP_SERVER).connect(); - } -} diff --git a/packages/cli-mcp/src/services/McpServerFactory.spec.ts b/packages/cli-mcp/src/services/McpServerFactory.spec.ts index 54929e909..27986d44f 100644 --- a/packages/cli-mcp/src/services/McpServerFactory.spec.ts +++ b/packages/cli-mcp/src/services/McpServerFactory.spec.ts @@ -65,24 +65,11 @@ describe("McpServerFactory", () => { injector().settings.set("mcp.mode", "stdio"); const result = inject(MCP_SERVER); - const loggerSpy = vi.spyOn(logger(), "info"); await result.connect(); - expect(loggerSpy).toHaveBeenCalledWith({event: "MCP_SERVER_CONNECT"}); - expect(loggerSpy).toHaveBeenCalledWith({event: "MCP_SERVER_CONNECTED"}); expect(mcpStdioServer).toHaveBeenCalledWith(result.server); }); - it("should log connection events", async () => { - const result = injector().invoke(MCP_SERVER); - const loggerSpy = vi.spyOn(logger(), "info"); - - await result.connect(); - - expect(loggerSpy).toHaveBeenCalledTimes(2); - expect(loggerSpy).toHaveBeenNthCalledWith(1, {event: "MCP_SERVER_CONNECT"}); - expect(loggerSpy).toHaveBeenNthCalledWith(2, {event: "MCP_SERVER_CONNECTED"}); - }); }); describe("http", () => { beforeEach(() => @@ -102,10 +89,9 @@ describe("McpServerFactory", () => { const result = inject(MCP_SERVER); const loggerSpy = vi.spyOn(logger(), "info"); - await result.connect(); + await result.connect("streamable-http"); - expect(loggerSpy).toHaveBeenCalledWith({event: "MCP_SERVER_CONNECT"}); - expect(loggerSpy).toHaveBeenCalledWith({event: "MCP_SERVER_CONNECTED"}); + expect(loggerSpy).toHaveBeenCalledWith({event: "MCP_SERVER_CONNECT", mode: "streamable-http"}); expect(mcpStreamableServer).toHaveBeenCalled(); }); }); diff --git a/packages/cli-mcp/src/services/McpServerFactory.ts b/packages/cli-mcp/src/services/McpServerFactory.ts index 4ee3836a7..5f51ecfe2 100644 --- a/packages/cli-mcp/src/services/McpServerFactory.ts +++ b/packages/cli-mcp/src/services/McpServerFactory.ts @@ -9,7 +9,7 @@ import {mcpStreamableServer} from "./McpStreamableServer.js"; export const MCP_SERVER = injectable(McpServer) .factory(() => { - const mode = constant<"streamable-http" | "stdio">("mcp.mode"); + const defaultMode = constant<"streamable-http" | "stdio">("mcp.mode"); const name = constant("name")!; const server = new McpServer({ @@ -42,16 +42,14 @@ export const MCP_SERVER = injectable(McpServer) return { server, - async connect() { - logger().info({event: "MCP_SERVER_CONNECT"}); - + async connect(mode: "streamable-http" | "stdio" | undefined = defaultMode) { if (mode === "streamable-http") { + logger().info({event: "MCP_SERVER_CONNECT", mode}); + await mcpStreamableServer(server); } else { await mcpStdioServer(server); } - - logger().info({event: "MCP_SERVER_CONNECTED"}); } }; }) diff --git a/packages/cli-mcp/src/services/McpStdioServer.spec.ts b/packages/cli-mcp/src/services/McpStdioServer.spec.ts index 05c6e8e18..cdcbce7e5 100644 --- a/packages/cli-mcp/src/services/McpStdioServer.spec.ts +++ b/packages/cli-mcp/src/services/McpStdioServer.spec.ts @@ -1,4 +1,5 @@ import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js"; +import {DITest} from "@tsed/di"; import {beforeEach, describe, expect, it, vi} from "vitest"; import {mcpStdioServer} from "./McpStdioServer.js"; @@ -15,6 +16,7 @@ describe("mcpStdioServer", () => { let mockServer: McpServer; let mockConnect: any; + beforeEach(() => DITest.create()); beforeEach(async () => { vi.clearAllMocks(); diff --git a/packages/cli-mcp/src/services/McpStdioServer.ts b/packages/cli-mcp/src/services/McpStdioServer.ts index b61da0dda..895548f42 100644 --- a/packages/cli-mcp/src/services/McpStdioServer.ts +++ b/packages/cli-mcp/src/services/McpStdioServer.ts @@ -1,9 +1,12 @@ import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js"; +import {logger} from "@tsed/di"; export async function mcpStdioServer(server: McpServer) { const {StdioServerTransport} = await import("@modelcontextprotocol/sdk/server/stdio.js"); const transport = new StdioServerTransport(); + logger().stop(); + return server.connect(transport); } diff --git a/packages/cli-mcp/src/services/McpStreamableServer.ts b/packages/cli-mcp/src/services/McpStreamableServer.ts index e71ed816d..a141375e8 100644 --- a/packages/cli-mcp/src/services/McpStreamableServer.ts +++ b/packages/cli-mcp/src/services/McpStreamableServer.ts @@ -1,4 +1,5 @@ import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js"; +import {logger} from "@tsed/di"; export async function mcpStreamableServer(server: McpServer) { const {StreamableHTTPServerTransport} = await import("@modelcontextprotocol/sdk/server/streamableHttp.js"); @@ -25,12 +26,23 @@ export async function mcpStreamableServer(server: McpServer) { const port = parseInt(process.env.PORT || "3000"); - app - .listen(port, () => { - console.log(`Demo MCP Server running on http://localhost:${port}/mcp`); - }) - .on("error", (error: any) => { - console.error("Server error:", error); - process.exit(1); - }); + return new Promise((resolve, reject) => { + app + .listen(port, () => { + logger().info({ + event: "MCP_STREAMABLE_SERVER", + state: "OK", + message: `Running http://localhost:${port}/mcp` + }); + }) + .on("close", () => resolve(true)) + .on("error", (error: any) => { + logger().error({ + event: "MCP_STREAMABLE_SERVER", + state: "KO", + message: error.message + }); + reject(error); + }); + }); } From 82bea98da59f496f1317ed40bc56dda96c811603 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 10 Dec 2025 21:21:14 +0100 Subject: [PATCH 10/18] chore(mcp): update integration scripts and add Prisma support in tests --- packages/cli-plugin-prisma/tests/init.integration.spec.ts | 6 +++++- tools/integration/package.json | 4 ++-- yarn.lock | 1 - 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli-plugin-prisma/tests/init.integration.spec.ts b/packages/cli-plugin-prisma/tests/init.integration.spec.ts index 90716410f..558402352 100644 --- a/packages/cli-plugin-prisma/tests/init.integration.spec.ts +++ b/packages/cli-plugin-prisma/tests/init.integration.spec.ts @@ -91,6 +91,8 @@ describe("Prisma: Init cmd", () => { "yarn install", "yarn add --ignore-engines @tsed/logger @tsed/logger-std @tsed/engines @tsed/barrels ajv cross-env @swc/core @swc/cli @swc/helpers @swc-node/register typescript body-parser cors compression cookie-parser express method-override", "yarn add -D --ignore-engines @types/node @types/multer tslib nodemon @types/cors @types/express @types/compression @types/cookie-parser @types/method-override", + "yarn add --ignore-engines @tsed/logger @tsed/logger-std @tsed/engines @tsed/barrels ajv cross-env @swc/core @swc/cli @swc/helpers @swc-node/register typescript body-parser cors compression cookie-parser express method-override @tsed/prisma @prisma/client", + "yarn add -D --ignore-engines @types/node @types/multer tslib nodemon @types/cors @types/express @types/compression @types/cookie-parser @types/method-override prisma", "npx prisma init", ] `); @@ -122,7 +124,9 @@ describe("Prisma: Init cmd", () => { "build": "yarn run barrels && swc src --out-dir dist -s --strip-leading-paths", "barrels": "barrels", "start": "yarn run barrels && nodemon src/index.ts", - "start:prod": "cross-env NODE_ENV=production node --import @swc-node/register/esm-register src/index.js" + "start:prod": "cross-env NODE_ENV=production node --import @swc-node/register/esm-register src/index.js", + "prisma:migrate": "npx prisma migrate dev --name init", + "prisma:generate": "npx prisma generate" }, "dependencies": { "@tsed/ajv": "5.58.1", diff --git a/tools/integration/package.json b/tools/integration/package.json index 340cdffe1..7a5731d34 100644 --- a/tools/integration/package.json +++ b/tools/integration/package.json @@ -11,8 +11,8 @@ }, "scripts": { "run-cmd": "cross-env NODE_ENV=development CLI_MODE=ts CI=true node --import @swc-node/register/esm-register index.ts", - "run-mcp-stdio": "npx @modelcontextprotocol/inspector node -e NODE_ENV=development -e CLI_MODE=ts -e CI=true -e LOG_SERVER_URL=http://localhost:3838 --import @swc-node/register/esm-register mcp.ts", - "run-mcp-http": "cross-env NODE_ENV=development CLI_MODE=ts CI=true USE_MCP_HTTP=true node --import @swc-node/register/esm-register index.ts mcp", + "run-mcp-stdio": "npx @modelcontextprotocol/inspector node -e NODE_ENV=development -e CLI_MODE=ts -e CI=true -e LOG_SERVER_URL=http://localhost:3838 --import @swc-node/register/esm-register mcp.ts mcp", + "run-mcp-http": "cross-env NODE_ENV=development CLI_MODE=ts CI=true USE_MCP_HTTP=true node --import @swc-node/register/esm-register index.ts mcp --http", "mcp:inspector": "npx @modelcontextprotocol/inspector", "start:help": "yarn run-cmd -h", "start:help:g": "yarn run-cmd generate -h", diff --git a/yarn.lock b/yarn.lock index e52561b29..a7156afcc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2866,7 +2866,6 @@ __metadata: optional: false bin: tsed: lib/esm/bin/tsed.js - tsed-mcp: lib/esm/bin/tsed-mcp.js languageName: unknown linkType: soft From 07276d52a00453e5571dfc9f0646053d9b1d0dcb Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 23 Dec 2025 14:30:40 +0100 Subject: [PATCH 11/18] chore: bump @tsed dependencies to v8.20.0 and update related configurations - Updated `package.json` and `yarn.lock` files to use `@tsed` packages >=8.20.0. - Added `start:init:options` command to `tools/integration/package.json`. - Adjusted commitlint config to include `body-max-line-length` rule. - Synced MCP-related dependencies and scripts with the new version. --- commitlint.config.js | 3 +- package.json | 10 ++-- packages/cli-mcp/package.json | 12 ++--- packages/cli/package.json | 12 ++--- tools/integration/package.json | 1 + yarn.lock | 84 +++++++++++++++++----------------- 6 files changed, 62 insertions(+), 60 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index a4a781ae9..51991efbc 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,6 +1,7 @@ export default { extends: ["@commitlint/config-conventional"], rules: { - "header-max-length": [2, "always", 200] + "header-max-length": [2, "always", 200], + "body-max-line-length": [2, "always", 300] } }; diff --git a/package.json b/package.json index 97f22f838..05aa61624 100644 --- a/package.json +++ b/package.json @@ -44,13 +44,13 @@ }, "homepage": "https://github.com/tsedio/tsed-cli", "dependencies": { - "@tsed/core": ">=8.18.0", - "@tsed/di": ">=8.18.0", + "@tsed/core": ">=8.20.0", + "@tsed/di": ">=8.20.0", "@tsed/logger": ">=8.0.3", "@tsed/logger-std": ">=8.0.3", - "@tsed/normalize-path": ">=8.18.0", - "@tsed/openspec": ">=8.18.0", - "@tsed/schema": ">=8.18.0", + "@tsed/normalize-path": ">=8.20.0", + "@tsed/openspec": ">=8.20.0", + "@tsed/schema": ">=8.20.0", "axios": "^1.7.7", "chalk": "^5.3.0", "commander": "^12.1.0", diff --git a/packages/cli-mcp/package.json b/packages/cli-mcp/package.json index f132ae28e..63e159b43 100644 --- a/packages/cli-mcp/package.json +++ b/packages/cli-mcp/package.json @@ -51,14 +51,14 @@ "@swc/core": "1.7.26", "@swc/helpers": "^0.5.13", "@tsed/cli-core": "workspace:*", - "@tsed/core": ">=8.18.0", - "@tsed/di": ">=8.18.0", - "@tsed/hooks": ">=8.18.0", + "@tsed/core": ">=8.20.0", + "@tsed/di": ">=8.20.0", + "@tsed/hooks": ">=8.20.0", "@tsed/logger": ">=8.0.3", "@tsed/logger-std": ">=8.0.3", - "@tsed/normalize-path": ">=8.18.0", - "@tsed/openspec": ">=8.18.0", - "@tsed/schema": ">=8.18.0", + "@tsed/normalize-path": ">=8.20.0", + "@tsed/openspec": ">=8.20.0", + "@tsed/schema": ">=8.20.0", "chalk": "^5.3.0", "change-case": "^5.4.4", "consolidate": "^1.0.4", diff --git a/packages/cli/package.json b/packages/cli/package.json index 6318eccc5..4ce19739b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,14 +52,14 @@ "@swc/helpers": "^0.5.13", "@tsed/cli-core": "workspace:*", "@tsed/cli-mcp": "workspace:*", - "@tsed/core": ">=8.18.0", - "@tsed/di": ">=8.18.0", - "@tsed/hooks": ">=8.18.0", + "@tsed/core": ">=8.20.0", + "@tsed/di": ">=8.20.0", + "@tsed/hooks": ">=8.20.0", "@tsed/logger": ">=8.0.3", "@tsed/logger-std": ">=8.0.3", - "@tsed/normalize-path": ">=8.18.0", - "@tsed/openspec": ">=8.18.0", - "@tsed/schema": ">=8.18.0", + "@tsed/normalize-path": ">=8.20.0", + "@tsed/openspec": ">=8.20.0", + "@tsed/schema": ">=8.20.0", "chalk": "^5.3.0", "change-case": "^5.4.4", "consolidate": "^1.0.4", diff --git a/tools/integration/package.json b/tools/integration/package.json index 7a5731d34..12a70c931 100644 --- a/tools/integration/package.json +++ b/tools/integration/package.json @@ -19,6 +19,7 @@ "start:help:i": "yarn run-cmd init -h", "start:version": "yarn run-cmd --version", "start:init:help": "yarn run-cmd init -h", + "start:init:options": "yarn run-cmd init-options --indent 2", "start:init:test": "yarn run-cmd init init -r ./.tmp/init/default --features=oidc --arch=default --convention=conv_default --platform=express --package-manager=npm --skip-prompt .", "start:init:test:jest": "yarn run-cmd init -r ./.tmp/init/default --features=jest --arch=default --convention=conv_default --platform=express --package-manager=npm --skip-prompt .", "start:init:run": "yarn run-cmd init -r ./.tmp/init/default", diff --git a/yarn.lock b/yarn.lock index a7156afcc..b93a0c545 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2532,14 +2532,14 @@ __metadata: "@swc/core": "npm:1.7.26" "@swc/helpers": "npm:^0.5.13" "@tsed/cli-core": "workspace:*" - "@tsed/core": "npm:>=8.18.0" - "@tsed/di": "npm:>=8.18.0" - "@tsed/hooks": "npm:>=8.18.0" + "@tsed/core": "npm:>=8.20.0" + "@tsed/di": "npm:>=8.20.0" + "@tsed/hooks": "npm:>=8.20.0" "@tsed/logger": "npm:>=8.0.3" "@tsed/logger-std": "npm:>=8.0.3" - "@tsed/normalize-path": "npm:>=8.18.0" - "@tsed/openspec": "npm:>=8.18.0" - "@tsed/schema": "npm:>=8.18.0" + "@tsed/normalize-path": "npm:>=8.20.0" + "@tsed/openspec": "npm:>=8.20.0" + "@tsed/schema": "npm:>=8.20.0" "@tsed/typescript": "workspace:*" "@types/change-case": "npm:^2.3.1" "@types/consolidate": "npm:0.14.4" @@ -2733,15 +2733,15 @@ __metadata: "@commitlint/config-conventional": "npm:19.5.0" "@swc/core": "npm:1.7.26" "@swc/helpers": "npm:0.5.13" - "@tsed/core": "npm:>=8.18.0" - "@tsed/di": "npm:>=8.18.0" + "@tsed/core": "npm:>=8.20.0" + "@tsed/di": "npm:>=8.20.0" "@tsed/logger": "npm:>=8.0.3" "@tsed/logger-std": "npm:>=8.0.3" "@tsed/markdown-it-symbols": "npm:3.20.8" "@tsed/monorepo-utils": "npm:3.0.0" - "@tsed/normalize-path": "npm:>=8.18.0" - "@tsed/openspec": "npm:>=8.18.0" - "@tsed/schema": "npm:>=8.18.0" + "@tsed/normalize-path": "npm:>=8.20.0" + "@tsed/openspec": "npm:>=8.20.0" + "@tsed/schema": "npm:>=8.20.0" "@tsed/ts-doc": "npm:^4.1.0" "@types/inquirer-autocomplete-prompt": "npm:3.0.3" "@types/node": "npm:22.7.4" @@ -2815,14 +2815,14 @@ __metadata: "@swc/helpers": "npm:^0.5.13" "@tsed/cli-core": "workspace:*" "@tsed/cli-mcp": "workspace:*" - "@tsed/core": "npm:>=8.18.0" - "@tsed/di": "npm:>=8.18.0" - "@tsed/hooks": "npm:>=8.18.0" + "@tsed/core": "npm:>=8.20.0" + "@tsed/di": "npm:>=8.20.0" + "@tsed/hooks": "npm:>=8.20.0" "@tsed/logger": "npm:>=8.0.3" "@tsed/logger-std": "npm:>=8.0.3" - "@tsed/normalize-path": "npm:>=8.18.0" - "@tsed/openspec": "npm:>=8.18.0" - "@tsed/schema": "npm:>=8.18.0" + "@tsed/normalize-path": "npm:>=8.20.0" + "@tsed/openspec": "npm:>=8.20.0" + "@tsed/schema": "npm:>=8.20.0" "@tsed/typescript": "workspace:*" "@types/change-case": "npm:^2.3.1" "@types/consolidate": "npm:0.14.4" @@ -2869,19 +2869,19 @@ __metadata: languageName: unknown linkType: soft -"@tsed/core@npm:>=8.18.0": - version: 8.18.2 - resolution: "@tsed/core@npm:8.18.2" +"@tsed/core@npm:>=8.20.0": + version: 8.20.0 + resolution: "@tsed/core@npm:8.20.0" dependencies: reflect-metadata: "npm:^0.2.2" tslib: "npm:2.7.0" - checksum: 10/b2800e2d71bed97633780047354d2d8805f7dee86910297cde62486f697df503ed93eaf650668e39340a17314cf39dc99a52875c069590550a6eee0f74c7e19c + checksum: 10/beb02c18df256acfff04d3b52269105406e808b488d1a04f09343a6381f8988ba22c60c6028932e8b3146b4ab7113aa9ddd944f6d2026d37a6793789ea54906a languageName: node linkType: hard -"@tsed/di@npm:>=8.18.0": - version: 8.18.2 - resolution: "@tsed/di@npm:8.18.2" +"@tsed/di@npm:>=8.20.0": + version: 8.20.0 + resolution: "@tsed/di@npm:8.20.0" dependencies: tslib: "npm:2.7.0" uuid: "npm:^10.0.0" @@ -2896,17 +2896,17 @@ __metadata: optional: false "@tsed/logger": optional: false - checksum: 10/5bb136218f6dc95208cf67b68881ef24e639ec8e6a64d484fad34a2051970c7f8e28c32ac12f80130f24764868c12a158dbf69eeb18d0bfb89fd3b30bc436bb3 + checksum: 10/c13d1da707d4c0c47df0e6013d3a0886fbc2fa00976705bbd2193c611e763b2707a8b038f7f282c6fbf4c377eaf74659de98cfbed71fedc1744b4095738beb41 languageName: node linkType: hard -"@tsed/hooks@npm:>=8.18.0": - version: 8.18.2 - resolution: "@tsed/hooks@npm:8.18.2" +"@tsed/hooks@npm:>=8.20.0": + version: 8.20.0 + resolution: "@tsed/hooks@npm:8.20.0" dependencies: reflect-metadata: "npm:^0.2.2" tslib: "npm:2.7.0" - checksum: 10/cbcc6e33fbaf07751dd14c1a8a1e55109a644cb3b229f5bb25e0ed71378ed1c2deba6103ecea1c530b9af1fb50e0a0517f333e61389b0b710f0c99566347450c + checksum: 10/be6680366cd560331bbbed500d1b793a2033136ebc6bcc3efab96219675170ed4eca4cc6739d996b800523fcbe42eb09ef7992ec5b514aa207a2d4c1b63a2408 languageName: node linkType: hard @@ -3003,28 +3003,28 @@ __metadata: languageName: node linkType: hard -"@tsed/normalize-path@npm:>=8.18.0": - version: 8.18.2 - resolution: "@tsed/normalize-path@npm:8.18.2" +"@tsed/normalize-path@npm:>=8.20.0": + version: 8.20.0 + resolution: "@tsed/normalize-path@npm:8.20.0" dependencies: normalize-path: "npm:3.0.0" tslib: "npm:2.7.0" - checksum: 10/59609f694e53d99e71ec054664372a79f892f097c3f003afbaaaf1defc9ec1fc5d27dc23e9c01d18c063b083f6882d771595a1b1f706d537d4d3f5b26221ab67 + checksum: 10/1de000359b56e181a6a3172289945904d756a111019ea201a970b7b2473f1896be83fc1dcfbb8640988535ab78107d4f7eb7202ff0250f812e379807b7fad710 languageName: node linkType: hard -"@tsed/openspec@npm:8.18.2, @tsed/openspec@npm:>=8.18.0": - version: 8.18.2 - resolution: "@tsed/openspec@npm:8.18.2" - checksum: 10/2ed970acb36c88bf600a3064ba65931a8a2830f1975eb14790b309077a628c2cac2a7dde0c86d94cc52993c782d105fadf70cd9faf842783bdbd8642d7a2c019 +"@tsed/openspec@npm:8.20.0, @tsed/openspec@npm:>=8.20.0": + version: 8.20.0 + resolution: "@tsed/openspec@npm:8.20.0" + checksum: 10/30bbc14515b57b32edf1a0efb40de20d4bd11c07f5ca789518c0604253bf99015f871851eec26564e55abb4394216ccc8961d0370fa109de5b80365c2d793794 languageName: node linkType: hard -"@tsed/schema@npm:>=8.18.0": - version: 8.18.2 - resolution: "@tsed/schema@npm:8.18.2" +"@tsed/schema@npm:>=8.20.0": + version: 8.20.0 + resolution: "@tsed/schema@npm:8.20.0" dependencies: - "@tsed/openspec": "npm:8.18.2" + "@tsed/openspec": "npm:8.20.0" change-case: "npm:^5.4.4" json-schema: "npm:0.4.0" picomatch: "npm:4.0.2" @@ -3039,7 +3039,7 @@ __metadata: optional: false "@tsed/openspec": optional: false - checksum: 10/fdb9c120e9afa12e62f6466a4046457e916e612a7b9dbb2e7bbaafb685b96d19dfd5a0ebc098af1754e673f23471432062727cf173731241774d4b112c2775cb + checksum: 10/e4e74d6024406aba7b324c6c861d164f7f611c658ba180f90833bf83d2665b615a43857114ce809d22e7741ec70253e6d8a3db1709b0ea50d945e0b8d3a58f55 languageName: node linkType: hard From 619d9174010083891d30c88ce1b739118d99d3cc Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 23 Dec 2025 14:45:41 +0100 Subject: [PATCH 12/18] refactor(cli): unify feature keys under `orm` and update TypeORM integration logic - Replaced `db` references with `orm` throughout CLI to standardize terminology. - Updated TypeORM integration to use `orm:typeorm` and adjusted templates, prompts, and tests accordingly. - Improved consistency in feature mapping and selection mechanisms. --- .../tests/init.integration.spec.ts | 2 +- .../src/hooks/TypeORMInitHook.ts | 4 +- .../src/templates/datasource.template.ts | 2 +- .../init/init.integration.spec.ts | 2 +- .../commands/init/config/FeaturesPrompt.ts | 58 ++++++------- .../init/mappers/mapToContext.spec.ts | 84 ++++++++++--------- .../init/prompts/getFeaturesPrompt.spec.ts | 58 ++++++------- 7 files changed, 107 insertions(+), 103 deletions(-) diff --git a/packages/cli-plugin-prisma/tests/init.integration.spec.ts b/packages/cli-plugin-prisma/tests/init.integration.spec.ts index 558402352..707cfde96 100644 --- a/packages/cli-plugin-prisma/tests/init.integration.spec.ts +++ b/packages/cli-plugin-prisma/tests/init.integration.spec.ts @@ -46,7 +46,7 @@ describe("Prisma: Init cmd", () => { db: true, typeorm: true, mysql: true, - features: [FeatureType.DB, FeatureType.PRISMA], + features: [FeatureType.ORM, FeatureType.PRISMA], srcDir: "src", pnpm: false, npm: false, diff --git a/packages/cli-plugin-typeorm/src/hooks/TypeORMInitHook.ts b/packages/cli-plugin-typeorm/src/hooks/TypeORMInitHook.ts index fce7cd82d..633d6b0c0 100644 --- a/packages/cli-plugin-typeorm/src/hooks/TypeORMInitHook.ts +++ b/packages/cli-plugin-typeorm/src/hooks/TypeORMInitHook.ts @@ -4,7 +4,7 @@ import {injectable} from "@tsed/di"; import {pascalCase} from "change-case"; function getDatabase(ctx: RenderDataContext): CliDatabases | undefined { - return ctx.features?.find((type) => type.includes("typeorm:"))?.split(":")[1] as CliDatabases; + return ctx.features?.find((type) => type.includes("orm:typeorm:"))?.split(":")[2] as CliDatabases; } export class TypeORMInitHook implements CliCommandHooks { @@ -38,7 +38,7 @@ export class TypeORMInitHook implements CliCommandHooks { return tasks; } - const typeormDataSource = data.features?.find((value) => value.startsWith("typeorm:")); + const typeormDataSource = data.features?.find((value) => value.startsWith("orm:typeorm:")); return [ ...tasks, diff --git a/packages/cli-plugin-typeorm/src/templates/datasource.template.ts b/packages/cli-plugin-typeorm/src/templates/datasource.template.ts index 9f7c56a0d..625f93c38 100644 --- a/packages/cli-plugin-typeorm/src/templates/datasource.template.ts +++ b/packages/cli-plugin-typeorm/src/templates/datasource.template.ts @@ -78,7 +78,7 @@ export default defineTemplate({ name: "typeormDataSource", message: "Which database type?", when(state: any) { - return state.type === "typeorm:datasource"; + return state.type === "orm:typeorm:datasource"; }, source: (state: any, keyword: string) => { if (keyword) { diff --git a/packages/cli-plugin-typeorm/test/integrations/init/init.integration.spec.ts b/packages/cli-plugin-typeorm/test/integrations/init/init.integration.spec.ts index 3635623ac..57c913b9e 100644 --- a/packages/cli-plugin-typeorm/test/integrations/init/init.integration.spec.ts +++ b/packages/cli-plugin-typeorm/test/integrations/init/init.integration.spec.ts @@ -39,7 +39,7 @@ describe("TypeORM: Init cmd", () => { db: true, typeorm: true, mysql: true, - features: [FeatureType.DB, FeatureType.TYPEORM, FeatureType.TYPEORM_MYSQL], + features: [FeatureType.ORM, FeatureType.TYPEORM, FeatureType.TYPEORM_MYSQL], srcDir: "src", pnpm: false, npm: false, diff --git a/packages/cli/src/commands/init/config/FeaturesPrompt.ts b/packages/cli/src/commands/init/config/FeaturesPrompt.ts index f4dea246b..fa6618c11 100644 --- a/packages/cli/src/commands/init/config/FeaturesPrompt.ts +++ b/packages/cli/src/commands/init/config/FeaturesPrompt.ts @@ -18,7 +18,7 @@ export enum FeatureType { PASSPORTJS = "passportjs", CONFIG = "config", COMMANDS = "commands", - DB = "db", + ORM = "orm", DOC = "doc", // CONFIG @@ -33,38 +33,38 @@ export enum FeatureType { CONFIG_POSTGRES = "config:postgres:premium", // DOC - SWAGGER = "swagger", - SCALAR = "scalar", + SWAGGER = "doc:swagger", + SCALAR = "doc:scalar", // ORM - PRISMA = "prisma", - MONGOOSE = "mongoose", + PRISMA = "orm:prisma", + MONGOOSE = "orm:mongoose", // TYPEORM - TYPEORM = "typeorm", - TYPEORM_MYSQL = "typeorm:mysql", - TYPEORM_MARIADB = "typeorm:mariadb", - TYPEORM_POSTGRES = "typeorm:postgres", - TYPEORM_COCKROACHDB = "typeorm:cockroachdb", - TYPEORM_SQLITE = "typeorm:sqlite", - TYPEORM_BETTER_SQLITE3 = "typeorm:better-sqlite3", - TYPEORM_CORDOVA = "typeorm:cordova", - TYPEORM_NATIVESCRIPT = "typeorm:nativescript", - TYPEORM_ORACLE = "typeorm:oracle", - TYPEORM_MSSQL = "typeorm:mssql", - TYPEORM_MONGODB = "typeorm:mongodb", - TYPEORM_SQLJS = "typeorm:sqljs", - TYPEORM_REACTNATIVE = "typeorm:reactnative", - TYPEORM_EXPO = "typeorm:expo", + TYPEORM = "orm:typeorm", + TYPEORM_MYSQL = "orm:typeorm:mysql", + TYPEORM_MARIADB = "orm:typeorm:mariadb", + TYPEORM_POSTGRES = "orm:typeorm:postgres", + TYPEORM_COCKROACHDB = "orm:typeorm:cockroachdb", + TYPEORM_SQLITE = "orm:typeorm:sqlite", + TYPEORM_BETTER_SQLITE3 = "orm:typeorm:better-sqlite3", + TYPEORM_CORDOVA = "orm:typeorm:cordova", + TYPEORM_NATIVESCRIPT = "orm:typeorm:nativescript", + TYPEORM_ORACLE = "orm:typeorm:oracle", + TYPEORM_MSSQL = "orm:typeorm:mssql", + TYPEORM_MONGODB = "orm:typeorm:mongodb", + TYPEORM_SQLJS = "orm:typeorm:sqljs", + TYPEORM_REACTNATIVE = "orm:typeorm:reactnative", + TYPEORM_EXPO = "orm:typeorm:expo", // TESTING & LINTER TESTING = "testing", - JEST = "jest", - VITEST = "vitest", + JEST = "testing:jest", + VITEST = "testing:vitest", LINTER = "linter", - ESLINT = "eslint", - LINT_STAGED = "lintstaged", - PRETTIER = "prettier" + ESLINT = "linter:eslint", + LINT_STAGED = "linter:lintstaged", + PRETTIER = "linter:prettier" } export const FeaturesMap: Record = { @@ -92,7 +92,7 @@ export const FeaturesMap: Record = { [FeatureType.DOC]: { name: "Documentation" }, - [FeatureType.DB]: { + [FeatureType.ORM]: { name: "Database" }, [FeatureType.PASSPORTJS]: { @@ -419,11 +419,11 @@ export const FeaturesPrompt = (availableRuntimes: string[], availablePackageMana { type: "checkbox", name: "features", - message: "Check the features needed for your project", + message: "Choose the features needed for your project", choices: [ FeatureType.CONFIG, FeatureType.GRAPHQL, - FeatureType.DB, + FeatureType.ORM, FeatureType.PASSPORTJS, FeatureType.SOCKETIO, FeatureType.DOC, @@ -461,7 +461,7 @@ export const FeaturesPrompt = (availableRuntimes: string[], availablePackageMana message: "Choose a ORM manager", type: "list", name: "featuresDB", - when: hasFeature(FeatureType.DB), + when: hasFeature(FeatureType.ORM), choices: [FeatureType.PRISMA, FeatureType.MONGOOSE, FeatureType.TYPEORM] }, { diff --git a/packages/cli/src/commands/init/mappers/mapToContext.spec.ts b/packages/cli/src/commands/init/mappers/mapToContext.spec.ts index e4cd2c218..589b1d58b 100644 --- a/packages/cli/src/commands/init/mappers/mapToContext.spec.ts +++ b/packages/cli/src/commands/init/mappers/mapToContext.spec.ts @@ -14,7 +14,7 @@ describe("mapToContext", () => { convention: ProjectConvention.ANGULAR, features: [ FeatureType.GRAPHQL, - FeatureType.DB, + FeatureType.ORM, FeatureType.PASSPORTJS, FeatureType.SOCKETIO, FeatureType.SWAGGER, @@ -29,44 +29,48 @@ describe("mapToContext", () => { packageManager: PackageManager.PNPM, runtime: "node" }); - expect(result).toEqual({ - architecture: "feature", - commands: true, - convention: "angular", - premium: true, - db: true, - features: [ - "graphql", - "db", - "passportjs", - "socketio", - "swagger", - "oidc", - "testing", - "linter", - "commands", - "config:postgres:premium", - "typeorm", - "typeorm:mariadb" - ], - graphql: true, - linter: true, - mariadb: true, - oidc: true, - postgres: true, - config: true, - configPostgres: true, - packageManager: "pnpm", - passportjs: true, - platform: "koa", - projectName: "name", - root: ".", - socketio: true, - swagger: true, - testing: true, - typeorm: true, - runtime: "node", - typeormMariadb: true - }); + expect(result).toMatchInlineSnapshot(` + { + "architecture": "feature", + "commands": true, + "config": true, + "configPostgres": true, + "convention": "angular", + "doc": true, + "docSwagger": true, + "features": [ + "graphql", + "orm", + "passportjs", + "socketio", + "doc:swagger", + "oidc", + "testing", + "linter", + "commands", + "config:postgres:premium", + "orm:typeorm", + "orm:typeorm:mariadb", + ], + "graphql": true, + "linter": true, + "oidc": true, + "orm": true, + "ormTypeorm": true, + "ormTypeormMariadb": true, + "packageManager": "pnpm", + "passportjs": true, + "platform": "koa", + "postgres": true, + "premium": true, + "projectName": "name", + "root": ".", + "runtime": "node", + "socketio": true, + "swagger": true, + "testing": true, + "typeorm": true, + } + `); }); }); diff --git a/packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts b/packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts index c099a0b96..98f0b0ec9 100644 --- a/packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts +++ b/packages/cli/src/commands/init/prompts/getFeaturesPrompt.spec.ts @@ -82,10 +82,6 @@ describe("getFeaturesPrompt", () => { "name": "Configuration sources", "value": "config", }, - { - "name": "Database", - "value": "db", - }, { "name": "Documentation", "value": "doc", @@ -111,6 +107,10 @@ describe("getFeaturesPrompt", () => { "name": "OpenID Connect provider", "value": "oidc", }, + { + "name": "Database", + "value": "orm", + }, { "devDependencies": { "@tsed/cli-plugin-passport": "{{cliVersion}}", @@ -137,7 +137,7 @@ describe("getFeaturesPrompt", () => { "value": "testing", }, ], - "message": "Check the features needed for your project", + "message": "Choose the features needed for your project", "name": "features", "type": "checkbox", }, @@ -224,14 +224,14 @@ describe("getFeaturesPrompt", () => { "@tsed/swagger": "{{tsedVersion}}", }, "name": "Swagger", - "value": "swagger", + "value": "doc:swagger", }, { "dependencies": { "@tsed/scalar": "{{tsedVersion}}", }, "name": "Scalar", - "value": "scalar", + "value": "doc:scalar", }, ], "message": "Choose a documentation plugin", @@ -246,14 +246,14 @@ describe("getFeaturesPrompt", () => { "@tsed/cli-plugin-prisma": "{{cliVersion}}", }, "name": "Prisma", - "value": "prisma", + "value": "orm:prisma", }, { "devDependencies": { "@tsed/cli-plugin-mongoose": "{{cliVersion}}", }, "name": "Mongoose", - "value": "mongoose", + "value": "orm:mongoose", }, { "devDependencies": { @@ -261,7 +261,7 @@ describe("getFeaturesPrompt", () => { "typeorm": "latest", }, "name": "TypeORM", - "value": "typeorm", + "value": "orm:typeorm", }, ], "message": "Choose a ORM manager", @@ -276,83 +276,83 @@ describe("getFeaturesPrompt", () => { "mysql2": "latest", }, "name": "MySQL", - "value": "typeorm:mysql", + "value": "orm:typeorm:mysql", }, { "dependencies": { "mariadb": "latest", }, "name": "MariaDB", - "value": "typeorm:mariadb", + "value": "orm:typeorm:mariadb", }, { "dependencies": { "pg": "latest", }, "name": "Postgres", - "value": "typeorm:postgres", + "value": "orm:typeorm:postgres", }, { "dependencies": { "cockroachdb": "latest", }, "name": "CockRoachDB", - "value": "typeorm:cockroachdb", + "value": "orm:typeorm:cockroachdb", }, { "dependencies": { "sqlite3": "latest", }, "name": "SQLite", - "value": "typeorm:sqlite", + "value": "orm:typeorm:sqlite", }, { "dependencies": { "better-sqlite3": "latest", }, "name": "Better SQLite3", - "value": "typeorm:better-sqlite3", + "value": "orm:typeorm:better-sqlite3", }, { "name": "Cordova", - "value": "typeorm:cordova", + "value": "orm:typeorm:cordova", }, { "name": "NativeScript", - "value": "typeorm:nativescript", + "value": "orm:typeorm:nativescript", }, { "dependencies": { "oracledb": "latest", }, "name": "Oracle", - "value": "typeorm:oracle", + "value": "orm:typeorm:oracle", }, { "dependencies": { "mssql": "latest", }, "name": "MsSQL", - "value": "typeorm:mssql", + "value": "orm:typeorm:mssql", }, { "dependencies": { "mongodb": "latest", }, "name": "MongoDB", - "value": "typeorm:mongodb", + "value": "orm:typeorm:mongodb", }, { "name": "SQL.js", - "value": "typeorm:sqljs", + "value": "orm:typeorm:sqljs", }, { "name": "ReactNative", - "value": "typeorm:reactnative", + "value": "orm:typeorm:reactnative", }, { "name": "Expo", - "value": "typeorm:expo", + "value": "orm:typeorm:expo", }, ], "message": "Which TypeORM you want to install?", @@ -373,14 +373,14 @@ describe("getFeaturesPrompt", () => { "@tsed/cli-plugin-vitest": "{{cliVersion}}", }, "name": "Vitest", - "value": "vitest", + "value": "testing:vitest", }, { "devDependencies": { "@tsed/cli-plugin-jest": "{{cliVersion}}", }, "name": "Jest (unstable with ESM)", - "value": "jest", + "value": "testing:jest", }, ], "message": "Choose unit framework", @@ -396,7 +396,7 @@ describe("getFeaturesPrompt", () => { "@tsed/cli-plugin-eslint": "{{cliVersion}}", }, "name": "EsLint", - "value": "eslint", + "value": "linter:eslint", }, ], "message": "Choose linter tools framework", @@ -408,11 +408,11 @@ describe("getFeaturesPrompt", () => { "choices": [ { "name": "Prettier", - "value": "prettier", + "value": "linter:prettier", }, { "name": "Lint on commit", - "value": "lintstaged", + "value": "linter:lintstaged", }, ], "message": "Choose extra linter tools", From 6f1eec0f742e5b93795cd153624cea1d16c5aed4 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 23 Dec 2025 14:52:27 +0100 Subject: [PATCH 13/18] refactor(cli-core): enhance schema metadata handling and improve command option consistency - Updated `getCommandMetadata` to unify handling of `args` and `options`, removing unused `allowUnknownOption` parameter. - Introduced `x-opt` metadata key in the schema for improved flexibility and custom parsing. - Added enhancements to `JsonSchema` with helpers like `prompt`, `opt`, `when`, and `choices`. - Adjusted tests to reflect revised `args` mapping and schema extensions. - Minor improvements in `CommandOptions` typings and schema definitions for clarity and maintainability. --- packages/cli-core/src/decorators/command.ts | 4 +- packages/cli-core/src/fn/command.spec.ts | 4 +- packages/cli-core/src/fn/command.ts | 39 ++++++++++++++++++- .../cli-core/src/interfaces/CommandOptions.ts | 6 ++- .../src/utils/getCommandMetadata.spec.ts | 4 +- .../cli-core/src/utils/getCommandMetadata.ts | 17 ++++---- packages/cli/src/interfaces/InitCmdOptions.ts | 2 +- 7 files changed, 56 insertions(+), 20 deletions(-) diff --git a/packages/cli-core/src/decorators/command.ts b/packages/cli-core/src/decorators/command.ts index 2f3d72591..52694d103 100644 --- a/packages/cli-core/src/decorators/command.ts +++ b/packages/cli-core/src/decorators/command.ts @@ -1,7 +1,7 @@ import {command} from "../fn/command.js"; -import type {CommandOptions} from "../interfaces/CommandOptions.js"; +import type {BaseCommandOptions} from "../interfaces/CommandOptions.js"; -export function Command(options: CommandOptions): ClassDecorator { +export function Command(options: BaseCommandOptions): ClassDecorator { return (token) => { command({...options, token}); }; diff --git a/packages/cli-core/src/fn/command.spec.ts b/packages/cli-core/src/fn/command.spec.ts index 8e262690a..6e4680e17 100644 --- a/packages/cli-core/src/fn/command.spec.ts +++ b/packages/cli-core/src/fn/command.spec.ts @@ -26,7 +26,7 @@ describe("command()", () => { const handler: CommandProvider["$exec"] = vi.fn(); const prompt: CommandProvider["$prompt"] = vi.fn(); - const options: CommandOptions = { + const options: CommandOptions = { name: "test", description: "description", handler, @@ -59,7 +59,7 @@ describe("command()", () => { $exec(): any {} } - const options: CommandOptions = { + const options: CommandOptions = { name: "test-class", description: "description", token: TestCommand diff --git a/packages/cli-core/src/fn/command.ts b/packages/cli-core/src/fn/command.ts index 129a4d576..1bfccae2a 100644 --- a/packages/cli-core/src/fn/command.ts +++ b/packages/cli-core/src/fn/command.ts @@ -1,9 +1,46 @@ import type {Type} from "@tsed/core"; -import {type FactoryTokenProvider, injectable, type TokenProvider} from "@tsed/di"; +import {type FactoryTokenProvider, injectable} from "@tsed/di"; +import {JsonSchema} from "@tsed/schema"; import type {CommandOptions} from "../interfaces/CommandOptions.js"; import type {CommandProvider} from "../interfaces/index.js"; +type SchemaChoice = { + label: string; + value: string; + checked?: ((ctx: any) => boolean) | boolean; + items?: SchemaChoice[]; +}; + +declare module "@tsed/schema" { + interface JsonSchema { + prompt(label: string): this; + + opt(value: string): this; + + when(fn: (ctx: any) => boolean): this; + + choices(value: SchemaChoice[]): this; + } +} + +JsonSchema.add("prompt", function prompt(label: string) { + this.customKey("x-label", label); + return this; +}) + .add("when", function when(fn: (ctx: any) => boolean) { + this.customKey("x-when", fn); + return this; + }) + .add("opt", function opt(v: string) { + this.customKey("x-opt", v); + return this; + }) + .add("choices", function choices(choices: SchemaChoice[]) { + this.customKey("x-choices", choices); + return this; + }); + export function command(options: CommandOptions) { if (!options.token) { return injectable>>(Symbol.for(`COMMAND_${options.name}`) as any) diff --git a/packages/cli-core/src/interfaces/CommandOptions.ts b/packages/cli-core/src/interfaces/CommandOptions.ts index 2c1607498..8327f1734 100644 --- a/packages/cli-core/src/interfaces/CommandOptions.ts +++ b/packages/cli-core/src/interfaces/CommandOptions.ts @@ -89,6 +89,8 @@ export interface BaseCommandOptions { enableFeatures?: string[]; disableReadUpPkg?: boolean; + + bindLogger?: boolean; } interface FunctionalCommandOptions extends BaseCommandOptions { @@ -98,8 +100,8 @@ interface FunctionalCommandOptions extends BaseCommandOptions { [key: string]: any; } -interface ClassCommandOptions extends BaseCommandOptions { - token: TokenProvider>; +export interface ClassCommandOptions extends BaseCommandOptions { + token: TokenProvider; [key: string]: any; } diff --git a/packages/cli-core/src/utils/getCommandMetadata.spec.ts b/packages/cli-core/src/utils/getCommandMetadata.spec.ts index 36ee01e03..8ab1576fa 100644 --- a/packages/cli-core/src/utils/getCommandMetadata.spec.ts +++ b/packages/cli-core/src/utils/getCommandMetadata.spec.ts @@ -60,7 +60,7 @@ describe("getCommandMetadata", () => { // enrich properties with metadata used by getCommandMetadata const props = schema.get("properties") as any; - props.filename.set("arg", "file").set("description", "The file to process").set("default", "index.ts"); + props.filename.set("arg", "filename").set("description", "The file to process").set("default", "index.ts"); props.verbose.set("opt", "--verbose").set("description", "Enable verbose mode").set("default", false); props.list.set("opt", "--list").set("description", "List of items"); @@ -80,7 +80,7 @@ describe("getCommandMetadata", () => { // args mapped from property with `arg` expect(metadata.args).toEqual( expect.objectContaining({ - file: expect.objectContaining({ + filename: expect.objectContaining({ description: "The file to process", defaultValue: "index.ts", type: String diff --git a/packages/cli-core/src/utils/getCommandMetadata.ts b/packages/cli-core/src/utils/getCommandMetadata.ts index 77a33940c..206c02f4f 100644 --- a/packages/cli-core/src/utils/getCommandMetadata.ts +++ b/packages/cli-core/src/utils/getCommandMetadata.ts @@ -10,7 +10,6 @@ export function getCommandMetadata(token: TokenProvider): CommandMetadata { name, alias, args = {}, - allowUnknownOption, description, options = {}, enableFeatures, @@ -32,21 +31,19 @@ export function getCommandMetadata(token: TokenProvider): CommandMetadata { required: schema.isRequired(propertyKey) }; - const opt = propertySchema.get("opt"); + const opt = propertySchema.get("x-opt"); if (opt) { options[opt] = { ...base, customParser: schema.get("custom-parser") } satisfies CommandOpts; - } - - const arg = propertySchema.get("arg"); - - if (arg) { - args[arg] = base satisfies CommandArg; + } else { + args[propertyKey] = base satisfies CommandArg; } }); + + opts.allowUnknownOption = !!schema.get("additionalProperties"); } return { @@ -56,10 +53,10 @@ export function getCommandMetadata(token: TokenProvider): CommandMetadata { args, description, options, - allowUnknownOption: !!allowUnknownOption, enableFeatures: enableFeatures || [], disableReadUpPkg: !!disableReadUpPkg, bindLogger, - ...opts + ...opts, + allowUnknownOption: !!opts.allowUnknownOption }; } diff --git a/packages/cli/src/interfaces/InitCmdOptions.ts b/packages/cli/src/interfaces/InitCmdOptions.ts index e75601a43..1e4532a0b 100644 --- a/packages/cli/src/interfaces/InitCmdOptions.ts +++ b/packages/cli/src/interfaces/InitCmdOptions.ts @@ -3,7 +3,7 @@ import type {RenderDataContext} from "./RenderDataContext.js"; export interface InitOptions extends RenderDataContext { root: string; srcDir: string; - skipPrompt: boolean; + skipPrompt?: boolean; GH_TOKEN?: string; } From 966e4b060b643ee09e6b4205fef5249b0de49e1f Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 23 Dec 2025 15:24:57 +0100 Subject: [PATCH 14/18] feat(cli-core): add input validation via `inputSchema` for commands - Introduced `inputSchema` in `CommandMetadata` for command input validation. - Integrated schema validation logic in `createCommand` to verify inputs before lifecycle execution. - Enhanced tests to cover valid and invalid input scenarios during command execution. --- .../cli-core/src/services/CliService.spec.ts | 88 +++++++++++++++++++ packages/cli-core/src/services/CliService.ts | 23 ++++- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/packages/cli-core/src/services/CliService.spec.ts b/packages/cli-core/src/services/CliService.spec.ts index 0feef57f6..b4f996e48 100644 --- a/packages/cli-core/src/services/CliService.spec.ts +++ b/packages/cli-core/src/services/CliService.spec.ts @@ -2,8 +2,10 @@ import {CliPlatformTest} from "@tsed/cli-testing"; import {Store} from "@tsed/core"; import {DIContext, injector, logger} from "@tsed/di"; +import {s} from "@tsed/schema"; import {Command} from "commander"; +import type {CommandMetadata} from "../interfaces/CommandMetadata.js"; import {CliService} from "./CliService.js"; describe("CliService", () => { @@ -166,4 +168,90 @@ describe("CliService", () => { expect(() => service["build"](duplicateProvider)).toThrow("The generate command is already registered"); }); + + it("should validate command inputs via inputSchema before running lifecycle", async () => { + const service = await CliPlatformTest.invoke(CliService); + const schema = s.object({ + blueprint: s.string().required(), + feature: s.string().required(), + count: s.number().required() + }); + const metadata: CommandMetadata = { + name: "generate", + description: "Generate something", + alias: undefined, + args: { + blueprint: { + description: "Blueprint name", + required: true, + type: String + } + }, + options: {}, + allowUnknownOption: false, + enableFeatures: [], + disableReadUpPkg: false, + bindLogger: true, + inputSchema: schema + }; + + const cmd = service.createCommand(metadata); + const runLifecycle = vi.spyOn(service, "runLifecycle").mockResolvedValue(undefined as never); + + service.program.args = ["controller"]; + cmd.args = ["generate", "controller"]; + cmd.setOptionValue("feature", "rest"); + cmd.setOptionValue("count", "3"); + + await (cmd as any)._actionHandler(["controller"]); + + expect(runLifecycle).toHaveBeenCalledTimes(1); + const [, data, ctx] = runLifecycle.mock.calls[0]; + expect(data).toEqual( + expect.objectContaining({ + blueprint: "controller", + feature: "rest", + count: 3, + verbose: false, + rawArgs: ["controller"] + }) + ); + expect(ctx).toBeInstanceOf(DIContext); + }); + + it("should report validation errors and skip lifecycle when inputSchema fails", async () => { + const service = await CliPlatformTest.invoke(CliService); + const schema = s.object({ + feature: s.string().required() + }); + const metadata: CommandMetadata = { + name: "deploy", + description: "Deploy something", + alias: undefined, + args: {}, + options: {}, + allowUnknownOption: false, + enableFeatures: [], + disableReadUpPkg: false, + bindLogger: true, + inputSchema: schema + }; + const cmd = service.createCommand(metadata); + const runLifecycle = vi.spyOn(service, "runLifecycle").mockResolvedValue(undefined as never); + const log = logger(); + const errorSpy = vi.spyOn(log, "error").mockReturnValue(undefined as never); + + cmd.args = ["deploy"]; + service.program.args = []; + + expect(() => (cmd as any)._actionHandler(["missing-required"])).toThrow("Validation error"); + + expect(runLifecycle).not.toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith({ + event: "VALIDATION_ERROR", + errors: expect.any(Array) + }); + + errorSpy.mockRestore(); + }); }); diff --git a/packages/cli-core/src/services/CliService.ts b/packages/cli-core/src/services/CliService.ts index 22005b3f7..12ffea3ed 100644 --- a/packages/cli-core/src/services/CliService.ts +++ b/packages/cli-core/src/services/CliService.ts @@ -1,4 +1,4 @@ -import {classOf} from "@tsed/core"; +import {classOf, isArrowFn} from "@tsed/core"; import { configuration, constant, @@ -28,7 +28,7 @@ import type {Task} from "../interfaces/index.js"; import {PackageManagersModule} from "../packageManagers/index.js"; import {createSubTasks, createTasksRunner} from "../utils/createTasksRunner.js"; import {getCommandMetadata} from "../utils/getCommandMetadata.js"; -import {mapCommanderOptions} from "../utils/index.js"; +import {mapCommanderOptions, validate} from "../utils/index.js"; import {mapCommanderArgs} from "../utils/mapCommanderArgs.js"; import {parseOption} from "../utils/parseOption.js"; import {CliHooks} from "./CliHooks.js"; @@ -185,7 +185,7 @@ export class CliService { } public createCommand(metadata: CommandMetadata) { - const {args, name, options, description, alias, allowUnknownOption} = metadata; + const {args, name, options, description, alias, allowUnknownOption, inputSchema} = metadata; if (this.commands.has(name)) { return this.commands.get(name).command; @@ -201,7 +201,7 @@ export class CliService { ); const allOpts = mapCommanderOptions(commandName, this.program.commands); - const data: CommandData = { + let data: CommandData = { ...allOpts, verbose: !!this.program.opts().verbose, ...mappedArgs, @@ -209,6 +209,21 @@ export class CliService { rawArgs }; + if (inputSchema) { + const {isValid, errors, value} = validate(data, isArrowFn(inputSchema) ? inputSchema() : inputSchema); + + if (isValid) { + data = value; + } else { + logger().error({ + event: "VALIDATION_ERROR", + errors + }); + + throw new Error("Validation error"); + } + } + const $ctx = new DIContext({ id: v4(), injector: injector(), From 16ae8da6708e7684d7f29794dbce6205342f8843 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 23 Dec 2025 15:33:33 +0100 Subject: [PATCH 15/18] refactor(cli): replace `InitFileSchema` with `InitSchema` for enhanced input validation and extensibility - Migrated from `InitFileSchema` to `InitSchema` in the init command for improved schema flexibility and better prompt-driven configuration. - Updated references and adjusted schema definitions to support dynamic prompts and richer options. --- packages/cli/src/commands/init/InitCmd.ts | 4 +- .../commands/init/config/InitFileSchema.ts | 29 -- .../src/commands/init/config/InitSchema.ts | 338 ++++++++++++++++++ 3 files changed, 340 insertions(+), 31 deletions(-) delete mode 100644 packages/cli/src/commands/init/config/InitFileSchema.ts create mode 100644 packages/cli/src/commands/init/config/InitSchema.ts diff --git a/packages/cli/src/commands/init/InitCmd.ts b/packages/cli/src/commands/init/InitCmd.ts index 25214c55e..6c943155f 100644 --- a/packages/cli/src/commands/init/InitCmd.ts +++ b/packages/cli/src/commands/init/InitCmd.ts @@ -37,7 +37,7 @@ import {BunRuntime} from "../../runtimes/supports/BunRuntime.js"; import {NodeRuntime} from "../../runtimes/supports/NodeRuntime.js"; import {CliProjectService} from "../../services/CliProjectService.js"; import {FeaturesMap, FeatureType} from "./config/FeaturesPrompt.js"; -import {InitFileSchema} from "./config/InitFileSchema.js"; +import {InitSchema} from "./config/InitSchema.js"; import {mapToContext} from "./mappers/mapToContext.js"; import {getFeaturesPrompt} from "./prompts/getFeaturesPrompt.js"; @@ -98,7 +98,7 @@ export class InitCmd implements CommandProvider { initialOptions = { ...initialOptions, - ...(await this.cliLoadFile.loadFile(file, InitFileSchema)) + ...(await this.cliLoadFile.loadFile(file, InitSchema())) }; } diff --git a/packages/cli/src/commands/init/config/InitFileSchema.ts b/packages/cli/src/commands/init/config/InitFileSchema.ts deleted file mode 100644 index b2a977830..000000000 --- a/packages/cli/src/commands/init/config/InitFileSchema.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {PackageManager} from "@tsed/cli-core"; -import {s} from "@tsed/schema"; - -import {PlatformType, ProjectConvention} from "../../../interfaces/index.js"; -import {FeatureType} from "./FeaturesPrompt.js"; - -export const InitFileSchema = s - .object({ - tsedVersion: s.string().optional().description("The CLI will use the given tsed version to generate the project"), - projectName: s.string().optional().description("The project name"), - platform: s - .string() - .enum(PlatformType) - .default(PlatformType.EXPRESS) - .description("Node.js framework used to run server (Express, Koa, Fastify)"), - convention: s - .string() - .enum(ProjectConvention) - .default(ProjectConvention.DEFAULT) - .description("Project convention (Ts.ED or Angular style)"), - packageManager: s.string().enum(PackageManager).default(PackageManager.NPM).description("Used project manager to install dependencies"), - runtime: s - .string() - .enum("node", "babel", "swc", "webpack", "bun") - .description("The javascript runtime used to start application (node, node + webpack, node + swc, node + babel, bun)"), - features: s.array().items(s.string().enum(FeatureType)).required().minItems(1).description("List of feature to create the projet"), - skipPrompt: s.boolean().default(false).description("Skip the prompt") - }) - .unknown(); diff --git a/packages/cli/src/commands/init/config/InitSchema.ts b/packages/cli/src/commands/init/config/InitSchema.ts new file mode 100644 index 000000000..d7fb9e931 --- /dev/null +++ b/packages/cli/src/commands/init/config/InitSchema.ts @@ -0,0 +1,338 @@ +import {PackageManager, PackageManagersModule} from "@tsed/cli-core"; +import {inject} from "@tsed/di"; +import {s} from "@tsed/schema"; + +import {DEFAULT_TSED_TAGS} from "../../../constants/index.js"; +import {ArchitectureConvention, PlatformType, ProjectConvention} from "../../../interfaces/index.js"; +import type {RuntimeTypes} from "../../../interfaces/RuntimeTypes.js"; +import {RuntimesModule} from "../../../runtimes/RuntimesModule.js"; +import {FeaturesMap, FeatureType} from "../../init/config/FeaturesPrompt.js"; + +export const InitSchema = () => + s + .object({ + root: s.string().required().description("Current working directory to initialize Ts.ED project"), + projectName: s + .string() + .optional() + .prompt("What is your project name") + .when((ctx) => ctx.root !== ".") + .description("Set the project name. By default, the project is the same as the name directory.") + .opt("-n, --project-name "), + platform: s + .enums(PlatformType) + .default(PlatformType.EXPRESS) + .prompt("Choose the target Framework:") + .choices([ + { + label: "Express.js", + value: PlatformType.EXPRESS, + checked: (options: any) => options.platform === PlatformType.EXPRESS || !options.platform // todo maybe it can be infered by default() and item value() + }, + { + label: "Koa.js", + value: PlatformType.KOA, + checked: (options: any) => options.platform === PlatformType.KOA || !options.platform + }, + { + label: "Fastify.js (beta)", + value: PlatformType.KOA, + checked: (options: any) => options.platform === PlatformType.KOA || !options.platform + } + ]) + .description("Set the default platform for Ts.ED (Express.js, Koa.js or Fastify.js)") + .opt("-p, --platform "), + architecture: s + .string() + .enum(ArchitectureConvention) + .default(ArchitectureConvention.DEFAULT) + .prompt("Choose the architecture for your project:") + .description("Architecture convention for tree directory") + .choices([ + { + label: "Ts.ED", + value: ArchitectureConvention.DEFAULT + }, + { + label: "Feature", + value: ArchitectureConvention.FEATURE + } + ]) + .opt("-a, --arch "), + convention: s + .enums(ProjectConvention) + .default(ProjectConvention.DEFAULT) + .prompt("Choose the file naming convention:") + .description("Set the default file naming convention (Ts.ED, Angular).") + .choices([ + { + label: "Ts.ED", + value: ProjectConvention.DEFAULT + }, + { + label: "Angular", + value: ProjectConvention.ANGULAR + } + ]) + .opt("-c, --convention "), + features: s + .array() + .items(s.enums(FeatureType)) + .prompt("Choose the features needed for your project") + .description("List of features to enable (swagger, graphql, prisma, etc.).") + .choices( + [ + { + label: "Commands", + value: FeatureType.COMMANDS + }, + { + label: "Configuration sources", + value: FeatureType.CONFIG, + items: [ + { + label: "Envs", + value: FeatureType.CONFIG_ENVS + }, + { + label: "Dotenv", + value: FeatureType.CONFIG_DOTENV + }, + { + label: "JSON", + value: FeatureType.CONFIG_JSON + }, + { + label: "YAML", + value: FeatureType.CONFIG_YAML + }, + { + label: "AWS Secrets Manager (Premium)", + value: FeatureType.CONFIG_AWS_SECRETS + }, + { + label: "IORedis (Premium)", + value: FeatureType.CONFIG_IOREDIS + }, + { + label: "MongoDB (Premium)", + value: FeatureType.CONFIG_MONGO + }, + { + label: "Vault (Premium)", + value: FeatureType.CONFIG_VAULT + }, + { + label: "Postgres (Premium)", + value: FeatureType.CONFIG_POSTGRES + } + ] + }, + { + label: "ORM", + value: FeatureType.ORM, + items: [ + { + label: "Prisma", + value: FeatureType.PRISMA + }, + { + label: "Mongoose", + value: FeatureType.MONGOOSE + }, + { + label: "TypeORM", + value: FeatureType.TYPEORM, + items: [ + { + label: "MySQL", + value: FeatureType.TYPEORM_MYSQL + }, + { + label: "MariaDB", + value: "db:typeorm:mariadb" + }, + { + label: "Postgres", + value: "db:typeorm:postgres" + }, + { + label: "CockRoachDB", + value: "db:typeorm:cockroachdb" + }, + { + label: "SQLite", + value: "db:typeorm:sqlite" + }, + { + label: "Better SQLite3", + value: "db:typeorm:better-sqlite3" + }, + { + label: "Cordova", + value: "db:typeorm:cordova" + }, + { + label: "NativeScript", + value: "db:typeorm:nativescript" + }, + { + label: "Oracle", + value: "db:typeorm:oracle" + }, + { + label: "MsSQL", + value: "db:typeorm:mssql" + }, + { + label: "MongoDB", + value: "db:typeorm:mongodb" + }, + { + label: "SQL.js", + value: "db:typeorm:sqljs" + }, + { + label: "ReactNative", + value: "db:typeorm:reactnative" + }, + { + label: "Expo", + value: "db:typeorm:expo" + } + ] + } + ] + }, + { + label: "Documentation", + value: "doc", + items: [ + { + label: "Swagger", + value: "doc:swagger" + }, + { + label: "Scalar", + value: "doc:scalar" + } + ] + }, + { + label: "TypeGraphQL", + value: "graphql", + items: [] + }, + { + label: "Linter", + value: "linter", + items: [ + { + label: "EsLint", + value: "linter:eslint" + }, + { + label: "Prettier", + value: "linter:prettier" + }, + { + label: "Lint on commit", + value: "linter:lintstaged" + } + ] + }, + { + label: "OpenID Connect provider", + value: "oidc", + items: [] + }, + { + label: "Passport.js", + value: "passportjs", + items: [] + }, + { + label: "Socket.io", + value: "socketio", + items: [] + }, + { + label: "Testing", + value: "testing", + items: [ + { + label: "Vitest", + value: "testing:vitest" + }, + { + label: "Jest (unstable with ESM)", + value: "testing:jest" + } + ] + } + ] + // Object.values(FeatureType) + // .filter((value) => !value.includes(":")) + // .sort() + // .map((key) => { + // if (FeaturesMap[key]) { + // return { + // label: FeaturesMap[key]!.name, + // value: key, + // items: Object.entries(FeaturesMap) + // .filter(([value]) => { + // return value.startsWith(key + ":"); + // }) + // .map(([value, feature]) => ({ + // label: feature.name, + // value: value + // })) + // }; + // } + // }) + // .filter(Boolean) as {label: string; value: string}[] + ) + .opt("--features "), + runtime: s + .enums(inject(RuntimesModule).list() as any[]) + .default("node") + .description("Runtime (node, bun, ...).") + .opt("--runtime "), + packageManager: s + .enums(inject(PackageManagersModule).list() as any[]) + .default(PackageManager.NPM) + .choices([ + { + label: FeaturesMap[PackageManager.NPM]!.name, + value: PackageManager.NPM + }, + { + label: FeaturesMap[PackageManager.YARN_BERRY]!.name, + value: PackageManager.YARN_BERRY + }, + { + label: FeaturesMap[PackageManager.PNPM]!.name, + value: PackageManager.PNPM + } + ]) + .description("Package manager (npm, pnpm, yarn, bun).") + .opt("-m, --package-manager "), + GH_TOKEN: s + .string() + .optional() + .description( + "GitHub token to install premium plugins. For example config:aws_secrets:premium or all features endings by `:premium` needs a GH_TOKEN" + ) + .opt("--gh-token "), + tsedVersion: s + .string() + .optional() + .default(DEFAULT_TSED_TAGS) + .description("Use a specific version of Ts.ED (format: x.x.x).") + .opt("-t, --tsed-version "), + file: s.string().optional().description("Location of a file in which the features are defined.").opt("-f, --file "), + skipPrompt: s.boolean().optional().default(false).description("Skip the prompt installation").opt("-s, --skip-prompt") + }) + .unknown(); + +// export type InitOptions = s.infer>; From a2457013e9ceaef5908ed600d240af40549b6067 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 23 Dec 2025 16:25:14 +0100 Subject: [PATCH 16/18] chore: bump @tsed dependencies to v8.20.1 and update related configurations - Updated `package.json` and `yarn.lock` files to use `@tsed` packages >=8.20.1. - Synced MCP-related dependencies and configurations with the new version. --- package.json | 10 +- .../src/interfaces/CommandMetadata.ts | 36 +- .../cli-core/src/services/CliService.spec.ts | 30 +- packages/cli-core/src/services/CliService.ts | 8 +- .../src/utils/getCommandMetadata.spec.ts | 21 +- .../cli-core/src/utils/getCommandMetadata.ts | 64 +-- packages/cli-mcp/package.json | 12 +- packages/cli/package.json | 12 +- packages/cli/src/commands/init/InitCmd.ts | 102 +---- .../src/commands/init/config/InitSchema.ts | 402 +++++++++--------- yarn.lock | 84 ++-- 11 files changed, 343 insertions(+), 438 deletions(-) diff --git a/package.json b/package.json index 05aa61624..94b6eb759 100644 --- a/package.json +++ b/package.json @@ -44,13 +44,13 @@ }, "homepage": "https://github.com/tsedio/tsed-cli", "dependencies": { - "@tsed/core": ">=8.20.0", - "@tsed/di": ">=8.20.0", + "@tsed/core": ">=8.20.1", + "@tsed/di": ">=8.20.1", "@tsed/logger": ">=8.0.3", "@tsed/logger-std": ">=8.0.3", - "@tsed/normalize-path": ">=8.20.0", - "@tsed/openspec": ">=8.20.0", - "@tsed/schema": ">=8.20.0", + "@tsed/normalize-path": ">=8.20.1", + "@tsed/openspec": ">=8.20.1", + "@tsed/schema": ">=8.20.1", "axios": "^1.7.7", "chalk": "^5.3.0", "commander": "^12.1.0", diff --git a/packages/cli-core/src/interfaces/CommandMetadata.ts b/packages/cli-core/src/interfaces/CommandMetadata.ts index 70212616c..8f72c4910 100644 --- a/packages/cli-core/src/interfaces/CommandMetadata.ts +++ b/packages/cli-core/src/interfaces/CommandMetadata.ts @@ -1,24 +1,24 @@ import type {BaseCommandOptions, CommandArg, CommandOpts} from "./CommandOptions.js"; -export interface CommandMetadata extends BaseCommandOptions { - /** - * CommandProvider arguments - */ - args: { - [key: string]: CommandArg; - }; - /** - * CommandProvider options - */ - options: { - [key: string]: CommandOpts; - }; - - allowUnknownOption?: boolean; - +export interface CommandMetadata extends Omit, "args" | "options" | "allowUnknownOption"> { enableFeatures: string[]; - disableReadUpPkg: boolean; - bindLogger: boolean; + + getOptions(): { + /** + * CommandProvider arguments + */ + args: { + [key: string]: CommandArg; + }; + /** + * CommandProvider options + */ + options: { + [key: string]: CommandOpts; + }; + + allowUnknownOption?: boolean; + }; } diff --git a/packages/cli-core/src/services/CliService.spec.ts b/packages/cli-core/src/services/CliService.spec.ts index b4f996e48..6a0962453 100644 --- a/packages/cli-core/src/services/CliService.spec.ts +++ b/packages/cli-core/src/services/CliService.spec.ts @@ -180,15 +180,19 @@ describe("CliService", () => { name: "generate", description: "Generate something", alias: undefined, - args: { - blueprint: { - description: "Blueprint name", - required: true, - type: String - } + getOptions() { + return { + args: { + blueprint: { + description: "Blueprint name", + required: true, + type: String + } + }, + options: {}, + allowUnknownOption: false + }; }, - options: {}, - allowUnknownOption: false, enableFeatures: [], disableReadUpPkg: false, bindLogger: true, @@ -228,9 +232,13 @@ describe("CliService", () => { name: "deploy", description: "Deploy something", alias: undefined, - args: {}, - options: {}, - allowUnknownOption: false, + getOptions() { + return { + args: {}, + options: {}, + allowUnknownOption: false + }; + }, enableFeatures: [], disableReadUpPkg: false, bindLogger: true, diff --git a/packages/cli-core/src/services/CliService.ts b/packages/cli-core/src/services/CliService.ts index 12ffea3ed..ea9ef1f42 100644 --- a/packages/cli-core/src/services/CliService.ts +++ b/packages/cli-core/src/services/CliService.ts @@ -185,20 +185,22 @@ export class CliService { } public createCommand(metadata: CommandMetadata) { - const {args, name, options, description, alias, allowUnknownOption, inputSchema} = metadata; + const {name, description, alias, inputSchema} = metadata; if (this.commands.has(name)) { return this.commands.get(name).command; } - let cmd = this.program.command(name); + const {args, options, allowUnknownOption} = metadata.getOptions(); const onAction = (commandName: string) => { const [, ...rawArgs] = cmd.args; + const mappedArgs = mapCommanderArgs( args, this.program.args.filter((arg) => commandName === arg) ); + const allOpts = mapCommanderOptions(commandName, this.program.commands); let data: CommandData = { @@ -241,6 +243,8 @@ export class CliService { return this.runLifecycle(name, data, $ctx); }; + let cmd = this.program.command(name); + if (alias) { cmd = cmd.alias(alias); } diff --git a/packages/cli-core/src/utils/getCommandMetadata.spec.ts b/packages/cli-core/src/utils/getCommandMetadata.spec.ts index 8ab1576fa..58c34beff 100644 --- a/packages/cli-core/src/utils/getCommandMetadata.spec.ts +++ b/packages/cli-core/src/utils/getCommandMetadata.spec.ts @@ -20,7 +20,11 @@ class TestCmd2 {} describe("getCommandMetadata", () => { it("should return command metadata", () => { - expect(getCommandMetadata(TestCmd)).toEqual({ + const {getOptions, ...result} = getCommandMetadata(TestCmd); + expect({ + ...result, + ...getOptions() + }).toEqual({ args: {}, allowUnknownOption: false, description: "description", @@ -33,8 +37,13 @@ describe("getCommandMetadata", () => { options: {}, token: TestCmd }); - - expect(getCommandMetadata(TestCmd2)).toEqual({ + }); + it("should return command metadata (2)", () => { + const {getOptions, ...result} = getCommandMetadata(TestCmd2); + expect({ + ...result, + ...getOptions() + }).toEqual({ args: {}, allowUnknownOption: false, description: "description", @@ -78,7 +87,7 @@ describe("getCommandMetadata", () => { expect(metadata.inputSchema).toBe(schema); // args mapped from property with `arg` - expect(metadata.args).toEqual( + expect(metadata.getOptions().args).toEqual( expect.objectContaining({ filename: expect.objectContaining({ description: "The file to process", @@ -89,7 +98,7 @@ describe("getCommandMetadata", () => { ); // options mapped from properties with `opt`, including customParser from root schema - expect(metadata.options).toEqual( + expect(metadata.getOptions().options).toEqual( expect.objectContaining({ "--verbose": expect.objectContaining({ description: "Enable verbose mode", @@ -124,7 +133,7 @@ describe("getCommandMetadata", () => { expect(metadata.inputSchema).toBeTypeOf("function"); // getCommandMetadata resolves the function and maps options - expect(metadata.options).toEqual( + expect(metadata.getOptions().options).toEqual( expect.objectContaining({ "-c": expect.objectContaining({ description: "Count", diff --git a/packages/cli-core/src/utils/getCommandMetadata.ts b/packages/cli-core/src/utils/getCommandMetadata.ts index 206c02f4f..7cda362fe 100644 --- a/packages/cli-core/src/utils/getCommandMetadata.ts +++ b/packages/cli-core/src/utils/getCommandMetadata.ts @@ -19,44 +19,48 @@ export function getCommandMetadata(token: TokenProvider): CommandMetadata { ...opts } = Store.from(token)?.get("command") as CommandOptions; - if (inputSchema) { - const schema = isArrowFn(inputSchema) ? inputSchema() : inputSchema; - - Object.entries(schema.get("properties") || {})?.forEach(([propertyKey, propertySchema]) => { - const base = { - type: propertySchema.getTarget(), - itemType: propertySchema.isCollection ? propertySchema.get("items").getTarget() : undefined, - description: propertySchema.get("description") || "", - defaultValue: propertySchema.get("default"), - required: schema.isRequired(propertyKey) - }; - - const opt = propertySchema.get("x-opt"); - - if (opt) { - options[opt] = { - ...base, - customParser: schema.get("custom-parser") - } satisfies CommandOpts; - } else { - args[propertyKey] = base satisfies CommandArg; - } - }); - - opts.allowUnknownOption = !!schema.get("additionalProperties"); - } - return { name, inputSchema, alias, - args, description, - options, enableFeatures: enableFeatures || [], disableReadUpPkg: !!disableReadUpPkg, bindLogger, ...opts, - allowUnknownOption: !!opts.allowUnknownOption + getOptions() { + if (inputSchema) { + const schema = isArrowFn(inputSchema) ? inputSchema() : inputSchema; + + Object.entries(schema.get("properties") || {})?.forEach(([propertyKey, propertySchema]) => { + const base = { + type: propertySchema.getTarget(), + itemType: propertySchema.isCollection ? propertySchema.get("items").getTarget() : undefined, + description: propertySchema.get("description") || "", + defaultValue: propertySchema.get("default"), + required: schema.isRequired(propertyKey) + }; + + const opt = propertySchema.get("x-opt"); + + if (opt) { + options[opt] = { + ...base, + customParser: schema.get("custom-parser") + } satisfies CommandOpts; + } else { + args[propertyKey] = base satisfies CommandArg; + } + }); + + opts.allowUnknownOption = !!schema.get("additionalProperties"); + } + + return { + args, + options, + allowUnknownOption: !!opts.allowUnknownOption + }; + } }; } diff --git a/packages/cli-mcp/package.json b/packages/cli-mcp/package.json index 63e159b43..a4556af97 100644 --- a/packages/cli-mcp/package.json +++ b/packages/cli-mcp/package.json @@ -51,14 +51,14 @@ "@swc/core": "1.7.26", "@swc/helpers": "^0.5.13", "@tsed/cli-core": "workspace:*", - "@tsed/core": ">=8.20.0", - "@tsed/di": ">=8.20.0", - "@tsed/hooks": ">=8.20.0", + "@tsed/core": ">=8.20.1", + "@tsed/di": ">=8.20.1", + "@tsed/hooks": ">=8.20.1", "@tsed/logger": ">=8.0.3", "@tsed/logger-std": ">=8.0.3", - "@tsed/normalize-path": ">=8.20.0", - "@tsed/openspec": ">=8.20.0", - "@tsed/schema": ">=8.20.0", + "@tsed/normalize-path": ">=8.20.1", + "@tsed/openspec": ">=8.20.1", + "@tsed/schema": ">=8.20.1", "chalk": "^5.3.0", "change-case": "^5.4.4", "consolidate": "^1.0.4", diff --git a/packages/cli/package.json b/packages/cli/package.json index 4ce19739b..1f60c8d40 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,14 +52,14 @@ "@swc/helpers": "^0.5.13", "@tsed/cli-core": "workspace:*", "@tsed/cli-mcp": "workspace:*", - "@tsed/core": ">=8.20.0", - "@tsed/di": ">=8.20.0", - "@tsed/hooks": ">=8.20.0", + "@tsed/core": ">=8.20.1", + "@tsed/di": ">=8.20.1", + "@tsed/hooks": ">=8.20.1", "@tsed/logger": ">=8.0.3", "@tsed/logger-std": ">=8.0.3", - "@tsed/normalize-path": ">=8.20.0", - "@tsed/openspec": ">=8.20.0", - "@tsed/schema": ">=8.20.0", + "@tsed/normalize-path": ">=8.20.1", + "@tsed/openspec": ">=8.20.1", + "@tsed/schema": ">=8.20.1", "chalk": "^5.3.0", "change-case": "^5.4.4", "consolidate": "^1.0.4", diff --git a/packages/cli/src/commands/init/InitCmd.ts b/packages/cli/src/commands/init/InitCmd.ts index 6c943155f..e25ef2ecb 100644 --- a/packages/cli/src/commands/init/InitCmd.ts +++ b/packages/cli/src/commands/init/InitCmd.ts @@ -12,7 +12,6 @@ import { createSubTasks, createTasksRunner, inject, - PackageManager, PackageManagersModule, ProjectPackageJson, type QuestionOptions, @@ -23,7 +22,7 @@ import {constant} from "@tsed/di"; import {$asyncAlter} from "@tsed/hooks"; import {kebabCase} from "change-case"; -import {DEFAULT_TSED_TAGS, TEMPLATE_DIR} from "../../constants/index.js"; +import {TEMPLATE_DIR} from "../../constants/index.js"; import {exec} from "../../fn/exec.js"; import {render} from "../../fn/render.js"; import {taskOutput} from "../../fn/taskOutput.js"; @@ -54,44 +53,6 @@ export class InitCmd implements CommandProvider { protected execa = inject(CliExeca); protected fs = inject(CliFs); - checkPrecondition(ctx: InitOptions) { - const isValid = (types: any, value: any) => (value ? Object.values(types).includes(value) : true); - - if (!isValid(PlatformType, ctx.platform)) { - throw new Error(`Invalid selected platform: ${ctx.platform}. Possible values: ${Object.values(PlatformType).join(", ")}.`); - } - - if (!isValid(ArchitectureConvention, ctx.architecture)) { - throw new Error( - `Invalid selected architecture: ${ctx.architecture}. Possible values: ${Object.values(ArchitectureConvention).join(", ")}.` - ); - } - - if (!isValid(ProjectConvention, ctx.convention)) { - throw new Error(`Invalid selected convention: ${ctx.convention}. Possible values: ${Object.values(ProjectConvention).join(", ")}.`); - } - - const runtimes = this.runtimes.list(); - if (!runtimes.includes(ctx.runtime)) { - throw new Error(`Invalid selected runtime: ${ctx.runtime}. Possible values: ${runtimes.join(", ")}.`); - } - - const managers = this.packageManagers.list(); - if (!managers.includes(ctx.packageManager)) { - throw new Error(`Invalid selected package manager: ${ctx.packageManager}. Possible values: ${managers.join(", ")}.`); - } - - if (ctx.features) { - ctx.features.forEach((value) => { - const feature = FeaturesMap[value.toLowerCase()]; - - if (!feature) { - throw new Error(`Invalid selected feature: ${value}. Possible values: ${Object.values(FeatureType).join(", ")}.`); - } - }); - } - } - async $prompt(initialOptions: Partial): Promise { if (initialOptions.file) { const file = join(this.packageJson.cwd, initialOptions.file); @@ -196,7 +157,6 @@ export class InitCmd implements CommandProvider { ctx ); - this.checkPrecondition(ctx); const runtime = this.runtimes.get(); ctx = { @@ -444,64 +404,6 @@ command({ token: InitCmd, name: "init", description: "Init a new Ts.ED project", - args: { - root: { - type: String, - defaultValue: ".", - description: "Root directory to initialize the Ts.ED project" - } - }, - options: { - "-n, --project-name ": { - type: String, - defaultValue: "", - description: "Set the project name. By default, the project is the same as the name directory." - }, - "-a, --arch ": { - type: String, - defaultValue: ArchitectureConvention.DEFAULT, - description: `Set the default architecture convention (${ArchitectureConvention.DEFAULT} or ${ArchitectureConvention.FEATURE})` - }, - "-c, --convention ": { - type: String, - defaultValue: ProjectConvention.DEFAULT, - description: `Set the default project convention (${ArchitectureConvention.DEFAULT} or ${ArchitectureConvention.FEATURE})` - }, - "-p, --platform ": { - type: String, - defaultValue: PlatformType.EXPRESS, - description: "Set the default platform for Ts.ED (express, koa or fastify)" - }, - "--features ": { - type: Array, - itemType: String, - defaultValue: [], - description: "List of the Ts.ED features." - }, - "--runtime ": { - itemType: String, - defaultValue: "node", - description: "The default runtime used to run the project" - }, - "-m, --package-manager ": { - itemType: String, - defaultValue: PackageManager.YARN, - description: "The default package manager to install the project" - }, - "-t, --tsed-version ": { - type: String, - defaultValue: DEFAULT_TSED_TAGS, - description: "Use a specific version of Ts.ED (format: 5.x.x)." - }, - "-f, --file ": { - type: String, - description: "Location of a file in which the features are defined." - }, - "-s, --skip-prompt": { - type: Boolean, - defaultValue: false, - description: "Skip the prompt." - } - }, + inputSchema: InitSchema, disableReadUpPkg: true }); diff --git a/packages/cli/src/commands/init/config/InitSchema.ts b/packages/cli/src/commands/init/config/InitSchema.ts index d7fb9e931..fb45e2360 100644 --- a/packages/cli/src/commands/init/config/InitSchema.ts +++ b/packages/cli/src/commands/init/config/InitSchema.ts @@ -80,218 +80,196 @@ export const InitSchema = () => .items(s.enums(FeatureType)) .prompt("Choose the features needed for your project") .description("List of features to enable (swagger, graphql, prisma, etc.).") - .choices( - [ - { - label: "Commands", - value: FeatureType.COMMANDS - }, - { - label: "Configuration sources", - value: FeatureType.CONFIG, - items: [ - { - label: "Envs", - value: FeatureType.CONFIG_ENVS - }, - { - label: "Dotenv", - value: FeatureType.CONFIG_DOTENV - }, - { - label: "JSON", - value: FeatureType.CONFIG_JSON - }, - { - label: "YAML", - value: FeatureType.CONFIG_YAML - }, - { - label: "AWS Secrets Manager (Premium)", - value: FeatureType.CONFIG_AWS_SECRETS - }, - { - label: "IORedis (Premium)", - value: FeatureType.CONFIG_IOREDIS - }, - { - label: "MongoDB (Premium)", - value: FeatureType.CONFIG_MONGO - }, - { - label: "Vault (Premium)", - value: FeatureType.CONFIG_VAULT - }, - { - label: "Postgres (Premium)", - value: FeatureType.CONFIG_POSTGRES - } - ] - }, - { - label: "ORM", - value: FeatureType.ORM, - items: [ - { - label: "Prisma", - value: FeatureType.PRISMA - }, - { - label: "Mongoose", - value: FeatureType.MONGOOSE - }, - { - label: "TypeORM", - value: FeatureType.TYPEORM, - items: [ - { - label: "MySQL", - value: FeatureType.TYPEORM_MYSQL - }, - { - label: "MariaDB", - value: "db:typeorm:mariadb" - }, - { - label: "Postgres", - value: "db:typeorm:postgres" - }, - { - label: "CockRoachDB", - value: "db:typeorm:cockroachdb" - }, - { - label: "SQLite", - value: "db:typeorm:sqlite" - }, - { - label: "Better SQLite3", - value: "db:typeorm:better-sqlite3" - }, - { - label: "Cordova", - value: "db:typeorm:cordova" - }, - { - label: "NativeScript", - value: "db:typeorm:nativescript" - }, - { - label: "Oracle", - value: "db:typeorm:oracle" - }, - { - label: "MsSQL", - value: "db:typeorm:mssql" - }, - { - label: "MongoDB", - value: "db:typeorm:mongodb" - }, - { - label: "SQL.js", - value: "db:typeorm:sqljs" - }, - { - label: "ReactNative", - value: "db:typeorm:reactnative" - }, - { - label: "Expo", - value: "db:typeorm:expo" - } - ] - } - ] - }, - { - label: "Documentation", - value: "doc", - items: [ - { - label: "Swagger", - value: "doc:swagger" - }, - { - label: "Scalar", - value: "doc:scalar" - } - ] - }, - { - label: "TypeGraphQL", - value: "graphql", - items: [] - }, - { - label: "Linter", - value: "linter", - items: [ - { - label: "EsLint", - value: "linter:eslint" - }, - { - label: "Prettier", - value: "linter:prettier" - }, - { - label: "Lint on commit", - value: "linter:lintstaged" - } - ] - }, - { - label: "OpenID Connect provider", - value: "oidc", - items: [] - }, - { - label: "Passport.js", - value: "passportjs", - items: [] - }, - { - label: "Socket.io", - value: "socketio", - items: [] - }, - { - label: "Testing", - value: "testing", - items: [ - { - label: "Vitest", - value: "testing:vitest" - }, - { - label: "Jest (unstable with ESM)", - value: "testing:jest" - } - ] - } - ] - // Object.values(FeatureType) - // .filter((value) => !value.includes(":")) - // .sort() - // .map((key) => { - // if (FeaturesMap[key]) { - // return { - // label: FeaturesMap[key]!.name, - // value: key, - // items: Object.entries(FeaturesMap) - // .filter(([value]) => { - // return value.startsWith(key + ":"); - // }) - // .map(([value, feature]) => ({ - // label: feature.name, - // value: value - // })) - // }; - // } - // }) - // .filter(Boolean) as {label: string; value: string}[] - ) + .choices([ + { + label: "Commands", + value: FeatureType.COMMANDS + }, + { + label: "Configuration sources", + value: FeatureType.CONFIG, + items: [ + { + label: "Envs", + value: FeatureType.CONFIG_ENVS + }, + { + label: "Dotenv", + value: FeatureType.CONFIG_DOTENV + }, + { + label: "JSON", + value: FeatureType.CONFIG_JSON + }, + { + label: "YAML", + value: FeatureType.CONFIG_YAML + }, + { + label: "AWS Secrets Manager (Premium)", + value: FeatureType.CONFIG_AWS_SECRETS + }, + { + label: "IORedis (Premium)", + value: FeatureType.CONFIG_IOREDIS + }, + { + label: "MongoDB (Premium)", + value: FeatureType.CONFIG_MONGO + }, + { + label: "Vault (Premium)", + value: FeatureType.CONFIG_VAULT + }, + { + label: "Postgres (Premium)", + value: FeatureType.CONFIG_POSTGRES + } + ] + }, + { + label: "ORM", + value: FeatureType.ORM, + items: [ + { + label: "Prisma", + value: FeatureType.PRISMA + }, + { + label: "Mongoose", + value: FeatureType.MONGOOSE + }, + { + label: "TypeORM", + value: FeatureType.TYPEORM, + items: [ + { + label: "MySQL", + value: FeatureType.TYPEORM_MYSQL + }, + { + label: "MariaDB", + value: "db:typeorm:mariadb" + }, + { + label: "Postgres", + value: "db:typeorm:postgres" + }, + { + label: "CockRoachDB", + value: "db:typeorm:cockroachdb" + }, + { + label: "SQLite", + value: "db:typeorm:sqlite" + }, + { + label: "Better SQLite3", + value: "db:typeorm:better-sqlite3" + }, + { + label: "Cordova", + value: "db:typeorm:cordova" + }, + { + label: "NativeScript", + value: "db:typeorm:nativescript" + }, + { + label: "Oracle", + value: "db:typeorm:oracle" + }, + { + label: "MsSQL", + value: "db:typeorm:mssql" + }, + { + label: "MongoDB", + value: "db:typeorm:mongodb" + }, + { + label: "SQL.js", + value: "db:typeorm:sqljs" + }, + { + label: "ReactNative", + value: "db:typeorm:reactnative" + }, + { + label: "Expo", + value: "db:typeorm:expo" + } + ] + } + ] + }, + { + label: "Documentation", + value: "doc", + items: [ + { + label: "Swagger", + value: "doc:swagger" + }, + { + label: "Scalar", + value: "doc:scalar" + } + ] + }, + { + label: "TypeGraphQL", + value: "graphql", + items: [] + }, + { + label: "Linter", + value: "linter", + items: [ + { + label: "EsLint", + value: "linter:eslint" + }, + { + label: "Prettier", + value: "linter:prettier" + }, + { + label: "Lint on commit", + value: "linter:lintstaged" + } + ] + }, + { + label: "OpenID Connect provider", + value: "oidc", + items: [] + }, + { + label: "Passport.js", + value: "passportjs", + items: [] + }, + { + label: "Socket.io", + value: "socketio", + items: [] + }, + { + label: "Testing", + value: "testing", + items: [ + { + label: "Vitest", + value: "testing:vitest" + }, + { + label: "Jest (unstable with ESM)", + value: "testing:jest" + } + ] + } + ]) .opt("--features "), runtime: s .enums(inject(RuntimesModule).list() as any[]) diff --git a/yarn.lock b/yarn.lock index b93a0c545..ef83dd21a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2532,14 +2532,14 @@ __metadata: "@swc/core": "npm:1.7.26" "@swc/helpers": "npm:^0.5.13" "@tsed/cli-core": "workspace:*" - "@tsed/core": "npm:>=8.20.0" - "@tsed/di": "npm:>=8.20.0" - "@tsed/hooks": "npm:>=8.20.0" + "@tsed/core": "npm:>=8.20.1" + "@tsed/di": "npm:>=8.20.1" + "@tsed/hooks": "npm:>=8.20.1" "@tsed/logger": "npm:>=8.0.3" "@tsed/logger-std": "npm:>=8.0.3" - "@tsed/normalize-path": "npm:>=8.20.0" - "@tsed/openspec": "npm:>=8.20.0" - "@tsed/schema": "npm:>=8.20.0" + "@tsed/normalize-path": "npm:>=8.20.1" + "@tsed/openspec": "npm:>=8.20.1" + "@tsed/schema": "npm:>=8.20.1" "@tsed/typescript": "workspace:*" "@types/change-case": "npm:^2.3.1" "@types/consolidate": "npm:0.14.4" @@ -2733,15 +2733,15 @@ __metadata: "@commitlint/config-conventional": "npm:19.5.0" "@swc/core": "npm:1.7.26" "@swc/helpers": "npm:0.5.13" - "@tsed/core": "npm:>=8.20.0" - "@tsed/di": "npm:>=8.20.0" + "@tsed/core": "npm:>=8.20.1" + "@tsed/di": "npm:>=8.20.1" "@tsed/logger": "npm:>=8.0.3" "@tsed/logger-std": "npm:>=8.0.3" "@tsed/markdown-it-symbols": "npm:3.20.8" "@tsed/monorepo-utils": "npm:3.0.0" - "@tsed/normalize-path": "npm:>=8.20.0" - "@tsed/openspec": "npm:>=8.20.0" - "@tsed/schema": "npm:>=8.20.0" + "@tsed/normalize-path": "npm:>=8.20.1" + "@tsed/openspec": "npm:>=8.20.1" + "@tsed/schema": "npm:>=8.20.1" "@tsed/ts-doc": "npm:^4.1.0" "@types/inquirer-autocomplete-prompt": "npm:3.0.3" "@types/node": "npm:22.7.4" @@ -2815,14 +2815,14 @@ __metadata: "@swc/helpers": "npm:^0.5.13" "@tsed/cli-core": "workspace:*" "@tsed/cli-mcp": "workspace:*" - "@tsed/core": "npm:>=8.20.0" - "@tsed/di": "npm:>=8.20.0" - "@tsed/hooks": "npm:>=8.20.0" + "@tsed/core": "npm:>=8.20.1" + "@tsed/di": "npm:>=8.20.1" + "@tsed/hooks": "npm:>=8.20.1" "@tsed/logger": "npm:>=8.0.3" "@tsed/logger-std": "npm:>=8.0.3" - "@tsed/normalize-path": "npm:>=8.20.0" - "@tsed/openspec": "npm:>=8.20.0" - "@tsed/schema": "npm:>=8.20.0" + "@tsed/normalize-path": "npm:>=8.20.1" + "@tsed/openspec": "npm:>=8.20.1" + "@tsed/schema": "npm:>=8.20.1" "@tsed/typescript": "workspace:*" "@types/change-case": "npm:^2.3.1" "@types/consolidate": "npm:0.14.4" @@ -2869,19 +2869,19 @@ __metadata: languageName: unknown linkType: soft -"@tsed/core@npm:>=8.20.0": - version: 8.20.0 - resolution: "@tsed/core@npm:8.20.0" +"@tsed/core@npm:>=8.20.1": + version: 8.20.1 + resolution: "@tsed/core@npm:8.20.1" dependencies: reflect-metadata: "npm:^0.2.2" tslib: "npm:2.7.0" - checksum: 10/beb02c18df256acfff04d3b52269105406e808b488d1a04f09343a6381f8988ba22c60c6028932e8b3146b4ab7113aa9ddd944f6d2026d37a6793789ea54906a + checksum: 10/1a2739886f7a2e51ece11d6d98bbb1c3092abd5bc8162569230d62b0cedf5565bcb09d3d20990265e8f04e9786c6ffb2e332a12feb1720d58d81f4c5423c687d languageName: node linkType: hard -"@tsed/di@npm:>=8.20.0": - version: 8.20.0 - resolution: "@tsed/di@npm:8.20.0" +"@tsed/di@npm:>=8.20.1": + version: 8.20.1 + resolution: "@tsed/di@npm:8.20.1" dependencies: tslib: "npm:2.7.0" uuid: "npm:^10.0.0" @@ -2896,17 +2896,17 @@ __metadata: optional: false "@tsed/logger": optional: false - checksum: 10/c13d1da707d4c0c47df0e6013d3a0886fbc2fa00976705bbd2193c611e763b2707a8b038f7f282c6fbf4c377eaf74659de98cfbed71fedc1744b4095738beb41 + checksum: 10/9e6a832dfd6c5bb8035b102938db36b5fd6114cabe3eb7bed1a72ad74cc9f81043e711809bc3b7831a84fba00b1902ed5170317a3171a806d3ffb3c4c822cfb1 languageName: node linkType: hard -"@tsed/hooks@npm:>=8.20.0": - version: 8.20.0 - resolution: "@tsed/hooks@npm:8.20.0" +"@tsed/hooks@npm:>=8.20.1": + version: 8.20.1 + resolution: "@tsed/hooks@npm:8.20.1" dependencies: reflect-metadata: "npm:^0.2.2" tslib: "npm:2.7.0" - checksum: 10/be6680366cd560331bbbed500d1b793a2033136ebc6bcc3efab96219675170ed4eca4cc6739d996b800523fcbe42eb09ef7992ec5b514aa207a2d4c1b63a2408 + checksum: 10/d833dfdd509c068f772e1c5c98a743af7edbd57c37db3e554a388fd434c3d423e2674145c3645ae9c3a75bd83d6ecd218420694331a539b4c51c9c08c2e84c44 languageName: node linkType: hard @@ -3003,28 +3003,28 @@ __metadata: languageName: node linkType: hard -"@tsed/normalize-path@npm:>=8.20.0": - version: 8.20.0 - resolution: "@tsed/normalize-path@npm:8.20.0" +"@tsed/normalize-path@npm:>=8.20.1": + version: 8.20.1 + resolution: "@tsed/normalize-path@npm:8.20.1" dependencies: normalize-path: "npm:3.0.0" tslib: "npm:2.7.0" - checksum: 10/1de000359b56e181a6a3172289945904d756a111019ea201a970b7b2473f1896be83fc1dcfbb8640988535ab78107d4f7eb7202ff0250f812e379807b7fad710 + checksum: 10/de8996ba992a3cb9620ae37704b1211d93699ee0f5fe60592bb6ee314965c57ed738faebbf88e0b0b6eef1a3074304c2892f7ec120a52219c9d506a3598130d4 languageName: node linkType: hard -"@tsed/openspec@npm:8.20.0, @tsed/openspec@npm:>=8.20.0": - version: 8.20.0 - resolution: "@tsed/openspec@npm:8.20.0" - checksum: 10/30bbc14515b57b32edf1a0efb40de20d4bd11c07f5ca789518c0604253bf99015f871851eec26564e55abb4394216ccc8961d0370fa109de5b80365c2d793794 +"@tsed/openspec@npm:8.20.1, @tsed/openspec@npm:>=8.20.1": + version: 8.20.1 + resolution: "@tsed/openspec@npm:8.20.1" + checksum: 10/4ac12ccf0f3c5d12f0df0c9a2032c821ff956395ff8139f33589a0e62183de789104657bd3b0d4270895579e8abd288ac5417adfcb1701abf100cd55f094b324 languageName: node linkType: hard -"@tsed/schema@npm:>=8.20.0": - version: 8.20.0 - resolution: "@tsed/schema@npm:8.20.0" +"@tsed/schema@npm:>=8.20.1": + version: 8.20.1 + resolution: "@tsed/schema@npm:8.20.1" dependencies: - "@tsed/openspec": "npm:8.20.0" + "@tsed/openspec": "npm:8.20.1" change-case: "npm:^5.4.4" json-schema: "npm:0.4.0" picomatch: "npm:4.0.2" @@ -3039,7 +3039,7 @@ __metadata: optional: false "@tsed/openspec": optional: false - checksum: 10/e4e74d6024406aba7b324c6c861d164f7f611c658ba180f90833bf83d2665b615a43857114ce809d22e7741ec70253e6d8a3db1709b0ea50d945e0b8d3a58f55 + checksum: 10/4678a4d18714b4e9d4c24fbc16534209f6e4d3113ff732b88009c49c53a91edc1b39f2766dcd4035519f80d8e5cbe3145ed4abe61f4843767c327aa45b63235b languageName: node linkType: hard From 20ddbe0cf6ac501a2342bcc769e236577ae45678 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 23 Dec 2025 16:31:58 +0100 Subject: [PATCH 17/18] feat(cli): add `init-options` command to display available init options for external tools - Implemented `InitOptionsCommand` to provide detailed options for the `init` command. - Replaced old `InitCmd.spec.ts` tests with a new structure and approach. - Updated commands registry to include `InitOptionsCommand`. --- packages/cli/src/commands/index.ts | 3 +- .../cli/src/commands/init/InitCmd.spec.ts | 139 ------------------ packages/cli/src/commands/init/InitCmd.ts | 4 +- .../cli/src/commands/init/InitOptionsCmd.ts | 25 ++++ .../src/commands/init/config/InitSchema.ts | 2 - 5 files changed, 28 insertions(+), 145 deletions(-) delete mode 100644 packages/cli/src/commands/init/InitCmd.spec.ts create mode 100644 packages/cli/src/commands/init/InitOptionsCmd.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 1ab882e00..0fb792c78 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -1,9 +1,10 @@ import {AddCmd} from "./add/AddCmd.js"; import {GenerateCmd} from "./generate/GenerateCmd.js"; import {InitCmd} from "./init/InitCmd.js"; +import {InitOptionsCommand} from "./init/InitOptionsCmd.js"; import {McpCommand} from "./mcp/McpCommand.js"; import {RunCmd} from "./run/RunCmd.js"; import {CreateTemplateCommand} from "./template/CreateTemplateCommand.js"; import {UpdateCmd} from "./update/UpdateCmd.js"; -export default [AddCmd, InitCmd, GenerateCmd, UpdateCmd, RunCmd, CreateTemplateCommand, McpCommand]; +export default [AddCmd, InitCmd, InitOptionsCommand, GenerateCmd, UpdateCmd, RunCmd, CreateTemplateCommand, McpCommand]; diff --git a/packages/cli/src/commands/init/InitCmd.spec.ts b/packages/cli/src/commands/init/InitCmd.spec.ts deleted file mode 100644 index 142aecf66..000000000 --- a/packages/cli/src/commands/init/InitCmd.spec.ts +++ /dev/null @@ -1,139 +0,0 @@ -import {PackageManagersModule} from "@tsed/cli-core"; -// @ts-ignore -import {CliPlatformTest} from "@tsed/cli-testing"; -import {catchError} from "@tsed/core"; - -import {InitCmd} from "../../../src/commands/init/InitCmd.js"; -import {RuntimesModule} from "../../runtimes/RuntimesModule.js"; - -async function getServiceFixture() { - const packageManagers = { - list: vi.fn().mockReturnValue([]) - }; - - const runtimes = { - list: vi.fn().mockReturnValue(["node"]) - }; - - const service = await CliPlatformTest.invoke(InitCmd, [ - { - token: PackageManagersModule, - use: packageManagers - }, - { - token: RuntimesModule, - use: runtimes - } - ]); - - return {service, packageManagers, runtimes}; -} - -describe("InitCmd", () => { - beforeEach(() => CliPlatformTest.create()); - afterEach(() => CliPlatformTest.reset()); - describe("checkPrecondition()", () => { - it("should throw error (platform)", async () => { - const {service} = await getServiceFixture(); - const result = catchError(() => { - service.checkPrecondition({ - platform: "wrong" - } as any); - }); - expect(result?.message).toEqual("Invalid selected platform: wrong. Possible values: express, koa, fastify."); - }); - - it("should throw error (architecture)", async () => { - const {service} = await getServiceFixture(); - - const result = catchError(() => { - service.checkPrecondition({ - architecture: "wrong" - } as any); - }); - expect(result?.message).toEqual("Invalid selected architecture: wrong. Possible values: arc_default, feature."); - }); - - it("should throw error (convention)", async () => { - const {service} = await getServiceFixture(); - - const result = catchError(() => { - service.checkPrecondition({ - convention: "wrong" - } as any); - }); - expect(result?.message).toEqual("Invalid selected convention: wrong. Possible values: conv_default, angular."); - }); - - it("should not throw error (package manager)", async () => { - const {service, packageManagers} = await getServiceFixture(); - - packageManagers.list.mockReturnValue(["npm"]); - - const result = catchError(() => { - service.checkPrecondition({ - runtime: "node", - packageManager: "npm" - } as any); - }); - - expect(result?.message).toEqual(undefined); - }); - - it("should throw error (package manager)", async () => { - const {service, packageManagers} = await getServiceFixture(); - - packageManagers.list.mockReturnValue(["yarn", "npm", "pnpm"]); - - const result = catchError(() => { - service.checkPrecondition({ - runtime: "node", - packageManager: "unknown" - } as any); - }); - expect(result?.message).toEqual("Invalid selected package manager: unknown. Possible values: yarn, npm, pnpm."); - }); - - it("should throw error (runtime)", async () => { - const {service, packageManagers} = await getServiceFixture(); - - packageManagers.list.mockReturnValue(["yarn", "npm", "pnpm"]); - - const result = catchError(() => { - service.checkPrecondition({ - packageManager: "unknown" - } as any); - }); - expect(result?.message).toEqual("Invalid selected runtime: undefined. Possible values: node."); - }); - - it("should throw error (runtime-2)", async () => { - const {service, packageManagers} = await getServiceFixture(); - - packageManagers.list.mockReturnValue(["yarn", "npm", "pnpm"]); - - const result = catchError(() => { - service.checkPrecondition({ - runtime: "unknown", - packageManager: "unknown" - } as any); - }); - expect(result?.message).toEqual("Invalid selected runtime: unknown. Possible values: node."); - }); - - it("should throw error (features)", async () => { - const {service, packageManagers} = await getServiceFixture(); - - packageManagers.list.mockReturnValue(["yarn", "npm", "pnpm"]); - - const result = catchError(() => { - service.checkPrecondition({ - packageManager: "yarn", - runtime: "node", - features: ["wrong"] - } as any); - }); - expect(result?.message).toContain("Invalid selected feature: wrong. Possible values: "); - }); - }); -}); diff --git a/packages/cli/src/commands/init/InitCmd.ts b/packages/cli/src/commands/init/InitCmd.ts index e25ef2ecb..582f2f981 100644 --- a/packages/cli/src/commands/init/InitCmd.ts +++ b/packages/cli/src/commands/init/InitCmd.ts @@ -26,10 +26,8 @@ import {TEMPLATE_DIR} from "../../constants/index.js"; import {exec} from "../../fn/exec.js"; import {render} from "../../fn/render.js"; import {taskOutput} from "../../fn/taskOutput.js"; -import {ArchitectureConvention} from "../../interfaces/ArchitectureConvention.js"; -import {type InitCmdContext, PlatformType} from "../../interfaces/index.js"; +import {type InitCmdContext} from "../../interfaces/index.js"; import type {InitOptions} from "../../interfaces/InitCmdOptions.js"; -import {ProjectConvention} from "../../interfaces/ProjectConvention.js"; import {PlatformsModule} from "../../platforms/PlatformsModule.js"; import {RuntimesModule} from "../../runtimes/RuntimesModule.js"; import {BunRuntime} from "../../runtimes/supports/BunRuntime.js"; diff --git a/packages/cli/src/commands/init/InitOptionsCmd.ts b/packages/cli/src/commands/init/InitOptionsCmd.ts new file mode 100644 index 000000000..93605a7a4 --- /dev/null +++ b/packages/cli/src/commands/init/InitOptionsCmd.ts @@ -0,0 +1,25 @@ +import {command} from "@tsed/cli-core"; +import {s} from "@tsed/schema"; + +import {InitSchema} from "./config/InitSchema.js"; + +export const InitOptionsCommand = command({ + name: "init-options", + description: "Display available options for Ts.ED init command for external tool", + disableReadUpPkg: true, + inputSchema: s.object({ + indent: s.number().default(0).description("Json indentation value").opt("-i, --indent ") + }), + handler(data) { + console.log( + JSON.stringify( + InitSchema().toJSON({ + useAlias: false, + customKeys: true + }), + null, + data.indent + ) + ); + } +}).token(); diff --git a/packages/cli/src/commands/init/config/InitSchema.ts b/packages/cli/src/commands/init/config/InitSchema.ts index fb45e2360..1d021b80e 100644 --- a/packages/cli/src/commands/init/config/InitSchema.ts +++ b/packages/cli/src/commands/init/config/InitSchema.ts @@ -312,5 +312,3 @@ export const InitSchema = () => skipPrompt: s.boolean().optional().default(false).description("Skip the prompt installation").opt("-s, --skip-prompt") }) .unknown(); - -// export type InitOptions = s.infer>; From 3a956accd71879133687dea38481278e14207665 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 24 Dec 2025 12:15:21 +0100 Subject: [PATCH 18/18] fix: bump @tsed dependencies to v8.21.0 and update related configurations - Updated `package.json` and `yarn.lock` files to use `@tsed` packages >=8.21.0. - Synced MCP-related dependencies and updated schema imports to align with the new version. - Adjusted MCP commands to utilize the revised `InitSchema` structure. --- package.json | 10 +-- packages/cli-mcp/package.json | 12 +-- packages/cli/package.json | 12 +-- packages/cli/src/commands/mcp/McpCommand.ts | 2 +- .../src/commands/mcp/schema/InitMCPSchema.ts | 42 ++-------- yarn.lock | 84 +++++++++---------- 6 files changed, 68 insertions(+), 94 deletions(-) diff --git a/package.json b/package.json index 94b6eb759..dc689a9d2 100644 --- a/package.json +++ b/package.json @@ -44,13 +44,13 @@ }, "homepage": "https://github.com/tsedio/tsed-cli", "dependencies": { - "@tsed/core": ">=8.20.1", - "@tsed/di": ">=8.20.1", + "@tsed/core": ">=8.21.0", + "@tsed/di": ">=8.21.0", "@tsed/logger": ">=8.0.3", "@tsed/logger-std": ">=8.0.3", - "@tsed/normalize-path": ">=8.20.1", - "@tsed/openspec": ">=8.20.1", - "@tsed/schema": ">=8.20.1", + "@tsed/normalize-path": ">=8.21.0", + "@tsed/openspec": ">=8.21.0", + "@tsed/schema": ">=8.21.0", "axios": "^1.7.7", "chalk": "^5.3.0", "commander": "^12.1.0", diff --git a/packages/cli-mcp/package.json b/packages/cli-mcp/package.json index a4556af97..a75899871 100644 --- a/packages/cli-mcp/package.json +++ b/packages/cli-mcp/package.json @@ -51,14 +51,14 @@ "@swc/core": "1.7.26", "@swc/helpers": "^0.5.13", "@tsed/cli-core": "workspace:*", - "@tsed/core": ">=8.20.1", - "@tsed/di": ">=8.20.1", - "@tsed/hooks": ">=8.20.1", + "@tsed/core": ">=8.21.0", + "@tsed/di": ">=8.21.0", + "@tsed/hooks": ">=8.21.0", "@tsed/logger": ">=8.0.3", "@tsed/logger-std": ">=8.0.3", - "@tsed/normalize-path": ">=8.20.1", - "@tsed/openspec": ">=8.20.1", - "@tsed/schema": ">=8.20.1", + "@tsed/normalize-path": ">=8.21.0", + "@tsed/openspec": ">=8.21.0", + "@tsed/schema": ">=8.21.0", "chalk": "^5.3.0", "change-case": "^5.4.4", "consolidate": "^1.0.4", diff --git a/packages/cli/package.json b/packages/cli/package.json index 1f60c8d40..6fd81b2b9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,14 +52,14 @@ "@swc/helpers": "^0.5.13", "@tsed/cli-core": "workspace:*", "@tsed/cli-mcp": "workspace:*", - "@tsed/core": ">=8.20.1", - "@tsed/di": ">=8.20.1", - "@tsed/hooks": ">=8.20.1", + "@tsed/core": ">=8.21.0", + "@tsed/di": ">=8.21.0", + "@tsed/hooks": ">=8.21.0", "@tsed/logger": ">=8.0.3", "@tsed/logger-std": ">=8.0.3", - "@tsed/normalize-path": ">=8.20.1", - "@tsed/openspec": ">=8.20.1", - "@tsed/schema": ">=8.20.1", + "@tsed/normalize-path": ">=8.21.0", + "@tsed/openspec": ">=8.21.0", + "@tsed/schema": ">=8.21.0", "chalk": "^5.3.0", "change-case": "^5.4.4", "consolidate": "^1.0.4", diff --git a/packages/cli/src/commands/mcp/McpCommand.ts b/packages/cli/src/commands/mcp/McpCommand.ts index dafb387b3..f148f61e9 100644 --- a/packages/cli/src/commands/mcp/McpCommand.ts +++ b/packages/cli/src/commands/mcp/McpCommand.ts @@ -4,7 +4,7 @@ import {inject} from "@tsed/di"; import {s} from "@tsed/schema"; const McpSchema = s.object({ - http: s.boolean().default(false).description("Run MCP using HTTP server").customKey("opt", "--http") + http: s.boolean().default(false).description("Run MCP using HTTP server").opt("--http") }); export const McpCommand = command({ diff --git a/packages/cli/src/commands/mcp/schema/InitMCPSchema.ts b/packages/cli/src/commands/mcp/schema/InitMCPSchema.ts index 480da5738..921b1836e 100644 --- a/packages/cli/src/commands/mcp/schema/InitMCPSchema.ts +++ b/packages/cli/src/commands/mcp/schema/InitMCPSchema.ts @@ -1,37 +1,11 @@ -import {PackageManagersModule} from "@tsed/cli-core"; -import {inject} from "@tsed/di"; import {s} from "@tsed/schema"; -import {ArchitectureConvention, PlatformType, ProjectConvention} from "../../../interfaces/index.js"; -import {RuntimesModule} from "../../../runtimes/RuntimesModule.js"; -import {FeatureType} from "../../init/config/FeaturesPrompt.js"; +import {InitSchema} from "../../init/config/InitSchema.js"; -export const InitMCPSchema = () => - s.object({ - cwd: s.string().required().description("Current working directory to initialize Ts.ED project"), - projectName: s.string().description("Project name. Defaults to the current folder name."), - platform: s.string().enum(PlatformType).default(PlatformType.EXPRESS).description("Target platform (express, koa, fastify)."), - convention: s - .string() - .enum(ProjectConvention) - .default(ProjectConvention.DEFAULT) - .description("Project convention (default, nest, etc.)."), - runtime: s.string().enum(inject(RuntimesModule).list()).default("node").description("Runtime (node, bun, ...)."), - packageManager: s - .string() - .enum(inject(PackageManagersModule).list()) - .default("npm") - .description("Package manager (npm, pnpm, yarn, bun)."), - architecture: s - .string() - .enum(ArchitectureConvention) - .default(ArchitectureConvention.DEFAULT) - .description("Architecture convention (default, feature, ...)."), - features: s.array().items(s.string().enum(FeatureType)).description("List of features to enable (swagger, graphql, prisma, etc.)."), - GH_TOKEN: s - .string() - .optional() - .description( - "GitHub token to install premium plugins. For example config:aws_secrets:premium or all features endings by `:premium` needs a GH_TOKEN" - ) - }); +export const InitMCPSchema = () => { + return s + .object({ + cwd: s.string().required().description("Current working directory to initialize Ts.ED project") + }) + .merge(InitSchema().omit("root", "skipPrompts", "file")); +}; diff --git a/yarn.lock b/yarn.lock index ef83dd21a..721581bbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2532,14 +2532,14 @@ __metadata: "@swc/core": "npm:1.7.26" "@swc/helpers": "npm:^0.5.13" "@tsed/cli-core": "workspace:*" - "@tsed/core": "npm:>=8.20.1" - "@tsed/di": "npm:>=8.20.1" - "@tsed/hooks": "npm:>=8.20.1" + "@tsed/core": "npm:>=8.21.0" + "@tsed/di": "npm:>=8.21.0" + "@tsed/hooks": "npm:>=8.21.0" "@tsed/logger": "npm:>=8.0.3" "@tsed/logger-std": "npm:>=8.0.3" - "@tsed/normalize-path": "npm:>=8.20.1" - "@tsed/openspec": "npm:>=8.20.1" - "@tsed/schema": "npm:>=8.20.1" + "@tsed/normalize-path": "npm:>=8.21.0" + "@tsed/openspec": "npm:>=8.21.0" + "@tsed/schema": "npm:>=8.21.0" "@tsed/typescript": "workspace:*" "@types/change-case": "npm:^2.3.1" "@types/consolidate": "npm:0.14.4" @@ -2733,15 +2733,15 @@ __metadata: "@commitlint/config-conventional": "npm:19.5.0" "@swc/core": "npm:1.7.26" "@swc/helpers": "npm:0.5.13" - "@tsed/core": "npm:>=8.20.1" - "@tsed/di": "npm:>=8.20.1" + "@tsed/core": "npm:>=8.21.0" + "@tsed/di": "npm:>=8.21.0" "@tsed/logger": "npm:>=8.0.3" "@tsed/logger-std": "npm:>=8.0.3" "@tsed/markdown-it-symbols": "npm:3.20.8" "@tsed/monorepo-utils": "npm:3.0.0" - "@tsed/normalize-path": "npm:>=8.20.1" - "@tsed/openspec": "npm:>=8.20.1" - "@tsed/schema": "npm:>=8.20.1" + "@tsed/normalize-path": "npm:>=8.21.0" + "@tsed/openspec": "npm:>=8.21.0" + "@tsed/schema": "npm:>=8.21.0" "@tsed/ts-doc": "npm:^4.1.0" "@types/inquirer-autocomplete-prompt": "npm:3.0.3" "@types/node": "npm:22.7.4" @@ -2815,14 +2815,14 @@ __metadata: "@swc/helpers": "npm:^0.5.13" "@tsed/cli-core": "workspace:*" "@tsed/cli-mcp": "workspace:*" - "@tsed/core": "npm:>=8.20.1" - "@tsed/di": "npm:>=8.20.1" - "@tsed/hooks": "npm:>=8.20.1" + "@tsed/core": "npm:>=8.21.0" + "@tsed/di": "npm:>=8.21.0" + "@tsed/hooks": "npm:>=8.21.0" "@tsed/logger": "npm:>=8.0.3" "@tsed/logger-std": "npm:>=8.0.3" - "@tsed/normalize-path": "npm:>=8.20.1" - "@tsed/openspec": "npm:>=8.20.1" - "@tsed/schema": "npm:>=8.20.1" + "@tsed/normalize-path": "npm:>=8.21.0" + "@tsed/openspec": "npm:>=8.21.0" + "@tsed/schema": "npm:>=8.21.0" "@tsed/typescript": "workspace:*" "@types/change-case": "npm:^2.3.1" "@types/consolidate": "npm:0.14.4" @@ -2869,19 +2869,19 @@ __metadata: languageName: unknown linkType: soft -"@tsed/core@npm:>=8.20.1": - version: 8.20.1 - resolution: "@tsed/core@npm:8.20.1" +"@tsed/core@npm:>=8.21.0": + version: 8.21.0 + resolution: "@tsed/core@npm:8.21.0" dependencies: reflect-metadata: "npm:^0.2.2" tslib: "npm:2.7.0" - checksum: 10/1a2739886f7a2e51ece11d6d98bbb1c3092abd5bc8162569230d62b0cedf5565bcb09d3d20990265e8f04e9786c6ffb2e332a12feb1720d58d81f4c5423c687d + checksum: 10/0f99142c739644bb469934ec4693e1dc613582b9074106a0755c7bf18cf087a4f5724f9f599cbf9a9af4ae84d2d5c891ce4a6ca23a67c223b04f7a84933345c8 languageName: node linkType: hard -"@tsed/di@npm:>=8.20.1": - version: 8.20.1 - resolution: "@tsed/di@npm:8.20.1" +"@tsed/di@npm:>=8.21.0": + version: 8.21.0 + resolution: "@tsed/di@npm:8.21.0" dependencies: tslib: "npm:2.7.0" uuid: "npm:^10.0.0" @@ -2896,17 +2896,17 @@ __metadata: optional: false "@tsed/logger": optional: false - checksum: 10/9e6a832dfd6c5bb8035b102938db36b5fd6114cabe3eb7bed1a72ad74cc9f81043e711809bc3b7831a84fba00b1902ed5170317a3171a806d3ffb3c4c822cfb1 + checksum: 10/ee8836d53548a985cfe472f395c1117e6c96117c6343cb51ee0f50d245893f8bade860754f78b24dcd09ff875372c7b328ca95456e1f94c11e935874b29b1cbe languageName: node linkType: hard -"@tsed/hooks@npm:>=8.20.1": - version: 8.20.1 - resolution: "@tsed/hooks@npm:8.20.1" +"@tsed/hooks@npm:>=8.21.0": + version: 8.21.0 + resolution: "@tsed/hooks@npm:8.21.0" dependencies: reflect-metadata: "npm:^0.2.2" tslib: "npm:2.7.0" - checksum: 10/d833dfdd509c068f772e1c5c98a743af7edbd57c37db3e554a388fd434c3d423e2674145c3645ae9c3a75bd83d6ecd218420694331a539b4c51c9c08c2e84c44 + checksum: 10/58002cfeaf23ecedfdf90f527058a1f25c4346973ea2e201a3e5d9842fe9c4d093fc584cd52be562752b957f1a7b01848c8a0bbdea4de94f8e9e8e353e60ee77 languageName: node linkType: hard @@ -3003,28 +3003,28 @@ __metadata: languageName: node linkType: hard -"@tsed/normalize-path@npm:>=8.20.1": - version: 8.20.1 - resolution: "@tsed/normalize-path@npm:8.20.1" +"@tsed/normalize-path@npm:>=8.21.0": + version: 8.21.0 + resolution: "@tsed/normalize-path@npm:8.21.0" dependencies: normalize-path: "npm:3.0.0" tslib: "npm:2.7.0" - checksum: 10/de8996ba992a3cb9620ae37704b1211d93699ee0f5fe60592bb6ee314965c57ed738faebbf88e0b0b6eef1a3074304c2892f7ec120a52219c9d506a3598130d4 + checksum: 10/90ec188403435ebdd587d7baace397697b703ca61720d1fa14077a5cb6590bd889a63ade3ea2ebb0e11db8c016d525a16ed9a946bef15b6a62b0984fd229551f languageName: node linkType: hard -"@tsed/openspec@npm:8.20.1, @tsed/openspec@npm:>=8.20.1": - version: 8.20.1 - resolution: "@tsed/openspec@npm:8.20.1" - checksum: 10/4ac12ccf0f3c5d12f0df0c9a2032c821ff956395ff8139f33589a0e62183de789104657bd3b0d4270895579e8abd288ac5417adfcb1701abf100cd55f094b324 +"@tsed/openspec@npm:8.21.0, @tsed/openspec@npm:>=8.21.0": + version: 8.21.0 + resolution: "@tsed/openspec@npm:8.21.0" + checksum: 10/f4dd87f9007721fb8e71d7efada10197378f9e22d1bfcf6acb6442c40181d708820ab313a807c23c7e6a0ddedfb85f3084954ee64918ce00f7b6dce45dce1f43 languageName: node linkType: hard -"@tsed/schema@npm:>=8.20.1": - version: 8.20.1 - resolution: "@tsed/schema@npm:8.20.1" +"@tsed/schema@npm:>=8.21.0": + version: 8.21.0 + resolution: "@tsed/schema@npm:8.21.0" dependencies: - "@tsed/openspec": "npm:8.20.1" + "@tsed/openspec": "npm:8.21.0" change-case: "npm:^5.4.4" json-schema: "npm:0.4.0" picomatch: "npm:4.0.2" @@ -3039,7 +3039,7 @@ __metadata: optional: false "@tsed/openspec": optional: false - checksum: 10/4678a4d18714b4e9d4c24fbc16534209f6e4d3113ff732b88009c49c53a91edc1b39f2766dcd4035519f80d8e5cbe3145ed4abe61f4843767c327aa45b63235b + checksum: 10/c9db92abe934059259798e233147166488dd9255e15a0a1876453821c2441996d039bb5719c33e8837166f0873b6e9b3c354b86b375924b28a187bb398a4b301 languageName: node linkType: hard