Framework Comparison
The same CLI implemented in multiple frameworks. A greet command with
two subcommands (hello and goodbye), string options, and boolean
flags — showing how each library approaches the same problem.
Each file below implements the same CLI: a greet command with hello and goodbye
subcommands, string options, and boolean flags. Compare how each framework handles
subcommand registration, option definition, and handler execution.
CLI Forge
Uses a fluent builder with full TypeScript inference — each .option() call
expands the handler's type.
1import { cli } from 'cli-forge';
2
3cli('greet')
4 .command('hello', {
5 description: 'Say hello to someone',
6 builder: (args) =>
7 args
8 .option('name', {
9 type: 'string',
10 description: 'Name to greet',
11 default: 'World',
12 })
13 .option('uppercase', {
14 type: 'boolean',
15 description: 'Print greeting in uppercase',
16 default: false,
17 }),
18 handler: (args) => {
19 const msg = `Hello, ${args.name}!`;
20 console.log(args.uppercase ? msg.toUpperCase() : msg);
21 },
22 })
23 .command('goodbye', {
24 description: 'Say goodbye to someone',
25 builder: (args) =>
26 args
27 .option('name', {
28 type: 'string',
29 description: 'Name to bid farewell',
30 default: 'World',
31 })
32 .option('formal', {
33 type: 'boolean',
34 description: 'Use formal farewell',
35 default: false,
36 }),
37 handler: (args) => {
38 console.log(
39 args.formal ? `Farewell, ${args.name}.` : `Bye, ${args.name}!`
40 );
41 },
42 })
43 .forge();yargs
Similar fluent builder API. Type inference relies on @types/yargs; complex CLIs
often need manual interfaces.
1import yargs from 'yargs';
2import { hideBin } from 'yargs/helpers';
3
4yargs(hideBin(process.argv))
5 .command(
6 'hello',
7 'Say hello to someone',
8 (yargs) =>
9 yargs
10 .option('name', {
11 type: 'string',
12 description: 'Name to greet',
13 default: 'World',
14 })
15 .option('uppercase', {
16 type: 'boolean',
17 description: 'Print greeting in uppercase',
18 default: false,
19 }),
20 (args) => {
21 const msg = `Hello, ${args.name}!`;
22 console.log(args.uppercase ? msg.toUpperCase() : msg);
23 }
24 )
25 .command(
26 'goodbye',
27 'Say goodbye to someone',
28 (yargs) =>
29 yargs
30 .option('name', {
31 type: 'string',
32 description: 'Name to bid farewell',
33 default: 'World',
34 })
35 .option('formal', {
36 type: 'boolean',
37 description: 'Use formal farewell',
38 default: false,
39 }),
40 (args) => {
41 console.log(
42 args.formal ? `Farewell, ${args.name}.` : `Bye, ${args.name}!`
43 );
44 }
45 )
46 .demandCommand(1)
47 .help()
48 .parse();commander
String-based option definitions ('--name <string>'). Type-safe options require
the separate @commander-js/extra-typings package.
1import { Command } from 'commander';
2
3const program = new Command('greet');
4
5program
6 .command('hello')
7 .description('Say hello to someone')
8 .option('--name <string>', 'Name to greet', 'World')
9 .option('--uppercase', 'Print greeting in uppercase', false)
10 .action((opts) => {
11 const msg = `Hello, ${opts.name}!`;
12 console.log(opts.uppercase ? msg.toUpperCase() : msg);
13 });
14
15program
16 .command('goodbye')
17 .description('Say goodbye to someone')
18 .option('--name <string>', 'Name to bid farewell', 'World')
19 .option('--formal', 'Use formal farewell', false)
20 .action((opts) => {
21 console.log(
22 opts.formal ? `Farewell, ${opts.name}.` : `Bye, ${opts.name}!`
23 );
24 });
25
26program.parse();oclif
Class-based commands with static flag declarations. In a real project, each command lives in its own file and is auto-discovered from the directory structure.
1import { Command, Flags } from '@oclif/core';
2
3// oclif uses class-based commands, typically in separate files.
4// A real oclif project would use `oclif generate` scaffolding
5// with filesystem-based command discovery. This single-file
6// example adds a manual dispatcher for demonstration purposes.
7
8class Hello extends Command {
9 static override description = 'Say hello to someone';
10
11 static override flags = {
12 name: Flags.string({
13 description: 'Name to greet',
14 default: 'World',
15 }),
16 uppercase: Flags.boolean({
17 description: 'Print greeting in uppercase',
18 default: false,
19 }),
20 };
21
22 async run() {
23 const { flags } = await this.parse(Hello);
24 const msg = `Hello, ${flags.name}!`;
25 this.log(flags.uppercase ? msg.toUpperCase() : msg);
26 }
27}
28
29class Goodbye extends Command {
30 static override description = 'Say goodbye to someone';
31
32 static override flags = {
33 name: Flags.string({
34 description: 'Name to bid farewell',
35 default: 'World',
36 }),
37 formal: Flags.boolean({
38 description: 'Use formal farewell',
39 default: false,
40 }),
41 };
42
43 async run() {
44 const { flags } = await this.parse(Goodbye);
45 this.log(
46 flags.formal ? `Farewell, ${flags.name}.` : `Bye, ${flags.name}!`
47 );
48 }
49}
50
51// Manual dispatcher — in a real oclif project, the framework
52// handles routing via the command manifest.
53(async () => {
54 const [cmd, ...args] = process.argv.slice(2);
55 if (cmd === 'hello') {
56 await Hello.run(args);
57 } else if (cmd === 'goodbye') {
58 await Goodbye.run(args);
59 } else {
60 console.log('Usage: greet <hello|goodbye> [options]');
61 console.log('Commands:');
62 console.log(' hello Say hello to someone');
63 console.log(' goodbye Say goodbye to someone');
64 }
65})();clipanion
Class-based commands compiled into a state machine. Powered by decorators and the typanion validation library.
1import { Builtins, Cli, Command, Option } from 'clipanion';
2
3class HelloCommand extends Command {
4 static override paths = [['hello']];
5
6 static override usage = Command.Usage({
7 description: 'Say hello to someone',
8 });
9
10 name = Option.String('--name', 'World', {
11 description: 'Name to greet',
12 });
13
14 uppercase = Option.Boolean('--uppercase', false, {
15 description: 'Print greeting in uppercase',
16 });
17
18 async execute() {
19 const msg = `Hello, ${this.name}!`;
20 this.context.stdout.write(
21 (this.uppercase ? msg.toUpperCase() : msg) + '\n'
22 );
23 }
24}
25
26class GoodbyeCommand extends Command {
27 static override paths = [['goodbye']];
28
29 static override usage = Command.Usage({
30 description: 'Say goodbye to someone',
31 });
32
33 name = Option.String('--name', 'World', {
34 description: 'Name to bid farewell',
35 });
36
37 formal = Option.Boolean('--formal', false, {
38 description: 'Use formal farewell',
39 });
40
41 async execute() {
42 const msg = this.formal
43 ? `Farewell, ${this.name}.`
44 : `Bye, ${this.name}!`;
45 this.context.stdout.write(msg + '\n');
46 }
47}
48
49const cli = new Cli({ binaryName: 'greet' });
50cli.register(HelloCommand);
51cli.register(GoodbyeCommand);
52cli.register(Builtins.HelpCommand);
53cli.runExit(process.argv.slice(2));cac
Lightweight fluent builder. Options are parsed at runtime with no compile-time type inference.
1import cac from 'cac';
2
3const cli = cac('greet');
4
5cli
6 .command('hello', 'Say hello to someone')
7 .option('--name <name>', 'Name to greet', { default: 'World' })
8 .option('--uppercase', 'Print greeting in uppercase', { default: false })
9 .action((opts) => {
10 const msg = `Hello, ${opts.name}!`;
11 console.log(opts.uppercase ? msg.toUpperCase() : msg);
12 });
13
14cli
15 .command('goodbye', 'Say goodbye to someone')
16 .option('--name <name>', 'Name to bid farewell', { default: 'World' })
17 .option('--formal', 'Use formal farewell', { default: false })
18 .action((opts) => {
19 console.log(
20 opts.formal ? `Farewell, ${opts.name}.` : `Bye, ${opts.name}!`
21 );
22 });
23
24cli.help();
25cli.parse();meow
Minimal single-function parser. Subcommands are positional arguments handled
manually with if/else.
1import meow from 'meow';
2import { pathToFileURL } from 'node:url';
3
4// meow is a minimal parser — subcommands are handled manually
5// by reading positional arguments from cli.input.
6
7const cli = meow(
8 `
9 Usage
10 $ greet <command>
11
12 Commands
13 hello Say hello to someone
14 goodbye Say goodbye to someone
15
16 Options
17 --name Name to greet (default: World)
18 --uppercase Print greeting in uppercase
19 --formal Use formal farewell
20`,
21 {
22 // meow v14 requires import.meta for ESM package resolution.
23 // Construct a compatible object from __filename for CJS contexts.
24 importMeta: { url: pathToFileURL(__filename).href } as ImportMeta,
25 flags: {
26 name: { type: 'string', default: 'World' },
27 uppercase: { type: 'boolean', default: false },
28 formal: { type: 'boolean', default: false },
29 },
30 }
31);
32
33const [command] = cli.input;
34
35if (command === 'hello') {
36 const msg = `Hello, ${cli.flags.name}!`;
37 console.log(cli.flags.uppercase ? msg.toUpperCase() : msg);
38} else if (command === 'goodbye') {
39 console.log(
40 cli.flags.formal
41 ? `Farewell, ${cli.flags.name}.`
42 : `Bye, ${cli.flags.name}!`
43 );
44} else {
45 cli.showHelp();
46}citty
Declarative command definitions from the UnJS ecosystem, built on Node.js
util.parseArgs.
1import { defineCommand, runMain } from 'citty';
2
3const hello = defineCommand({
4 meta: { name: 'hello', description: 'Say hello to someone' },
5 args: {
6 name: {
7 type: 'string',
8 description: 'Name to greet',
9 default: 'World',
10 },
11 uppercase: {
12 type: 'boolean',
13 description: 'Print greeting in uppercase',
14 default: false,
15 },
16 },
17 run({ args }) {
18 const msg = `Hello, ${args.name}!`;
19 console.log(args.uppercase ? msg.toUpperCase() : msg);
20 },
21});
22
23const goodbye = defineCommand({
24 meta: { name: 'goodbye', description: 'Say goodbye to someone' },
25 args: {
26 name: {
27 type: 'string',
28 description: 'Name to bid farewell',
29 default: 'World',
30 },
31 formal: {
32 type: 'boolean',
33 description: 'Use formal farewell',
34 default: false,
35 },
36 },
37 run({ args }) {
38 console.log(
39 args.formal ? `Farewell, ${args.name}.` : `Bye, ${args.name}!`
40 );
41 },
42});
43
44const main = defineCommand({
45 meta: { name: 'greet', description: 'A greeting CLI' },
46 subCommands: { hello, goodbye },
47});
48
49runMain(main);cleye
Declarative CLI builder with TypeScript command narrowing — checking
argv.command narrows the available flags.
1import { cli, command } from 'cleye';
2
3// cleye returns a parsed argv object with command narrowing.
4// Subcommands are defined with command() and passed to cli().
5
6const argv = cli({
7 name: 'greet',
8 commands: [
9 command({
10 name: 'hello',
11 flags: {
12 name: {
13 type: String,
14 description: 'Name to greet',
15 default: 'World',
16 },
17 uppercase: {
18 type: Boolean,
19 description: 'Print greeting in uppercase',
20 default: false,
21 },
22 },
23 }),
24 command({
25 name: 'goodbye',
26 flags: {
27 name: {
28 type: String,
29 description: 'Name to bid farewell',
30 default: 'World',
31 },
32 formal: {
33 type: Boolean,
34 description: 'Use formal farewell',
35 default: false,
36 },
37 },
38 }),
39 ],
40});
41
42if (argv.command === 'hello') {
43 const msg = `Hello, ${argv.flags.name}!`;
44 console.log(argv.flags.uppercase ? msg.toUpperCase() : msg);
45} else if (argv.command === 'goodbye') {
46 console.log(
47 argv.flags.formal
48 ? `Farewell, ${argv.flags.name}.`
49 : `Bye, ${argv.flags.name}!`
50 );
51}@effect/cli
Commands are Effect values with typed errors and dependency injection. Requires the full Effect ecosystem.
1import { Command, Options } from '@effect/cli';
2import { NodeContext, NodeRuntime } from '@effect/platform-node';
3import { Console, Effect } from 'effect';
4
5// @effect/cli models commands as Effect values with typed
6// errors and dependency injection via layers.
7
8const nameOpt = Options.text('name').pipe(
9 Options.withDefault('World')
10);
11
12const hello = Command.make(
13 'hello',
14 { name: nameOpt, uppercase: Options.boolean('uppercase') },
15 ({ name, uppercase }) => {
16 const msg = `Hello, ${name}!`;
17 return Console.log(uppercase ? msg.toUpperCase() : msg);
18 }
19);
20
21const goodbye = Command.make(
22 'goodbye',
23 { name: nameOpt, formal: Options.boolean('formal') },
24 ({ name, formal }) =>
25 Console.log(formal ? `Farewell, ${name}.` : `Bye, ${name}!`)
26);
27
28const greet = Command.make('greet').pipe(
29 Command.withSubcommands([hello, goodbye])
30);
31
32const app = Command.run(greet, {
33 name: 'greet',
34 version: '1.0.0',
35});
36
37app(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);Node.js util.parseArgs
Built-in parser with no subcommand concept. Everything beyond basic string/boolean parsing is manual.
1import { parseArgs } from 'node:util';
2
3// Node.js util.parseArgs is deliberately minimal — it has no
4// concept of subcommands, help generation, or type coercion
5// beyond string/boolean. Everything else is manual.
6
7const { values, positionals } = parseArgs({
8 args: process.argv.slice(2),
9 options: {
10 name: { type: 'string', default: 'World' },
11 uppercase: { type: 'boolean', default: false },
12 formal: { type: 'boolean', default: false },
13 },
14 allowPositionals: true,
15 strict: true,
16});
17
18const [command] = positionals;
19
20if (command === 'hello') {
21 const msg = `Hello, ${values.name}!`;
22 console.log(values.uppercase ? msg.toUpperCase() : msg);
23} else if (command === 'goodbye') {
24 console.log(
25 values.formal
26 ? `Farewell, ${values.name}.`
27 : `Bye, ${values.name}!`
28 );
29} else {
30 console.log('Usage: greet <hello|goodbye> [options]');
31 console.log(' --name <string> Name to greet (default: World)');
32 console.log(' --uppercase Print greeting in uppercase');
33 console.log(' --formal Use formal farewell');
34}All Example Files
1import cac from 'cac';
2
3const cli = cac('greet');
4
5cli
6 .command('hello', 'Say hello to someone')
7 .option('--name <name>', 'Name to greet', { default: 'World' })
8 .option('--uppercase', 'Print greeting in uppercase', { default: false })
9 .action((opts) => {
10 const msg = `Hello, ${opts.name}!`;
11 console.log(opts.uppercase ? msg.toUpperCase() : msg);
12 });
13
14cli
15 .command('goodbye', 'Say goodbye to someone')
16 .option('--name <name>', 'Name to bid farewell', { default: 'World' })
17 .option('--formal', 'Use formal farewell', { default: false })
18 .action((opts) => {
19 console.log(
20 opts.formal ? `Farewell, ${opts.name}.` : `Bye, ${opts.name}!`
21 );
22 });
23
24cli.help();
25cli.parse();
26