@orkestrel/console 0.0.10 → 0.0.12

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