flagstaff 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ofri Peretz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,25 +1,372 @@
1
- # flagstaff
2
-
3
- **Not yet released.** This version reserves the name; the first working release follows
4
- [`docs/intents/cli-render/`](https://github.com/ofri-peretz/burgee/tree/main/docs/intents/cli-render).
5
-
6
- A **flagstaff** is the staff a flag flies from. It is the simplest part of the whole
7
- apparatus and the only one that is always in view: a flag is hoisted on it, held there "at the
8
- dip" or "close up", changed, and lowered when it is done. That is a terminal render loop — the
9
- place frames are hoisted, held, changed and lowered, in view of whoever is reading.
10
-
11
- ## What it will be
12
-
13
- - **A frame loop with a static projection.** A spinner animates on a TTY, prints one line
14
- per state on a pipe, emits one event under `--json`, and is plain text in accessible mode.
15
- A component without a static projection is refused at registration.
16
- - **Plugins as data.** A third-party spinner, progress style or character is a plain object;
17
- the built-ins ship in the same shape. A plugin is a flag someone else made, flown from the
18
- same staff.
19
- - **Drop-in paths** for ora, log-update, boxen and cli-table3, graded by their own suites.
20
- - **No layout engine**, on purpose. Box, columns and a status line are the ceiling.
21
- - **Zero external dependencies.**
22
-
23
- Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on burgee declares
24
- what it is, pennon gives it colours, flagstaff flies it, answering is how it replies. Each is
25
- an independent package; none requires the others.
1
+ <p align="center">
2
+ <a href="https://github.com/ofri-peretz/burgee/tree/main/packages/flagstaff" target="blank">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/flagstaff-lockup.svg" />
5
+ <img src="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/flagstaff-lockup-light.svg" alt="flagstaff" width="360" />
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ <p align="center">
11
+ The staff the flag flies from — a terminal frame loop that an agent can read.
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/flagstaff"><img src="https://img.shields.io/npm/v/flagstaff?style=flat-square&color=0a6b47" alt="npm version" /></a>
16
+ <img src="https://img.shields.io/badge/dependencies-roundel-0a6b47?style=flat-square" alt="One dependency: roundel" />
17
+ <img src="https://img.shields.io/badge/Node.js-24+-green.svg?style=flat-square" alt="Node.js 24+" />
18
+ <img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
19
+ </p>
20
+
21
+ ora animates a spinner and, off a terminal, prints frames anyway — `\r` after `\r` into the
22
+ log an agent reads back. Ink fixes the terminal by shipping React and a layout engine.
23
+ **flagstaff** is the staff the flag flies from: a frame loop that hoists a component, holds
24
+ it, changes it and lowers it, and a **static projection** that is what every mode but the
25
+ terminal gets — one line per state on a pipe, one event per transition under `--json`,
26
+ plain text for a screen reader. Plugins are data. No layout engine. One dependency, and it
27
+ is [roundel](../roundel/README.md).
28
+
29
+ A **flagstaff** is the simplest part of the whole apparatus and the only one that is always
30
+ in view: a flag is hoisted on it, held there, changed, and lowered when it is done. That is a
31
+ terminal render loop — the place frames are hoisted, held, changed and lowered, in view of
32
+ whoever is reading.
33
+
34
+ ## Use
35
+
36
+ ```js
37
+ import { hoist } from 'flagstaff/loop';
38
+ import { spinner } from 'flagstaff/spinner';
39
+
40
+ const flag = hoist(spinner(), rt, { text: 'building' });
41
+ // ... work ...
42
+ flag.update({ text: 'linking' });
43
+ flag.lower({ text: 'built', status: 'ok' });
44
+ ```
45
+
46
+ `rt` is a runtime — `{ env, isTTY: { stdout }, stdout, stderr, clock }`; burgee's satisfies
47
+ it, so does a literal in a test. Which of the five modes runs is decided once by roundel's
48
+ output policy, never by this package:
49
+
50
+ | mode | what the same three calls write |
51
+ | :-- | :-- |
52
+ | `tty` | `⠋ building` repainted in place on the clock, then `✔ built` left on screen |
53
+ | `pipe`, `ci` | `… building` ⏎ `… linking` ⏎ `✔ built` — one line per state change, no `\r`, no escape |
54
+ | `json` | `{"event":"spinner","state":{"text":"building"}}` … one NDJSON event per transition, on stderr |
55
+ | `accessible` | the static text, never a redraw |
56
+
57
+ ## What is here
58
+
59
+ ### The loop
60
+
61
+ `hoist(component, rt, initial, { json })` returns `{ update(state), lower(state?), mode }`.
62
+ A component is `{ name, static(state), frame?(t, state), interval? }`. `static` is
63
+ required and is the artifact: what a pipe, an agent, a screen reader and the docs gallery
64
+ read. `frame` is optional and decorative. Nothing in the loop reads `process`; time comes
65
+ from `rt.clock`, so `manualClock()` makes a spinner's terminal output a fixed string a test
66
+ can assert byte for byte.
67
+
68
+ ### The plugin host
69
+
70
+ A plugin is one plain object, validated against [`schema.json`](./src/schema.json) — the
71
+ same file that ships in the tarball — and registered once:
72
+
73
+ ```js
74
+ // a third-party plugin, in full
75
+ export default {
76
+ name: 'nyan',
77
+ spinners: { nyan: { frames: ['≋', '≈', '~'], interval: 80, static: '…' } },
78
+ };
79
+ ```
80
+
81
+ ```js
82
+ import { register } from 'flagstaff/plugin';
83
+ register(nyan);
84
+ spinner('nyan');
85
+ ```
86
+
87
+ Keys: `spinners`, `borders`, `glyphs` (`ok`, `fail`, `warn`, `info`, `running` — change them
88
+ and every built-in that draws one changes), `tokens` (a roundel theme), `components`. A spinner
89
+ or component without a `static` is refused at `register()` with `E_NO_STATIC_PROJECTION`
90
+ and a fix. The built-in `dots` and `line` styles are a plugin of exactly this shape,
91
+ registered through the same door, so the built-ins cannot grow an API a plugin cannot reach.
92
+
93
+ `register()` is the **only** way in, and that is a property rather than a convention:
94
+ `registered()` hands back a copy — new maps over the frozen objects `register()` stored — so
95
+ `registered().spinners.set(…)` puts nothing in the registry and `.clear()` empties nothing.
96
+ A contribution that never met `validate()` cannot be reached by `spinner()`, `box()` or the
97
+ gallery, which is what makes "refused at the door" (U3) a fact about the code rather than
98
+ advice. Freezing also means the object you registered stays yours: edit it afterwards and
99
+ the registry does not change.
100
+
101
+ A component may declare `sample: { running, done }` — the two states `flagstaff check` and
102
+ the docs gallery *show* it with. The loop never reads it; a running program's state comes
103
+ from the program. Without one, both assume `{ phase: 'running' }` / `{ phase: 'done' }` and
104
+ say so in the output, rather than rendering an invented state as if it were yours.
105
+
106
+ ### The built-ins
107
+
108
+ Five components, each on its own subpath, each answering the static projection for itself:
109
+
110
+ ```js
111
+ import { progress } from 'flagstaff/progress';
112
+ import { tasks } from 'flagstaff/tasks';
113
+ import { box, boxComponent } from 'flagstaff/box';
114
+ import { table, tableComponent } from 'flagstaff/table';
115
+ ```
116
+
117
+ | component | on a terminal | everywhere else |
118
+ | :-- | :-- | :-- |
119
+ | `spinner` | `⠹ building` | `… building`, then `✔ built` |
120
+ | `progress` | a bar of blocks | `12/30 files · 40%` |
121
+ | `tasks` | every task, the running one animated | one line per task that has **settled** |
122
+ | `box` | the border, padding and title | `title: text` |
123
+ | `table` | the grid | one line per row of `header: value` pairs |
124
+
125
+ That table is the package's argument in one place. A bar of `█` in a log file tells an agent
126
+ nothing and tells a screen reader less; the count and the percentage tell both.
127
+
128
+ `box` and `table` are also plain string functions, because most callers want the string:
129
+
130
+ ```js
131
+ box('Ready on :3000', { title: 'dev', width: 40 });
132
+ table([['ora', '99'], ['log-update', '99']], { head: ['host', 'tests'] });
133
+ ```
134
+
135
+ No layout engine, and there will not be one: these measure with `width()`, wrap with
136
+ `wrap()`, and join strings.
137
+
138
+ ### The ora path
139
+
140
+ `flagstaff/ora` is ora 9's whole API, graded **99 / 99 by ora's own test suite** through
141
+ [`compat-oracle`](../compat-oracle/README.md). One import changes:
142
+
143
+ ```diff
144
+ -import ora from 'ora';
145
+ +import ora from 'flagstaff/ora';
146
+ ```
147
+
148
+ Everything else stays: `ora({ text, spinner, color, indent, prefixText, suffixText })`,
149
+ `.start() .stop() .succeed() .fail() .warn() .info() .stopAndPersist()`, `oraPromise()`,
150
+ the `spinners` corpus, the stream hooks that keep a `console.log` above the frame, the
151
+ synchronized-output sequences, the render deferral, the stdin discarder.
152
+
153
+ What changes is the bill. ora 9.4.1 ships 113,577 B of JavaScript across **seventeen
154
+ packages** — ora, chalk, cli-spinners, string-width, log-symbols, cli-cursor,
155
+ restore-cursor, onetime, mimic-function, signal-exit, is-interactive, is-unicode-supported,
156
+ stdin-discarder, yoctocolors, strip-ansi, ansi-regex, get-east-asian-width. `flagstaff/ora`
157
+ is 55,641 B across **two** — itself and roundel — **49% of ora's**, and 20,250 B of that is
158
+ the spinner corpus ora's API re-exports. Nothing in it reaches the frame loop, so a program
159
+ that migrates its spinner today can adopt `hoist()` a file at a time, or never.
160
+
161
+ Both sides are counted the same way, so the number reproduces: shipped code and data —
162
+ `.js`/`.mjs`/`.cjs` plus the `.json` a module imports, `package.json` never counted. Ours is
163
+ the import graph walked from `dist/` by `weight.test.ts`; ora's is every package that graph
164
+ touches in ora's own resolved tree, counted whole. Counting ora the stricter way — only the
165
+ 27 files its graph actually reaches — gives 101,809 B, and `flagstaff/ora` is still 55% of
166
+ that.
167
+
168
+ The cursor comes back the way ora's does. A spinner that hid the cursor restores it on a
169
+ clean exit **and** on `SIGINT`, `SIGTERM` and `SIGHUP` — node does not run `'exit'`
170
+ listeners for a signalled process, and Ctrl+C is how a spinner usually dies — then re-raises
171
+ the signal so the process still terminates, unless the program installed its own handler for
172
+ it. That is what ora buys with `restore-cursor` → `signal-exit`; here it is twenty lines and
173
+ no dependency. ora's own 99 never kill a process, so `ora.test.ts` grades it instead.
174
+
175
+ **The repo's accessible switch does not reach this surface.** `CLI_ACCESSIBLE=1` changes
176
+ what `roundel/policy` decides for every other entry point in the stack, but `flagstaff/ora`
177
+ imports `roundel/chalk` and never the policy, because a façade that reinterpreted its host
178
+ would fail the host's suite — so on a terminal `CLI_ACCESSIBLE=1` still animates and still
179
+ writes cursor escapes (measured: 4 escapes, cursor hidden), where `CI=true` disables the
180
+ spinner outright (0 escapes) because that is ora's own rule. Off a terminal it is moot. Use
181
+ `hoist()` when you want the switch to be honoured.
182
+
183
+ The static projection is the reason to move on eventually, not the reason to move:
184
+ `hoist()` is what gives a pipe one line per state instead of frames. `flagstaff/ora` is the
185
+ door, and it is deliberately ora's behaviour to the byte.
186
+
187
+ ### The boxen path
188
+
189
+ `flagstaff/boxen` is boxen 8's API, graded **84 / 84 by boxen's own test suite** — every one
190
+ of whose cases is a snapshot of the exact characters the box comes out as.
191
+
192
+ ```diff
193
+ -import boxen from 'boxen';
194
+ +import boxen from 'flagstaff/boxen';
195
+ ```
196
+
197
+ `borderStyle` (all eight of cli-boxes', a style object, or `none`), `borderColor`,
198
+ `backgroundColor`, `dimBorder`, `title` and `titleAlignment`, `textAlignment`, `padding`,
199
+ `margin`, `width`, `height`, `float`, `fullscreen`, and `_borderStyles`.
200
+
201
+ The drawing **is** the contract here, and matching it byte for byte is the compatibility
202
+ claim rather than a way of avoiding one: a user leaving boxen cares about one thing, whether
203
+ the box still looks the same. It carries cli-boxes' table itself rather than reading the
204
+ plugin registry — a façade whose drawing changed when somebody registered a plugin would be
205
+ reinterpreting its host. Named borders through the registry are `flagstaff/box`'s job.
206
+
207
+ ### The cli-table3 path
208
+
209
+ `flagstaff/cli-table3` is cli-table3 0.6.5's API, graded **29 / 29 by cli-table3's own test
210
+ suite** — the 38 of its cases that go through the public surface, less the nine that grade
211
+ `cli-table`, the *legacy* incumbent, and so pass whatever the target is. The other 197
212
+ `require('../src/…')` and test its four internal modules directly; those are reported beside
213
+ the number and never gate it, because passing them would mean copying cli-table3's file
214
+ layout rather than matching its behaviour.
215
+
216
+ ```diff
217
+ -const Table = require('cli-table3');
218
+ +import Table from 'flagstaff/cli-table3';
219
+ ```
220
+
221
+ `head`, `chars`, `style` (padding, `head`, `border`, `compact`), `colWidths`, `rowHeights`,
222
+ `colAligns`, `rowAligns`, `truncate`, `wordWrap`, `wrapOnWordBoundary`, per-cell `colSpan`,
223
+ `rowSpan`, `hAlign`, `vAlign`, `href`, and the `debug` channel with `table.messages` and
224
+ `Table.reset()`. It extends `Array`, because cli-table3 does and its callers push rows onto
225
+ it.
226
+
227
+ Four dependencies folded into one module rather than four: cli-table3's `table.js`,
228
+ `layout-manager.js`, `cell.js` and `utils.js` become one file, because the architecture is
229
+ not the contract — the drawing is.
230
+
231
+ ### The log-update path
232
+
233
+ `flagstaff/log-update` is log-update 8's API, graded **99 / 99 by log-update's own test
234
+ suite** — which renders every frame through a real terminal emulator and asserts the
235
+ screen, not the bytes.
236
+
237
+ ```diff
238
+ -import logUpdate from 'log-update';
239
+ +import logUpdate from 'flagstaff/log-update';
240
+ ```
241
+
242
+ `logUpdate()`, `.clear()`, `.done()`, `.persist()`, `createLogUpdate(stream, options)` and
243
+ `logUpdateStderr`, with the row-level diffing intact: a five-row frame whose last row is a
244
+ counter costs one row of output per tick, not five.
245
+
246
+ log-update ships 113.4 KB across **sixteen** packages. This is 29.6 KB across **none** —
247
+ the subpath reaches no package at all, not even roundel.
248
+ It carries no port of `slice-ansi` — the wrapper already makes every row self-contained,
249
+ so clipping a frame to the terminal's height is an array slice. `signal-exit`, 22.0 KB of
250
+ those sixteen, is 1.4 KB here: `cursor.ts`, shared with the ora façade because both
251
+ incumbents port the same `cli-cursor` → `restore-cursor` → `signal-exit` chain. Ctrl+C
252
+ mid-frame puts your cursor back, and still terminates — unless your program installed its
253
+ own `SIGINT` handler, in which case it is delivered once, to you, and this stays out of it.
254
+
255
+ **This is the one façade that lowers a layer guarantee, and it says so.** R5 — no cursor
256
+ escape off a terminal — cannot survive here: log-update's own suite asserts erase sequences
257
+ on a plain non-TTY stream, so a façade that suppressed them would fail the suite that is
258
+ the whole claim. What survives is the half the complaint behind R5 was actually about:
259
+ nothing this writes is ever a `\r`, **and never an absolute cursor-home** — every move is
260
+ a relative row move — on a terminal or off one, so a captured transcript stays parseable.
261
+ `log-update.test.ts` asserts both halves, and `hoist()` is what gives you the whole
262
+ guarantee.
263
+
264
+ ### Bringing a corpus with you
265
+
266
+ The ecosystem already has ~80 spinner styles and eight border sets, as plain JSON. Neither
267
+ is bundled here — the weight of a corpus nobody asked for is the thing this package exists
268
+ to avoid — so `flagstaff/import` turns the one you have into an ordinary plugin:
269
+
270
+ ```js
271
+ import cliSpinners from 'cli-spinners';
272
+ import { fromCliSpinners } from 'flagstaff/import';
273
+ import { register } from 'flagstaff/plugin';
274
+
275
+ register(fromCliSpinners(cliSpinners));
276
+ spinner('moon');
277
+ ```
278
+
279
+ `fromCliBoxes(cliBoxes)` does the same for borders, after which `box('…', { border:
280
+ 'arrow' })` draws with one. Both go through the same `register()` and the same schema, so
281
+ the gallery opens full and a third-party plugin starts as a copy of one of these. The
282
+ importer is 838 B and reaches nothing.
283
+
284
+ ### `flagstaff check`
285
+
286
+ ```bash
287
+ npx flagstaff check ./nyan.mjs
288
+ ```
289
+
290
+ Loads the file, validates it, and prints every contribution in all five modes side by side —
291
+ escapes made visible — so an author, or an agent that just wrote one, sees the static
292
+ projection next to the animation before anything ships. Exit 1 on a refusal, with the code
293
+ and the fix.
294
+
295
+ It opens with a census of what it found and closes with the verdict, so `ok` is never
296
+ printed before the rendering that would justify it:
297
+
298
+ ```text
299
+ nyan — 1 spinner, 0 borders, 0 components, 0 glyphs, 0 tokens
300
+ spinner nyan
301
+ tty ␛[?25l≋ working␛[1G␛[0J…
302
+ pipe ~nyan~ working⏎ ✔ done⏎
303
+ …
304
+ nyan: ok
305
+ ```
306
+
307
+ `0 spinners, 0 components` is how a misspelled key tells on itself. The schema is
308
+ `additionalProperties: true` on purpose — a key another package in the family reads belongs
309
+ in the same object — so a typo cannot be refused by the schema, and `check` is the surface
310
+ that has to notice:
311
+
312
+ ```text
313
+ typo — 0 spinners, 0 borders, 0 components, 0 glyphs, 0 tokens
314
+ unknown componets, spinner — flagstaff reads none of these; a key another package in the family reads is allowed here
315
+ E_NO_CONTRIBUTION: typo registers, but contributes nothing flagstaff can render
316
+ fix: flagstaff reads spinners, borders, components, glyphs and tokens; check those spellings. …
317
+ ```
318
+
319
+ Each component block names the state it was rendered with — the component's own `sample`
320
+ when it declares one, and otherwise the assumed `{ phase }` shape, said out loud. A `static`
321
+ that throws on the state it is handed is a refusal like any other, naming the modes it broke
322
+ in (`json` emits the state and never calls `static`, so it is usually the one that survives):
323
+
324
+ ```text
325
+ E_COMPONENT_THREW: g threw in tty, pipe, ci, accessible
326
+ fix: `static(state)` must return a string for the state it is rendered with; …
327
+ ```
328
+
329
+ ## Weight
330
+
331
+ Every subpath is a lock, not a convention, and the numbers below are asserted by
332
+ `weight.test.ts` against `dist/`, not estimated: `flagstaff/loop` reaches 4.4 KB on disk and
333
+ never the plugin registry; `flagstaff/plugin` 8.4 KB, of which 2.4 KB is the schema;
334
+ `flagstaff/spinner` 9.4 KB; `flagstaff/ora` 46.5 KB — 55.9 KB with roundel counted, against
335
+ ora's own 113.6 KB; `flagstaff/log-update` 29.6 KB, reaching **no package at all**, against
336
+ log-update's own 113.4 KB across sixteen; `flagstaff/boxen` 33.7 KB — 43.0 KB with roundel
337
+ counted, against boxen's own 132.4 KB across nineteen; `flagstaff/cli-table3` 32.9 KB —
338
+ 42.3 KB with roundel, against cli-table3's own 106.0 KB across seven. The three façades share `wrap.js` and
339
+ `width.js`, and the first two share `cursor.js`; none reaches another's port, and none
340
+ reaches the core. `sideEffects: false` lets a
341
+ bundler drop what a program does not use. ESM with a `default` condition, so
342
+ `require('flagstaff/spinner')` works from CommonJS on Node ≥ 24.
343
+
344
+ ## What is next
345
+
346
+ - **Drop-in paths** for boxen and cli-table3, graded by their own suites through
347
+ `compat-oracle` the way ora and log-update already are.
348
+
349
+ Every component, every registered plugin and every border is on the
350
+ [gallery](https://github.com/ofri-peretz/burgee/blob/main/apps/docs/content/docs/gallery.mdx),
351
+ which is generated by running them — the static projection beside the animation, in all
352
+ five modes.
353
+
354
+ ## Following along
355
+
356
+ The intent and design are committed before the code is, so you can read what it will be —
357
+ and argue with it — before it exists:
358
+
359
+ - [`.sdlc/intents/flagstaff/`](https://github.com/ofri-peretz/burgee/tree/main/.sdlc/intents/flagstaff)
360
+ — the intent and the design.
361
+ - [Open an issue](https://github.com/ofri-peretz/burgee/issues) if your CLI needs something
362
+ this list does not cover. That is the cheapest time to say so.
363
+
364
+ ---
365
+
366
+ Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on
367
+ [burgee](https://www.npmjs.com/package/burgee) declares what it is,
368
+ [roundel](https://www.npmjs.com/package/roundel) carries its colours, flagstaff flies it, and
369
+ [caique](https://www.npmjs.com/package/caique) answers back. Each is an independent package;
370
+ none requires the others.
371
+
372
+ MIT © Ofri Peretz — see [LICENSE](./LICENSE).
package/THIRD-PARTY.md ADDED
@@ -0,0 +1,44 @@
1
+ # Third-party material shipped in this package
2
+
3
+ flagstaff has one runtime dependency (`roundel`) and writes its own code. Two things in
4
+ the tarball are not ours, and are here with their licence.
5
+
6
+ ## `dist/spinners.json` — cli-spinners
7
+
8
+ The spinner corpus reached through `flagstaff/ora` is `spinners.json` from
9
+ [cli-spinners](https://github.com/sindresorhus/cli-spinners) 3.2.0, unedited. It is data,
10
+ not code: `flagstaff/ora` re-exports it as `spinners` because ora does, and ora's own test
11
+ suite reads it. Nothing else in the package loads it — the core (`flagstaff`,
12
+ `flagstaff/loop`, `flagstaff/plugin`, `flagstaff/spinner`) ships two spinners of its own
13
+ and a lock that fails if any entry reaches this file.
14
+
15
+ > MIT License
16
+ >
17
+ > Copyright (c) Sindre Sorhus \<sindresorhus@gmail.com\> (https://sindresorhus.com)
18
+ >
19
+ > Permission is hereby granted, free of charge, to any person obtaining a copy of this
20
+ > software and associated documentation files (the "Software"), to deal in the Software
21
+ > without restriction, including without limitation the rights to use, copy, modify,
22
+ > merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
23
+ > permit persons to whom the Software is furnished to do so, subject to the following
24
+ > conditions:
25
+ >
26
+ > The above copyright notice and this permission notice shall be included in all copies or
27
+ > substantial portions of the Software.
28
+ >
29
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
30
+ > INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
31
+ > PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
32
+ > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
33
+ > OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34
+ > OTHER DEALINGS IN THE SOFTWARE.
35
+
36
+ ## The East Asian Width ranges in `dist/width.js`
37
+
38
+ The `WIDE` table is derived from the Unicode Character Database's `EastAsianWidth.txt`
39
+ (Unicode 17), the same derivation `get-east-asian-width` publishes. The Unicode data files
40
+ are distributed under the [Unicode licence](https://www.unicode.org/license.txt), which
41
+ permits redistribution of derived data with the notice above kept.
42
+
43
+ Neither package is a dependency: nothing is installed, and both are here as data compiled
44
+ into the build.
package/dist/box.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { type BorderStyle, type Component } from './plugin.js';
2
+ export interface BoxOptions {
3
+ /** A registered border's name, or a style of your own. Default `round`. */
4
+ border?: string | BorderStyle;
5
+ /** Cells of padding left and right of the text, and rows above and below. Default 1 / 0. */
6
+ padding?: {
7
+ x?: number;
8
+ y?: number;
9
+ };
10
+ /** A title written into the top border. Truncated with `…` if the box is too narrow. */
11
+ title?: string;
12
+ /** Columns the whole box may occupy, borders included. Text wraps to fit. Default 80. */
13
+ width?: number;
14
+ }
15
+ /** Draw `text` in a box, as a string. The many callers who want only this want only this. */
16
+ export declare function box(text: string, options?: BoxOptions): string;
17
+ export interface BoxState {
18
+ text: string;
19
+ title?: string;
20
+ }
21
+ /** A box as a component: the text off a terminal, the drawing on one (R1). */
22
+ export declare function boxComponent(options?: BoxOptions): Component<BoxState>;
package/dist/box.js ADDED
@@ -0,0 +1,59 @@
1
+ import { width } from 'linegauge';
2
+ import { wrap } from 'linegauge/wrap';
3
+ import { muted } from 'roundel/tokens';
4
+ import { lookupBorder } from './plugin.js';
5
+ const DEFAULT_WIDTH = 80;
6
+ const DEFAULT_PAD_X = 1;
7
+ const DEFAULT_PAD_Y = 0;
8
+ const BORDER_CELLS = 2;
9
+ const ELLIPSIS = '…';
10
+ const resolve = (border) => (typeof border === 'string' ? lookupBorder(border) : border);
11
+ const padEnd = (line, cells) => line + ' '.repeat(Math.max(0, cells - width(line)));
12
+ function fitTitle(title, cells) {
13
+ if (width(title) <= cells)
14
+ return title;
15
+ if (cells <= 1)
16
+ return '';
17
+ let out = '';
18
+ for (const character of title) {
19
+ if (width(out + character) > cells - 1)
20
+ break;
21
+ out += character;
22
+ }
23
+ return out + ELLIPSIS;
24
+ }
25
+ function topBorder(style, inner, title) {
26
+ if (style.top === '')
27
+ return '';
28
+ if (title === undefined || title === '')
29
+ return style.topLeft + style.top.repeat(inner) + style.topRight;
30
+ const fitted = fitTitle(title, Math.max(0, inner - BORDER_CELLS - BORDER_CELLS));
31
+ if (fitted === '')
32
+ return style.topLeft + style.top.repeat(inner) + style.topRight;
33
+ const label = ` ${fitted} `;
34
+ const rest = Math.max(0, inner - width(label) - 1);
35
+ return `${style.topLeft}${style.top}${label}${style.top.repeat(rest)}${style.topRight}`;
36
+ }
37
+ export function box(text, options = {}) {
38
+ const style = resolve(options.border ?? 'round');
39
+ const padX = options.padding?.x ?? DEFAULT_PAD_X;
40
+ const padY = options.padding?.y ?? DEFAULT_PAD_Y;
41
+ const total = options.width ?? DEFAULT_WIDTH;
42
+ const borderCells = style.left === '' ? 0 : BORDER_CELLS;
43
+ const inner = Math.max(1, total - borderCells);
44
+ const content = Math.max(1, inner - padX * BORDER_CELLS);
45
+ const wrapped = wrap(text, content, { hard: true, trim: false }).split('\n');
46
+ const blank = Array.from({ length: padY }, () => '');
47
+ const pad = ' '.repeat(padX);
48
+ const rows = [...blank, ...wrapped, ...blank].map((line) => `${style.left}${pad}${padEnd(line, content)}${pad}${style.right}`);
49
+ const top = topBorder(style, inner, options.title);
50
+ const bottom = style.bottom === '' ? '' : style.bottomLeft + style.bottom.repeat(inner) + style.bottomRight;
51
+ return [top, ...rows, bottom].filter((row) => row !== '').join('\n');
52
+ }
53
+ export function boxComponent(options = {}) {
54
+ return {
55
+ name: 'box',
56
+ static: (state) => (state.title === undefined || state.title === '' ? state.text : `${state.title}: ${state.text}`),
57
+ frame: (_t, state) => muted(box(state.text, state.title === undefined ? options : { ...options, title: state.title })),
58
+ };
59
+ }
@@ -0,0 +1,56 @@
1
+ export interface BoxenBorderStyle {
2
+ topLeft: string;
3
+ top: string;
4
+ topRight: string;
5
+ left: string;
6
+ right: string;
7
+ bottomLeft: string;
8
+ bottom: string;
9
+ bottomRight: string;
10
+ /** Retro-compatibility: sets `left` and `right` together. */
11
+ vertical?: string;
12
+ /** Retro-compatibility: sets `top` and `bottom` together. */
13
+ horizontal?: string;
14
+ }
15
+ export interface Spacing {
16
+ top?: number;
17
+ right?: number;
18
+ bottom?: number;
19
+ left?: number;
20
+ }
21
+ export interface BoxenOptions {
22
+ borderStyle?: string | BoxenBorderStyle;
23
+ borderColor?: string;
24
+ backgroundColor?: string;
25
+ dimBorder?: boolean;
26
+ title?: string;
27
+ titleAlignment?: 'left' | 'center' | 'right';
28
+ textAlignment?: 'left' | 'center' | 'right';
29
+ /** @deprecated boxen's own name for `textAlignment`, still honoured. */
30
+ align?: 'left' | 'center' | 'right';
31
+ padding?: number | Spacing;
32
+ margin?: number | Spacing;
33
+ width?: number;
34
+ height?: number;
35
+ float?: 'left' | 'center' | 'right';
36
+ fullscreen?: boolean | ((columns: number, rows: number) => [number, number]);
37
+ }
38
+ /**
39
+ * cli-boxes 4, as data. boxen re-exports this as `_borderStyles`, so it is part of the
40
+ * surface a caller can reach and not an implementation detail to be hidden.
41
+ */
42
+ declare const BOXES: Record<string, BoxenBorderStyle>;
43
+ /**
44
+ * Draw a box around `text`, exactly as boxen 8 draws it.
45
+ *
46
+ * An invalid `borderColor` or `backgroundColor` throws rather than drawing something
47
+ * plausible, because boxen throws: a colour name that is not one is a typo, and a box drawn
48
+ * in the wrong colour is a bug somebody ships.
49
+ */
50
+ /** boxen re-exports cli-boxes under this name, so it is surface a caller can reach. */
51
+ export { BOXES as _borderStyles };
52
+ /**
53
+ * `import boxen from 'boxen'` is the incumbent's surface. A named export here would break
54
+ * every migration this file exists to serve, so the house rule yields to the host.
55
+ */
56
+ export default function boxen(text: string, options?: BoxenOptions): string;