cli-tool-development
npx machina-cli add skill autohandai/community-skills/cli-tool-development --openclawFiles (1)
SKILL.md
4.1 KB
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
#!/usr/bin/env nodeshebang to entry file - Support both flags (
-f) and options (--force) - Provide helpful error messages with suggestions
- Support
--helpand--versionflags - Use exit codes: 0 for success, 1 for error
- Support piping and stdin when appropriate
- Respect
NO_COLORenvironment variable
Source
git clone https://github.com/autohandai/community-skills/blob/main/cli-tool-development/SKILL.mdView on GitHub Overview
Learn how to create scalable CLI apps using Node.js, Commander, and Ink. This skill covers project structure, interactive UIs, prompts, configuration, and robust packaging.
How This Skill Works
Start with a Commander-based entry (src/index.ts), wire commands in a cli.ts and separate handlers in commands/. Use Ink for rich TUIs and Enquirer and Chalk/Ora for prompts and status feedback. Persist user configuration in a config.json in the user home directory and coordinate error handling and packaging with a bin entry in package.json.
When to Use It
- You're building a multi-command CLI tool (init, deploy, config) and need a clean structure.
- You want interactive prompts to gather user input during setup or configuration.
- You require a rich terminal UI for menus or live status using Ink.
- You need to persist user preferences or settings in a simple home directory config.
- You aim to deliver a polished tool with helpful errors and a robust --help experience.
Quick Start
- Step 1: Scaffold project structure (src/index.ts, cli.ts, commands/, ui/, utils/).
- Step 2: Create a Commander-based CLI entry and a sample init command with options.
- Step 3: Add interactive prompts (Enquirer), status feedback (Chalk & Ora), and an Ink-based UI; wire a simple config load/save.
Best Practices
- Add a shebang (#!/usr/bin/env node) to the entry file.
- Support both flags (-f) and long options (--force) for flexibility.
- Provide helpful error messages with actionable suggestions.
- Ensure --help is comprehensive and commands are well-documented.
- Package as an npm bin with a dist output and proper module type in package.json.
Example Use Cases
- A tool that initializes a project with a selectable template via -t/--template.
- A multi-command CLI with commands like init, build, and deploy.
- A config manager that reads/writes ~/.mytool/config.json.
- An interactive menu built with Ink to navigate options and exit cleanly.
- A UI that uses Enquirer for prompts and Chalk/Ora for real-time feedback.
Frequently Asked Questions
Add this skill to your agents