CLI Forge vs. citty
citty is a zero-dependency CLI builder from the UnJS ecosystem, built on Node.js's native util.parseArgs.
CLI Forge strengths
- Rich option types —
number,array, andobjectoptions (example). citty supportsstring,boolean,enum, andpositional. - Validation — Choices, conflicts, implications (example), and custom validators. citty has
requiredand enum constraints. - Config files — Built-in loading with
extends(example). - Documentation generation, interactive shell (example), test harness (example) — None of these exist in citty.
Shared strengths
- Middleware / hooks — CLI Forge has a middleware pipeline (example) that transforms args before handlers run. citty takes a different approach with
setup/cleanuphooks and a plugin system for reusable lifecycle behavior. Both solve cross-cutting concerns, with different trade-offs.
citty strengths
- Zero dependencies — Uses Node.js's native
util.parseArgs. - Plugin system —
defineCittyPlugin()for reusable setup/cleanup hooks. - Lazy async commands — Subcommands can be dynamically imported for fast startup.
- Pre-1.0 flexibility — Still evolving; the API surface is minimal and focused.
Side-by-side example
The same CLI — a greet command with hello and goodbye subcommands — implemented in both libraries.
import { defineCommand, runMain } from 'citty';
const hello = defineCommand({
meta: { name: 'hello', description: 'Say hello to someone' },
args: {
name: {
type: 'string',
description: 'Name to greet',
default: 'World',
},
uppercase: {
type: 'boolean',
description: 'Print greeting in uppercase',
default: false,
},
},
run({ args }) {
const msg = `Hello, ${args.name}!`;
console.log(args.uppercase ? msg.toUpperCase() : msg);
},
});
const goodbye = defineCommand({
meta: { name: 'goodbye', description: 'Say goodbye to someone' },
args: {
name: {
type: 'string',
description: 'Name to bid farewell',
default: 'World',
},
formal: {
type: 'boolean',
description: 'Use formal farewell',
default: false,
},
},
run({ args }) {
console.log(
args.formal ? `Farewell, ${args.name}.` : `Bye, ${args.name}!`
);
},
});
const main = defineCommand({
meta: { name: 'greet', description: 'A greeting CLI' },
subCommands: { hello, goodbye },
});
runMain(main);
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();