CLI Forge vs. oclif
oclif is a full-featured CLI framework maintained by Salesforce. It powers the Heroku CLI, Salesforce CLI, and Twilio CLI.
CLI Forge strengths
- Fluent builder API — Chainable builder pattern. oclif requires class-based commands in separate files with static property declarations.
- Type accumulation — Types flow through the builder chain. In oclif, each command types its own flags/args via
this.parse()without accumulation across a command tree. - Object options — Nested, typed object options (example). oclif has no equivalent.
- Middleware — General-purpose middleware pipeline (example). oclif has lifecycle hooks but they're file-based declarations, not inline composition.
- Config file inheritance — Built-in config loading with
extends(example). oclif supports framework-level configuration but not end-user option config files. - Lightweight setup — Define a CLI in a single file. oclif is designed around code generation and scaffolding.
Shared strengths
- Test harness — CLI Forge has
TestHarness(example). oclif has@oclif/test. - Auto help generation — Both generate help text from command and flag definitions.
- Env variable support — Both allow options to be populated from environment variables. CLI Forge uses declarative mapping (example). oclif uses
envon flag definitions. - Doc generation — CLI Forge has
generate-docs. oclif generates markdown docs.
oclif strengths
- Plugin system — Mature plugin architecture where plugins can add commands and hooks, and users can install plugins at runtime.
- Distribution —
oclif packcreates installable artifacts (deb, macOS, Windows). - Enterprise-proven — Powers production CLIs at Salesforce, Heroku, Twilio, and Shopify.
- Filesystem routing — Commands are auto-discovered from the directory structure.
- JSON output — Built-in
--jsonflag support withenableJsonFlag.
Side-by-side example
The same CLI — a greet command with hello and goodbye subcommands — implemented in both libraries. Note that oclif normally uses class-based commands in separate files with scaffolding.
import { Command, Flags } from '@oclif/core';
// oclif uses class-based commands, typically in separate files.
// A real oclif project would use `oclif generate` scaffolding
// with filesystem-based command discovery. This single-file
// example adds a manual dispatcher for demonstration purposes.
class Hello extends Command {
static override description = 'Say hello to someone';
static override flags = {
name: Flags.string({
description: 'Name to greet',
default: 'World',
}),
uppercase: Flags.boolean({
description: 'Print greeting in uppercase',
default: false,
}),
};
async run() {
const { flags } = await this.parse(Hello);
const msg = `Hello, ${flags.name}!`;
this.log(flags.uppercase ? msg.toUpperCase() : msg);
}
}
class Goodbye extends Command {
static override description = 'Say goodbye to someone';
static override flags = {
name: Flags.string({
description: 'Name to bid farewell',
default: 'World',
}),
formal: Flags.boolean({
description: 'Use formal farewell',
default: false,
}),
};
async run() {
const { flags } = await this.parse(Goodbye);
this.log(
flags.formal ? `Farewell, ${flags.name}.` : `Bye, ${flags.name}!`
);
}
}
// Manual dispatcher — in a real oclif project, the framework
// handles routing via the command manifest.
(async () => {
const [cmd, ...args] = process.argv.slice(2);
if (cmd === 'hello') {
await Hello.run(args);
} else if (cmd === 'goodbye') {
await Goodbye.run(args);
} else {
console.log('Usage: greet <hello|goodbye> [options]');
console.log('Commands:');
console.log(' hello Say hello to someone');
console.log(' goodbye Say goodbye to someone');
}
})();
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();