Commander Js
Build structured CLI applications with Commander.js including commands, options, and argument parsing
You are an expert in building command-line tools with Commander.js, the most widely used Node.js CLI framework. ## Key Points - Use `requiredOption()` instead of manually validating required flags — Commander handles the error message and exit code automatically. - Split commands into separate files and use `addCommand()` for maintainability as CLIs grow beyond 3-4 commands. - Provide sensible defaults and use the third argument of `.option()` to set them, so your help output documents the default values. - **Forgetting to call `program.parse()`** — without it, Commander never processes arguments and the program exits silently, which is one of the most confusing first-time bugs. - **Ignoring Commander's auto-conversion of kebab-case** — `--dry-run` becomes `options.dryRun` automatically, and being unaware of this convention causes confusion when destructuring option objects. - Forgetting to call `program.parse()` at the end — without it, nothing executes and the process exits silently. - Using camelCase in option names without realizing Commander auto-converts `--dry-run` to `options.dryRun` — this is intentional but can confuse newcomers when destructuring. ## Quick Example ```bash npm install commander ```
skilldb get cli-development-skills/Commander JsFull skill: 171 linesCommander.js — CLI Development
You are an expert in building command-line tools with Commander.js, the most widely used Node.js CLI framework.
Overview
Commander.js provides a complete solution for building CLI applications with support for commands, subcommands, options, arguments, and automatic help generation. It follows a declarative API that maps naturally to CLI conventions.
Setup & Configuration
Install Commander.js:
npm install commander
Basic program structure:
import { program } from 'commander';
program
.name('my-cli')
.description('A CLI tool for doing things')
.version('1.0.0');
program
.command('init')
.description('Initialize a new project')
.argument('<name>', 'project name')
.option('-t, --template <type>', 'template to use', 'default')
.option('-d, --dry-run', 'show what would be created without writing files')
.action((name, options) => {
console.log(`Creating project "${name}" with template "${options.template}"`);
if (options.dryRun) console.log('(dry run)');
});
program.parse();
TypeScript setup with typed options:
import { Command, Option } from 'commander';
interface ServeOptions {
port: number;
host: string;
open: boolean;
}
const program = new Command();
program
.command('serve')
.addOption(new Option('-p, --port <number>', 'port to listen on').default(3000).argParser(parseInt))
.option('-H, --host <address>', 'host to bind to', 'localhost')
.option('--open', 'open browser on start', false)
.action((options: ServeOptions) => {
console.log(`Serving on http://${options.host}:${options.port}`);
});
Core Patterns
Subcommands with separate files
// commands/deploy.js
import { Command } from 'commander';
export const deploy = new Command('deploy')
.description('Deploy the application')
.argument('<environment>', 'target environment')
.option('--skip-build', 'skip the build step')
.action((environment, options) => {
// deployment logic
});
// index.js
import { program } from 'commander';
import { deploy } from './commands/deploy.js';
program.addCommand(deploy);
program.parse();
Variadic arguments and required options
program
.command('add')
.description('Add files to staging')
.argument('<files...>', 'files to add')
.requiredOption('-m, --message <msg>', 'commit message is required')
.action((files, options) => {
console.log(`Adding ${files.length} files: ${files.join(', ')}`);
console.log(`Message: ${options.message}`);
});
Custom argument processing
function parseRange(value) {
const [min, max] = value.split('-').map(Number);
if (isNaN(min) || isNaN(max)) throw new Error('Range must be min-max');
return { min, max };
}
function collect(value, previous) {
return previous.concat([value]);
}
program
.option('-r, --range <range>', 'numeric range (e.g. 1-10)', parseRange)
.option('-t, --tag <tag>', 'add tag (repeatable)', collect, []);
Global options and hooks
program
.option('--verbose', 'enable verbose output')
.option('--config <path>', 'path to config file', './config.json')
.hook('preAction', (thisCommand, actionCommand) => {
const opts = thisCommand.opts();
if (opts.verbose) {
console.log(`Running command: ${actionCommand.name()}`);
console.log(`Config: ${opts.config}`);
}
});
Best Practices
- Use
requiredOption()instead of manually validating required flags — Commander handles the error message and exit code automatically. - Split commands into separate files and use
addCommand()for maintainability as CLIs grow beyond 3-4 commands. - Provide sensible defaults and use the third argument of
.option()to set them, so your help output documents the default values.
Core Philosophy
Commander.js succeeds because it maps the mental model of CLI usage directly to code. A command, its arguments, its options, and its action handler form a declarative description of what the CLI does. Lean into this declarative style: define your commands completely with descriptions, defaults, and types before writing any implementation logic. The help output that Commander generates from your declarations is your CLI's documentation — make it good.
Start with the user experience and work backward to the code. Before writing a single Commander definition, write out the --help output you want users to see. What commands exist? What options does each accept? What are the defaults? This exercise reveals naming inconsistencies, missing flags, and awkward command hierarchies before you invest in implementation.
As CLIs grow, organization matters. Split each command into its own file and use addCommand() to compose them. A single file with 500 lines of chained .command() calls is unreadable and unmaintainable. Each command file should export a self-contained Command instance that can be tested independently.
Anti-Patterns
-
Putting all commands in a single file — defining ten commands with their handlers in one giant chain makes the code unreadable and untestable; split commands into separate modules and compose with
addCommand(). -
Manually validating required options — using
if (!opts.name) { console.error(...); process.exit(1); }whenrequiredOption()handles this automatically with proper error formatting and exit codes. -
Forgetting to call
program.parse()— without it, Commander never processes arguments and the program exits silently, which is one of the most confusing first-time bugs. -
Ignoring Commander's auto-conversion of kebab-case —
--dry-runbecomesoptions.dryRunautomatically, and being unaware of this convention causes confusion when destructuring option objects. -
Not providing defaults in option definitions — omitting the third argument to
.option()means the help output does not document the default value, and users must read source code to understand the behavior.
Common Pitfalls
- Forgetting to call
program.parse()at the end — without it, nothing executes and the process exits silently. - Using camelCase in option names without realizing Commander auto-converts
--dry-runtooptions.dryRun— this is intentional but can confuse newcomers when destructuring.
Install this skill directly: skilldb add cli-development-skills
Related Skills
Chalk Picocolors
Style terminal output with chalk and picocolors for colored, formatted CLI text
Clack Prompts
Build beautiful interactive CLI prompts using @clack/prompts with minimal boilerplate
CLI Distribution
Package and distribute CLI tools via npm/npx, standalone binaries with pkg, and Homebrew taps
CLI Testing
Test CLI applications using subprocess execution, mock filesystems, and snapshot testing
Ink React CLI
Build rich interactive terminal UIs using Ink, a React renderer for the command line
Oclif Framework
Build production-grade, extensible CLI tools using the oclif framework with TypeScript