@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.
- package/README.md +23 -21
- package/dist/src/browser/index.d.ts +85 -74
- package/dist/src/browser/index.js +69 -70
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +990 -1143
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +826 -757
- package/dist/src/core/index.d.ts +826 -757
- package/dist/src/core/index.js +987 -1134
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +130 -153
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +222 -199
- package/dist/src/server/index.d.ts +222 -199
- package/dist/src/server/index.js +130 -151
- package/dist/src/server/index.js.map +1 -1
- package/package.json +12 -13
package/dist/src/core/index.cjs
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
110
|
+
/** Holds the bell control character (`U+0007`) that can terminate an OSC sequence. */
|
|
111
111
|
var BEL = String.fromCharCode(7);
|
|
112
|
-
/**
|
|
112
|
+
/** Holds the Control Sequence Introducer (`ESC[`) that opens every SGR sequence. */
|
|
113
113
|
var CSI = `[`;
|
|
114
|
-
/**
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
160
|
-
* is a styling choice
|
|
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
|
-
*
|
|
176
|
-
* most
|
|
177
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
190
|
+
var LOG_LEVELS = Object.freeze([
|
|
188
191
|
"debug",
|
|
189
192
|
"info",
|
|
190
193
|
"warn",
|
|
191
194
|
"error"
|
|
192
195
|
]);
|
|
193
196
|
/**
|
|
194
|
-
*
|
|
195
|
-
* Unicode box-drawing glyphs at
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
269
|
-
* in (`success` green, `error` red, `warn` yellow, `info` blue). The
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
294
|
-
* {@link import('./types.js').ReporterOptions}
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
305
|
+
/** Sets the default {@link BorderStyle} the box / table renderers frame with when none is given — `single`. */
|
|
300
306
|
var DEFAULT_BORDER = "single";
|
|
301
|
-
/**
|
|
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
|
-
/**
|
|
309
|
+
/** Holds the default fill character {@link import('./helpers.js').renderSeparator} draws its rule with — `─`. */
|
|
304
310
|
var SEPARATOR_FILL = "─";
|
|
305
311
|
/**
|
|
306
|
-
*
|
|
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
|
-
*
|
|
312
|
-
* (and so `Reporter.timing`) switches from a `…ms` rendering to a `…s` (seconds, 2 d.p.)
|
|
313
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
*
|
|
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
|
-
*
|
|
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}
|
|
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
|
-
*
|
|
360
|
-
* the
|
|
361
|
-
*
|
|
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
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
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
|
-
*
|
|
388
|
-
* progress bar with — the full block `█` (U+2588). A single visible cell; a consumer
|
|
389
|
-
* {@link import('./types.js').
|
|
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
|
-
*
|
|
394
|
-
* progress bar with — the light-shade block `░` (U+2591). A single visible cell; a
|
|
395
|
-
* it
|
|
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
|
-
*
|
|
400
|
-
* import('./helpers.js').renderBar} fills
|
|
401
|
-
* its bar to
|
|
402
|
-
*
|
|
403
|
-
*
|
|
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
|
-
*
|
|
408
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
481
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
529
|
+
* Removes every ANSI escape sequence from `text`, returning the plain visible string.
|
|
637
530
|
*
|
|
638
531
|
* @remarks
|
|
639
|
-
* Strips SGR color/style codes
|
|
640
|
-
* sequences (titles, hyperlinks) — see {@link ANSI_PATTERN}. A
|
|
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
|
-
*
|
|
657
|
-
* (meaningful whitespace), plus DEL —
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 (
|
|
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
|
|
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
|
-
*
|
|
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()`,
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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) <
|
|
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) >
|
|
800
|
-
* `
|
|
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
|
|
805
|
-
* @param alignment - Where to position `text` within
|
|
806
|
-
* @returns `text` fitted to exactly `
|
|
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,
|
|
739
|
+
function align(text, columns, alignment = DEFAULT_ALIGN) {
|
|
816
740
|
const visible = width(text);
|
|
817
|
-
if (visible >
|
|
818
|
-
const deficit =
|
|
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
|
-
*
|
|
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
|
-
*
|
|
843
|
-
* single optional-styling primitive every renderer applies to
|
|
844
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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 `
|
|
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
|
|
873
|
-
* @returns `unit` tiled to exactly `
|
|
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,
|
|
882
|
-
if (
|
|
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(
|
|
809
|
+
return [...unit.repeat(Math.ceil(columns / per))].slice(0, columns).join("");
|
|
886
810
|
}
|
|
887
811
|
/**
|
|
888
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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 (
|
|
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
|
-
*
|
|
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`
|
|
939
|
-
* exactly as the same text written on POSIX; 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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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})
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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]'`);
|
|
1104
|
-
* `String(value)`. So a `Capture` can never crash the program whose `console.*` it
|
|
1105
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1163
|
-
* and the `(current/total)` count (`█████░░░░░ 50% (5/10)`). Pure: same
|
|
1164
|
-
* same string.
|
|
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
|
|
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
|
|
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
|
|
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` (
|
|
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
|
|
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
|
-
*
|
|
1273
|
-
* SGR escape codes,
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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/
|
|
1175
|
+
//#region src/core/Retention.ts
|
|
1313
1176
|
/**
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
*
|
|
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
|
-
* - **
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
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
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
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
|
|
1347
|
-
#loggers = /* @__PURE__ */ new Map();
|
|
1348
|
-
#level;
|
|
1349
|
-
#sink;
|
|
1350
|
-
#styler;
|
|
1351
|
-
#theme;
|
|
1352
|
-
#format;
|
|
1205
|
+
var Retention = class {
|
|
1353
1206
|
#limit;
|
|
1354
|
-
#
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
this.#
|
|
1358
|
-
this.#
|
|
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
|
-
|
|
1392
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
1218
|
+
records(level) {
|
|
1219
|
+
if (level === void 0) return [...this.#records];
|
|
1220
|
+
return [...this.#buckets.get(level) ?? []];
|
|
1396
1221
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1222
|
+
clear() {
|
|
1223
|
+
this.#records.length = 0;
|
|
1224
|
+
for (const bucket of this.#buckets.values()) bucket.length = 0;
|
|
1399
1225
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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/
|
|
1232
|
+
//#region src/core/Capture.ts
|
|
1415
1233
|
/**
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
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
|
-
* - **
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1433
|
-
* - **
|
|
1434
|
-
*
|
|
1435
|
-
* @
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
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
|
|
1264
|
+
var Capture = class {
|
|
1444
1265
|
#emitter;
|
|
1445
|
-
#
|
|
1446
|
-
#
|
|
1447
|
-
#fill;
|
|
1448
|
-
#empty;
|
|
1266
|
+
#levels;
|
|
1267
|
+
#mirror;
|
|
1449
1268
|
#sink;
|
|
1450
|
-
#
|
|
1451
|
-
#
|
|
1452
|
-
#
|
|
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
|
|
1459
|
-
...options
|
|
1274
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
1275
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
1460
1276
|
});
|
|
1461
|
-
this.#
|
|
1462
|
-
this.#
|
|
1463
|
-
this.#
|
|
1464
|
-
this.#
|
|
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
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1300
|
+
stop() {
|
|
1491
1301
|
if (!this.#active) return;
|
|
1492
|
-
this.#advance(this.#total, message);
|
|
1493
1302
|
this.#active = false;
|
|
1494
|
-
|
|
1495
|
-
this.#
|
|
1496
|
-
this.#
|
|
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
|
-
|
|
1499
|
-
if (
|
|
1500
|
-
this.#
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
#
|
|
1508
|
-
this.#
|
|
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
|
-
#
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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/
|
|
1340
|
+
//#region src/core/Styler.ts
|
|
1531
1341
|
/**
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
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
|
-
* - **
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
*
|
|
1547
|
-
*
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
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
|
|
1562
|
-
#
|
|
1563
|
-
#
|
|
1564
|
-
#
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
this.#
|
|
1568
|
-
this.#
|
|
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
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
1582
|
-
this.#
|
|
1380
|
+
/** Reports whether styling is applied; when `false`, the surface returns text unchanged. */
|
|
1381
|
+
get enabled() {
|
|
1382
|
+
return this.#enabled;
|
|
1583
1383
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
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
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
-
|
|
1593
|
-
this.#
|
|
1447
|
+
#render(text) {
|
|
1448
|
+
return this.#enabled ? this.#renderer.render(this.#style, text) : text;
|
|
1594
1449
|
}
|
|
1595
|
-
|
|
1596
|
-
this.#
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
-
*
|
|
1881
|
-
* renderer
|
|
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
|
-
*
|
|
1892
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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 '@
|
|
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
|
-
*
|
|
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
|
|
1940
|
-
* `statuses` merge per
|
|
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 '@
|
|
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
|
|
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
|
-
*
|
|
1978
|
-
* through the `console` methods
|
|
1979
|
-
* {@link
|
|
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`
|
|
1986
|
-
* `Capture`
|
|
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)
|
|
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 '@
|
|
1585
|
+
* import { createConsoleSink } from '@orkestrel/console'
|
|
1996
1586
|
*
|
|
1997
|
-
* const sink = createConsoleSink() // snapshots console.*
|
|
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
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
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
|
-
*
|
|
2079
|
-
*
|
|
2080
|
-
*
|
|
2081
|
-
* the
|
|
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
|
-
* - **
|
|
2088
|
-
* `
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2095
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
2102
|
-
*
|
|
2103
|
-
*
|
|
2104
|
-
*
|
|
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
|
-
*
|
|
2107
|
-
*
|
|
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
|
-
|
|
2111
|
-
|
|
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
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
* {@link
|
|
2117
|
-
*
|
|
2118
|
-
*
|
|
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
|
-
* - **
|
|
2125
|
-
*
|
|
2126
|
-
*
|
|
2127
|
-
*
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
2130
|
-
*
|
|
2131
|
-
*
|
|
2132
|
-
*
|
|
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
|
-
*
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2140
|
-
*
|
|
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
|
-
|
|
2146
|
-
|
|
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
|
-
*
|
|
2150
|
-
*
|
|
2151
|
-
*
|
|
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
|
-
*
|
|
2160
|
-
*
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
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
|
-
*
|
|
2189
|
-
*
|
|
2190
|
-
*
|
|
2191
|
-
*
|
|
2192
|
-
*
|
|
2193
|
-
* - **
|
|
2194
|
-
*
|
|
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
|
-
*
|
|
2199
|
-
*
|
|
2200
|
-
*
|
|
2201
|
-
*
|
|
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
|
-
|
|
2206
|
-
|
|
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/
|
|
2053
|
+
//#region src/core/Progress.ts
|
|
2210
2054
|
/**
|
|
2211
|
-
*
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
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
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
*
|
|
2221
|
-
*
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
*
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2229
|
-
* the
|
|
2230
|
-
*
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
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
|
|
2238
|
-
*
|
|
2239
|
-
*
|
|
2240
|
-
*
|
|
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
|
|
2085
|
+
var Progress = class {
|
|
2245
2086
|
#emitter;
|
|
2246
|
-
#
|
|
2087
|
+
#total;
|
|
2088
|
+
#width;
|
|
2089
|
+
#fill;
|
|
2090
|
+
#empty;
|
|
2247
2091
|
#sink;
|
|
2248
2092
|
#styler;
|
|
2249
2093
|
#theme;
|
|
2250
|
-
#
|
|
2251
|
-
#
|
|
2252
|
-
#
|
|
2253
|
-
#
|
|
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
|
|
2258
|
-
...options
|
|
2100
|
+
...options.on !== void 0 ? { on: options.on } : {},
|
|
2101
|
+
...options.error !== void 0 ? { error: options.error } : {}
|
|
2259
2102
|
});
|
|
2260
|
-
this.#
|
|
2261
|
-
|
|
2262
|
-
this.#
|
|
2263
|
-
this.#
|
|
2264
|
-
this.#
|
|
2265
|
-
this.#
|
|
2266
|
-
this.#
|
|
2267
|
-
this.#
|
|
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
|
|
2273
|
-
return this.#
|
|
2115
|
+
get active() {
|
|
2116
|
+
return this.#active;
|
|
2274
2117
|
}
|
|
2275
|
-
|
|
2276
|
-
this.#
|
|
2118
|
+
get succeeded() {
|
|
2119
|
+
return this.#succeeded;
|
|
2277
2120
|
}
|
|
2278
|
-
|
|
2279
|
-
this.#
|
|
2121
|
+
get current() {
|
|
2122
|
+
return this.#current;
|
|
2280
2123
|
}
|
|
2281
|
-
|
|
2282
|
-
this.#
|
|
2124
|
+
get total() {
|
|
2125
|
+
return this.#total;
|
|
2283
2126
|
}
|
|
2284
|
-
|
|
2285
|
-
this.#
|
|
2127
|
+
update(current, message) {
|
|
2128
|
+
if (!this.#active) return;
|
|
2129
|
+
this.#advance(current, message);
|
|
2130
|
+
this.#paint(false);
|
|
2286
2131
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
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
|
-
|
|
2291
|
-
this.#
|
|
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
|
-
#
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
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
|
-
#
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
...this
|
|
2311
|
-
...
|
|
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
|
-
|
|
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.
|
|
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
|