--- name: cli-builder description: Expert guide for building command-line interfaces with Node.js (Commander, Inquirer, Ora) or Python (Click, Typer, Rich). Use when creating CLI tools, terminal UX, argument parsing, or interactive prompts. --- # CLI Builder Skill ## Overview This skill helps you build professional command-line interfaces with excellent user experience. Covers argument parsing, interactive prompts, progress indicators, colored output, and cross-platform compatibility. ## CLI Design Philosophy ### Principles of Good CLI Design 1. **Predictable**: Follow conventions users expect 2. **Helpful**: Provide clear help text and error messages 3. **Composable**: Work well with pipes and other tools 4. **Forgiving**: Accept common variations in input ### Design Guidelines - **DO**: Use conventional flag names (`-v`, `--verbose`, `-h`, `--help`) - **DO**: Provide meaningful exit codes - **DO**: Support `--version` and `--help` on all commands - **DO**: Use colors meaningfully (errors=red, success=green) - **DON'T**: Require interactive input when running in pipes - **DON'T**: Print to stdout when outputting errors - **DON'T**: Ignore signals (Ctrl+C should exit cleanly) ## Node.js CLI Development ### Project Setup ```bash # Initialize CLI project mkdir my-cli && cd my-cli npm init -y # Install core dependencies npm install commander chalk ora inquirer # Optional: TypeScript support npm install -D typescript @types/node @types/inquirer ts-node ``` ### Package.json Configuration ```json { "name": "my-cli", "version": "1.0.0", "description": "A powerful CLI tool", "bin": { "mycli": "./bin/cli.js" }, "files": [ "bin", "dist" ], "scripts": { "build": "tsc", "dev": "ts-node src/cli.ts", "link": "npm link" }, "engines": { "node": ">=18.0.0" } } ``` ### Commander.js - Command Structure ```typescript // src/cli.ts import { Command } from 'commander'; import { version } from '../package.json'; const program = new Command(); program .name('mycli') .description('A powerful CLI for doing awesome things') .version(version, '-v, --version', 'Display version number'); // Simple command program .command('init') .description('Initialize a new project') .argument('[name]', 'Project name', 'my-project') .option('-t, --template ', 'Template to use', 'default') .option('--no-git', 'Skip git initialization') .option('-f, --force', 'Overwrite existing files') .action(async (name, options) => { console.log(`Creating project: ${name}`); console.log(`Template: ${options.template}`); console.log(`Git: ${options.git}`); }); // Command with subcommands const config = program .command('config') .description('Manage configuration'); config .command('get ') .description('Get a configuration value') .action((key) => { console.log(`Getting config: ${key}`); }); config .command('set ') .description('Set a configuration value') .action((key, value) => { console.log(`Setting ${key} = ${value}`); }); config .command('list') .description('List all configuration') .option('--json', 'Output as JSON') .action((options) => { if (options.json) { console.log(JSON.stringify({ key: 'value' }, null, 2)); } else { console.log('key = value'); } }); // Parse arguments program.parse(); ``` ### Chalk - Colored Output ```typescript // src/utils/logger.ts import chalk from 'chalk'; export const logger = { info: (msg: string) => console.log(chalk.blue('info'), msg), success: (msg: string) => console.log(chalk.green('success'), msg), warning: (msg: string) => console.log(chalk.yellow('warning'), msg), error: (msg: string) => console.error(chalk.red('error'), msg), // Styled output title: (msg: string) => console.log(chalk.bold.underline(msg)), dim: (msg: string) => console.log(chalk.dim(msg)), // Formatted output list: (items: string[]) => { items.forEach(item => console.log(chalk.gray(' -'), item)); }, // Table-like output keyValue: (pairs: Record) => { const maxKeyLen = Math.max(...Object.keys(pairs).map(k => k.length)); Object.entries(pairs).forEach(([key, value]) => { console.log( chalk.cyan(key.padEnd(maxKeyLen)), chalk.gray(':'), value ); }); } }; // Usage logger.title('Project Configuration'); logger.keyValue({ 'Name': 'my-project', 'Template': 'typescript', 'Version': '1.0.0' }); ``` ### Ora - Progress Spinners ```typescript // src/utils/spinner.ts import ora, { Ora } from 'ora'; export function createSpinner(text: string): Ora { return ora({ text, spinner: 'dots', color: 'cyan' }); } // Usage patterns async function downloadWithProgress() { const spinner = createSpinner('Downloading dependencies...'); spinner.start(); try { await downloadFiles(); spinner.succeed('Dependencies downloaded'); } catch (error) { spinner.fail('Download failed'); throw error; } } // Sequential spinners async function setupProject() { const steps = [ { text: 'Creating directory structure', fn: createDirs }, { text: 'Installing dependencies', fn: installDeps }, { text: 'Initializing git', fn: initGit }, { text: 'Configuring project', fn: configure } ]; for (const step of steps) { const spinner = createSpinner(step.text); spinner.start(); try { await step.fn(); spinner.succeed(); } catch (error) { spinner.fail(); throw error; } } } ``` ### Inquirer - Interactive Prompts ```typescript // src/prompts/init.ts import inquirer from 'inquirer'; interface ProjectAnswers { name: string; template: string; features: string[]; initGit: boolean; installDeps: boolean; } export async function promptProjectSetup(): Promise { return inquirer.prompt([ { type: 'input', name: 'name', message: 'Project name:', default: 'my-project', validate: (input) => { if (!/^[a-z0-9-]+$/.test(input)) { return 'Name must be lowercase alphanumeric with dashes'; } return true; } }, { type: 'list', name: 'template', message: 'Select a template:', choices: [ { name: 'Minimal - Basic setup', value: 'minimal' }, { name: 'Standard - Recommended defaults', value: 'standard' }, { name: 'Full - Kitchen sink', value: 'full' } ], default: 'standard' }, { type: 'checkbox', name: 'features', message: 'Select features:', choices: [ { name: 'TypeScript', value: 'typescript', checked: true }, { name: 'ESLint', value: 'eslint', checked: true }, { name: 'Prettier', value: 'prettier', checked: true }, { name: 'Testing (Jest)', value: 'jest' }, { name: 'CI/CD (GitHub Actions)', value: 'github-actions' } ] }, { type: 'confirm', name: 'initGit', message: 'Initialize git repository?', default: true }, { type: 'confirm', name: 'installDeps', message: 'Install dependencies now?', default: true, when: (answers) => answers.template !== 'minimal' } ]); } // Advanced: Dynamic prompts export async function promptWithContext(context: { hasExisting: boolean }) { const questions = []; if (context.hasExisting) { questions.push({ type: 'confirm', name: 'overwrite', message: 'Directory exists. Overwrite?', default: false }); } // Add more questions... return inquirer.prompt(questions); } ``` ### Complete CLI Example ```typescript #!/usr/bin/env node // bin/cli.ts import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import inquirer from 'inquirer'; import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { join } from 'path'; const program = new Command(); program .name('create-app') .description('Create a new application') .version('1.0.0'); program .command('create') .argument('[name]', 'Project name') .option('-t, --template