CLI tool
tj/commander.js avatar
tj/commander.js

Commander.js: a Node.js CLI parser where the option definitions are the documentation

node.js command-line interfaces made easy

28,412 stars1,774 forksJavaScriptMIT

At a glance

What is it?
Commander.js turns argument parsing, usage errors and help output into a handful of chained method calls. It fits small Node tools and multi-command CLIs alike, but its strictness and its Node 22.12.0 floor are decisions you have to accept up front.
Who is it for?
Adopt Commander.js if you are writing a Node.js CLI and want option parsing, usage errors and help text generated from one set of declarations, including multi-command programs with per-command action handlers. Skip it if your runtime is older than Node 22.12.0, or if you want a parser that accepts unknown flags, since Commander errors on them by default.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Commander.js removes: hand-rolled argv parsing

Every Node.js CLI starts the same way. You read process.argv, slice off the first two entries, walk the rest looking for strings that start with a hyphen, and try to work out whether the token after --port is its value or a positional argument. Then you write the usage message by hand, and it drifts out of sync the first time you rename a flag.

Commander.js exists to delete that layer. The README describes it as "the complete solution for node.js command-line interfaces" and says you write code to describe your command line interface, after which Commander parses arguments into options and command-arguments, displays usage errors, and implements a help system. The audience is Node developers shipping a CLI as an npm package or an internal script: the kind of program that needs --first and -s, --separator <char> rather than a full interactive shell.

The design bet is that declarations should be the single source of truth. An option is defined once with .option(), and that same definition feeds parsing, the error messages, and the generated help. There is no separate schema file and no help template to maintain.

How parsing, options and subcommands fit together

A Commander program is a Command object. You attach options with .option(), positional parameters with .argument(), and subcommands with .command(). Calling .parse() reads the process arguments; .opts() returns the parsed options as an object. Multi-word options such as --template-engine are normalized to camelCase, so the property is templateEngine.

The README's split example shows the shape: .option('--first') declares a boolean, .option('-s, --separator <char>') declares an option that takes its value from the following argument, and .argument('<string>') declares one required positional. Actions receive the parsed values. In the string-util example, the split subcommand carries its own description, its own argument, its own options and an .action() callback, and the generated help lists them under Arguments and Options headings.

Strictness is the other half of the mechanism. Commander is strict and displays an error for unrecognised options, and the README's sample run shows the suggestion machinery in action: passing --fits produces "error: unknown option '--fits'" followed by "(Did you mean --first?)". That behaviour is worth noticing before you adopt it, because it is a policy, not a bug. A typo in a flag name fails loudly instead of being silently ignored.

The repository layout backs this up. index.js and lib/ hold the implementation, typings/index.d.ts carries the TypeScript declarations, and examples/ holds roughly two dozen runnable files covering aliases, custom argument processing, help groups, nested commands and lifecycle hooks. The examples directory is the practical documentation for anything the README only names.

Installing Commander.js and writing a first subcommand

Installation is a single npm command. The README gives it as:

bash
npm install commander

The package.json in the repository sets "type": "module" and exports a single entry point, so both import and require work depending on your file extension. Note the engines field: node >=22.12.0. If your deployment target is older, this version will not run there.

The README recommends a local Command object for larger programs that may use Commander in multiple ways, including unit testing, rather than the exported global program. A minimal file looks like this:

js
import { Command } from 'commander';
const program = new Command();

program
  .name('string-util')
  .description('CLI to some JavaScript string utilities')
  .version('0.8.0');

program.command('split')
  .description('Split a string into substrings and display as an array')
  .argument('<string>', 'string to split')
  .option('--first', 'display just the first substring')
  .option('-s, --separator <char>', 'separator character', ',')
  .action((str, options) => {
    const limit = options.first ? 1 : undefined;
    console.log(str.split(options.separator, limit));
  });

program.parse();

Run it and the help is already there. The README shows `node string-util.js help split` printing a Usage line, the description, an Arguments block with the string parameter, and an Options block listing --first, -s, --separator <char> with its default of ",", and -h, --help. The default value you passed as the third argument to .option() shows up in the help text without extra work. Running `node string-util.js split --separator=- a-b-c` prints `[ 'a', 'b', 'c' ]`.

That is the whole loop: declare, parse, and let the help write itself. The examples/ directory has closer starting points for aliases, hooks and nested commands once the basic shape is in place.

Where Commander.js pushes back: strictness, defaults and Node version

