# Why flagstaff

> flagstaff against ora, log-update, boxen and cli-table3, one capability per row, every cell linked to the test, grade or source that proves it.

Source: https://flagstaff.interlace.tools/docs/why-flagstaff

ora, log-update, boxen and cli-table3 each draw one thing well on a terminal. flagstaff draws
the same four things — through drop-in paths graded by each one's own test suite — on one
frame loop, and adds what a terminal-only library has no reason to have: a text form of every
component for pipes, CI logs and screen readers, and one event per state change for an agent
reading `--json`.

The table below is the whole comparison. Every mark links to its evidence: a test in this
repository for ours, and for theirs the source file of the exact version compat-oracle grades,
or that package's own test suite. `scripts/capabilities-lock.test.ts` fails the build when a
cited test no longer contains the title it is cited for, when a source no longer contains the
line it is quoted for, or when a source we say lacks something has gained it.

✓ 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.

### Output in every environment

| Capability | **flagstaff** | ora | log-update | boxen | cli-table3 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **One line per state change on a pipe** — A log file or an agent reading a pipe gets each state once, as text, instead of repainted frames or erase sequences. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/loop.test.ts) | [◐ off an interactive terminal it writes the start line and the final line; text set in between is not written](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.js) | [✗ writes erase sequences on every stream; its only isTTY check chooses synchronized output](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [— returns one string; it has no states to change](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [— returns one string; it has no states to change](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/table.js) |
| **A box that prints its text off a terminal** — Off a terminal the box component writes `title: text`, so a log or an agent reads the message rather than border characters. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/builtins.test.ts) | [— draws no boxes](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.js) | [— draws no boxes](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [✗ never checks the stream, so a pipe gets the same bordered drawing](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [— draws tables, not boxes](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/table.js) |
| **A table that prints header-and-value pairs off a terminal** — Off a terminal each row is written as `header: value` pairs, which a log, an agent or a screen reader can read without the grid. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/builtins.test.ts) | [— draws no tables](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.js) | [— draws no tables](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [— draws no tables](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [✗ never checks the stream, so a pipe gets the same grid](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/table.js) |
| **A screen-reader mode, on a terminal too** — With `CLI_ACCESSIBLE=1` the output policy chooses the accessible mode ahead of the terminal check, so a component hoisted with `hoist()` prints its plain text once per state and never redraws; the `flagstaff/ora` drop-in keeps ora's behaviour instead. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/roundel/src/policy.test.ts) | [✗ animates on any interactive terminal](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.js) | [✗ redraws on any stream](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [✗ draws the border for every reader](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [✗ draws the grid for every reader](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/table.js) |

### Agents and automation

| Capability | **flagstaff** | ora | log-update | boxen | cli-table3 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **`--json`: one NDJSON event per state transition** — A component hoisted with `{ json: true }` writes each transition as a JSON object on stderr and leaves stdout for the result, so an agent parses state instead of scraping a terminal. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/loop.test.ts) | [✗](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.js) | [✗](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [✗](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [✗](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/table.js) |
| **An injectable clock for animation** — A test drives frames with `manualClock()` instead of patching global timers, so a spinner's terminal output is the same bytes on every run. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/loop.test.ts) | [✗ frames are scheduled on the global timer](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.js) | [— draws only when called; the caller owns the timing](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [— does not animate](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [— does not animate](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/table.js) |
| **A checker for plugins before they ship** — `npx flagstaff check ./plugin.mjs` renders every contribution in all five modes side by side, and exits 1 with a code and a fix on a refusal. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/cli.test.ts) | [✗ ships no command](https://cdn.jsdelivr.net/npm/ora@9.4.1/package.json) | [— takes no styles or plugins](https://cdn.jsdelivr.net/npm/log-update@8.0.0/package.json) | [✗ ships no command](https://cdn.jsdelivr.net/npm/boxen@8.0.1/package.json) | [✗ ships no command](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/package.json) |

### One render engine

| Capability | **flagstaff** | ora | log-update | boxen | cli-table3 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Spinner, progress, tasks, box and table on one loop** — Every component is hoisted, updated and lowered the same way, and every one gets the same five output modes. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/builtins.test.ts) | [✗ a spinner only](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.js) | [✗ repaints whatever string it is given; it has no components](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [✗ a box only](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [✗ a table only](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/table.js) |
| **Custom styles through a validated plugin registry** — A spinner or border style is registered once, validated against the published JSON schema, and every component that draws one can use it by name. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/plugin.test.ts) | [◐ a custom spinner object is passed to each instance; there is no registry and no schema](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.js) | [— has no styles to register](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [◐ a custom border object is passed to each call; there is no registry](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [◐ custom border characters are passed to each table; there is no registry and no validation](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/utils.js) |
| **Every style must carry a text form** — A spinner or component without a static projection is refused at registration with `E_NO_STATIC_PROJECTION`, so nothing registered can only animate. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/plugin.test.ts) | [✗ a spinner is frames and an interval; nothing carries a text form](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.d.ts) | [— has no styles](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [— a box does not animate](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [— a table does not animate](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/table.js) |

### Safety and correctness

| Capability | **flagstaff** | ora | log-update | boxen | cli-table3 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Cursor restored on SIGINT, SIGTERM and SIGHUP** — A spinner killed with Ctrl+C gives the cursor back and the process still terminates. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/loop-signal.test.ts) | [✓ through cli-cursor, restore-cursor and signal-exit](https://cdn.jsdelivr.net/npm/cli-cursor@5.0.0/index.js) | [✓ through cli-cursor, restore-cursor and signal-exit](https://cdn.jsdelivr.net/npm/cli-cursor@5.0.0/index.js) | [— never hides the cursor](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [— never hides the cursor](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/table.js) |
| **Borders stay aligned around wide (CJK) characters** — Rows are measured in terminal columns, so a double-width character does not push a border out of line. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/builtins.test.ts) | [— draws no borders](https://cdn.jsdelivr.net/npm/ora@9.4.1/index.js) | [— draws no borders](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [✓](https://cdn.jsdelivr.net/npm/boxen@8.0.1/index.js) | [✓](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/src/utils.js) |

### Weight

| Capability | **flagstaff** | ora | log-update | boxen | cli-table3 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Every runtime dependency from the same repository** — Installing it adds closeout, linegauge, paratext and roundel, released from this repository through one pipeline, and nothing else. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/shape.test.ts) | [✗ eight dependencies from other repositories](https://cdn.jsdelivr.net/npm/ora@9.4.1/package.json) | [✗ six dependencies from other repositories](https://cdn.jsdelivr.net/npm/log-update@8.0.0/package.json) | [✗ eight dependencies from other repositories](https://cdn.jsdelivr.net/npm/boxen@8.0.1/package.json) | [✗ string-width, and @colors/colors as an optional dependency](https://cdn.jsdelivr.net/npm/cli-table3@0.6.5/package.json) |

### Compatibility

| Capability | **flagstaff** | ora | log-update | boxen | cli-table3 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Passes ora's own test suite** — `flagstaff/ora` is graded by ora 9.4.1's own tests, unedited, so changing the import keeps ora's behaviour. | [✓ 99 / 99 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/ora.json) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/ora/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/log-update/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/boxen/tests/main.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/cli-table3/test/table-test.js) |
| **Passes log-update's own test suite** — `flagstaff/log-update` is graded by log-update 8.0.0's own tests, which render every frame through a terminal emulator and assert the screen. | [✓ 99 / 99 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/log-update.json) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/ora/test.js) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/log-update/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/boxen/tests/main.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/cli-table3/test/table-test.js) |
| **Passes boxen's own test suite** — `flagstaff/boxen` is graded by boxen 8.0.1's own tests, each a snapshot of the exact characters the box comes out as. | [✓ 84 / 84 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/boxen.json) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/ora/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/log-update/test.js) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/boxen/tests/main.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/cli-table3/test/table-test.js) |
| **Passes cli-table3's own test suite** — `flagstaff/cli-table3` is graded by the cases of cli-table3 0.6.5's own suite that go through its public API. | [✓ 29 / 29 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/cli-table3.json) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/ora/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/log-update/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/boxen/tests/main.js) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/cli-table3/test/table-test.js) |

## Reading it

- **"on a pipe", "off a terminal"** mean the loop, `hoist()`. The four drop-ins keep their
  incumbents' behaviour to the byte, including off a terminal, because that is what their
  grade measures. [Incremental migration](/docs/recipes/incremental-migration) moves a program
  from one to the other a file at a time.
- **Parity rows are here too.** ora and log-update restore the cursor on a signal; boxen and
  cli-table3 measure wide characters correctly. A row where they match us is a row a reader
  would otherwise have to go and check.
- **— does not apply** is not a soft ✗. A table library that never hides the cursor has
  nothing to restore; the cell says why.

## What is not in the table

A row goes in only when every cell of it can be proved. These were left out:

- **Weight.** The per-subpath byte figures are asserted by
  [`weight.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/weight.test.ts)
  and published on [Benchmarks](https://burgee.interlace.tools/docs/benchmarks), measured the
  same way on both sides. They are not a yes-or-no capability, so they are not a row.
- **`NO_COLOR`.** flagstaff honours it through roundel's tokens, and roundel's tests prove the
  rule; no flagstaff test renders a component under it yet. ora's colour comes from chalk 5.6.2,
  whose [colour detection](https://cdn.jsdelivr.net/npm/chalk@5.6.2/source/vendor/supports-color/index.js)
  does not read `NO_COLOR`. See
  [NO_COLOR and accessibility](/docs/guides/accessibility).
- **Emoji width.** The width tests use CJK text; none uses emoji, so the row says CJK.
