Creates a new CLI command following the Commander.js pattern in src/commands/. Handles command 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 fixing bugs in existing commands.
export async function myCommand(options?: OptionType). Never use default exports..command() chain in src/cli.ts, wrapped with tracked() for telemetry.throw new Error('__exit__') to exit gracefully without printing the error message. Use chalk for user-facing messages.Create the command file at src/commands/{commandName}.ts with named async export.
export async function {commandName}Command(options?: { optionName?: optionType }) { ... }Handle errors consistently: Wrap error-prone operations in try/catch. Distinguish between user errors and system errors:
console.error(chalk.red('message')); throw new Error('__exit__');throw new Error('Detailed error message'); — this will print and exit with code 1..fail() before throwing.Import and register in src/cli.ts in the correct location:
import { {commandName}Command } from './commands/{commandName}.js';sources)..command('{kebab-name}').description('...').option(...).action(tracked('{kebab-name}', {commandName}Command))sources add): sources.command('add').description(...).action(tracked('sources:add', sourcesAddCommand))tracked('{command-name}', handler) for automatic telemetry.Define options (if needed):
.option() chains before .action(): .option('--flag', 'Description') or .option('--opt <value>', 'Description').option('--opt <value>', 'Description', parseFunction).action(tracked('name', (opts) => command(opts)))Verify before proceeding:
new Error('__exit__') for user errors.User says: "Add a command to show config status"
Actions taken:
Result: caliber status displays config status; caliber status --json outputs JSON.
Code example:
import chalk from 'chalk';
import { loadConfig } from '../llm/config.js';
export async function statusCommand(options?: { json?: boolean }) {
const config = loadConfig();
if (options?.json) {
console.log(JSON.stringify({ configured: !!config }, null, 2));
return;
}
console.log(chalk.bold('Status'));
console.log(` LLM: ${chalk.green(config?.provider || 'Not configured')}`);
}
Registration in src/cli.ts:
import { statusCommand } from './commands/status.js';
program
.command('status')
.description('Show config status')
.option('--json', 'Output as JSON')
.action(tracked('status', statusCommand));
User says: "Add a sources add subcommand"
Actions taken:
Result: caliber sources add ../lib adds a source.
Code example:
export async function sourcesAddCommand(sourcePath: string) {
if (!fs.existsSync(sourcePath)) {
console.log(chalk.red(`Path not found: ${sourcePath}`));
throw new Error('__exit__');
}
const existing = loadSourcesConfig(process.cwd());
existing.push({ type: 'repo', path: sourcePath });
writeSourcesConfig(process.cwd(), existing);
console.log(chalk.green(`Added ${sourcePath}`));
}
Registration:
const sources = program.command('sources');
sources
.command('add')
.argument('<path>', 'Path to add')
.action(tracked('sources:add', sourcesAddCommand));
User says: "Add init with --agent flag supporting comma-separated values"
Actions taken:
Result: caliber init --agent claude,cursor passes parsed array to handler.
Parser code:
function parseAgentOption(value: string) {
const agents = value.split(',').map(s => s.trim().toLowerCase());
if (agents.length === 0) {
console.error('Invalid agent');
process.exit(1);
}
return agents;
}
program.command('init')
.option('--agent <type>', 'Agents (comma-separated)', parseAgentOption)
.action(tracked('init', initCommand));
Issue: "SyntaxError: The requested module does not provide an export named 'myCommand'"
export async function myCommand(...) (not export default).Issue: Command appears in help but crashes when run
tracked() or function import mismatch.tracked('command-name', handler).Issue: "Error: exit" appears in output for user errors
console.error(chalk.red('message')); throw new Error('__exit__'); for user-facing errors.Issue: --dry-run flag not recognized
.option() or wrong camelCase in interface..option('--dry-run', 'Description') and ensure options interface has dryRun?: boolean.Issue: Subcommand crashes but parent command works
program.command() instead of groupVar.command() for subcommands.const sources = program.command('sources'); sources.command('add')...Issue: Telemetry not appearing
tracked() or wrong command name..action(tracked('{kebab-case}', handler)) wraps handler. Use colon for subcommands like 'sources:add'.Issue: "Cannot find module" with relative imports
.ts extension in imports..js extension: import { x } from '../lib/file.js' (required for ESM).38:["$","$L42",null,{"content":"$43","frontMatter":{"name":"adding-a-command","description":"Creates a new CLI command following the Commander.js pattern in src/commands/. Handles command 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 fixing bugs in existing commands.","paths":["src/commands/**/*.ts","src/cli.ts"]}}]