@orkestrel/console 0.0.1

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.
@@ -0,0 +1,2183 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _orkestrel_emitter = require("@orkestrel/emitter");
3
+ let _orkestrel_contract = require("@orkestrel/contract");
4
+ //#region src/core/constants.ts
5
+ /**
6
+ * Each {@link Color}'s SGR FOREGROUND parameter — the 8 base colors at 30–37 and their
7
+ * bright variants at 90–97. `default` is intentionally absent (it emits no code).
8
+ */
9
+ var FOREGROUND_CODES = Object.freeze({
10
+ black: 30,
11
+ red: 31,
12
+ green: 32,
13
+ yellow: 33,
14
+ blue: 34,
15
+ magenta: 35,
16
+ cyan: 36,
17
+ white: 37,
18
+ brightBlack: 90,
19
+ brightRed: 91,
20
+ brightGreen: 92,
21
+ brightYellow: 93,
22
+ brightBlue: 94,
23
+ brightMagenta: 95,
24
+ brightCyan: 96,
25
+ brightWhite: 97
26
+ });
27
+ /**
28
+ * Each {@link Color}'s SGR BACKGROUND parameter — the 8 base colors at 40–47 and their
29
+ * bright variants at 100–107. `default` is intentionally absent (it emits no code).
30
+ */
31
+ var BACKGROUND_CODES = Object.freeze({
32
+ black: 40,
33
+ red: 41,
34
+ green: 42,
35
+ yellow: 43,
36
+ blue: 44,
37
+ magenta: 45,
38
+ cyan: 46,
39
+ white: 47,
40
+ brightBlack: 100,
41
+ brightRed: 101,
42
+ brightGreen: 102,
43
+ brightYellow: 103,
44
+ brightBlue: 104,
45
+ brightMagenta: 105,
46
+ brightCyan: 106,
47
+ brightWhite: 107
48
+ });
49
+ /**
50
+ * Each {@link Attribute}'s SGR "on" parameter — `bold` 1, `dim` 2, `italic` 3,
51
+ * `underline` 4, `inverse` 7, `strikethrough` 9. The renderer composes several by
52
+ * joining their codes with `;` in one SGR sequence.
53
+ */
54
+ var ATTRIBUTE_CODES = Object.freeze({
55
+ bold: 1,
56
+ dim: 2,
57
+ italic: 3,
58
+ underline: 4,
59
+ inverse: 7,
60
+ strikethrough: 9
61
+ });
62
+ /**
63
+ * The EMPTY {@link Style} — no foreground, no background, no attributes — frozen. The
64
+ * neutral starting point a base styler builds from, and what a renderer passes through
65
+ * unchanged (it carries no codes). Deeply frozen, so it is safe to share as the base.
66
+ */
67
+ var EMPTY_STYLE = Object.freeze({ attributes: Object.freeze([]) });
68
+ /**
69
+ * Every named {@link Color} except `default`, frozen — the colors the styler exposes as
70
+ * chainable accessors. The source of truth for the color axis; the styler drives its
71
+ * accessors from this array so the literals live in one place.
72
+ */
73
+ var COLORS = Object.freeze([
74
+ "black",
75
+ "red",
76
+ "green",
77
+ "yellow",
78
+ "blue",
79
+ "magenta",
80
+ "cyan",
81
+ "white",
82
+ "brightBlack",
83
+ "brightRed",
84
+ "brightGreen",
85
+ "brightYellow",
86
+ "brightBlue",
87
+ "brightMagenta",
88
+ "brightCyan",
89
+ "brightWhite"
90
+ ]);
91
+ /**
92
+ * Every {@link Attribute}, frozen — the attributes the styler exposes as chainable
93
+ * accessors. The source of truth for the attribute axis.
94
+ */
95
+ var ATTRIBUTES = Object.freeze([
96
+ "bold",
97
+ "dim",
98
+ "italic",
99
+ "underline",
100
+ "inverse",
101
+ "strikethrough"
102
+ ]);
103
+ /** The SGR RESET parameter (0) — terminates a styled run, clearing all colors and attributes. */
104
+ var RESET_CODE = 0;
105
+ /**
106
+ * The ESC control character (`U+001B`) that begins every ANSI escape sequence. Built
107
+ * with `String.fromCharCode` so no raw control character appears in source.
108
+ */
109
+ var ESC = String.fromCharCode(27);
110
+ /** The BEL control character (`U+0007`) that can terminate an OSC sequence. */
111
+ var BEL = String.fromCharCode(7);
112
+ /** The Control Sequence Introducer (`ESC[`) that opens every SGR sequence. */
113
+ var CSI = `[`;
114
+ /** The full SGR reset sequence (`ESC[0m`) appended after a styled run. */
115
+ var RESET = `${CSI}0m`;
116
+ /**
117
+ * Matches any ANSI/VT escape sequence — CSI (SGR color/style plus cursor/erase/scroll,
118
+ * including colon-parameterized SGR), OSC / DCS / PM / APC / SOS string sequences
119
+ * (titles, hyperlinks, device strings), the `nF` charset-select family, and the
120
+ * two-byte `Fp` / `Fe` / `Fs` sequences (e.g. `ESC 7`, `ESC D`, `ESC c` RIS). Global, so
121
+ * `strip` removes every occurrence.
122
+ *
123
+ * @remarks
124
+ * A global `RegExp` carries a mutable `lastIndex`; a scan must build a FRESH `RegExp`
125
+ * from this one's `source` + `flags` rather than reuse this instance's `lastIndex`. This
126
+ * is the canonical definition, not a shared scanner. The alternation is ORDERED so the
127
+ * CSI / string-family arms (which can start with a byte a later single-byte arm would
128
+ * also match) win first; every arm uses disjoint, non-nested character classes, so the
129
+ * match is linear in input length — no catastrophic backtracking (ReDoS-safe) even on an
130
+ * adversarial run of digits inside an unterminated CSI. Built from `String.fromCharCode`
131
+ * so no control-character literal appears in a regex source (the codebase idiom).
132
+ */
133
+ var ANSI_PATTERN = new RegExp(`(?:\\[[0-?]*[ -/]*[@-~]|\\][^]*(?:|\\\\)|[P^_X][^]*(?:|\\\\)|[ -/]+[0-~]|[0-?]|[@-OQ-WYZ\\\\]|[\`-~])`, "g");
134
+ /**
135
+ * Matches every C0 control character EXCEPT `\t` / `\n` / `\r` (which are meaningful
136
+ * whitespace), plus DEL (`0x7F`) — the non-printing bytes {@link
137
+ * import('./helpers.js').stripControls} removes. Global, ASCII-only source (no raw
138
+ * control-character literal), so a scan builds a fresh `RegExp` the same way as
139
+ * {@link ANSI_PATTERN} to avoid a mutated `lastIndex`.
140
+ *
141
+ * @remarks
142
+ * Deliberately SEPARATE from {@link ANSI_PATTERN}: `strip()` must stay pure ANSI-escape
143
+ * removal (width / alignment computations depend on it leaving raw C0 bytes alone), while
144
+ * C0-stripping is an ADDITIONAL, orthogonal pass a non-TTY output sink applies on top.
145
+ */
146
+ var CONTROL_PATTERN = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(8)}${String.fromCharCode(11)}${String.fromCharCode(12)}${String.fromCharCode(14)}-${String.fromCharCode(31)}${String.fromCharCode(127)}]`, "g");
147
+ /**
148
+ * Each {@link LogLevel}'s numeric SEVERITY — the ascending order the level gate compares
149
+ * through (`debug` 0 < `info` 1 < `warn` 2 < `error` 3). A record is kept when its level's
150
+ * severity is at or above the logger's threshold. The source of truth for level ordering.
151
+ */
152
+ var LEVEL_SEVERITY = Object.freeze({
153
+ debug: 0,
154
+ info: 1,
155
+ warn: 2,
156
+ error: 3
157
+ });
158
+ /**
159
+ * Each {@link LogLevel}'s default label {@link Color} — the level's VISUAL treatment, which
160
+ * is a styling choice ORTHOGONAL to the level itself (never a separate pseudo-level). The
161
+ * logger colors the level label through its styler with these; swapping a color never
162
+ * changes leveling. `debug` is cyan, `info` blue, `warn` yellow, `error` red.
163
+ *
164
+ * @remarks
165
+ * Excludes `default` so each value indexes a real styler accessor (the styler exposes a
166
+ * getter per non-`default` {@link Color}) — a level always renders in a concrete color.
167
+ */
168
+ var LEVEL_COLORS = Object.freeze({
169
+ debug: "cyan",
170
+ info: "blue",
171
+ warn: "yellow",
172
+ error: "red"
173
+ });
174
+ /**
175
+ * The default bounded-retention cap for a {@link import('./types.js').LoggerInterface} — at
176
+ * most this many recent records are kept (oldest dropped first). Retention is ALWAYS bounded
177
+ * (never the unbounded buffer scsr leaked); a consumer overrides it via `options.limit`.
178
+ */
179
+ var DEFAULT_LOG_LIMIT = 1e3;
180
+ /** The default {@link LogLevel} threshold a logger gates at when none is supplied — `info`. */
181
+ var DEFAULT_LOG_LEVEL = "info";
182
+ /**
183
+ * Every {@link LogLevel}, in ascending severity order — the levels a logger exposes as
184
+ * methods and the manager fans out to. The source of truth for the level axis (drives
185
+ * exhaustive tests); aligned with {@link LEVEL_SEVERITY}.
186
+ */
187
+ var LEVELS = Object.freeze([
188
+ "debug",
189
+ "info",
190
+ "warn",
191
+ "error"
192
+ ]);
193
+ /**
194
+ * The complete {@link BorderChars} junction set for each {@link BorderStyle} — the standard
195
+ * Unicode box-drawing glyphs at the four line weights. The renderers ({@link
196
+ * import('./helpers.js').renderBox} / {@link import('./helpers.js').renderTable}) look the
197
+ * style up here, so no glyph literal lives in a renderer. Deeply frozen.
198
+ *
199
+ * @remarks
200
+ * `round` shares `single`'s edges and tees — only its corners differ (the rounded `╭╮╰╯`).
201
+ */
202
+ var BORDER_CHARS = Object.freeze({
203
+ single: Object.freeze({
204
+ horizontal: "─",
205
+ vertical: "│",
206
+ topLeft: "┌",
207
+ topRight: "┐",
208
+ bottomLeft: "└",
209
+ bottomRight: "┘",
210
+ cross: "┼",
211
+ teeDown: "┬",
212
+ teeUp: "┴",
213
+ teeRight: "├",
214
+ teeLeft: "┤"
215
+ }),
216
+ double: Object.freeze({
217
+ horizontal: "═",
218
+ vertical: "║",
219
+ topLeft: "╔",
220
+ topRight: "╗",
221
+ bottomLeft: "╚",
222
+ bottomRight: "╝",
223
+ cross: "╬",
224
+ teeDown: "╦",
225
+ teeUp: "╩",
226
+ teeRight: "╠",
227
+ teeLeft: "╣"
228
+ }),
229
+ round: Object.freeze({
230
+ horizontal: "─",
231
+ vertical: "│",
232
+ topLeft: "╭",
233
+ topRight: "╮",
234
+ bottomLeft: "╰",
235
+ bottomRight: "╯",
236
+ cross: "┼",
237
+ teeDown: "┬",
238
+ teeUp: "┴",
239
+ teeRight: "├",
240
+ teeLeft: "┤"
241
+ }),
242
+ heavy: Object.freeze({
243
+ horizontal: "━",
244
+ vertical: "┃",
245
+ topLeft: "┏",
246
+ topRight: "┓",
247
+ bottomLeft: "┗",
248
+ bottomRight: "┛",
249
+ cross: "╋",
250
+ teeDown: "┳",
251
+ teeUp: "┻",
252
+ teeRight: "┣",
253
+ teeLeft: "┫"
254
+ })
255
+ });
256
+ /**
257
+ * Each {@link StatusLevel}'s icon glyph — the leading mark a {@link
258
+ * import('./types.js').ReporterInterface.status} outcome line shows: `success` ✔, `error` ✖,
259
+ * `warn` ⚠, `info` ℹ. The narrative-outcome counterpart to a log level's label; frozen.
260
+ */
261
+ var STATUS_ICONS = Object.freeze({
262
+ success: "✔",
263
+ error: "✖",
264
+ warn: "⚠",
265
+ info: "ℹ"
266
+ });
267
+ /**
268
+ * Each {@link StatusLevel}'s {@link Color} — the icon + message color a `status` line renders
269
+ * in (`success` green, `error` red, `warn` yellow, `info` blue). The VISUAL treatment of a
270
+ * narrative outcome, colored through the reporter's styler; orthogonal to leveling, like
271
+ * {@link LEVEL_COLORS}. Excludes `default` so each value indexes a real styler accessor.
272
+ */
273
+ var STATUS_COLORS = Object.freeze({
274
+ success: "green",
275
+ error: "red",
276
+ warn: "yellow",
277
+ info: "blue"
278
+ });
279
+ /**
280
+ * Every {@link StatusLevel}, frozen — the outcomes a `status` line supports (drives exhaustive
281
+ * tests). The source of truth for the status axis; aligned with {@link STATUS_ICONS} /
282
+ * {@link STATUS_COLORS}.
283
+ */
284
+ var STATUS_LEVELS = Object.freeze([
285
+ "success",
286
+ "error",
287
+ "warn",
288
+ "info"
289
+ ]);
290
+ /**
291
+ * The tree connectors {@link import('./helpers.js').renderTree} draws — the `├─` branch (a
292
+ * non-last child), the `└─` corner (the last child), the `│ ` guide (carried down through an
293
+ * earlier branch's descendants), and the ` ` gap (under a last branch). Frozen.
294
+ */
295
+ var TREE_CHARS = Object.freeze({
296
+ branch: "├─ ",
297
+ corner: "└─ ",
298
+ guide: "│ ",
299
+ gap: " "
300
+ });
301
+ /**
302
+ * The default visible column width for the width-aware renderers — the separator rule and a
303
+ * {@link import('./helpers.js').renderBox} with no explicit `width`, and the reporter's
304
+ * `section` rule. A sane terminal default (80 columns); a caller overrides it per-call or via
305
+ * {@link import('./types.js').ReporterOptions}`.width`.
306
+ */
307
+ var DEFAULT_WIDTH = 80;
308
+ /** The default horizontal padding inside a box's edges ({@link import('./helpers.js').renderBox}) — one cell. */
309
+ var DEFAULT_PADDING = 1;
310
+ /** The default {@link BorderStyle} the box / table renderers frame with when none is given — `single`. */
311
+ var DEFAULT_BORDER = "single";
312
+ /** The default cell {@link Alignment} a {@link import('./types.js').ColumnSpec} uses when none is given — `left`. */
313
+ var DEFAULT_ALIGN = "left";
314
+ /** The default fill character {@link import('./helpers.js').renderSeparator} draws its rule with — `─`. */
315
+ var SEPARATOR_FILL = "─";
316
+ /**
317
+ * The single padding cell on each side of a separator's embedded title (` title `) — keeps the
318
+ * title from butting against the rule. One space.
319
+ */
320
+ var SEPARATOR_TITLE_GAP = " ";
321
+ /**
322
+ * The number of milliseconds at or above which {@link import('./helpers.js').formatDuration}
323
+ * (and so `Reporter.timing`) switches from a `…ms` rendering to a `…s` (seconds, 2 d.p.)
324
+ * rendering — exactly one second.
325
+ */
326
+ var SECOND_MS = 1e3;
327
+ /**
328
+ * Every {@link CaptureLevel}, frozen — the `console.*` methods a {@link
329
+ * import('./types.js').CaptureInterface} intercepts by default (and the source of truth for the
330
+ * capture-level axis; drives exhaustive tests). The universal console methods: `log`, `info`,
331
+ * `warn`, `error`, `debug`.
332
+ */
333
+ var CAPTURE_LEVELS = Object.freeze([
334
+ "log",
335
+ "info",
336
+ "warn",
337
+ "error",
338
+ "debug"
339
+ ]);
340
+ /**
341
+ * The default set of {@link CaptureLevel}s a Capture patches when `options.levels` is omitted —
342
+ * all five universal `console.*` methods ({@link CAPTURE_LEVELS}). A consumer narrows it (e.g. just
343
+ * `['warn', 'error']`) via `options.levels`.
344
+ */
345
+ var DEFAULT_CAPTURE_LEVELS = CAPTURE_LEVELS;
346
+ /**
347
+ * The default bounded-buffer cap for a {@link import('./types.js').CaptureInterface} — at most this
348
+ * many recent {@link CapturedMessage}s are retained per buffer (the total buffer AND each by-level
349
+ * bucket; oldest dropped first). Capture retention is ALWAYS bounded so a long-running capture can
350
+ * never grow without bound (the same retention precedent as {@link DEFAULT_LOG_LIMIT}); a consumer
351
+ * overrides it via `options.limit`.
352
+ */
353
+ var DEFAULT_CAPTURE_LIMIT = 1e3;
354
+ /**
355
+ * Each {@link CaptureLevel}'s {@link LogLevel} for the optional sink forward — the projection the
356
+ * Capture routes through when writing an intercepted call to a {@link
357
+ * import('./types.js').SinkInterface} (`sink.write(text, CAPTURE_LEVEL_MAP[level])`). `warn` /
358
+ * `error` / `debug` / `info` map to their matching {@link LogLevel}; `log` maps to `info` (a plain
359
+ * console log is informational — the default stream), so a stream-aware sink routes `warn` / `error`
360
+ * captures to the right stream. The source of truth for the capture-to-log projection.
361
+ */
362
+ var CAPTURE_LEVEL_MAP = Object.freeze({
363
+ log: "info",
364
+ info: "info",
365
+ warn: "warn",
366
+ error: "error",
367
+ debug: "debug"
368
+ });
369
+ /**
370
+ * The default spinner frame cycle a {@link import('./types.js').SpinnerInterface} advances through —
371
+ * the ten braille-pattern glyphs (U+2800 block) that read as a smoothly rotating dot, the universal
372
+ * terminal-spinner convention. Frozen; a consumer swaps the whole cycle via `options.frames`.
373
+ *
374
+ * @remarks
375
+ * Braille glyphs are single visible cells, so every frame occupies one column — the spinner glyph
376
+ * never shifts the message beside it as it advances. The source of truth for the default frame axis.
377
+ */
378
+ var SPINNER_FRAMES = Object.freeze([
379
+ "⠋",
380
+ "⠙",
381
+ "⠹",
382
+ "⠸",
383
+ "⠼",
384
+ "⠴",
385
+ "⠦",
386
+ "⠧",
387
+ "⠇",
388
+ "⠏"
389
+ ]);
390
+ /**
391
+ * The default timer period in milliseconds between a {@link import('./types.js').SpinnerInterface}'s
392
+ * frames — the `setInterval` interval `start()` arms. Eighty milliseconds (≈12.5 frames/second) is
393
+ * the conventional spinner cadence: fast enough to read as motion, slow enough not to thrash a
394
+ * terminal. A consumer overrides it via `options.interval`.
395
+ */
396
+ var DEFAULT_SPINNER_INTERVAL = 80;
397
+ /**
398
+ * The default FILLED-cell glyph {@link import('./helpers.js').renderBar} draws the completed run of a
399
+ * progress bar with — the full block `█` (U+2588). A single visible cell; a consumer overrides it via
400
+ * {@link import('./types.js').ProgressBarOptions}`.fill`.
401
+ */
402
+ var BAR_FILL = "█";
403
+ /**
404
+ * The default EMPTY-cell glyph {@link import('./helpers.js').renderBar} draws the remaining run of a
405
+ * progress bar with — the light-shade block `░` (U+2591). A single visible cell; a consumer overrides
406
+ * it via {@link import('./types.js').ProgressBarOptions}`.empty`.
407
+ */
408
+ var BAR_EMPTY = "░";
409
+ /**
410
+ * The default visible cell count of a progress-bar TRACK — the glyph run {@link
411
+ * import('./helpers.js').renderBar} fills (and a {@link import('./types.js').ProgressInterface} sizes
412
+ * its bar to). Thirty cells is a compact, terminal-friendly default; a consumer overrides it via
413
+ * `options.width`. Distinct from {@link DEFAULT_WIDTH} (the renderers' 80-column line width) — a bar
414
+ * track is one inline element, not a full-width rule.
415
+ */
416
+ var DEFAULT_BAR_WIDTH = 30;
417
+ //#endregion
418
+ //#region src/core/errors.ts
419
+ /**
420
+ * An error thrown by the console layer.
421
+ *
422
+ * @remarks
423
+ * Carries a {@link ConsoleErrorCode} and an optional `context` bag. Thrown for: an
424
+ * internal invariant violated at a defensive, structurally-unreachable guard
425
+ * (`INVARIANT`) — the one throw site in this codebase today.
426
+ */
427
+ var ConsoleError = class extends Error {
428
+ code;
429
+ context;
430
+ constructor(code, message, context) {
431
+ super(message);
432
+ this.name = "ConsoleError";
433
+ this.code = code;
434
+ this.context = context;
435
+ }
436
+ };
437
+ /**
438
+ * Narrow an unknown caught value to a {@link ConsoleError}.
439
+ *
440
+ * @param value - The value to test (typically a `catch` binding)
441
+ * @returns `true` when `value` is a {@link ConsoleError}
442
+ *
443
+ * @example
444
+ * ```ts
445
+ * try {
446
+ * createStyler().style
447
+ * } catch (error) {
448
+ * if (isConsoleError(error) && error.code === 'INVARIANT') report(error)
449
+ * }
450
+ * ```
451
+ */
452
+ function isConsoleError(value) {
453
+ return value instanceof ConsoleError;
454
+ }
455
+ //#endregion
456
+ //#region src/core/helpers.ts
457
+ /**
458
+ * Remove every ANSI escape sequence from `text`, returning the plain visible string.
459
+ *
460
+ * @remarks
461
+ * Strips SGR color/style codes AND other CSI controls (cursor, erase) plus OSC
462
+ * sequences (titles, hyperlinks) — see {@link ANSI_PATTERN}. A FRESH `RegExp` is built
463
+ * per call from the canonical pattern's `source` + `flags`, so the shared global
464
+ * pattern's `lastIndex` is never mutated across calls (re-entrant and deterministic).
465
+ *
466
+ * @param text - Any string, styled or plain
467
+ * @returns `text` with all ANSI escapes removed
468
+ *
469
+ * @example
470
+ * ```ts
471
+ * strip('\x1b[31mred\x1b[0m') // 'red'
472
+ * ```
473
+ */
474
+ function strip(text) {
475
+ return text.replace(new RegExp(ANSI_PATTERN.source, ANSI_PATTERN.flags), "");
476
+ }
477
+ /**
478
+ * Remove every non-printing C0 control character from `text` EXCEPT `\t` / `\n` / `\r`
479
+ * (meaningful whitespace), plus DEL — returning the sanitized string.
480
+ *
481
+ * @remarks
482
+ * Deliberately SEPARATE from {@link strip} (ANSI-escape removal only, so `width` /
483
+ * `align` stay untouched) — this is the additional pass a non-TTY output sink applies
484
+ * on top of `strip`, so a captured `\x07` bell or stray `\x00` never reaches a log file
485
+ * / non-terminal target. A FRESH `RegExp` is built per call from {@link CONTROL_PATTERN}'s
486
+ * `source` + `flags`, the same re-entrant idiom as `strip`.
487
+ *
488
+ * @param text - Any string, possibly carrying raw control bytes
489
+ * @returns `text` with C0 controls (other than tab/newline/CR) and DEL removed
490
+ *
491
+ * @example
492
+ * ```ts
493
+ * stripControls('a\x07b\nc') // 'ab\nc'
494
+ * ```
495
+ */
496
+ function stripControls(text) {
497
+ return text.replace(new RegExp(CONTROL_PATTERN.source, CONTROL_PATTERN.flags), "");
498
+ }
499
+ /**
500
+ * The visible width of `text` — its length after ANSI escapes are stripped, counted in
501
+ * Unicode code points (so an astral character such as an emoji counts as one, not the
502
+ * two UTF-16 units `String.length` would report).
503
+ *
504
+ * @remarks
505
+ * The basis for terminal layout (box / table / progress alignment): the column count a
506
+ * styled string occupies, independent of its escape codes. It does NOT account for
507
+ * wide (CJK / fullwidth) glyphs occupying two cells — a deliberate, documented
508
+ * simplification at this layer; callers needing east-asian width handle it above.
509
+ *
510
+ * @param text - Any string, styled or plain
511
+ * @returns The count of visible code points
512
+ *
513
+ * @example
514
+ * ```ts
515
+ * width('\x1b[1mhi\x1b[0m') // 2
516
+ * ```
517
+ */
518
+ function width(text) {
519
+ return [...strip(text)].length;
520
+ }
521
+ /**
522
+ * Whether a record at `level` passes a logger gated at `threshold` — i.e. its severity is
523
+ * at or above the threshold's.
524
+ *
525
+ * @remarks
526
+ * The level gate (AGENTS §5 — the comparison lives here, not inlined in the logger). Reads
527
+ * the ascending {@link LEVEL_SEVERITY} order: `meetsLevel('warn', 'error')` is `true`
528
+ * (error ≥ warn), `meetsLevel('warn', 'info')` is `false` (info < warn).
529
+ *
530
+ * @param threshold - The logger's configured minimum {@link LogLevel}
531
+ * @param level - The record's {@link LogLevel}
532
+ * @returns `true` when `level` is at least as severe as `threshold`
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * meetsLevel('info', 'error') // true
537
+ * meetsLevel('error', 'warn') // false
538
+ * ```
539
+ */
540
+ function meetsLevel(threshold, level) {
541
+ return LEVEL_SEVERITY[level] >= LEVEL_SEVERITY[threshold];
542
+ }
543
+ /**
544
+ * Format a {@link LogRecord}'s `time` (epoch milliseconds) as an ISO-8601 timestamp string.
545
+ *
546
+ * @remarks
547
+ * Deterministic and serializable — `new Date(time).toISOString()`, e.g.
548
+ * `1716900000000 → '2024-05-28T12:40:00.000Z'`. The timestamp portion of the formatted log
549
+ * line; kept a pure helper so the line layout and the logger stay decoupled.
550
+ *
551
+ * @param time - Epoch milliseconds (a record's `time`)
552
+ * @returns The ISO-8601 timestamp
553
+ */
554
+ function formatTime(time) {
555
+ return new Date(time).toISOString();
556
+ }
557
+ /**
558
+ * Format a {@link LogRecord} into a single styled line — the default human line layout a
559
+ * {@link import('./types.js').LoggerInterface} writes to its sink.
560
+ *
561
+ * @remarks
562
+ * Layout: `{time} {LEVEL} {[name]} {message}{ data}` — the ISO timestamp (dimmed), the
563
+ * upper-cased level label (colored by {@link LEVEL_COLORS} — styling ORTHOGONAL to level),
564
+ * the originating logger's `name` in brackets (omitted when absent), the message, and the
565
+ * structured `data` appended as compact JSON (omitted when absent / empty). Coloring flows
566
+ * through the injected `styler`, so a disabled styler yields a plain line and a browser
567
+ * `%c` styler (C-f) retargets it — the layout never changes. Pure: same record + styler →
568
+ * same line.
569
+ *
570
+ * @param record - The {@link LogRecord} to render
571
+ * @param styler - The {@link StylerInterface} the labels are colored through
572
+ * @returns The formatted, styled line (no trailing newline — the sink's target adds it)
573
+ *
574
+ * @example
575
+ * ```ts
576
+ * formatRecord({ level: 'warn', message: 'low disk', time: 0, name: 'fs' }, createStyler())
577
+ * // '<dim>1970-01-01T00:00:00.000Z</> <yellow>WARN</> [fs] low disk'
578
+ * ```
579
+ */
580
+ function formatRecord(record, styler) {
581
+ const time = styler.dim(formatTime(record.time));
582
+ const label = styler[LEVEL_COLORS[record.level]](record.level.toUpperCase());
583
+ const name = record.name === void 0 ? "" : ` ${styler.dim(`[${record.name}]`)}`;
584
+ const data = record.data === void 0 || Object.keys(record.data).length === 0 ? "" : ` ${styler.dim(JSON.stringify(record.data))}`;
585
+ return `${time} ${label}${name} ${record.message}${data}`;
586
+ }
587
+ /**
588
+ * Pad (or, when over budget, truncate) `text` to exactly `target` VISIBLE columns, positioning
589
+ * it by `alignment`. The width primitive the box / table renderers align every cell with.
590
+ *
591
+ * @remarks
592
+ * Measures with {@link width} (visible code points, ANSI-aware), so a styled string aligns by
593
+ * its visible content, not its escape codes. When `width(text) < target`, the deficit is added
594
+ * as spaces — all trailing (`left`), all leading (`right`), or split with the extra space on
595
+ * the right (`center`). When `width(text) > target`, the VISIBLE characters are sliced to
596
+ * `target` (a defensive guard — the renderers size columns to fit, so this is rarely hit; it
597
+ * slices the stripped text, so it never bisects an escape sequence into a broken half).
598
+ *
599
+ * @param text - The cell content (may be styled)
600
+ * @param target - The visible column count to fit `text` into
601
+ * @param alignment - Where to position `text` within the width; defaults to `left`
602
+ * @returns `text` fitted to exactly `target` visible columns
603
+ *
604
+ * @example
605
+ * ```ts
606
+ * align('hi', 5) // 'hi '
607
+ * align('hi', 5, 'right') // ' hi'
608
+ * align('hi', 5, 'center') // ' hi '
609
+ * ```
610
+ */
611
+ function align(text, target, alignment = DEFAULT_ALIGN) {
612
+ const visible = width(text);
613
+ if (visible > target) return [...strip(text)].slice(0, target).join("");
614
+ const deficit = target - visible;
615
+ if (alignment === "right") return `${" ".repeat(deficit)}${text}`;
616
+ if (alignment === "center") {
617
+ const left = Math.floor(deficit / 2);
618
+ return `${" ".repeat(left)}${text}${" ".repeat(deficit - left)}`;
619
+ }
620
+ return `${text}${" ".repeat(deficit)}`;
621
+ }
622
+ /**
623
+ * Format a millisecond duration as a compact human string — `…ms` below one second, `…s`
624
+ * (seconds to 2 decimal places) at or above one second. The timing rendering behind
625
+ * {@link import('./types.js').ReporterInterface.timing}.
626
+ *
627
+ * @remarks
628
+ * `999 → '999ms'`, `1000 → '1.00s'`, `1230 → '1.23s'` (the threshold is {@link SECOND_MS}).
629
+ * Pure and deterministic; kept a shared helper so the layout and the reporter stay decoupled.
630
+ *
631
+ * @param ms - The duration in milliseconds
632
+ * @returns The formatted duration (`'<n>ms'` or `'<n>s'`)
633
+ */
634
+ function formatDuration(ms) {
635
+ return ms < 1e3 ? `${ms}ms` : `${(ms / SECOND_MS).toFixed(2)}s`;
636
+ }
637
+ /**
638
+ * Color `text` through `styler`, or return it verbatim when `styler` is `undefined` — the
639
+ * single optional-styling primitive every renderer applies to its border / title / connector
640
+ * glyphs (AGENTS §5 — the ONE styler seam, shared, never re-hand-rolled per renderer).
641
+ *
642
+ * @remarks
643
+ * The renderers all take an OPTIONAL `styler`: present ⇒ glyphs are colored, absent ⇒ plain.
644
+ * Folding that `styler === undefined ? text : styler(text)` ternary into one exported helper
645
+ * keeps the renderers terse and the styling decision in one tested place. A disabled styler
646
+ * (`enabled: false`) is still a styler — it returns its text verbatim — so passing one paints
647
+ * a no-op, exactly as omitting it does.
648
+ *
649
+ * @param styler - The {@link StylerInterface} to color with, or `undefined` for no styling
650
+ * @param text - The glyphs / text to color
651
+ * @returns `styler(text)` when a styler is given, else `text` unchanged
652
+ */
653
+ function paint(styler, text) {
654
+ return styler === void 0 ? text : styler(text);
655
+ }
656
+ /**
657
+ * Repeat `unit` until it fills exactly `count` VISIBLE columns, trimming a trailing partial
658
+ * unit so the run is never over-wide — the fill primitive the separator + box edges draw with.
659
+ *
660
+ * @remarks
661
+ * Counts in code points ({@link width}-consistent), so a multi-cell or astral `unit` is laid
662
+ * down whole and the result is sliced to exactly `count` visible columns. `count <= 0` (or an
663
+ * empty / zero-width `unit`) yields `''`.
664
+ *
665
+ * @param unit - The (possibly multi-character) fill unit
666
+ * @param count - The visible column count to fill
667
+ * @returns `unit` tiled to exactly `count` visible columns
668
+ *
669
+ * @example
670
+ * ```ts
671
+ * repeatTo('─', 4) // '────'
672
+ * repeatTo('=-', 5) // '=-=-='
673
+ * ```
674
+ */
675
+ function repeatTo(unit, count) {
676
+ if (count <= 0) return "";
677
+ const per = width(unit);
678
+ if (per === 0) return "";
679
+ return [...unit.repeat(Math.ceil(count / per))].slice(0, count).join("");
680
+ }
681
+ /**
682
+ * The cell at `index` of a (possibly ragged) row — `''` when the row is shorter than the
683
+ * column count, so a short row pads out instead of throwing (the ragged-row guard
684
+ * {@link renderTable} reads every cell through).
685
+ *
686
+ * @param row - The row's cells
687
+ * @param index - The column index to read
688
+ * @returns The cell text, or `''` when the row has no cell at `index`
689
+ */
690
+ function cellAt(row, index) {
691
+ return row[index] ?? "";
692
+ }
693
+ /**
694
+ * Render a horizontal rule — an optional centered title embedded in a line of fill characters,
695
+ * to a fixed visible width. Pure: same {@link SeparatorOptions} → same string.
696
+ *
697
+ * @remarks
698
+ * - **Plain rule.** With no `title`, returns `fill` repeated to `width` visible columns.
699
+ * - **Titled rule.** With a `title`, centers ` title ` (one {@link SEPARATOR_TITLE_GAP} each
700
+ * side) in the line, splitting the remaining fill between the two sides (the extra column,
701
+ * when the remainder is odd, goes to the right). The visible width stays exactly `width`,
702
+ * even when the title is styled (the title's escape codes don't count toward the budget) —
703
+ * a title at least as wide as `width` yields just the gapped title (no fill).
704
+ * - **Styling.** When `options.styler` is given, the fill runs (and the embedded title) are
705
+ * colored through it; the layout is identical with or without color, since width is measured
706
+ * on the visible content (AGENTS — width-aware via {@link width}).
707
+ *
708
+ * @param options - See {@link SeparatorOptions}
709
+ * @returns The rule line (no trailing newline)
710
+ *
711
+ * @example
712
+ * ```ts
713
+ * renderSeparator({ width: 10 }) // '──────────'
714
+ * renderSeparator({ title: 'Build', width: 13 }) // '── Build ──' (centered)
715
+ * ```
716
+ */
717
+ function renderSeparator(options) {
718
+ const total = options.width ?? 80;
719
+ const fill = options.fill ?? "─";
720
+ if (options.title === void 0) return paint(options.styler, repeatTo(fill, total));
721
+ const gapped = ` ${paint(options.styler, options.title)} `;
722
+ const room = total - width(options.title) - 2;
723
+ if (room <= 0) return gapped;
724
+ const left = Math.floor(room / 2);
725
+ return `${paint(options.styler, repeatTo(fill, left))}${gapped}${paint(options.styler, repeatTo(fill, room - left))}`;
726
+ }
727
+ /**
728
+ * Render `content` framed in box-drawing characters, optionally captioned, width-aware so
729
+ * styled content stays aligned inside the frame. Pure: same {@link BoxOptions} → same string.
730
+ *
731
+ * @remarks
732
+ * - **Lines.** `content` is split on `\n`; each line is padded (left-aligned) to the inner
733
+ * width by {@link align} — measured on VISIBLE width, so a styled line never breaks the
734
+ * right edge. The inner width is the widest line's visible width (or `width − borders −
735
+ * 2·padding` when an explicit `width` is given and is wider), plus `padding` blank cells
736
+ * inside each {@link BorderChars.vertical} edge.
737
+ * - **Title.** An optional `title` is embedded in the TOP border (` title `), the remaining
738
+ * top edge drawn as fill; a title wider than the inner width widens the box to fit it.
739
+ * - **Border + styling.** The {@link BorderStyle} (`options.border`, default
740
+ * {@link DEFAULT_BORDER}) selects the glyph set from {@link BORDER_CHARS}; `options.styler`
741
+ * colors the frame + title when given (content cells are written as supplied).
742
+ * - **Multi-line result.** Returns the box as `\n`-joined rows (top, one row per content line,
743
+ * bottom) with no trailing newline.
744
+ *
745
+ * @param options - See {@link BoxOptions}
746
+ * @returns The framed box (multiple lines joined by `\n`)
747
+ */
748
+ function renderBox(options) {
749
+ const chars = BORDER_CHARS[options.border ?? "single"];
750
+ const padding = Math.max(0, Math.trunc(options.padding ?? 1));
751
+ const styler = options.styler;
752
+ const lines = options.content.split("\n");
753
+ const titleRoom = options.title === void 0 ? 0 : width(options.title) + 2 + 1 - padding * 2;
754
+ const budget = options.width === void 0 ? 0 : options.width - 2 - padding * 2;
755
+ const inner = lines.reduce((max, line) => Math.max(max, width(line)), Math.max(0, titleRoom, budget));
756
+ const gutter = " ".repeat(padding);
757
+ const bar = paint(styler, chars.vertical);
758
+ const boxTop = () => {
759
+ const span = inner + padding * 2;
760
+ if (options.title === void 0) return paint(styler, `${chars.topLeft}${repeatTo(chars.horizontal, span)}${chars.topRight}`);
761
+ const caption = ` ${options.title} `;
762
+ const room = span - width(caption);
763
+ const lead = paint(styler, repeatTo(chars.horizontal, 1));
764
+ const rest = room - 1 <= 0 ? "" : paint(styler, repeatTo(chars.horizontal, room - 1));
765
+ return `${paint(styler, chars.topLeft)}${lead}${paint(styler, caption)}${rest}${paint(styler, chars.topRight)}`;
766
+ };
767
+ const top = boxTop();
768
+ const bottom = paint(styler, `${chars.bottomLeft}${repeatTo(chars.horizontal, inner + padding * 2)}${chars.bottomRight}`);
769
+ return [
770
+ top,
771
+ ...lines.map((line) => `${bar}${gutter}${align(line, inner)}${gutter}${bar}`),
772
+ bottom
773
+ ].join("\n");
774
+ }
775
+ /**
776
+ * Render a bordered grid of `columns` + `rows` with per-column alignment and width-aware
777
+ * column sizing. Pure: same {@link TableOptions} → same string.
778
+ *
779
+ * @remarks
780
+ * - **Column sizing — visible width.** Each column is sized to the widest VISIBLE width
781
+ * ({@link width}) among its header label and its cells, so an already-styled cell never
782
+ * breaks the column (its escape codes don't count toward the width).
783
+ * - **Ragged rows.** A row shorter than the column count is padded with empty cells; a longer
784
+ * row is truncated to the column count — a ragged input never throws.
785
+ * - **Alignment.** Each cell is positioned by its column's {@link ColumnSpec.align} (default
786
+ * {@link DEFAULT_ALIGN}) via {@link align}.
787
+ * - **Frame.** The {@link BorderStyle} (`options.border`, default {@link DEFAULT_BORDER})
788
+ * draws the outer frame, the header rule (a `teeRight … cross … teeLeft` line), and the
789
+ * `vertical` column separators; `options.styler` colors the frame + header labels when
790
+ * given. Returns the table as `\n`-joined rows (top, header, rule, one row per data row,
791
+ * bottom), no trailing newline.
792
+ *
793
+ * @param options - See {@link TableOptions}
794
+ * @returns The rendered table (multiple lines joined by `\n`)
795
+ */
796
+ function renderTable(options) {
797
+ const chars = BORDER_CHARS[options.border ?? "single"];
798
+ const styler = options.styler;
799
+ const columns = options.columns;
800
+ const widths = columns.map((column, index) => options.rows.reduce((max, row) => Math.max(max, width(cellAt(row, index))), width(column.label)));
801
+ const aligns = columns.map((column) => column.align ?? "left");
802
+ const tableRow = (cells) => {
803
+ const bar = paint(styler, chars.vertical);
804
+ return `${bar}${cells.map((cell, index) => ` ${align(cell, widths[index] ?? width(cell), aligns[index] ?? "left")} `).join(bar)}${bar}`;
805
+ };
806
+ const tableEdge = (left, mid, right) => {
807
+ const segments = widths.map((columnWidth) => repeatTo(chars.horizontal, columnWidth + 2));
808
+ return paint(styler, `${left}${segments.join(mid)}${right}`);
809
+ };
810
+ const header = tableRow(columns.map((column) => paint(styler, column.label)));
811
+ const body = options.rows.map((row) => tableRow(columns.map((_column, index) => cellAt(row, index))));
812
+ return [
813
+ tableEdge(chars.topLeft, chars.teeDown, chars.topRight),
814
+ header,
815
+ tableEdge(chars.teeRight, chars.cross, chars.teeLeft),
816
+ ...body,
817
+ tableEdge(chars.bottomLeft, chars.teeUp, chars.bottomRight)
818
+ ].join("\n");
819
+ }
820
+ /**
821
+ * Render a nested {@link TreeNode} tree with box-drawing connectors. Pure: same
822
+ * {@link TreeOptions} → same string.
823
+ *
824
+ * @remarks
825
+ * The `root` label is the unindented first line; its descendants are drawn beneath it with
826
+ * {@link TREE_CHARS} — `├─ ` before each child but the last, `└─ ` before the last, and the
827
+ * carried prefix using `│ ` under an ancestor that still has later siblings or ` ` under a
828
+ * last ancestor (so the guides line up exactly under the branch they descend from). Node
829
+ * labels are written as given (an already-styled label is honored); `options.styler` colors
830
+ * the connectors when supplied. Returns the tree as `\n`-joined lines, no trailing newline.
831
+ *
832
+ * @param options - See {@link TreeOptions}
833
+ * @returns The rendered tree (multiple lines joined by `\n`)
834
+ *
835
+ * @example
836
+ * ```ts
837
+ * renderTree({ root: { label: 'root', children: [{ label: 'a' }, { label: 'b' }] } })
838
+ * // root
839
+ * // ├─ a
840
+ * // └─ b
841
+ * ```
842
+ */
843
+ function renderTree(options) {
844
+ return [options.root.label, ...renderTreeChildren(options.root.children ?? [], "", options.styler)].join("\n");
845
+ }
846
+ /**
847
+ * Render the connector-prefixed lines for a {@link TreeNode} list — the recursive core
848
+ * behind {@link renderTree}. Each child is drawn as `prefix` + its connector (`├─ ` for
849
+ * any but the last, `└─ ` for the last) + its label, with its own descendants recursed
850
+ * beneath under the carried guide (`│ ` under a non-last node, ` ` under the last).
851
+ *
852
+ * @remarks
853
+ * A centralized, exported recursion branch (AGENTS §5) so it is directly testable and
854
+ * reusable outside {@link renderTree}'s top-level `root.label` framing.
855
+ *
856
+ * @param nodes - The sibling {@link TreeNode}s to render at this depth
857
+ * @param prefix - The guide/gap string carried in from the ancestor chain (`''` at the root)
858
+ * @param styler - The {@link StylerInterface} connectors are colored through, when supplied
859
+ * @returns The rendered lines for `nodes` and all their descendants
860
+ *
861
+ * @example
862
+ * ```ts
863
+ * renderTreeChildren([{ label: 'a' }, { label: 'b' }], '')
864
+ * // ['├─ a', '└─ b']
865
+ * ```
866
+ */
867
+ function renderTreeChildren(nodes, prefix, styler) {
868
+ const lines = [];
869
+ nodes.forEach((node, index) => {
870
+ const last = index === nodes.length - 1;
871
+ lines.push(`${prefix}${paint(styler, last ? TREE_CHARS.corner : TREE_CHARS.branch)}${node.label}`);
872
+ const carry = `${prefix}${paint(styler, last ? TREE_CHARS.gap : TREE_CHARS.guide)}`;
873
+ lines.push(...renderTreeChildren(node.children ?? [], carry, styler));
874
+ });
875
+ return lines;
876
+ }
877
+ /**
878
+ * Stringify ONE captured console argument into a line fragment — the per-argument rule behind
879
+ * {@link formatArgs}: an `Error` → `name: message`, a plain object / array → circular-safe JSON,
880
+ * anything else (string, number, boolean, `null`, `undefined`, symbol, function) → `String(value)`.
881
+ *
882
+ * @remarks
883
+ * - **Total + never throws.** Like a guard (§14), this never throws on adversarial input — a value
884
+ * carrying a circular reference, a `BigInt`, or a throwing `toJSON` is rendered, not raised. The
885
+ * `JSON.stringify` runs with a circular-guard replacer (a seen-set drops a back-reference as
886
+ * `'[Circular]'`); should `JSON.stringify` still throw (e.g. a `BigInt`), the value falls back to
887
+ * `String(value)`. So a `Capture` can never crash the program whose `console.*` it intercepts.
888
+ * - **`Error` first.** An `Error` renders as `name: message` (e.g. `TypeError: bad`) — the useful
889
+ * one-line form, since `JSON.stringify(error)` is `{}` (its fields are non-enumerable).
890
+ * - **Objects → JSON.** A non-null `object` (including an array) is `JSON.stringify`d; a primitive
891
+ * (or `null` / `undefined` / `function` / `symbol`) goes through `String`.
892
+ *
893
+ * @param value - One console argument (any value)
894
+ * @returns The argument's one-line string form
895
+ *
896
+ * @example
897
+ * ```ts
898
+ * stringifyValue('hi') // 'hi'
899
+ * stringifyValue({ a: 1 }) // '{"a":1}'
900
+ * stringifyValue(new TypeError('bad')) // 'TypeError: bad'
901
+ * const cycle: Record<string, unknown> = {}
902
+ * cycle.self = cycle
903
+ * stringifyValue(cycle) // '{"self":"[Circular]"}'
904
+ * ```
905
+ */
906
+ function stringifyValue(value) {
907
+ if (value instanceof Error) return `${value.name}: ${value.message}`;
908
+ if (value === null || typeof value !== "object") return String(value);
909
+ const seen = /* @__PURE__ */ new WeakSet();
910
+ try {
911
+ return JSON.stringify(value, (_key, nested) => {
912
+ if (nested !== null && typeof nested === "object") {
913
+ if (seen.has(nested)) return "[Circular]";
914
+ seen.add(nested);
915
+ }
916
+ return nested;
917
+ });
918
+ } catch {
919
+ return String(value);
920
+ }
921
+ }
922
+ /**
923
+ * Stringify a captured `console.*` argument list into ONE line — the text of a {@link
924
+ * import('./types.js').CapturedMessage}. Each argument is rendered by {@link stringifyValue} and
925
+ * the parts are space-joined, mirroring how a console concatenates its arguments.
926
+ *
927
+ * @remarks
928
+ * Total and never throws (it composes {@link stringifyValue}, which is total) — a `Capture` builds
929
+ * every message through this, so intercepting `console.*` can never crash the underlying program.
930
+ * An empty argument list yields `''` (an empty `console.log()` is captured as a blank line).
931
+ *
932
+ * @param args - The arguments a `console.*` method was called with
933
+ * @returns The arguments stringified and space-joined into one line
934
+ *
935
+ * @example
936
+ * ```ts
937
+ * formatArgs(['count', 3, { ok: true }]) // 'count 3 {"ok":true}'
938
+ * formatArgs([]) // ''
939
+ * ```
940
+ */
941
+ function formatArgs(args) {
942
+ return args.map(stringifyValue).join(" ");
943
+ }
944
+ /**
945
+ * Render a determinate progress bar string — a filled / empty glyph track followed by the percentage
946
+ * and the `(current/total)` count (`█████░░░░░ 50% (5/10)`). Pure: same {@link ProgressBarOptions} →
947
+ * same string. The animation-layer sibling of the C-c `render*` renderers (box / table / tree /
948
+ * separator), shared so a {@link import('./types.js').ProgressInterface} and any direct caller draw
949
+ * the ONE bar — never a second, hand-rolled one (AGENTS §5; scsr shipped three).
950
+ *
951
+ * @remarks
952
+ * - **Fill fraction, clamped.** The filled cell count is `round((current / total) · width)` with
953
+ * `current` clamped to `[0, total]`, so an overrun never over-fills and a negative never under-fills.
954
+ * A `total <= 0` renders a FULL track (there is nothing to fill toward — the work is trivially done).
955
+ * - **Width-aware track.** The filled run is `fill` tiled to the filled cell count and the empty run
956
+ * `empty` tiled to the remainder, each via {@link repeatTo} — so the TRACK is exactly `width` VISIBLE
957
+ * columns even for a multi-cell glyph (its escape codes / extra cells never break the width).
958
+ * - **Styling.** `options.styler` colors the FILLED run only (the empty run + the trailing
959
+ * `percent (count)` label stay plain), through {@link paint}; the layout is identical with or
960
+ * without color, since the track is measured on visible width.
961
+ * - **Label.** The percentage is the rounded `current / total` (e.g. `50%`); the count is the CLAMPED
962
+ * `current` over `total` (`(5/10)`), a single space separating the track, the percent, and the count.
963
+ *
964
+ * @param options - See {@link ProgressBarOptions}
965
+ * @returns The rendered bar line (no trailing newline)
966
+ *
967
+ * @example
968
+ * ```ts
969
+ * renderBar({ current: 5, total: 10, width: 10 }) // '█████░░░░░ 50% (5/10)'
970
+ * renderBar({ current: 10, total: 10, width: 4 }) // '████ 100% (10/10)'
971
+ * ```
972
+ */
973
+ function renderBar(options) {
974
+ const track = options.width ?? 30;
975
+ const fill = options.fill ?? "█";
976
+ const empty = options.empty ?? "░";
977
+ const current = options.total <= 0 ? 0 : Math.max(0, Math.min(options.total, options.current));
978
+ const fraction = options.total <= 0 ? 1 : current / options.total;
979
+ const filledCells = Math.round(fraction * track);
980
+ return `${`${paint(options.styler, repeatTo(fill, filledCells))}${repeatTo(empty, track - filledCells)}`} ${Math.round(fraction * 100)}% (${current}/${options.total})`;
981
+ }
982
+ //#endregion
983
+ //#region src/core/ANSIRenderer.ts
984
+ /**
985
+ * The cross-environment default {@link RendererInterface} — renders style DATA as ANSI
986
+ * SGR escape codes, exactly as `Scheduler` is the `setTimeout` default for its seam. It
987
+ * is the single styling output the whole console / terminal system uses in a terminal;
988
+ * the browser `%c` / CSS renderer (the C-f branch) implements the SAME contract over
989
+ * the SAME {@link Style}, so retargeting changes the renderer, never the style model.
990
+ *
991
+ * @remarks
992
+ * - **Style is DATA in, SGR string out.** It reads the style's `foreground` /
993
+ * `background` / `attributes` and emits one `ESC[…m` sequence whose parameters are the
994
+ * mapped SGR numbers (foreground 30–37 / 90–97, background 40–47 / 100–107, attributes
995
+ * 1 / 2 / 3 / 4 / 7 / 9), followed by `text`, terminated by the reset `ESC[0m`.
996
+ * - **Multiple attributes compose** — their codes join with `;` in a single sequence
997
+ * (`ESC[1;4;31m` for bold + underline + red), so one open and one reset wrap the run.
998
+ * - **`default` and unset colors emit no code** — a `default` (or absent) `foreground` /
999
+ * `background` leaves the terminal's own ink.
1000
+ * - **The empty style and the empty string pass through** — when there is nothing to
1001
+ * apply (no colors, no attributes) or `text` is `''`, `text` is returned VERBATIM with
1002
+ * no escape codes, so an unstyled render never injects a stray reset.
1003
+ * - **Stateless and event-free** — no fields, no events; safe to share one instance.
1004
+ */
1005
+ var ANSIRenderer = class {
1006
+ /**
1007
+ * Wrap `text` in the SGR codes for `style`. Returns `text` unchanged when the style
1008
+ * is empty or `text` is `''`.
1009
+ */
1010
+ render(style, text) {
1011
+ if (text === "") return text;
1012
+ const codes = this.#codes(style);
1013
+ if (codes.length === 0) return text;
1014
+ return `${CSI}${codes.join(";")}m${text}${RESET}`;
1015
+ }
1016
+ #codes(style) {
1017
+ const codes = [];
1018
+ for (const attribute of style.attributes) codes.push(ATTRIBUTE_CODES[attribute]);
1019
+ if (style.foreground !== void 0 && style.foreground !== "default") codes.push(FOREGROUND_CODES[style.foreground]);
1020
+ if (style.background !== void 0 && style.background !== "default") codes.push(BACKGROUND_CODES[style.background]);
1021
+ return codes;
1022
+ }
1023
+ };
1024
+ //#endregion
1025
+ //#region src/core/Styler.ts
1026
+ /**
1027
+ * The fluent, composable styler — the consumer-facing API over the style engine. It
1028
+ * builds a {@link Style} (style as DATA) and renders it through an injected
1029
+ * {@link RendererInterface} (the ANSI default, or a browser `%c` renderer at C-f). Each
1030
+ * color / attribute accessor is immutable copy-on-write: it returns a NEW styler's
1031
+ * surface with the token added, so `styler.red.bold('hi')` composes without mutating,
1032
+ * and a base styler is freely reusable.
1033
+ *
1034
+ * @remarks
1035
+ * - **Callable surface.** A `Styler` is not itself callable; its {@link surface} getter
1036
+ * returns the {@link StylerInterface} — a render FUNCTION carrying the chainable
1037
+ * accessors. The accessors are installed as LAZY getters (`Object.defineProperties`),
1038
+ * so a chain materializes only the stylers it actually walks — never the full tree —
1039
+ * and the recursion terminates. The factory returns that surface; this class is the
1040
+ * engine behind it.
1041
+ * - **Immutable.** `#foreground` and `#attribute` return a fresh `Styler` (the style is
1042
+ * rebuilt, never mutated). A later color of the same channel WINS (last write); a
1043
+ * repeated attribute is idempotent (de-duplicated, order preserved).
1044
+ * - **`enabled` switch.** When `false`, the render function returns text VERBATIM — no
1045
+ * renderer call, no escape codes (for a non-TTY / `NO_COLOR` / piped output).
1046
+ * - **Event-free** — a pure styling primitive (AGENTS §13), like `Scheduler`.
1047
+ */
1048
+ var Styler = class Styler {
1049
+ #renderer;
1050
+ #enabled;
1051
+ #style;
1052
+ constructor(renderer, enabled, style) {
1053
+ this.#renderer = renderer;
1054
+ this.#enabled = enabled;
1055
+ this.#style = style;
1056
+ }
1057
+ /** The accumulated style DATA — the empty style on a base styler. */
1058
+ get style() {
1059
+ return this.#style;
1060
+ }
1061
+ /** Whether styling is applied; when `false`, the surface returns text unchanged. */
1062
+ get enabled() {
1063
+ return this.#enabled;
1064
+ }
1065
+ /**
1066
+ * The fluent {@link StylerInterface} value — a render function (`text => string`) with
1067
+ * `style`, `enabled`, and every {@link Color} / {@link Attribute} as a LAZY accessor
1068
+ * (each computes the next styler's surface only when read). This is what consumers
1069
+ * hold and call.
1070
+ *
1071
+ * @remarks
1072
+ * The accessors are defined as getters (not eagerly-merged values), so accessing one
1073
+ * builds exactly one child styler — the tree is never fully materialized and the
1074
+ * construction terminates. The assembled function is then narrowed to
1075
+ * {@link StylerInterface} through {@link #isSurface} (a real structural check), so no
1076
+ * type assertion is used (AGENTS §1 / §14 — narrow, never assert).
1077
+ */
1078
+ get surface() {
1079
+ const render = (text) => this.#enabled ? this.#renderer.render(this.#style, text) : text;
1080
+ const descriptors = {
1081
+ style: {
1082
+ value: this.#style,
1083
+ enumerable: true
1084
+ },
1085
+ enabled: {
1086
+ value: this.#enabled,
1087
+ enumerable: true
1088
+ }
1089
+ };
1090
+ for (const color of COLORS) descriptors[color] = {
1091
+ get: () => this.#foreground(color).surface,
1092
+ enumerable: true
1093
+ };
1094
+ for (const attribute of ATTRIBUTES) descriptors[attribute] = {
1095
+ get: () => this.#attribute(attribute).surface,
1096
+ enumerable: true
1097
+ };
1098
+ const surface = Object.defineProperties(render, descriptors);
1099
+ if (this.#isSurface(surface)) return surface;
1100
+ throw new ConsoleError("INVARIANT", "console: styler surface construction is incomplete");
1101
+ }
1102
+ #isSurface(value) {
1103
+ return typeof value === "function" && "style" in value && "enabled" in value && "red" in value && "bold" in value;
1104
+ }
1105
+ #foreground(color) {
1106
+ return new Styler(this.#renderer, this.#enabled, Object.freeze({
1107
+ ...this.#style,
1108
+ foreground: color
1109
+ }));
1110
+ }
1111
+ #attribute(attribute) {
1112
+ if (this.#style.attributes.includes(attribute)) return this;
1113
+ return new Styler(this.#renderer, this.#enabled, Object.freeze({
1114
+ ...this.#style,
1115
+ attributes: Object.freeze([...this.#style.attributes, attribute])
1116
+ }));
1117
+ }
1118
+ };
1119
+ //#endregion
1120
+ //#region src/core/Capture.ts
1121
+ /**
1122
+ * An observable console interceptor (AGENTS §13) — it takes control of the global `console.*` on
1123
+ * the READ side. While `active`, every configured `console.x` call is captured as a frozen
1124
+ * {@link CapturedMessage}, buffered (total + by level, bounded), emitted on `capture`, and — per
1125
+ * options — mirrored to the real console and/or forwarded to a {@link SinkInterface}.
1126
+ *
1127
+ * @remarks
1128
+ * - **Snapshot-at-start (the no-capture-loop principle).** `start()` snapshots the CURRENT
1129
+ * `console[level]` for each configured {@link CaptureLevel}, then installs the wrappers. The
1130
+ * mirror writes through that snapshot — so our OWN console sink output (the Logger / Reporter,
1131
+ * which snapshot the real `console` at creation) is never recaptured: `Capture` catches
1132
+ * THIRD-PARTY `console.*`, not our writes. Create your loggers BEFORE installing a capture.
1133
+ * - **Idempotent + PROCESS-GLOBAL + NON-REENTRANT.** `start()` while already `active` is a no-op
1134
+ * (never double-patches); `stop()` while inactive is a no-op. It patches the ONE global
1135
+ * `console`, so at most ONE capture may be active at a time — running two concurrently
1136
+ * interleaves their buffers and clobbers each other's restore.
1137
+ * - **Bounded buffers.** `messages()` / `messages(level)` — the total buffer and each by-level
1138
+ * bucket are each capped at `limit`
1139
+ * (oldest dropped first), never unbounded — the same retention precedent as {@link Logger}.
1140
+ * - **Lifecycle (§10).** `start` / `stop` toggle interception (emitting `start` / `stop`);
1141
+ * `destroy()` stops (restoring `console`) then destroys the emitter.
1142
+ *
1143
+ * @example
1144
+ * ```ts
1145
+ * const capture = new Capture({ levels: ['warn', 'error'], mirror: true })
1146
+ * capture.start()
1147
+ * console.warn('third-party noise') // captured AND mirrored to the real console
1148
+ * capture.messages('warn') // [{ level: 'warn', text: 'third-party noise', time: … }]
1149
+ * capture.stop() // console.warn restored
1150
+ * ```
1151
+ */
1152
+ var Capture = class {
1153
+ #emitter;
1154
+ #levels;
1155
+ #mirror;
1156
+ #sink;
1157
+ #limit;
1158
+ #messages = [];
1159
+ #buckets = /* @__PURE__ */ new Map();
1160
+ #originals = /* @__PURE__ */ new Map();
1161
+ #active = false;
1162
+ constructor(options) {
1163
+ this.#emitter = new _orkestrel_emitter.Emitter({
1164
+ on: options?.on,
1165
+ error: options?.error
1166
+ });
1167
+ this.#levels = options?.levels ?? DEFAULT_CAPTURE_LEVELS;
1168
+ this.#mirror = options?.mirror ?? false;
1169
+ this.#sink = options?.sink;
1170
+ this.#limit = options?.limit ?? 1e3;
1171
+ for (const level of this.#levels) this.#buckets.set(level, []);
1172
+ }
1173
+ get emitter() {
1174
+ return this.#emitter;
1175
+ }
1176
+ get active() {
1177
+ return this.#active;
1178
+ }
1179
+ start() {
1180
+ if (this.#active) return;
1181
+ this.#active = true;
1182
+ const target = console;
1183
+ for (const level of this.#levels) {
1184
+ const original = target[level];
1185
+ this.#originals.set(level, original);
1186
+ const mirror = original.bind(console);
1187
+ target[level] = (...args) => this.#intercept(level, args, mirror);
1188
+ }
1189
+ this.#emitter.emit("start");
1190
+ }
1191
+ stop() {
1192
+ if (!this.#active) return;
1193
+ this.#active = false;
1194
+ const target = console;
1195
+ for (const [level, original] of this.#originals) target[level] = original;
1196
+ this.#originals.clear();
1197
+ this.#emitter.emit("stop");
1198
+ }
1199
+ messages(level) {
1200
+ if (level === void 0) return [...this.#messages];
1201
+ return [...this.#buckets.get(level) ?? []];
1202
+ }
1203
+ clear() {
1204
+ this.#messages.length = 0;
1205
+ for (const bucket of this.#buckets.values()) bucket.length = 0;
1206
+ }
1207
+ destroy() {
1208
+ this.stop();
1209
+ this.#emitter.destroy();
1210
+ }
1211
+ #intercept(level, args, mirror) {
1212
+ const message = this.#capture(level, args);
1213
+ this.#retain(message);
1214
+ this.#emitter.emit("capture", message);
1215
+ if (this.#mirror) mirror(...args);
1216
+ if (this.#sink !== void 0) try {
1217
+ this.#sink.write(message.text, CAPTURE_LEVEL_MAP[level]);
1218
+ } catch {}
1219
+ }
1220
+ #capture(level, args) {
1221
+ return Object.freeze({
1222
+ level,
1223
+ text: formatArgs(args),
1224
+ time: Date.now()
1225
+ });
1226
+ }
1227
+ #retain(message) {
1228
+ this.#push(this.#messages, message);
1229
+ const bucket = this.#buckets.get(message.level);
1230
+ if (bucket !== void 0) this.#push(bucket, message);
1231
+ }
1232
+ #push(buffer, message) {
1233
+ buffer.push(message);
1234
+ if (buffer.length > this.#limit) buffer.shift();
1235
+ }
1236
+ };
1237
+ //#endregion
1238
+ //#region src/core/LoggerManager.ts
1239
+ /**
1240
+ * An event-free registry of named {@link Logger}s plus a convenience fan-out — the §9
1241
+ * manager over the logging layer (a registry, never observable itself; each {@link Logger}
1242
+ * owns its own `emitter`).
1243
+ *
1244
+ * @remarks
1245
+ * - **Registry (§9).** Loggers live in an insertion-ordered `Map` keyed by `name`.
1246
+ * `register(name, options?)` mints a {@link Logger} named `name` — the manager's default
1247
+ * `level` / `sink` / `styler` / `limit` / `silent` flow in unless `options` OVERRIDES them
1248
+ * (`name` is always the registry key, so any `options.name` is ignored) — stores it (a
1249
+ * re-`register` of the same name OVERWRITES, last write wins), and returns it. `count` is
1250
+ * the map size, `logger(name)` looks one up, `loggers()` lists them in insertion order.
1251
+ * - **Removal (§9.2).** `remove()` clears ALL, `remove(name)` drops ONE (`true` if present),
1252
+ * `remove(names)` drops a batch (`true` if any was removed). `clear()` empties the registry.
1253
+ * (Removal does NOT `destroy` the returned loggers — a caller still holding one keeps using
1254
+ * it; the manager simply stops tracking it.)
1255
+ * - **Fan-out.** `debug` / `info` / `warn` / `error(message, data?)` forward the one call to
1256
+ * EVERY registered logger; each gates / emits / writes per its own `level` and `sink`. A
1257
+ * fan-out over an empty registry is a no-op.
1258
+ * - **Event-free.** No emitter, no events — the manager is a pure registry; observability is
1259
+ * per-{@link Logger}.
1260
+ *
1261
+ * @example
1262
+ * ```ts
1263
+ * const manager = new LoggerManager({ level: 'warn' })
1264
+ * manager.register('http') // inherits the `warn` default
1265
+ * manager.register('db', { level: 'debug' }) // overrides to `debug`
1266
+ * manager.warn('slow', { ms: 900 }) // fans out to both loggers
1267
+ * manager.count // 2
1268
+ * ```
1269
+ */
1270
+ var LoggerManager = class {
1271
+ #loggers = /* @__PURE__ */ new Map();
1272
+ #level;
1273
+ #sink;
1274
+ #styler;
1275
+ #limit;
1276
+ #silent;
1277
+ constructor(options) {
1278
+ this.#level = options?.level;
1279
+ this.#sink = options?.sink;
1280
+ this.#styler = options?.styler;
1281
+ this.#limit = options?.limit;
1282
+ this.#silent = options?.silent;
1283
+ }
1284
+ get count() {
1285
+ return this.#loggers.size;
1286
+ }
1287
+ register(name, options) {
1288
+ const logger = new Logger({
1289
+ level: this.#level,
1290
+ sink: this.#sink,
1291
+ styler: this.#styler,
1292
+ limit: this.#limit,
1293
+ silent: this.#silent,
1294
+ ...options,
1295
+ name
1296
+ });
1297
+ this.#loggers.set(name, logger);
1298
+ return logger;
1299
+ }
1300
+ logger(name) {
1301
+ return this.#loggers.get(name);
1302
+ }
1303
+ loggers() {
1304
+ return [...this.#loggers.values()];
1305
+ }
1306
+ debug(message, data) {
1307
+ for (const logger of this.#loggers.values()) logger.debug(message, data);
1308
+ }
1309
+ info(message, data) {
1310
+ for (const logger of this.#loggers.values()) logger.info(message, data);
1311
+ }
1312
+ warn(message, data) {
1313
+ for (const logger of this.#loggers.values()) logger.warn(message, data);
1314
+ }
1315
+ error(message, data) {
1316
+ for (const logger of this.#loggers.values()) logger.error(message, data);
1317
+ }
1318
+ remove(names) {
1319
+ if (names === void 0) {
1320
+ this.#loggers.clear();
1321
+ return;
1322
+ }
1323
+ if ((0, _orkestrel_contract.isArray)(names)) {
1324
+ let removed = false;
1325
+ for (const name of names) if (this.#loggers.delete(name)) removed = true;
1326
+ return removed;
1327
+ }
1328
+ return this.#loggers.delete(names);
1329
+ }
1330
+ clear() {
1331
+ this.#loggers.clear();
1332
+ }
1333
+ };
1334
+ //#endregion
1335
+ //#region src/core/Progress.ts
1336
+ /**
1337
+ * An update-driven, observable progress bar (AGENTS §13) — {@link update} recomputes the bar via
1338
+ * {@link renderBar}, writes `\r` + bar to its {@link SinkInterface}, and emits the `{ current, total }`
1339
+ * on `update`. The leading `\r` is what an overwrite-capable sink (the C-g TTY sink) redraws on; a
1340
+ * plain sink (C-f) degrades to a fresh, non-overwriting line — the line-OVERWRITE is the SINK's job.
1341
+ * UNIVERSAL — the one {@link StylerInterface} + the one {@link SinkInterface}, no `node:*`, no
1342
+ * `process.stdout`. NO self-timer (unlike {@link import('./Spinner.js').Spinner}) — the caller drives it.
1343
+ *
1344
+ * @remarks
1345
+ * - **Update-driven.** Each {@link update} clamps `current` to `[0, total]`, renders the bar (filled
1346
+ * to `current / total`, with the trailing `percent (current/total)` + message) via {@link renderBar},
1347
+ * emits `update`, and writes `'\r' + bar`. Progress advances only when the caller reports it.
1348
+ * - **Outcome lines.** {@link complete} renders a FULL bar (`current = total`) + message, terminated by
1349
+ * a newline, emits a final `update` then `complete`, and marks `completed`. {@link failure} renders the
1350
+ * bar at its CURRENT fill + message + newline and routes to the sink's error stream (no `complete` —
1351
+ * the work did not finish). Both are terminal: a later {@link update} is ignored once `active` is false.
1352
+ * - **Bounded.** `current` is always clamped to `[0, total]`; {@link completed} reports whether
1353
+ * {@link complete} has run; {@link active} is `true` until a {@link complete} / {@link failure}.
1354
+ * - **Lifecycle (§10).** {@link destroy} destroys the emitter (there is no timer to clear).
1355
+ *
1356
+ * @example
1357
+ * ```ts
1358
+ * const progress = new Progress({ total: 100, message: 'downloading' })
1359
+ * progress.update(40) // ████████████░░░░░░░░░░░░░░░░░░ 40% (40/100) downloading
1360
+ * progress.update(80, 'almost there')
1361
+ * progress.complete('done') // a full bar, committed with a newline
1362
+ * ```
1363
+ */
1364
+ var Progress = class {
1365
+ #emitter;
1366
+ #total;
1367
+ #width;
1368
+ #sink;
1369
+ #styler;
1370
+ #message;
1371
+ #current = 0;
1372
+ #active = true;
1373
+ #completed = false;
1374
+ constructor(options) {
1375
+ this.#emitter = new _orkestrel_emitter.Emitter({
1376
+ on: options.on,
1377
+ error: options.error
1378
+ });
1379
+ this.#total = options.total;
1380
+ this.#width = options.width ?? 30;
1381
+ this.#sink = options.sink ?? createConsoleSink();
1382
+ this.#styler = options.styler ?? createStyler();
1383
+ this.#message = options.message ?? "";
1384
+ }
1385
+ get emitter() {
1386
+ return this.#emitter;
1387
+ }
1388
+ get active() {
1389
+ return this.#active;
1390
+ }
1391
+ get completed() {
1392
+ return this.#completed;
1393
+ }
1394
+ get current() {
1395
+ return this.#current;
1396
+ }
1397
+ get total() {
1398
+ return this.#total;
1399
+ }
1400
+ update(current, message) {
1401
+ if (!this.#active) return;
1402
+ this.#advance(current, message);
1403
+ this.#paint(false);
1404
+ }
1405
+ complete(message) {
1406
+ if (!this.#active) return;
1407
+ this.#advance(this.#total, message);
1408
+ this.#active = false;
1409
+ this.#completed = true;
1410
+ this.#paint(true);
1411
+ this.#emitter.emit("complete");
1412
+ }
1413
+ failure(message) {
1414
+ if (!this.#active) return;
1415
+ this.#advance(this.#current, message);
1416
+ this.#active = false;
1417
+ this.#paint(true, "error");
1418
+ }
1419
+ destroy() {
1420
+ this.#emitter.destroy();
1421
+ }
1422
+ #advance(current, message) {
1423
+ this.#current = Math.max(0, Math.min(this.#total, current));
1424
+ if (message !== void 0) this.#message = message;
1425
+ this.#emitter.emit("update", {
1426
+ current: this.#current,
1427
+ total: this.#total
1428
+ });
1429
+ }
1430
+ #paint(final, level) {
1431
+ const bar = renderBar({
1432
+ current: this.#current,
1433
+ total: this.#total,
1434
+ width: this.#width,
1435
+ styler: this.#styler.cyan
1436
+ });
1437
+ const line = this.#message === "" ? bar : `${bar} ${this.#message}`;
1438
+ this.#sink.write(`\r${line}${final ? "\n" : ""}`, level);
1439
+ }
1440
+ };
1441
+ //#endregion
1442
+ //#region src/core/Reporter.ts
1443
+ /**
1444
+ * A lean, event-free narrative reporter (AGENTS §13) — the composable verb set for human /
1445
+ * build-run output. Each verb FORMATS its line through the shared {@link StylerInterface} and
1446
+ * the pure layout renderers ({@link renderSeparator} / {@link renderBox} / {@link renderTable}
1447
+ * / {@link renderTree}) and WRITES it to a {@link SinkInterface} — the SAME styler + sink
1448
+ * substrate the logger uses, never a second colorizer.
1449
+ *
1450
+ * @remarks
1451
+ * - **A SMALL set, not a grab-bag.** `section` / `step` / `timing` / `status` / `table` /
1452
+ * `tree` / `box` / `line` / `blank`. No spinner / bar (the animation chunk), no buffering /
1453
+ * capture (the capture chunk), no level retention (the logger). Just format + write.
1454
+ * - **`status` is a narrative OUTCOME, not a log level.** Its {@link StatusLevel} (`success` /
1455
+ * `error` / `warn` / `info`) is distinct from {@link import('./types.js').LogLevel}: an icon
1456
+ * ({@link STATUS_ICONS}) + a color ({@link STATUS_COLORS}), with `error` routed to the sink's
1457
+ * error stream (the `level` hint forwarded to {@link SinkInterface.write}) — there is no
1458
+ * gating and no severity ordering.
1459
+ * - **Width-aware.** `section` (and a `box` with no explicit `width`) lay out to the reporter's
1460
+ * `#width`; the renderers measure on VISIBLE width (ANSI-aware), so styled content aligns.
1461
+ * - **Event-free (§13).** No `#emitter` — a pure formatting front-end with no observable
1462
+ * lifecycle (like the renderers and `Scheduler`). It is reusable and holds no per-call state.
1463
+ *
1464
+ * @example
1465
+ * ```ts
1466
+ * const reporter = new Reporter()
1467
+ * reporter.section('Build')
1468
+ * reporter.step('compiling', { index: 1, total: 3 }) // [1/3] compiling
1469
+ * reporter.timing('bundle', 1234) // bundle … 1.23s
1470
+ * reporter.status('success', 'done') // ✔ done
1471
+ * ```
1472
+ */
1473
+ var Reporter = class {
1474
+ #sink;
1475
+ #styler;
1476
+ #width;
1477
+ constructor(options) {
1478
+ this.#sink = options?.sink ?? createConsoleSink();
1479
+ this.#styler = options?.styler ?? createStyler();
1480
+ this.#width = options?.width ?? 80;
1481
+ }
1482
+ section(title) {
1483
+ this.#sink.write(renderSeparator({
1484
+ title,
1485
+ width: this.#width,
1486
+ styler: this.#styler.dim
1487
+ }));
1488
+ }
1489
+ step(message, position) {
1490
+ const prefix = position === void 0 ? "" : `${this.#styler.cyan(`[${position.index}/${position.total}]`)} `;
1491
+ this.#sink.write(`${prefix}${message}`);
1492
+ }
1493
+ timing(label, ms) {
1494
+ this.#sink.write(`${label} ${this.#styler.dim(`… ${formatDuration(ms)}`)}`);
1495
+ }
1496
+ status(level, message) {
1497
+ const color = this.#styler[STATUS_COLORS[level]];
1498
+ const line = `${color(STATUS_ICONS[level])} ${color(message)}`;
1499
+ this.#sink.write(line, level === "error" ? "error" : void 0);
1500
+ }
1501
+ table(options) {
1502
+ this.#sink.write(renderTable({
1503
+ styler: this.#styler.dim,
1504
+ ...options
1505
+ }));
1506
+ }
1507
+ tree(options) {
1508
+ this.#sink.write(renderTree({
1509
+ styler: this.#styler.dim,
1510
+ ...options
1511
+ }));
1512
+ }
1513
+ box(options) {
1514
+ this.#sink.write(renderBox({
1515
+ width: this.#width,
1516
+ styler: this.#styler.dim,
1517
+ ...options
1518
+ }));
1519
+ }
1520
+ line(text) {
1521
+ this.#sink.write(text);
1522
+ }
1523
+ blank(count = 1) {
1524
+ for (let index = 0; index < count; index += 1) this.#sink.write("");
1525
+ }
1526
+ };
1527
+ //#endregion
1528
+ //#region src/core/Spinner.ts
1529
+ /**
1530
+ * A self-driving, observable activity spinner (AGENTS §13) — a glyph cycle that advances on a
1531
+ * periodic timer, writing each `\r` + frame line to its {@link SinkInterface} and emitting it on
1532
+ * `frame`. The leading `\r` is what an overwrite-capable sink (the C-g TTY sink) redraws on; a plain
1533
+ * sink (C-f) degrades to a fresh, non-overwriting line — the line-OVERWRITE is the SINK's job, never
1534
+ * the spinner's. UNIVERSAL — `setInterval` + the one {@link StylerInterface} + the one
1535
+ * {@link SinkInterface}, no `node:*`, no `process.stdout`.
1536
+ *
1537
+ * @remarks
1538
+ * - **Self-driving but deterministically testable.** `start()` arms a `setInterval` that calls
1539
+ * {@link tick} each `interval`; each {@link tick} builds the styled `glyph + message` line for the
1540
+ * current frame, emits it on `frame`, writes `'\r' + line` to the sink, then advances the frame
1541
+ * index (wrapping). A test drives frames by calling {@link tick} directly (NO real clock) and proves
1542
+ * the timer arms / clears with fake timers.
1543
+ * - **Leak-free timer.** The interval is ALWAYS cleared on {@link success} / {@link failure} /
1544
+ * {@link stop} / {@link destroy} — `#handle` is the single source of `active`, set on arm and unset
1545
+ * on clear, so a spinner never leaks a running interval.
1546
+ * - **Idempotent `start`.** A {@link start} while already `active` is a no-op (it never arms a second
1547
+ * timer).
1548
+ * - **Outcome lines.** {@link success} / {@link failure} clear the timer then write + emit a FINAL line —
1549
+ * the {@link STATUS_ICONS} `✔` / `✖` (colored via {@link STATUS_COLORS}) + the message — terminated
1550
+ * by a newline (the activity is over; the line is committed, not overwritten). {@link failure} routes to
1551
+ * the sink's error stream.
1552
+ * - **Lifecycle (§10).** {@link stop} clears the timer and LEAVES the current line; {@link destroy}
1553
+ * stops then destroys the emitter. {@link update} swaps the message and re-renders immediately when
1554
+ * `active`.
1555
+ *
1556
+ * @example
1557
+ * ```ts
1558
+ * const spinner = new Spinner({ message: 'building' })
1559
+ * spinner.start() // arms the timer, paints the first frame to the sink
1560
+ * spinner.update('bundling') // message changes, re-rendered at once
1561
+ * spinner.success('built in 1.2s') // ✔ built in 1.2s — timer cleared, line committed
1562
+ * ```
1563
+ */
1564
+ var Spinner = class {
1565
+ #emitter;
1566
+ #frames;
1567
+ #interval;
1568
+ #sink;
1569
+ #styler;
1570
+ #message;
1571
+ #handle;
1572
+ #index = 0;
1573
+ constructor(options) {
1574
+ this.#emitter = new _orkestrel_emitter.Emitter({
1575
+ on: options?.on,
1576
+ error: options?.error
1577
+ });
1578
+ const frames = options?.frames ?? SPINNER_FRAMES;
1579
+ this.#frames = frames.length === 0 ? SPINNER_FRAMES : frames;
1580
+ this.#interval = options?.interval ?? 80;
1581
+ this.#sink = options?.sink ?? createConsoleSink();
1582
+ this.#styler = options?.styler ?? createStyler();
1583
+ this.#message = options?.message ?? "";
1584
+ }
1585
+ get emitter() {
1586
+ return this.#emitter;
1587
+ }
1588
+ get active() {
1589
+ return this.#handle !== void 0;
1590
+ }
1591
+ get message() {
1592
+ return this.#message;
1593
+ }
1594
+ start() {
1595
+ if (this.#handle !== void 0) return;
1596
+ this.#handle = setInterval(() => this.tick(), this.#interval);
1597
+ this.#emitter.emit("start");
1598
+ this.tick();
1599
+ }
1600
+ tick() {
1601
+ this.#paint(this.#line());
1602
+ this.#index = (this.#index + 1) % this.#frames.length;
1603
+ }
1604
+ update(message) {
1605
+ this.#message = message;
1606
+ if (this.#handle !== void 0) this.#paint(this.#line());
1607
+ }
1608
+ success(message) {
1609
+ this.#finish("success", message);
1610
+ }
1611
+ failure(message) {
1612
+ this.#finish("error", message);
1613
+ }
1614
+ stop() {
1615
+ if (this.#handle === void 0) return;
1616
+ clearInterval(this.#handle);
1617
+ this.#handle = void 0;
1618
+ this.#emitter.emit("stop");
1619
+ }
1620
+ destroy() {
1621
+ this.stop();
1622
+ this.#emitter.destroy();
1623
+ }
1624
+ #finish(level, message) {
1625
+ this.stop();
1626
+ const text = message ?? this.#message;
1627
+ if (message !== void 0) this.#message = message;
1628
+ const color = this.#styler[STATUS_COLORS[level]];
1629
+ const line = `${color(STATUS_ICONS[level])} ${color(text)}`;
1630
+ this.#emitter.emit("frame", line);
1631
+ this.#sink.write(`\r${line}\n`, level === "error" ? "error" : void 0);
1632
+ }
1633
+ #line() {
1634
+ const glyph = this.#styler.cyan(this.#frames[this.#index] ?? "");
1635
+ return this.#message === "" ? glyph : `${glyph} ${this.#message}`;
1636
+ }
1637
+ #paint(line) {
1638
+ this.#emitter.emit("frame", line);
1639
+ this.#sink.write(`\r${line}`);
1640
+ }
1641
+ };
1642
+ //#endregion
1643
+ //#region src/core/factories.ts
1644
+ /**
1645
+ * Create the cross-environment default {@link RendererInterface} — the ANSI / SGR
1646
+ * renderer that turns style DATA into terminal escape codes. The default behind
1647
+ * {@link createStyler}; construct one directly to render a {@link import('./types.js').Style}
1648
+ * without the fluent surface, or to share one instance across stylers.
1649
+ *
1650
+ * @returns A stateless ANSI {@link RendererInterface}
1651
+ *
1652
+ * @example
1653
+ * ```ts
1654
+ * import { createANSIRenderer } from '@src/core'
1655
+ *
1656
+ * const renderer = createANSIRenderer()
1657
+ * renderer.render({ foreground: 'red', attributes: ['bold'] }, 'alert') // '\x1b[1;31malert\x1b[0m'
1658
+ * ```
1659
+ */
1660
+ function createANSIRenderer() {
1661
+ return new ANSIRenderer();
1662
+ }
1663
+ /**
1664
+ * Create the fluent, composable {@link StylerInterface} — the consumer-facing styling
1665
+ * API. It builds a {@link import('./types.js').Style} under the hood and renders it
1666
+ * through a {@link RendererInterface} (the ANSI default), so `styler.red.bold('hi')`
1667
+ * yields styled text. Chains are immutable, so a base styler is freely reusable.
1668
+ *
1669
+ * @param options - See {@link StylerOptions}
1670
+ * @returns A base {@link StylerInterface}
1671
+ *
1672
+ * @remarks
1673
+ * - `options.renderer` swaps the output target without touching the style model — pass a
1674
+ * browser `%c` / CSS renderer (the C-f branch) to retarget; defaults to the ANSI
1675
+ * renderer (the cross-environment default).
1676
+ * - `options.enabled` is the no-color switch: when `false`, the styler returns text
1677
+ * VERBATIM (for a non-TTY, `NO_COLOR`, or piped output); defaults to `true`.
1678
+ *
1679
+ * @example
1680
+ * ```ts
1681
+ * import { createStyler } from '@src/core'
1682
+ *
1683
+ * const style = createStyler()
1684
+ * style.red.bold('error') // bold red
1685
+ * style.red(style.underline('link')) // composes either way
1686
+ *
1687
+ * // Disable for a non-TTY — every call returns its text unchanged.
1688
+ * const plain = createStyler({ enabled: false })
1689
+ * plain.green('ok') // 'ok'
1690
+ * ```
1691
+ */
1692
+ function createStyler(options) {
1693
+ return new Styler(options?.renderer ?? new ANSIRenderer(), options?.enabled ?? true, EMPTY_STYLE).surface;
1694
+ }
1695
+ /**
1696
+ * Create the default {@link SinkInterface} — a console sink that routes by level and writes
1697
+ * through the `console` methods SNAPSHOTTED at creation. The default output target behind
1698
+ * {@link createLogger}.
1699
+ *
1700
+ * @returns A console {@link SinkInterface}
1701
+ *
1702
+ * @remarks
1703
+ * - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` /
1704
+ * `console.error` AT CALL TIME and writes through those references. So when a later
1705
+ * `Capture` (C-d) PATCHES `console.*`, this sink still reaches the REAL streams — the
1706
+ * writer and the capturer never feed each other (the no-capture-loop principle). Create
1707
+ * the sink (or the logger) BEFORE installing a capture for this to hold.
1708
+ * - **Routes by level.** `error` → the snapshotted `console.error`, `warn` →
1709
+ * `console.warn`, every other level → `console.log`. The `level` is supplied by the
1710
+ * logger; an omitted `level` goes to `console.log`.
1711
+ *
1712
+ * @example
1713
+ * ```ts
1714
+ * import { createConsoleSink } from '@src/core'
1715
+ *
1716
+ * const sink = createConsoleSink() // snapshots console.* now
1717
+ * sink.write('boom', 'error') // → the real console.error, even after a later console patch
1718
+ * ```
1719
+ */
1720
+ function createConsoleSink() {
1721
+ const log = console.log.bind(console);
1722
+ const warn = console.warn.bind(console);
1723
+ const error = console.error.bind(console);
1724
+ return { write(text, level) {
1725
+ if (level === "error") {
1726
+ error(text);
1727
+ return;
1728
+ }
1729
+ if (level === "warn") {
1730
+ warn(text);
1731
+ return;
1732
+ }
1733
+ log(text);
1734
+ } };
1735
+ }
1736
+ /**
1737
+ * Create an observable, leveled {@link LoggerInterface} — the entry point into structured
1738
+ * logging. Each `debug` / `info` / `warn` / `error` call builds a frozen
1739
+ * {@link import('./types.js').LogRecord}, gates it by severity, retains a bounded tail,
1740
+ * ALWAYS emits it on `entry` (the transport seam), and — unless `silent` — writes a styled
1741
+ * line to its sink.
1742
+ *
1743
+ * @param options - See {@link LoggerOptions}
1744
+ * @returns A {@link LoggerInterface}
1745
+ *
1746
+ * @remarks
1747
+ * - **Record + event = transport (§13).** Subscribe `logger.emitter.on('entry', …)` to tee
1748
+ * records to a file / JSON / remote transport; the event fires for every accepted record,
1749
+ * even when `silent` (silence suppresses only the SINK WRITE).
1750
+ * - **Bounded retention.** `entries()` returns the recent records, capped at `options.limit`
1751
+ * (default {@link DEFAULT_LOG_LIMIT}); never unbounded.
1752
+ * - **Sink + styler defaults.** `options.sink` defaults to {@link createConsoleSink} (the
1753
+ * snapshotted, level-routing console sink); `options.styler` to {@link createStyler} (ANSI).
1754
+ * Styling is orthogonal to level — a level only chooses a label color.
1755
+ *
1756
+ * @example
1757
+ * ```ts
1758
+ * import { createLogger } from '@src/core'
1759
+ *
1760
+ * const logger = createLogger({ name: 'http', level: 'info' })
1761
+ * logger.info('request', { method: 'GET', path: '/' })
1762
+ * logger.debug('verbose') // dropped — below the info threshold
1763
+ * ```
1764
+ */
1765
+ function createLogger(options) {
1766
+ return new Logger(options);
1767
+ }
1768
+ /**
1769
+ * Create an event-free {@link LoggerManagerInterface} — a §9 registry of named loggers plus
1770
+ * a convenience fan-out. It mints + stores {@link LoggerInterface}s keyed by name (its
1771
+ * defaults flowing into each), looks them up, removes them, and broadcasts a one-off log to
1772
+ * every registered logger.
1773
+ *
1774
+ * @param options - See {@link LoggerManagerOptions}
1775
+ * @returns A {@link LoggerManagerInterface}
1776
+ *
1777
+ * @remarks
1778
+ * - **Defaults flow in.** `options.level` / `sink` / `styler` / `limit` / `silent` are the
1779
+ * defaults flowed into every `register`ed logger unless that call's options override them.
1780
+ * - **Event-free.** The manager carries NO emitter (each registered logger owns its own
1781
+ * observable `emitter`) — it is a pure registry.
1782
+ *
1783
+ * @example
1784
+ * ```ts
1785
+ * import { createLoggerManager } from '@src/core'
1786
+ *
1787
+ * const loggers = createLoggerManager({ level: 'warn' })
1788
+ * loggers.register('http')
1789
+ * loggers.register('db', { level: 'debug' }) // overrides the default
1790
+ * loggers.warn('slow', { ms: 900 }) // fans out to both
1791
+ * ```
1792
+ */
1793
+ function createLoggerManager(options) {
1794
+ return new LoggerManager(options);
1795
+ }
1796
+ /**
1797
+ * Create a lean, event-free {@link ReporterInterface} — the entry point into narrative
1798
+ * reporting. Each verb (`section` / `step` / `timing` / `status` / `table` / `tree` / `box` /
1799
+ * `line` / `blank`) formats through the shared styler + the pure layout renderers and writes to
1800
+ * the sink — human / build-run narration over the SAME substrate the logger uses.
1801
+ *
1802
+ * @param options - See {@link ReporterOptions}
1803
+ * @returns A {@link ReporterInterface}
1804
+ *
1805
+ * @remarks
1806
+ * - **One styler, one sink.** `options.styler` defaults to {@link createStyler} (ANSI) and
1807
+ * `options.sink` to {@link createConsoleSink} (the snapshotted, level-routing console sink) —
1808
+ * no second colorizer. A `status('error', …)` routes to the sink's error stream.
1809
+ * - **Width-aware.** `options.width` (default {@link DEFAULT_WIDTH}) sizes `section` and a
1810
+ * `box` with no explicit width; the renderers align on VISIBLE width so styled content keeps
1811
+ * its columns.
1812
+ * - **Event-free (§13).** The reporter carries no emitter — a pure formatting front-end, like
1813
+ * the renderers and `Scheduler`. Reach for a {@link createLogger} when you need observable,
1814
+ * leveled, transportable records instead.
1815
+ *
1816
+ * @example
1817
+ * ```ts
1818
+ * import { createReporter } from '@src/core'
1819
+ *
1820
+ * const reporter = createReporter()
1821
+ * reporter.section('Build')
1822
+ * reporter.step('bundling', { index: 2, total: 5 }) // [2/5] bundling
1823
+ * reporter.status('success', 'built in 1.2s') // ✔ built in 1.2s
1824
+ *
1825
+ * // Disable color (a non-TTY) — every line is plain.
1826
+ * const plain = createReporter({ styler: createStyler({ enabled: false }) })
1827
+ * ```
1828
+ */
1829
+ function createReporter(options) {
1830
+ return new Reporter(options);
1831
+ }
1832
+ /**
1833
+ * Create an observable {@link CaptureInterface} — console interception on the READ side. While
1834
+ * `active`, every configured `console.*` call is captured as a frozen
1835
+ * {@link import('./types.js').CapturedMessage}, buffered (total + by level, bounded), emitted on
1836
+ * `capture`, and — per options — mirrored to the real console and/or forwarded to a
1837
+ * {@link SinkInterface}.
1838
+ *
1839
+ * @param options - See {@link CaptureOptions}
1840
+ * @returns A {@link CaptureInterface} (inactive until `start()`)
1841
+ *
1842
+ * @remarks
1843
+ * - **Snapshot-at-start — no capture loop.** `start()` snapshots the CURRENT `console[level]` per
1844
+ * configured level, then patches; the mirror writes through that snapshot. Our OWN console sink
1845
+ * output (the Logger / Reporter, which snapshot `console` at creation) is never recaptured —
1846
+ * `Capture` catches THIRD-PARTY `console.*`, not our writes. Create your loggers FIRST.
1847
+ * - **PROCESS-GLOBAL + NON-REENTRANT.** It patches the one global `console`, so at most ONE
1848
+ * capture may be active at a time; two concurrent captures interleave and clobber each other's
1849
+ * restore. Prefer {@link withCapture} for a scoped, self-restoring capture.
1850
+ * - **Bounded.** `options.limit` (default {@link import('./constants.js').DEFAULT_CAPTURE_LIMIT})
1851
+ * caps both the total buffer and each by-level bucket; never unbounded.
1852
+ *
1853
+ * @example
1854
+ * ```ts
1855
+ * import { createCapture } from '@src/core'
1856
+ *
1857
+ * const capture = createCapture({ levels: ['warn', 'error'] })
1858
+ * capture.start()
1859
+ * console.error('boom') // captured, NOT mirrored (mirror defaults to false)
1860
+ * capture.messages('error') // [{ level: 'error', text: 'boom', time: … }]
1861
+ * capture.stop()
1862
+ * ```
1863
+ */
1864
+ function createCapture(options) {
1865
+ return new Capture(options);
1866
+ }
1867
+ /**
1868
+ * Run `fn` with the global `console.*` captured for its duration, returning the function's `value`
1869
+ * plus the {@link import('./types.js').CapturedMessage}s it logged — the scoped, self-restoring
1870
+ * ergonomic form of {@link createCapture}.
1871
+ *
1872
+ * @param fn - The function to run under capture; may be sync (returns `T`) or async (returns
1873
+ * `Promise<T>`)
1874
+ * @param options - See {@link CaptureOptions} (`levels` / `mirror` / `sink` / `limit` / `on` /
1875
+ * `error`); the capture is started for the duration of `fn` regardless
1876
+ * @returns For a sync `fn`, a {@link CaptureResult}`<T>` (`{ value, messages }`); for an async
1877
+ * `fn`, a `Promise<CaptureResult<T>>` (awaited, then console restored)
1878
+ *
1879
+ * @remarks
1880
+ * - **Always restores.** `start()` runs before `fn`; `stop()` runs in a `finally`, so `console` is
1881
+ * restored even if `fn` throws / rejects (the throw / rejection still propagates). The capture
1882
+ * is local — created, used, and destroyed within the call.
1883
+ * - **Sync vs async.** A `fn` returning a `Promise` is detected and AWAITED before `stop()`, so
1884
+ * captures during the async work are included; a plain `fn` stops synchronously. The return type
1885
+ * follows `fn`'s (overloaded).
1886
+ * - **PROCESS-GLOBAL caveat.** Like {@link createCapture}, this patches the one global `console`.
1887
+ * Concurrent `withCapture` calls (or a `withCapture` around other capturing code) INTERLEAVE —
1888
+ * each captures every `console.*` call in flight, and the inner `stop()` restores whatever the
1889
+ * outer had installed. Use it for sequential, scoped capture, not overlapping captures.
1890
+ *
1891
+ * @example
1892
+ * ```ts
1893
+ * import { withCapture } from '@src/core'
1894
+ *
1895
+ * const { value, messages } = withCapture(() => {
1896
+ * console.log('working')
1897
+ * return 42
1898
+ * })
1899
+ * value // 42
1900
+ * messages.map((m) => m.text) // ['working']
1901
+ *
1902
+ * // Async — awaited before console is restored.
1903
+ * const out = await withCapture(async () => {
1904
+ * console.warn('async noise')
1905
+ * return 'done'
1906
+ * })
1907
+ * out.value // 'done'
1908
+ * ```
1909
+ */
1910
+ function withCapture(fn, options) {
1911
+ const capture = new Capture(options);
1912
+ capture.start();
1913
+ const settle = (value) => {
1914
+ const messages = capture.messages();
1915
+ capture.destroy();
1916
+ return {
1917
+ value,
1918
+ messages
1919
+ };
1920
+ };
1921
+ try {
1922
+ const result = fn();
1923
+ if (result instanceof Promise) return result.then((value) => settle(value), (error) => {
1924
+ capture.destroy();
1925
+ throw error;
1926
+ });
1927
+ return settle(result);
1928
+ } catch (error) {
1929
+ capture.destroy();
1930
+ throw error;
1931
+ }
1932
+ }
1933
+ /**
1934
+ * Create a self-driving, observable {@link SpinnerInterface} — a live activity spinner. `start()`
1935
+ * arms a periodic timer that advances a glyph cycle, writing each `\r` + frame line to its sink and
1936
+ * emitting it on `frame`; `success` / `failure` commit a final `✔` / `✖` line. The leading `\r` is the
1937
+ * sink's to redraw on — a TTY sink (C-g) overwrites for a smooth animation, a plain sink (C-f)
1938
+ * degrades to a fresh line.
1939
+ *
1940
+ * @param options - See {@link SpinnerOptions}
1941
+ * @returns A {@link SpinnerInterface} (inactive until `start()`)
1942
+ *
1943
+ * @remarks
1944
+ * - **Universal + leak-free.** Built on `setInterval` + the one styler + the one sink (no `node:*`,
1945
+ * no `process.stdout`); the timer is ALWAYS cleared on `success` / `failure` / `stop` / `destroy`, so
1946
+ * it never leaks. `start()` is idempotent (no second timer while `active`).
1947
+ * - **Observable (§13).** Subscribe `spinner.emitter.on('frame', …)` to mirror the animation without
1948
+ * a terminal; `start` / `stop` bracket the timer lifecycle. `options.sink` defaults to
1949
+ * {@link createConsoleSink}, `options.styler` to {@link createStyler} (ANSI).
1950
+ *
1951
+ * @example
1952
+ * ```ts
1953
+ * import { createSpinner } from '@src/core'
1954
+ *
1955
+ * const spinner = createSpinner({ message: 'building' })
1956
+ * spinner.start()
1957
+ * spinner.success('built in 1.2s') // ✔ built in 1.2s — timer cleared, line committed
1958
+ * ```
1959
+ */
1960
+ function createSpinner(options) {
1961
+ return new Spinner(options);
1962
+ }
1963
+ /**
1964
+ * Create an update-driven, observable {@link ProgressInterface} — a live progress bar. Each
1965
+ * `update(current)` recomputes the bar, writes `\r` + bar to its sink, and emits `{ current, total }`
1966
+ * on `update`; `complete` / `failure` commit a final line. The leading `\r` is the sink's to redraw on —
1967
+ * a TTY sink (C-g) overwrites, a plain sink (C-f) degrades to a fresh line. NO self-timer — the caller
1968
+ * drives the bar.
1969
+ *
1970
+ * @param options - See {@link ProgressOptions} (`total` is required)
1971
+ * @returns A {@link ProgressInterface}
1972
+ *
1973
+ * @remarks
1974
+ * - **Universal + update-driven.** Built on the one styler + the one sink (no `node:*`, no
1975
+ * `process.stdout`); progress advances only when the caller reports it. `current` is always clamped
1976
+ * to `[0, total]`. `options.sink` defaults to {@link createConsoleSink}, `options.styler` to
1977
+ * {@link createStyler} (ANSI).
1978
+ * - **Observable (§13).** Subscribe `progress.emitter.on('update', …)` to mirror progress without a
1979
+ * terminal; `complete` signals a successful finish.
1980
+ *
1981
+ * @example
1982
+ * ```ts
1983
+ * import { createProgress } from '@src/core'
1984
+ *
1985
+ * const progress = createProgress({ total: 100, message: 'downloading' })
1986
+ * progress.update(40)
1987
+ * progress.complete('done')
1988
+ * ```
1989
+ */
1990
+ function createProgress(options) {
1991
+ return new Progress(options);
1992
+ }
1993
+ //#endregion
1994
+ //#region src/core/Logger.ts
1995
+ /**
1996
+ * An observable, leveled logger (AGENTS §13) — the entry point into the structured-logging
1997
+ * pipeline. Each `debug` / `info` / `warn` / `error` call builds a frozen {@link LogRecord},
1998
+ * gates it by severity, retains a bounded tail of accepted records, ALWAYS emits it on
1999
+ * `entry` (the transport seam), and — unless `silent` — formats it into a styled line and
2000
+ * writes it to its {@link SinkInterface}.
2001
+ *
2002
+ * @remarks
2003
+ * - **Record + event = transport (§13).** An accepted record is frozen and emitted on
2004
+ * `entry` BEFORE anything else observable — every file / JSON / remote transport rides
2005
+ * `emitter.on('entry')`. The event fires even when `silent`: silence suppresses only the
2006
+ * SINK WRITE, never the record or the event, so transports keep flowing.
2007
+ * - **Leveled gate.** A record whose {@link LogLevel} is below the logger's `level` threshold
2008
+ * is dropped ENTIRELY — no record built past the level check, no event, no retention, no
2009
+ * write (see {@link meetsLevel}).
2010
+ * - **Bounded retention.** Accepted records accrue in a ring capped at `limit` (default
2011
+ * {@link DEFAULT_LOG_LIMIT}); the oldest is dropped when full. `entries()` returns a copy,
2012
+ * oldest first; `clear()` empties it. NEVER unbounded (scsr's leak).
2013
+ * - **Styled write, orthogonal to level.** The line ({@link formatRecord}) is colored through
2014
+ * the injected `styler` (the ANSI default, or a browser `%c` styler at C-f) — a level only
2015
+ * chooses a label color; styling is not a level. A disabled styler yields a plain line.
2016
+ * - **Snapshotted sink.** The default {@link createConsoleSink} writes to the `console`
2017
+ * methods captured at creation, so a later `Capture` patching `console` can't loop the
2018
+ * sink's output back into itself.
2019
+ *
2020
+ * @example
2021
+ * ```ts
2022
+ * const logger = new Logger({ name: 'http', level: 'info' })
2023
+ * logger.emitter.on('entry', (record) => archive(record)) // transport hook
2024
+ * logger.info('request', { method: 'GET', path: '/' }) // styled line to the console sink
2025
+ * logger.debug('verbose') // dropped — below the `info` threshold
2026
+ * logger.entries() // [the info record]
2027
+ * ```
2028
+ */
2029
+ var Logger = class {
2030
+ #emitter;
2031
+ #level;
2032
+ #name;
2033
+ #sink;
2034
+ #styler;
2035
+ #limit;
2036
+ #silent;
2037
+ #entries = [];
2038
+ constructor(options) {
2039
+ this.#emitter = new _orkestrel_emitter.Emitter({
2040
+ on: options?.on,
2041
+ error: options?.error
2042
+ });
2043
+ this.#level = options?.level ?? "info";
2044
+ this.#name = options?.name;
2045
+ this.#sink = options?.sink ?? createConsoleSink();
2046
+ this.#styler = options?.styler ?? createStyler();
2047
+ this.#limit = options?.limit ?? 1e3;
2048
+ this.#silent = options?.silent ?? false;
2049
+ }
2050
+ get emitter() {
2051
+ return this.#emitter;
2052
+ }
2053
+ get level() {
2054
+ return this.#level;
2055
+ }
2056
+ get name() {
2057
+ return this.#name;
2058
+ }
2059
+ debug(message, data) {
2060
+ this.#log("debug", message, data);
2061
+ }
2062
+ info(message, data) {
2063
+ this.#log("info", message, data);
2064
+ }
2065
+ warn(message, data) {
2066
+ this.#log("warn", message, data);
2067
+ }
2068
+ error(message, data) {
2069
+ this.#log("error", message, data);
2070
+ }
2071
+ entries() {
2072
+ return [...this.#entries];
2073
+ }
2074
+ clear() {
2075
+ this.#entries.length = 0;
2076
+ }
2077
+ destroy() {
2078
+ this.#entries.length = 0;
2079
+ this.#emitter.destroy();
2080
+ }
2081
+ #log(level, message, data) {
2082
+ if (!meetsLevel(this.#level, level)) return;
2083
+ const record = this.#record(level, message, data);
2084
+ this.#retain(record);
2085
+ this.#emitter.emit("entry", record);
2086
+ if (this.#silent) return;
2087
+ this.#sink.write(formatRecord(record, this.#styler), level);
2088
+ }
2089
+ #record(level, message, data) {
2090
+ return Object.freeze({
2091
+ level,
2092
+ message,
2093
+ time: Date.now(),
2094
+ ...this.#name === void 0 ? {} : { name: this.#name },
2095
+ ...data === void 0 ? {} : { data: Object.freeze({ ...data }) }
2096
+ });
2097
+ }
2098
+ #retain(record) {
2099
+ this.#entries.push(record);
2100
+ if (this.#entries.length > this.#limit) this.#entries.shift();
2101
+ }
2102
+ };
2103
+ //#endregion
2104
+ exports.ANSIRenderer = ANSIRenderer;
2105
+ exports.ANSI_PATTERN = ANSI_PATTERN;
2106
+ exports.ATTRIBUTES = ATTRIBUTES;
2107
+ exports.ATTRIBUTE_CODES = ATTRIBUTE_CODES;
2108
+ exports.BACKGROUND_CODES = BACKGROUND_CODES;
2109
+ exports.BAR_EMPTY = BAR_EMPTY;
2110
+ exports.BAR_FILL = BAR_FILL;
2111
+ exports.BEL = BEL;
2112
+ exports.BORDER_CHARS = BORDER_CHARS;
2113
+ exports.CAPTURE_LEVELS = CAPTURE_LEVELS;
2114
+ exports.CAPTURE_LEVEL_MAP = CAPTURE_LEVEL_MAP;
2115
+ exports.COLORS = COLORS;
2116
+ exports.CONTROL_PATTERN = CONTROL_PATTERN;
2117
+ exports.CSI = CSI;
2118
+ exports.Capture = Capture;
2119
+ exports.ConsoleError = ConsoleError;
2120
+ exports.DEFAULT_ALIGN = DEFAULT_ALIGN;
2121
+ exports.DEFAULT_BAR_WIDTH = DEFAULT_BAR_WIDTH;
2122
+ exports.DEFAULT_BORDER = DEFAULT_BORDER;
2123
+ exports.DEFAULT_CAPTURE_LEVELS = DEFAULT_CAPTURE_LEVELS;
2124
+ exports.DEFAULT_CAPTURE_LIMIT = DEFAULT_CAPTURE_LIMIT;
2125
+ exports.DEFAULT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
2126
+ exports.DEFAULT_LOG_LIMIT = DEFAULT_LOG_LIMIT;
2127
+ exports.DEFAULT_PADDING = DEFAULT_PADDING;
2128
+ exports.DEFAULT_SPINNER_INTERVAL = DEFAULT_SPINNER_INTERVAL;
2129
+ exports.DEFAULT_WIDTH = DEFAULT_WIDTH;
2130
+ exports.EMPTY_STYLE = EMPTY_STYLE;
2131
+ exports.ESC = ESC;
2132
+ exports.FOREGROUND_CODES = FOREGROUND_CODES;
2133
+ exports.LEVELS = LEVELS;
2134
+ exports.LEVEL_COLORS = LEVEL_COLORS;
2135
+ exports.LEVEL_SEVERITY = LEVEL_SEVERITY;
2136
+ exports.Logger = Logger;
2137
+ exports.LoggerManager = LoggerManager;
2138
+ exports.Progress = Progress;
2139
+ exports.RESET = RESET;
2140
+ exports.RESET_CODE = RESET_CODE;
2141
+ exports.Reporter = Reporter;
2142
+ exports.SECOND_MS = SECOND_MS;
2143
+ exports.SEPARATOR_FILL = SEPARATOR_FILL;
2144
+ exports.SEPARATOR_TITLE_GAP = SEPARATOR_TITLE_GAP;
2145
+ exports.SPINNER_FRAMES = SPINNER_FRAMES;
2146
+ exports.STATUS_COLORS = STATUS_COLORS;
2147
+ exports.STATUS_ICONS = STATUS_ICONS;
2148
+ exports.STATUS_LEVELS = STATUS_LEVELS;
2149
+ exports.Spinner = Spinner;
2150
+ exports.Styler = Styler;
2151
+ exports.TREE_CHARS = TREE_CHARS;
2152
+ exports.align = align;
2153
+ exports.cellAt = cellAt;
2154
+ exports.createANSIRenderer = createANSIRenderer;
2155
+ exports.createCapture = createCapture;
2156
+ exports.createConsoleSink = createConsoleSink;
2157
+ exports.createLogger = createLogger;
2158
+ exports.createLoggerManager = createLoggerManager;
2159
+ exports.createProgress = createProgress;
2160
+ exports.createReporter = createReporter;
2161
+ exports.createSpinner = createSpinner;
2162
+ exports.createStyler = createStyler;
2163
+ exports.formatArgs = formatArgs;
2164
+ exports.formatDuration = formatDuration;
2165
+ exports.formatRecord = formatRecord;
2166
+ exports.formatTime = formatTime;
2167
+ exports.isConsoleError = isConsoleError;
2168
+ exports.meetsLevel = meetsLevel;
2169
+ exports.paint = paint;
2170
+ exports.renderBar = renderBar;
2171
+ exports.renderBox = renderBox;
2172
+ exports.renderSeparator = renderSeparator;
2173
+ exports.renderTable = renderTable;
2174
+ exports.renderTree = renderTree;
2175
+ exports.renderTreeChildren = renderTreeChildren;
2176
+ exports.repeatTo = repeatTo;
2177
+ exports.stringifyValue = stringifyValue;
2178
+ exports.strip = strip;
2179
+ exports.stripControls = stripControls;
2180
+ exports.width = width;
2181
+ exports.withCapture = withCapture;
2182
+
2183
+ //# sourceMappingURL=index.cjs.map