cli-development

Patterns and best practices for building command-line interfaces using Commander.js, with focus on argument parsing, user experience, and output formatting.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "cli-development" with this command: npx skills add pwarnock/liaison-toolkit/pwarnock-liaison-toolkit-cli-development

CLI Development

Patterns and best practices for building command-line interfaces using Commander.js, with focus on argument parsing, user experience, and output formatting.

When to use this skill

Use this skill when:

  • Creating new CLI commands or subcommands

  • Adding options, arguments, or flags to commands

  • Parsing and validating user input

  • Formatting CLI output (spinners, colors, tables)

  • Handling errors and exit codes

  • Writing help text and usage documentation

Commander.js Command Patterns

Basic Command Structure

import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora';

export function createMyCommand(): Command { const command = new Command('mycommand');

command .description('Brief description of what this command does') .option('-f, --force', 'Force operation without confirmation') .option('-o, --output <path>', 'Output file path') .action(handleMyCommand);

return command; }

async function handleMyCommand(options: any): Promise<void> { const spinner = ora('Processing...').start();

try { // Command logic here spinner.succeed(chalk.green('✅ Operation completed')); } catch (error) { spinner.fail(chalk.red(❌ Failed: ${error instanceof Error ? error.message : String(error)})); process.exit(1); } }

Subcommands Pattern

From packages/liaison/src/commands/skill.ts :

export function createSkillCommand(): Command { const command = new Command('skill'); command.description('Manage Agent Skills');

// Subcommand: liaison skill init command .command('init') .description('Initialize Agent Skills in this project') .option('--global', 'Initialize globally (~/.skills/)') .option('--copy', 'Copy instead of symlink (Windows compatibility)') .option('--location <path>', 'Custom skills location') .action(initSkills);

// Subcommand: liaison skill create <name> command .command('create <name>') .description('Create a new skill') .option('--description <text>', 'Skill description') .option('--template <type>', 'Skill template (workflow, library, qa, deployment)', 'workflow') .option('--location <path>', 'Create skill at custom location') .action(createSkill);

return command; }

Argument and Option Patterns

Required Arguments

command .command('create <name>') // Required argument .action((name: string, options: any) => { console.log(Creating: ${name}); });

Optional Arguments

command .command('validate [path]') // Optional argument (square brackets) .action((path?: string) => { const targetPath = path || '.skills'; });

Options with Values

command .option('-o, --output <path>', 'Output file path') // Required value .option('-t, --template <type>', 'Template type', 'workflow') // With default .option('--format [fmt]', 'Output format', 'table') // Optional value

Boolean Flags

command .option('-f, --force', 'Force operation') .option('--no-cache', 'Disable caching') // Boolean negation

Input Validation

Validate Arguments

async function createSkill(name: string, options: any): Promise<void> { const spinner = ora('Creating skill...').start();

// Validate skill name format if (!name.match(/^[a-z0-9]+(-[a-z0-9]+)*$/)) { spinner.fail(chalk.red('Invalid skill name. Use lowercase alphanumeric with hyphens only.')); process.exit(1); }

// Check if already exists try { await fs.access(skillPath); spinner.fail(chalk.red(Skill "${name}" already exists at ${skillPath})); process.exit(1); } catch { // Good, doesn't exist yet }

// Proceed with creation... }

Validate Options

async function listSkills(options: any): Promise<void> { const validFormats = ['table', 'json', 'xml'];

if (options.format && !validFormats.includes(options.format)) { console.error(chalk.red(Invalid format: ${options.format})); console.error(chalk.yellow(Valid formats: ${validFormats.join(', ')})); process.exit(1); }

// Proceed... }

Output Formatting

Using Spinners (ora)

import ora from 'ora';

const spinner = ora('Loading...').start();

// Update spinner text spinner.text = 'Processing items...';

// Success spinner.succeed(chalk.green('✅ Operation completed'));

// Warning spinner.warn(chalk.yellow('⚠️ Warning message'));

// Failure spinner.fail(chalk.red('❌ Operation failed'));

// Stop without status spinner.stop();

Using Colors (chalk)

import chalk from 'chalk';

console.log(chalk.green('Success message')); console.log(chalk.yellow('Warning message')); console.log(chalk.red('Error message')); console.log(chalk.blue('Info message')); console.log(chalk.cyan('Highlight text')); console.log(chalk.bold('Bold text'));

Table Output

From packages/liaison/src/commands/skill.ts:252-260 :

// Format list as table console.log(chalk.bold('\nAvailable Skills:\n')); const table = skills .map(skill => ${chalk.cyan(skill.name.padEnd(30))} ${skill.description.substring(0, 60)} ) .join('\n'); console.log(table); console.log(\n Total: ${chalk.green(skills.length)} skill(s)\n);

JSON Output

if (options.format === 'json') { console.log(JSON.stringify(result, null, 2)); return; }

Error Handling

Graceful Error Messages

try { await performOperation(); } catch (error) { console.error(chalk.red(\n❌ Operation failed:\n)); console.error(chalk.red( ${error instanceof Error ? error.message : String(error)}));

if (options.verbose && error instanceof Error && error.stack) { console.error(chalk.dim('\nStack trace:')); console.error(chalk.dim(error.stack)); }

process.exit(1); }

Exit Codes

// Success process.exit(0);

// General error process.exit(1);

// Invalid usage process.exit(2);

// Validation error process.exit(3);

Help Text and Documentation

Command Description

command .description('Create a new skill') // Brief one-liner .usage('[options] <name>') // Usage pattern .addHelpText('after', Examples: $ liaison skill create my-skill $ liaison skill create my-skill --template library $ liaison skill create my-skill --description "My custom skill" );

Custom Help

command.addHelpCommand(false); // Disable default help command

command.on('--help', () => { console.log(''); console.log('Additional Information:'); console.log(' This command creates a new skill following the Agent Skills standard'); console.log(' Learn more: https://agentskills.io'); });

Common Patterns

Confirmation Prompts

import readline from 'readline';

async function confirmAction(message: string): Promise<boolean> { const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

return new Promise(resolve => { rl.question(${message} (y/N): , answer => { rl.close(); resolve(answer.toLowerCase() === 'y'); }); }); }

// Usage if (!options.force && await confirmAction('Delete all data?')) { await deleteData(); }

Progress Indicators

for (let i = 0; i < items.length; i++) { spinner.text = Processing item ${i + 1} of ${items.length}...; await processItem(items[i]); }

Verbose Mode

function log(message: string, verbose: boolean = false): void { if (verbose) { console.log(chalk.dim([DEBUG] ${message})); } }

// Usage command.option('-v, --verbose', 'Enable verbose output');

async function myAction(options: any): Promise<void> { log('Starting operation...', options.verbose); // ... }

Verification

After implementing CLI features:

  • Help text is clear and shows examples

  • All options have descriptions

  • Input validation provides helpful error messages

  • Output is formatted consistently (colors, spinners)

  • Exit codes are appropriate (0 for success, non-zero for errors)

  • Error messages are user-friendly (no raw stack traces)

  • Commands work with --help flag

  • Progress feedback for long operations

Examples from liaison-toolkit

Example 1: Skill Create Command

// packages/liaison/src/commands/skill.ts:168-226 async function createSkill(name: string, options: any): Promise<void> { const spinner = ora('Creating skill...').start();

try { // Validate skill name if (!name.match(/^[a-z0-9]+(-[a-z0-9]+)*$/)) { spinner.fail(chalk.red('Invalid skill name. Use lowercase alphanumeric with hyphens only.')); process.exit(1); }

const skillsDir = options.location || '.skills';
const skillPath = join(skillsDir, name);

// Check if skill already exists
try {
  await fs.access(skillPath);
  spinner.fail(chalk.red(`Skill "${name}" already exists at ${skillPath}`));
  process.exit(1);
} catch {
  // Good, doesn't exist yet
}

// Create skill directory
spinner.text = 'Creating skill directory...';
await fs.mkdir(skillPath, { recursive: true });

// Create subdirectories
await fs.mkdir(join(skillPath, 'references'), { recursive: true });
await fs.mkdir(join(skillPath, 'scripts'), { recursive: true });
await fs.mkdir(join(skillPath, 'assets'), { recursive: true });

// Generate and write SKILL.md
spinner.text = 'Creating SKILL.md...';
const skillContent = generateSkillTemplate(
  name,
  options.description || `Skill: ${name}`,
  options.template,
);
await fs.writeFile(join(skillPath, 'SKILL.md'), skillContent);

spinner.succeed(chalk.green(`✅ Skill "${name}" created successfully`));
console.log(chalk.blue('\n📝 Next steps:'));
console.log(`  1. Edit: ${chalk.cyan(`${skillPath}/SKILL.md`)}`);
console.log(`  2. Add references: ${chalk.cyan(`${skillPath}/references/`)}`);
console.log(`  3. Validate: ${chalk.cyan(`liaison skill validate ${skillPath}`)}`);

} catch (error) { spinner.fail( chalk.red( Failed to create skill: ${error instanceof Error ? error.message : String(error)}, ), ); process.exit(1); } }

Example 2: Skill List Command

// packages/liaison/src/commands/skill.ts:232-269 async function listSkills(options: any): Promise<void> { const spinner = ora('Discovering skills...').start();

try { const locations = options.location ? [options.location] : ['.skills']; const skills = await discoverSkills({ locations });

spinner.stop();

if (skills.length === 0) {
  console.log(chalk.yellow('No skills found. Run: liaison skill create &#x3C;name>'));
  return;
}

if (options.format === 'json') {
  console.log(JSON.stringify(skills, null, 2));
} else if (options.format === 'xml') {
  console.log(generateAvailableSkillsXml(skills));
} else {
  // Table format
  console.log(chalk.bold('\nAvailable Skills:\n'));
  const table = skills
    .map(
      (skill) =>
        `  ${chalk.cyan(skill.name.padEnd(30))} ${skill.description.substring(0, 60)}`,
    )
    .join('\n');
  console.log(table);
  console.log(`\n  Total: ${chalk.green(skills.length)} skill(s)\n`);
}

} catch (error) { spinner.fail( chalk.red( Failed to list skills: ${error instanceof Error ? error.message : String(error)}, ), ); process.exit(1); } }

Related Resources

  • Commander.js Documentation

  • chalk (Terminal colors)

  • ora (Spinners)

  • Node.js readline

  • CLI UX best practices: 12 Factor CLI Apps

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

bun-development

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

subagent-coordination

No summary provided by upstream source.

Repository SourceNeeds Review
Security

security-scanning

No summary provided by upstream source.

Repository SourceNeeds Review