tj/commander.js
> The most-installed Node.js argument parser — a small, dependency-free library for describing CLIs as a tree of commands, options, and arguments.
GitHub repo · npm: commander · License: MIT
Overview
Commander is a command-line interface library for Node.js, originally written by TJ Holowaychuk in 20111 and maintained since the late 2010s primarily by John Gee (shadowspawn) and other contributors. You declare a program's options, sub-commands, and positional arguments with a chained builder API; Commander parses process.argv, validates it, dispatches to an action handler, and generates --help output. As of 2026 it is one of the most-depended-upon packages on npm, pulled in transitively by a large share of the CLI tooling in the JavaScript ecosystem.
Its defining choice is scope restraint. Commander has zero runtime dependencies2 and deliberately does not do prompting, colored output, autocompletion, or config-file loading — those are left to other libraries. What it does cover is thorough: strict unknown-option rejection, negatable booleans, variadic and optional option-arguments, required options, custom argument coercion, sub-command trees, life-cycle hooks, and environment-variable fallbacks. The tension for adopters is between this and heavier frameworks (yargs, oclif): Commander gives you a parser and a help generator, not an application skeleton.
The library ships as ESM ("type": "module") with a CommonJS interop path and bundled TypeScript declarations3. Version 15 requires Node.js >= 22.122; older Node lines must pin to an earlier major.
Getting Started
npm install commander
// string-util.js — ESM
import { Command } from 'commander';
const program = new Command();
program
.name('string-util')
.description('CLI to some JavaScript string utilities')
.version('1.0.0');
program.command('split')
.description('Split a string into substrings')
.argument('<string>', 'string to split')
.option('-s, --separator <char>', 'separator character', ',')
.option('--first', 'display just the first substring')
.action((str, options) => {
const limit = options.first ? 1 : undefined;
console.log(str.split(options.separator, limit));
});
program.parse();
$ node string-util.js split --separator=- a-b-c
[ 'a', 'b', 'c' ]
$ node string-util.js split --oops a-b-c
error: unknown option '--oops'
Architecture / How It Works
The public surface is small — Command, Option, Argument, Help — but the model behind it is a tree of Command objects. .command() creates and links a child; sub-commands nest arbitrarily. Each node carries its own options, arguments, help configuration, and optional action handler.
Parsing is a hand-written argument scanner, not a grammar or schema. It walks argv left to right, classifying tokens as options (long, short, combined short like -ds, --opt=value), option-arguments, or operands. -- terminates option processing. Anything not consumed by a command's own options is passed down to a matched sub-command or left in program.args. Because the scanner is stateful and greedy, precedence rules matter: options with a required value consume the next token unconditionally, while optional-argument options ([value]) are non-greedy and stop at a leading dash — a distinction that produces most of the "why did my flag eat that argument" surprises.
Option names are normalized to camelCase (--template-engine → opts().templateEngine). Since v7 the recommended access path is .opts() and action-handler parameters rather than reading options as properties on the command object; the legacy property-storage behavior still exists behind storeOptionsAsProperties() for older code4. The Option class exposes the less-common features — .choices(), .env(), .default(), .preset(), .conflicts(), .implies(), .argParser() — added via .addOption().
Sub-commands come in two forms: action handlers (a callback on the same process) and stand-alone executables (Commander spawns a separate file, e.g. myprog-install, as a child process, git-style). Life-cycle hooks (preAction, postAction, preSubcommand) let you run logic around dispatch. Help is generated by the Help class, which is subclassable for custom layouts.
Production Notes
- The global
programsingleton is shared state.import { program }is convenient for one-file scripts, but the same instance persists across calls within a process. For anything unit-tested or embedded, construct a freshnew Command()per invocation instead — reusing the singleton leaks parsed option values between runs. - Commander calls
process.exit()by default. On a parse error or on--help/--version, it writes output and exits the process. That is correct for a CLI but hostile to tests and to embedding Commander inside a larger app. Use.exitOverride()to make it throw aCommanderErrorinstead, and.configureOutput()to redirect writes5. - The v7 upgrade was the big behavioral break. Command-arguments moved to being passed as explicit action-handler parameters, and reading options as properties on the command was demoted in favor of
.opts(). Migrating a large v6-or-earlier CLI is mechanical but touches every action handler4. - Stand-alone executable sub-commands resist bundling. Because they are discovered and spawned as separate files at runtime, single-file bundlers (esbuild,
pkg,ncc) and packaged binaries do not trace them. Prefer action-handler sub-commands if you bundle. - Optional-argument greediness is a recurring footgun.
--flag [value]will not consume-xbut--flag <value>will; negative numbers are a special case that is accepted as a value. Read the options-in-depth docs before relying on ambiguous forms. - Node baseline moves with majors. v15 requires Node >= 22.12; v12 required Node >= 18; v9 dropped Node 12. Pin the major to your runtime — upgrading Commander can silently raise your minimum Node.
- No prompts, colors, or completion by design. If you need interactive input or shell completion, layer separate libraries; Commander will not grow them.
When to Use / When Not
Use when:
- You want a small, stable, dependency-free parser with good
--helpgeneration. - Your CLI is a program with flags and a handful of sub-commands, driven by action handlers.
- You value a long-lived, conservative API over a batteries-included framework.
Avoid when:
- You are building a large multi-team CLI product that wants plugins, generators, and a testing harness — reach for a framework (oclif).
- You want middleware, command grouping, and rich composition semantics out of the box (yargs).
- You need only to read a flat
--key valuemap with no help system — a minimal parser is lighter.
Alternatives
- yargs/yargs — heavier, more features (middleware, command modules, built-in completion); use when you want more framework and don't mind the surface area.
- oclif/oclif — opinionated CLI framework with plugins and generators; use for large, long-lived CLI products, not small scripts.
- sindresorhus/meow — minimal ESM-first wrapper over a low-level parser; use for tiny single-purpose CLIs.
- minimistjs/minimist — bare argv-to-object parser, no help or validation; use when you want to build your own layer on top.
- cacjs/cac — small parser with a similar builder feel and built-in help; use when you want a lighter Commander-like API.
History
| Version | Date | Notes |
|---|---|---|
| 1.x | 2011 | Initial release by TJ Holowaychuk1. |
| 5.0 | 2020-03-12 | Modernized parsing; dropped older Node lines. |
| 7.0 | 2021-01-13 | Command-arguments passed to action handlers; .opts() favored over properties4. |
| 8.0 | 2021-06-23 | .argument() API, argument coercion refinements. |
| 9.0 | 2022-01-28 | Dropped Node 12. |
| 10.0 | 2023-01-14 | Node baseline raised; internal cleanups. |
| 11.0 | 2023-05-27 | Help groups and configuration additions. |
| 12.0 | 2024-02-03 | Requires Node >= 18. |
| 13.0 | 2024-12-30 | Continued API refinement. |
| 14.0 | 2025-05-16 | Help output and typing improvements. |
| 15.0 | 2026-05-29 | Requires Node >= 22.12; ESM-first2. |
References
- ^ Commander.js repository history; original author TJ Holowaychuk, first published 2011. https://github.com/tj/commander.js
- ^
package.jsonat v15.0.0 — zerodependencies,"type": "module",engines.node">=22.12.0". https://github.com/tj/commander.js/blob/master/package.json - ^ Commander README — TypeScript declarations bundled, CommonJS and ESM usage. https://github.com/tj/commander.js/blob/master/Readme.md
- ^ Commander CHANGELOG / v7 release notes — action-handler argument and options-access changes. https://github.com/tj/commander.js/blob/master/CHANGELOG.md
- ^ Commander README, "Override exit and output handling" —
.exitOverride()and.configureOutput(). https://github.com/tj/commander.js/blob/master/Readme.md#override-exit-and-output-handling
Tags
javascript, typescript, nodejs, cli, argument-parser, command-line, library, zero-dependency, esm, developer-tools