--- name: cli-tool-development description: Build professional CLI tools with Node.js, commander, and Ink license: MIT compatibility: nodejs 18+, typescript 5+ allowed-tools: read_file write_file apply_patch run_command search_with_context --- # CLI Tool Development ## Project Structure ``` src/ index.ts # Entry point with shebang cli.ts # Commander setup commands/ # Command handlers ui/ # Ink components (if interactive) utils/ # Helpers types.ts # Type definitions ``` ## Commander.js Setup ```typescript #!/usr/bin/env node import { Command } from 'commander'; import packageJson from '../package.json' with { type: 'json' }; const program = new Command(); program .name('mytool') .description('My awesome CLI tool') .version(packageJson.version); program .command('init') .description('Initialize a new project') .option('-t, --template ', 'Template to use', 'default') .option('-f, --force', 'Overwrite existing files', false) .action(async (options) => { await initCommand(options); }); program.parseAsync(); ``` ## User Feedback with Chalk & Ora ```typescript import chalk from 'chalk'; import ora from 'ora'; // Status messages console.log(chalk.green('✓') + ' Operation complete'); console.log(chalk.red('✗') + ' Operation failed'); console.log(chalk.yellow('⚠') + ' Warning message'); // Progress spinner const spinner = ora('Loading...').start(); try { await longOperation(); spinner.succeed('Done!'); } catch (error) { spinner.fail('Failed'); } ``` ## Interactive Prompts with Enquirer ```typescript import enquirer from 'enquirer'; const { name } = await enquirer.prompt<{ name: string }>({ type: 'input', name: 'name', message: 'Project name:', validate: (v) => v.length > 0 || 'Name required', }); const { confirm } = await enquirer.prompt<{ confirm: boolean }>({ type: 'confirm', name: 'confirm', message: 'Continue?', initial: true, }); ``` ## Ink for Rich TUI ```tsx import React, { useState } from 'react'; import { render, Box, Text, useInput } from 'ink'; function App() { const [selected, setSelected] = useState(0); const items = ['Option 1', 'Option 2', 'Option 3']; useInput((input, key) => { if (key.downArrow) setSelected(s => Math.min(s + 1, items.length - 1)); if (key.upArrow) setSelected(s => Math.max(s - 1, 0)); if (key.return) process.exit(0); }); return ( {items.map((item, i) => ( {i === selected ? '>' : ' '} {item} ))} ); } render(); ``` ## Configuration Management ```typescript import fs from 'fs-extra'; import path from 'node:path'; import os from 'node:os'; const CONFIG_DIR = path.join(os.homedir(), '.mytool'); const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); async function loadConfig(): Promise { await fs.ensureDir(CONFIG_DIR); if (await fs.pathExists(CONFIG_FILE)) { return fs.readJson(CONFIG_FILE); } return getDefaultConfig(); } async function saveConfig(config: Config): Promise { await fs.writeJson(CONFIG_FILE, config, { spaces: 2 }); } ``` ## Error Handling ```typescript // Graceful exit handling process.on('SIGINT', () => { console.log('\nCancelled'); process.exit(0); }); // Top-level error handler try { await program.parseAsync(); } catch (error) { console.error(chalk.red('Error:'), error.message); process.exit(1); } ``` ## Package.json Setup ```json { "bin": { "mytool": "./dist/index.js" }, "files": ["dist"], "type": "module", "scripts": { "build": "tsup src/index.ts --format esm --dts", "dev": "tsx src/index.ts" } } ``` ## Best Practices 1. Add `#!/usr/bin/env node` shebang to entry file 2. Support both flags (`-f`) and options (`--force`) 3. Provide helpful error messages with suggestions 4. Support `--help` and `--version` flags 5. Use exit codes: 0 for success, 1 for error 6. Support piping and stdin when appropriate 7. Respect `NO_COLOR` environment variable