CLI Forge vs. util.parseArgs
util.parseArgs is Node.js's built-in argument parser, available since Node.js v18.3.0 (stable in v20.0.0). It is intentionally minimal — a low-level primitive for simple scripts.
CLI Forge strengths
CLI Forge provides everything that util.parseArgs deliberately excludes:
- Rich option types —
number,array, andobjectin addition to string and boolean (example).util.parseArgssupports only'string'and'boolean'. - Type inference — Fully typed parsed output.
util.parseArgsreturns untyped values. - Subcommands — Full command tree with nested subcommands, builders, and handlers.
- Help generation — Automatic help text from option definitions.
- Coercion — Automatic type coercion (strings to numbers, etc.).
- Middleware (example), config files (example), env variables (example), doc generation, interactive shell (example), test harness (example) — None of these exist in
util.parseArgs.
Shared strengths
- Validation / strict mode — Both reject unknown flags in strict mode. CLI Forge adds required, choices, conflicts, implications (example), and custom validators on top.
util.parseArgs strengths
- Zero dependencies, built-in — Part of Node.js core. No installation needed.
- Minimal API surface — A single function call with a simple config object. No abstractions to learn.
- Tokens API — Returns detailed parse tokens for custom post-processing, useful when building your own parser on top.
- No lock-in — No supply chain risk. Maintained as long as Node.js exists.
util.parseArgs is best suited for simple scripts with a handful of flags. For anything involving subcommands, validation, help text, or type safety, a library like CLI Forge will save significant effort.
Side-by-side example
The same CLI — a greet command with hello and goodbye subcommands — implemented in both approaches. Note how util.parseArgs requires entirely manual subcommand dispatch and help generation.
import { parseArgs } from 'node:util';
// Node.js util.parseArgs is deliberately minimal — it has no
// concept of subcommands, help generation, or type coercion
// beyond string/boolean. Everything else is manual.
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
name: { type: 'string', default: 'World' },
uppercase: { type: 'boolean', default: false },
formal: { type: 'boolean', default: false },
},
allowPositionals: true,
strict: true,
});
const [command] = positionals;
if (command === 'hello') {
const msg = `Hello, ${values.name}!`;
console.log(values.uppercase ? msg.toUpperCase() : msg);
} else if (command === 'goodbye') {
console.log(
values.formal
? `Farewell, ${values.name}.`
: `Bye, ${values.name}!`
);
} else {
console.log('Usage: greet <hello|goodbye> [options]');
console.log(' --name <string> Name to greet (default: World)');
console.log(' --uppercase Print greeting in uppercase');
console.log(' --formal Use formal farewell');
}
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();