Ai-setup adding-a-command
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.
install
source · Clone the upstream repo
git clone https://github.com/caliber-ai-org/ai-setup
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/caliber-ai-org/ai-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.cursor/skills/adding-a-command" ~/.claude/skills/caliber-ai-org-ai-setup-adding-a-command-82e1fc && rm -rf "$T"
manifest:
.cursor/skills/adding-a-command/SKILL.mdsource content
Adding a Command
Critical
- Commands MUST be registered in
usingsrc/cli.ts
and.command()
with the.action()
wrapper for telemetry.tracked() - Command file MUST be in
and export a default async function with signature:src/commands/{commandName}.ts
.async (options: CommandOptions, ctx: CLIContext) => Promise<void> - Always use
wrapper intracked(commandName, async () => { ... })
to enable telemetry tracking.src/cli.ts - Test file MUST be in
with at least one happy-path test.src/commands/__tests__/{commandName}.test.ts
Instructions
-
Create command file in
src/commands/{commandName}.ts- Export default async function:
export default async (options: any, ctx: CLIContext) => { ... } - Import
fromCLIContextsrc/cli.ts - Use
for output (respects quiet mode viactx.log()
)--quiet - Use
for async operationsctx.spinner() - Verify function signature matches existing commands like
src/commands/score.ts
- Export default async function:
-
Register in src/cli.ts
- Import the command:
import addCommand from './commands/mycommand.js' - Add command definition:
program .command('mycommand') .description('One-line description') .option('--option', 'Option description') .action(tracked('mycommand', addCommand)) - Verify imports are
(ESM).js - Verify
wrapper is applied totracked().action()
- Import the command:
-
Add telemetry event in
src/telemetry/events.ts- Add event type:
export type MyCommandEvent = { type: 'mycommand:start' | 'mycommand:success' | 'mycommand:error'; ... } - Include
field for timed eventsduration?: number - Update
export type AllEvents = ... | MyCommandEvent - Verify event matches pattern in existing events
- Add event type:
-
Create test file in
src/commands/__tests__/{commandName}.test.ts- Import
,describe
,it
,expect
fromvivitest - Import command from parent:
import addCommand from '../mycommand.js' - Create mock
:CLIContext{ log: vi.fn(), spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) } - Test happy path:
await addCommand({}, ctx); expect(ctx.log).toHaveBeenCalled() - Verify test runs:
npx vitest run src/commands/__tests__/{commandName}.test.ts
- Import
-
Build and validate
- Run
→ verify no TypeScript errorsnpm run build - Run
→ verify ESLint passesnpm run lint - Test command:
→ verify options listednode dist/bin.js mycommand --help - Run
→ verify new test passesnpm run test
- Run
Examples
User says: "Add a new
verify command that checks if config files exist"
Actions:
- Create
:src/commands/verify.tsimport { 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'}`); }; - Register in
:src/cli.tsimport verifyCommand from './commands/verify.js'; program .command('verify') .description('Verify configuration files exist') .action(tracked('verify', verifyCommand)) - Add to
:src/telemetry/events.tsexport type VerifyEvent = { type: 'verify:start' | 'verify:success' | 'verify:error'; duration?: number; }; - Create
with happy-path testsrc/commands/__tests__/verify.test.ts - Run
npm run build && npm run test && npm run lint
Common Issues
"Cannot find module './commands/mycommand.js'"
- Verify file is at
(TypeScript)src/commands/mycommand.ts - Verify import in
usessrc/cli.ts
extension:.jsfrom './commands/mycommand.js' - Rebuild:
npm run build
"tracked is not exported from src/cli.ts"
- Verify
function exists intracked()
(it should; check existing commands)src/cli.ts - Verify you're using it as
.action(tracked('commandName', commandFunction))
Test fails with "CLIContext is not a constructor"
- Create mock object manually:
const ctx = { log: vi.fn(), spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) } - Do NOT try to instantiate CLIContext; it's an interface
"--option not recognized" when testing
- Verify
is chained in.option()
BEFOREsrc/cli.ts.action() - Rebuild and test with
npm run build && node dist/bin.js mycommand --help