Claude-skill-registry cli-tool-development
Build professional CLI tools with Node.js, commander, and Ink
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cli-tool-development" ~/.claude/skills/majiayu000-claude-skill-registry-cli-tool-development && rm -rf "$T"
manifest:
skills/data/cli-tool-development/SKILL.mdsource content
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
#!/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 <name>', 'Template to use', 'default') .option('-f, --force', 'Overwrite existing files', false) .action(async (options) => { await initCommand(options); }); program.parseAsync();
User Feedback with Chalk & Ora
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
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
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 ( <Box flexDirection="column"> {items.map((item, i) => ( <Text key={i} color={i === selected ? 'cyan' : undefined}> {i === selected ? '>' : ' '} {item} </Text> ))} </Box> ); } render(<App />);
Configuration Management
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<Config> { await fs.ensureDir(CONFIG_DIR); if (await fs.pathExists(CONFIG_FILE)) { return fs.readJson(CONFIG_FILE); } return getDefaultConfig(); } async function saveConfig(config: Config): Promise<void> { await fs.writeJson(CONFIG_FILE, config, { spaces: 2 }); }
Error Handling
// 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
{ "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
- Add
shebang to entry file#!/usr/bin/env node - Support both flags (
) and options (-f
)--force - Provide helpful error messages with suggestions
- Support
and--help
flags--version - Use exit codes: 0 for success, 1 for error
- Support piping and stdin when appropriate
- Respect
environment variableNO_COLOR