一键导入
cli-author
Write Node.js CLI tools with zero dependencies. Use when creating command-line tools, argument parsing, colored output, or interactive prompts.
菜单
Write Node.js CLI tools with zero dependencies. Use when creating command-line tools, argument parsing, colored output, or interactive prompts.
Generate standardized static site structures. Use when creating new websites, setting up demos, or need consistent site structure with SEO, PWA, and accessibility foundations.
Modern CSS organization with native @import, @layer cascade control, CSS nesting, design tokens, and element-focused selectors. AUTO-INVOKED when editing .css files.
Define and use custom HTML elements. Use when creating new components, defining custom tags, or using project-specific elements beyond standard HTML5.
Umbrella coordinator for image handling. Coordinates responsive-images, placeholder-images, and automation scripts. Use when adding images to any page, optimizing existing images, or setting up image pipelines.
Generate SVG placeholder images for prototypes. Use when adding placeholder images for layouts, mockups, or development. Supports simple, labeled, and brand-aware types.
Modern responsive image techniques using picture element, srcset, sizes, and modern formats. Use when adding images that need to adapt to different screen sizes, resolutions, or support modern image formats.
| name | cli-author |
| description | Write Node.js CLI tools with zero dependencies. Use when creating command-line tools, argument parsing, colored output, or interactive prompts. |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
Write professional Node.js command-line tools using zero-dependency patterns.
| 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 |
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']
| 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 |
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;
}
#!/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);
#!/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);
#!/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);
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/
// 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}`);
| 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);
});
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')
|| {};
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');
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();
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/);
});
});
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);
}
When writing CLI tools:
Structure
#!/usr/bin/env nodebin/ directorysrc/ (importable as library)Arguments
--help and -h show usage--version shows version from package.jsonOutput
--quiet or --json for scriptingRobustness