mrkeyoor.com_
Sat 08 Aug 21:01 UTC
npmCLI & Toolingupdated 08 Aug 2026

clipanion

Clipanion is the typed command-line framework behind Yarn. You model each command as a class, declare positional arguments and flags with Option helpers, and register one or more command paths with a Cli instance. It generates parsing rules and formatted help, supports nested and overlapping commands, can pass unknown trailing arguments to another program, and integrates with Typanion for runtime validation and coercion. The design is especially good for a TypeScript CLI with many subcommands, but it is more framework-like than a small argv parser.

Verdict

Clipanion is an excellent fit for a large TypeScript CLI whose command grammar is genuinely complicated, particularly if nested paths and option proxying matter. For a small new tool, its release-candidate latest tag, class ceremony, and quiet maintenance make Commander, cac, or Node's built-in parser easier recommendations.

API stability3/5The Command, Cli, Option, static paths, and execute concepts are consistent across the current documentation, and the repository supplies a v2-to-v3 codemod rather than pretending that earlier changes were painless. The concern is the published channel: npm's latest tag is 4.0.0-rc.4, not a final major release, and the source labels at least one processing option experimental. Pin the exact version if reproducible parsing behavior matters.
Docs4/5The linked documentation site is live and covers getting started, every option family, nested paths, help metadata, Typanion validation, execution contexts, error handling, inheritance, and the Cli methods. Examples are short and TypeScript-focused. The main weakness is version clarity: the site does not prominently distinguish the npm release candidate from earlier stable behavior, so edge cases still require reading current source types and tests.
Maintenance2/5The repository is not archived and the package is not deprecated, but both the latest npm publication and the last GitHub push occurred on September 6, 2024. The repository currently reports 42 open issues and pull requests combined. That is a long quiet period for a package whose default release remains an RC, so adopters should expect to diagnose uncommon parser behavior themselves or pin a known working build.
Ecosystem4/5Clipanion is used by Yarn, which is unusually strong real-world proof for nested commands, transparent option forwarding, formatted help, and command composition. It received 4,530,457 npm downloads in the measured week and has 1,256 GitHub stars. Typanion provides a coherent validation companion, but the surrounding plugin and tutorial ecosystem is much smaller than Commander or yargs, so integrations are more likely to be custom code.

Use it if

  • You are building a TypeScript CLI with nested commands such as workspace list or config set and want each command isolated in a class
  • You need unusual argument shapes such as options repeated into arrays, counters, required rests, or transparent proxy arguments without a double-dash separator
  • You want generated command help, version handling, typed option properties, and runtime validation from the same command definitions
  • You need commands to call other commands while overriding stdout, environment values, or application-specific context
Skip it if

Setup reality

Installation is one package command and there are no native builds, credentials, or configuration files. The setup cost is structural instead. Each action becomes a Command subclass with an async execute method, option declarations are class fields, and multiple actions need static paths plus registration in a Cli instance. TypeScript is where the API makes the most sense; JavaScript works, but gives up much of the inferred-option benefit. The current npm latest tag points to 4.0.0-rc.4, so an unpinned install opts into a release candidate. The manifest also installs Typanion as a dependency and declares it as a peer, despite the README tagline claiming no runtime dependencies. Strings are not converted to numbers on their own, optional positionals need an explicit required: false setting, and all commands are hidden from the global help list until they define static usage metadata. Built-in help and version commands are not magic either: create a Cli and register Builtins.HelpCommand and Builtins.VersionCommand. Use this.context.stdout and stderr instead of console if you want composition and testable output. runExit sets process.exitCode but deliberately does not terminate the process, so leftover handles can keep a CLI alive. Option.Rest and Option.Proxy are order-dependent and mutually exclusive, which matters when refactoring base command classes.

Patterns

Run one command with inferred process argumentsrun-single-command

import {Command, Option, runExit} from 'clipanion';

class GreetCommand extends Command {
  name = Option.String();

  async execute() {
    this.context.stdout.write(`Hello ${this.name}!\n`);
  }
}

await runExit(GreetCommand);

runExit reads process.argv, sets process.exitCode, and does not call process.exit, so open timers or sockets can still keep the program alive.

Register nested commands and an aliasregister-subcommands

import {Cli, Command} from 'clipanion';

class WorkspaceListCommand extends Command {
  static paths = [['workspace', 'list'], ['ws', 'ls']];
  async execute() {
    this.context.stdout.write('app\nweb\n');
  }
}

const cli = new Cli({binaryName: 'acme', binaryVersion: '1.0.0'});
cli.register(WorkspaceListCommand);
await cli.runExit(process.argv.slice(2));

A command may have several paths, and each path is an array of literal tokens rather than one space-separated string.

Handle invocation with no command pathdefine-default-command

