Create a new CLI command following Commander.js pattern. Handles command file in src/commands/, registration in src/cli.ts, telemetry tracking via tracked() wrapper, and option parsing. Use when user says 'add command', 'new CLI command', 'create subcommand', or adds files to src/commands/. Do NOT use for modifying existing commands or refactoring command structure.
src/cli.ts using .command() and .action() with the tracked() wrapper for telemetry.src/commands/{commandName}.ts and export a default async function with signature: async (options: CommandOptions, ctx: CLIContext) => Promise<void>.tracked(commandName, async () => { ... }) wrapper in src/cli.ts to enable telemetry tracking.src/commands/__tests__/{commandName}.test.ts with at least one happy-path test.Create command file in src/commands/{commandName}.ts
export default async (options: any, ctx: CLIContext) => { ... }CLIContext from src/cli.tsctx.log() for output (respects quiet mode via )--quietctx.spinner() for async operationssrc/commands/score.tsRegister in src/cli.ts
import addCommand from './commands/mycommand.js'program
.command('mycommand')
.description('One-line description')
.option('--option', 'Option description')
.action(tracked('mycommand', addCommand))
.js (ESM)tracked() wrapper is applied to .action()Add telemetry event in src/telemetry/events.ts
export type MyCommandEvent = { type: 'mycommand:start' | 'mycommand:success' | 'mycommand:error'; ... }duration?: number field for timed eventsexport type AllEvents = ... | MyCommandEventCreate test file in src/commands/__tests__/{commandName}.test.ts
describe, it, expect, vi from vitestimport addCommand from '../mycommand.js'CLIContext: { log: vi.fn(), spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) }await addCommand({}, ctx); expect(ctx.log).toHaveBeenCalled()npx vitest run src/commands/__tests__/{commandName}.test.tsBuild and validate
npm run build → verify no TypeScript errorsnpm run lint → verify ESLint passesnode dist/bin.js mycommand --help → verify options listednpm run test → verify new test passesUser says: "Add a new verify command that checks if config files exist"
Actions:
src/commands/verify.ts:
import { CLIContext } from '../cli.js';
export default async (options: any, ctx: CLIContext) => {
const spinner = ctx.spinner('Verifying config files...');
spinner.start();
const exists = await checkFilesExist();
spinner.stop();
ctx.log(`✓ Config files ${exists ? 'found' : 'missing'}`);
};
src/cli.ts:
import verifyCommand from './commands/verify.js';
program
.command('verify')
.description('Verify configuration files exist')
.action(tracked('verify', verifyCommand))
src/telemetry/events.ts:
export type VerifyEvent = {
type: 'verify:start' | 'verify:success' | 'verify:error';
duration?: number;
};
src/commands/__tests__/verify.test.ts with happy-path testnpm run build && npm run test && npm run lint"Cannot find module './commands/mycommand.js'"
src/commands/mycommand.ts (TypeScript)src/cli.ts uses .js extension: from './commands/mycommand.js'npm run build"tracked is not exported from src/cli.ts"
tracked() function exists in src/cli.ts (it should; check existing commands).action(tracked('commandName', commandFunction))Test fails with "CLIContext is not a constructor"
const ctx = { log: vi.fn(), spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) }"--option not recognized" when testing
.option() is chained in src/cli.ts BEFORE .action()npm run build && node dist/bin.js mycommand --help38:["$","$L42",null,{"content":"$43","frontMatter":{"name":"adding-a-command","description":"Create a new CLI command following Commander.js pattern. Handles command file in src/commands/, registration in src/cli.ts, telemetry tracking via tracked() wrapper, and option parsing. Use when user says 'add command', 'new CLI command', 'create subcommand', or adds files to src/commands/. Do NOT use for modifying existing commands or refactoring command structure."}}]