# burgee vs the alternatives

> Measured against commander, yargs, oclif, cac and citty — every capability row marked built is in the package today, with its tests.

Source: https://burgee.interlace.tools/docs/comparison

**This page is maintained by hand, and that is the reason to distrust it most.** Four of
its rows disagreed with the suite that now measures them — an installed size 5.4x under,
an import cost that flattered us by 12 ms, a claim that burgee landed level with cac
against a measured 1.45x, and a compatibility count of 1,361 that has been
1,360 since the oracle's baseline was set. [Benchmarks](/docs/benchmarks) is generated from `npm run bench` and is the
authority; this page exists to put six frameworks in one table, which that one does not
do, and every cell here now says where it comes from.

- **⚙️ measured** by `npm run bench` on this repo, all four packages in one run on one
  machine, at the date below.
- **📖 surveyed** in [`competitor-landscape.md`](https://github.com/ofri-peretz/burgee/blob/main/.sdlc/research/competitor-landscape.md)
  §2 on a different machine at a different time. Never compare a ⚙️ against a 📖 — that is
  a comparison between two machines.

## Measured

⚙️ rows: 2026-10-19 (`66e4611`), a GitHub Actions runner (AMD EPYC 7763, 4 cores), linux/x64, Node
24.13, 52 interleaved spawns per variant. Milliseconds are a property of that machine, so the deltas below are for reading,
never for gating; the machine-independent figure is the ratio on
[Benchmarks](/docs/benchmarks).

| | **burgee** | commander | yargs | @oclif/core | cac | citty |
| :--- | ---: | ---: | ---: | ---: | ---: | ---: |
| Downloads / week | new | 508.2M | 259.3M | 10.9M | 49.4M | 30.5M |
| Runtime dependencies | **5**, none outside the burgee family | 0 | 6 | **18** | 0 | 0 |
| Installed size ⚙️ | 1607 KB | 203 KB | 515 KB | 912 KB 📖 | 40 KB | 52 KB 📖 |
| Full CLI run over bare node ⚙️ | +19.1 ms | +15.3 ms | +77.1 ms | +131 ms 📖 | +4.1 ms | +6 ms 📖 |

Three things in that table got worse when they were measured rather than asserted, and
they stay in it.

**Installed size.** burgee installs at **1607 KB**. It is our largest number, it is our least
flattering one, and it has now been wrong on this page twice in opposite directions.

First it said 104 KB, a guess at the tarball rather than a measurement. That was corrected
to **561 KB** on 2026-09-09 — the real `npm pack` unpacked size, which is what lands in a
user's `node_modules` — and then the package grew to **2.3x that figure** while the cell sat
still, because the cell was read from a *published* measurement rather than from a fresh
install. Every check on this page compared the page against that file and stayed green.

Fixed 2026-09-16: the row is now measured against the tree by CI on every run, so it cannot
fall behind again. It is checked there and not on a developer's laptop for a reason worth
stating, since it is the same class of mistake: the walk starts at a workspace link, so
locally it counts a developer's dev dependencies too and reads several hundred KB more for the
same commit. The cell is whatever `npm i burgee` puts on disk on the runner, and nothing else.

**85 KB of it arrived on 2026-09-17 and is worth naming.** `burgee/commander` now spawns
sub-binaries through `bellpull` instead of `node:child_process`, so burgee installs bellpull's
whole package — 86,113 bytes. What it buys is a Windows subcommand that actually works: upstream
commander sends every Windows spawn through `node`, which is a workaround for `spawn` not
searching `PATHEXT`, and it is simply wrong for a subcommand that is a `.cmd`, a `.bat`, or has
a shebang that is not node. The check on this row is what surfaced the cost, which is the
argument for the check.

**24 KB more arrived on 2026-09-30:** `seniority/yaml` (24,226 bytes), the YAML parser that
`seniority`'s cosmiconfig drop-in needs to read the YAML files cosmiconfig reads. burgee never
loads it — no module on burgee's path imports it, and its bundled column did not move — but it
is on disk.

**About 90 KB left on 2026-10-11.** Every family package published a declaration file for
every module, and a TypeScript consumer can only open the ones an `exports` entry reaches.
burgee alone shipped 35 that none does, 76,504 bytes of them (`migrate.d.ts` was the largest).
The build now prunes them by the closure the TypeScript compiler computes from the entry
points, and a check fails if any shipped declaration imports one that was not shipped
([D-20261011](https://github.com/ofri-peretz/burgee/blob/main/.sdlc/decisions/D-20261011-tarball-unreachable-declarations.md)).

It buys no dependency outside the burgee family and seven drop-in front ends. It is still 1607 KB, and the
bundled column on [Benchmarks](/docs/benchmarks) — what an application actually grows by —
is the number to judge us on. That one is over its own ceiling too, and says so.

**Speed.** This page previously ran two rows, an import cost and a full run, with burgee's
cells in both unsourced. The suite times one thing, a full CLI run as a process, so there
is one row. burgee does **not** land level with cac: it starts at **1.47x** cac's cold
start against a published target of at or below cac, and that target is recorded as not
met. It read 1.32x on 2026-09-09 and 1.56x on 2026-10-08, and CI runs since 2026-10-06 have
read 1.47x to 1.56x: burgee's start-up got slower in that month, and this page says so rather
than keeping the older figure. Since D-157 the gate is what is reachable: a downward-only ratchet at 1.55x
cac, set from CI's own spread and only ever lowered, which it still meets. The `burgee/yargs`
drop-in starts at 0.64x yargs (0.82x on 2026-09-09). The `burgee/commander` drop-in starts at
**1.07x** commander (1.18x on 2026-10-08, 1.08x on 2026-09-09).

These are the figures from the CI runner, which is where the suite now publishes from: the
same commit measured eleven times there put `burgee ÷ cac` inside 2.3%, where a developer
laptop under load spread the same ratio from 1.351 to 1.957 in a single day. The absolute
milliseconds are a property of that machine and are not comparable with the two 📖 columns,
which come from a survey on somebody else's. **The ratios are what the gates and bands
watch**, because a ratio taken between two spawns in the same run cancels the machine out.

Moving to that machine did not uniformly flatter us. `burgee ÷ cac` improved from 1.45x to
1.32x and commander went from a small loss to a level result; `burgee/yargs ÷ yargs` moved
against us, from 0.69x to 0.82x. The machine was chosen for its variance, before these
numbers were read.

burgee's speed comes from `node:util.parseArgs` being in the standard library, not from a
faster language: burgee is TypeScript, exactly like commander and yargs. On this run it buys
a lead over yargs, and neither parity with commander nor a lead over cac.

`@oclif/core`'s **131 ms** is a surveyed number, not one this suite reproduces, and it is
carried here rather than dropped because eighteen runtime dependencies against none outside the burgee family is the
comparison this table exists for. Treat it as the survey's, not as ours.

## Capabilities, with evidence

burgee against commander, yargs and meow, the three frameworks compat-oracle grades its drop-in paths against.
Every mark links to its evidence — a test in this repository for burgee, and for the others the
source file at the version the oracle grades, or that framework's own test suite — and
`scripts/capabilities-lock.test.ts` fails the build when a cited test or quoted source stops
saying what the cell claims. A row is here only when every cell of it can be proved.

✓ yes · ◐ partial (what is missing is said) · ✗ no · — does not apply. Every cell links to its evidence: our test or grade, or the incumbent’s source at the version compat-oracle grades.

### Agents and automation

| Capability | **burgee** | commander | yargs | meow |
| :-- | :-- | :-- | :-- | :-- |
| **`--json` envelope** — Every command answers `--json` with one `{ ok, data }` or `{ ok, error }` document on stdout, from the same declaration as its text output. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/shape.test.ts) | [✗](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [✗](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/yargs-factory.js) | [✗ meow returns the parsed flags to the program; it writes no output format of its own.](https://github.com/sindresorhus/meow/blob/v14.1.0/source/index.js) |
| **`--schema` (the CLI as data)** — Any program prints its commands, arguments, options, choices, defaults and effects as JSON, without running a handler. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/schema.test.ts) | [✗](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [✗](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/yargs-factory.js) | [✗](https://github.com/sindresorhus/meow/blob/v14.1.0/source/index.js) |
| **`--mcp` (an MCP server)** — Any program serves its commands as MCP tools over stdio, so an agent can call them without a wrapper. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/mcp.test.ts) | [✗](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [✗](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/yargs-factory.js) | [✗](https://github.com/sindresorhus/meow/blob/v14.1.0/source/index.js) |
| **Agent detection** — A program run by a coding agent knows which agent it is and turns interaction off, even when a terminal is attached. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/agent.test.ts) | [✗ No agent variable is read anywhere in the installed package.](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [✗ No agent variable is read anywhere in the installed package.](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/yargs-factory.js) | [✗](https://github.com/sindresorhus/meow/blob/v14.1.0/source/index.js) |
| **Exit-code contract** — Usage, config, auth, cancellation and runtime failures each exit with their own fixed code, so a script can branch on the code instead of the message. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/exit-code.test.ts) | [✗ Every error commander raises, an unknown option included, exits 1; a program can pass its own code to error(), but there is no set of named codes.](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [✗ Every failure yargs reports, usage or handler, exits 1.](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/usage.js) | [◐ only incorrect usage has its own code (2); there is none for config, auth or cancellation](https://github.com/sindresorhus/meow/blob/v14.1.0/source/index.js) |

### Shell and terminal

| Capability | **burgee** | commander | yargs | meow |
| :-- | :-- | :-- | :-- | :-- |
| **Completions for bash, zsh, fish and PowerShell** — Every program prints a completion script for each of the four shells, generated from the same declaration as its help. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/completions-shells.test.ts) | [✗](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [◐ bash, zsh and fish only; no PowerShell script](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/completion-templates.js) | [✗](https://github.com/sindresorhus/meow/blob/v14.1.0/readme.md) |
| **Cleanup on Ctrl+C** — A cleanup a command registered runs when the user presses Ctrl+C, and the process then ends by the signal, which a shell reports as 130. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/pty-signal.test.ts) | [✗ commander's only signal listeners forward the signal to an executable subcommand's child process.](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [✗ No signal is handled anywhere in the installed package.](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/yargs-factory.js) | [— meow parses and returns; it runs no command, so there is nothing of its own to clean up.](https://github.com/sindresorhus/meow/blob/v14.1.0/source/index.js) |
| **No help screen on a runtime error** — A command that fails prints its error, not the usage text, so the message is not buried under help it did not ask for. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/define-error.test.ts) | [✓ Help after an error is opt-in and covers commander's own usage errors; an action's error is not caught at all and escapes with its stack.](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [◐ a rejected handler prints the help screen by default; showHelpOnFail(false) stops that only by stopping it for usage errors too](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/usage.js) | [— meow parses and returns; it runs no command, so a runtime error never passes through it.](https://github.com/sindresorhus/meow/blob/v14.1.0/source/index.js) |

### Configuration

| Capability | **burgee** | commander | yargs | meow |
| :-- | :-- | :-- | :-- | :-- |
| **Config, env and flag layers with the winning source named** — Values resolve flag over env over config file over package.json over default, and `config explain` or `--explain <option>` says which layer won. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/config-explain.test.ts) | [◐ reads no config file; a program loads one itself and sets each value with the 'config' source](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [◐ no record of which layer (flag, env, config) supplied a value](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/yargs-factory.js) | [✗ meow reads package.json for the version and description only; it has no env or config-file layer.](https://github.com/sindresorhus/meow/blob/v14.1.0/readme.md) |
| **`--version` from package.json** — `--version` prints the version from the package.json that owns the entry file, unless the program declares its own. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/burgee/src/env.test.ts) | [✗ The program passes the version string to .version() itself.](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [✓](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/yargs-factory.js) | [✓](https://github.com/sindresorhus/meow/blob/v14.1.0/readme.md) |

### Compatibility

| Capability | **burgee** | commander | yargs | meow |
| :-- | :-- | :-- | :-- | :-- |
| **Passes commander's own test suite** — `burgee/commander` runs commander's own tests at v15.0.0, unedited apart from the import, so a commander program keeps working after the import changes. | [✓ 1,360 / 1,360 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/commander.json) | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/commander/tests/command.action.test.js) | [— A different API; commander's suite does not apply to it.](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/yargs-factory.js) | [— A different API; commander's suite does not apply to it.](https://github.com/sindresorhus/meow/blob/v14.1.0/readme.md) |
| **Passes yargs' own test suite** — `burgee/yargs` runs yargs' own tests at v18.2.0, unedited apart from the import, so a yargs program keeps working after the import changes. | [✓ 816 / 816 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/yargs.json) | [— A different API; yargs' suite does not apply to it.](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [✓ its own suite; the oracle's control run is 814 / 816, because two usage tests read the version from the nearest package.json, which inside a vendored copy is not yargs'](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/yargs/test/yargs.mjs) | [— A different API; yargs' suite does not apply to it.](https://github.com/sindresorhus/meow/blob/v14.1.0/readme.md) |
| **Passes meow's own test suite** — `burgee/meow` runs meow's own tests at v14.1.0, unedited apart from the import, and the count it passes is published. | [◐ 146 / 148 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/meow.json) | [— A different API; meow's suite does not apply to it.](https://cdn.jsdelivr.net/npm/commander@15.0.0/lib/command.js) | [— A different API; meow's suite does not apply to it.](https://cdn.jsdelivr.net/npm/yargs@18.2.0/build/lib/yargs-factory.js) | [✓ The oracle's control run is 146 / 148: one case imports meow's rollup build and one needs normalize-package-data, neither present in the vendored copy.](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/meow/test/test.js) |

## Capabilities across six frameworks

This table is older and hand-maintained, and covers oclif, cac and citty, which the table above
does not grade. Where the two disagree, the one above is the one with evidence: this table said
commander dumps its help on a runtime error, and commander's own source says help after an error
is off by default.

| | **burgee** | commander | yargs | oclif | cac | citty |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| One file, no build step | ✅ *(enforced by test)* | ✅ | ✅ | ❌ scaffold | ✅ | ✅ |
| **Plugins** that add commands | ✅ **built** | ❌ *(RFC #2505, closed unlanded)* | ❌ | ✅ | ❌ | ❌ |
| Plugins on *commander* syntax | ✅ **built** | — | — | — | — | — |
| `--json` envelope | ✅ **built** | ❌ | ❌ | partial | ❌ | ❌ |
| Exit-code contract | ✅ **built** | ❌ | ❌ | partial | ❌ | ❌ |
| No help dumped on a runtime error | ✅ **built** | ✅ | ❌ | ❌ | ❌ | ❌ |
| Weight budget per entry point | ✅ **built** | ❌ | ❌ | ❌ | ❌ | ❌ |
| commander compatibility | ✅ 1,360 / 1,360 of commander's own tests | — | ❌ | ❌ | ❌ | ❌ |
| yargs compatibility | ✅ 816 / 816 of yargs' own tests | ❌ | — | ❌ | ❌ | ❌ |
| `--schema` (the CLI as data) | ✅ **built** | ❌ | ❌ | manifest | ❌ | ❌ |
| `--mcp` (an MCP server) | ✅ **built** | ❌ | ❌ | ❌ | ❌ | ❌ |
| Shell completions | ✅ **built** | ❌ | partial | ✅ | ❌ | partial |
| Types derived from one schema | ✅ **built** | partial | partial | ✅ | partial | ✅ |

✅ built · 🔨 in progress

**Plugins** means a package that adds commands to a program it did not write, which is what
oclif's plugins and burgee's do. citty 0.2.2 has a `plugins` option, but each one is a pair
of `setup` and `cleanup` hooks run around a command and adds no command, so its cell stays
❌.

## The three that matter

**Plugins.** commander has no plugin system — its RFC has been open for years — and yargs
has none either. burgee has one, and it works on a program written in *commander's* syntax:
change one import and you gain a capability commander cannot sell you at any price.

**Nothing you rely on breaks.** Compatibility is graded by commander's own 1,360 tests and
yargs' 816, run in our CI, published as a pass rate that only goes up. Not the word
"compatible" — a number you can check. (This paragraph said 1,215 and the table said 1,361;
both were wrong, and the count the oracle's baseline actually holds is 1,360. It is on
[Benchmarks](/docs/benchmarks), where it is read from the oracle rather than retyped.)

**One declaration, every surface.** Help, `--json`, `--schema`, `--mcp`, completions and
types are all projections of the same manifest, so they cannot drift, and adding the next
one is an emitter rather than a feature.

## What we do not have

**Users.** commander and yargs do 767M downloads a week between them; burgee is days old.
Every number above is real and none of it matters until someone installs it. The
compatibility burn-down is published from the first commit precisely so that gap is visible
rather than glossed.

## Reproducing

The ⚙️ rows come from the suite, not from a shell loop, because a shell loop measures
whatever else the machine was doing:

```bash
git clone https://github.com/ofri-peretz/burgee && cd burgee
npm install && npx turbo run build
npm run bench -- --check
```

That writes `benchmarks/results/cli-benchmarks/<date>.json`, which carries every number on
[Benchmarks](/docs/benchmarks) along with the resolved version and path of every package it
measured. The 📖 rows are not reproduced by it; they are
[`competitor-landscape.md`](https://github.com/ofri-peretz/burgee/blob/main/.sdlc/research/competitor-landscape.md)
§2's, taken on a different machine.
