diff --git a/packages/cli/src/cli-program.test.ts b/packages/cli/src/cli-program.test.ts new file mode 100644 index 00000000..79b0d23a --- /dev/null +++ b/packages/cli/src/cli-program.test.ts @@ -0,0 +1,54 @@ +import type { Command } from '@commander-js/extra-typings'; +import { describe, expect, it } from 'vitest'; +import { buildKtxProgram } from './cli-program.js'; +import type { KtxCliIo, KtxCliPackageInfo } from './cli-runtime.js'; + +function stubIo(): KtxCliIo { + return { + stdout: { isTTY: false, columns: 80, write: () => {} }, + stderr: { write: () => {} }, + }; +} + +function stubPackageInfo(): KtxCliPackageInfo { + return { name: '@ktx/cli', version: '0.0.0-test', contextPackageName: '@ktx/context' }; +} + +describe('buildKtxProgram', () => { + it('returns a Command named "ktx" with all registered top-level subcommands', () => { + const program: Command = buildKtxProgram({ + io: stubIo(), + deps: {}, + packageInfo: stubPackageInfo(), + runInit: async () => 0, + }); + + expect(program.name()).toBe('ktx'); + const topLevel = program.commands.map((command) => command.name()).sort(); + for (const expected of ['setup', 'connection', 'ingest', 'sl', 'dev']) { + expect(topLevel).toContain(expected); + } + }); + + it('does not parse argv or invoke action handlers', () => { + let wrote = ''; + const io: KtxCliIo = { + stdout: { + isTTY: false, + columns: 80, + write: (chunk) => { + wrote += chunk; + }, + }, + stderr: { + write: (chunk) => { + wrote += chunk; + }, + }, + }; + + buildKtxProgram({ io, deps: {}, packageInfo: stubPackageInfo(), runInit: async () => 0 }); + + expect(wrote).toBe(''); + }); +}); diff --git a/packages/cli/src/cli-program.ts b/packages/cli/src/cli-program.ts index 408f3620..e2091bef 100644 --- a/packages/cli/src/cli-program.ts +++ b/packages/cli/src/cli-program.ts @@ -33,6 +33,14 @@ interface KtxCommanderProgramOptions { runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise; } +export interface BuildKtxProgramOptions { + io: KtxCliIo; + deps: KtxCliDeps; + packageInfo: KtxCliPackageInfo; + runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise; + setExitCode?: (code: number) => void; +} + type CommanderExitLike = { exitCode: number; code: string; message: string }; interface KtxGlobalOptionValues { @@ -288,6 +296,35 @@ async function runBareInteractiveCommand( return 0; } +export function buildKtxProgram(options: BuildKtxProgramOptions): Command { + const program = createBaseProgram(options.packageInfo, options.io); + program.hook('preAction', (_thisCommand, actionCommand) => { + writeProjectDir(options.io, actionCommand as CommandPathNode); + }); + + const context: KtxCliCommandContext = { + io: options.io, + deps: options.deps, + packageInfo: options.packageInfo, + setExitCode: options.setExitCode ?? (() => {}), + runInit: options.runInit, + writeDebug: (command: string, commandContext: CommandWithGlobalOptions) => { + writeDebug(options.io, commandContext, command); + }, + }; + + registerSetupCommands(program, context); + registerConnectionCommands(program, context); + registerPublicIngestCommands(program, context); + registerWikiCommands(program, context); + registerSlCommands(program, context); + registerStatusCommands(program, context); + registerAgentCommands(program, context); + registerDevCommands(program, context); + + return program; +} + export async function runCommanderKtxCli( argv: string[], io: KtxCliIo, @@ -297,11 +334,16 @@ export async function runCommanderKtxCli( ): Promise { profileMark('commander:entry'); let exitCode = 0; - const program = createBaseProgram(info, io); - program.hook('preAction', (_thisCommand, actionCommand) => { - writeProjectDir(io, actionCommand as CommandPathNode); + const program = buildKtxProgram({ + io, + deps, + packageInfo: info, + runInit: options.runInit, + setExitCode: (code: number) => { + exitCode = code; + }, }); - profileMark('commander:base-program'); + profileMark('commander:program-built'); const context: KtxCliCommandContext = { io, deps, @@ -315,30 +357,6 @@ export async function runCommanderKtxCli( }, }; - registerSetupCommands(program, context); - profileMark('commander:register-setup'); - - registerConnectionCommands(program, context); - profileMark('commander:register-connection'); - - registerPublicIngestCommands(program, context); - profileMark('commander:register-public-ingest'); - - registerWikiCommands(program, context); - profileMark('commander:register-wiki'); - - registerSlCommands(program, context); - profileMark('commander:register-sl'); - - registerStatusCommands(program, context); - profileMark('commander:register-status'); - - registerAgentCommands(program, context); - profileMark('commander:register-agent'); - - registerDevCommands(program, context); - profileMark('commander:register-dev'); - if (argv.length === 0) { if (io.stdout.isTTY === true) { try {