CLI Forge vs. clipanion
Clipanion is a class-based CLI library created by the author of Yarn. It powers Yarn Berry.
CLI Forge strengths
- Fluent API — Chainable builder pattern vs. class-based command definitions with static properties.
- Middleware — First-class middleware pipeline (example). Clipanion has no middleware; class inheritance serves a similar but more limited purpose.
- Config file support — Built-in config loading with
extends(example). - Environment variables — Declarative env var mapping to options (example).
- Object options — Typed nested objects (example). Clipanion options are flat strings/booleans/counters/arrays; numbers require typanion validators.
- Documentation generation — Built-in doc generation.
- Interactive shell — Opt-in REPL (example).
- Test harness —
TestHarnessfor parsing tests (example).
clipanion strengths
- State machine parser — Commands compile into an optimized state machine, enabling command overloading and path overlaps disambiguated by required options.
- Command proxying —
Option.Proxy()captures remaining args transparently without--. Useful for wrapper commands. - Typanion validation — Tight integration with typanion for composable runtime validation.
- Battle-tested — Powers Yarn Berry, one of the most complex CLIs in the JavaScript ecosystem.
Side-by-side example
The same CLI — a greet command with hello and goodbye subcommands — implemented in both libraries.
import { Builtins, Cli, Command, Option } from 'clipanion';
class HelloCommand extends Command {
static override paths = [['hello']];
static override usage = Command.Usage({
description: 'Say hello to someone',
});
name = Option.String('--name', 'World', {
description: 'Name to greet',
});
uppercase = Option.Boolean('--uppercase', false, {
description: 'Print greeting in uppercase',
});
async execute() {
const msg = `Hello, ${this.name}!`;
this.context.stdout.write(
(this.uppercase ? msg.toUpperCase() : msg) + '\n'
);
}
}
class GoodbyeCommand extends Command {
static override paths = [['goodbye']];
static override usage = Command.Usage({
description: 'Say goodbye to someone',
});
name = Option.String('--name', 'World', {
description: 'Name to bid farewell',
});
formal = Option.Boolean('--formal', false, {
description: 'Use formal farewell',
});
async execute() {
const msg = this.formal
? `Farewell, ${this.name}.`
: `Bye, ${this.name}!`;
this.context.stdout.write(msg + '\n');
}
}
const cli = new Cli({ binaryName: 'greet' });
cli.register(HelloCommand);
cli.register(GoodbyeCommand);
cli.register(Builtins.HelpCommand);
cli.runExit(process.argv.slice(2));
import { cli } from 'cli-forge';
cli('greet')
.command('hello', {
description: 'Say hello to someone',
builder: (args) =>
args
.option('name', {
type: 'string',
description: 'Name to greet',
default: 'World',
})
.option('uppercase', {
type: 'boolean',
description: 'Print greeting in uppercase',
default: false,
}),
handler: (args) => {
const msg = `Hello, ${args.name}!`;
console.log(args.uppercase ? msg.toUpperCase() : msg);
},
})
.command('goodbye', {
description: 'Say goodbye to someone',
builder: (args) =>
args
.option('name', {
type: 'string',
description: 'Name to bid farewell',
default: 'World',
})
.option('formal', {
type: 'boolean',
description: 'Use formal farewell',
default: false,
}),
handler: (args) => {
console.log(
args.formal ? `Farewell, ${args.name}.` : `Bye, ${args.name}!`
);
},
})
.forge();