The strict parser is the first real constraint. Commander errors on unrecognised options, so a wrapper script that forwards unknown flags to another tool will fail unless you configure parsing to allow it. The README has a Parsing Configuration section, but the default is rejection, and that default is what most projects will meet first.

Option values are strings unless you intervene. The separator example passes '-' on the command line and the code passes it straight to String.prototype.split. Custom option processing is documented, and the examples directory includes custom argument processing, but there is no automatic type coercion: if you want a number, you convert it yourself in the action handler.

The Node floor is the second constraint, and it is easy to miss. package.json declares node >=22.12.0. A CLI published to npm inherits that floor for anyone who installs it, so shipping Commander 15 means shipping a Node 22.12.0 requirement to your users. That is a distribution decision, not just a local one.

The third case is scale. Commander is a parser and a help generator. It does not read configuration files, does not define a plugin system, and does not manage environment-variable precedence. If your CLI needs layered configuration from files plus flags, you are writing that layer on top, and Commander will only handle the flag half.

Commander.js compared with yargs and Ink

The comparison people search for is Commander.js versus yargs, and the difference is in where the interface is defined. Commander builds the program as a chain of method calls on a Command object: .option(), .argument(), .command(), .action(). The declarations live in JavaScript, in the order you write them, and the help is derived from them.

yargs takes a configuration-object approach, where options are described in a data structure and the parser reasons over that description, including more automatic coercion and configuration-driven behaviour. If your team prefers describing options as data rather than as chained calls, yargs matches that instinct. Commander's counter-argument is that the chain is executable documentation: the option definition, its description and its default sit in one statement that the parser actually reads.

The other name that comes up is Ink, and it is not really a competitor. Ink is for building terminal user interfaces out of React components, which means interactive, redrawn output. Commander produces a conventional line-oriented CLI: parse once, run an action, print text and exit. Choosing between them is choosing between an interactive application and a command, and most projects that reach for Commander want the command.

Maintenance, upgrades and the MIT licence

The repository is not archived and the last push was on 2026-09-14, days before this writing. The most recent release listed is v15.0.0 on 2026-05-29, preceded by v15.0.0-0 on 2026-02-21 and v14.0.3 on 2026-01-31. That cadence, with a prerelease before the major, suggests the project uses prerelease tags to stage breaking changes rather than shipping them directly.

Upgrade cost is concentrated in majors. A v14 to v15 move is where behaviour changes land, so CHANGELOG.md is the file to read before bumping the version in package.json. The Node engine requirement is part of that calculation: raising Commander can raise the minimum Node version your own package declares.

The licence is MIT, declared in package.json and present as LICENSE in the repository root. MIT is permissive and imposes no copyleft obligation on your own code. That is a statement about what the repository declares, not legal advice; if licence compatibility matters to your organisation, have someone qualified review it. The package also carries a package-support.json file and a Support section in the README, including a "Commander for enterprise" entry, which is where commercial support terms would be described.

Editorial conclusion

Adopt Commander.js if you are writing a Node.js CLI and want option parsing, usage errors and help text generated from one set of declarations, including multi-command programs with per-command action handlers. Skip it if your runtime is older than Node 22.12.0, or if you want a parser that accepts unknown flags, since Commander errors on them by default. Before committing, check your Node version against the engines field in package.json and read the examples/ directory for the command shape closest to yours.

Frequently asked questions

How do I use Commander.js in a Node.js script?

Import Command from the commander package, create a Command instance, then chain .option(), .argument() and .command() calls to describe your interface before calling .parse(). The README's split example shows a program with a boolean option, a value option and one required argument, where .opts() returns the parsed options.

What is Commander.js?

It is a Node.js library for building command-line interfaces, described in its README as the complete solution for node.js command-line interfaces. It parses arguments into options and command-arguments, displays usage errors, and generates a help system from the declarations you write.

How does Commander.js compare with yargs?

Commander.js declares the interface through chained method calls on a Command object, while yargs describes options through a configuration object. Commander's help text and usage errors are derived from the same chained declarations the parser reads.

How does Commander.js compare with Ink?

Ink builds interactive terminal user interfaces out of React components, with redrawn output. Commander.js produces a conventional line-oriented CLI: parse once, run an action, print text and exit, with help generated from the declarations.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. tj/commander.js on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tj-commander-js.svg)](https://hysenlabs.com/projects/tj-commander-js)