# flagstaff/box > Every export of flagstaff/box, with its signature and doc comment: box, boxComponent, plus 3 types. Source: https://flagstaff.interlace.tools/docs/api/box ```ts import { box, boxComponent } from 'flagstaff/box'; ``` ## Functions ### box Draw `text` in a box, as a string. The many callers who want only this want only this. ```ts function box(text: string, options?: BoxOptions): string; ``` | Parameter | Type | | :-- | :-- | | `text` | `string` | | `options` (optional) | `BoxOptions` | **Returns** `string` ### boxComponent A box as a component: the text off a terminal, the drawing on one (R1). ```ts function boxComponent(options?: BoxOptions): Component; ``` | Parameter | Type | | :-- | :-- | | `options` (optional) | `BoxOptions` | **Returns** `Component` ## Interfaces ### BoxOptions ```ts interface BoxOptions { /** A registered border's name, or a style of your own. Default `round`. */ border?: string | BorderStyle; /** Cells of padding left and right of the text, and rows above and below. Default 1 / 0. */ padding?: { x?: number; y?: number; }; /** A title written into the top border. Truncated with `…` if the box is too narrow. */ title?: string; /** Columns the whole box may occupy, borders included. Text wraps to fit. Default 80. */ width?: number; /** * A url or a path the box's text points at — `file://…` for a path a terminal should be * able to open, which is the case R12 is named after. The title is left alone: it is a * label for the box, and a link around it would claim the border is clickable too. */ href?: string; /** * The terminal the link is rendered for. Omitted, the real process is read through this * package's seam. Supply one and the whole path is pure. */ terminal?: Terminal; } ``` ### BoxState ```ts interface BoxState { text: string; title?: string; /** Per-state destination, overriding the one the component was built with. */ href?: string; } ``` ## Types ### Terminal The slice of the world paratext needs in order to answer. Derived from `linkFor` rather than imported: `paratext/link` publishes the functions, not the type, and a second specifier for a type would put `paratext` in this package's graph for something `verbatimModuleSyntax` erases anyway. ```ts type Terminal = Parameters[0]; ``` --- # flagstaff/boxen > Every export of flagstaff/boxen, with its signature and doc comment: boxen, _borderStyles, plus 7 types. Source: https://flagstaff.interlace.tools/docs/api/boxen ```ts import boxen from 'flagstaff/boxen'; import { _borderStyles } from 'flagstaff/boxen'; ``` ## Functions ### default The default export, declared as `boxen`. Draw a box around `text`, exactly as boxen 9 draws it. ```ts function boxen(input: string, options?: BoxenOptions): string; ``` | Parameter | Type | | :-- | :-- | | `input` | `string` | | `options` (optional) | `BoxenOptions` | **Returns** `string` ## Constants ### _borderStyles cli-boxes 4, as data. boxen re-exports this as `_borderStyles`, so it is part of the surface a caller can reach and not an implementation detail to be hidden. ```ts const BOXES: Record; ``` ## Interfaces ### BoxenBorderStyle ```ts interface BoxenBorderStyle { topLeft: string; top: string; topRight: string; left: string; right: string; bottomLeft: string; bottom: string; bottomRight: string; /** @deprecated boxen's own name for `left` and `right` together; a fallback for either. */ vertical?: string; /** @deprecated boxen's own name for `top` and `bottom` together; a fallback for either. */ horizontal?: string; } ``` ### BoxenOptions ```ts interface BoxenOptions { borderStyle?: string | BoxenBorderStyle; borderColor?: Color; backgroundColor?: Color; /** The border's background: a colour, `'inherit'` (the default) to take `backgroundColor`, or `undefined` for none. */ borderBackgroundColor?: Color | 'inherit' | undefined; dimBorder?: boolean; title?: string; titleColor?: Color; titleAlignment?: Alignment; footer?: string; footerAlignment?: Alignment; textAlignment?: Alignment; /** @deprecated boxen's own name for `textAlignment`, still honoured. */ align?: Alignment; padding?: number | Spacing; margin?: number | Spacing; width?: number | `${number}`; maxWidth?: number | `${number}`; height?: number | `${number}`; float?: Alignment; fullscreen?: boolean | ((columns: number, rows: number) => [number, number]); } ``` ### Spacing ```ts interface Spacing { top?: number; right?: number; bottom?: number; left?: number; } ``` ## Types ### Boxes ```ts type Boxes = typeof BOXES; ``` ### Color boxen's `Color`: one of chalk's sixteen foreground names, or a hex. ```ts type Color = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray' | 'grey' | 'blackBright' | 'redBright' | 'greenBright' | 'yellowBright' | 'blueBright' | 'magentaBright' | 'cyanBright' | 'whiteBright' | (string & Record); ``` ### CustomBorderStyle ```ts type CustomBorderStyle = BoxenBorderStyle; ``` ### Options ```ts type Options = BoxenOptions; ``` --- # flagstaff/cli-table3 > Every export of flagstaff/cli-table3, with its signature and doc comment: strlen, pad, truncate, wordWrap, hyperlink, mergeOptions and 8 more, plus 3 types. Source: https://flagstaff.interlace.tools/docs/api/cli-table3 ```ts import Table from 'flagstaff/cli-table3'; import { strlen, pad, truncate, … } from 'flagstaff/cli-table3'; ``` ## Functions ### hyperlink OSC 8 — the terminal hyperlink escape, and **not this package's to spell** (R12). The sequence is paratext's `link` capability rendered against {@link EMITTING }; this function contributes the argument order and upstream's `url || text`, and nothing else. ```ts function hyperlink(url: string, text: string): string; ``` | Parameter | Type | | :-- | :-- | | `url` | `string` | | `text` | `string` | **Returns** `string` ### makeTableLayout ```ts function makeTableLayout(rows: unknown[]): Drawable[][]; ``` | Parameter | Type | | :-- | :-- | | `rows` | `unknown[]` | **Returns** `Drawable[][]` ### mergeOptions ```ts function mergeOptions(options?: TableOptions, defaults?: TableOptions): TableOptions; ``` | Parameter | Type | | :-- | :-- | | `options` (optional) | `TableOptions` | | `defaults` (optional) | `TableOptions` | **Returns** `TableOptions` ### pad Pad to `len` columns. `right` pads on the left, which is upstream's naming, not a typo. ```ts function pad(str: string, len: number, padChar: string, dir?: string): string; ``` | Parameter | Type | | :-- | :-- | | `str` | `string` | | `len` | `number` | | `padChar` | `string` | | `dir` (optional) | `string` | **Returns** `string` ### strlen Widest line, measured with SGR removed. Upstream's `strlen`. ```ts function strlen(str: unknown): number; ``` | Parameter | Type | | :-- | :-- | | `str` | `unknown` | **Returns** `number` ### truncate ```ts function truncate(str: string, desiredLength: number, truncateChar?: string): string; ``` | Parameter | Type | | :-- | :-- | | `str` | `string` | | `desiredLength` | `number` | | `truncateChar` (optional) | `string` | **Returns** `string` ### wordWrap ```ts function wordWrap(maxLength: number, input: string, wrapOnWordBoundary?: boolean): string[]; ``` | Parameter | Type | | :-- | :-- | | `maxLength` | `number` | | `input` | `string` | | `wrapOnWordBoundary` (optional) | `boolean` | **Returns** `string[]` ## Classes ### default The default export, declared as `Table`. A table. Extends `Array`, because cli-table3 does and its callers `push` rows onto it. ```ts class Table extends Array { readonly options: ResolvedOptions; readonly messages?: string[]; constructor(opts?: TableOptions); /** Clears the module-level message log. The suite calls it after every case. */ static reset(): void; toString(): string; get width(): number; } ``` ### Cell ```ts class Cell implements Drawable { x: number; y: number; colSpan: number; rowSpan: number; content: string; options: Record; chars: TableChars; truncate: string; paddingLeft: number; paddingRight: number; head?: string[] | undefined; border?: string[] | undefined; fixedWidth?: number | null | undefined; lines: string[]; desiredWidth: number; desiredHeight: number; widths: number[]; heights: number[]; width: number; height: number; hAlign?: string | undefined; vAlign?: string | undefined; drawRight: boolean; cells?: Drawable[][] | undefined; href?: string | undefined; constructor(options?: unknown); mergeTableOptions(tableOptions: ResolvedOptions, cells: Drawable[][]): void; computeLines(tableOptions: ResolvedOptions): string[]; /** * The href is deliberately *not* applied here. `drawLine` truncates the line it is given, * and a hyperlink wrapped around the content before that point is cut through the middle * of its own URL — upstream's issue #338, which emits `\x1b]8;;http://e…` and a terminal * escape that never closes. The link goes on after truncation instead, around whatever * text actually survived. */ wrapLines(computedLines: string[]): string[]; init(tableOptions: ResolvedOptions): void; draw(lineNum: number | string, spanningCell?: number): string; drawTop(drawRight: boolean): string; private topLeftChar; /** The left edge, which a row-spanning neighbour turns into a `rightMid`. */ private leftEdge; drawLine(lineNum: number, drawRight: boolean, forceTruncationSymbol: boolean, spanningCell?: number): string; private stylizeLine; drawBottom(drawRight: boolean): string; drawEmpty(drawRight: boolean, spanningCell?: number): string; } ``` ### ColSpanCell A placeholder that draws nothing, holding the place of a column-spanned cell. ```ts class ColSpanCell implements Drawable { x: number; y: number; colSpan: number; rowSpan: number; draw(lineNum?: number | string): string; init(): void; mergeTableOptions(): void; } ``` ### module.exports What `require()` returns: the default export, `Table`, under the name Node's `require(esm)` reads. ### RowSpanCell A placeholder that defers to the cell above it, offset by the rows in between. ```ts class RowSpanCell implements Drawable { readonly originalCell: Cell; x: number; y: number; colSpan: number; rowSpan: number; private cellOffset; private offset; constructor(originalCell: Cell); init(tableOptions: ResolvedOptions): void; draw(lineNum: number | string): string; mergeTableOptions(): void; } ``` ### Table A table. Extends `Array`, because cli-table3 does and its callers `push` rows onto it. ```ts class Table extends Array { readonly options: ResolvedOptions; readonly messages?: string[]; constructor(opts?: TableOptions); /** Clears the module-level message log. The suite calls it after every case. */ static reset(): void; toString(): string; get width(): number; } ``` ## Constants ### computeHeights ```ts const computeHeights: (vals: number[], table: Drawable[][]) => void; ``` ### computeWidths ```ts const computeWidths: (vals: number[], table: Drawable[][]) => void; ``` ## Interfaces ### TableChars ```ts interface TableChars { [name: string]: string; } ``` ### TableOptions ```ts interface TableOptions { chars?: Partial; truncate?: string; colWidths?: (number | null)[]; rowHeights?: (number | null)[]; colAligns?: (string | undefined)[]; rowAligns?: (string | undefined)[]; style?: TableStyle; head?: unknown[]; wordWrap?: boolean; /** cli-table3's older name for `wordWrap`, still honoured. */ textWrap?: boolean; wrapOnWordBoundary?: boolean; } ``` ### TableStyle ```ts interface TableStyle { 'padding-left'?: number; 'padding-right'?: number; head?: string[]; border?: string[]; compact?: boolean; } ``` --- # flagstaff/diff > Every export of flagstaff/diff, with its signature and doc comment: diff, plus 1 type. Source: https://flagstaff.interlace.tools/docs/api/diff ```ts import { diff } from 'flagstaff/diff'; ``` ## Functions ### diff A unified diff: unchanged off a terminal, numbered and coloured on one. ```ts function diff(): Component; ``` **Returns** `Component` ## Interfaces ### DiffState ```ts interface DiffState { /** A unified diff, as `git diff` or `diff -u` prints it. */ diff: string; } ``` --- # flagstaff/import > Every export of flagstaff/import, with its signature and doc comment: fromCliSpinners, fromCliBoxes, plus 4 types. Source: https://flagstaff.interlace.tools/docs/api/import `flagstaff/import` (R11) — the two data corpora the ecosystem already has, as plugins. ```js import cliSpinners from 'cli-spinners'; import { fromCliSpinners } from 'flagstaff/import'; import { register } from 'flagstaff/plugin'; register(fromCliSpinners(cliSpinners)); spinner('moon'); ``` ```ts import { fromCliSpinners, fromCliBoxes } from 'flagstaff/import'; ``` ## Functions ### fromCliBoxes Turn cli-boxes' `boxes.json` into a plugin. Its shape is already ours, so this is a copy. ```ts function fromCliBoxes(corpus: Record, { name }?: FromCliBoxesOptions): Plugin; ``` | Parameter | Type | | :-- | :-- | | `corpus` | `Record` | | `{ name }` (optional) | `FromCliBoxesOptions` | **Returns** `Plugin` ### fromCliSpinners Turn cli-spinners' `spinners.json` into a plugin. The corpus stays the caller's. ```ts function fromCliSpinners(corpus: Record, { name, staticFor }?: FromCliSpinnersOptions): Plugin; ``` | Parameter | Type | | :-- | :-- | | `corpus` | `Record` | | `{ name, staticFor }` (optional) | `FromCliSpinnersOptions` | **Returns** `Plugin` ## Interfaces ### CliSpinner cli-spinners' shape: no `static`, because it has no projection for a pipe. ```ts interface CliSpinner { frames: string[]; interval?: number; } ``` ### FromCliBoxesOptions ```ts interface FromCliBoxesOptions { name?: string; } ``` ### FromCliSpinnersOptions ```ts interface FromCliSpinnersOptions { /** The plugin's name, which is what `flagstaff check` and the gallery label it with. */ name?: string; /** * The static projection for a style, given its name and its frames. Default: `'…'` for * every one of them. */ staticFor?: (name: string, spinner: CliSpinner) => string; } ``` ## Types ### CliBox cli-boxes' shape — the same eight keys `BorderStyle` has, which is why it imports whole. ```ts type CliBox = BorderStyle; ``` --- # flagstaff > Every export of flagstaff, with its signature and doc comment: hoist, manualClock, frameWriter, box, boxComponent, fromCliSpinners and 14 more, plus 32 types. Source: https://flagstaff.interlace.tools/docs/api flagstaff — the staff the flag flies from. Re-exports only; each subpath is its own entry and costs only itself, so prefer `flagstaff/spinner` when that is all you need. ```ts import { hoist, manualClock, frameWriter, … } from 'flagstaff'; ``` ## Functions ### progress A progress bar: `12/30 files · 40%` off a terminal, a drawn bar on one. This is the root entry's, which always draws; `flagstaff/progress` reports to an MCP client instead, inside a call that asked. ```ts function progress({ width, glyphs }?: ProgressOptions): Component; ``` | Parameter | Type | | :-- | :-- | | `{ width, glyphs }` (optional) | `ProgressOptions` | **Returns** `Component` ## Re-exported Documented on the page of the entry point that declares them. | Export | Kind | Documented in | | :-- | :-- | :-- | | `box` | function | [`flagstaff/box`](/docs/api/box#box) | | `boxComponent` | function | [`flagstaff/box`](/docs/api/box#boxcomponent) | | `frameWriter` | function | [`flagstaff/loop`](/docs/api/loop#framewriter) | | `fromCliBoxes` | function | [`flagstaff/import`](/docs/api/import#fromcliboxes) | | `fromCliSpinners` | function | [`flagstaff/import`](/docs/api/import#fromclispinners) | | `glyph` | function | [`flagstaff/plugin`](/docs/api/plugin#glyph) | | `hoist` | function | [`flagstaff/loop`](/docs/api/loop#hoist) | | `lookupBorder` | function | [`flagstaff/plugin`](/docs/api/plugin#lookupborder) | | `lookupSpinner` | function | [`flagstaff/plugin`](/docs/api/plugin#lookupspinner) | | `manualClock` | function | [`flagstaff/loop`](/docs/api/loop#manualclock) | | `register` | function | [`flagstaff/plugin`](/docs/api/plugin#register) | | `registered` | function | [`flagstaff/plugin`](/docs/api/plugin#registered) | | `spinner` | function | [`flagstaff/spinner`](/docs/api/spinner#spinner) | | `table` | function | [`flagstaff/table`](/docs/api/table#table) | | `tableComponent` | function | [`flagstaff/table`](/docs/api/table#tablecomponent) | | `tasks` | function | [`flagstaff/tasks`](/docs/api/tasks#tasks) | | `validate` | function | [`flagstaff/plugin`](/docs/api/plugin#validate) | | `PluginError` | class | [`flagstaff/plugin`](/docs/api/plugin#pluginerror) | | `CONTRACT` | const | [`flagstaff/plugin`](/docs/api/plugin#contract) | | `BorderStyle` | interface | [`flagstaff/plugin`](/docs/api/plugin#borderstyle) | | `BoxOptions` | interface | [`flagstaff/box`](/docs/api/box#boxoptions) | | `BoxState` | interface | [`flagstaff/box`](/docs/api/box#boxstate) | | `CliSpinner` | interface | [`flagstaff/import`](/docs/api/import#clispinner) | | `Clock` | interface | [`flagstaff/loop`](/docs/api/loop#clock) | | `ColumnOptions` | interface | [`flagstaff/table`](/docs/api/table#columnoptions) | | `Component` | interface | [`flagstaff/plugin`](/docs/api/plugin#component) | | `FrameWriter` | interface | [`flagstaff/loop`](/docs/api/loop#framewriter) | | `FromCliBoxesOptions` | interface | [`flagstaff/import`](/docs/api/import#fromcliboxesoptions) | | `FromCliSpinnersOptions` | interface | [`flagstaff/import`](/docs/api/import#fromclispinnersoptions) | | `Hoisted` | interface | [`flagstaff/loop`](/docs/api/loop#hoisted) | | `Linked` | interface | [`flagstaff/table`](/docs/api/table#linked) | | `ManualClock` | interface | [`flagstaff/loop`](/docs/api/loop#manualclock) | | `Plugin` | interface | [`flagstaff/plugin`](/docs/api/plugin#plugin) | | `ProgressOptions` | interface | [`flagstaff/progress`](/docs/api/progress#progressoptions) | | `ProgressState` | interface | [`flagstaff/progress`](/docs/api/progress#progressstate) | | `Runtime` | interface | [`flagstaff/loop`](/docs/api/loop#runtime) | | `SpinnerDef` | interface | [`flagstaff/plugin`](/docs/api/plugin#spinnerdef) | | `SpinnerState` | interface | [`flagstaff/spinner`](/docs/api/spinner#spinnerstate) | | `TableOptions` | interface | [`flagstaff/table`](/docs/api/table#tableoptions) | | `TableState` | interface | [`flagstaff/table`](/docs/api/table#tablestate) | | `Task` | interface | [`flagstaff/tasks`](/docs/api/tasks#task) | | `TasksOptions` | interface | [`flagstaff/tasks`](/docs/api/tasks#tasksoptions) | | `TasksState` | interface | [`flagstaff/tasks`](/docs/api/tasks#tasksstate) | | `Writer` | interface | [`flagstaff/loop`](/docs/api/loop#writer) | | `Cell` | type | [`flagstaff/table`](/docs/api/table#cell) | | `CliBox` | type | [`flagstaff/import`](/docs/api/import#clibox) | | `PluginErrorCode` | type | [`flagstaff/plugin`](/docs/api/plugin#pluginerrorcode) | | `Row` | type | [`flagstaff/table`](/docs/api/table#row) | | `SpinnerStatus` | type | [`flagstaff/spinner`](/docs/api/spinner#spinnerstatus) | | `TaskStatus` | type | [`flagstaff/tasks`](/docs/api/tasks#taskstatus) | | `Terminal` | type | [`flagstaff/box`](/docs/api/box#terminal) | --- # flagstaff/log-tail > Every export of flagstaff/log-tail, with its signature and doc comment: logTail, plus 3 types. Source: https://flagstaff.interlace.tools/docs/api/log-tail ```ts import { logTail } from 'flagstaff/log-tail'; ``` ## Functions ### logTail A log tail: the whole stream, appended, off a terminal; its last `height` rows on one. ```ts function logTail({ height }?: LogTailOptions): Component; ``` | Parameter | Type | | :-- | :-- | | `{ height }` (optional) | `LogTailOptions` | **Returns** `Component` ## Interfaces ### LogTailOptions ```ts interface LogTailOptions { /** Rows the tail shows on a terminal. Default 5; anything below 1 is 1. */ height?: number; } ``` ### LogTailState ```ts interface LogTailState { /** Every entry the stream has produced so far, oldest first. */ lines: readonly LogLine[]; } ``` ## Types ### LogLine One entry of the stream: a line of output, or the start of a step. ```ts type LogLine = string | { step: string; }; ``` --- # flagstaff/log-update > Every export of flagstaff/log-update, with its signature and doc comment: createLogUpdate, logUpdateStderr, logUpdate, plus 4 types. Source: https://flagstaff.interlace.tools/docs/api/log-update ```ts import logUpdate from 'flagstaff/log-update'; import { createLogUpdate, logUpdateStderr } from 'flagstaff/log-update'; ``` ## Functions ### createLogUpdate A renderer bound to one stream. `logUpdate` is this over the runtime's stdout. ```ts function createLogUpdate(stream: LogUpdateStream, { showCursor: keepCursor, defaultWidth, defaultHeight }?: LogUpdateOptions): LogUpdate; ``` | Parameter | Type | | :-- | :-- | | `stream` | `LogUpdateStream` | | `{ showCursor: keepCursor, defaultWidth, defaultHeight }` (optional) | `LogUpdateOptions` | **Returns** `LogUpdate` ## Constants ### default The default export, declared as `logUpdate`. ```ts const logUpdate: LogUpdate; ``` ### logUpdateStderr ```ts const logUpdateStderr: LogUpdate; ``` ## Interfaces ### LogUpdate ```ts interface LogUpdate { (...text: unknown[]): void; /** Erase the current frame, leaving nothing behind. */ clear(): void; /** Give up the rows: the frame stays on screen and the next render starts fresh. */ done(): void; /** Replace the frame with text that stays, unclipped, and start fresh after it. */ persist(...text: unknown[]): void; } ``` ### LogUpdateOptions ```ts interface LogUpdateOptions { /** Leave the cursor visible while a frame is up. Default false. */ showCursor?: boolean; /** Columns to assume when the stream reports none. Default 80. */ defaultWidth?: number; /** Rows to assume when the stream reports none. Default 24. */ defaultHeight?: number; } ``` ### LogUpdateStream What log-update writes to: the runtime's stdout by default, anything stream-shaped in a test. ```ts interface LogUpdateStream { write(chunk: string): unknown; columns?: number; rows?: number; isTTY?: boolean; } ``` ## Types ### Options ```ts type Options = LogUpdateOptions; ``` --- # flagstaff/loop > Every export of flagstaff/loop, with its signature and doc comment: hoist, manualClock, frameWriter, plus 6 types. Source: https://flagstaff.interlace.tools/docs/api/loop The frame loop (R1). `hoist()` raises a component, `update()` changes its state, `lower()` takes it down and leaves the static line behind. Which of the four projections runs is the output policy's decision (`roundel/policy`), read once from the runtime; this file never looks at a stream's `isTTY` or an environment variable itself (U2). ```ts import { hoist, manualClock, frameWriter } from 'flagstaff/loop'; ``` ## Functions ### frameWriter The family's one frame writer over `out` — see `FrameWriter`. ```ts function frameWriter(out: Writer): FrameWriter; ``` | Parameter | Type | | :-- | :-- | | `out` | `Writer` | **Returns** `FrameWriter` ### hoist Hoist a component in its initial state. Pass `{ json: true }` when the run was asked for `--json`. ```ts function hoist(component: Component, rt: Runtime, initial: S, opts?: ModeOptions): Hoisted; ``` | Parameter | Type | | :-- | :-- | | `component` | `Component` | | `rt` | `Runtime` | | `initial` | `S` | | `opts` (optional) | `ModeOptions` | **Returns** `Hoisted` ### manualClock A clock that moves only when told (R9): the same frames every run, so a spinner snapshots. ```ts function manualClock(start?: number): ManualClock; ``` | Parameter | Type | | :-- | :-- | | `start` (optional) | `number` | **Returns** `ManualClock` ## Interfaces ### Clock Time as the loop sees it: burgee's `Runtime.clock` satisfies it, so does `manualClock()`. ```ts interface Clock { now(): number; schedule(fn: () => void, ms: number): () => void; } ``` ### FrameWriter The frame-writing seam (controlroom R3, R16). `paint` takes the whole frame, one string per line, and writes only what changed since the last paint, wrapped in synchronized output; a frame identical to the last writes nothing. `release` forgets the painted frame without erasing it: what is on screen becomes scrollback, and the next paint starts at the cursor. ```ts interface FrameWriter { paint(lines: readonly string[]): void; release(): void; } ``` ### Hoisted ```ts interface Hoisted { readonly mode: OutputMode; update(state: S): void; /** Lower the flag: the final state's static projection is what stays on screen. */ lower(state?: S): void; } ``` ### ManualClock ```ts interface ManualClock extends Clock { /** Advance by `ms`, running every callback that falls due, earliest first. */ tick(ms: number): void; } ``` ### Runtime What the loop needs from the world: the policy's slice plus the two streams and a clock. ```ts interface Runtime extends PolicyRuntime { stdout: Writer; stderr: Writer; clock: Clock; } ``` ### Writer ```ts interface Writer { write(chunk: string): unknown; /** * How wide the terminal is, read at every paint so a resize is honoured. A frame wider than * this wraps on screen, and the rows it wraps onto have to be erased with the rest; a Node * TTY stream carries it, and a writer without it is taken to be `DEFAULT_COLUMNS` wide. */ readonly columns?: number | undefined; /** * How tall the terminal is, read at every paint like the width (frames-fit-the-screen R1). * A writer without it — a pipe — is unbounded, and no frame is clipped. */ readonly rows?: number | undefined; } ``` --- # flagstaff/markdown > Every export of flagstaff/markdown, with its signature and doc comment: markdownBlocks, markdown, plus 3 types. Source: https://flagstaff.interlace.tools/docs/api/markdown ```ts import { markdownBlocks, markdown } from 'flagstaff/markdown'; ``` ## Functions ### markdown Markdown streamed in: its committed source off a terminal, the document so far drawn on one. ```ts function markdown(): Component; ``` **Returns** `Component` ### markdownBlocks The blocks committed so far. Only complete lines are read — a line is complete when a newline follows it, or when the stream is `done` — which is what makes the answer the same wherever the stream was cut. ```ts function markdownBlocks(text: string, done?: boolean): MarkdownBlock[]; ``` | Parameter | Type | | :-- | :-- | | `text` | `string` | | `done` (optional) | `boolean` | **Returns** `MarkdownBlock[]` ## Interfaces ### MarkdownBlock ```ts interface MarkdownBlock { kind: MarkdownBlockKind; /** The block's source, its lines as they arrived. */ source: string; /** Where the block's source ends in the stream: `text.slice(0, end)` is committed. */ end: number; } ``` ### MarkdownState ```ts interface MarkdownState { /** Everything the stream has produced so far. */ text: string; /** The stream has ended: the last block is committed too. */ done?: boolean; } ``` ## Types ### MarkdownBlockKind ```ts type MarkdownBlockKind = 'heading' | 'paragraph' | 'list' | 'code'; ``` --- # flagstaff/ora > Every export of flagstaff/ora, with its signature and doc comment: ora, oraPromise, spinners, Ora, plus 10 types. Source: https://flagstaff.interlace.tools/docs/api/ora ```ts import ora from 'flagstaff/ora'; import { oraPromise, spinners, Ora } from 'flagstaff/ora'; ``` ## Functions ### default The default export, declared as `ora`. ```ts function ora(options?: Options | string): Ora; ``` | Parameter | Type | | :-- | :-- | | `options` (optional) | `Options \| string` | **Returns** `Ora` ### oraPromise Run an action under a spinner, and land on the right symbol whichever way it ends. The action's own outcome wins: a `failText` that throws, or a persist the stream cannot write, never replaces the action's error, and a persist that fails never discards its result. A `successText` that throws is the caller's error and is not drawn as a failure. ```ts function oraPromise(action: Promise | ((spinner: Ora) => Promise), options?: PromiseOptions | string | null): Promise; ``` | Parameter | Type | | :-- | :-- | | `action` | `Promise \| ((spinner: Ora) => Promise)` | | `options` (optional) | `PromiseOptions \| string \| null` | **Returns** `Promise` ## Classes ### Ora ```ts class Ora { #private; constructor(options?: Options | string); get indent(): number; set indent(indent: number | undefined); get interval(): number; get spinner(): SpinnerDefinition; set spinner(spinner: SpinnerDefinition | string | undefined); get text(): string; set text(value: string | undefined); get prefixText(): Affix; set prefixText(value: Affix | undefined); get suffixText(): Affix; set suffixText(value: Affix | undefined); get isSpinning(): boolean; get color(): Color | false | undefined; set color(value: Color | false | undefined); get isEnabled(): boolean; set isEnabled(value: boolean); get isSilent(): boolean; set isSilent(value: boolean); frame(): string; clear(): this; /** * A stream without the readline cursor methods — a CI log stream that reports a TTY — gets * the frame written where the cursor is. Unfinished external output is left alone until its * newline, with no timeout: resized rows cannot be counted reliably, and a replay would * duplicate rows already in scrollback. */ render(): this; start(text?: string): this; stop(): this; succeed(text?: string): this; fail(text?: string): this; warn(text?: string): this; info(text?: string): this; stopAndPersist(options?: PersistOptions | null): this; } ``` ## Constants ### spinners Every spinner cli-spinners ships, unedited — ora re-exports it and so do we. ```ts const spinners: Record; ``` ## Interfaces ### Options ```ts interface Options { text?: string; prefixText?: Affix; suffixText?: Affix; spinner?: SpinnerDefinition | string; color?: Color | false; hideCursor?: boolean; indent?: number; interval?: number | undefined; stream?: OraStream; isEnabled?: boolean; isSilent?: boolean; discardStdin?: boolean; } ``` ### OraStream What ora writes to: the runtime's stderr by default, anything stream-shaped in a test. ```ts interface OraStream { write(chunk: string, encoding?: unknown, callback?: unknown): boolean; isTTY?: boolean; columns?: number; rows?: number; cursorTo?(x: number): unknown; moveCursor?(dx: number, dy: number): unknown; clearLine?(direction: number): unknown; once?(event: string, listener: () => void): unknown; removeListener?(event: string, listener: () => void): unknown; } ``` ### PersistOptions ```ts interface PersistOptions { symbol?: string; text?: string; prefixText?: Affix; suffixText?: Affix; } ``` ### PromiseOptions ```ts interface PromiseOptions extends Options { successText?: string | ((result: T) => string); failText?: string | ((error: unknown) => string); successSymbol?: string; failSymbol?: string; } ``` ### SpinnerDefinition ```ts interface SpinnerDefinition { interval?: number; frames: string[]; } ``` ## Types ### Affix A prefix or suffix, resolved on every frame when it is a function. ```ts type Affix = string | (() => string); ``` ### Color ```ts type Color = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray'; ``` ### PrefixTextGenerator ```ts type PrefixTextGenerator = () => string; ``` ### Spinner ```ts type Spinner = SpinnerDefinition; ``` ### SuffixTextGenerator ```ts type SuffixTextGenerator = () => string; ``` --- # flagstaff/plugin > Every export of flagstaff/plugin, with its signature and doc comment: validate, register, registered, lookupSpinner, lookupBorder, glyph and 2 more, plus 5 types. Source: https://flagstaff.interlace.tools/docs/api/plugin ```ts import { validate, register, registered, … } from 'flagstaff/plugin'; ``` ## Functions ### glyph A glyph by meaning; the built-ins define every meaning the built-in components use. ```ts function glyph(meaning: string): string; ``` | Parameter | Type | | :-- | :-- | | `meaning` | `string` | **Returns** `string` ### lookupBorder A border by name, for `box()`; every style the built-ins ship is registered like any other. ```ts function lookupBorder(name: string): BorderStyle; ``` | Parameter | Type | | :-- | :-- | | `name` | `string` | **Returns** `BorderStyle` ### lookupSpinner A spinner by name, or a refusal that lists the names that exist. ```ts function lookupSpinner(name: string): SpinnerDef; ``` | Parameter | Type | | :-- | :-- | | `name` | `string` | **Returns** `SpinnerDef` ### register The only wiring (R4, U9): validate, then keep every key this package understands. A later plugin's entry replaces an earlier one of the same name, so a user overrides a built-in by registering their own — the built-ins go through this same door first. ```ts function register(plugin: unknown): void; ``` | Parameter | Type | | :-- | :-- | | `plugin` | `unknown` | **Returns** `void` ### registered A copy of what has been registered — the docs gallery and `flagstaff check` are projections of this. A copy rather than the registry itself, because `Readonly` freezes the property bindings and not the `Map`s behind them: handing the live registry out made `set`, `delete` and `clear` a second door beside `register()`, through which a spinner with no `static` — or a component with no projection at all — could be put in without ever meeting `validate()` (#58). U3's refusal has to be structural to mean anything, so there is one way in. The values are the frozen objects `register()` stored, so nothing reached through here writes back. ```ts function registered(): Readonly; ``` **Returns** `Readonly` ### validate Refuse what the schema refuses, and say why. A missing `static` gets its own code and fix, because it is the one mistake a plugin author makes on purpose (U3). ```ts function validate(plugin: unknown): asserts plugin is Plugin; ``` | Parameter | Type | | :-- | :-- | | `plugin` | `unknown` | **Returns** `asserts plugin is Plugin` ## Classes ### PluginError A refused plugin says what is wrong, where, and what to do about it. ```ts class PluginError extends Error { readonly code: PluginErrorCode; readonly fix: string; constructor(code: PluginErrorCode, message: string, fix: string); } ``` ## Constants ### CONTRACT ```ts const CONTRACT = 1; ``` ## Interfaces ### BorderStyle A border style, in cli-boxes' shape exactly, so that corpus imports unchanged. ```ts interface BorderStyle { topLeft: string; top: string; topRight: string; left: string; right: string; bottomLeft: string; bottom: string; bottomRight: string; } ``` ### Component What the loop hoists: a static projection, and optionally the animated form. ```ts interface Component { name: string; static(state: S): string; frame?(t: number, state: S): string; /** Milliseconds between repaints when `frame` is given; `DEFAULT_INTERVAL` otherwise. */ interval?: number; /** * The two states this component is *shown* with by `flagstaff check` and the docs gallery. * The loop never reads it: a component's real state comes from the program. Declared here * because a grader that invents a state renders the wrong thing and says `ok` (#59). */ sample?: { running: S; done: S; }; } ``` ### Plugin The whole plugin contract. Keys another package in the family understands are kept for it. ```ts interface Plugin { name: string; contract?: typeof CONTRACT; tokens?: Record; glyphs?: Record; spinners?: Record; borders?: Record; components?: Record>; } ``` ### SpinnerDef A spinner style, in cli-spinners' shape plus the projection a pipe prints. ```ts interface SpinnerDef { frames: string[]; interval: number; static: string; } ``` ## Types ### PluginErrorCode The family's whole refusal vocabulary (plugin-contract R8). One union, and every refusal any surface of this package prints is a member of it — the registry's five, and the two `flagstaff check` adds for what only a renderer can discover: a plugin that validates but contributes nothing renderable, and a component whose `static` throws on the state it is shown with. ```ts type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_NO_STATIC_PROJECTION' | 'E_PLUGIN_CONTRACT' | 'E_UNKNOWN_SPINNER' | 'E_UNKNOWN_BORDER' | 'E_NO_CONTRIBUTION' | 'E_COMPONENT_THREW' | 'E_UNKNOWN_KIND'; ``` --- # flagstaff/progress > Every export of flagstaff/progress, with its signature and doc comment: progress, plus 2 types. Source: https://flagstaff.interlace.tools/docs/api/progress The progress bar, which reports to an MCP client instead of drawing when it runs inside a burgee `--mcp` tool call that asked for progress (mcp-current R5). ```ts import { progress } from 'flagstaff/progress'; ``` ## Functions ### progress A progress bar: `12/30 files · 40%` off a terminal, a drawn bar on one, and MCP progress inside a call that asked. ```ts function progress(options?: ProgressOptions): Component; ``` | Parameter | Type | | :-- | :-- | | `options` (optional) | `ProgressOptions` | **Returns** `Component` ## Interfaces ### ProgressOptions ```ts interface ProgressOptions { /** Cells the bar occupies on a terminal. Default 24. */ width?: number; /** The two characters the bar is drawn with. Default `█` and `░`. */ glyphs?: { filled: string; empty: string; }; } ``` ### ProgressState ```ts interface ProgressState { /** Units done so far; clamped into `[0, total]` on the way out. */ done: number; total: number; /** What is being counted, e.g. `files`. Read in both projections. */ label?: string; } ``` --- # flagstaff/spinner > Every export of flagstaff/spinner, with its signature and doc comment: spinner, plus 2 types. Source: https://flagstaff.interlace.tools/docs/api/spinner ```ts import { spinner } from 'flagstaff/spinner'; ``` ## Functions ### spinner A spinner in the named style (`dots` by default); every style is a registered plugin's. ```ts function spinner(style?: string): Component; ``` | Parameter | Type | | :-- | :-- | | `style` (optional) | `string` | **Returns** `Component` ## Interfaces ### SpinnerState ```ts interface SpinnerState { text: string; /** `running` when omitted. */ status?: SpinnerStatus; } ``` ## Types ### SpinnerStatus ```ts type SpinnerStatus = 'running' | 'ok' | 'fail' | 'warn' | 'info'; ``` --- # flagstaff/tab-bar > Every export of flagstaff/tab-bar, with its signature and doc comment: tabBar, plus 2 types. Source: https://flagstaff.interlace.tools/docs/api/tab-bar ```ts import { tabBar } from 'flagstaff/tab-bar'; ``` ## Functions ### tabBar A tab bar: the active label off a terminal, every tab with the active one marked on one. ```ts function tabBar({ separator }?: TabBarOptions): Component; ``` | Parameter | Type | | :-- | :-- | | `{ separator }` (optional) | `TabBarOptions` | **Returns** `Component` ## Interfaces ### TabBarOptions ```ts interface TabBarOptions { /** Drawn between tabs, muted. Default ` · `. */ separator?: string; } ``` ### TabBarState ```ts interface TabBarState { /** The labels, in order. */ tabs: readonly string[]; /** Index of the active tab. Out of range, no tab is active and the static is empty. */ active: number; } ``` --- # flagstaff/table > Every export of flagstaff/table, with its signature and doc comment: table, tableComponent, plus 6 types. Source: https://flagstaff.interlace.tools/docs/api/table ```ts import { table, tableComponent } from 'flagstaff/table'; ``` ## Functions ### table Draw `rows` as a table, as a string. ```ts function table(rows: Row[], options?: TableOptions): string; ``` | Parameter | Type | | :-- | :-- | | `rows` | `Row[]` | | `options` (optional) | `TableOptions` | **Returns** `string` ### tableComponent A table as a component: `header: value` pairs off a terminal, the grid on one (R1). ```ts function tableComponent(options?: TableOptions): Component; ``` | Parameter | Type | | :-- | :-- | | `options` (optional) | `TableOptions` | **Returns** `Component` ## Interfaces ### ColumnOptions One column's bounds (`table-column-widths` R1). ```ts interface ColumnOptions { /** * How far the fit may narrow the column while another can still give way. A whole number * from 0. It bounds the shrink and is not a width: a column narrower than it stays so. */ min?: number; /** A cap no content pushes the column past. A whole number from 1, and at least `min`. */ max?: number; /** * What a cell wider than its column does (R3): `'wrap'` onto more physical rows, the * default and the only behaviour before this option, or `'truncate'` to one row ending in * `…`. A person's view only: the static projection carries the full cell either way (R4). */ overflow?: 'wrap' | 'truncate'; } ``` ### Linked A cell or a body with a destination attached. `href` is a url or a path — `file://` for a path a terminal should be able to open, which is the case R12 is named after. ```ts interface Linked { readonly text: string; readonly href: string; } ``` ### TableOptions ```ts interface TableOptions { /** Column headers. Omitted, the table is drawn without a header row. */ head?: string[]; /** Columns the whole table may occupy. Default 80. */ width?: number; /** Alignment per column; `left` for any column not named. */ align?: ('left' | 'right')[]; /** * Bounds per column, positional like `align`; a hole or a missing entry is unbounded. Both * are content widths, as `width()` measures them, padding not counted. */ columns?: (ColumnOptions | undefined)[]; /** * The terminal a linked cell is rendered for. Omitted, the real process is read through * this package's seam. Supply one and the whole path is pure — which is how `link.test.ts` * grades both branches without a terminal in sight. */ terminal?: Terminal; } ``` ### TableState ```ts interface TableState { head?: string[]; rows: Row[]; } ``` ## Types ### Cell A table cell: text, or text with somewhere to go. A plain string stays a plain string. ```ts type Cell = string | Linked; ``` ### Row A row of cells. A plain `string[]` is still a row — a destination is opt-in per cell. ```ts type Row = Cell[]; ``` --- # flagstaff/tasks > Every export of flagstaff/tasks, with its signature and doc comment: tasks, plus 4 types. Source: https://flagstaff.interlace.tools/docs/api/tasks ```ts import { tasks } from 'flagstaff/tasks'; ``` ## Functions ### tasks A task list: settled tasks one line each off a terminal, the whole list drawn on one. ```ts function tasks({ spinner }?: TasksOptions): Component; ``` | Parameter | Type | | :-- | :-- | | `{ spinner }` (optional) | `TasksOptions` | **Returns** `Component` ## Interfaces ### Task ```ts interface Task { title: string; /** `pending` when omitted. */ status?: TaskStatus; /** A line under the title — the step's current detail, shown only on a terminal. */ detail?: string; } ``` ### TasksOptions ```ts interface TasksOptions { /** The spinner style the running task is drawn with. Default `dots`. */ spinner?: string; } ``` ### TasksState ```ts interface TasksState { tasks: Task[]; } ``` ## Types ### TaskStatus ```ts type TaskStatus = 'pending' | 'running' | 'ok' | 'fail' | 'warn' | 'info'; ``` --- # Changelog > Every release of flagstaff, newest first, from its CHANGELOG.md — what changed and the pull request it came from. Source: https://flagstaff.interlace.tools/docs/changelog ## 1.8.2 ### Patch Changes - [#1483](https://github.com/ofri-peretz/burgee/pull/1483) [`3e69fb2`](https://github.com/ofri-peretz/burgee/commit/3e69fb2d8d068210347ac71d2886d6a4309859d1) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The package manifest names its author and publishes with `publishConfig` `{ "access": "public", "provenance": true }`. burgee's `homepage` is its landing page, https://burgee.interlace.tools. oxmutate gains `keywords`, so npm search finds it, and a `sideEffects` list (its CLI, its worker and its Vitest runner run on load), so a bundler can drop the rest. No code changes. - Updated dependencies [[`3e69fb2`](https://github.com/ofri-peretz/burgee/commit/3e69fb2d8d068210347ac71d2886d6a4309859d1)]: - closeout@1.1.10 - linegauge@1.2.1 - paratext@1.0.13 - roundel@1.3.2 ## 1.8.1 ### Patch Changes - [#1472](https://github.com/ofri-peretz/burgee/pull/1472) [`99fefcf`](https://github.com/ofri-peretz/burgee/commit/99fefcffcd5e9f98e63912c674944bbacfc74aa5) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Three fixes. - **A plugin key named like an `Object.prototype` member is checked like any other.** The schema walker read `properties.toString` and `properties.constructor` off the prototype, as a schema that admits anything, so `{ name: 'x', toString: 1 }` passed an object that allows no other keys. A required key is now present only when the object has it, or inherits it from a prototype of its own (a component written as a class still keeps `static` there) — `{}` no longer satisfies `required: ['constructor']`. - **`markdown` reads a line that opens with a code span as a paragraph, not a fence.** ` ```x``` is code ` opened a code block that swallowed the rest of the reply. A backtick fence's info string holds no backtick (CommonMark), so it is now a paragraph with the code span drawn; a tilde fence still takes any info string. - **`markdown` drops a heading's closing run of `#`s when it draws it.** `# Title #` was drawn as `Title #`; it is now `Title`, as CommonMark has it. A `#` with text against it (`# C#`, `# a #b`) is still text. - Updated dependencies [[`99fefcf`](https://github.com/ofri-peretz/burgee/commit/99fefcffcd5e9f98e63912c674944bbacfc74aa5), [`99fefcf`](https://github.com/ofri-peretz/burgee/commit/99fefcffcd5e9f98e63912c674944bbacfc74aa5)]: - paratext@1.0.12 - roundel@1.3.1 ## 1.8.0 ### Minor Changes - [#1411](https://github.com/ofri-peretz/burgee/pull/1411) [`e500294`](https://github.com/ofri-peretz/burgee/commit/e500294af9e3810f108039d637aef7162affb166) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/ora` follows ora 10.0.0, graded 223 / 223 by ora 10's own suite (control 223 / 223). The frame is laid out the way a terminal shows it — a tab to its stop, a wide character that does not fit moved to the next row, a carriage return back to the start of the row — and kept above the last row of the screen, with a truncation message that shortens to fit. A resize never raises the number of rows erased, a line another writer left unfinished pauses rendering until its newline (there is no 200 ms timeout any more), and a timer, drain or external-write render whose affix function or stream throws stops the spinner instead of crashing the process. `{ isEnabled: false }` logs a line on every `.start()`, a symbol is written plain on a stream that cannot render colour, an unknown spinner name throws on every terminal, and `oraPromise()` keeps the action's own error or result when `failText`, `successText` or the final write throws. The weight comparisons are restated as each incumbent resolves today: ora 10.0.0 at 128,182 B across 17 packages, log-update 8.0.0 at 114,295 B and boxen 9.0.0 at 115,780 B. ### Patch Changes - Updated dependencies [[`e500294`](https://github.com/ofri-peretz/burgee/commit/e500294af9e3810f108039d637aef7162affb166)]: - linegauge@1.2.0 ## 1.7.5 ### Patch Changes - [#1274](https://github.com/ofri-peretz/burgee/pull/1274) [`8c9c088`](https://github.com/ofri-peretz/burgee/commit/8c9c0884bb2abd87dd8781d5483db915d35a7d9c) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The README's first paragraph no longer names burgee: flagstaff leads with its own problem and its own incumbents, as every layer does (`cli-output-stack` R12). The dependency statement stays in the third paragraph and the badge — four dependencies, all from this repository, none outside it. ## 1.7.4 ### Patch Changes - [#1258](https://github.com/ofri-peretz/burgee/pull/1258) [`e3b3a41`](https://github.com/ofri-peretz/burgee/commit/e3b3a41745ecdc853d741c457306c566541271b6) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `check` reads a `.json` plugin file as data. It handed every file to `import()`, and Node refuses a `.json` module without an import attribute (ERR_IMPORT_ATTRIBUTE_MISSING), so a plugin written as JSON got Node's message and no verdict, from the host's own `check` and through `burgee check`, which runs it. - [#1266](https://github.com/ofri-peretz/burgee/pull/1266) [`02e6f9f`](https://github.com/ofri-peretz/burgee/commit/02e6f9fcd779d892e7097659a6bd4c2f0e5b5a04) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The family plugin schema (`schema.json`, identical in every plugin host) now describes `formats`, burgee's output-format key. This package reads nothing new, and a plugin carrying `formats` registers here as before. - [#1252](https://github.com/ofri-peretz/burgee/pull/1252) [`efc994e`](https://github.com/ofri-peretz/burgee/commit/efc994ea20dae1f31990a7afef9dfa3c34fee596) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Every benchmark figure in the README, a ratio, a byte count or a cold-start time, now links to the benchmarks page where it is measured. - Updated dependencies [[`e3b3a41`](https://github.com/ofri-peretz/burgee/commit/e3b3a41745ecdc853d741c457306c566541271b6), [`02e6f9f`](https://github.com/ofri-peretz/burgee/commit/02e6f9fcd779d892e7097659a6bd4c2f0e5b5a04), [`efc994e`](https://github.com/ofri-peretz/burgee/commit/efc994ea20dae1f31990a7afef9dfa3c34fee596), [`efc994e`](https://github.com/ofri-peretz/burgee/commit/efc994ea20dae1f31990a7afef9dfa3c34fee596)]: - closeout@1.1.8 - linegauge@1.1.5 - paratext@1.0.11 - roundel@1.2.1 ## 1.7.3 ### Patch Changes - [#1186](https://github.com/ofri-peretz/burgee/pull/1186) [`af24328`](https://github.com/ofri-peretz/burgee/commit/af243288e27cbffbcffac18af7112172e46a003b) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Published declarations now keep each doc comment's first paragraph, its code examples and its `@param`, `@returns`, `@throws`, `@example`, `@deprecated`, `@default` and `@see` tags, and drop the longer design notes below them, so the tarballs are smaller (burgee by 5.7% gzipped, bellpull and closeout by about a fifth). Types are unchanged, and hover text in an editor keeps its summary. `burgee`'s README also names `burgee/program-schema.json`, the JSON Schema every `--schema` document validates against. - Updated dependencies [[`af24328`](https://github.com/ofri-peretz/burgee/commit/af243288e27cbffbcffac18af7112172e46a003b), [`122635d`](https://github.com/ofri-peretz/burgee/commit/122635dce20998d1cba0eaa3c9df9c38bfd47c8b)]: - closeout@1.1.6 - linegauge@1.1.4 - paratext@1.0.10 - roundel@1.2.0 ## 1.7.2 ### Patch Changes - [#1160](https://github.com/ofri-peretz/burgee/pull/1160) [`0b381a0`](https://github.com/ofri-peretz/burgee/commit/0b381a0d394092edf2ea21a9143a10111aa72fe4) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Export `./package.json`. `require('/package.json')` threw `ERR_PACKAGE_PATH_NOT_EXPORTED`, which broke tools that read a dependency's manifest that way: bundlers, license scanners and version checks. - Updated dependencies [[`0b381a0`](https://github.com/ofri-peretz/burgee/commit/0b381a0d394092edf2ea21a9143a10111aa72fe4)]: - closeout@1.1.5 - linegauge@1.1.3 - paratext@1.0.9 - roundel@1.1.3 ## 1.7.1 ### Patch Changes - [#1130](https://github.com/ofri-peretz/burgee/pull/1130) [`c05c86e`](https://github.com/ofri-peretz/burgee/commit/c05c86e2caa0fc3a89f97f7fe9c37bd7b876bfaf) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Every drop-in subpath now type-checks under `moduleResolution: node10`, and under `module: commonjs` with no resolution set, which is TypeScript's default there. `import … from 'burgee/commander'` used to fail with TS2307 in such a project, although `require()` of the same specifier worked. Each package now declares `types` and a `typesVersions` map, derived from `exports`, with one key per typed subpath pointing at the file `exports` already names. `exports` is unchanged, so `node16`, `nodenext` and `bundler` read exactly what they read before. `controlroom/ink`'s declarations no longer name the untyped `react-reconciler`, which was TS7016 for consumers with `skipLibCheck: false`. - Updated dependencies [[`c05c86e`](https://github.com/ofri-peretz/burgee/commit/c05c86e2caa0fc3a89f97f7fe9c37bd7b876bfaf)]: - closeout@1.1.4 - linegauge@1.1.2 - paratext@1.0.8 - roundel@1.1.2 ## 1.7.0 ### Minor Changes - [#1095](https://github.com/ofri-peretz/burgee/pull/1095) [`98a0004`](https://github.com/ofri-peretz/burgee/commit/98a00049a453aac7a5b4b69451ab51e0abc4618f) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - A live frame never outgrows the terminal. `frameWriter` reads the writer's `rows` at every paint and clips a frame taller than the screen to its tail under one dim `… N more` line, so a repaint never climbs above the top row and leaves lines it cannot erase. `release` writes the clipped frame whole, so nothing it held is lost, and a change of height repaints from the top. A writer without `rows` (a pipe) is never clipped, and the static projection is untouched. `Writer` gains an optional `rows`. ## 1.6.0 ### Minor Changes - [#1072](https://github.com/ofri-peretz/burgee/pull/1072) [`582bc15`](https://github.com/ofri-peretz/burgee/commit/582bc15ec32466190f19db6f24c934e59cfb4012) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/progress` reports to an MCP client instead of drawing. Inside a burgee `--mcp` tool call whose client sent `_meta.progressToken`, the bar sends each count as `notifications/progress`, with its static line (`12/30 files · 40%`) as the message, and draws nothing into the tool result. Anywhere else it draws as before. The bridge is on the `flagstaff/progress` subpath only: `progress` from the root `flagstaff` entry always draws, so a program that never serves MCP does not pay for it. ### Patch Changes - Updated dependencies [[`4a62884`](https://github.com/ofri-peretz/burgee/commit/4a6288481a77dce34c496af5a5ffbaed900483c6)]: - paratext@1.0.7 ## 1.5.0 ### Minor Changes - [#1058](https://github.com/ofri-peretz/burgee/pull/1058) [`31a38e7`](https://github.com/ofri-peretz/burgee/commit/31a38e7ef3cfd5140af7cb9840e591d38cdc0fc5) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - A `table()` column can say what happens past its width: `columns: [{ overflow: 'truncate' }]` cuts a cell to one row ending in `…`, by display width through `linegauge/truncate`, with a link kept around the text that survives. `'wrap'`, the default, is the behaviour tables have always had. `tableComponent()`'s static projection prints every cell whole whatever `columns` says. ### Patch Changes - [#1046](https://github.com/ofri-peretz/burgee/pull/1046) [`1797388`](https://github.com/ofri-peretz/burgee/commit/1797388f27a294803b12839ff4d074226bd40917) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Two `ora` spinners on one stream, stopped in the order they started, leave `write` as they found it. The second used to restore the first's wrapper as its "original", so a stopped spinner stayed hooked into every later write and cleared lines it no longer owned. - Updated dependencies [[`1797388`](https://github.com/ofri-peretz/burgee/commit/1797388f27a294803b12839ff4d074226bd40917)]: - closeout@1.1.3 ## 1.4.0 ### Minor Changes - [#1054](https://github.com/ofri-peretz/burgee/pull/1054) [`b45f141`](https://github.com/ofri-peretz/burgee/commit/b45f1413f40bc775329b066af1528250cd5145f3) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `table()` takes `columns`, per-column `min` and `max` content widths, positional like `align`. A column with `max` never grows past it; a column with `min` is not narrowed below it while another column can give way, and when the mins add up past `width` the table is drawn wider rather than break them. Bounds that cannot hold (`min > max`, negative, fractional, `max` below 1) throw a `TypeError` naming the column. A table without `columns` draws exactly as before, and the static projection is unchanged. ### Patch Changes - [#1045](https://github.com/ofri-peretz/burgee/pull/1045) [`c4fd9a3`](https://github.com/ofri-peretz/burgee/commit/c4fd9a3cb10bc36bef8c468365addfc6deb572cf) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The family plugin schema (`schema.json`, identical in every plugin host) now describes `locales`, burgee's locale-pack key. A plugin carrying `locales` validates against the shared schema as it does against `burgee/locale-pack-schema.json`, which lists every message id. This package reads nothing new, and a plugin carrying `locales` registers here as before. - Updated dependencies [[`c4fd9a3`](https://github.com/ofri-peretz/burgee/commit/c4fd9a3cb10bc36bef8c468365addfc6deb572cf)]: - closeout@1.1.2 - linegauge@1.1.1 - paratext@1.0.6 - roundel@1.1.1 ## 1.3.0 ### Minor Changes - [#1014](https://github.com/ofri-peretz/burgee/pull/1014) [`65afee5`](https://github.com/ofri-peretz/burgee/commit/65afee5c96e4cae62dbb9b0ba28fab98fffdbf01) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - A quiet runtime. roundel's `Runtime` gains an optional `quiet` field, and flagstaff draws nothing when it is `true`: no spinner, no progress, no status line, and the frame clock never armed. Data and errors stay the caller's to print and are never quieted. burgee's global `--quiet` sets it. ### Patch Changes - [#1036](https://github.com/ofri-peretz/burgee/pull/1036) [`1357061`](https://github.com/ofri-peretz/burgee/commit/1357061168d836d87389dc0881b4a52df648cdc3) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The plugin host's JSON Schema walker reads a node's own keywords beside its `anyOf`: a branch that admits the value no longer excuses a sibling `required` or `properties`. flagstaff's own plugin schema uses no `anyOf`, so no plugin is accepted or refused differently. - [#1031](https://github.com/ofri-peretz/burgee/pull/1031) [`45a70be`](https://github.com/ofri-peretz/burgee/commit/45a70bec5d1ace1345a883afe10a3a081b081711) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The JSON Schema walker the plugin host validates with reads four more keywords: `anyOf`, `enum`, a list of types, and a `$ref` into draft-07 `definitions`. flagstaff's own plugin schema uses none of them, so no plugin is accepted or refused differently. burgee's tests use them to validate every message its `--mcp` server writes against the MCP 2025-06-18 schema. - [#1011](https://github.com/ofri-peretz/burgee/pull/1011) [`1968eb4`](https://github.com/ofri-peretz/burgee/commit/1968eb46feec57bb047a9c4a4288e0cf4442f4b2) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The README quick start runs as written. On npm it was broken for flagstaff (`rt` undefined) and linegauge (`styled` undefined), controlroom's imported flagstaff without installing it, and paratext's printed nothing. No code changed; `readme-quick-start-lock` now runs every package's quick start. - [#1022](https://github.com/ofri-peretz/burgee/pull/1022) [`ecb0a00`](https://github.com/ofri-peretz/burgee/commit/ecb0a00ca68e9a3b3584aefabb9dcf97e3ff967a) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The tarball no longer carries declaration files that no `exports` entry reaches. TypeScript could never open them, because a deep import is refused by `exports`. Every entry's types are unchanged. The ten packages drop 71 files and 118,312 bytes between them, burgee 35 files and 76,504 bytes, which takes its tarball from 251,814 to 225,278 bytes gzipped. - Updated dependencies [[`bfe4ab8`](https://github.com/ofri-peretz/burgee/commit/bfe4ab8b13ff04293345a69007cad9e0cb81c678), [`1968eb4`](https://github.com/ofri-peretz/burgee/commit/1968eb46feec57bb047a9c4a4288e0cf4442f4b2), [`65afee5`](https://github.com/ofri-peretz/burgee/commit/65afee5c96e4cae62dbb9b0ba28fab98fffdbf01), [`bcc438a`](https://github.com/ofri-peretz/burgee/commit/bcc438ac3936ee1cd530931ea090c6a8bdd016b2), [`ecb0a00`](https://github.com/ofri-peretz/burgee/commit/ecb0a00ca68e9a3b3584aefabb9dcf97e3ff967a)]: - linegauge@1.1.0 - paratext@1.0.5 - roundel@1.1.0 - closeout@1.1.1 ## 1.2.5 ### Patch Changes - [#938](https://github.com/ofri-peretz/burgee/pull/938) [`9babb6d`](https://github.com/ofri-peretz/burgee/commit/9babb6da1ae7783c7e8367f89e3da9dffc85ffdb) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - npm description and README: the description says what the package does, then its dependency fact in one of two wordings ("Zero dependencies." or "No dependency outside the burgee family."), then its drop-in paths. The README's family section states how the family is built — the leaves, one-way dependencies, nothing from outside the family, trusted publishing with provenance — and its table column is "Migrates from". No API, compatibility grade or benchmark changes. - Updated dependencies [[`9babb6d`](https://github.com/ofri-peretz/burgee/commit/9babb6da1ae7783c7e8367f89e3da9dffc85ffdb)]: - closeout@1.0.4 - linegauge@1.0.8 - paratext@1.0.4 - roundel@1.0.4 ## 1.2.4 ### Patch Changes - [#917](https://github.com/ofri-peretz/burgee/pull/917) [`735f0f6`](https://github.com/ofri-peretz/burgee/commit/735f0f68048f2d0f5e4effad151ed5132b480bb8) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - README: the opening paragraph says what the package does first. The drop-in path follows it as one line, naming the incumbent and `npx burgee migrate`, and every compatibility row and benchmark is unchanged. - Updated dependencies [[`735f0f6`](https://github.com/ofri-peretz/burgee/commit/735f0f68048f2d0f5e4effad151ed5132b480bb8)]: - closeout@1.0.3 - linegauge@1.0.7 - paratext@1.0.3 - roundel@1.0.3 ## 1.2.3 ### Patch Changes - [#897](https://github.com/ofri-peretz/burgee/pull/897) [`f29f159`](https://github.com/ofri-peretz/burgee/commit/f29f159ca0c9746f58d8ba84eec9be72b1140093) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - controlroom has its own docs site at https://controlroom.interlace.tools: its README's Docs line, its `homepage`, and every family table now link there. - Updated dependencies [[`f29f159`](https://github.com/ofri-peretz/burgee/commit/f29f159ca0c9746f58d8ba84eec9be72b1140093)]: - closeout@1.0.2 - linegauge@1.0.6 - paratext@1.0.2 - roundel@1.0.2 ## 1.2.2 ### Patch Changes - [#848](https://github.com/ofri-peretz/burgee/pull/848) [`6ae533e`](https://github.com/ofri-peretz/burgee/commit/6ae533e4610c32bad557f5d74a24d0c9a0f07f7e) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The family `schema.json` describes what five values look like. `contract` is `1`, and burgee refuses a plugin that declares none. A caique widget's `static(spec)` gets `{ kind, message, ...sample.done }`. A seniority `rank` sits between the built-in layers at flag 0, environment 10, config file 20, `package.json` 30 and default 40. A burgee hook's `filter` is `{ command: RegExp }`, now with `command` required, so the schema and the host refuse the same filters. - [#857](https://github.com/ofri-peretz/burgee/pull/857) [`d80b2d0`](https://github.com/ofri-peretz/burgee/commit/d80b2d0f4a535400c6825b42ac2d8e1be2827f4e) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The family `schema.json` closes a bellpull resolver's `when` with `additionalProperties: false`, so the schema and bellpull's own validation refuse the same unknown keys. - [#854](https://github.com/ofri-peretz/burgee/pull/854) [`fd8e659`](https://github.com/ofri-peretz/burgee/commit/fd8e659076420852b954c8dc57828f575c1df8b8) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/boxen` draws a box about 40% faster: text with no escape, tab or backspace skips the control-character walk, and a border bar of ASCII or box-drawing characters is cut by index instead of walked. Output is unchanged; boxen 9.0.0's own suite still passes 213 / 213. - Updated dependencies [[`6ae533e`](https://github.com/ofri-peretz/burgee/commit/6ae533e4610c32bad557f5d74a24d0c9a0f07f7e), [`d80b2d0`](https://github.com/ofri-peretz/burgee/commit/d80b2d0f4a535400c6825b42ac2d8e1be2827f4e)]: - closeout@1.0.1 - linegauge@1.0.5 - paratext@1.0.1 - roundel@1.0.1 ## 1.2.1 ### Patch Changes - Updated dependencies [[`1945d65`](https://github.com/ofri-peretz/burgee/commit/1945d65f22aafabd380d67f1e40341d6ef3aa6fa), [`1945d65`](https://github.com/ofri-peretz/burgee/commit/1945d65f22aafabd380d67f1e40341d6ef3aa6fa), [`9ebef65`](https://github.com/ofri-peretz/burgee/commit/9ebef654930ac4565f1ee04ec5f8b6f735c6a1d9), [`1945d65`](https://github.com/ofri-peretz/burgee/commit/1945d65f22aafabd380d67f1e40341d6ef3aa6fa)]: - closeout@1.0.0 - paratext@1.0.0 - roundel@1.0.0 ## 1.2.0 ### Minor Changes - [#827](https://github.com/ofri-peretz/burgee/pull/827) [`cd34546`](https://github.com/ofri-peretz/burgee/commit/cd345466edf47327be61a469eba5add35e1da2a2) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/boxen` is boxen 9.0.0's API, graded 213 / 213 by boxen 9's own suite (control 213 / 213). New options: `footer` and `footerAlignment`, `titleColor`, `borderBackgroundColor`, `maxWidth`; a tab, a backspace or a cursor move inside the text, a label or a border is written the way a terminal would draw it; a border side may be wider than one column or empty; and a size or spacing that is not a usable number means its default. Two of boxen 8's answers change with boxen 9: a hex colour must be real hex (`#GGG` now throws, as it does in boxen 9), and `vertical` / `horizontal` are a fallback for the sides rather than an override of them. ### Patch Changes - [#824](https://github.com/ofri-peretz/burgee/pull/824) [`4be8d59`](https://github.com/ofri-peretz/burgee/commit/4be8d5999875f7decb1963a91a0ad6cce3289176) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The README states ora's current tree, 114,102 B (it was 113,577 B on 2026-09-09); `flagstaff/ora` is still 49% of it. ## 1.1.1 ### Patch Changes - [#816](https://github.com/ofri-peretz/burgee/pull/816) [`5230016`](https://github.com/ofri-peretz/burgee/commit/52300169a98ece41b3544e5316833464e63a7c83) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - controlroom hosts plugins (R10): `keymaps` and `panes` register through `controlroom/plugin`'s `register()` against the family schema, a screen takes either by name, and `controlroom check ` reports what a plugin contributes. The family schema every host ships gains the `keymaps` and `panes` definitions. - Updated dependencies [[`5230016`](https://github.com/ofri-peretz/burgee/commit/52300169a98ece41b3544e5316833464e63a7c83), [`5230016`](https://github.com/ofri-peretz/burgee/commit/52300169a98ece41b3544e5316833464e63a7c83), [`5230016`](https://github.com/ofri-peretz/burgee/commit/52300169a98ece41b3544e5316833464e63a7c83)]: - closeout@0.7.0 - paratext@0.9.0 - linegauge@1.0.4 - roundel@0.6.3 ## 1.1.0 ### Minor Changes - [#806](https://github.com/ofri-peretz/burgee/pull/806) [`f30e011`](https://github.com/ofri-peretz/burgee/commit/f30e011b129abe89c4c79706e4e6a7432c2fab7f) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - New `flagstaff/markdown`: a reply streamed as tokens arrive, committed a block at a time (headings, lists, emphasis, inline code, fenced code); off a terminal it prints the markdown's own source, each block once. New `flagstaff/diff`: a unified diff with old and new line numbers on a terminal, and the diff unchanged everywhere else. - [#806](https://github.com/ofri-peretz/burgee/pull/806) [`f30e011`](https://github.com/ofri-peretz/burgee/commit/f30e011b129abe89c4c79706e4e6a7432c2fab7f) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `frameWriter()` from `flagstaff/loop`: paint a whole frame and only the changed rows are written, in one synchronized-output block. `hoist` repaints through it, so a terminal frame no longer redraws unchanged text. New `flagstaff/log-tail` (the last lines of a stream, `┊` and `◆`, a static projection that appends) and `flagstaff/tab-bar` (the active tab's label off a terminal), each registered through `register()`. The task list's pending mark is the replaceable `pending` glyph. ## 1.0.3 ### Patch Changes - Updated dependencies [[`b44f426`](https://github.com/ofri-peretz/burgee/commit/b44f426621ed799700cceda8c979de8f759f56b7), [`6195b99`](https://github.com/ofri-peretz/burgee/commit/6195b99344a21b4a05ab100fc38358deab229ce8)]: - paratext@0.8.0 - roundel@0.6.1 ## 1.0.2 ### Patch Changes - [#763](https://github.com/ofri-peretz/burgee/pull/763) [`6c2e9c5`](https://github.com/ofri-peretz/burgee/commit/6c2e9c5cee5d9962c0d75d84a766c76b76760f7f) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Faster, and lighter: - `link()` from `paratext` reads the process once, on its first call, where it used to rebuild the runtime and re-read the environment on every call. - `paratext/terminal-link` decides hyperlink support once per stream, as `supports-hyperlinks` does at import, and emits a link as a single concatenation. - Templates are parsed once, and `eraseLines` keeps the strings for the counts a redraw uses. - In B5 (ours ÷ incumbent, in-process), `paratext` against ansi-escapes went from 8.9× to 0.97× locally, and `paratext/terminal-link` against terminal-link from 20.9× to 0.78×. - The root bundle is 1,356 B smaller (8,417 → 7,061), because the plugin-schema fragments paratext and flagstaff import to validate no longer carry the schema's prose. The published `schema.json` is unchanged. - Updated dependencies [[`6c2e9c5`](https://github.com/ofri-peretz/burgee/commit/6c2e9c5cee5d9962c0d75d84a766c76b76760f7f), [`6c2e9c5`](https://github.com/ofri-peretz/burgee/commit/6c2e9c5cee5d9962c0d75d84a766c76b76760f7f)]: - linegauge@1.0.1 - paratext@0.7.5 ## 1.0.1 ### Patch Changes - Updated dependencies [[`a115799`](https://github.com/ofri-peretz/burgee/commit/a1157991a5defddadfbea49ba8ea3bf161d4a832)]: - roundel@0.6.0 ## 1.0.0 ### Major Changes - [#685](https://github.com/ofri-peretz/burgee/pull/685) [`6ef8227`](https://github.com/ofri-peretz/burgee/commit/6ef822762f5ad19564215945d7e76aa329614585) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - flagstaff 1.0.0. No API changes from 0.4: this release makes a promise. Every published entry point is now under semver and can change incompatibly only in a new major: `flagstaff` and `flagstaff/loop`, `plugin`, `import`, `spinner`, `progress`, `tasks`, `box` and `table`, the four drop-in subpaths, the `schema.json` plugin contract and the `flagstaff` bin. Each drop-in path is graded at 100% by its incumbent's own test suite, vendored at the release tag and run unmodified: ora 9.4.1 by 99 of 99 cases (`flagstaff/ora`), log-update 8.0.0 by 99 of 99 (`flagstaff/log-update`), boxen 8.0.1 by 84 of 84 (`flagstaff/boxen`) and cli-table3 0.6.5 by 29 of 29 (`flagstaff/cli-table3`). Out of scope: `flagstaff/boxen` is a boxen 8 drop-in. boxen 9 is released but is not graded or claimed, and a program on it is left alone by `burgee migrate`. Earlier majors of the four incumbents are not claimed either. ### Patch Changes - [#680](https://github.com/ofri-peretz/burgee/pull/680) [`7f59bc2`](https://github.com/ofri-peretz/burgee/commit/7f59bc2a612aa71925d5eca1db96d44d9f488c9a) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Raw mode is paired with its undo through `closeout/cursor`'s `rawMode`, the way the cursor already was. - `caique/raw`: a list prompt on a stream that was already in raw mode — a prompt library's, or the program's own — no longer switches raw mode off when it ends; it used to call `setRawMode(false)` unconditionally and take the keyboard from its owner. Raw mode the prompt did turn on is now turned off on `SIGINT` and `SIGTERM` too, not only in the prompt's own `finally`. `KeyStream` gains an optional `isRaw`. - `flagstaff/ora`: stdin-discarder's raw mode goes through the same pairing. Behaviour is unchanged except that an already-raw stdin is no longer written to at all. - [#711](https://github.com/ofri-peretz/burgee/pull/711) [`d26728b`](https://github.com/ofri-peretz/burgee/commit/d26728b26b44cbcc5edafca6bf1a91acbd1e1d35) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Code no test could reach is gone, and nothing a caller can observe changes. - `table()`: the column-shrinking loop no longer checks for a column already at the three-cell minimum — `table()` never offers less than that per column, so while the table is too wide its widest column is always wider than the minimum. The widths are found with `Math.max`/`indexOf` instead of an indexed loop with `?? 0` fallbacks that could not fire. Ties still go to the leftmost column. `flagstaff/table` is 342 B lighter. - The terminal projection no longer keeps a placeholder cursor net for a `close()` without an `open()`, or for a second `close()`: `hoist()` opens first and lowers at most once, so neither can happen. - [#685](https://github.com/ofri-peretz/burgee/pull/685) [`6ef8227`](https://github.com/ofri-peretz/burgee/commit/6ef822762f5ad19564215945d7e76aa329614585) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Fixed: `hoist()` on a terminal left stale rows behind when a frame was wider than the terminal. The tty projection counted the rows it had painted as lines of text, so a line that wrapped onto the next row was erased as one and the rows it wrapped onto stayed on screen under every repaint. It now counts painted rows with `linegauge`'s `lineCount` at the writer's `columns` (80 when the writer does not say), the measurement `flagstaff/ora` already clears by. `Writer` gains an optional `columns`, which `process.stdout` already carries. - [#665](https://github.com/ofri-peretz/burgee/pull/665) [`12fea8e`](https://github.com/ofri-peretz/burgee/commit/12fea8eae4d556b11dc693581cca73e16c2b633c) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Three defects carried over from the incumbents, and three places where a port answered differently from the incumbent on a bad value. - `burgee/yargs/parser`: with `unknown-options-as-args`, every `-`-prefixed argument ran through five flag regexes, and two of them backtracked — one quadratically, one cubically (a 4,000-character argument took ten seconds). They are linear scans now, and give the same answer as the regexes for every input, checked against them over every short string of the characters involved. yargs' own suite still passes 804 of 804. - `burgee/yargs/parser`: `{ "a": null }` in one config and `a.b` from a default or a second config threw "Cannot read properties of null". A `null` parent is now an absent one, as the parser's own key lookup already treated it. - `burgee/yargs`: `showHelp()` with an async default-command builder that rejected left an unhandled rejection that ended the process. The rejection now goes to `fail`, where yargs sends a command handler's rejection, so a `.fail()` handler receives it. - `burgee/meow`: `importMeta: null` throws meow's own "The `importMeta` option is required" TypeError instead of a null dereference, and `input: null` or an array is refused as meow refuses it. - `flagstaff/cli-table3`: a style name that cannot be read off a colour function (`caller`, `arguments`) draws the cell plain, as cli-table3 does, instead of throwing out of `toString()`. - [#674](https://github.com/ofri-peretz/burgee/pull/674) [`e9f45d8`](https://github.com/ofri-peretz/burgee/commit/e9f45d85d9db5b2e3dcaa1e43f292a1281a6952a) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - README: family header, badges, install, migrating, the family table. Every package README now opens the same way — lockup, tagline, one badge row in one order (npm version, downloads, Quality Gate, the package's own coverage, OpenSSF Scorecard, unpacked size, dependencies, types, Node, licence, npm provenance), a row of compatibility badges read from the graded baseline — and carries the same sections in the same order: Install for npm, pnpm, yarn and bun, Quick start, Migrating as a before/after diff, Compatibility, Benchmarks, For agents, API, and a generated table of the nine packages. Links are absolute, so they work on npm as well as GitHub. - [#684](https://github.com/ofri-peretz/burgee/pull/684) [`6c7b55a`](https://github.com/ofri-peretz/burgee/commit/6c7b55a01ea2e3aa1993419f78fbf6858ade5f8b) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `paratext/csi`: the CSI half of `ansi-escapes` as a subpath of its own — cursor moves, erases, scrolling, the alternate screen and synchronized output, byte-exact with `ansi-escapes` 7.3.0. It is 2,700 bytes and, unlike the package root, registers no built-ins when imported. `flagstaff/log-update`, `flagstaff/ora`, `caique/raw` and `caique/inquirer` take their cursor sequences from it instead of carrying their own copies; caique now depends on paratext. Output is unchanged, with one spelling difference: `caique/raw`'s repaint clears with `ESC[J` rather than the equivalent `ESC[0J`. `caique/inquirer` keeps `@inquirer/ansi`'s answer of nothing for a zero-row move, where `ansi-escapes`' `cursorUp(0)` is `ESC[0A`. - [#678](https://github.com/ofri-peretz/burgee/pull/678) [`088cecc`](https://github.com/ofri-peretz/burgee/commit/088ceccb7dda1cbe878950c631f49f48980dd2e2) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Whether anybody is there, whether to colour, and whether a tick can be drawn are roundel's questions, and three packages answered them by hand. - `roundel/terminal` (new subpath, 878 B, reaching nothing): `interactive(rt)` — a terminal on stdin, no `CI`, and no agent variable (`CLAUDECODE`, `AI_AGENT`, `CURSOR_AGENT`, `CODEX_THREAD_ID`, `GEMINI_CLI`, exported as `AGENTS`), with `FORCE_TTY=1` as the override — and `unicode(rt)`, is-unicode-supported 2.1.0's table over `{ env, platform }`. - `caique/decide` now depends on `roundel` and asks `interactive()`. **Behaviour change:** under an agent that has a terminal — `CLAUDECODE=1` and a TTY on stdin — a missing required value is refused with a usage error naming the flag (`--x is required when nobody is there to answer`) instead of prompting and hanging the agent. `FORCE_TTY=1` now prompts even without a terminal on stdin, as it does for burgee. - `caique/inquirer`'s tick and `flagstaff/ora`'s log symbols and spinner fallback use roundel's `unicode()`. caique's copy was a four-condition subset: the Linux console (`TERM=linux`) now gets `√` rather than `✔`, and ConEmu/Cmder, Terminus, Alacritty, rxvt-unicode and JetBrains' terminal on Windows now get `✔`, as `figures` draws them. - `burgee` help colour is roundel's `colorLevel(rt) > 0`. **Behaviour changes:** `NO_COLOR` now beats `FORCE_COLOR`; `--no-color` and `--color=…` on the command line are honoured; `CLI_ACCESSIBLE` turns help colour off; and a terminal that sets no `TERM` (Windows' conhost) gets plain help unless `FORCE_COLOR`, `--color` or `COLORTERM` asks for colour. - `burgee/contrast` rounds with roundel's `round2`; no output changes. - `paratext`: the supports-color fork behind `paratext/terminal-link` is unchanged, and now held to roundel's policy by a parity test everywhere their two incumbents agree. - Updated dependencies [[`6d8aadf`](https://github.com/ofri-peretz/burgee/commit/6d8aadff01a4c65ef3be16fe2082b0abd0c00b90), [`ff4159d`](https://github.com/ofri-peretz/burgee/commit/ff4159d25544fd45257f145e40ff0c5f8dfc4b3a), [`5e635b0`](https://github.com/ofri-peretz/burgee/commit/5e635b0a79c16ff2b459a4d00c80d334d5d945d1), [`ff4159d`](https://github.com/ofri-peretz/burgee/commit/ff4159d25544fd45257f145e40ff0c5f8dfc4b3a), [`6ef8227`](https://github.com/ofri-peretz/burgee/commit/6ef822762f5ad19564215945d7e76aa329614585), [`7c77cd2`](https://github.com/ofri-peretz/burgee/commit/7c77cd25c8fbeea4199c38c135e3a8937907f849), [`6ef8227`](https://github.com/ofri-peretz/burgee/commit/6ef822762f5ad19564215945d7e76aa329614585), [`6ef8227`](https://github.com/ofri-peretz/burgee/commit/6ef822762f5ad19564215945d7e76aa329614585), [`e9f45d8`](https://github.com/ofri-peretz/burgee/commit/e9f45d85d9db5b2e3dcaa1e43f292a1281a6952a), [`8a338ba`](https://github.com/ofri-peretz/burgee/commit/8a338baa50bb754051b18c00dbd0972976cccba1), [`6c7b55a`](https://github.com/ofri-peretz/burgee/commit/6c7b55a01ea2e3aa1993419f78fbf6858ade5f8b), [`620fc74`](https://github.com/ofri-peretz/burgee/commit/620fc74af013834ca6b15faa2772aeb94c5013b1), [`088cecc`](https://github.com/ofri-peretz/burgee/commit/088ceccb7dda1cbe878950c631f49f48980dd2e2), [`4319563`](https://github.com/ofri-peretz/burgee/commit/4319563b902c9d968786196f495cf47e7d4d9d39)]: - closeout@0.6.0 - linegauge@1.0.0 - paratext@0.7.3 - roundel@0.5.5 ## 0.4.4 ### Patch Changes - [#627](https://github.com/ofri-peretz/burgee/pull/627) [`4a629a9`](https://github.com/ofri-peretz/burgee/commit/4a629a9ff1129f2ce6f0c34e7cfc1d159304d18a) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Lint with every published Interlace ESLint plugin, and fix what the upgrade surfaced. - caique: the inquirer theme merge skips `__proto__`, `constructor` and `prototype` keys, so a theme object cannot swap the merged object's prototype. - burgee: last-element reads use `.at(-1)`. - burgee, closeout, flagstaff, roundel: helpers that capture nothing from their enclosing function move to module scope. - seniority: suppression comments name the `no-dynamic-require` rule that now reports the config loader's dynamic `require`. No public API or output changes. - Updated dependencies [[`4a629a9`](https://github.com/ofri-peretz/burgee/commit/4a629a9ff1129f2ce6f0c34e7cfc1d159304d18a)]: - closeout@0.5.4 - roundel@0.5.4 ## 0.4.3 ### Patch Changes - [#604](https://github.com/ofri-peretz/burgee/pull/604) [`0e7b1e8`](https://github.com/ofri-peretz/burgee/commit/0e7b1e88a7021350cf689f109c7728f799be1589) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Each README links to its migration guides under the docs link: "Migrating from: chalk", "ora · log-update · boxen · cli-table3", and so on. That puts a path from the npm page to the guide for the library you are replacing. No code changes. - Updated dependencies [[`0e7b1e8`](https://github.com/ofri-peretz/burgee/commit/0e7b1e88a7021350cf689f109c7728f799be1589)]: - roundel@0.5.3 - linegauge@0.5.4 - closeout@0.5.3 - paratext@0.7.2 ## 0.4.2 ### Patch Changes - [#474](https://github.com/ofri-peretz/burgee/pull/474) [`1955419`](https://github.com/ofri-peretz/burgee/commit/19554194342b55f8893161f894a8c2a4df1b0f21) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - burgee plugins can hook two more stages. `parse` runs before the command is resolved: it receives argv and may return a replacement, which is how an alias plugin maps `d` to `deploy`. `shutdown` runs once as the program exits, whether the command succeeded or failed. The family `schema.json` shipped in every package now describes both stages. - [#522](https://github.com/ofri-peretz/burgee/pull/522) [`f4be6a8`](https://github.com/ofri-peretz/burgee/commit/f4be6a8733e338bea4483992edeaa72fcddd36fd) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `require('bellpull/cross-spawn')`, `require('flagstaff/cli-table3')`, `require('burgee/yargs')`, `require('seniority/dotenv')` and `require('seniority/rc')` now return what the incumbent's `require()` does — the function, the class, the factory, the object — instead of an ES module namespace. Each exports its default as `'module.exports'`, which is what Node hands a CommonJS caller, and which yargs' own entry already does. `const spawn = require('…'); spawn(…)` threw before. - [#519](https://github.com/ofri-peretz/burgee/pull/519) [`77ff1cb`](https://github.com/ofri-peretz/burgee/commit/77ff1cb8e595d32bb8bddf441ae21fc61e6f247d) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The drop-ins now export their incumbents' type names, so a TypeScript program migrates by its import alone: `roundel/chalk` gains chalk's `Color`, `ForegroundColor`, `BackgroundColor`, `Modifiers` and `Options`; `flagstaff/ora` gains `Spinner`, `PrefixTextGenerator` and `SuffixTextGenerator`; `flagstaff/boxen` gains `Options`, `CustomBorderStyle` and `Boxes`; `flagstaff/log-update`, `linegauge`, `linegauge/wrap` and `closeout/exit-hook` gain `Options`; `burgee/yargs/parser` gains `Arguments`, `Options` and `Configuration`. Types only — no runtime bytes. - Updated dependencies [[`866b972`](https://github.com/ofri-peretz/burgee/commit/866b9724652bebea867a730b8f2ea5e0ca63f5ab), [`1955419`](https://github.com/ofri-peretz/burgee/commit/19554194342b55f8893161f894a8c2a4df1b0f21), [`1b002e0`](https://github.com/ofri-peretz/burgee/commit/1b002e0b72f8dc1aa61020fb40c26e50949f16e7), [`77ff1cb`](https://github.com/ofri-peretz/burgee/commit/77ff1cb8e595d32bb8bddf441ae21fc61e6f247d), [`dc1b1a7`](https://github.com/ofri-peretz/burgee/commit/dc1b1a7156f7548d7229b54b9f3d367bfda0af08), [`0592441`](https://github.com/ofri-peretz/burgee/commit/0592441c9ca8f81098a4aff48f15cfb141a0bece)]: - paratext@0.7.0 - closeout@0.5.2 - linegauge@0.5.2 - roundel@0.5.2 ## 0.4.1 ### Patch Changes - [#508](https://github.com/ofri-peretz/burgee/pull/508) [`1aae1e2`](https://github.com/ofri-peretz/burgee/commit/1aae1e2186ce88421067df5317795773419e53d0) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - ` --help` and `--version` answer instead of crashing. The bin took its first argument as the plugin file to import, so `roundel --help` failed with `Cannot find module '…/--help'` and exit 1. `-h`/`--help` now print usage and exit 0, `-V`/`--version` print the version and exit 0, and any other flag where the plugin file belongs is a usage error, exit 2. - Updated dependencies [[`1aae1e2`](https://github.com/ofri-peretz/burgee/commit/1aae1e2186ce88421067df5317795773419e53d0)]: - closeout@0.5.1 - linegauge@0.5.1 - paratext@0.6.1 - roundel@0.5.1 ## 0.4.0 ### Minor Changes - [#507](https://github.com/ofri-peretz/burgee/pull/507) [`b8e97dc`](https://github.com/ofri-peretz/burgee/commit/b8e97dcb64772e413f0b6f9e17e063c73314d242) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Runs on Node 20 and 22, not just 24+: `engines.node` is now `^20.19.0 || >=22.13.0`. Those are the first releases where `require(esm)` loads without a warning, so the CommonJS `require()` path keeps working. Every package's test suite runs on exactly 20.19.0 and 22.13.0, on Linux, macOS and Windows. caique's prompts no longer call `Promise.withResolvers`, which Node 20 doesn't have. ### Patch Changes - Updated dependencies [[`9800b43`](https://github.com/ofri-peretz/burgee/commit/9800b43d9c74a49dfb66d04a40fd0d1c48892e20), [`b8e97dc`](https://github.com/ofri-peretz/burgee/commit/b8e97dcb64772e413f0b6f9e17e063c73314d242)]: - closeout@0.5.0 - linegauge@0.5.0 - paratext@0.6.0 - roundel@0.5.0 ## 0.3.7 ### Patch Changes - [#494](https://github.com/ofri-peretz/burgee/pull/494) [`f7f6d4b`](https://github.com/ofri-peretz/burgee/commit/f7f6d4b8e8f9d9c7010bd4c81fda4b4d106fc9f0) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Each package's `homepage` and README docs link now point at its own documentation site, `https://.interlace.tools`, instead of a page on burgee's site. The old `burgee.interlace.tools/docs/packages/` URLs answer with a 301 to the new host, so nothing already linked breaks. closeout's README override example also resolves to the current release again (`npm:closeout@^0.4`; the 0.4.0 release left it at `^0.3`). - Updated dependencies [[`f7f6d4b`](https://github.com/ofri-peretz/burgee/commit/f7f6d4b8e8f9d9c7010bd4c81fda4b4d106fc9f0)]: - roundel@0.4.3 - linegauge@0.4.4 - closeout@0.4.1 - paratext@0.5.4 ## 0.3.6 ### Patch Changes - Updated dependencies [[`69563d1`](https://github.com/ofri-peretz/burgee/commit/69563d1fb14d9a4b29364446bae2fc48d86f6103), [`2dc573f`](https://github.com/ofri-peretz/burgee/commit/2dc573f884e7a4cc46829cd8f2c949a17f07710c)]: - closeout@0.4.0 - paratext@0.5.3 - linegauge@0.4.3 ## 0.3.5 ### Patch Changes - [#454](https://github.com/ofri-peretz/burgee/pull/454) [`b4584e7`](https://github.com/ofri-peretz/burgee/commit/b4584e719bc0064b294aab5ea6da1c11f698f0e9) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Every package's npm `homepage` now points at its page on the docs site, `https://burgee.interlace.tools/docs/packages/`, and each README links it under the header. The keywords add what people and models search for: `burgee` gains `cli-framework`, `argument-parser`, `subcommands`, `json-schema`, `mcp-server`, `model-context-protocol`, `ai-agent`, `llm`, `shell-completion`, `typescript`, `zero-dependency`, `commander-alternative` and `yargs-alternative`; the other eight gain `agent`, `ai-agent`, `non-tty`, `json` and `zero-dependency` where the package does that — `zero-dependency` only on the six that install nothing at all. `burgee`'s README gains a short FAQ (commander alternative, agent use, MCP, dependencies) and states the compatibility counts the oracle holds — 1,360 / 1,360 of commander's tests and 804 / 804 of yargs' — where it had said 1,215 and 1,185. `caique`'s README no longer calls a released package pre-release. - [#442](https://github.com/ofri-peretz/burgee/pull/442) [`bdaf364`](https://github.com/ofri-peretz/burgee/commit/bdaf364f81564c1700cf1adec18f927afe6c60c9) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Every package now lists `plugin`, `plugins` and `extensible` in its npm keywords, because every package takes plugins through one shared contract. A plugin is a plain object, validated against the `schema.json` that ships in every package, and checked with the package's own `check` command. Each package reads its own key and ignores the rest, so one object can extend any subset of the family. The [plugins page](https://github.com/ofri-peretz/burgee/blob/main/apps/docs/content/docs/plugins.mdx) has a nine-layer example that every package's `check` accepts in CI. - [#435](https://github.com/ofri-peretz/burgee/pull/435) [`7888524`](https://github.com/ofri-peretz/burgee/commit/78885245eb292cd4a40541fe09382a198c9c45cf) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `schema.json` now describes every plugin host in the family. The one schema each package ships as its plugin contract used to cover only four hosts: roundel's `tokens`, flagstaff's `glyphs`, `spinners`, `borders` and `components`, paratext's `capabilities`, and linegauge's `widths`. Five hosts validated their keys in their own code, but the file an author (or a model) writes against said nothing about them. It now describes all of them: - bellpull `resolvers`, including the absolute-path rule on `paths` - caique `widgets` - closeout `handlers`, including the phases a plugin may use - seniority `sources`, including the rank bounds - burgee `commands`, `hooks` and `enforce` Where the schema can express a rule, it gives the same verdict as the host's own validator, and a test holds the two together. Function-valued fields (`static`, `run`, `read`, `handler`) are described and required, but not typed, because JSON Schema can't say "function". **flagstaff** now validates a plugin against only its own keys, not the whole family schema. It no longer refuses a plugin over another host's key, which lets one plugin object contribute to several hosts. Its entry points are also 4.7–5.9 KB lighter for it. - [#445](https://github.com/ofri-peretz/burgee/pull/445) [`dac303e`](https://github.com/ofri-peretz/burgee/commit/dac303e944e889ac4175ac38c94e4ca0f0ca5358) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Every package now declares `sideEffects` truthfully, so bundlers can drop what you don't import. Six packages declared nothing, so no bundler could drop any of their modules. A named import from the root now bundles to the same bytes as the same import from its subpath: | import | before | after | | :------------------------------------ | ------: | ------: | | `import { explain } from 'seniority'` | 2,939 B | 1,067 B | | `import { decide } from 'caique'` | 1,235 B | 734 B | | `import { strip } from 'linegauge'` | 1,102 B | 940 B | | `import { once } from 'closeout'` | 353 B | 235 B | flagstaff and roundel used to declare `false`, but each ships a `check` command whose file runs when loaded. Each now lists that file, which is the true statement. paratext also lists the two modules that register its built-in capabilities when they load. - [#457](https://github.com/ofri-peretz/burgee/pull/457) [`44edb2f`](https://github.com/ofri-peretz/burgee/commit/44edb2f6f03a36f1b6773105867f41c899ffb6cd) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - flagstaff's npm description no longer says "Zero dependencies" (it takes four, all in-family); caique's README badge names both of its dependencies. - Updated dependencies [[`b4584e7`](https://github.com/ofri-peretz/burgee/commit/b4584e719bc0064b294aab5ea6da1c11f698f0e9), [`bdaf364`](https://github.com/ofri-peretz/burgee/commit/bdaf364f81564c1700cf1adec18f927afe6c60c9), [`7888524`](https://github.com/ofri-peretz/burgee/commit/78885245eb292cd4a40541fe09382a198c9c45cf), [`dac303e`](https://github.com/ofri-peretz/burgee/commit/dac303e944e889ac4175ac38c94e4ca0f0ca5358)]: - closeout@0.3.1 - linegauge@0.4.1 - paratext@0.5.1 - roundel@0.4.1 ## 0.3.4 ### Patch Changes - [#430](https://github.com/ofri-peretz/burgee/pull/430) [`4d1b2b3`](https://github.com/ofri-peretz/burgee/commit/4d1b2b399cff354864d1e2e843a19fde80ef1f30) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `check` now reports every refusal with its code and its fix, wherever it was raised. Some plugin files register themselves on import: they call `register()` at the top of the module and export the result. Until now, when such a file was refused, the error was thrown inside `check`'s `import()`, before the only `try` that turns a `PluginError` into `E_PLUGIN_SCHEMA: …` plus a `fix:` line. The author got the bare message on stderr, with no code and no fix. Now the whole of `check` runs inside that one handler, so every refusal comes out the same way on every host. - [#421](https://github.com/ofri-peretz/burgee/pull/421) [`db3c59e`](https://github.com/ofri-peretz/burgee/commit/db3c59e3dcd373c7e6e4a057715adb173766523f) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - linegauge hosts plugins — `widths`, and it is the ninth of nine. `scripts/extension-surface-lock.test.ts` has carried `linegauge: { plugin: false }` since it was written, and the row was empty honestly: a width function is not obviously extensible, and an extension point invented to fill a table is worse than a gap that says so. What makes `widths` real is that the package already admits the problem. `width.ts` says ambiguous-width characters are _"counted narrow, which is what a terminal does unless it has been told it is rendering an East Asian locale"_ — and that covers only the ambiguity Unicode sanctions. A Nerd Font putting a two-column icon in the Private Use Area, a code point added by a Unicode release newer than the table compiled into this build, a font drawing U+2500 wide: each is a real, local disagreement with the built-in answer, and until now a user had no way to settle it short of patching the package. ```js export default { name: "nerd-font", widths: { icons: { ranges: [[0xe000, 0xf8ff]], columns: 2, why: "Nerd Font patches two-column icons into the PUA; measured in WezTerm", }, }, }; ``` Three fields of plain data, so a plugin can arrive as JSON, be diffed, be generated and be printed without running its author's code (R7). **`why` is required**, which no other `$def` in the family does: a width table with no provenance cannot be audited when it turns out to be wrong, and _wrong_ is the normal outcome for ambiguous width. A later registration wins over an earlier one and over the built-in tables, which is the point — the built-in answer is right for most terminals and the user is the authority on theirs. An override applies **before** the zero-width and emoji rules, or it would be decorative. A program with no plugin pays one `length === 0` per cluster, and the ASCII fast path never reaches it. The family schema gains `widthRange`, `widthOverride` and `widths`, and because it is one byte-identical file across every host, **every host that validates against it grows by about 1.3 KB** — five flagstaff budgets and two paratext ones moved for a definition only linegauge reads. That trade is the design's and is recorded as D-108 rather than absorbed. - Updated dependencies [[`4d1b2b3`](https://github.com/ofri-peretz/burgee/commit/4d1b2b399cff354864d1e2e843a19fde80ef1f30), [`db3c59e`](https://github.com/ofri-peretz/burgee/commit/db3c59e3dcd373c7e6e4a057715adb173766523f), [`db3c59e`](https://github.com/ofri-peretz/burgee/commit/db3c59e3dcd373c7e6e4a057715adb173766523f)]: - closeout@0.3.0 - linegauge@0.4.0 - paratext@0.5.0 - roundel@0.4.0 ## 0.3.3 ### Patch Changes - [#404](https://github.com/ofri-peretz/burgee/pull/404) [`c70ff9c`](https://github.com/ofri-peretz/burgee/commit/c70ff9c6bf00ec4aff9b8a735f4246dd3193fb1a) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/cli-table3`'s `Cell` carries `ColSpanCell` and `RowSpanCell`, the way cli-table3's own `src/cell.js` publishes them — `module.exports = Cell; module.exports.ColSpanCell = …; module.exports.RowSpanCell = …`. A caller who reaches `Cell.RowSpanCell` on the incumbent now reaches it here. Both names were already named exports of the module; this is a second spelling of the incumbent's shape, and nothing new is published. With it, cli-table3's internal suite reads **103 / 104** against a control of 103 / 104 — the target matches the reference exactly, up from 90. ## 0.3.2 ### Patch Changes - [#398](https://github.com/ofri-peretz/burgee/pull/398) [`ee35904`](https://github.com/ofri-peretz/burgee/commit/ee35904e987a0133f2c0b51371227f840e0fd677) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - A `__proto__` key in a caller's corpus stays a style. `fromCliSpinners` and `fromCliBoxes` built their maps by assigning `map[key] = value` over the caller's JSON. JSON can carry the key `__proto__`, and that assignment hands it to the prototype setter rather than defining a property: the entry vanished from the plugin _and_ whatever it held became the fallback that every other `lookupSpinner` and `lookupBorder` inherited. Both now build with `Object.fromEntries`, which defines an own property for every key, and the returned map keeps `Object.prototype`. The registry's `deepFrozen` copy and caique's prompt binding write the same shape from a loop they cannot turn into an expression, and use `Object.defineProperty` instead. `flagstaff/import` is 80 bytes lighter for it — 838 B to 758 B — because two loops became two expressions. ## 0.3.1 ### Patch Changes - [#375](https://github.com/ofri-peretz/burgee/pull/375) [`98ac9a3`](https://github.com/ofri-peretz/burgee/commit/98ac9a38adce19bd8067d47b72573bb5fe0a3637) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/cli-table3` — cli-table3's internal surface now hangs off the default export, and the informational internals column moves **0 / 104 to 90 / 104** against a control of 103 / 104. No behaviour changed and the gated row is 29 / 29 before and after. All 104 internal cases were failing as `X is not a function`: the compat oracle reaches a target's internals through a CommonJS shim whose body is `module.exports = loaded?.default ?? loaded`, and that unwrap hands the suite the `Table` class rather than the namespace where `Cell`, `strlen`, `computeWidths` and fifteen more already lived. `Object.assign(Table, { … })` at the foot of the module publishes them the way cli-table3's own `src/cell.js` publishes `ColSpanCell` and `RowSpanCell` — a second spelling of names this subpath already exported, plus six that were private only because nothing had asked. Two ceilings are recorded with the measurement in `compat-oracle/src/hosts.ts` rather than chased: 13 cases in `table-layout-test.js` that resolve `Cell` to `Table` because the shim collapses four internal modules onto one entry, and the 94 cases of `cell-test.js`, which never register in the control run either. `./cli-table3`'s byte ratchet rises 29,000 to 29,300 for the 240 B this costs, with the reasoning in `weight.test.ts`. It is a ceiling moving in the loosening direction and is the owner's to reverse. - [#373](https://github.com/ofri-peretz/burgee/pull/373) [`a1f1d40`](https://github.com/ofri-peretz/burgee/commit/a1f1d40b7d1e7244f3a180943b641bb3b491d256) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Stage 2's artifact is now `spec.md`, the name Anthropic's AI-Native SDLC playbook gives it, so the source comments and README sections that cite a package's own design document point at `spec.md` rather than `design.md`. No behaviour changes. The published tarballs do move, by two bytes per surviving reference — `design.md` is nine characters and `spec.md` is seven — so the four packages carrying a weight band were re-measured against it: linegauge 83,538 to 83,536; paratext 66,343 to 66,341; closeout 84,455 to 84,453; bellpull 86,113 to 86,107. - Updated dependencies [[`f3224f4`](https://github.com/ofri-peretz/burgee/commit/f3224f4f43da21bbeeac931c2ec8afc50f0c3235), [`2f6cb16`](https://github.com/ofri-peretz/burgee/commit/2f6cb160f488668c56d61e3e3f0ed612137295af), [`a1f1d40`](https://github.com/ofri-peretz/burgee/commit/a1f1d40b7d1e7244f3a180943b641bb3b491d256)]: - paratext@0.4.0 - closeout@0.2.1 - linegauge@0.3.1 ## 0.3.0 ### Minor Changes - [#340](https://github.com/ofri-peretz/burgee/pull/340) [`c123029`](https://github.com/ofri-peretz/burgee/commit/c12302982e13432d7145399def4c790890546cc3) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/box` and `flagstaff/table` render a path or a url as a terminal hyperlink, and `flagstaff` is the first package in the family to build on `paratext` (paratext R12). A table cell may be `{ text, href }` and a box may be given `{ href }`. On a terminal believed to understand OSC 8 the text becomes a real hyperlink; on a pipe, in a log, under `TERM=dumb`, and for a screen reader it reads `src/index.ts (file:///repo/src/index.ts)` — the destination survives rather than being dropped with the escape, and no control byte reaches a file. Neither the guess nor the sequence is flagstaff's: both come from `paratext/link`, and `flagstaff` passes it a runtime instead of re-deciding. `flagstaff/cli-table3`'s `hyperlink()` now builds its sequence the same way. It still emits unconditionally and byte for byte what upstream emits — that is the drop-in contract, and cli-table3 still grades 29 / 29 — but the escape itself is no longer written out a second time in this package. `src/link.test.ts` locks that: no published file here spells an OSC 8 sequence of its own. `paratext/link` rather than `paratext`: 2,410 B and no registry against 20,221 B and `registerBuiltins()` at import, measured in paratext's own `dist/`. `flagstaff/table` grew 1,675 B and `flagstaff/box` 1,502 B; the two budgets in `weight.test.ts` moved with them and the reasoning is recorded there. ### Patch Changes - [#332](https://github.com/ofri-peretz/burgee/pull/332) [`3ea38c3`](https://github.com/ofri-peretz/burgee/commit/3ea38c363b9f0cee9f1c1c2dae4037b047e98328) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff` no longer carries its own copy of "put the cursor back however the process dies". `src/cursor.ts` is deleted and its three consumers — the `ora` façade, the `log-update` façade and the loop's `tty` projection — reach `closeout` instead, which owns `restore-cursor` and `signal-exit` and grades 6 / 6 against `restore-cursor`'s own suite. That module's own header argued there is exactly one correct implementation of this and that a second copy is a second place to get it wrong; it was the second copy. There was a third, in `caique`. The two façades use `closeout/restore-cursor`, whose contract is the one their incumbents grade: the stream is a property of the _process_ — stderr if it is a terminal, else stdout — decided when you call, and written at exit whatever `isTTY` says by then. The projection uses `closeout`'s `onExit` in the `restore` phase, because it draws on the stream the Runtime handed it and must not learn that `process` exists. `closeout/exit-hook` is deliberately **not** used: it is faithful to its own incumbent, which never registers SIGHUP, so a closing terminal would not have reached the restore. **A defect went with it.** The deleted module held a process-wide `cursorRestoreInstalled` flag — first caller installs the net, every later caller gets a no-op. That reads like a guard against a duplicate restore. It was a lost one: the second surface's writer was never registered, so a program with a hoisted frame on stdout and a spinner on stderr hid two cursors and put back one, leaving stderr's hidden. Measured on the previous build at `stderr { hide: 1, show: 0 }`. Registering per caller fixes it, and `src/cursor-net.test.ts` grades both halves — every hidden stream restored, and the one redundant (idempotent) show that two surfaces on a single stream now write. No compatibility row moves: ora 99 / 99, log-update 99 / 99, boxen 84 / 84. None of those suites kills the process, which is why the guarantee is graded by flagstaff's own signal cases against the built `dist/` in a child that is really signalled. `flagstaff` now depends on `closeout`. The per-entry weight measurements fall — `.` −1,666 B, `./loop` −1,666 B, `./ora` −1,591 B, `./log-update` −1,591 B — and **nothing got lighter**: the walk stops at a bare specifier, so the code left the measurement while staying in the program. Installed bytes go up, not down. - [#326](https://github.com/ofri-peretz/burgee/pull/326) [`88f7ba6`](https://github.com/ofri-peretz/burgee/commit/88f7ba65e3a79ed20bf7c5bc4feae8b87684122b) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - One `Runtime` seam per package, and one file in each that names the process (Y9). `roundel/src/runtime.ts` and `flagstaff/src/runtime.ts` each declare a `Runtime` — the slice of the world that package actually needs — and a `processRuntime()` that is the only place the real process is named. Six files stop naming it: `roundel/chalk`, and flagstaff's `cli`, `ora`, `boxen`, `cursor` and `log-update`. Nothing about the ports' behaviour moved, and the shape of each seam is what holds that. roundel's returns a literal, because chalk's contract is to detect the terminal once at import; flagstaff's hands back the live process narrowed to the interface, because its incumbents read the process at call time — boxen takes `stdout.columns` every time a box is drawn, so a box drawn after a resize still uses the new width, and ora still hooks the real stream objects and still looks up `kill` when it re-signals a swallowed Ctrl+C. The compatibility rows are unchanged: chalk 58/58, ora 99/99, log-update 99/99, boxen 84/84, restore-cursor 6/6. - [#294](https://github.com/ofri-peretz/burgee/pull/294) [`3f92a60`](https://github.com/ofri-peretz/burgee/commit/3f92a6099b1b8d5d476c64405ca963d40bf9af45) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - paratext validates against the family plugin schema, with its shape under `capabilities`. paratext shipped its own `schema.json` whose root _was_ one capability, so the family had three plugin schemas where the contract says one (PRINCIPLES 14, `plugin-contract` R2). The capability shape is now `$defs/capability` of the shared file, reached through a `capabilities` key beside `spinners`, `tokens` and `components`, and `packages/*/src/schema.json` hashes to one value. flagstaff and roundel ship the same bytes: their published `./schema.json` gains the capability definitions and nothing about what they validate changes. `check()` follows the schema's `$defs/capabilityDocument` and takes either shape: - a plugin carrying its capabilities under `capabilities`, which is where they live from now on, and whose problems are reported at `capabilities.`; - **deprecated** — one capability written as the whole document, which is what a 0.2 capability file looks like. It still validates, and `check()` returns a `deprecated:` line saying to move it under `capabilities`. paratext 1.0 stops accepting it (`.sdlc/PLAN.md` D2). `refusals()` and `isDeprecation()` are exported to tell the two kinds of line apart; the lines that are not deprecations are the ones that block, and the ones `register()` throws on. `register(capability)` is unchanged: it takes one capability, not a document, so it neither reports nor accepts the document-level deprecation. - [#339](https://github.com/ofri-peretz/burgee/pull/339) [`f295630`](https://github.com/ofri-peretz/burgee/commit/f2956301d5f9dcbcac0b001b00ebaf0315891fac) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `schema.json` constrains token names, because it was promising something no host honours. `tokens` was described as any name to a `#rrggbb` colour. `roundel`'s `validate()` accepts ten semantic names — `error`, `warn`, `ok`, `hint`, `muted`, `command`, `flag`, `value`, `heading`, `ground` — and throws on everything else. So a plugin author doing exactly what their own `E_PLUGIN_SCHEMA` error tells them, comparing their object against `roundel/schema.json`, got a green from the schema and `"accent" is not a token` from `register()`. Measured 2026-09-16 with `{ accent: '#336699' }`. The schema now carries `propertyNames.enum`, and `scripts/plugin-contract-lock.test.ts` pins the enum and the runtime set to each other from both sides, so neither can grow a name the other does not know. Every host ships a byte-identical copy of this file (`plugin-schema-lock.test.ts` asserts it), which is why nine packages are listed. Only the key `roundel` owns is constrained: describing `widgets`, `handlers`, `sources`, `resolvers` or `commands` in a file all eight hosts share is what made _flagstaff_ start validating caique's key last time (`PluginError: plugin.widgets.later: expected object, got boolean`), and those stay in `plugin-schema-lock`'s `UNDESCRIBED` list with that reason. `linegauge` is in the list for a different change: `ceilings.json`'s R9 block now records the bar as D1's tree-inclusive ceiling — 83,538 against 170,342, a ratio of 0.4904 — and keeps the superseded `get-east-asian-width` bar beside it with the count of entries that cleared it. - Updated dependencies [[`c8acb28`](https://github.com/ofri-peretz/burgee/commit/c8acb2848714a1cbe3e26a2f9895d7498fb4f096), [`fc640dd`](https://github.com/ofri-peretz/burgee/commit/fc640ddec11255c12d1e0948c5b8e99cc3f3263b), [`3f92a60`](https://github.com/ofri-peretz/burgee/commit/3f92a6099b1b8d5d476c64405ca963d40bf9af45), [`3ea38c3`](https://github.com/ofri-peretz/burgee/commit/3ea38c363b9f0cee9f1c1c2dae4037b047e98328), [`fc640dd`](https://github.com/ofri-peretz/burgee/commit/fc640ddec11255c12d1e0948c5b8e99cc3f3263b), [`3ea38c3`](https://github.com/ofri-peretz/burgee/commit/3ea38c363b9f0cee9f1c1c2dae4037b047e98328), [`c8acb28`](https://github.com/ofri-peretz/burgee/commit/c8acb2848714a1cbe3e26a2f9895d7498fb4f096), [`88f7ba6`](https://github.com/ofri-peretz/burgee/commit/88f7ba65e3a79ed20bf7c5bc4feae8b87684122b), [`c8acb28`](https://github.com/ofri-peretz/burgee/commit/c8acb2848714a1cbe3e26a2f9895d7498fb4f096), [`3f92a60`](https://github.com/ofri-peretz/burgee/commit/3f92a6099b1b8d5d476c64405ca963d40bf9af45), [`fc640dd`](https://github.com/ofri-peretz/burgee/commit/fc640ddec11255c12d1e0948c5b8e99cc3f3263b), [`3ea38c3`](https://github.com/ofri-peretz/burgee/commit/3ea38c363b9f0cee9f1c1c2dae4037b047e98328), [`0f00f72`](https://github.com/ofri-peretz/burgee/commit/0f00f7273ab0ca111c5869e2b08eb79314df7f70), [`f295630`](https://github.com/ofri-peretz/burgee/commit/f2956301d5f9dcbcac0b001b00ebaf0315891fac)]: - closeout@0.2.0 - linegauge@0.3.0 - roundel@0.3.1 - paratext@0.3.0 ## 0.2.1 ### Patch Changes - Updated dependencies [[`214f6f8`](https://github.com/ofri-peretz/burgee/commit/214f6f83b16068d7dc53d79799fba03c26a3cbe2), [`214f6f8`](https://github.com/ofri-peretz/burgee/commit/214f6f83b16068d7dc53d79799fba03c26a3cbe2), [`ecedfa2`](https://github.com/ofri-peretz/burgee/commit/ecedfa2ed0c7aaed23d77c4d02d7c94156a78ce9), [`ea58e63`](https://github.com/ofri-peretz/burgee/commit/ea58e637ee0ee6cdcc655478f6bef54d854f6c0f), [`3d744d9`](https://github.com/ofri-peretz/burgee/commit/3d744d99d92cdc7fc675ad105e4e4b9c6eebca5f), [`59d910c`](https://github.com/ofri-peretz/burgee/commit/59d910c1da7bca519bd1c2d5ca43b6c57e260621), [`93114d3`](https://github.com/ofri-peretz/burgee/commit/93114d33b5ac3f9a0ab3b48506255897af40133b)]: - roundel@0.3.0 - linegauge@0.2.0 ## 0.2.0 ### Minor Changes - [#86](https://github.com/ofri-peretz/burgee/pull/86) [`e927678`](https://github.com/ofri-peretz/burgee/commit/e927678f4590e29765d9db947f6141eb12aa34d8) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Add `flagstaff/boxen`: boxen 8's API, graded **84 / 84 by boxen's own test suite**. Every one of boxen's 84 cases is a snapshot of the exact characters the box comes out as, so matching the drawing byte for byte _is_ the compatibility claim rather than a way of avoiding one — a user leaving boxen cares about one thing, whether the box still looks the same. `borderStyle` (all eight cli-boxes styles, a style object, or `none`), `borderColor`, `backgroundColor`, `dimBorder`, `title`/`titleAlignment`, `textAlignment`, `padding`, `margin`, `width`, `height`, `float`, `fullscreen`, and the `_borderStyles` re-export. Eight dependencies folded in. boxen reaches `string-width`, `wrap-ansi`, `cli-boxes`, `ansi-align`, `widest-line`, `camelcase`, `chalk` and `type-fest`; this reaches `width.js` and `wrap.js` — both already shipped for `flagstaff/ora` and `flagstaff/log-update` — plus `roundel/chalk`. **43.0 KB in two packages, against boxen 8.0.1's 151.4 KB in fourteen.** It carries cli-boxes' table itself rather than reading the plugin registry: `_borderStyles` is boxen's public surface, and a façade whose drawing changed when somebody registered a plugin would be reinterpreting its host. Named borders through the registry stay `flagstaff/box`'s job. - [#116](https://github.com/ofri-peretz/burgee/pull/116) [`3daa412`](https://github.com/ofri-peretz/burgee/commit/3daa4126eba162ff036caeac26c5568ca2096b47) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Add `flagstaff/cli-table3`: cli-table3 0.6.5's API, graded **29 / 29 by cli-table3's own test suite**. The full option surface — `head`, `chars`, `style`, `colWidths`, `rowHeights`, `colAligns`, `rowAligns`, `truncate`, `wordWrap`, `wrapOnWordBoundary`, per-cell `colSpan`, `rowSpan`, `hAlign`, `vAlign`, `href`, and the `debug` channel with `table.messages` and `Table.reset()`. It extends `Array`, because cli-table3 does and its callers push rows onto it. **One module, not four.** Upstream is `table.js`, `layout-manager.js`, `cell.js` and `utils.js`, and 201 of its 234 cases test those files directly. Those are reported beside the number and never gate it — passing them would mean copying cli-table3's file layout rather than matching its behaviour, which is the one thing a façade owes its users. **42.3 KB in two packages, against cli-table3 0.6.5's 161.7 KB in seven.** It reaches `width.js`, already shipped for the other three façades, plus `roundel/chalk` for the two default styles. It carries its own wrapping rather than sharing `wrap.js`: cli-table3 splits on `/(\s+)/` and counts with its own `strlen`, which a wrap-ansi port does not reproduce. ### Patch Changes - [#149](https://github.com/ofri-peretz/burgee/pull/149) [`0d2520b`](https://github.com/ofri-peretz/burgee/commit/0d2520b04d297880f7117e54757361d0e04018c4) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Correct the published weight comparisons against boxen and cli-table3, both of which overstated the incumbent. boxen 8.0.1 is 132,414 B across nineteen packages, not the 151,351 in fourteen the README and weight rules claimed; cli-table3 0.6.5 is 105,983 across seven, not 161,690. ora and log-update reproduce to the byte and are unchanged. - [#82](https://github.com/ofri-peretz/burgee/pull/82) [`61bd11b`](https://github.com/ofri-peretz/burgee/commit/61bd11b9a1bf1fe73dd5a6e76e0898614e988ec7) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - One door into the registry, and a `check` that grades what it actually found. `registered()` handed out the live registry behind a `Readonly` type that freezes property bindings, not the `Map`s behind them — so `registered().spinners.set(…)` put a spinner with no static projection where `spinner()` would find it, and `.clear()` removed the built-ins. It now returns a copy over the frozen objects `register()` stores, which makes U3's "a contribution without a static projection is refused at the door" a property of the code rather than advice. Registering also copies: edit your plugin object afterwards and the registry does not change. `flagstaff check` opens with a census of the contributions it found and closes with the verdict, so `ok` is never printed before the rendering that would justify it. A plugin whose keys are misspelled — the schema allows unknown keys on purpose, for the rest of the family — is now `E_NO_CONTRIBUTION` and exit 1 with the unknown keys named, rather than `ok` and exit 0. Each component block states the state it was rendered with: a component may declare `sample: { running, done }`, and without one the assumed `{ phase }` shape is said out loud instead of silently invented. A `static` that throws is `E_COMPONENT_THREW` with a fix and the modes it broke in, rather than an uncaught crash after an `ok`. roundel is bumped with it because the plugin schema is hosted in both packages and both publish it: `packages/roundel/src/schema.json` gained the same `sample` key, and `plugin-schema-lock.test.ts` requires the two to be byte-identical. Without a roundel release the copies would agree in git and disagree in the registry — the contract's own "byte-identical in every tarball" rule holding in the repository and breaking where anyone would actually read it. This is the first contract change since roundel became a plugin host, so the pairing is worth establishing now rather than after the second one. Both of those codes are now members of the exported `PluginErrorCode`, which is the union every refusal in the family comes from. They were bare string literals inside `cli.ts`, so a second host could have spelled either one its own way and nothing would have noticed — the plugin contract's "one error vocabulary" held only as long as nobody tested it. `refuse()` takes `PluginErrorCode` rather than `string`, and a repo lock reads each host's declaration out of its source and refuses any `E_…` literal that is not in it. The union is a type, so this costs no bytes on any subpath. - [#117](https://github.com/ofri-peretz/burgee/pull/117) [`ec43c47`](https://github.com/ofri-peretz/burgee/commit/ec43c473f7cb609d6690087f1fc369240d56a315) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `register()` lists a plugin name once instead of accumulating it — every other contribution already landed in a `Map`, so re-registering replaced entries while the name list grew, and `flagstaff check` and the docs gallery both project that list. - [#75](https://github.com/ofri-peretz/burgee/pull/75) [`f2eaa6a`](https://github.com/ofri-peretz/burgee/commit/f2eaa6a7d170e24827a5bbb110ad9968b12d19e9) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `hoist()` puts the cursor back when a signal ends the process. `close()` restored it, and `close()` does not run for a signal with no listener — so Ctrl+C during a frame left the terminal with no cursor at all. The loop now shares the `cursor.js` both façades use. - [#182](https://github.com/ofri-peretz/burgee/pull/182) [`28a838f`](https://github.com/ofri-peretz/burgee/commit/28a838f0c1314fb79594d9d8bd8e02de785ea80a) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `linegauge` is real: `width` and `wrap` move out of `flagstaff` into the foundation package that was reserved for them (F1, the move only). The default export is `width`, call-compatible with `string-width`'s default. `flagstaff` imports them and deletes both files; its 227 tests pass unchanged, and B4's bundled bytes are identical to the byte — the code went to a different file, not away. - Updated dependencies [[`61bd11b`](https://github.com/ofri-peretz/burgee/commit/61bd11b9a1bf1fe73dd5a6e76e0898614e988ec7), [`8586f58`](https://github.com/ofri-peretz/burgee/commit/8586f58542e7896675c0b6fa8815278f8d22d4a3), [`28a838f`](https://github.com/ofri-peretz/burgee/commit/28a838f0c1314fb79594d9d8bd8e02de785ea80a), [`7c5eeb0`](https://github.com/ofri-peretz/burgee/commit/7c5eeb0c04a9a692db748ea7f3ccb2f3339fa5a2)]: - roundel@0.2.0 - linegauge@0.1.0 ## 0.1.0 ### Minor Changes - [#45](https://github.com/ofri-peretz/burgee/pull/45) [`4e376fe`](https://github.com/ofri-peretz/burgee/commit/4e376fe5381b8b7e94237273ab8f65096c3bb6a1) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The first working release: `hoist()` with a static projection per mode, `register()` over `schema.json`, the `dots` and `line` spinners as a plugin, and `flagstaff check`. - [#62](https://github.com/ofri-peretz/burgee/pull/62) [`5e34676`](https://github.com/ofri-peretz/burgee/commit/5e3467603b5d1f84f8257f095f1e95f8154fc935) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/ora` — ora 9's whole API, graded 99 / 99 by ora's own test suite. One import changes; the seventeen packages ora ships become two, 113,577 B of JavaScript becomes 55,641 B (49%), and the spinner corpus, the display width, the log symbols, the cursor control — restored on `SIGINT`, `SIGTERM` and `SIGHUP` as well as a clean exit — and the stdin discarder come with it. Both sides counted the same way from the entry point: shipped `.js` plus the `.json` a module imports, `package.json` excluded. ### Patch Changes - [#66](https://github.com/ofri-peretz/burgee/pull/66) [`b43e1d3`](https://github.com/ofri-peretz/burgee/commit/b43e1d3ccd62aad40b295cf4151aaff526d24acb) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The four remaining built-ins: `progress`, `tasks`, `box` and `table`, each on its own subpath. `box()` and `table()` are also plain string functions for the callers who want the string. Every one answers the static projection separately — a progress bar is `12/30 files · 40%` off a terminal, a table is `header: value` pairs — because a stripped drawing is not information. - [#68](https://github.com/ofri-peretz/burgee/pull/68) [`8666f70`](https://github.com/ofri-peretz/burgee/commit/8666f70c46bfd09111815ee1e950466bea125ea7) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/log-update` restores the cursor when the process is signalled, not only when it exits normally. It shipped with `process.once('exit', …)` and nothing else — which node does not run when a signal with no listener terminates the process — so Ctrl+C mid-frame left the terminal with no cursor. That is the same defect `flagstaff/ora` fixed before it shipped, so the fix is now one module, `src/cursor.ts`, that both façades import: `signal-exit`'s 22.0 KB in 1.4 KB, with the re-raise and its `listenerCount` guard, so a program that installed its own `SIGINT` handler is still delivered exactly one signal and is never overruled. Graded per façade against the built `dist/` in a child process that is really signalled. - [#66](https://github.com/ofri-peretz/burgee/pull/66) [`b43e1d3`](https://github.com/ofri-peretz/burgee/commit/b43e1d3ccd62aad40b295cf4151aaff526d24acb) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/import` — `fromCliSpinners()` and `fromCliBoxes()` turn the corpora you already have into ordinary plugins, through the same `register()` and the same schema. Neither corpus is bundled; 838 B, reaching nothing. The plugin contract gains `borders`, and the built-in border styles move into the built-ins plugin with the spinners, so `box()` can draw with a style a plugin registered. - [#66](https://github.com/ofri-peretz/burgee/pull/66) [`b43e1d3`](https://github.com/ofri-peretz/burgee/commit/b43e1d3ccd62aad40b295cf4151aaff526d24acb) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - `flagstaff/log-update` — log-update 8's API, graded 99 / 99 by log-update's own suite, which renders every frame through a real terminal emulator. Sixteen packages and 113.4 KB become **none** and 28.7 KB: the subpath reaches no package at all. Brings `wrap()`, an ANSI-aware wrapper graded differentially against wrap-ansi, which `box` and `table` share. - [#76](https://github.com/ofri-peretz/burgee/pull/76) [`847f79a`](https://github.com/ofri-peretz/burgee/commit/847f79a02e3fbd82b58a360db91b46bd7ed09c48) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Export `flagstaff/schema.json`. `PluginError`'s fix for `E_PLUGIN_SCHEMA` tells a plugin author to "compare the object against flagstaff/schema.json", and that specifier did not resolve — following the advice got `ERR_PACKAGE_PATH_NOT_EXPORTED`. The file already shipped in the tarball; only the `exports` entry was missing. - [#66](https://github.com/ofri-peretz/burgee/pull/66) [`b43e1d3`](https://github.com/ofri-peretz/burgee/commit/b43e1d3ccd62aad40b295cf4151aaff526d24acb) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - The static projection writes only what is new. A component whose projection is a growing list — `tasks`, whose static is every task that has settled — reprinted every line already in the log on each change; it now writes the lines past the common prefix. An empty projection writes nothing at all, rather than a blank line. Found by generating the docs gallery from the components themselves. - Updated dependencies [[`183ebc9`](https://github.com/ofri-peretz/burgee/commit/183ebc90d38c7a23afda1f923c9b147560458334), [`c65bad8`](https://github.com/ofri-peretz/burgee/commit/c65bad85111fd29a2c5941ea2b3b7d6034dff7ff)]: - roundel@0.1.0 --- # Coming from boxen > A boxen alternative with a drop-in path: import boxen from flagstaff/boxen, graded 213 / 213 by boxen's own test suite — then a box component whose static projection gives a pipe, an agent and a screen reader the text and title, not the border. Source: https://flagstaff.interlace.tools/docs/coming-from/boxen **flagstaff** is a **boxen alternative** you adopt by changing one import. `flagstaff/boxen` is boxen 9's API, and boxen 9.0.0's own test suite is the grade. boxen 8 is no longer graded: 9 changed two of 8's answers — a hex colour must be real hex, and `vertical` / `horizontal` are a fallback for the sides rather than an override — and the façade follows 9. ## Migrate from boxen in one import ```diff - import boxen from 'boxen'; + import boxen from 'flagstaff/boxen'; ``` Everything else stays: `boxen(text, options)` with `borderStyle` (all eight of cli-boxes' styles, a style object of your own, or `none`), `borderColor`, `backgroundColor`, `borderBackgroundColor`, `dimBorder`, `title`, `titleColor`, `titleAlignment`, `footer`, `footerAlignment`, `textAlignment`, `padding`, `margin`, `width`, `maxWidth`, `height`, `float` and `fullscreen`, plus `_borderStyles` and the `Options`, `CustomBorderStyle`, `Spacing` and `Color` types. An invalid colour throws, as boxen's does. ## Is flagstaff compatible with boxen? Graded, not claimed. boxen's own suite, vendored at 9.0.0 and unmodified apart from the import specifier, runs against `flagstaff/boxen` beside a control that runs it against real boxen. Its cases assert the exact characters the box comes out as. The suite's colour cases set `chalk.level` on the program's own `chalk`, so the grade runs the suite the way `burgee migrate` leaves a program that imports both: `chalk` is `roundel/chalk`, the one `flagstaff/boxen` draws with. | | passing | rate | | :-- | --: | --: | | `flagstaff/boxen` | 213 / 213 | 100.0% | | boxen itself (control) | 213 / 213 | 100.0% | From [Compatibility](https://burgee.interlace.tools/docs/compatibility), which `npm run compat:page` generates from the oracle's last run; that page is the authority. The same page grades flagstaff's `ora`, `log-update` and `cli-table3` paths. ## What you gain over boxen For a box the drawing **is** the contract, so `flagstaff/boxen` matches it byte for byte and the façade alone changes the dependency tree, not the output. boxen 9.0.0 resolves to fourteen packages; its seven direct dependencies — `string-width`, `wrap-ansi`, `slice-ansi`, `widest-line`, `cli-boxes`, `chalk` and `type-fest` — are folded in here, and the subpath reaches only `linegauge` and `roundel/chalk`, both from the burgee family. The gain beyond that is `flagstaff/box`, a separate API you adopt a file at a time: `box(text, { border, padding, title, width, href })` for the string, and `boxComponent()` to put the box on the frame loop: | mode | what `hoist(boxComponent(), …)` writes | | :-- | :-- | | `tty` | the border, padding and title | | `pipe`, `ci`, `accessible` | `title: text` — no border characters | | `json` | one NDJSON event per transition, on stderr | A border is noise to a screen reader and to a log an agent parses; the title and the text are not. That **static projection** is required of every flagstaff component, and which mode runs is decided once by roundel's output policy, never by the component. A box with an `href` draws its text as a terminal hyperlink where the terminal is believed to support OSC 8, and as `text (url)` everywhere else. Named borders come from the plugin registry, and `fromCliBoxes()` brings the cli-boxes set with you. ## When to switch from boxen - Your boxes end up in CI logs, in an agent's context or in front of a screen reader. - You want boxen's exact drawing without its fourteen-package tree — the import swap alone does that. The loop, the built-ins and the other drop-in paths are on [flagstaff](/docs). --- # Coming from cli-table3 > A cli-table3 alternative with a drop-in path: import Table from flagstaff/cli-table3, graded 29 / 29 by cli-table3's own test suite — then a table component whose static projection gives a pipe and an agent one line of header: value pairs per row. Source: https://flagstaff.interlace.tools/docs/coming-from/cli-table3 **flagstaff** is a **cli-table3 alternative** you adopt by changing one import. `flagstaff/cli-table3` is cli-table3 0.6.5's API, and cli-table3's own test suite is the grade. ## Migrate from cli-table3 in one import ```diff - const Table = require('cli-table3'); + import Table from 'flagstaff/cli-table3'; ``` From CommonJS, `require('flagstaff/cli-table3')` hands back the class, as `require('cli-table3')` does, on Node 22.13+. Everything else stays: `new Table({ head, chars, style, colWidths, rowHeights, colAligns, rowAligns, truncate, wordWrap, wrapOnWordBoundary })`, with `style`'s `padding-left`, `padding-right`, `head`, `border` and `compact`; per-cell `colSpan`, `rowSpan`, `hAlign`, `vAlign` and `href`; `.toString()` and `.width`; and the `debug` channel with `table.messages` and `Table.reset()`. It extends `Array`, because cli-table3 does and its callers `push` rows onto it. ## Is flagstaff compatible with cli-table3? Graded, not claimed. cli-table3's own suite, vendored at 0.6.5 and unmodified apart from the import specifier, runs against `flagstaff/cli-table3` beside a control that runs it against real cli-table3. The gate is the cases that go through the public surface, less nine that call `commonTests(require('cli-table'))` — the *legacy* package, which pass whatever the target is and are excluded by name: | | passing | rate | | :-- | --: | --: | | `flagstaff/cli-table3` | 29 / 29 | 100.0% | | cli-table3 itself (control) | 29 / 29 | 100.0% | From [Compatibility](https://burgee.interlace.tools/docs/compatibility), which `npm run compat:page` generates from the oracle's last run; that page is the authority. The rest of the suite `require`s cli-table3's internal modules directly; those are reported in an informational column — 103 / 104, equal to the control's 103 / 104 — and never gate the number, because passing them would mean copying cli-table3's file layout rather than matching its behaviour. The same page grades flagstaff's `ora`, `log-update` and `boxen` paths. ## What you gain over cli-table3 For a table the drawing **is** the contract — column-width negotiation, span fill-in and ANSI state across wrapped lines are all upstream's — so the façade alone changes the dependency tree and not the output. cli-table3 0.6.5 resolves to seven packages; this subpath reaches `linegauge`, `paratext/link` and `roundel/chalk`, all from the burgee family. The gain beyond that is `flagstaff/table`, a smaller API you adopt a file at a time — no spans, and column widths that are measured rather than negotiated: `table(rows, { head, width, align })` for the string, and `tableComponent()` to put the table on the frame loop: | mode | what `hoist(tableComponent(), …)` writes | | :-- | :-- | | `tty` | the grid | | `pipe`, `ci`, `accessible` | one line per row of `header: value` pairs | | `json` | one NDJSON event per transition, on stderr | A grid of box characters is unreadable in a log and worse to a screen reader; pairs are what an agent parses without knowing anything about the drawing. That **static projection** is required of every flagstaff component, and which mode runs is decided once by roundel's output policy. A linked cell, `{ text, href }`, is a terminal hyperlink where the terminal is believed to support OSC 8 and `text (url)` everywhere else — cli-table3's `href` emits the sequence unconditionally, and the façade keeps that because it is the incumbent's behaviour. ## When to switch from cli-table3 - Your tables are read by an agent, parsed from CI logs or read by a screen reader. - You want cli-table3's exact drawing without its dependency tree — the import swap alone does that. The loop, the built-ins and the other drop-in paths are on [flagstaff](/docs). --- # Coming from log-update > A log-update alternative with a drop-in path: import logUpdate from flagstaff/log-update, graded 99 / 99 by log-update's own test suite — then a frame loop whose static projection gives pipes one line per state and --json one event per transition. Source: https://flagstaff.interlace.tools/docs/coming-from/log-update **flagstaff** is a **log-update alternative** you adopt by changing one import. `flagstaff/log-update` is log-update 8's public API, and log-update's own test suite is the grade. ## Migrate from log-update in one import ```diff - import logUpdate, { createLogUpdate, logUpdateStderr } from 'log-update'; + import logUpdate, { createLogUpdate, logUpdateStderr } from 'flagstaff/log-update'; ``` Everything else stays: `logUpdate(...text)`, `.clear()`, `.done()`, `.persist()`, `logUpdateStderr`, and `createLogUpdate(stream, { showCursor, defaultWidth, defaultHeight })`. Frames wrap to the stream's width and clip to its height, and the row-level diffing is intact: a five-row frame whose last row is a counter costs one row of output per tick, not five. Ctrl+C mid-frame puts the cursor back and still terminates. ## Is flagstaff compatible with log-update? Graded, not claimed. log-update's own suite, vendored at 8.0.0 and unmodified apart from the import specifier, runs against `flagstaff/log-update` beside a control that runs it against real log-update. It renders every frame through a real terminal emulator and asserts the screen, not the bytes: | | passing | rate | | :-- | --: | --: | | `flagstaff/log-update` | 99 / 99 | 100.0% | | log-update itself (control) | 99 / 99 | 100.0% | From [Compatibility](https://burgee.interlace.tools/docs/compatibility), which `npm run compat:page` generates from the oracle's last run; that page is the authority. The same page grades flagstaff's `ora`, `boxen` and `cli-table3` paths. ## What you gain over log-update `flagstaff/log-update` is deliberately log-update's behaviour, so the façade alone changes the dependency tree and not the output: log-update 8 resolves to sixteen packages, and this subpath reaches two, `linegauge/wrap` and `closeout`, both from the burgee family. It is also the one flagstaff façade that keeps a cursor escape off a terminal, and it says so: log-update's suite asserts erase sequences on a plain non-TTY stream, so suppressing them would fail the grade. What it does hold is that nothing it writes is ever a `\r` or an absolute cursor-home — every move is a relative row move — so a captured transcript stays parseable. The whole guarantee is the frame loop underneath it, `hoist()`, which you adopt a file at a time: | mode | what `hoist()` writes for the same three calls | | :-- | :-- | | `tty` | the frame, repainted in place, then the final state left on screen | | `pipe`, `ci` | one line per state change — no `\r`, no escape | | `json` | one NDJSON event per transition, on stderr | | `accessible` | the static text, never a redraw | Every flagstaff component has a required `static(state)`, and that **static projection** is what a pipe, an agent and a screen reader get. Which mode runs is decided once by roundel's output policy, never by the component. ## When to switch from log-update - Your CLI's output is read by an agent or captured in CI logs. - You redraw status, progress or a task list and want it on one loop with the spinners, boxes and tables — `flagstaff/progress` and `flagstaff/tasks` are built for it. The loop, the built-ins and the other drop-in paths are on [flagstaff](/docs). --- # Coming from ora > An ora alternative with a drop-in path: import ora from flagstaff/ora, graded 223 / 223 by ora's own test suite — then a frame loop whose static projection gives pipes one line per state and --json one event per transition. Source: https://flagstaff.interlace.tools/docs/coming-from/ora **flagstaff** is an **ora alternative** you adopt by changing one import. `flagstaff/ora` is ora 10's whole API, and ora's own test suite is the grade. ## Migrate from ora in one import ```diff - import ora from 'ora'; + import ora from 'flagstaff/ora'; ``` Everything else stays: `ora({ text, spinner, color, indent, prefixText, suffixText })`, `.start() .stop() .succeed() .fail() .warn() .info() .stopAndPersist()`, `oraPromise()` and the `spinners` corpus. The cursor is restored on a clean exit and on `SIGINT`, `SIGTERM` and `SIGHUP`, as ora's is. ## Is flagstaff compatible with ora? Graded, not claimed. ora's own suite, vendored at 10.0.0 and unmodified apart from the import specifier, runs against `flagstaff/ora` beside a control that runs it against real ora: | | passing | rate | | :-- | --: | --: | | `flagstaff/ora` | 223 / 223 | 100.0% | | ora itself (control) | 223 / 223 | 100.0% | From [Compatibility](https://burgee.interlace.tools/docs/compatibility), which `npm run compat:page` generates from the oracle's last run; that page is the authority. The same page grades flagstaff's `log-update`, `boxen` and `cli-table3` paths. ## What you gain over ora `flagstaff/ora` is deliberately ora's behaviour to the byte, so the façade alone changes the dependency tree and not the output. The gain is the frame loop underneath it, `hoist()`, which you adopt a file at a time: | mode | what `hoist()` writes for the same three calls | | :-- | :-- | | `tty` | the spinner, repainted in place, then the final line left on screen | | `pipe`, `ci` | one line per state change — no `\r`, no escape | | `json` | one NDJSON event per transition, on stderr | | `accessible` | the static text, never a redraw | ora, off a terminal, writes its start line and its final line and nothing in between, so a log never sees `linking` between `building` and `built` (`ora.test.ts` asserts exactly that of `flagstaff/ora`, which behaves as ora does). Every flagstaff component has a required `static(state)`, and that **static projection** is what a pipe, an agent and a screen reader get. Which mode runs is decided once by roundel's output policy, never by the component. ## When to switch from ora - Your CLI's output is read by an agent or captured in CI logs. - You want spinners, progress, boxes and tables on one loop rather than four packages. The loop, the built-ins and the other drop-in paths are on [flagstaff](/docs). --- # Compatibility > How flagstaff's four drop-ins are graded — each incumbent's own test suite, unedited — the current grades, and the differences that remain. Source: https://flagstaff.interlace.tools/docs/drop-ins `flagstaff/ora`, `flagstaff/log-update`, `flagstaff/boxen` and `flagstaff/cli-table3` are graded, not described as compatible. Each is run against its incumbent's **own test suite**, by [compat-oracle](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/README.md), in CI. ✓ 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. ### 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. | [✓ 223 / 223 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.test.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.test.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 9.0.0's own tests, which assert the exact characters the box comes out as. | [✓ 213 / 213 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.test.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.test.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) | The counts are compat-oracle's baselines, the pass count each drop-in is held to. The family's [compatibility page](https://burgee.interlace.tools/docs/compatibility) is generated from the oracle's last run and is the authority for the current figures, beside every other drop-in in the family. ## How a suite is graded 1. The incumbent's repository is cloned at the release tag of the graded version — ora 10.0.0, log-update 8.0.0, boxen 9.0.0, cli-table3 0.6.5 — and its test directory copied into `packages/compat-oracle/vendor/`. No incumbent ships its tests to npm, so a tarball could not be used. Each copy's `PROVENANCE` file names the tag, the commit and the command that reproduces it. 2. The only edit is the import that reaches the library: it is rewritten to a shim generated per run. Assertions, fixtures and helpers are upstream's, byte for byte. 3. A **control run** points the shim at the real incumbent first. That proves the harness before it grades anything of ours, and the control's total is what every rate is measured against — so a test file that fails to load cannot flatter the rate. 4. The **target run** points the same shim at flagstaff's drop-in. ## What each grade covers - **ora — 99 cases.** ora's suite never kills a process, so it does not reach the signal path. flagstaff's own `ora.test.ts` grades that instead: the cursor comes back on `SIGINT`, `SIGTERM` and `SIGHUP`, and the process still terminates. - **log-update — 99 cases**, each rendered through a terminal emulator with the screen asserted. - **boxen — 213 cases**, each asserting the exact characters the box comes out as. Its colour cases set `chalk.level` on the program's own `chalk`, so they run with `chalk` moved to `roundel/chalk`, as `burgee migrate` moves it. - **cli-table3 — 29 cases.** cli-table3's suite has 38 cases that go through its public API; nine of them grade `cli-table`, the older incumbent, and pass whatever the target is, so they are excluded by name. The rest of its suite `require`s its four internal modules directly; those cases are reported beside the grade on the family page and never gate it, because passing them would mean copying cli-table3's file layout rather than matching its behaviour. ## Known differences These are deliberate, and each is written down where the code is: - **`flagstaff/log-update` writes erase sequences off a terminal**, because log-update's suite asserts them on a non-TTY stream. It never writes a carriage return or an absolute cursor-home, which the suite does not require ([Live regions](/docs/guides/live-regions)). - **`flagstaff/ora` does not read `CLI_ACCESSIBLE`.** It imports roundel's chalk and never the output policy, as ora never does; `hoist()` is what honours the switch ([NO_COLOR and accessibility](/docs/guides/accessibility)). - **Eleven of cli-table3's type names are not exported by `flagstaff/cli-table3`.** cli-table3 declares them — `CellOptions`, `TableConstructorOptions` and the rest — on a CommonJS `export =` namespace, where a program reaches them as `Table.CellOptions`; the drop-in is an ES module and does not carry that namespace. `scripts/drop-in-type-surface-lock.test.ts` names all eleven with that reason, and fails when one is exported without the list shrinking. - **`flagstaff/boxen` draws with its own copy of cli-boxes' borders**, not the plugin registry, so registering a plugin never changes what a boxen caller draws. `box()` is where registered borders are used. ## Types and CommonJS Apart from those eleven, each drop-in exports every name its incumbent exports, types included, so a TypeScript program migrates by its import alone; the type-surface lock checks every name against the installed incumbent. `require('flagstaff/cli-table3')` returns the class, as `require('cli-table3')` does, not an ES module namespace. --- # FAQ > Short answers about flagstaff — the runtime, modes, stdout versus stderr, Ink, CommonJS, colours and testing — each with where to read more. Source: https://flagstaff.interlace.tools/docs/faq ## Why does `hoist()` take a runtime instead of reading `process`? So the mode is decided once, by one rule, and so a test can pass a literal. The loop reads the environment, whether stdout is a terminal, two streams and a clock from what you hand it, and nothing else. [Getting started](/docs/getting-started#the-runtime) has a runtime for a real process in a dozen lines; a burgee program already has one. ## Which mode will my program get? The first that matches: `json` if you passed `{ json: true }`; `accessible` if `CLI_ACCESSIBLE` is set; `tty` if stdout is a terminal; `ci` if `CI` is set; otherwise `pipe`. A CI job that attaches a terminal therefore animates — the terminal check comes before `CI`. The table is on [Getting started](/docs/getting-started#the-five-modes). ## Where does output go? The terminal, pipe, CI and accessible modes write to **stdout**. `--json` writes its events to **stderr**, so stdout is left for your program's own result. The drop-ins write where their incumbents do: `flagstaff/ora` to stderr by default, as ora does. ## I print a spinner in a loop and my CI log has thousands of lines. Off a terminal, each state that changes the text is one line. Update when the text is worth reading — see [Progress in logs](/docs/recipes/progress-in-logs). ## Does it replace Ink? No. flagstaff has no layout engine and no React; `box` and `table` measure, wrap and join strings. It covers the widgets ora, log-update, boxen and cli-table3 cover, on one loop, with a text form for each. A full-screen interactive interface is what Ink is built for, and flagstaff is not. ## Can I use it from CommonJS? Yes, on Node 22.13+: the package is ESM with a `default` condition, so `require('flagstaff/spinner')` loads it through `require(esm)`. `shape.test.ts` installs the packed tarball and does exactly that. ## Why are my spinner's glyphs not coloured? flagstaff paints through roundel's tokens, which are plain until your program calls `fly()` from `roundel/theme`. After that, `NO_COLOR`, `--json` and a pipe nobody asked to colour still turn colour off. See [NO_COLOR and accessibility](/docs/guides/accessibility). ## How do I test what my CLI prints? Pass a runtime with `manualClock()` and streams that collect what is written; the terminal transcript is then a fixed string. See [Testing terminal output](/docs/recipes/testing-output). ## Are the cli-spinners styles included? No, and neither are cli-boxes' borders: two spinner styles and six borders ship built in. `flagstaff/import` turns the corpus you already depend on into a plugin in one call — [Plugins](/docs/guides/plugins#bringing-a-corpus). ## Is the ora drop-in exactly ora? It passes all 223 cases of ora 10.0.0's own suite, and its behaviour is ora's to the byte, including in a pipe. The differences that remain are listed on [Compatibility](/docs/drop-ins#known-differences). --- # Getting started > Install flagstaff, hoist a spinner, and see the same three calls in a terminal, in a pipe and under --json. Source: https://flagstaff.interlace.tools/docs/getting-started flagstaff is a frame loop. You **hoist** a component with an initial state, **update** it as work moves, and **lower** it with the final state, which is the line left behind. Which of the five output modes runs is decided once, when you hoist, from the environment you pass in. ## Install ```bash npm install flagstaff ``` It is ESM with a `default` condition, so `require('flagstaff/spinner')` also works from CommonJS on Node 22.13+. It installs four packages from this repository — [closeout](https://closeout.interlace.tools/docs), [linegauge](https://linegauge.interlace.tools/docs), [paratext](https://paratext.interlace.tools/docs) and [roundel](https://roundel.interlace.tools/docs) — and nothing else ([`shape.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/shape.test.ts) installs the packed tarballs and checks). ## The runtime The loop never reads `process` itself. It takes a runtime: the environment, whether stdout is a terminal, the two streams, and a clock. For a real process that is a few lines, and every example on this site imports it from this file: ```js title="rt.mjs" import process from 'node:process'; /** The real process as flagstaff's loop reads it: environment, terminal, streams and a clock. */ export const rt = { env: process.env, isTTY: { stdout: process.stdout.isTTY === true }, stdout: process.stdout, stderr: process.stderr, clock: { now: () => Date.now(), schedule(fn, ms) { const id = setTimeout(fn, ms); return () => clearTimeout(id); }, }, }; /** `--json` on the command line: the loop writes events to stderr instead of drawing. */ export const json = process.argv.includes('--json'); ``` A CLI built on [burgee](https://burgee.interlace.tools/docs) already has one: burgee's runtime satisfies the same shape. In a test, use a literal with `manualClock()` from `flagstaff/loop`, and the terminal output becomes a fixed string (see [Testing terminal output](/docs/recipes/testing-output)). ## A first program ```js title="build.mjs" import { setTimeout as sleep } from 'node:timers/promises'; import { hoist } from 'flagstaff/loop'; import { spinner } from 'flagstaff/spinner'; import { json, rt } from './rt.mjs'; const flag = hoist(spinner(), rt, { text: 'building' }, { json }); await sleep(400); flag.update({ text: 'linking' }); await sleep(400); flag.lower({ text: 'built', status: 'ok' }); ``` Run it in a terminal and the spinner animates in place, hiding the cursor while it runs, then leaves `✔ built` on screen and shows the cursor again. Pipe it, and the same three calls write one line per state: ```text title="node build.mjs" … building … linking ✔ built ``` Ask for `--json`, and stdout stays empty while stderr carries one event per transition — the form an agent parses: ```text title="node build.mjs --json" {"event":"spinner","state":{"text":"building"}} {"event":"spinner","state":{"text":"linking"}} {"event":"spinner","state":{"text":"built","status":"ok"}} ``` Every output block on this site is checked: `tests/examples.test.ts` writes each titled file, runs the command in the block's title, and compares. ## The five modes | mode | chosen when | what is written | | :-- | :-- | :-- | | `json` | the program passed `{ json: true }` | one NDJSON event per transition, on stderr | | `accessible` | `CLI_ACCESSIBLE` is set, terminal or not | the text form once per state, never a redraw | | `tty` | stdout is a terminal | frames repainted in place, the final text left behind | | `ci` | `CI` is set and stdout is not a terminal | the text form once per state | | `pipe` | anything else | the text form once per state | The first row that matches wins. The rule is roundel's [`outputMode`](https://github.com/ofri-peretz/burgee/blob/main/packages/roundel/src/policy.ts), the same one every package in the family asks, so a spinner and a prompt cannot disagree about the terminal. The [static projection](/docs/guides/static-projection) guide covers what "the text form" is and how a component declares it. ## Where next - [Guides](/docs/guides/static-projection): the loop, spinners, live regions, boxes, tables, accessibility, plugins. - [Why flagstaff](/docs/why-flagstaff): what it does that ora, log-update, boxen and cli-table3 do not, cell by cell, with the evidence. - [Coming from ora](/docs/coming-from/ora) and the other three: change one import. - [API reference](/docs/api): every export of every entry point. --- # NO_COLOR and accessibility > How flagstaff decides colour and redraws — NO_COLOR, FORCE_COLOR, --color, CLI_ACCESSIBLE — and what a screen-reader user gets. Source: https://flagstaff.interlace.tools/docs/guides/accessibility flagstaff decides nothing about the terminal itself. Two questions — may I redraw, and may I colour — are answered by [roundel](https://roundel.interlace.tools/docs)'s output policy, the one every package in the family asks. Its rules are in [`policy.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/roundel/src/policy.ts) and pinned by `policy.test.ts`. ## Redraws: `CLI_ACCESSIBLE` A screen reader reads a redraw as a stream of fragments. Set `CLI_ACCESSIBLE=1` and the policy chooses the **accessible** mode ahead of the terminal check: every component hoisted with `hoist()` prints its [static projection](/docs/guides/static-projection) once per state and never redraws — on a terminal as well as off one. A box is its title and text, a table is `header: value` lines, a progress bar is `12/30 files · 40%`. ```text title="CLI_ACCESSIBLE=1 node build.mjs" … building … linking ✔ built ``` `--json` still wins over it: a program asked for structured output gets structured output. An empty `CLI_ACCESSIBLE=` counts as unset, the way `NO_COLOR` does. The drop-ins are the exception, on purpose: they behave as their incumbents do. `flagstaff/ora` imports roundel's chalk and never the policy, because a façade that reinterpreted its host would fail the host's own suite — so on a terminal `CLI_ACCESSIBLE=1` does not stop it animating, where `CI=true` does, because that is ora's own rule. `hoist()` is what honours the switch. ## Colour: `NO_COLOR` wins flagstaff paints its glyphs through roundel's tokens, and a token is plain text until the program calls `fly()` from `roundel/theme` to decide a colour level. Once it has, the level is, first match wins: 1. **none** under `--json`, when `NO_COLOR` is set to anything but empty, or in accessible mode unless colour was asked for; 2. **what was asked**, when `FORCE_COLOR`, `--color` or `--no-color` asked — the level named, or the terminal's own level and never less than basic colour when none was named; 3. **none** in a pipe nobody asked to colour; 4. **the terminal's own**, from `TERM` and `COLORTERM`, or a known CI runner's. The full order, edge cases included (`TERM=dumb`, Azure Pipelines), is `colorLevel` in the same file. So `NO_COLOR` beats an explicit `--color`: ```js title="colour.mjs" import { hoist } from 'flagstaff/loop'; import { spinner } from 'flagstaff/spinner'; import { fly } from 'roundel/theme'; import { json, rt } from './rt.mjs'; fly({}, { ...rt, argv: process.argv }, { json }); hoist(spinner(), rt, { text: 'building' }, { json }).lower({ text: 'built', status: 'ok' }); ``` ```text title="NO_COLOR=1 node colour.mjs --color" … building ✔ built ``` Colour is never how a state is told apart: `ok` and `fail` are `✔` and `✖` before they are green and red, so the text form carries the whole meaning with colour off. ## What a screen-reader user can rely on - No redraws with `CLI_ACCESSIBLE=1`, from anything hoisted on the loop. - No cursor movement, erase or carriage return from `hoist()` off a terminal, in any mode ([`loop.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/loop.test.ts)). - A text form for every component: a plugin without one is refused at registration ([Plugins](/docs/guides/plugins)). - The cursor is shown again after `lower()`, and after `SIGINT`, `SIGTERM` or `SIGHUP`. --- # Boxes > Draw a box as a string with box(), or hoist boxComponent() so a pipe gets "title: text" instead of a border. Source: https://flagstaff.interlace.tools/docs/guides/boxes `flagstaff/box` has two exports: `box()`, which returns the drawing as a string, and `boxComponent()`, which hoists on the loop like any other component. Most callers want the string. ## `box(text, options)` ```js title="banner.mjs" import { box } from 'flagstaff/box'; console.log(box('Ready on :3000', { title: 'dev', width: 30 })); console.log(box('Ready on :3000', { border: 'double', padding: { x: 2, y: 1 }, width: 30 })); ``` ```text title="node banner.mjs" ╭─ dev ──────────────────────╮ │ Ready on :3000 │ ╰────────────────────────────╯ ╔════════════════════════════╗ ║ ║ ║ Ready on :3000 ║ ║ ║ ╚════════════════════════════╝ ``` | option | default | meaning | | :-- | :-- | :-- | | `border` | `round` | a registered border's name, or a `BorderStyle` object of your own | | `padding` | `{ x: 1, y: 0 }` | cells left and right of the text, rows above and below | | `title` | — | written into the top border, cut with `…` when the box is too narrow | | `width` | 80 | columns for the whole box, borders included; the text wraps to fit | | `href` | — | a URL, or a `file://` path, that the text links to | | `terminal` | the real process | the terminal `href` is rendered for; pass one to make the call pure | The built-in borders are `round`, `single`, `double`, `bold`, `classic` and `none`, registered through the same `register()` a plugin uses. cli-boxes' eight sets are not bundled; if you depend on [cli-boxes](https://www.npmjs.com/package/cli-boxes), `fromCliBoxes()` turns it into a plugin, after which `box('…', { border: 'arrow' })` draws with one. A name nobody registered is refused with `E_UNKNOWN_BORDER` and the names that exist. ## Width is measured, not counted Every row is measured in terminal columns with [linegauge](https://linegauge.interlace.tools/docs), so a double-width character takes two columns and the border stays in line. A box with a CJK body fits exactly the width it was given at 10, 16, 24 and 40 columns — that is a test, [`builtins.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/builtins.test.ts). There is no layout engine: `box` measures, wraps and joins strings. ## Links With `href`, the text becomes an OSC 8 hyperlink on a terminal that supports one, through [paratext](https://paratext.interlace.tools/docs). Where the link cannot be emitted — and in every static projection — the destination is written out after the text instead, so it is not lost: ```js title="docs-link.mjs" import { box } from 'flagstaff/box'; const pipe = { env: {}, isTTY: { stdout: false } }; console.log(box('docs', { width: 20, href: 'https://example.com', terminal: pipe })); ``` ```text title="node docs-link.mjs" ╭──────────────────╮ │ docs (https://ex │ │ ample.com) │ ╰──────────────────╯ ``` ## `boxComponent()`: a box with a text form A box printed with `console.log` is a drawing everywhere, including in a log file and in front of a screen reader. Hoisted as a component, the drawing is only for a terminal; everywhere else the box is its title and its text: ```js title="done.mjs" import { boxComponent } from 'flagstaff/box'; import { hoist } from 'flagstaff/loop'; import { json, rt } from './rt.mjs'; hoist(boxComponent({ width: 40 }), rt, { title: 'deploy', text: 'Live at https://example.com' }, { json }).lower(); ``` ```text title="node done.mjs" deploy: Live at https://example.com ``` The state is `{ text, title?, href? }`; an `href` in the state overrides the component's. ## Coming from boxen `flagstaff/boxen` is boxen 9's API, graded by boxen's own test suite, every case of which is a snapshot of the exact characters. It carries cli-boxes' table itself rather than reading the plugin registry, so registering a plugin never changes what a boxen caller draws. See [Coming from boxen](/docs/coming-from/boxen). --- # Live regions > Redraw a block of lines in place with flagstaff/log-update, or with a component that also has a text form for pipes. Source: https://flagstaff.interlace.tools/docs/guides/live-regions A live region is a block of lines redrawn in place: a dashboard of workers, a download list, a status panel. flagstaff has two ways to write one, and the choice is about what happens when the output is not a terminal. ## `flagstaff/log-update`: log-update's API `flagstaff/log-update` is log-update 8's API, graded by log-update's own test suite — which renders every frame through a terminal emulator and asserts the screen. ```js import logUpdate from 'flagstaff/log-update'; logUpdate('worker 1: idle\nworker 2: fetching'); logUpdate('worker 1: parsing\nworker 2: fetching'); // redraws only the row that changed logUpdate.done(); // keep the last frame, start a new region below it ``` `logUpdate()`, `.clear()`, `.done()`, `.persist()`, `createLogUpdate(stream, options)` and `logUpdateStderr` are all there, with log-update's row-level diffing: a frame whose last row is a counter costs one row of output per tick. Text too wide for the terminal is wrapped and every wrapped row closes its colour and reopens it on the next, so a region never bleeds. Because it is log-update's behaviour, it is log-update's behaviour off a terminal too: the erase sequences are written to a pipe as well, since log-update's own suite asserts them there and a drop-in that suppressed them would fail it. Two things this façade guarantees that the suite does not ask for, and [`log-update.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/log-update.test.ts) holds: it never writes a carriage return, and never an absolute cursor-home — every move is a relative row move, so a captured transcript stays parseable. The cursor it hides comes back on `SIGINT`, `SIGTERM` and `SIGHUP`, through [closeout](https://closeout.interlace.tools/docs). ## A component: the same region, with a text form When the output may be a log or an agent, write the region as a component. The frame is the panel; the static text is what a reader off a terminal needs from it: ```js title="workers.mjs" import { hoist } from 'flagstaff/loop'; import { json, rt } from './rt.mjs'; const workers = { name: 'workers', frame: (_t, s) => s.workers.map((w) => `${w.name.padEnd(9)} ${w.job}`).join('\n'), static: (s) => `${s.workers.filter((w) => w.job === 'idle').length} of ${s.workers.length} workers idle`, }; const flag = hoist(workers, rt, { workers: [{ name: 'worker 1', job: 'fetching' }, { name: 'worker 2', job: 'fetching' }] }, { json }); flag.update({ workers: [{ name: 'worker 1', job: 'parsing' }, { name: 'worker 2', job: 'fetching' }] }); flag.update({ workers: [{ name: 'worker 1', job: 'idle' }, { name: 'worker 2', job: 'parsing' }] }); flag.lower({ workers: [{ name: 'worker 1', job: 'idle' }, { name: 'worker 2', job: 'idle' }] }); ``` On a terminal that is a two-row panel repainted in place, erased from its first line. In a pipe the first update changed nothing a reader needs, and was not written: ```text title="node workers.mjs" 0 of 2 workers idle 1 of 2 workers idle 2 of 2 workers idle ``` ## Which to use | | `flagstaff/log-update` | a component on `hoist()` | | :-- | :-- | :-- | | API | log-update's, unchanged | `hoist` / `update` / `lower` | | on a terminal | row-diffed redraws | the frame, repainted | | in a pipe or CI | log-update's erase sequences | the static text, once per change | | under `--json` | not applicable | one event per transition | | with `CLI_ACCESSIBLE=1` | redraws, as log-update does | the static text, never a redraw | Keep the façade for code that already uses log-update; write a component for anything whose output is read back. --- # Plugins > Add spinners, borders, glyphs, colour tokens and components as one plain object, validated at register() and checked before it ships with flagstaff check. Source: https://flagstaff.interlace.tools/docs/guides/plugins A flagstaff plugin is one plain object, validated against [`schema.json`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json) — the same file that ships in the package as `flagstaff/schema.json` — and registered once. The built-in spinners, borders and glyphs are a plugin of exactly this shape, registered through the same door, so the built-ins cannot use anything a plugin cannot. ## The shape ```js title="nyan.mjs" export default { name: 'nyan', spinners: { nyan: { frames: ['≋', '≈', '~'], interval: 80, static: '~nyan~' } }, }; ``` | key | what it contributes | | :-- | :-- | | `name` | the plugin's name, in `flagstaff check` and the gallery | | `spinners` | `{ frames, interval, static }` per style | | `borders` | the eight characters of a box border, in cli-boxes' shape | | `glyphs` | `ok`, `fail`, `warn`, `info`, `running` — change one and every built-in that draws it changes | | `tokens` | a roundel colour theme, as `#rrggbb` values | | `components` | `{ static, frame?, interval?, sample? }` per component | | `contract` | the plugin contract version, `1`; a newer one is refused with the upgrade named | Keys another package in the family reads may sit in the same object; flagstaff keeps only its own. ## Registering ```js title="use-nyan.mjs" import { hoist } from 'flagstaff/loop'; import { register } from 'flagstaff/plugin'; import { spinner } from 'flagstaff/spinner'; import nyan from './nyan.mjs'; import { json, rt } from './rt.mjs'; register(nyan); hoist(spinner('nyan'), rt, { text: 'working' }, { json }).lower({ text: 'done', status: 'ok' }); ``` ```text title="node use-nyan.mjs" ~nyan~ working ✔ done ``` A later registration of the same name replaces the earlier one, which is how you override a built-in. `register()` is the **only** way in: `registered()` hands back a copy, so setting or clearing what it returns changes nothing, and what was registered is frozen, so editing your object afterwards does not change the registry either ([`plugin.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/plugin.test.ts)). ## Refusals A plugin the schema refuses throws a `PluginError` with a `code` and a `fix`. The one mistake made on purpose — an animation with no text form — has its own: ```js title="no-static.mjs" import { register } from 'flagstaff/plugin'; try { register({ name: 'bad', spinners: { bad: { frames: ['a', 'b'], interval: 80 } } }); } catch (error) { console.log(`${error.code}: ${error.message}`); console.log(`fix: ${error.fix}`); } ``` ```text title="node no-static.mjs" E_NO_STATIC_PROJECTION: bad has no static projection fix: give it a `static`: the text a pipe, an agent or a screen reader gets instead of the animation ``` Every refusal any flagstaff surface prints is one of eight codes — `E_PLUGIN_SCHEMA`, `E_NO_STATIC_PROJECTION`, `E_PLUGIN_CONTRACT`, `E_UNKNOWN_SPINNER`, `E_UNKNOWN_BORDER`, `E_NO_CONTRIBUTION`, `E_COMPONENT_THREW`, `E_UNKNOWN_KIND` — and a fuzz test hands `register()` arbitrary input to hold that it either registers or refuses with a code and a fix, never an unhandled throw ([`plugin-fuzz.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/plugin-fuzz.test.ts)). ## `flagstaff check` Before a plugin ships, render it. `flagstaff check` loads the file, validates it, and prints every contribution in all five modes side by side, escapes made visible: ```text title="npx flagstaff check nyan.mjs" nyan — 1 spinner, 0 borders, 0 components, 0 glyphs, 0 tokens spinner nyan tty ␛[?25l␛[?2026h≋ working␛[?2026l␛[?2026h␛[1G␛[0J≈ working␛[?2026l␛[?2026h␛[1G␛[0J~ working␛[?2026l␛[?2026h␛[1G␛[0J≋ working␛[?2026l␛[?2026h␛[1G␛[0J✔ done␛[?2026l⏎ ␛[?25h pipe ~nyan~ working⏎ ✔ done⏎ ci ~nyan~ working⏎ ✔ done⏎ json {"event":"spinner","state":{"text":"working"}}⏎ {"event":"spinner","state":{"text":"done","status":"ok"}}⏎ accessible ~nyan~ working⏎ ✔ done⏎ nyan: ok ``` It opens with a census, so a misspelled key shows as `0 spinners`, and closes with the verdict, so `ok` is never printed before the rendering that justifies it. A refusal exits 1 with the code and the fix; a plugin that validates but contributes nothing flagstaff can render is `E_NO_CONTRIBUTION`, and a component whose `static` throws is `E_COMPONENT_THREW`, naming the modes it broke in. ## Components and `sample` A component contributed by a plugin may declare `sample: { running, done }`, the two states `flagstaff check` and the docs gallery show it with. The loop never reads it. Without one, both assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output, rather than rendering an invented state as if it were yours. ## Bringing a corpus cli-spinners' styles and cli-boxes' borders are not bundled. `flagstaff/import` turns the copy you already depend on into an ordinary plugin, through the same `register()` and schema: ```js import cliSpinners from 'cli-spinners'; import { fromCliSpinners } from 'flagstaff/import'; import { register } from 'flagstaff/plugin'; import { spinner } from 'flagstaff/spinner'; register(fromCliSpinners(cliSpinners)); spinner('moon'); ``` cli-spinners has no text form for its styles, so `fromCliSpinners` gives each one `…`, or whatever your `staticFor(name, spinner)` returns. `fromCliBoxes(cliBoxes)` does the same for borders; their shape is already flagstaff's. --- # Spinners, progress and task lists > The three components that show work in motion — spinner, progress and tasks — and what each prints off a terminal. Source: https://flagstaff.interlace.tools/docs/guides/spinners Three built-in components show work that is still going. Each is a factory on its own subpath, so a program that only wants a spinner pays for a spinner. ## Spinners ```js import { spinner } from 'flagstaff/spinner'; spinner(); // the `dots` style spinner('line'); // any registered style, by name ``` The state is `{ text, status }`. `status` is `running` when omitted, and one of `ok`, `fail`, `warn` or `info` when the work is done. A finished status draws its glyph — `✔`, `✖`, `⚠`, `ℹ` — the same in every mode; a running one is the style's frames on a terminal and its static text (`…` for the built-ins) everywhere else. ```js title="deploy.mjs" import { hoist } from 'flagstaff/loop'; import { spinner } from 'flagstaff/spinner'; import { json, rt } from './rt.mjs'; const flag = hoist(spinner('line'), rt, { text: 'uploading' }, { json }); flag.update({ text: 'uploading: retrying once', status: 'warn' }); flag.lower({ text: 'upload failed', status: 'fail' }); ``` ```text title="node deploy.mjs" … uploading ⚠ uploading: retrying once ✖ upload failed ``` Two styles ship built in, `dots` and `line`, and they are registered through the same `register()` a third-party plugin uses. The ~80 styles of [cli-spinners](https://www.npmjs.com/package/cli-spinners) are not bundled; if you already depend on it, `fromCliSpinners()` turns it into a plugin — see [Plugins](/docs/guides/plugins). An unknown name is refused with `E_UNKNOWN_SPINNER` and the list of names that exist. ## Progress ```js import { progress } from 'flagstaff/progress'; progress(); // a 24-cell bar of █ and ░ progress({ width: 40, glyphs: { filled: '#', empty: '.' } }); ``` The state is `{ done, total, label? }`, and `done` is clamped into `[0, total]`. A terminal gets a drawn bar with the count; everything else gets the count and the percentage, because a bar of blocks in a log tells a reader nothing: ```js title="copy.mjs" import { hoist } from 'flagstaff/loop'; import { progress } from 'flagstaff/progress'; import { json, rt } from './rt.mjs'; const flag = hoist(progress(), rt, { done: 0, total: 30, label: 'files' }, { json }); flag.update({ done: 12, total: 30, label: 'files' }); flag.lower({ done: 30, total: 30, label: 'files' }); ``` ```text title="node copy.mjs" 0/30 files · 0% 12/30 files · 40% 30/30 files · 100% ``` Off a terminal every state that changes the text is a line, so a loop that updates on every item writes a line per item. [Progress in logs](/docs/recipes/progress-in-logs) shows how to update in steps instead. ## Task lists ```js import { tasks } from 'flagstaff/tasks'; tasks(); // the running task drawn with the `dots` spinner tasks({ spinner: 'line' }); ``` The state is `{ tasks: [{ title, status?, detail? }] }`. `status` is `pending` when omitted, then `running`, then one of `ok`, `fail`, `warn` or `info`. A terminal draws every task, the running one animated and its `detail` under it. Off a terminal a task is printed **once, when it settles** — a pending or running task is not news yet, and a finished one is never printed twice: ```js title="release.mjs" import { hoist } from 'flagstaff/loop'; import { tasks } from 'flagstaff/tasks'; import { json, rt } from './rt.mjs'; const flag = hoist(tasks(), rt, { tasks: [{ title: 'install', status: 'running' }, { title: 'build' }, { title: 'test' }] }, { json }); flag.update({ tasks: [{ title: 'install', status: 'ok' }, { title: 'build', status: 'running', detail: 'tsc' }, { title: 'test' }] }); flag.update({ tasks: [{ title: 'install', status: 'ok' }, { title: 'build', status: 'ok' }, { title: 'test', status: 'running' }] }); flag.lower({ tasks: [{ title: 'install', status: 'ok' }, { title: 'build', status: 'ok' }, { title: 'test', status: 'fail' }] }); ``` ```text title="node release.mjs" ✔ install ✔ build ✖ test ``` [Build steps](/docs/recipes/build-steps) runs a real list of steps this way. ## Coming from ora `flagstaff/ora` is ora 10's API, graded by ora's own test suite, and it behaves as ora does — including in a pipe, where ora writes a start line and a final line and nothing in between. The spinner above is the loop underneath it; [Coming from ora](/docs/coming-from/ora) and [Incremental migration](/docs/recipes/incremental-migration) cover moving from one to the other. --- # The static projection > What a component prints in a pipe, in CI, for a screen reader and under --json — and how to write one. Source: https://flagstaff.interlace.tools/docs/guides/static-projection Every flagstaff component has a **static projection**: `static(state)`, a function from the component's state to plain text. It is required. The animation, `frame(t, state)`, is optional and only a terminal sees it. Everything else — a pipe, a CI log, a screen reader, the docs gallery — gets the static text. ## A component A component is a plain object: | key | required | what it is | | :-- | :-- | :-- | | `name` | yes | the event name under `--json` | | `static(state)` | yes | the text form of a state | | `frame(t, state)` | no | what a terminal shows at time `t`, in milliseconds since hoisting | | `interval` | no | milliseconds between repaints when there is a `frame`; 80 by default | Here is one written from scratch, with a frame for the terminal and a count for everyone else: ```js title="checks.mjs" import { setTimeout as sleep } from 'node:timers/promises'; import { hoist } from 'flagstaff/loop'; import { json, rt } from './rt.mjs'; const checks = { name: 'checks', interval: 100, static: (s) => `${s.done} of ${s.total} checked`, frame: (t, s) => `${'|/-\\'[Math.floor(t / 100) % 4]} ${s.done} of ${s.total} checked`, }; const flag = hoist(checks, rt, { done: 0, total: 3 }, { json }); for (let done = 1; done <= 3; done += 1) { await sleep(150); flag.update({ done, total: 3 }); } flag.update({ done: 3, total: 3 }); flag.lower(); ``` In a pipe, each state that changes the text is one line. The last `update` changed nothing, so it printed nothing, and `lower()` without a state keeps the one it has: ```text title="node checks.mjs" 0 of 3 checked 1 of 3 checked 2 of 3 checked 3 of 3 checked ``` ## The rules off a terminal These hold in the `pipe`, `ci` and `accessible` modes, and each is a test in [`loop.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/loop.test.ts) or [`builtins.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/builtins.test.ts): - **Once per change.** A state whose text is the same as the last one written prints nothing. - **No carriage return and no cursor escape.** Colour is allowed when the user explicitly asked for it (`FORCE_COLOR`, `--color`); a cursor move or an erase never is. - **A growing list is appended to, not reprinted.** When the new text starts with the lines already written, only the lines past them are written. That is how [`tasks`](/docs/guides/spinners#task-lists) prints each task once, as it settles. - **Nothing to say costs nothing.** An empty static text writes no blank line. ## Under `--json` Pass `{ json: true }` to `hoist()` when your program was run with `--json`. The loop then writes one NDJSON event per transition to **stderr** — `{"event": , "state": }` — and leaves stdout alone for your program's own result. An agent parses the state instead of scraping text: ```text title="node checks.mjs --json" {"event":"checks","state":{"done":0,"total":3}} {"event":"checks","state":{"done":1,"total":3}} {"event":"checks","state":{"done":2,"total":3}} {"event":"checks","state":{"done":3,"total":3}} {"event":"checks","state":{"done":3,"total":3}} {"event":"checks","state":{"done":3,"total":3}} ``` Every `update` is a transition here, including one that changes nothing, and `lower()` is one more: an agent sees when the component was lowered, not just its last state. The state is serialised as you passed it, so keep it plain data. ## Why a projection, not a stripped frame Stripping the escapes out of a frame does not make it text. A bar of blocks with the colour removed is still a bar of blocks; a box with its border is still a drawing. The static text is written for a reader who cannot see the drawing, which is why the built-ins say `12/30 files · 40%` rather than `████░░░░`, and `title: text` rather than a border. --- # Tables > Draw a table as a string with table(), or hoist tableComponent() so a pipe gets header-and-value pairs and --json gets the rows. Source: https://flagstaff.interlace.tools/docs/guides/tables `flagstaff/table` has `table()`, which returns the grid as a string, and `tableComponent()`, which hoists on the loop. ## `table(rows, options)` ```js title="grid.mjs" import { table } from 'flagstaff/table'; console.log(table([['ora', '99'], ['log-update', '99']], { head: ['host', 'tests'], align: ['left', 'right'], width: 30 })); console.log(table([['古池や', 'Bashō'], ['a very long cell that wraps', 'x']], { head: ['poem', 'poet'], width: 26 })); ``` ```text title="node grid.mjs" ┌────────────┬───────┐ │ host │ tests │ ├────────────┼───────┤ │ ora │ 99 │ │ log-update │ 99 │ └────────────┴───────┘ ┌────────────────┬───────┐ │ poem │ poet │ ├────────────────┼───────┤ │ 古池や │ Bashō │ │ a very long │ x │ │ cell that │ │ │ wraps │ │ └────────────────┴───────┘ ``` | option | default | meaning | | :-- | :-- | :-- | | `head` | — | column headers; without them there is no header row | | `width` | 80 | the most columns the whole table may take | | `align` | `left` | `'left'` or `'right'` per column | | `terminal` | the real process | the terminal a linked cell is rendered for | A row is an array of cells, and a cell is a string or `{ text, href }` — a link, rendered the way [box links](/docs/guides/boxes#links) are. Every row is measured in terminal columns, so a wide character never pushes the grid out, and a cell too wide for its column wraps inside it rather than widening the table. The table never overruns the width it was given ([`builtins.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/builtins.test.ts) checks 20, 40 and 80 columns with a wide cell). ## `tableComponent()`: rows a reader can use A grid in a log file is box-drawing characters around the data. Hoisted as a component, a table is drawn only on a terminal. Everywhere else each row is one line of `header: value` pairs: ```js title="results.mjs" import { hoist } from 'flagstaff/loop'; import { tableComponent } from 'flagstaff/table'; import { json, rt } from './rt.mjs'; const rows = [ ['api', 'passed', '112'], ['web', 'failed', '3'], ]; hoist(tableComponent({ head: ['suite', 'result', 'tests'] }), rt, { rows }, { json }).lower(); ``` ```text title="node results.mjs" suite: api, result: passed, tests: 112 suite: web, result: failed, tests: 3 ``` Without `head`, the pairs fall back to tab-separated values. Under `--json` the rows are the state, so an agent gets them as data: ```text title="node results.mjs --json" {"event":"table","state":{"rows":[["api","passed","112"],["web","failed","3"]]}} {"event":"table","state":{"rows":[["api","passed","112"],["web","failed","3"]]}} ``` Two events, because hoisting is one transition and lowering is another. ## Coming from cli-table3 `flagstaff/cli-table3` is cli-table3 0.6.5's API — `head`, `chars`, `style`, `colWidths`, `rowHeights`, alignment, `wordWrap`, `colSpan` and `rowSpan`, `href` — graded by the cases of cli-table3's own suite that go through its public API. It extends `Array`, as cli-table3 does. See [Coming from cli-table3](/docs/coming-from/cli-table3) and [Compatibility](/docs/drop-ins) for what that grade covers. --- # flagstaff > Spinners, progress, boxes and tables on one terminal frame loop, with a static projection for pipes, screen readers and agents, and a plugin host. No dependency outside the burgee family. Drop-in paths for ora, log-update, boxen and cli-table3. Source: https://flagstaff.interlace.tools/docs **flagstaff** is a terminal frame loop: it hoists a component, holds it, changes it and lowers it. Every mode but the terminal gets a **static projection** — one line per state on a pipe, one event per transition under `--json`, plain text for a screen reader — so no state is lost to the log an agent reads back. Plugins are data, and there is no layout engine. Drop-in for **ora**, **log-update**, **boxen** and **cli-table3**: `npx burgee migrate` moves the imports, each path graded by the incumbent's own suite — see [Migrating](#migrating). ora animates a spinner on a terminal and, off one, writes the line it started with and the line it stopped with — every state in between is lost. Ink fixes the terminal by shipping React and a layout engine. flagstaff takes four dependencies, all from this repository: [roundel](https://github.com/ofri-peretz/burgee/blob/main/packages/roundel/README.md), [paratext](https://github.com/ofri-peretz/burgee/blob/main/packages/paratext/README.md), [linegauge](https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/README.md) and [closeout](https://github.com/ofri-peretz/burgee/blob/main/packages/closeout/README.md). A **flagstaff** is the simplest part of the whole apparatus and the only one that is always in view: a flag is hoisted on it, held there, changed, and lowered when it is done. That is a terminal render loop — the place frames are hoisted, held, changed and lowered, in view of whoever is reading. ## Install ```bash npm install flagstaff pnpm add flagstaff yarn add flagstaff bun add flagstaff ``` ## Quick start ```js import { hoist } from 'flagstaff/loop'; import { spinner } from 'flagstaff/spinner'; const rt = { env: process.env, isTTY: { stdout: Boolean(process.stdout.isTTY) }, stdout: process.stdout, stderr: process.stderr, clock: { now: () => performance.now(), schedule(fn, ms) { const timer = setTimeout(fn, ms); return () => clearTimeout(timer); }, }, }; const flag = hoist(spinner(), rt, { text: 'building' }); // ... work ... flag.update({ text: 'linking' }); flag.lower({ text: 'built', status: 'ok' }); ``` `rt` is a runtime — `{ env, isTTY: { stdout }, stdout, stderr, clock }`, written out above; burgee's satisfies it, so does a literal with `manualClock()` in a test. Which of the five modes runs is decided once by roundel's output policy, never by this package: | mode | what the same three calls write | | :-- | :-- | | `tty` | `⠋ building` repainted in place on the clock, then `✔ built` left on screen | | `pipe`, `ci` | `… building` ⏎ `… linking` ⏎ `✔ built` — one line per state change, no `\r`, no escape | | `json` | `{"event":"spinner","state":{"text":"building"}}` … one NDJSON event per transition, on stderr | | `accessible` | the static text, never a redraw | ## What is here ### The loop `hoist(component, rt, initial, { json })` returns `{ update(state), lower(state?), mode }`. A component is `{ name, static(state), frame?(t, state), interval? }`. `static` is required and is the artifact: what a pipe, an agent, a screen reader and the docs gallery read. `frame` is optional and decorative. Nothing in the loop reads `process`; time comes from `rt.clock`, so `manualClock()` makes a spinner's terminal output a fixed string a test can assert byte for byte. ### The plugin host A plugin is one plain object, validated against [`schema.json`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json) — the same file that ships in the tarball — and registered once: ```js // a third-party plugin, in full export default { name: 'nyan', spinners: { nyan: { frames: ['≋', '≈', '~'], interval: 80, static: '…' } }, }; ``` ```js import { register } from 'flagstaff/plugin'; register(nyan); spinner('nyan'); ``` Keys: `spinners`, `borders`, `glyphs` (`ok`, `fail`, `warn`, `info`, `running` — change them and every built-in that draws one changes), `tokens` (a roundel theme), `components`. A spinner or component without a `static` is refused at `register()` with `E_NO_STATIC_PROJECTION` and a fix. The built-in `dots` and `line` styles are a plugin of exactly this shape, registered through the same door, so the built-ins cannot grow an API a plugin cannot reach. `register()` is the **only** way in, and that is a property rather than a convention: `registered()` hands back a copy — new maps over the frozen objects `register()` stored — so `registered().spinners.set(…)` puts nothing in the registry and `.clear()` empties nothing. A contribution that never met `validate()` cannot be reached by `spinner()`, `box()` or the gallery, which is what makes "refused at the door" (U3) a fact about the code rather than advice. Freezing also means the object you registered stays yours: edit it afterwards and the registry does not change. A component may declare `sample: { running, done }` — the two states `flagstaff check` and the docs gallery *show* it with. The loop never reads it; a running program's state comes from the program. Without one, both assume `{ phase: 'running' }` / `{ phase: 'done' }` and say so in the output, rather than rendering an invented state as if it were yours. ### The built-ins Five components, each on its own subpath, each answering the static projection for itself: ```js import { progress } from 'flagstaff/progress'; import { tasks } from 'flagstaff/tasks'; import { box, boxComponent } from 'flagstaff/box'; import { table, tableComponent } from 'flagstaff/table'; ``` | component | on a terminal | everywhere else | | :-- | :-- | :-- | | `spinner` | `⠹ building` | `… building`, then `✔ built` | | `progress` | a bar of blocks | `12/30 files · 40%` | | `tasks` | every task, the running one animated | one line per task that has **settled** | | `box` | the border, padding and title | `title: text` | | `table` | the grid | one line per row of `header: value` pairs | That table is the package's argument in one place. A bar of `█` in a log file tells an agent nothing and tells a screen reader less; the count and the percentage tell both. Inside a burgee `--mcp` tool call that asked for progress, `flagstaff/progress` goes one step further and draws nothing: it sends its count to the client as `notifications/progress`, with the static line as the message. That bridge lives on the subpath only; `progress` from the root `flagstaff` entry always draws. `box` and `table` are also plain string functions, because most callers want the string: ```js box('Ready on :3000', { title: 'dev', width: 40 }); table([['ora', '99'], ['log-update', '99']], { head: ['host', 'tests'] }); ``` No layout engine, and there will not be one: these measure with `width()`, wrap with `wrap()`, and join strings. A table column can be bounded. `columns` is positional, like `align`: `max` caps a column whatever its content, and `min` keeps it open while another column can give way. Both count the content's display width, not the padding. When the mins add up past `width`, the mins win and the table is wider than asked. A cell wider than its column wraps; `overflow: 'truncate'` cuts it to one row ending in `…` instead, by display width, with a link kept around what is left. The static projection has no columns to cap, so it prints every cell whole. ```js table(rows, { head: ['id', 'status'], columns: [{ max: 12, overflow: 'truncate' }, { min: 10 }] }); ``` ### A log tail and a tab bar Two output-only components for screens built on flagstaff — controlroom's panes, or a plain scrolling terminal. Neither reads a key. ```js import { logTail } from 'flagstaff/log-tail'; import { tabBar } from 'flagstaff/tab-bar'; hoist(logTail({ height: 5 }), rt, { lines: [{ step: 'Installing' }, 'npm install', 'added 12 packages'] }); hoist(tabBar(), rt, { tabs: ['Status', 'Tail logs', 'Visualizer'], active: 1 }); ``` | component | on a terminal | everywhere else | | :-- | :-- | :-- | | `logTail` | the last _n_ rows, `┊` before each line, `◆` on the step in progress — pinned to the top once it scrolls out | the whole stream, **appended**: each new line printed once, nothing repainted | | `tabBar` | `Status · Tail logs · Visualizer`, the active tab styled through roundel's `heading` (bracketed without colour) | the active tab's label | Importing either registers its built-in (`log-tail`, `tab-bar`) through the same public `register()` a plugin uses, so a plugin can replace it; the `┊` and `◆` marks are the `tail` and `step` glyphs. The task list's pending mark is the `pending` glyph too — a space unless a plugin sets one, `◻` say. ### A streamed reply and a diff Two components for chat-style programs, where a model's reply arrives a token at a time and an edit is shown before it is written. ```js import { markdown } from 'flagstaff/markdown'; import { diff } from 'flagstaff/diff'; let text = ''; const reply = hoist(markdown(), rt, { text }); for await (const token of stream) reply.update({ text: (text += token) }); reply.lower({ text, done: true }); hoist(diff(), rt, { diff: patch }).lower(); ``` | component | on a terminal | everywhere else | | :-- | :-- | :-- | | `markdown` | the reply so far: headings, lists, emphasis, inline code and fenced code styled through roundel; with no colour, the source as written | the markdown's own source, **committed a block at a time** — each block printed once, when a later line closes it | | `diff` | old and new line numbers on every line, removals and additions coloured | the diff unchanged, byte for byte | A block is committed only by a complete line — a blank line, a heading, a fence, a closing fence — so wherever the stream is cut, the blocks committed so far are the whole reply's first blocks; the suite cuts a fixture at every byte offset and holds that. There is no syntax highlighting inside a fence. `markdownBlocks(text, done)` is exported for a program that places committed blocks itself, as controlroom's inline screen does. ### Writing a whole frame `hoist` repaints one component. A program that lays several out in one frame paints the frame through the same writer `hoist` uses: ```js import { frameWriter } from 'flagstaff/loop'; const frame = frameWriter(process.stdout); frame.paint(['Status · Tail logs', '┊ npm install', '┊ added 12 packages']); frame.paint(['Status · Tail logs', '┊ npm install', '┊ added 13 packages']); // rewrites one row frame.release(); // the frame stays on screen as scrollback ``` `paint` writes only the rows that changed since the last paint, inside one synchronized-output block (`ESC[?2026h … ESC[?2026l`), so a terminal that supports it never shows a half-drawn frame and one that does not ignores the two sequences. It is the only grid-writing code in the family: controlroom composites through it rather than painting on its own. ### Bringing a corpus with you The ecosystem already has ~80 spinner styles and eight border sets, as plain JSON. Neither is bundled here — the weight of a corpus nobody asked for is the thing this package exists to avoid — so `flagstaff/import` turns the one you have into an ordinary plugin: ```js import cliSpinners from 'cli-spinners'; import { fromCliSpinners } from 'flagstaff/import'; import { register } from 'flagstaff/plugin'; register(fromCliSpinners(cliSpinners)); spinner('moon'); ``` `fromCliBoxes(cliBoxes)` does the same for borders, after which `box('…', { border: 'arrow' })` draws with one. Both go through the same `register()` and the same schema, so the gallery opens full and a third-party plugin starts as a copy of one of these. The importer is 838 B and reaches nothing. ### `flagstaff check` ```bash npx flagstaff check ./nyan.mjs ``` Loads the file, validates it, and prints every contribution in all five modes side by side — escapes made visible — so an author, or an agent that just wrote one, sees the static projection next to the animation before anything ships. Exit 1 on a refusal, with the code and the fix. It opens with a census of what it found and closes with the verdict, so `ok` is never printed before the rendering that would justify it: ```text nyan — 1 spinner, 0 borders, 0 components, 0 glyphs, 0 tokens spinner nyan tty ␛[?25l␛[?2026h≋ working␛[?2026l␛[?2026h␛[1G␛[0J… pipe ~nyan~ working⏎ ✔ done⏎ … nyan: ok ``` `0 spinners, 0 components` is how a misspelled key tells on itself. The schema is `additionalProperties: true` on purpose — a key another package in the family reads belongs in the same object — so a typo cannot be refused by the schema, and `check` is the surface that has to notice: ```text typo — 0 spinners, 0 borders, 0 components, 0 glyphs, 0 tokens unknown componets, spinner — flagstaff reads none of these; a key another package in the family reads is allowed here E_NO_CONTRIBUTION: typo registers, but contributes nothing flagstaff can render fix: flagstaff reads spinners, borders, components, glyphs and tokens; check those spellings. … ``` Each component block names the state it was rendered with — the component's own `sample` when it declares one, and otherwise the assumed `{ phase }` shape, said out loud. A `static` that throws on the state it is handed is a refusal like any other, naming the modes it broke in (`json` emits the state and never calls `static`, so it is usually the one that survives): ```text E_COMPONENT_THREW: g threw in tty, pipe, ci, accessible fix: `static(state)` must return a string for the state it is rendered with; … ``` ## Migrating Four drop-in paths, one import each. `npx burgee migrate --dry-run` lists the imports the codemod would rewrite — only drop-ins graded level with their incumbent — and `npx burgee migrate` makes the change ([Migrate](https://burgee.interlace.tools/docs/migrate)). ### The ora path `flagstaff/ora` is ora 10's whole API, graded **223 / 223 by ora's own test suite** through [`compat-oracle`](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/README.md). One import changes: ```diff -import ora from 'ora'; +import ora from 'flagstaff/ora'; ``` Everything else stays: `ora({ text, spinner, color, indent, prefixText, suffixText })`, `.start() .stop() .succeed() .fail() .warn() .info() .stopAndPersist()`, `oraPromise()`, the `spinners` corpus, the stream hooks that keep a `console.log` above the frame, the synchronized-output sequences, the pause while another writer's line is unfinished, the frame kept above the last row of the screen, the stdin discarder. What changes is the bill. ora 10.0.0 ships 128,182 B of JavaScript across **seventeen packages** — ora, chalk, cli-spinners, string-width, log-symbols, cli-cursor, restore-cursor, onetime, mimic-function, signal-exit, is-interactive, is-unicode-supported, stdin-discarder, yoctocolors, strip-ansi, ansi-regex, get-east-asian-width. `flagstaff/ora` is 53,486 B across **two** — itself and roundel — **42% of ora's**, and 20,250 B of that is the spinner corpus ora's API re-exports. Nothing in it reaches the frame loop, so a program that migrates its spinner today can adopt `hoist()` a file at a time, or never ([measured](https://burgee.interlace.tools/docs/benchmarks)). Both sides are counted the same way, so the number reproduces: shipped code and data — `.js`/`.mjs`/`.cjs` plus the `.json` a module imports, `package.json` never counted. Ours is the import graph walked from `dist/` by `weight.test.ts`; ora's is every package that graph touches in ora's own resolved tree, counted whole ([measured](https://burgee.interlace.tools/docs/benchmarks)). The cursor comes back the way ora's does. A spinner that hid the cursor restores it on a clean exit **and** on `SIGINT`, `SIGTERM` and `SIGHUP` — node does not run `'exit'` listeners for a signalled process, and Ctrl+C is how a spinner usually dies — then re-raises the signal so the process still terminates, unless the program installed its own handler for it. That is what ora buys with `restore-cursor` → `signal-exit`; here it is twenty lines and no dependency. ora's own 99 never kill a process, so `ora.test.ts` grades it instead. **The repo's accessible switch does not reach this surface.** `CLI_ACCESSIBLE=1` changes what `roundel/policy` decides for every other entry point in the stack, but `flagstaff/ora` imports `roundel/chalk` and never the policy, because a façade that reinterpreted its host would fail the host's suite — so on a terminal `CLI_ACCESSIBLE=1` still animates and still writes cursor escapes (measured: 4 escapes, cursor hidden), where `CI=true` disables the spinner outright (0 escapes) because that is ora's own rule. Off a terminal it is moot. Use `hoist()` when you want the switch to be honoured. The static projection is the reason to move on eventually, not the reason to move: `hoist()` is what gives a pipe one line per state instead of frames. `flagstaff/ora` is the door, and it is deliberately ora's behaviour to the byte. ### The log-update path `flagstaff/log-update` is log-update 8's API, graded **99 / 99 by log-update's own test suite** — which renders every frame through a real terminal emulator and asserts the screen, not the bytes. ```diff -import logUpdate from 'log-update'; +import logUpdate from 'flagstaff/log-update'; ``` `logUpdate()`, `.clear()`, `.done()`, `.persist()`, `createLogUpdate(stream, options)` and `logUpdateStderr`, with the row-level diffing intact: a five-row frame whose last row is a counter costs one row of output per tick, not five. log-update ships 114.3 KB across **sixteen** packages. This subpath reaches two, both from this repository: `linegauge/wrap` for the wrapper, and `closeout` for the cursor. It carries no port of `slice-ansi` — the wrapper already makes every row self-contained, so clipping a frame to the terminal's height is an array slice. `signal-exit`, 22.0 KB of those sixteen, is `closeout`'s to own, shared with the ora façade because both incumbents port the same `cli-cursor` → `restore-cursor` → `signal-exit` chain. Ctrl+C mid-frame puts your cursor back, and still terminates — unless your program installed its own `SIGINT` handler, in which case it is delivered once, to you, and this stays out of it ([measured](https://burgee.interlace.tools/docs/benchmarks)). **This is the one façade that lowers a layer guarantee, and it says so.** R5 — no cursor escape off a terminal — cannot survive here: log-update's own suite asserts erase sequences on a plain non-TTY stream, so a façade that suppressed them would fail the suite that is the whole claim. What survives is the half the complaint behind R5 was actually about: nothing this writes is ever a `\r`, **and never an absolute cursor-home** — every move is a relative row move — on a terminal or off one, so a captured transcript stays parseable. `log-update.test.ts` asserts both halves, and `hoist()` is what gives you the whole guarantee. ### The boxen path `flagstaff/boxen` is boxen 9's API, graded **213 / 213 by boxen 9.0.0's own test suite** — whose cases assert the exact characters the box comes out as. boxen 8 is no longer graded or claimed: 9 changed two of its answers (a hex colour must be real hex, and `vertical` / `horizontal` are a fallback for the sides rather than an override), and this follows 9. ```diff -import boxen from 'boxen'; +import boxen from 'flagstaff/boxen'; ``` `borderStyle` (all eight of cli-boxes', a style object, or `none`), `borderColor`, `backgroundColor`, `borderBackgroundColor`, `dimBorder`, `title`, `titleColor` and `titleAlignment`, `footer` and `footerAlignment`, `textAlignment`, `padding`, `margin`, `width`, `maxWidth`, `height`, `float`, `fullscreen`, and `_borderStyles`. A tab, a backspace or a cursor move inside the text, a label or a border is written the way a terminal would draw it, as boxen 9 does, so it cannot break the box. The drawing **is** the contract here, and matching it byte for byte is the compatibility claim rather than a way of avoiding one: a user leaving boxen cares about one thing, whether the box still looks the same. It carries cli-boxes' table itself rather than reading the plugin registry — a façade whose drawing changed when somebody registered a plugin would be reinterpreting its host. Named borders through the registry are `flagstaff/box`'s job. ### The cli-table3 path `flagstaff/cli-table3` is cli-table3 0.6.5's API, graded **29 / 29 by cli-table3's own test suite** — the 38 of its cases that go through the public surface, less the nine that grade `cli-table`, the *legacy* incumbent, and so pass whatever the target is. The other 197 `require('../src/…')` and test its four internal modules directly; those are reported beside the number and never gate it, because passing them would mean copying cli-table3's file layout rather than matching its behaviour. ```diff -const Table = require('cli-table3'); +import Table from 'flagstaff/cli-table3'; ``` `head`, `chars`, `style` (padding, `head`, `border`, `compact`), `colWidths`, `rowHeights`, `colAligns`, `rowAligns`, `truncate`, `wordWrap`, `wrapOnWordBoundary`, per-cell `colSpan`, `rowSpan`, `hAlign`, `vAlign`, `href`, and the `debug` channel with `table.messages` and `Table.reset()`. It extends `Array`, because cli-table3 does and its callers push rows onto it. Four dependencies folded into one module rather than four: cli-table3's `table.js`, `layout-manager.js`, `cell.js` and `utils.js` become one file, because the architecture is not the contract — the drawing is. ## Compatibility Each façade is graded by its incumbent's own suite, unedited, through [`compat-oracle`](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/README.md), beside a control run of the same suite against the incumbent itself. What each suite does and does not reach is under its path above — cli-table3's number counts only the cases that go through its public surface, and ora's never kills a process, so `ora.test.ts` grades the signal path instead. The current grades are generated under *Benchmarks* below and published on the [compatibility page](https://burgee.interlace.tools/docs/compatibility). ## Weight Every subpath is a lock, not a convention, and the numbers below are asserted by `weight.test.ts` against `dist/`, not estimated: `flagstaff/loop` reaches 4.4 KB on disk and never the plugin registry; `flagstaff/plugin` 8.4 KB, of which 2.4 KB is the schema; `flagstaff/spinner` 9.4 KB; `flagstaff/ora` 44.1 KB — 53.5 KB with roundel counted, against ora 10's own 128.2 KB; `flagstaff/log-update` 29.6 KB, against log-update's own 114.3 KB across sixteen, reaching only `linegauge/wrap` and `closeout`; `flagstaff/boxen` 17.6 KB — 67.8 KB with linegauge and roundel counted, against boxen 9's own 115.8 KB across fourteen; `flagstaff/cli-table3` 32.9 KB — 42.3 KB with roundel, against cli-table3's own 106.0 KB across seven. The three façades share `wrap.js` and `width.js`, and the first two share `closeout`; none reaches another's port, and none reaches the core. `sideEffects: false` lets a bundler drop what a program does not use. ESM with a `default` condition, so `require('flagstaff/spinner')` works from CommonJS on Node 22.13+ ([measured](https://burgee.interlace.tools/docs/benchmarks)). ## Benchmarks Every number here is produced by `npm run bench` and published at [burgee.interlace.tools/docs/benchmarks](https://burgee.interlace.tools/docs/benchmarks). Graded by the incumbent's own test suite: | suite | passing | | :-- | --: | | `boxen` | 213 / 213 | | `cli-table3` | 29 / 29 | | `log-update` | 99 / 99 | | `ora` | 223 / 223 | ## For agents - **Off a terminal, frames become facts.** Every component has a required `static` projection: one line per state on a pipe, one NDJSON event per transition under `--json`, plain text for a screen reader — never `\r` after `\r` into the log an agent reads back. - **The mode is decided once, by roundel's output policy**, never by this package, so the spinner, the prompt and the help cannot disagree about the terminal. - **A plugin can be checked before it ships.** `npx flagstaff check ./nyan.mjs` prints every contribution in all five modes side by side, escapes made visible, and exits 1 with the code and the fix on a refusal — so an agent that just wrote one sees the static projection before anything ships. - **The docs are machine-readable** at [flagstaff.interlace.tools/llms.txt](https://flagstaff.interlace.tools/llms.txt) and [llms-full.txt](https://flagstaff.interlace.tools/llms-full.txt). ## Gallery Every component, every registered plugin and every border is on the [gallery](https://burgee.interlace.tools/docs/gallery), which is generated by running them — the static projection beside the animation, in all five modes. ## Design documents The intent and the design are committed beside the code, with the status of every requirement: [`.sdlc/intents/flagstaff/`](https://github.com/ofri-peretz/burgee/tree/main/.sdlc/intents/flagstaff). If your CLI needs a component the [gallery](https://burgee.interlace.tools/docs/gallery) does not show, [open an issue](https://github.com/ofri-peretz/burgee/issues). ## API The loop, the plugin host and the built-ins are under [What is here](#what-is-here); every export, with its types, is on [flagstaff.interlace.tools](https://flagstaff.interlace.tools/docs). ## Where it sits Plugins register under the `tokens`, `glyphs`, `spinners`, `borders`, `components` keys, against the one schema the whole family shares. `controlroom` builds on it, and it builds on `closeout`, `linegauge`, `paratext`, `roundel`. ## The family Eleven packages, one repository, one release pipeline. Each installs and works on its own, and each owns one job. Six of them — `bellpull`, `closeout`, `linegauge`, `paratext`, `roundel`, `seniority` — depend on nothing; the others depend only on packages in this table, and every dependency points one way, down the [layers](https://burgee.interlace.tools/docs/concepts/family). No package declares a dependency from outside the family; `react` and `react-reconciler` are optional peers of `controlroom`, which npm does not install; `vitest` is an optional peer of `oxmutate`, which npm does not install. [`package-shape-lock`](https://github.com/ofri-peretz/burgee/blob/main/scripts/package-shape-lock.test.ts) holds that for every manifest, and [`independence-install-lock`](https://github.com/ofri-peretz/burgee/blob/main/scripts/independence-install-lock.test.ts) installs each package alone and finds nothing but the family packages it declares. Releases are published by one workflow through npm trusted publishing: no npm token is used, and each release carries SLSA provenance, which `npm view dist.attestations` shows. [`trusted-publishing-lock`](https://github.com/ofri-peretz/burgee/blob/main/scripts/trusted-publishing-lock.test.ts) keeps both true. | Package | What it is | Migrates from | | :-- | :-- | :-- | | [burgee](https://burgee.interlace.tools/docs/packages/burgee) | The CLI framework: one declaration, every surface | commander, yargs and meow | | [roundel](https://roundel.interlace.tools/docs) | Colour: one output policy, semantic tokens, a theme | chalk, colorette, kleur, picocolors, supports-color and yoctocolors | | **flagstaff** (this package) | The frame loop: spinners, progress, boxes and tables | ora, log-update, boxen and cli-table3 | | [caique](https://caique.interlace.tools/docs) | Prompts that are flags first, and never hang | @inquirer/core | | [linegauge](https://linegauge.interlace.tools/docs) | Measuring, wrapping, truncating and slicing styled text | string-width, wrap-ansi, strip-ansi, slice-ansi, cli-truncate and widest-line | | [paratext](https://paratext.interlace.tools/docs) | Hyperlinks, images, title, clipboard and notifications | ansi-escapes, terminal-link and term-img | | [seniority](https://seniority.interlace.tools/docs) | Configuration precedence and discovery, with provenance | cosmiconfig, lilconfig, dotenv and rc | | [closeout](https://closeout.interlace.tools/docs) | Exit handlers, terminal restore and a bounded shutdown | signal-exit, exit-hook, restore-cursor, cli-cursor and onetime | | [bellpull](https://bellpull.interlace.tools/docs) | Subprocesses, and which executable actually ran | cross-spawn and which | | [controlroom](https://controlroom.interlace.tools/docs) | Keyboard-driven terminal screens, inline or full-screen | ink | | [oxmutate](https://oxmutate.interlace.tools/docs) | Mutation testing on the oxc engine, inside your own Vitest | Stryker | Every migration guide, and the family-wide [compatibility](https://burgee.interlace.tools/docs/compatibility) and [benchmarks](https://burgee.interlace.tools/docs/benchmarks) pages, are on [burgee.interlace.tools](https://burgee.interlace.tools/docs/packages). ## Contributing Issues and pull requests are welcome at [ofri-peretz/burgee](https://github.com/ofri-peretz/burgee/issues); read [CONTRIBUTING.md](https://github.com/ofri-peretz/burgee/blob/main/CONTRIBUTING.md) first. Report a vulnerability privately, as [SECURITY.md](https://github.com/ofri-peretz/burgee/blob/main/SECURITY.md) describes — never in a public issue. ## Licence MIT © Ofri Peretz — see [LICENSE](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/LICENSE). --- # Build steps as a task list > Run a list of async steps under a task list that animates on a terminal, prints each step once as it settles in CI, and stops at the first failure. Source: https://flagstaff.interlace.tools/docs/recipes/build-steps A build script runs steps in order and should say which one failed. On a terminal the list is drawn with the running step animated; in CI each step is one line, written when it finishes. ```js title="steps.mjs" import { setTimeout as sleep } from 'node:timers/promises'; import { hoist } from 'flagstaff/loop'; import { tasks } from 'flagstaff/tasks'; import { json, rt } from './rt.mjs'; const steps = [ { title: 'install', run: () => sleep(100) }, { title: 'build', run: () => sleep(100) }, { title: 'test', run: () => Promise.reject(new Error('2 failing')) }, { title: 'publish', run: () => sleep(100) }, ]; const list = steps.map(({ title }) => ({ title, status: 'pending' })); const flag = hoist(tasks(), rt, { tasks: list }, { json }); for (const [i, step] of steps.entries()) { list[i] = { title: step.title, status: 'running' }; flag.update({ tasks: [...list] }); try { await step.run(); list[i] = { title: step.title, status: 'ok' }; } catch (error) { list[i] = { title: step.title, status: 'fail', detail: error.message }; process.exitCode = 1; break; } } flag.lower({ tasks: [...list] }); ``` ```text title="node steps.mjs" exit="1" ✔ install ✔ build ✖ test ``` `publish` never ran and never settled, so nothing is printed for it. Pass a new array to each `update()` rather than mutating the one you hoisted with: the loop compares text, and a list you changed in place is the list it already printed. The script sets exit code 1, as a failing build should. Under `--json` an agent gets every transition, pending and running included, with the error message in `detail`. --- # Moving off ora a file at a time > Change ora's import to flagstaff/ora today, keep its behaviour to the byte, and move each spinner onto hoist() when that file is next touched. Source: https://flagstaff.interlace.tools/docs/recipes/incremental-migration `flagstaff/ora` is ora 10's API, graded by ora's own test suite. Changing the import changes the dependency tree and nothing about the output: ```diff -import ora from 'ora'; +import ora from 'flagstaff/ora'; ``` ```js title="legacy.mjs" import { setTimeout as sleep } from 'node:timers/promises'; import ora from 'flagstaff/ora'; const spinner = ora('building').start(); await sleep(100); spinner.text = 'linking'; await sleep(100); spinner.succeed('built'); ``` In a pipe it does what ora does: a start line and a final line on stderr, and the `linking` in between is not written. ```text title="node legacy.mjs" - building ✔ built ``` When the file is next touched, move the spinner onto the loop. The calls map one to one — `start()` is `hoist()`, setting `text` is `update()`, `succeed()` and `fail()` are `lower()` with a status: ```js title="migrated.mjs" import { setTimeout as sleep } from 'node:timers/promises'; import { hoist } from 'flagstaff/loop'; import { spinner } from 'flagstaff/spinner'; import { json, rt } from './rt.mjs'; const flag = hoist(spinner(), rt, { text: 'building' }, { json }); await sleep(100); flag.update({ text: 'linking' }); await sleep(100); flag.lower({ text: 'built', status: 'ok' }); ``` ```text title="node migrated.mjs" … building … linking ✔ built ``` Now every state reaches the log, `--json` gives an agent one event per transition, and `CLI_ACCESSIBLE=1` stops the animation on a terminal too. The loop writes to stdout where ora writes to stderr, so check anything that captured the old stream. `npx burgee migrate --dry-run` lists the imports the codemod would rewrite to a drop-in, and `npx burgee migrate` makes the change ([Migrate](https://burgee.interlace.tools/docs/migrate)). --- # Progress in logs > Keep a progress bar readable in a CI log by updating it in steps — one line per tenth of the work instead of one per item. Source: https://flagstaff.interlace.tools/docs/recipes/progress-in-logs Off a terminal every state that changes the text is a line. `12/200 items · 6%` changes on every item, so a loop that updates on every item writes two hundred lines. The bar on a terminal does not need every item either: update when the work has moved by a step. ```js title="count.mjs" import { hoist } from 'flagstaff/loop'; import { progress } from 'flagstaff/progress'; import { json, rt } from './rt.mjs'; const items = Array.from({ length: 200 }, (_, i) => i); const total = items.length; const step = total / 10; const flag = hoist(progress(), rt, { done: 0, total, label: 'items' }, { json }); let reported = 0; for (const [i] of items.entries()) { // ... the work for one item ... const done = i + 1; if (done === total || done - reported >= step) { flag.update({ done, total, label: 'items' }); reported = done; } } flag.lower(); ``` ```text title="node count.mjs" 0/200 items · 0% 20/200 items · 10% 40/200 items · 20% 60/200 items · 30% 80/200 items · 40% 100/200 items · 50% 120/200 items · 60% 140/200 items · 70% 160/200 items · 80% 180/200 items · 90% 200/200 items · 100% ``` The final `update` is always sent, so the last line is the whole count. `lower()` without a state keeps the last one and, off a terminal, writes nothing more. --- # Results an agent can read > Print a results table that is a grid on a terminal, header-and-value lines in a log, and plain JSON rows under --json. Source: https://flagstaff.interlace.tools/docs/recipes/results-for-agents A coding agent that runs your CLI reads its output back. A grid of box characters costs it tokens and parsing; `--json` should hand it the data. One component does all three. ```js title="report.mjs" import { hoist } from 'flagstaff/loop'; import { tableComponent } from 'flagstaff/table'; import { json, rt } from './rt.mjs'; const results = [ { suite: 'api', result: 'passed', tests: 112 }, { suite: 'web', result: 'failed', tests: 3 }, ]; const rows = results.map((r) => [r.suite, r.result, String(r.tests)]); const report = tableComponent({ head: ['suite', 'result', 'tests'], align: ['left', 'left', 'right'] }); hoist(report, rt, { rows }, { json }).lower(); if (json) process.stdout.write(`${JSON.stringify({ ok: true, data: results })}\n`); ``` In a log, each row is a line a person can grep and an agent can read: ```text title="node report.mjs" suite: api, result: passed, tests: 112 suite: web, result: failed, tests: 3 ``` Under `--json` the table's events go to stderr and stdout carries only the program's own result, so `node report.mjs --json | jq .data` gets exactly the data: ```text title="node report.mjs --json" {"ok":true,"data":[{"suite":"api","result":"passed","tests":112},{"suite":"web","result":"failed","tests":3}]} {"event":"table","state":{"rows":[["api","passed","112"],["web","failed","3"]]}} {"event":"table","state":{"rows":[["api","passed","112"],["web","failed","3"]]}} ``` (The page shows stdout, then stderr.) A CLI built on [burgee](https://burgee.interlace.tools/docs) writes that `{ ok, data }` envelope for you. --- # Testing terminal output > Assert a spinner's terminal output byte for byte with manualClock and a literal runtime — no fake timers, no terminal. Source: https://flagstaff.interlace.tools/docs/recipes/testing-output The loop takes its clock and its streams from the runtime, so a test can hand it a clock that only moves when told and streams that collect what is written. The terminal transcript is then a fixed string. ```js title="spinner-output.mjs" import assert from 'node:assert/strict'; import { hoist, manualClock } from 'flagstaff/loop'; import { spinner } from 'flagstaff/spinner'; function world({ tty }) { const out = []; const clock = manualClock(); const rt = { env: {}, isTTY: { stdout: tty }, stdout: { write: (s) => out.push(s) }, stderr: { write: () => {} }, clock }; return { rt, clock, text: () => out.join('') }; } const terminal = world({ tty: true }); const flag = hoist(spinner(), terminal.rt, { text: 'building' }); terminal.clock.tick(170); flag.lower({ text: 'built', status: 'ok' }); // Each frame is one synchronized-output block; a repaint clears from column 1 first. const frame = (bytes) => `\u001B[?2026h${bytes}\u001B[?2026l`; const repaint = (text) => frame(`\u001B[1G\u001B[0J${text}`); assert.equal(terminal.text(), `\u001B[?25l${frame('⠋ building')}${repaint('⠙ building')}${repaint('⠹ building')}${repaint('✔ built')}\n\u001B[?25h`); const pipe = world({ tty: false }); hoist(spinner(), pipe.rt, { text: 'building' }).lower({ text: 'built', status: 'ok' }); assert.equal(pipe.text(), '… building\n✔ built\n'); console.log('ok'); ``` ```text title="node spinner-output.mjs" ok ``` `tick(170)` runs every frame due by 170 ms — the dots style repaints every 80 — and nothing else. flagstaff's own suite asserts the same transcript twenty runs over ([`loop.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/loop.test.ts)). Set `env: { CI: 'true' }` or `{ CLI_ACCESSIBLE: '1' }` to test the other modes, or pass `{ json: true }` to `hoist()` and read stderr. --- # 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@10.0.0/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@9.0.0/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@10.0.0/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@9.0.0/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@10.0.0/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@9.0.0/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@10.0.0/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@9.0.0/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@10.0.0/index.js) | [✗](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [✗](https://cdn.jsdelivr.net/npm/boxen@9.0.0/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@10.0.0/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@9.0.0/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@10.0.0/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@9.0.0/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@10.0.0/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@9.0.0/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@10.0.0/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@9.0.0/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@10.0.0/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@9.0.0/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@9.0.0/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@10.0.0/index.js) | [— draws no borders](https://cdn.jsdelivr.net/npm/log-update@8.0.0/index.js) | [✓](https://cdn.jsdelivr.net/npm/boxen@9.0.0/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@10.0.0/package.json) | [✗ six dependencies from other repositories](https://cdn.jsdelivr.net/npm/log-update@8.0.0/package.json) | [✗ seven dependencies from other repositories](https://cdn.jsdelivr.net/npm/boxen@9.0.0/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. | [✓ 223 / 223 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.test.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.test.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 9.0.0's own tests, which assert the exact characters the box comes out as. | [✓ 213 / 213 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.test.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.test.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.