@orkestrel/console 0.0.11 → 0.0.13

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