Skip to main content
Technology & EngineeringCli Development171 lines

Commander Js

Build structured CLI applications with Commander.js including commands, options, and argument parsing

Quick Summary19 lines
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 lines
Paste into your CLAUDE.md or agent config

Commander.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); } when requiredOption() 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-run becomes options.dryRun automatically, 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-run to options.dryRun — this is intentional but can confuse newcomers when destructuring.

Install this skill directly: skilldb add cli-development-skills

Get CLI access →