CLI Forge vs. @effect/cli
@effect/cli is a CLI library built on the Effect ecosystem. Commands are Effect computations with typed errors, dependency injection, and structured concurrency.
CLI Forge strengths
- Standalone — Self-contained library. @effect/cli requires the Effect runtime (
effect,@effect/platform,@effect/platform-nodeor-bun), which is a significant dependency commitment. - Fluent builder API — Chainable
.option().command()pattern. @effect/cli uses separate constructors (Command.make,Options.text,Args.text) composed viapipe(). - Object options — Typed nested objects with dot-notation argument parsing (e.g.,
--config.host=X) (example). @effect/cli options are flat; they can be grouped viaOptions.all()but without dot-notation parsing. - Config file inheritance — Built-in config loading with
extends(example), write-back, and per-key provenance. @effect/cli hasConfigFileandConfigProviderbut neither supportsextendschains or write-back. - Documentation generation — Built-in
generate-docscommand. - Lower learning curve — Familiar API for developers coming from yargs or commander.
Shared strengths
- Full type-safe inference — Both provide full type inference for parsed arguments, though using very different type systems.
- Shell completions — Both generate shell completion scripts. @effect/cli has a built-in
--completionsflag. CLI Forge supports bash, zsh, fish, and PowerShell (example). - Validation — Both support validation. CLI Forge uses choices, conflicts, implications (example) and custom validators. @effect/cli uses Effect's
Schemamodule viaOptions.withSchema(). - Interactive prompting — Both support prompting for missing options. @effect/cli has
Options.withFallbackPrompt()and a built-in--wizardflag. CLI Forge uses a pluggable prompt provider system.
@effect/cli strengths
- Effect integration — Commands are Effect values with typed errors, dependency injection via layers, and structured concurrency. Fits naturally into an existing Effect application.
- Wizard mode — The
--wizardflag is built-in and requires no configuration. CLI Forge's prompting requires registering a provider.
Side-by-side example
The same CLI — a greet command with hello and goodbye subcommands — implemented in both libraries. Note the significant difference in programming model.
import { Command, Options } from '@effect/cli';
import { NodeContext, NodeRuntime } from '@effect/platform-node';
import { Console, Effect } from 'effect';
// @effect/cli models commands as Effect values with typed
// errors and dependency injection via layers.
const nameOpt = Options.text('name').pipe(
Options.withDefault('World')
);
const hello = Command.make(
'hello',
{ name: nameOpt, uppercase: Options.boolean('uppercase') },
({ name, uppercase }) => {
const msg = `Hello, ${name}!`;
return Console.log(uppercase ? msg.toUpperCase() : msg);
}
);
const goodbye = Command.make(
'goodbye',
{ name: nameOpt, formal: Options.boolean('formal') },
({ name, formal }) =>
Console.log(formal ? `Farewell, ${name}.` : `Bye, ${name}!`)
);
const greet = Command.make('greet').pipe(
Command.withSubcommands([hello, goodbye])
);
const app = Command.run(greet, {
name: 'greet',
version: '1.0.0',
});
app(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
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();