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