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-node or -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 via pipe().
  • 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 via Options.all() but without dot-notation parsing.
  • Config file inheritance — Built-in config loading with extends (example), write-back, and per-key provenance. @effect/cli has ConfigFile and ConfigProvider but neither supports extends chains or write-back.
  • Documentation generation — Built-in generate-docs command.
  • Lower learning curve — Familiar API for developers coming from yargs or commander.

Shared strengths

@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 --wizard flag 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();

Back to comparison overview · View all framework examples