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