cli-author
CLI Authoring Skill
Write professional Node.js command-line tools using zero-dependency patterns.
Core Principles
| Principle | Description |
|---|---|
| Zero Dependencies | Use Node.js built-ins only (util.parseArgs, readline, fs) |
| Fail Fast | Validate arguments early, exit with clear errors |
| Exit Codes | 0 = success, 1 = user error, 2 = system error |
| Respect Environment | NO_COLOR, TERM, CI detection |
| Unix Philosophy | Single purpose, composable with pipes |
Argument Parsing with util.parseArgs
Node.js 18+ includes native argument parsing:
import { parseArgs } from 'node:util';
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
output: { type: 'string', short: 'o' },
verbose: { type: 'boolean', short: 'v' },
help: { type: 'boolean', short: 'h' },
},
allowPositionals: true,
});
// values.output, values.verbose, values.help
// positionals = ['file1.js', 'file2.js']
Option Types
| Type | Example | Notes |
|---|---|---|
boolean |
--verbose, -v |
Flag, no value needed |
string |
--output file.txt, -o file.txt |
Requires value |
string (multiple) |
--tag a --tag b |
Use multiple: true |
Error Handling
try {
const { values } = parseArgs({ args, options, strict: true });
} catch (error) {
if (error.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
console.error(`Unknown option: ${error.message}`);
printHelp();
process.exit(1);
}
throw error;
}
CLI Structure Patterns
Simple (Single Action)
#!/usr/bin/env node
import { parseArgs } from 'node:util';
const { values, positionals } = parseArgs({...});
if (values.help) {
printHelp();
process.exit(0);
}
if (positionals.length === 0) {
console.error('Error: No files specified');
process.exit(1);
}
await processFiles(positionals, values);
Multi-Command (Git-style)
#!/usr/bin/env node
const [command, ...rest] = process.argv.slice(2);
const commands = {
init: () => import('./commands/init.js'),
build: () => import('./commands/build.js'),
help: () => import('./commands/help.js'),
};
if (!command || command === 'help' || command === '--help') {
showHelp(commands);
process.exit(0);
}
if (!commands[command]) {
console.error(`Unknown command: ${command}`);
console.error(`Run 'toolname help' for available commands`);
process.exit(1);
}
const { run } = await commands[command]();
await run(rest);
Interactive (Wizard)
#!/usr/bin/env node
import { createInterface } from 'node:readline';
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
async function wizard() {
const name = await question('Project name: ');
const type = await question('Type (lib/app): ');
rl.close();
return { name, type };
}
const config = await wizard();
await scaffold(config);
Help Text Convention
Usage: toolname [options] <command> [arguments]
Commands:
init Initialize a new project
build Build the project
help Show this help message
Options:
-o, --output <path> Output directory
-v, --verbose Enable verbose output
-h, --help Show help
--version Show version
Examples:
toolname init my-project
toolname build --output dist/
Colored Output
// ANSI color codes (respect NO_COLOR)
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
const colors = {
red: useColor ? '\x1b[31m' : '',
green: useColor ? '\x1b[32m' : '',
yellow: useColor ? '\x1b[33m' : '',
blue: useColor ? '\x1b[34m' : '',
dim: useColor ? '\x1b[2m' : '',
reset: useColor ? '\x1b[0m' : '',
};
// Status indicators
const success = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`);
const error = (msg) => console.error(`${colors.red}✗${colors.reset} ${msg}`);
const warn = (msg) => console.warn(`${colors.yellow}⚠${colors.reset} ${msg}`);
Exit Codes
| Code | Meaning | When to Use |
|---|---|---|
| 0 | Success | Normal completion |
| 1 | User Error | Invalid args, file not found, validation failed |
| 2 | System Error | Unexpected exception, crash |
| 130 | SIGINT | Ctrl+C (convention) |
process.on('uncaughtException', (error) => {
console.error('Unexpected error:', error.message);
process.exit(2);
});
process.on('SIGINT', () => {
console.log('\nCancelled');
process.exit(130);
});
Config File Loading
import { readFileSync, existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
function findConfig(startDir, filename) {
let dir = resolve(startDir);
while (dir !== dirname(dir)) {
const configPath = resolve(dir, filename);
if (existsSync(configPath)) {
return JSON.parse(readFileSync(configPath, 'utf-8'));
}
dir = dirname(dir);
}
return null;
}
// Load from .toolrc or package.json
const config = findConfig(process.cwd(), '.mytoolrc')
|| loadPackageJsonConfig('mytool')
|| {};
Progress Indicators
Spinner (Simple)
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let i = 0;
const spinner = setInterval(() => {
process.stdout.write(`\r${frames[i++ % frames.length]} Processing...`);
}, 80);
await longOperation();
clearInterval(spinner);
process.stdout.write('\r✓ Done\n');
Progress Bar
function progressBar(current, total, width = 30) {
const percent = current / total;
const filled = Math.round(width * percent);
const empty = width - filled;
const bar = '█'.repeat(filled) + '░'.repeat(empty);
return `[${bar}] ${Math.round(percent * 100)}%`;
}
// Usage
for (let i = 0; i <= 100; i++) {
process.stdout.write(`\r${progressBar(i, 100)}`);
await delay(50);
}
console.log();
Testing CLI Tools
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { execSync } from 'node:child_process';
describe('mytool CLI', () => {
it('should show help with --help', () => {
const output = execSync('node bin/mytool.js --help', {
encoding: 'utf-8',
});
assert.match(output, /Usage:/);
});
it('should exit 1 on invalid args', () => {
try {
execSync('node bin/mytool.js --invalid', {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.fail('Should have exited with error');
} catch (error) {
assert.strictEqual(error.status, 1);
}
});
it('should process files correctly', () => {
const output = execSync('node bin/mytool.js test.txt', {
encoding: 'utf-8',
});
assert.match(output, /Processed/);
});
});
Stdin/Stdout Piping
import { createInterface } from 'node:readline';
// Read from stdin if no files provided
if (positionals.length === 0 && !process.stdin.isTTY) {
const rl = createInterface({ input: process.stdin });
for await (const line of rl) {
process.stdout.write(processLine(line) + '\n');
}
} else if (positionals.length === 0) {
console.error('Error: No input. Pipe data or provide files.');
process.exit(1);
}
Checklist
When writing CLI tools:
Structure
- Shebang line:
#!/usr/bin/env node - Entry point in
bin/directory - Main logic in
src/(importable as library) - JSDoc documentation for main functions
Arguments
-
--helpand-hshow usage -
--versionshows version from package.json - Unknown options produce helpful error
- Required arguments validated early
Output
- Respect NO_COLOR environment variable
- Use stderr for errors, stdout for results
- Exit 0 on success, 1 on user error, 2 on system error
- Support
--quietor--jsonfor scripting
Robustness
- Handle SIGINT (Ctrl+C) gracefully
- Catch uncaught exceptions
- Validate file paths before use
- Work correctly when piped
Related Skills
- javascript-author - JavaScript coding patterns
- unit-testing - Testing with Node.js test runner
- error-handling - Error management patterns
- nodejs-backend - Server-side Node.js patterns
More from profpowell/vanilla-breeze
api-client
Fetch API patterns with error handling, retry logic, and caching. Use when building API integrations, handling network failures, or implementing offline-first data fetching.
44validation
Validate data with JSON Schema and AJV. Use when validating API requests, form submissions, database inputs, or any data boundaries. Provides deterministic validation with consistent error formats.
43fake-content
Generate realistic fake content for HTML prototypes. Use when populating pages with sample text, products, testimonials, or other content. NOT generic lorem ipsum.
15xhtml-author
Write valid XHTML-strict HTML5 markup. Use when creating HTML files, editing markup, building web pages, or writing any HTML content. Ensures semantic structure and XHTML syntax.
10layout-grid
Design-focused grid layout system with fluid scaling, responsive columns, and resolution-independent patterns. Use when creating page layouts, card grids, or multi-column designs.
8service-worker
Service worker patterns for offline support, caching strategies, and PWA functionality. Use when implementing offline-first features, caching, or background sync.
8