class StatusCommand extends Command {
  static paths = [Command.Default, ['status']];

  async execute() {
    this.context.stdout.write('ready\n');
  }
}

Command.Default is an empty path and can coexist with named paths, allowing both acme and acme status to select this class.

Declare required strings and boolean aliasesparse-flags

class DeployCommand extends Command {
  static paths = [['deploy']];
  target = Option.String('--target', {required: true});
  dryRun = Option.Boolean('-n,--dry-run', false);

  async execute() {
    this.context.stdout.write(`${this.target}: ${this.dryRun ? 'plan' : 'apply'}\n`);
  }
}

Comma-separated descriptors create aliases. A required named option is different from a required positional and produces a parser error when absent.

Collect a repeated option into an arraycollect-repeated-options

class NotifyCommand extends Command {
  static paths = [['notify']];
  emails = Option.Array('--email', []);

  async execute() {
    for (const email of this.emails)
      this.context.stdout.write(`${email}\n`);
  }
}

Option.Array collects repeated occurrences such as --email a@example.com --email b@example.com; the empty initial value avoids undefined when none are passed.

Accept any number of positional filesparse-rest-arguments

class HashCommand extends Command {
  static paths = [['hash']];
  files = Option.Rest({required: 1});

  async execute() {
    this.context.stdout.write(`${this.files.length} files\n`);
  }
}

Rest is order-dependent, must follow earlier positional fields in the class layout, and cannot be combined with Option.Proxy.

Forward trailing arguments to another programproxy-child-arguments

import {spawn} from 'node:child_process';

class RunCommand extends Command {
  static paths = [['run']];
  tool = Option.String();
  args = Option.Proxy();

  async execute() {
    const child = spawn(this.tool, this.args, {stdio: 'inherit'});
    return await new Promise<number>((resolve, reject) => {
      child.on('error', reject);
      child.on('exit', code => resolve(code ?? 1));
    });
  }
}

Proxy stops Clipanion parsing once reached, so child flags can pass through without --. Validate or allowlist executable names when input is untrusted.

Count repeated verbosity flagscount-verbosity

class BuildCommand extends Command {
  static paths = [['build']];
  verbosity = Option.Counter('-v,--verbose', 0);

  async execute() {
    this.context.stdout.write(`level=${this.verbosity}\n`);
  }
}

Short booleans and counters can be batched, so -vvv produces 3; passing --no-verbose resets the counter to zero.

Coerce and validate a port with Typanionvalidate-number-option

import * as t from 'typanion';

const isPort = t.applyCascade(t.isNumber(), [
  t.isInteger(),
  t.isInInclusiveRange(1, 65535),
]);

class ServeCommand extends Command {
  port = Option.String('--port', '3000', {validator: isPort});
  async execute() {
    this.context.stdout.write(`port=${this.port}\n`);
  }
}

Option.String does not infer numbers from text. The validator both checks and coerces the value, and TypeScript infers the resulting number type.

Publish usage text and built-in helpadd-generated-help

import {Builtins, Cli, Command} from 'clipanion';

class CleanCommand extends Command {
  static paths = [['clean']];
  static usage = Command.Usage({
    category: 'Workspace',
    description: 'Remove generated files',
    examples: [['Clean the workspace', '$0 clean']],
  });
  async execute() {}
}

const cli = new Cli({binaryName: 'acme'});
cli.register(CleanCommand);
cli.register(Builtins.HelpCommand);

Commands without static usage are hidden from the global help listing, and the help command must be registered when constructing Cli manually.

Show a clean user-facing errorreport-usage-error

import {Command, UsageError} from 'clipanion';

class RemoveCommand extends Command {
  force = Option.Boolean('--force', false);
  async execute() {
    if (!this.force)
      throw new UsageError('Pass --force to confirm removal');
  }
}

UsageError prints the message and command usage without an internal stack trace. A normal Error is treated as an unexpected failure.

Pass typed application context into commandsinject-command-context

import type {BaseContext} from 'clipanion';

type AppContext = BaseContext & {cwd: string};

class PwdCommand extends Command<AppContext> {
  async execute() {
    this.context.stdout.write(`${this.context.cwd}\n`);
  }
}

const cli = new Cli<AppContext>({binaryName: 'acme'});
cli.register(PwdCommand);
await cli.runExit(process.argv.slice(2), {cwd: process.cwd()});

Custom contexts extend BaseContext. Clipanion fills the standard streams and environment, while every required custom key must be supplied to run or runExit.

Alternatives

PackageRegistryPick it when
commandernpmChoose it for the most familiar command and option API, wide adoption, and abundant integration examples
yargsnpmChoose it for mature command builders, middleware, completion support, and detailed validation of conventional CLIs
cacnpmChoose it for a smaller, simpler CLI builder when class-based commands and advanced argument grammars are unnecessary