CLI Tools
Rules
- Commander.js for argument parsing —
.command(), .option(), .argument() with descriptions
- Interactive prompts:
@clack/prompts (modern, beautiful) or inquirer — for user input when args aren't provided
- Spinners:
ora for long-running operations — start, update message, succeed/fail
- Colors:
chalk for styled output — use sparingly: red for errors, green for success, yellow for warnings, dim for secondary info
- Config files:
cosmiconfig — searches .toolrc, .toolrc.json, tool.config.js, package.json key automatically
- Exit codes:
0 for success, 1 for general error, 2 for usage error — always process.exit(code)
--help and --version flags: Commander adds these automatically — write clear descriptions for every option
- Error handling: catch errors at the top level, print user-friendly message to stderr, exit with code 1
- Publish:
bin field in package.json, #!/usr/bin/env node shebang, "type": "module" for ESM
Patterns
#!/usr/bin/env node
import { Command } from "commander";
import * as p from "@clack/prompts";
import ora from "ora";
import chalk from "chalk";
const program = new Command()
.name("my-tool")
.description("A CLI that does useful things")
.version("1.0.0");
program
.command("init")
.description("Initialize a new project")
.option("-t, --template <name>", "template to use", "default")
.action(async (options) => {
p.intro(chalk.bold("Project Setup"));
const name = await p.text({ message: "Project name?", placeholder: "my-app" });
if (p.isCancel(name)) { p.cancel("Cancelled."); process.exit(0); }
const spinner = ora("Creating project...").start();
await createProject(name, options.template);
spinner.succeed(chalk.green("Project created!"));
p.outro(`Run ${chalk.cyan(`cd ${name} && npm install`)}`);
});
program.parse();
Avoid
- Printing errors to stdout — use
console.error() or process.stderr.write() for errors
- Exiting without a code on failure — always
process.exit(1) so scripts and CI detect the failure
- Walls of unformatted text — use chalk colors, sections, and whitespace for readability
- Requiring interactive input with no flag alternative — always support
--flag overrides for CI/scripting
- Forgetting the shebang
#!/usr/bin/env node — without it, npx and global installs won't work