@orkestrel/console 0.0.5 → 0.0.7
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/dist/src/browser/index.d.ts +32 -17
- package/dist/src/browser/index.js +19 -24
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +596 -394
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +224 -77
- package/dist/src/core/index.d.ts +224 -77
- package/dist/src/core/index.js +594 -393
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +47 -13
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +50 -19
- package/dist/src/server/index.d.ts +50 -19
- package/dist/src/server/index.js +47 -14
- package/dist/src/server/index.js.map +1 -1
- package/package.json +6 -4
package/dist/src/core/index.cjs
CHANGED
|
@@ -288,17 +288,6 @@ var STATUS_LEVELS = Object.freeze([
|
|
|
288
288
|
"info"
|
|
289
289
|
]);
|
|
290
290
|
/**
|
|
291
|
-
* The tree connectors {@link import('./helpers.js').renderTree} draws — the `├─` branch (a
|
|
292
|
-
* non-last child), the `└─` corner (the last child), the `│ ` guide (carried down through an
|
|
293
|
-
* earlier branch's descendants), and the ` ` gap (under a last branch). Frozen.
|
|
294
|
-
*/
|
|
295
|
-
var TREE_CHARS = Object.freeze({
|
|
296
|
-
branch: "├─ ",
|
|
297
|
-
corner: "└─ ",
|
|
298
|
-
guide: "│ ",
|
|
299
|
-
gap: " "
|
|
300
|
-
});
|
|
301
|
-
/**
|
|
302
291
|
* The default visible column width for the width-aware renderers — the separator rule and a
|
|
303
292
|
* {@link import('./helpers.js').renderBox} with no explicit `width`, and the reporter's
|
|
304
293
|
* `section` rule. A sane terminal default (80 columns); a caller overrides it per-call or via
|
|
@@ -414,6 +403,74 @@ var BAR_EMPTY = "░";
|
|
|
414
403
|
* track is one inline element, not a full-width rule.
|
|
415
404
|
*/
|
|
416
405
|
var DEFAULT_BAR_WIDTH = 30;
|
|
406
|
+
/**
|
|
407
|
+
* The default {@link Theme} — every role bound to its default {@link Style}, deeply frozen.
|
|
408
|
+
* The base {@link import('./factories.js').createTheme} merges over, and the theme every
|
|
409
|
+
* entity uses when none is supplied.
|
|
410
|
+
*
|
|
411
|
+
* @remarks
|
|
412
|
+
* - `levels` — each {@link LogLevel} label in its {@link LEVEL_COLORS} color, no attributes.
|
|
413
|
+
* - `statuses` — each {@link StatusLevel}'s {@link STATUS_ICONS} glyph in its
|
|
414
|
+
* {@link STATUS_COLORS} color.
|
|
415
|
+
* - `accent` — `cyan`: the spinner glyph, the progress fill, a step prefix.
|
|
416
|
+
* - `chrome` — `dim`: separators, box / table / tree frames, and a log line's timestamp /
|
|
417
|
+
* name / data surround. A color-free attribute, so chrome recedes on any background.
|
|
418
|
+
*/
|
|
419
|
+
var DEFAULT_THEME = Object.freeze({
|
|
420
|
+
levels: Object.freeze({
|
|
421
|
+
debug: Object.freeze({
|
|
422
|
+
foreground: LEVEL_COLORS.debug,
|
|
423
|
+
attributes: EMPTY_STYLE.attributes
|
|
424
|
+
}),
|
|
425
|
+
info: Object.freeze({
|
|
426
|
+
foreground: LEVEL_COLORS.info,
|
|
427
|
+
attributes: EMPTY_STYLE.attributes
|
|
428
|
+
}),
|
|
429
|
+
warn: Object.freeze({
|
|
430
|
+
foreground: LEVEL_COLORS.warn,
|
|
431
|
+
attributes: EMPTY_STYLE.attributes
|
|
432
|
+
}),
|
|
433
|
+
error: Object.freeze({
|
|
434
|
+
foreground: LEVEL_COLORS.error,
|
|
435
|
+
attributes: EMPTY_STYLE.attributes
|
|
436
|
+
})
|
|
437
|
+
}),
|
|
438
|
+
statuses: Object.freeze({
|
|
439
|
+
success: Object.freeze({
|
|
440
|
+
icon: STATUS_ICONS.success,
|
|
441
|
+
style: Object.freeze({
|
|
442
|
+
foreground: STATUS_COLORS.success,
|
|
443
|
+
attributes: EMPTY_STYLE.attributes
|
|
444
|
+
})
|
|
445
|
+
}),
|
|
446
|
+
error: Object.freeze({
|
|
447
|
+
icon: STATUS_ICONS.error,
|
|
448
|
+
style: Object.freeze({
|
|
449
|
+
foreground: STATUS_COLORS.error,
|
|
450
|
+
attributes: EMPTY_STYLE.attributes
|
|
451
|
+
})
|
|
452
|
+
}),
|
|
453
|
+
warn: Object.freeze({
|
|
454
|
+
icon: STATUS_ICONS.warn,
|
|
455
|
+
style: Object.freeze({
|
|
456
|
+
foreground: STATUS_COLORS.warn,
|
|
457
|
+
attributes: EMPTY_STYLE.attributes
|
|
458
|
+
})
|
|
459
|
+
}),
|
|
460
|
+
info: Object.freeze({
|
|
461
|
+
icon: STATUS_ICONS.info,
|
|
462
|
+
style: Object.freeze({
|
|
463
|
+
foreground: STATUS_COLORS.info,
|
|
464
|
+
attributes: EMPTY_STYLE.attributes
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
}),
|
|
468
|
+
accent: Object.freeze({
|
|
469
|
+
foreground: "cyan",
|
|
470
|
+
attributes: EMPTY_STYLE.attributes
|
|
471
|
+
}),
|
|
472
|
+
chrome: Object.freeze({ attributes: Object.freeze(["dim"]) })
|
|
473
|
+
});
|
|
417
474
|
//#endregion
|
|
418
475
|
//#region src/core/errors.ts
|
|
419
476
|
/**
|
|
@@ -453,6 +510,127 @@ function isConsoleError(value) {
|
|
|
453
510
|
return value instanceof ConsoleError;
|
|
454
511
|
}
|
|
455
512
|
//#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
|
|
456
634
|
//#region src/core/helpers.ts
|
|
457
635
|
/**
|
|
458
636
|
* Remove every ANSI escape sequence from `text`, returning the plain visible string.
|
|
@@ -519,6 +697,27 @@ function width(text) {
|
|
|
519
697
|
return [...strip(text)].length;
|
|
520
698
|
}
|
|
521
699
|
/**
|
|
700
|
+
* Snapshot and deeply freeze one {@link Style} value.
|
|
701
|
+
*
|
|
702
|
+
* @param style - The caller-owned style to snapshot
|
|
703
|
+
* @returns A frozen style record with an independently frozen attributes list
|
|
704
|
+
*
|
|
705
|
+
* @remarks
|
|
706
|
+
* The record spread captures accessor values once and preserves each present color channel.
|
|
707
|
+
* Copying `attributes` prevents later mutation of a caller-owned list from changing the result.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* ```ts
|
|
711
|
+
* freezeStyle({ foreground: 'red', attributes: ['bold'] })
|
|
712
|
+
* ```
|
|
713
|
+
*/
|
|
714
|
+
function freezeStyle(style) {
|
|
715
|
+
return Object.freeze({
|
|
716
|
+
...style,
|
|
717
|
+
attributes: Object.freeze([...style.attributes])
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
522
721
|
* Whether a record at `level` passes a logger gated at `threshold` — i.e. its severity is
|
|
523
722
|
* at or above the threshold's.
|
|
524
723
|
*
|
|
@@ -560,7 +759,7 @@ function formatTime(time) {
|
|
|
560
759
|
*
|
|
561
760
|
* @remarks
|
|
562
761
|
* Layout: `{time} {LEVEL} {[name]} {message}{ data}` — the ISO timestamp (dimmed), the
|
|
563
|
-
* upper-cased level label (
|
|
762
|
+
* upper-cased level label (rendered through the theme's level role),
|
|
564
763
|
* the originating logger's `name` in brackets (omitted when absent), the message, and the
|
|
565
764
|
* structured `data` appended as compact JSON (omitted when absent / empty). Coloring flows
|
|
566
765
|
* through the injected `styler`, so a disabled styler yields a plain line and a browser
|
|
@@ -569,19 +768,24 @@ function formatTime(time) {
|
|
|
569
768
|
*
|
|
570
769
|
* @param record - The {@link LogRecord} to render
|
|
571
770
|
* @param styler - The {@link StylerInterface} the labels are colored through
|
|
771
|
+
* @param theme - The {@link Theme} supplying the level and chrome roles
|
|
572
772
|
* @returns The formatted, styled line (no trailing newline — the sink's target adds it)
|
|
573
773
|
*
|
|
574
774
|
* @example
|
|
575
775
|
* ```ts
|
|
576
|
-
* formatRecord(
|
|
776
|
+
* formatRecord(
|
|
777
|
+
* { level: 'warn', message: 'low disk', time: 0, name: 'fs' },
|
|
778
|
+
* createStyler(),
|
|
779
|
+
* DEFAULT_THEME,
|
|
780
|
+
* )
|
|
577
781
|
* // '<dim>1970-01-01T00:00:00.000Z</> <yellow>WARN</> [fs] low disk'
|
|
578
782
|
* ```
|
|
579
783
|
*/
|
|
580
|
-
function formatRecord(record, styler) {
|
|
581
|
-
const time = styler.
|
|
582
|
-
const label = styler[
|
|
583
|
-
const name = record.name === void 0 ? "" : ` ${styler.
|
|
584
|
-
const data = record.data === void 0 || Object.keys(record.data).length === 0 ? "" : ` ${styler.
|
|
784
|
+
function formatRecord(record, styler, theme) {
|
|
785
|
+
const time = styler.render(theme.chrome, formatTime(record.time));
|
|
786
|
+
const label = styler.render(theme.levels[record.level], record.level.toUpperCase());
|
|
787
|
+
const name = record.name === void 0 ? "" : ` ${styler.render(theme.chrome, `[${record.name}]`)}`;
|
|
788
|
+
const data = record.data === void 0 || Object.keys(record.data).length === 0 ? "" : ` ${styler.render(theme.chrome, JSON.stringify(record.data))}`;
|
|
585
789
|
return `${time} ${label}${name} ${record.message}${data}`;
|
|
586
790
|
}
|
|
587
791
|
/**
|
|
@@ -648,10 +852,12 @@ function formatDuration(ms) {
|
|
|
648
852
|
*
|
|
649
853
|
* @param styler - The {@link StylerInterface} to color with, or `undefined` for no styling
|
|
650
854
|
* @param text - The glyphs / text to color
|
|
855
|
+
* @param style - An optional {@link Style} to render by value instead of the styler's chain
|
|
651
856
|
* @returns `styler(text)` when a styler is given, else `text` unchanged
|
|
652
857
|
*/
|
|
653
|
-
function paint(styler, text) {
|
|
654
|
-
|
|
858
|
+
function paint(styler, text, style) {
|
|
859
|
+
if (styler === void 0) return text;
|
|
860
|
+
return style === void 0 ? styler(text) : styler.render(style, text);
|
|
655
861
|
}
|
|
656
862
|
/**
|
|
657
863
|
* Repeat `unit` until it fills exactly `count` VISIBLE columns, trimming a trailing partial
|
|
@@ -717,12 +923,12 @@ function cellAt(row, index) {
|
|
|
717
923
|
function renderSeparator(options) {
|
|
718
924
|
const total = options.width ?? 80;
|
|
719
925
|
const fill = options.fill ?? "─";
|
|
720
|
-
if (options.title === void 0) return paint(options.styler, repeatTo(fill, total));
|
|
721
|
-
const gapped = ` ${paint(options.styler, options.title)} `;
|
|
926
|
+
if (options.title === void 0) return paint(options.styler, repeatTo(fill, total), options.style);
|
|
927
|
+
const gapped = ` ${paint(options.styler, options.title, options.style)} `;
|
|
722
928
|
const room = total - width(options.title) - 2;
|
|
723
929
|
if (room <= 0) return gapped;
|
|
724
930
|
const left = Math.floor(room / 2);
|
|
725
|
-
return `${paint(options.styler, repeatTo(fill, left))}${gapped}${paint(options.styler, repeatTo(fill, room - left))}`;
|
|
931
|
+
return `${paint(options.styler, repeatTo(fill, left), options.style)}${gapped}${paint(options.styler, repeatTo(fill, room - left), options.style)}`;
|
|
726
932
|
}
|
|
727
933
|
/**
|
|
728
934
|
* Render `content` framed in box-drawing characters, optionally captioned, width-aware so
|
|
@@ -754,18 +960,18 @@ function renderBox(options) {
|
|
|
754
960
|
const budget = options.width === void 0 ? 0 : options.width - 2 - padding * 2;
|
|
755
961
|
const inner = lines.reduce((max, line) => Math.max(max, width(line)), Math.max(0, titleRoom, budget));
|
|
756
962
|
const gutter = " ".repeat(padding);
|
|
757
|
-
const bar = paint(styler, chars.vertical);
|
|
963
|
+
const bar = paint(styler, chars.vertical, options.style);
|
|
758
964
|
const span = inner + padding * 2;
|
|
759
965
|
let top;
|
|
760
|
-
if (options.title === void 0) top = paint(styler, `${chars.topLeft}${repeatTo(chars.horizontal, span)}${chars.topRight}
|
|
966
|
+
if (options.title === void 0) top = paint(styler, `${chars.topLeft}${repeatTo(chars.horizontal, span)}${chars.topRight}`, options.style);
|
|
761
967
|
else {
|
|
762
968
|
const caption = ` ${options.title} `;
|
|
763
969
|
const room = span - width(caption);
|
|
764
|
-
const lead = paint(styler, repeatTo(chars.horizontal, 1));
|
|
765
|
-
const rest = room - 1 <= 0 ? "" : paint(styler, repeatTo(chars.horizontal, room - 1));
|
|
766
|
-
top = `${paint(styler, chars.topLeft)}${lead}${paint(styler, caption)}${rest}${paint(styler, chars.topRight)}`;
|
|
970
|
+
const lead = paint(styler, repeatTo(chars.horizontal, 1), options.style);
|
|
971
|
+
const rest = room - 1 <= 0 ? "" : paint(styler, repeatTo(chars.horizontal, room - 1), options.style);
|
|
972
|
+
top = `${paint(styler, chars.topLeft, options.style)}${lead}${paint(styler, caption, options.style)}${rest}${paint(styler, chars.topRight, options.style)}`;
|
|
767
973
|
}
|
|
768
|
-
const bottom = paint(styler, `${chars.bottomLeft}${repeatTo(chars.horizontal, inner + padding * 2)}${chars.bottomRight}
|
|
974
|
+
const bottom = paint(styler, `${chars.bottomLeft}${repeatTo(chars.horizontal, inner + padding * 2)}${chars.bottomRight}`, options.style);
|
|
769
975
|
const body = lines.map((line) => `${bar}${gutter}${align(line, inner)}${gutter}${bar}`);
|
|
770
976
|
return [
|
|
771
977
|
top,
|
|
@@ -800,14 +1006,14 @@ function renderTable(options) {
|
|
|
800
1006
|
const columns = options.columns;
|
|
801
1007
|
const widths = columns.map((column, index) => options.rows.reduce((max, row) => Math.max(max, width(cellAt(row, index))), width(column.label)));
|
|
802
1008
|
const aligns = columns.map((column) => column.align ?? "left");
|
|
803
|
-
const renderedRows = [columns.map((column) => paint(styler, column.label)), ...options.rows.map((row) => columns.map((_column, index) => cellAt(row, index)))].map((cells) => {
|
|
804
|
-
const bar = paint(styler, chars.vertical);
|
|
1009
|
+
const renderedRows = [columns.map((column) => paint(styler, column.label, options.style)), ...options.rows.map((row) => columns.map((_column, index) => cellAt(row, index)))].map((cells) => {
|
|
1010
|
+
const bar = paint(styler, chars.vertical, options.style);
|
|
805
1011
|
return `${bar}${cells.map((cell, index) => ` ${align(cell, widths[index] ?? width(cell), aligns[index] ?? "left")} `).join(bar)}${bar}`;
|
|
806
1012
|
});
|
|
807
1013
|
const segments = widths.map((columnWidth) => repeatTo(chars.horizontal, columnWidth + 2));
|
|
808
|
-
const top = paint(styler, `${chars.topLeft}${segments.join(chars.teeDown)}${chars.topRight}
|
|
809
|
-
const rule = paint(styler, `${chars.teeRight}${segments.join(chars.cross)}${chars.teeLeft}
|
|
810
|
-
const bottom = paint(styler, `${chars.bottomLeft}${segments.join(chars.teeUp)}${chars.bottomRight}
|
|
1014
|
+
const top = paint(styler, `${chars.topLeft}${segments.join(chars.teeDown)}${chars.topRight}`, options.style);
|
|
1015
|
+
const rule = paint(styler, `${chars.teeRight}${segments.join(chars.cross)}${chars.teeLeft}`, options.style);
|
|
1016
|
+
const bottom = paint(styler, `${chars.bottomLeft}${segments.join(chars.teeUp)}${chars.bottomRight}`, options.style);
|
|
811
1017
|
return [
|
|
812
1018
|
top,
|
|
813
1019
|
...renderedRows.slice(0, 1),
|
|
@@ -822,7 +1028,7 @@ function renderTable(options) {
|
|
|
822
1028
|
*
|
|
823
1029
|
* @remarks
|
|
824
1030
|
* The `root` label is the unindented first line; its descendants are drawn beneath it with
|
|
825
|
-
* {@link
|
|
1031
|
+
* {@link BORDER_CHARS} — `├─ ` before each child but the last, `└─ ` before the last, and the
|
|
826
1032
|
* carried prefix using `│ ` under an ancestor that still has later siblings or ` ` under a
|
|
827
1033
|
* last ancestor (so the guides line up exactly under the branch they descend from). Node
|
|
828
1034
|
* labels are written as given (an already-styled label is honored); `options.styler` colors
|
|
@@ -840,7 +1046,11 @@ function renderTable(options) {
|
|
|
840
1046
|
* ```
|
|
841
1047
|
*/
|
|
842
1048
|
function renderTree(options) {
|
|
843
|
-
|
|
1049
|
+
const border = options.border ?? "single";
|
|
1050
|
+
return [options.root.label, ...renderTreeChildren(options.root.children ?? [], "", {
|
|
1051
|
+
...options,
|
|
1052
|
+
border
|
|
1053
|
+
})].join("\n");
|
|
844
1054
|
}
|
|
845
1055
|
/**
|
|
846
1056
|
* Render the connector-prefixed lines for a {@link TreeNode} list — the recursive core
|
|
@@ -854,22 +1064,27 @@ function renderTree(options) {
|
|
|
854
1064
|
*
|
|
855
1065
|
* @param nodes - The sibling {@link TreeNode}s to render at this depth
|
|
856
1066
|
* @param prefix - The guide/gap string carried in from the ancestor chain (`''` at the root)
|
|
857
|
-
* @param
|
|
1067
|
+
* @param options - The required `border` selection plus optional connector `styler` and
|
|
1068
|
+
* by-value `style`
|
|
858
1069
|
* @returns The rendered lines for `nodes` and all their descendants
|
|
859
1070
|
*
|
|
860
1071
|
* @example
|
|
861
1072
|
* ```ts
|
|
862
|
-
* renderTreeChildren([{ label: 'a' }, { label: 'b' }], '')
|
|
1073
|
+
* renderTreeChildren([{ label: 'a' }, { label: 'b' }], '', { border: 'single' })
|
|
863
1074
|
* // ['├─ a', '└─ b']
|
|
864
1075
|
* ```
|
|
865
1076
|
*/
|
|
866
|
-
function renderTreeChildren(nodes, prefix,
|
|
1077
|
+
function renderTreeChildren(nodes, prefix, options) {
|
|
1078
|
+
const chars = BORDER_CHARS[options.border];
|
|
1079
|
+
const branch = `${chars.teeRight}${chars.horizontal} `;
|
|
1080
|
+
const corner = `${chars.bottomLeft}${chars.horizontal} `;
|
|
1081
|
+
const guide = `${chars.vertical} `;
|
|
867
1082
|
const lines = [];
|
|
868
1083
|
nodes.forEach((node, index) => {
|
|
869
1084
|
const last = index === nodes.length - 1;
|
|
870
|
-
lines.push(`${prefix}${paint(styler, last ?
|
|
871
|
-
const carry = `${prefix}${paint(styler, last ?
|
|
872
|
-
lines.push(...renderTreeChildren(node.children ?? [], carry,
|
|
1085
|
+
lines.push(`${prefix}${paint(options.styler, last ? corner : branch, options.style)}${node.label}`);
|
|
1086
|
+
const carry = `${prefix}${paint(options.styler, last ? " " : guide, options.style)}`;
|
|
1087
|
+
lines.push(...renderTreeChildren(node.children ?? [], carry, options));
|
|
873
1088
|
});
|
|
874
1089
|
return lines;
|
|
875
1090
|
}
|
|
@@ -976,7 +1191,77 @@ function renderBar(options) {
|
|
|
976
1191
|
const current = options.total <= 0 ? 0 : Math.max(0, Math.min(options.total, options.current));
|
|
977
1192
|
const fraction = options.total <= 0 ? 1 : current / options.total;
|
|
978
1193
|
const filledCells = Math.round(fraction * track);
|
|
979
|
-
return `${`${paint(options.styler, repeatTo(fill, filledCells))}${repeatTo(empty, track - filledCells)}`} ${Math.round(fraction * 100)}% (${current}/${options.total})`;
|
|
1194
|
+
return `${`${paint(options.styler, repeatTo(fill, filledCells), options.style)}${repeatTo(empty, track - filledCells)}`} ${Math.round(fraction * 100)}% (${current}/${options.total})`;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Run `fn` with the global `console.*` captured for its duration, returning the function's `value`
|
|
1198
|
+
* plus the {@link import('./types.js').CapturedMessage}s it logged — the scoped, self-restoring
|
|
1199
|
+
* ergonomic form of {@link createCapture}.
|
|
1200
|
+
*
|
|
1201
|
+
* @param fn - The function to run under capture; may be sync (returns `T`) or async (returns
|
|
1202
|
+
* `Promise<T>`)
|
|
1203
|
+
* @param options - See {@link CaptureOptions} (`levels` / `mirror` / `sink` / `limit` / `on` /
|
|
1204
|
+
* `error`); the capture is started for the duration of `fn` regardless
|
|
1205
|
+
* @returns For a sync `fn`, a {@link CaptureResult}`<T>` (`{ value, messages }`); for an async
|
|
1206
|
+
* `fn`, a `Promise<CaptureResult<T>>` (awaited, then console restored)
|
|
1207
|
+
*
|
|
1208
|
+
* @remarks
|
|
1209
|
+
* - **Always restores.** `start()` runs before `fn`; `stop()` runs in a `finally`, so `console` is
|
|
1210
|
+
* restored even if `fn` throws / rejects (the throw / rejection still propagates). The capture
|
|
1211
|
+
* is local — created, used, and destroyed within the call.
|
|
1212
|
+
* - **Sync vs async.** A `fn` returning a `Promise` is detected and AWAITED before `stop()`, so
|
|
1213
|
+
* captures during the async work are included; a plain `fn` stops synchronously. The return type
|
|
1214
|
+
* follows `fn`'s (overloaded).
|
|
1215
|
+
* - **PROCESS-GLOBAL caveat.** Like {@link createCapture}, this patches the one global `console`.
|
|
1216
|
+
* Concurrent `withCapture` calls (or a `withCapture` around other capturing code) INTERLEAVE —
|
|
1217
|
+
* each captures every `console.*` call in flight, and the inner `stop()` restores whatever the
|
|
1218
|
+
* outer had installed. Use it for sequential, scoped capture, not overlapping captures.
|
|
1219
|
+
*
|
|
1220
|
+
* @example
|
|
1221
|
+
* ```ts
|
|
1222
|
+
* import { withCapture } from '@src/core'
|
|
1223
|
+
*
|
|
1224
|
+
* const { value, messages } = withCapture(() => {
|
|
1225
|
+
* console.log('working')
|
|
1226
|
+
* return 42
|
|
1227
|
+
* })
|
|
1228
|
+
* value // 42
|
|
1229
|
+
* messages.map((m) => m.text) // ['working']
|
|
1230
|
+
*
|
|
1231
|
+
* // Async — awaited before console is restored.
|
|
1232
|
+
* const out = await withCapture(async () => {
|
|
1233
|
+
* console.warn('async noise')
|
|
1234
|
+
* return 'done'
|
|
1235
|
+
* })
|
|
1236
|
+
* out.value // 'done'
|
|
1237
|
+
* ```
|
|
1238
|
+
*/
|
|
1239
|
+
function withCapture(fn, options) {
|
|
1240
|
+
const capture = new Capture(options);
|
|
1241
|
+
capture.start();
|
|
1242
|
+
try {
|
|
1243
|
+
const result = fn();
|
|
1244
|
+
if (result instanceof Promise) return result.then((value) => {
|
|
1245
|
+
const messages = capture.messages();
|
|
1246
|
+
capture.destroy();
|
|
1247
|
+
return {
|
|
1248
|
+
value,
|
|
1249
|
+
messages
|
|
1250
|
+
};
|
|
1251
|
+
}, (error) => {
|
|
1252
|
+
capture.destroy();
|
|
1253
|
+
throw error;
|
|
1254
|
+
});
|
|
1255
|
+
const messages = capture.messages();
|
|
1256
|
+
capture.destroy();
|
|
1257
|
+
return {
|
|
1258
|
+
value: result,
|
|
1259
|
+
messages
|
|
1260
|
+
};
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
capture.destroy();
|
|
1263
|
+
throw error;
|
|
1264
|
+
}
|
|
980
1265
|
}
|
|
981
1266
|
//#endregion
|
|
982
1267
|
//#region src/core/ANSIRenderer.ts
|
|
@@ -1021,274 +1306,55 @@ var ANSIRenderer = class {
|
|
|
1021
1306
|
}
|
|
1022
1307
|
};
|
|
1023
1308
|
//#endregion
|
|
1024
|
-
//#region src/core/
|
|
1309
|
+
//#region src/core/LoggerManager.ts
|
|
1025
1310
|
/**
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
* color / attribute accessor is immutable copy-on-write: it returns a NEW styler's
|
|
1030
|
-
* surface with the token added, so `styler.red.bold('hi')` composes without mutating,
|
|
1031
|
-
* and a base styler is freely reusable.
|
|
1311
|
+
* An event-free registry of named {@link Logger}s plus a convenience fan-out — the §9
|
|
1312
|
+
* manager over the logging layer (a registry, never observable itself; each {@link Logger}
|
|
1313
|
+
* owns its own `emitter`).
|
|
1032
1314
|
*
|
|
1033
1315
|
* @remarks
|
|
1034
|
-
* - **
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
* - **
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
/** Whether styling is applied; when `false`, the surface returns text unchanged. */
|
|
1061
|
-
get enabled() {
|
|
1062
|
-
return this.#enabled;
|
|
1063
|
-
}
|
|
1064
|
-
/**
|
|
1065
|
-
* The fluent {@link StylerInterface} value — a render function (`text => string`) with
|
|
1066
|
-
* `style`, `enabled`, and every {@link Color} / {@link Attribute} as a LAZY accessor
|
|
1067
|
-
* (each computes the next styler's surface only when read). This is what consumers
|
|
1068
|
-
* hold and call.
|
|
1069
|
-
*
|
|
1070
|
-
* @remarks
|
|
1071
|
-
* The accessors are defined as getters (not eagerly-merged values), so accessing one
|
|
1072
|
-
* builds exactly one child styler — the tree is never fully materialized and the
|
|
1073
|
-
* construction terminates. The assembled function is then narrowed to
|
|
1074
|
-
* {@link StylerInterface} through {@link #isSurface} (a real structural check), so no
|
|
1075
|
-
* type assertion is used (AGENTS §1 / §14 — narrow, never assert).
|
|
1076
|
-
*/
|
|
1077
|
-
get surface() {
|
|
1078
|
-
const render = this.#render.bind(this);
|
|
1079
|
-
const descriptors = {
|
|
1080
|
-
style: {
|
|
1081
|
-
value: this.#style,
|
|
1082
|
-
enumerable: true
|
|
1083
|
-
},
|
|
1084
|
-
enabled: {
|
|
1085
|
-
value: this.#enabled,
|
|
1086
|
-
enumerable: true
|
|
1087
|
-
}
|
|
1088
|
-
};
|
|
1089
|
-
for (const color of COLORS) descriptors[color] = {
|
|
1090
|
-
get: this.#foregroundSurface.bind(this, color),
|
|
1091
|
-
enumerable: true
|
|
1092
|
-
};
|
|
1093
|
-
for (const attribute of ATTRIBUTES) descriptors[attribute] = {
|
|
1094
|
-
get: this.#attributeSurface.bind(this, attribute),
|
|
1095
|
-
enumerable: true
|
|
1096
|
-
};
|
|
1097
|
-
const surface = Object.defineProperties(render, descriptors);
|
|
1098
|
-
if (this.#isSurface(surface)) return surface;
|
|
1099
|
-
throw new ConsoleError("INVARIANT", "console: styler surface construction is incomplete");
|
|
1100
|
-
}
|
|
1101
|
-
#render(text) {
|
|
1102
|
-
return this.#enabled ? this.#renderer.render(this.#style, text) : text;
|
|
1103
|
-
}
|
|
1104
|
-
#foregroundSurface(color) {
|
|
1105
|
-
return this.#foreground(color).surface;
|
|
1106
|
-
}
|
|
1107
|
-
#attributeSurface(attribute) {
|
|
1108
|
-
return this.#attribute(attribute).surface;
|
|
1109
|
-
}
|
|
1110
|
-
#isSurface(value) {
|
|
1111
|
-
return typeof value === "function" && "style" in value && "enabled" in value && "red" in value && "bold" in value;
|
|
1112
|
-
}
|
|
1113
|
-
#foreground(color) {
|
|
1114
|
-
return new Styler(this.#renderer, this.#enabled, Object.freeze({
|
|
1115
|
-
...this.#style,
|
|
1116
|
-
foreground: color
|
|
1117
|
-
}));
|
|
1118
|
-
}
|
|
1119
|
-
#attribute(attribute) {
|
|
1120
|
-
if (this.#style.attributes.includes(attribute)) return this;
|
|
1121
|
-
return new Styler(this.#renderer, this.#enabled, Object.freeze({
|
|
1122
|
-
...this.#style,
|
|
1123
|
-
attributes: Object.freeze([...this.#style.attributes, attribute])
|
|
1124
|
-
}));
|
|
1125
|
-
}
|
|
1126
|
-
};
|
|
1127
|
-
//#endregion
|
|
1128
|
-
//#region src/core/Capture.ts
|
|
1129
|
-
/**
|
|
1130
|
-
* An observable console interceptor (AGENTS §13) — it takes control of the global `console.*` on
|
|
1131
|
-
* the READ side. While `active`, every configured `console.x` call is captured as a frozen
|
|
1132
|
-
* {@link CapturedMessage}, buffered (total + by level, bounded), emitted on `capture`, and — per
|
|
1133
|
-
* options — mirrored to the real console and/or forwarded to a {@link SinkInterface}.
|
|
1134
|
-
*
|
|
1135
|
-
* @remarks
|
|
1136
|
-
* - **Snapshot-at-start (the no-capture-loop principle).** `start()` snapshots the CURRENT
|
|
1137
|
-
* `console[level]` for each configured {@link CaptureLevel}, then installs the wrappers. The
|
|
1138
|
-
* mirror writes through that snapshot — so our OWN console sink output (the Logger / Reporter,
|
|
1139
|
-
* which snapshot the real `console` at creation) is never recaptured: `Capture` catches
|
|
1140
|
-
* THIRD-PARTY `console.*`, not our writes. Create your loggers BEFORE installing a capture.
|
|
1141
|
-
* - **Idempotent + PROCESS-GLOBAL + NON-REENTRANT.** `start()` while already `active` is a no-op
|
|
1142
|
-
* (never double-patches); `stop()` while inactive is a no-op. It patches the ONE global
|
|
1143
|
-
* `console`, so at most ONE capture may be active at a time — running two concurrently
|
|
1144
|
-
* interleaves their buffers and clobbers each other's restore.
|
|
1145
|
-
* - **Bounded buffers.** `messages()` / `messages(level)` — the total buffer and each by-level
|
|
1146
|
-
* bucket are each capped at `limit`
|
|
1147
|
-
* (oldest dropped first), never unbounded — the same retention precedent as {@link Logger}.
|
|
1148
|
-
* - **Lifecycle (§10).** `start` / `stop` toggle interception (emitting `start` / `stop`);
|
|
1149
|
-
* `destroy()` stops (restoring `console`) then destroys the emitter.
|
|
1150
|
-
*
|
|
1151
|
-
* @example
|
|
1152
|
-
* ```ts
|
|
1153
|
-
* const capture = new Capture({ levels: ['warn', 'error'], mirror: true })
|
|
1154
|
-
* capture.start()
|
|
1155
|
-
* console.warn('third-party noise') // captured AND mirrored to the real console
|
|
1156
|
-
* capture.messages('warn') // [{ level: 'warn', text: 'third-party noise', time: … }]
|
|
1157
|
-
* capture.stop() // console.warn restored
|
|
1158
|
-
* ```
|
|
1159
|
-
*/
|
|
1160
|
-
var Capture = class {
|
|
1161
|
-
#emitter;
|
|
1162
|
-
#levels;
|
|
1163
|
-
#mirror;
|
|
1164
|
-
#sink;
|
|
1165
|
-
#limit;
|
|
1166
|
-
#messages = [];
|
|
1167
|
-
#buckets = /* @__PURE__ */ new Map();
|
|
1168
|
-
#originals = /* @__PURE__ */ new Map();
|
|
1169
|
-
#active = false;
|
|
1170
|
-
constructor(options) {
|
|
1171
|
-
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1172
|
-
...options?.on !== void 0 ? { on: options.on } : {},
|
|
1173
|
-
...options?.error !== void 0 ? { error: options.error } : {}
|
|
1174
|
-
});
|
|
1175
|
-
this.#levels = options?.levels ?? DEFAULT_CAPTURE_LEVELS;
|
|
1176
|
-
this.#mirror = options?.mirror ?? false;
|
|
1177
|
-
this.#sink = options?.sink;
|
|
1178
|
-
this.#limit = options?.limit ?? 1e3;
|
|
1179
|
-
for (const level of this.#levels) this.#buckets.set(level, []);
|
|
1180
|
-
}
|
|
1181
|
-
get emitter() {
|
|
1182
|
-
return this.#emitter;
|
|
1183
|
-
}
|
|
1184
|
-
get active() {
|
|
1185
|
-
return this.#active;
|
|
1186
|
-
}
|
|
1187
|
-
start() {
|
|
1188
|
-
if (this.#active) return;
|
|
1189
|
-
this.#active = true;
|
|
1190
|
-
const target = console;
|
|
1191
|
-
for (const level of this.#levels) {
|
|
1192
|
-
const original = target[level];
|
|
1193
|
-
this.#originals.set(level, original);
|
|
1194
|
-
const mirror = original.bind(console);
|
|
1195
|
-
target[level] = this.#captureCall.bind(this, level, mirror);
|
|
1196
|
-
}
|
|
1197
|
-
this.#emitter.emit("start");
|
|
1198
|
-
}
|
|
1199
|
-
stop() {
|
|
1200
|
-
if (!this.#active) return;
|
|
1201
|
-
this.#active = false;
|
|
1202
|
-
const target = console;
|
|
1203
|
-
for (const [level, original] of this.#originals) target[level] = original;
|
|
1204
|
-
this.#originals.clear();
|
|
1205
|
-
this.#emitter.emit("stop");
|
|
1206
|
-
}
|
|
1207
|
-
messages(level) {
|
|
1208
|
-
if (level === void 0) return [...this.#messages];
|
|
1209
|
-
return [...this.#buckets.get(level) ?? []];
|
|
1210
|
-
}
|
|
1211
|
-
clear() {
|
|
1212
|
-
this.#messages.length = 0;
|
|
1213
|
-
for (const bucket of this.#buckets.values()) bucket.length = 0;
|
|
1214
|
-
}
|
|
1215
|
-
destroy() {
|
|
1216
|
-
this.stop();
|
|
1217
|
-
this.#emitter.destroy();
|
|
1218
|
-
}
|
|
1219
|
-
#captureCall(level, mirror, ...args) {
|
|
1220
|
-
this.#intercept(level, args, mirror);
|
|
1221
|
-
}
|
|
1222
|
-
#intercept(level, args, mirror) {
|
|
1223
|
-
const message = this.#capture(level, args);
|
|
1224
|
-
this.#retain(message);
|
|
1225
|
-
this.#emitter.emit("capture", message);
|
|
1226
|
-
if (this.#mirror) mirror(...args);
|
|
1227
|
-
if (this.#sink !== void 0) try {
|
|
1228
|
-
this.#sink.write(message.text, CAPTURE_LEVEL_MAP[level]);
|
|
1229
|
-
} catch {}
|
|
1230
|
-
}
|
|
1231
|
-
#capture(level, args) {
|
|
1232
|
-
return Object.freeze({
|
|
1233
|
-
level,
|
|
1234
|
-
text: formatArgs(args),
|
|
1235
|
-
time: Date.now()
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
#retain(message) {
|
|
1239
|
-
this.#push(this.#messages, message);
|
|
1240
|
-
const bucket = this.#buckets.get(message.level);
|
|
1241
|
-
if (bucket !== void 0) this.#push(bucket, message);
|
|
1242
|
-
}
|
|
1243
|
-
#push(buffer, message) {
|
|
1244
|
-
buffer.push(message);
|
|
1245
|
-
if (buffer.length > this.#limit) buffer.shift();
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
//#endregion
|
|
1249
|
-
//#region src/core/LoggerManager.ts
|
|
1250
|
-
/**
|
|
1251
|
-
* An event-free registry of named {@link Logger}s plus a convenience fan-out — the §9
|
|
1252
|
-
* manager over the logging layer (a registry, never observable itself; each {@link Logger}
|
|
1253
|
-
* owns its own `emitter`).
|
|
1254
|
-
*
|
|
1255
|
-
* @remarks
|
|
1256
|
-
* - **Registry (§9).** Loggers live in an insertion-ordered `Map` keyed by `name`.
|
|
1257
|
-
* `register(name, options?)` mints a {@link Logger} named `name` — the manager's default
|
|
1258
|
-
* `level` / `sink` / `styler` / `limit` / `silent` flow in unless `options` OVERRIDES them
|
|
1259
|
-
* (`name` is always the registry key, so any `options.name` is ignored) — stores it (a
|
|
1260
|
-
* re-`register` of the same name OVERWRITES, last write wins), and returns it. `count` is
|
|
1261
|
-
* the map size, `logger(name)` looks one up, `loggers()` lists them in insertion order.
|
|
1262
|
-
* - **Removal (§9.2).** `remove()` clears ALL, `remove(name)` drops ONE (`true` if present),
|
|
1263
|
-
* `remove(names)` drops a batch (`true` if any was removed). `clear()` empties the registry.
|
|
1264
|
-
* (Removal does NOT `destroy` the returned loggers — a caller still holding one keeps using
|
|
1265
|
-
* it; the manager simply stops tracking it.)
|
|
1266
|
-
* - **Fan-out.** `debug` / `info` / `warn` / `error(message, data?)` forward the one call to
|
|
1267
|
-
* EVERY registered logger; each gates / emits / writes per its own `level` and `sink`. A
|
|
1268
|
-
* fan-out over an empty registry is a no-op.
|
|
1269
|
-
* - **Event-free.** No emitter, no events — the manager is a pure registry; observability is
|
|
1270
|
-
* per-{@link Logger}.
|
|
1271
|
-
*
|
|
1272
|
-
* @example
|
|
1273
|
-
* ```ts
|
|
1274
|
-
* const manager = new LoggerManager({ level: 'warn' })
|
|
1275
|
-
* manager.register('http') // inherits the `warn` default
|
|
1276
|
-
* manager.register('db', { level: 'debug' }) // overrides to `debug`
|
|
1277
|
-
* manager.warn('slow', { ms: 900 }) // fans out to both loggers
|
|
1278
|
-
* manager.count // 2
|
|
1279
|
-
* ```
|
|
1316
|
+
* - **Registry (§9).** Loggers live in an insertion-ordered `Map` keyed by `name`.
|
|
1317
|
+
* `register(name, options?)` mints a {@link Logger} named `name` — the manager's default
|
|
1318
|
+
* `level` / `sink` / `styler` / `theme` / `format` / `limit` / `silent` flow in unless
|
|
1319
|
+
* `options` OVERRIDES them
|
|
1320
|
+
* (`name` is always the registry key, so any `options.name` is ignored) — stores it (a
|
|
1321
|
+
* re-`register` of the same name OVERWRITES, last write wins), and returns it. `count` is
|
|
1322
|
+
* the map size, `logger(name)` looks one up, `loggers()` lists them in insertion order.
|
|
1323
|
+
* - **Removal (§9.2).** `remove()` clears ALL, `remove(name)` drops ONE (`true` if present),
|
|
1324
|
+
* `remove(names)` drops a batch (`true` if any was removed).
|
|
1325
|
+
* (Removal does NOT `destroy` the returned loggers — a caller still holding one keeps using
|
|
1326
|
+
* it; the manager simply stops tracking it.)
|
|
1327
|
+
* - **Fan-out.** `debug` / `info` / `warn` / `error(message, data?)` forward the one call to
|
|
1328
|
+
* every registered logger in insertion order; each gates / emits / writes per its own `level`
|
|
1329
|
+
* and `sink`. A formatter throw is a programmer error and propagates, stopping the remaining
|
|
1330
|
+
* loggers for that call. A fan-out over an empty registry is a no-op.
|
|
1331
|
+
* - **Event-free.** No emitter, no events — the manager is a pure registry; observability is
|
|
1332
|
+
* per-{@link Logger}.
|
|
1333
|
+
*
|
|
1334
|
+
* @example
|
|
1335
|
+
* ```ts
|
|
1336
|
+
* const manager = new LoggerManager({ level: 'warn' })
|
|
1337
|
+
* manager.register('http') // inherits the `warn` default
|
|
1338
|
+
* manager.register('db', { level: 'debug' }) // overrides to `debug`
|
|
1339
|
+
* manager.warn('slow', { ms: 900 }) // fans out to both loggers
|
|
1340
|
+
* manager.count // 2
|
|
1341
|
+
* ```
|
|
1280
1342
|
*/
|
|
1281
1343
|
var LoggerManager = class {
|
|
1282
1344
|
#loggers = /* @__PURE__ */ new Map();
|
|
1283
1345
|
#level;
|
|
1284
1346
|
#sink;
|
|
1285
1347
|
#styler;
|
|
1348
|
+
#theme;
|
|
1349
|
+
#format;
|
|
1286
1350
|
#limit;
|
|
1287
1351
|
#silent;
|
|
1288
1352
|
constructor(options) {
|
|
1289
1353
|
this.#level = options?.level;
|
|
1290
1354
|
this.#sink = options?.sink;
|
|
1291
1355
|
this.#styler = options?.styler;
|
|
1356
|
+
this.#theme = options?.theme;
|
|
1357
|
+
this.#format = options?.format;
|
|
1292
1358
|
this.#limit = options?.limit;
|
|
1293
1359
|
this.#silent = options?.silent;
|
|
1294
1360
|
}
|
|
@@ -1300,6 +1366,8 @@ var LoggerManager = class {
|
|
|
1300
1366
|
...this.#level !== void 0 ? { level: this.#level } : {},
|
|
1301
1367
|
...this.#sink !== void 0 ? { sink: this.#sink } : {},
|
|
1302
1368
|
...this.#styler !== void 0 ? { styler: this.#styler } : {},
|
|
1369
|
+
...this.#theme !== void 0 ? { theme: this.#theme } : {},
|
|
1370
|
+
...this.#format !== void 0 ? { format: this.#format } : {},
|
|
1303
1371
|
...this.#limit !== void 0 ? { limit: this.#limit } : {},
|
|
1304
1372
|
...this.#silent !== void 0 ? { silent: this.#silent } : {},
|
|
1305
1373
|
...options,
|
|
@@ -1338,9 +1406,6 @@ var LoggerManager = class {
|
|
|
1338
1406
|
}
|
|
1339
1407
|
return this.#loggers.delete(names);
|
|
1340
1408
|
}
|
|
1341
|
-
clear() {
|
|
1342
|
-
this.#loggers.clear();
|
|
1343
|
-
}
|
|
1344
1409
|
};
|
|
1345
1410
|
//#endregion
|
|
1346
1411
|
//#region src/core/Progress.ts
|
|
@@ -1376,8 +1441,11 @@ var Progress = class {
|
|
|
1376
1441
|
#emitter;
|
|
1377
1442
|
#total;
|
|
1378
1443
|
#width;
|
|
1444
|
+
#fill;
|
|
1445
|
+
#empty;
|
|
1379
1446
|
#sink;
|
|
1380
1447
|
#styler;
|
|
1448
|
+
#theme;
|
|
1381
1449
|
#message;
|
|
1382
1450
|
#current = 0;
|
|
1383
1451
|
#active = true;
|
|
@@ -1389,8 +1457,11 @@ var Progress = class {
|
|
|
1389
1457
|
});
|
|
1390
1458
|
this.#total = options.total;
|
|
1391
1459
|
this.#width = options.width ?? 30;
|
|
1460
|
+
this.#fill = options.fill;
|
|
1461
|
+
this.#empty = options.empty;
|
|
1392
1462
|
this.#sink = options.sink ?? createConsoleSink();
|
|
1393
1463
|
this.#styler = options.styler ?? createStyler();
|
|
1464
|
+
this.#theme = options.theme ?? DEFAULT_THEME;
|
|
1394
1465
|
this.#message = options.message ?? "";
|
|
1395
1466
|
}
|
|
1396
1467
|
get emitter() {
|
|
@@ -1443,7 +1514,10 @@ var Progress = class {
|
|
|
1443
1514
|
current: this.#current,
|
|
1444
1515
|
total: this.#total,
|
|
1445
1516
|
width: this.#width,
|
|
1446
|
-
|
|
1517
|
+
...this.#fill === void 0 ? {} : { fill: this.#fill },
|
|
1518
|
+
...this.#empty === void 0 ? {} : { empty: this.#empty },
|
|
1519
|
+
styler: this.#styler,
|
|
1520
|
+
style: this.#theme.accent
|
|
1447
1521
|
});
|
|
1448
1522
|
const line = this.#message === "" ? bar : `${bar} ${this.#message}`;
|
|
1449
1523
|
this.#sink.write(`\r${line}${final ? "\n" : ""}`, level);
|
|
@@ -1464,7 +1538,7 @@ var Progress = class {
|
|
|
1464
1538
|
* capture (the capture chunk), no level retention (the logger). Just format + write.
|
|
1465
1539
|
* - **`status` is a narrative OUTCOME, not a log level.** Its {@link StatusLevel} (`success` /
|
|
1466
1540
|
* `error` / `warn` / `info`) is distinct from {@link import('./types.js').LogLevel}: an icon
|
|
1467
|
-
*
|
|
1541
|
+
* supplied theme status icon + style, with `error` routed to the sink's
|
|
1468
1542
|
* error stream (the `level` hint forwarded to {@link SinkInterface.write}) — there is no
|
|
1469
1543
|
* gating and no severity ordering.
|
|
1470
1544
|
* - **Width-aware.** `section` (and a `box` with no explicit `width`) lay out to the reporter's
|
|
@@ -1484,49 +1558,45 @@ var Progress = class {
|
|
|
1484
1558
|
var Reporter = class {
|
|
1485
1559
|
#sink;
|
|
1486
1560
|
#styler;
|
|
1561
|
+
#theme;
|
|
1487
1562
|
#width;
|
|
1488
1563
|
constructor(options) {
|
|
1489
1564
|
this.#sink = options?.sink ?? createConsoleSink();
|
|
1490
1565
|
this.#styler = options?.styler ?? createStyler();
|
|
1566
|
+
this.#theme = options?.theme ?? DEFAULT_THEME;
|
|
1491
1567
|
this.#width = options?.width ?? 80;
|
|
1492
1568
|
}
|
|
1493
1569
|
section(title) {
|
|
1494
1570
|
this.#sink.write(renderSeparator({
|
|
1495
1571
|
title,
|
|
1496
1572
|
width: this.#width,
|
|
1497
|
-
styler: this.#styler
|
|
1573
|
+
styler: this.#styler,
|
|
1574
|
+
style: this.#theme.chrome
|
|
1498
1575
|
}));
|
|
1499
1576
|
}
|
|
1500
1577
|
step(message, position) {
|
|
1501
|
-
const prefix = position === void 0 ? "" : `${this.#styler.
|
|
1578
|
+
const prefix = position === void 0 ? "" : `${this.#styler.render(this.#theme.accent, `[${position.index}/${position.total}]`)} `;
|
|
1502
1579
|
this.#sink.write(`${prefix}${message}`);
|
|
1503
1580
|
}
|
|
1504
1581
|
timing(label, ms) {
|
|
1505
|
-
this.#sink.write(`${label} ${this.#styler.
|
|
1582
|
+
this.#sink.write(`${label} ${this.#styler.render(this.#theme.chrome, `… ${formatDuration(ms)}`)}`);
|
|
1506
1583
|
}
|
|
1507
1584
|
status(level, message) {
|
|
1508
|
-
const
|
|
1509
|
-
const line = `${
|
|
1585
|
+
const status = this.#theme.statuses[level];
|
|
1586
|
+
const line = `${this.#styler.render(status.style, status.icon)} ${this.#styler.render(status.style, message)}`;
|
|
1510
1587
|
this.#sink.write(line, level === "error" ? "error" : void 0);
|
|
1511
1588
|
}
|
|
1512
1589
|
table(options) {
|
|
1513
|
-
this.#sink.write(renderTable(
|
|
1514
|
-
styler: this.#styler.dim,
|
|
1515
|
-
...options
|
|
1516
|
-
}));
|
|
1590
|
+
this.#sink.write(renderTable(this.#resolveStyle(options)));
|
|
1517
1591
|
}
|
|
1518
1592
|
tree(options) {
|
|
1519
|
-
this.#sink.write(renderTree(
|
|
1520
|
-
styler: this.#styler.dim,
|
|
1521
|
-
...options
|
|
1522
|
-
}));
|
|
1593
|
+
this.#sink.write(renderTree(this.#resolveStyle(options)));
|
|
1523
1594
|
}
|
|
1524
1595
|
box(options) {
|
|
1525
|
-
this.#sink.write(renderBox({
|
|
1596
|
+
this.#sink.write(renderBox(this.#resolveStyle({
|
|
1526
1597
|
width: this.#width,
|
|
1527
|
-
styler: this.#styler.dim,
|
|
1528
1598
|
...options
|
|
1529
|
-
}));
|
|
1599
|
+
})));
|
|
1530
1600
|
}
|
|
1531
1601
|
line(text) {
|
|
1532
1602
|
this.#sink.write(text);
|
|
@@ -1534,6 +1604,13 @@ var Reporter = class {
|
|
|
1534
1604
|
blank(count = 1) {
|
|
1535
1605
|
for (let index = 0; index < count; index += 1) this.#sink.write("");
|
|
1536
1606
|
}
|
|
1607
|
+
#resolveStyle(options) {
|
|
1608
|
+
return {
|
|
1609
|
+
styler: this.#styler,
|
|
1610
|
+
...options.styler === void 0 && options.style === void 0 ? { style: this.#theme.chrome } : {},
|
|
1611
|
+
...options
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1537
1614
|
};
|
|
1538
1615
|
//#endregion
|
|
1539
1616
|
//#region src/core/Spinner.ts
|
|
@@ -1557,7 +1634,7 @@ var Reporter = class {
|
|
|
1557
1634
|
* - **Idempotent `start`.** A {@link start} while already `active` is a no-op (it never arms a second
|
|
1558
1635
|
* timer).
|
|
1559
1636
|
* - **Outcome lines.** {@link success} / {@link failure} clear the timer then write + emit a FINAL line —
|
|
1560
|
-
* the
|
|
1637
|
+
* the supplied theme status icon + style (`✔` / `✖` by default) + the message — terminated
|
|
1561
1638
|
* by a newline (the activity is over; the line is committed, not overwritten). {@link failure} routes to
|
|
1562
1639
|
* the sink's error stream.
|
|
1563
1640
|
* - **Lifecycle (§10).** {@link stop} clears the timer and LEAVES the current line; {@link destroy}
|
|
@@ -1578,6 +1655,7 @@ var Spinner = class {
|
|
|
1578
1655
|
#interval;
|
|
1579
1656
|
#sink;
|
|
1580
1657
|
#styler;
|
|
1658
|
+
#theme;
|
|
1581
1659
|
#message;
|
|
1582
1660
|
#handle;
|
|
1583
1661
|
#index = 0;
|
|
@@ -1591,6 +1669,7 @@ var Spinner = class {
|
|
|
1591
1669
|
this.#interval = options?.interval ?? 80;
|
|
1592
1670
|
this.#sink = options?.sink ?? createConsoleSink();
|
|
1593
1671
|
this.#styler = options?.styler ?? createStyler();
|
|
1672
|
+
this.#theme = options?.theme ?? DEFAULT_THEME;
|
|
1594
1673
|
this.#message = options?.message ?? "";
|
|
1595
1674
|
}
|
|
1596
1675
|
get emitter() {
|
|
@@ -1636,13 +1715,13 @@ var Spinner = class {
|
|
|
1636
1715
|
this.stop();
|
|
1637
1716
|
const text = message ?? this.#message;
|
|
1638
1717
|
if (message !== void 0) this.#message = message;
|
|
1639
|
-
const
|
|
1640
|
-
const line = `${
|
|
1718
|
+
const status = this.#theme.statuses[level];
|
|
1719
|
+
const line = `${this.#styler.render(status.style, status.icon)} ${this.#styler.render(status.style, text)}`;
|
|
1641
1720
|
this.#emitter.emit("frame", line);
|
|
1642
1721
|
this.#sink.write(`\r${line}\n`, level === "error" ? "error" : void 0);
|
|
1643
1722
|
}
|
|
1644
1723
|
#line() {
|
|
1645
|
-
const glyph = this.#styler.
|
|
1724
|
+
const glyph = this.#styler.render(this.#theme.accent, this.#frames[this.#index] ?? "");
|
|
1646
1725
|
return this.#message === "" ? glyph : `${glyph} ${this.#message}`;
|
|
1647
1726
|
}
|
|
1648
1727
|
#paint(line) {
|
|
@@ -1651,6 +1730,148 @@ var Spinner = class {
|
|
|
1651
1730
|
}
|
|
1652
1731
|
};
|
|
1653
1732
|
//#endregion
|
|
1733
|
+
//#region src/core/Styler.ts
|
|
1734
|
+
/**
|
|
1735
|
+
* The fluent, composable styler — the consumer-facing API over the style engine. It
|
|
1736
|
+
* builds a {@link Style} (style as DATA) and renders it through an injected
|
|
1737
|
+
* {@link RendererInterface} (the ANSI default, or a browser `%c` renderer at C-f). Each
|
|
1738
|
+
* color / attribute accessor is immutable copy-on-write: it returns a NEW styler's
|
|
1739
|
+
* surface with the token added, so `styler.red.bold('hi')` composes without mutating,
|
|
1740
|
+
* and a base styler is freely reusable.
|
|
1741
|
+
*
|
|
1742
|
+
* @remarks
|
|
1743
|
+
* - **Callable surface.** A `Styler` is not itself callable; its {@link surface} getter
|
|
1744
|
+
* returns the {@link StylerInterface} — a render FUNCTION carrying the chainable
|
|
1745
|
+
* accessors. The accessors are installed as LAZY getters (`Object.defineProperties`),
|
|
1746
|
+
* so a chain materializes only the stylers it actually walks — never the full tree —
|
|
1747
|
+
* and the recursion terminates. The factory returns that surface; this class is the
|
|
1748
|
+
* engine behind it.
|
|
1749
|
+
* - **Immutable.** `#foreground` and `#attribute` return a fresh `Styler` (the style is
|
|
1750
|
+
* rebuilt, never mutated). A later color of the same channel WINS (last write); a
|
|
1751
|
+
* repeated attribute is idempotent (de-duplicated, order preserved).
|
|
1752
|
+
* - **Styling by value.** `render(style, text)` merges a {@link Style} over the accumulated
|
|
1753
|
+
* one and renders that — the same precedence a chain applies, reached with DATA instead of
|
|
1754
|
+
* accessor names. It is how a {@link import('./types.js').Theme} role is drawn.
|
|
1755
|
+
* - **`enabled` switch.** When `false`, the render function returns text VERBATIM — no
|
|
1756
|
+
* renderer call, no escape codes (for a non-TTY / `NO_COLOR` / piped output).
|
|
1757
|
+
* - **Event-free** — a pure styling primitive (AGENTS §13), like `Scheduler`.
|
|
1758
|
+
*/
|
|
1759
|
+
var Styler = class Styler {
|
|
1760
|
+
#renderer;
|
|
1761
|
+
#enabled;
|
|
1762
|
+
#style;
|
|
1763
|
+
constructor(renderer, enabled, style) {
|
|
1764
|
+
this.#renderer = renderer;
|
|
1765
|
+
this.#enabled = enabled;
|
|
1766
|
+
this.#style = style;
|
|
1767
|
+
}
|
|
1768
|
+
/** The accumulated style DATA — the empty style on a base styler. */
|
|
1769
|
+
get style() {
|
|
1770
|
+
return this.#style;
|
|
1771
|
+
}
|
|
1772
|
+
/** Whether styling is applied; when `false`, the surface returns text unchanged. */
|
|
1773
|
+
get enabled() {
|
|
1774
|
+
return this.#enabled;
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* The fluent {@link StylerInterface} value — a render function (`text => string`) with
|
|
1778
|
+
* `style`, `enabled`, and every {@link Color} / {@link Attribute} as a LAZY accessor
|
|
1779
|
+
* (each computes the next styler's surface only when read). This is what consumers
|
|
1780
|
+
* hold and call.
|
|
1781
|
+
*
|
|
1782
|
+
* @remarks
|
|
1783
|
+
* The accessors are defined as getters (not eagerly-merged values), so accessing one
|
|
1784
|
+
* builds exactly one child styler — the tree is never fully materialized and the
|
|
1785
|
+
* construction terminates. The assembled function is then narrowed to
|
|
1786
|
+
* {@link StylerInterface} through {@link #isSurface} (a real structural check), so no
|
|
1787
|
+
* type assertion is used (AGENTS §1 / §14 — narrow, never assert).
|
|
1788
|
+
*/
|
|
1789
|
+
get surface() {
|
|
1790
|
+
const callable = this.#render.bind(this);
|
|
1791
|
+
const descriptors = {
|
|
1792
|
+
style: {
|
|
1793
|
+
value: this.#style,
|
|
1794
|
+
enumerable: true
|
|
1795
|
+
},
|
|
1796
|
+
enabled: {
|
|
1797
|
+
value: this.#enabled,
|
|
1798
|
+
enumerable: true
|
|
1799
|
+
},
|
|
1800
|
+
render: {
|
|
1801
|
+
value: this.render.bind(this),
|
|
1802
|
+
enumerable: true
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
for (const color of COLORS) descriptors[color] = {
|
|
1806
|
+
get: this.#foregroundSurface.bind(this, color),
|
|
1807
|
+
enumerable: true
|
|
1808
|
+
};
|
|
1809
|
+
for (const attribute of ATTRIBUTES) descriptors[attribute] = {
|
|
1810
|
+
get: this.#attributeSurface.bind(this, attribute),
|
|
1811
|
+
enumerable: true
|
|
1812
|
+
};
|
|
1813
|
+
const surface = Object.defineProperties(callable, descriptors);
|
|
1814
|
+
if (this.#isSurface(surface)) return surface;
|
|
1815
|
+
throw new ConsoleError("INVARIANT", "console: styler surface construction is incomplete");
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Render `text` in `style` merged OVER the accumulated style — the by-value door beside
|
|
1819
|
+
* the accessor chain, and how a {@link import('./types.js').Theme} role is applied.
|
|
1820
|
+
*
|
|
1821
|
+
* @param style - The style to overlay; its colors win over the accumulated ones and its
|
|
1822
|
+
* attributes join them (de-duplicated, the accumulated ones first)
|
|
1823
|
+
* @param text - The text to wrap
|
|
1824
|
+
* @returns The rendered text — verbatim when `enabled` is `false`, and (by the
|
|
1825
|
+
* {@link RendererInterface} contract) when the merged style or `text` is empty
|
|
1826
|
+
*
|
|
1827
|
+
* @example
|
|
1828
|
+
* ```ts
|
|
1829
|
+
* import { createStyler, DEFAULT_THEME } from '@src/core'
|
|
1830
|
+
*
|
|
1831
|
+
* const styler = createStyler()
|
|
1832
|
+
* styler.render(DEFAULT_THEME.levels.warn, 'WARN') // yellow
|
|
1833
|
+
* styler.bold.render(DEFAULT_THEME.chrome, '│') // dim, over the accumulated bold
|
|
1834
|
+
* ```
|
|
1835
|
+
*/
|
|
1836
|
+
render(style, text) {
|
|
1837
|
+
return this.#enabled ? this.#renderer.render(this.#merge(style), text) : text;
|
|
1838
|
+
}
|
|
1839
|
+
#render(text) {
|
|
1840
|
+
return this.#enabled ? this.#renderer.render(this.#style, text) : text;
|
|
1841
|
+
}
|
|
1842
|
+
#merge(style) {
|
|
1843
|
+
const attributes = [...this.#style.attributes];
|
|
1844
|
+
for (const attribute of style.attributes) if (!attributes.includes(attribute)) attributes.push(attribute);
|
|
1845
|
+
return Object.freeze({
|
|
1846
|
+
...this.#style,
|
|
1847
|
+
...style,
|
|
1848
|
+
attributes: Object.freeze(attributes)
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
#foregroundSurface(color) {
|
|
1852
|
+
return this.#foreground(color).surface;
|
|
1853
|
+
}
|
|
1854
|
+
#attributeSurface(attribute) {
|
|
1855
|
+
return this.#attribute(attribute).surface;
|
|
1856
|
+
}
|
|
1857
|
+
#isSurface(value) {
|
|
1858
|
+
return typeof value === "function" && "style" in value && "enabled" in value && "render" in value && "red" in value && "bold" in value;
|
|
1859
|
+
}
|
|
1860
|
+
#foreground(color) {
|
|
1861
|
+
return new Styler(this.#renderer, this.#enabled, Object.freeze({
|
|
1862
|
+
...this.#style,
|
|
1863
|
+
foreground: color
|
|
1864
|
+
}));
|
|
1865
|
+
}
|
|
1866
|
+
#attribute(attribute) {
|
|
1867
|
+
if (this.#style.attributes.includes(attribute)) return this;
|
|
1868
|
+
return new Styler(this.#renderer, this.#enabled, Object.freeze({
|
|
1869
|
+
...this.#style,
|
|
1870
|
+
attributes: Object.freeze([...this.#style.attributes, attribute])
|
|
1871
|
+
}));
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
//#endregion
|
|
1654
1875
|
//#region src/core/factories.ts
|
|
1655
1876
|
/**
|
|
1656
1877
|
* Create the cross-environment default {@link RendererInterface} — the ANSI / SGR
|
|
@@ -1704,6 +1925,52 @@ function createStyler(options) {
|
|
|
1704
1925
|
return new Styler(options?.renderer ?? new ANSIRenderer(), options?.enabled ?? true, EMPTY_STYLE).surface;
|
|
1705
1926
|
}
|
|
1706
1927
|
/**
|
|
1928
|
+
* Create a {@link Theme} — the app-wide semantic style vocabulary, merged role by role over
|
|
1929
|
+
* {@link DEFAULT_THEME}. Hand one theme to a logger / reporter / spinner / progress and every
|
|
1930
|
+
* surface speaks it; omit `options` for the defaults.
|
|
1931
|
+
*
|
|
1932
|
+
* @param options - See {@link ThemeOptions}
|
|
1933
|
+
* @returns A frozen {@link Theme}
|
|
1934
|
+
*
|
|
1935
|
+
* @remarks
|
|
1936
|
+
* - **Merges per ROLE, not per theme.** An omitted role keeps its default, and `levels` /
|
|
1937
|
+
* `statuses` merge per ENTRY — `{ levels: { warn: … } }` restyles the `warn` label and
|
|
1938
|
+
* leaves `debug` / `info` / `error` untouched.
|
|
1939
|
+
* - **Frozen and shareable.** The factory snapshots and deep-freezes every style leaf. The
|
|
1940
|
+
* returned theme and its `levels` / `statuses` records are frozen. Each status record is also
|
|
1941
|
+
* copied and frozen. One theme is therefore safely shared across every entity.
|
|
1942
|
+
*
|
|
1943
|
+
* @example
|
|
1944
|
+
* ```ts
|
|
1945
|
+
* import { createStyler, createTheme } from '@src/core'
|
|
1946
|
+
*
|
|
1947
|
+
* const styler = createStyler()
|
|
1948
|
+
* const theme = createTheme({
|
|
1949
|
+
* levels: { warn: styler.brightYellow.bold.style }, // only the warn label changes
|
|
1950
|
+
* accent: styler.magenta.style, // spinner glyph, progress fill, step prefix
|
|
1951
|
+
* })
|
|
1952
|
+
* theme.levels.error // still the default red
|
|
1953
|
+
* ```
|
|
1954
|
+
*/
|
|
1955
|
+
function createTheme(options) {
|
|
1956
|
+
const levels = { ...DEFAULT_THEME.levels };
|
|
1957
|
+
for (const level of LEVELS) levels[level] = freezeStyle(options?.levels?.[level] ?? DEFAULT_THEME.levels[level]);
|
|
1958
|
+
const statuses = { ...DEFAULT_THEME.statuses };
|
|
1959
|
+
for (const status of STATUS_LEVELS) {
|
|
1960
|
+
const source = options?.statuses?.[status] ?? DEFAULT_THEME.statuses[status];
|
|
1961
|
+
statuses[status] = Object.freeze({
|
|
1962
|
+
icon: source.icon,
|
|
1963
|
+
style: freezeStyle(source.style)
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
return Object.freeze({
|
|
1967
|
+
levels: Object.freeze(levels),
|
|
1968
|
+
statuses: Object.freeze(statuses),
|
|
1969
|
+
accent: freezeStyle(options?.accent ?? DEFAULT_THEME.accent),
|
|
1970
|
+
chrome: freezeStyle(options?.chrome ?? DEFAULT_THEME.chrome)
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1707
1974
|
* Create the default {@link SinkInterface} — a console sink that routes by level and writes
|
|
1708
1975
|
* through the `console` methods SNAPSHOTTED at creation. The default output target behind
|
|
1709
1976
|
* {@link createLogger}.
|
|
@@ -1876,76 +2143,6 @@ function createCapture(options) {
|
|
|
1876
2143
|
return new Capture(options);
|
|
1877
2144
|
}
|
|
1878
2145
|
/**
|
|
1879
|
-
* Run `fn` with the global `console.*` captured for its duration, returning the function's `value`
|
|
1880
|
-
* plus the {@link import('./types.js').CapturedMessage}s it logged — the scoped, self-restoring
|
|
1881
|
-
* ergonomic form of {@link createCapture}.
|
|
1882
|
-
*
|
|
1883
|
-
* @param fn - The function to run under capture; may be sync (returns `T`) or async (returns
|
|
1884
|
-
* `Promise<T>`)
|
|
1885
|
-
* @param options - See {@link CaptureOptions} (`levels` / `mirror` / `sink` / `limit` / `on` /
|
|
1886
|
-
* `error`); the capture is started for the duration of `fn` regardless
|
|
1887
|
-
* @returns For a sync `fn`, a {@link CaptureResult}`<T>` (`{ value, messages }`); for an async
|
|
1888
|
-
* `fn`, a `Promise<CaptureResult<T>>` (awaited, then console restored)
|
|
1889
|
-
*
|
|
1890
|
-
* @remarks
|
|
1891
|
-
* - **Always restores.** `start()` runs before `fn`; `stop()` runs in a `finally`, so `console` is
|
|
1892
|
-
* restored even if `fn` throws / rejects (the throw / rejection still propagates). The capture
|
|
1893
|
-
* is local — created, used, and destroyed within the call.
|
|
1894
|
-
* - **Sync vs async.** A `fn` returning a `Promise` is detected and AWAITED before `stop()`, so
|
|
1895
|
-
* captures during the async work are included; a plain `fn` stops synchronously. The return type
|
|
1896
|
-
* follows `fn`'s (overloaded).
|
|
1897
|
-
* - **PROCESS-GLOBAL caveat.** Like {@link createCapture}, this patches the one global `console`.
|
|
1898
|
-
* Concurrent `withCapture` calls (or a `withCapture` around other capturing code) INTERLEAVE —
|
|
1899
|
-
* each captures every `console.*` call in flight, and the inner `stop()` restores whatever the
|
|
1900
|
-
* outer had installed. Use it for sequential, scoped capture, not overlapping captures.
|
|
1901
|
-
*
|
|
1902
|
-
* @example
|
|
1903
|
-
* ```ts
|
|
1904
|
-
* import { withCapture } from '@src/core'
|
|
1905
|
-
*
|
|
1906
|
-
* const { value, messages } = withCapture(() => {
|
|
1907
|
-
* console.log('working')
|
|
1908
|
-
* return 42
|
|
1909
|
-
* })
|
|
1910
|
-
* value // 42
|
|
1911
|
-
* messages.map((m) => m.text) // ['working']
|
|
1912
|
-
*
|
|
1913
|
-
* // Async — awaited before console is restored.
|
|
1914
|
-
* const out = await withCapture(async () => {
|
|
1915
|
-
* console.warn('async noise')
|
|
1916
|
-
* return 'done'
|
|
1917
|
-
* })
|
|
1918
|
-
* out.value // 'done'
|
|
1919
|
-
* ```
|
|
1920
|
-
*/
|
|
1921
|
-
function withCapture(fn, options) {
|
|
1922
|
-
const capture = new Capture(options);
|
|
1923
|
-
capture.start();
|
|
1924
|
-
try {
|
|
1925
|
-
const result = fn();
|
|
1926
|
-
if (result instanceof Promise) return result.then((value) => {
|
|
1927
|
-
const messages = capture.messages();
|
|
1928
|
-
capture.destroy();
|
|
1929
|
-
return {
|
|
1930
|
-
value,
|
|
1931
|
-
messages
|
|
1932
|
-
};
|
|
1933
|
-
}, (error) => {
|
|
1934
|
-
capture.destroy();
|
|
1935
|
-
throw error;
|
|
1936
|
-
});
|
|
1937
|
-
const messages = capture.messages();
|
|
1938
|
-
capture.destroy();
|
|
1939
|
-
return {
|
|
1940
|
-
value: result,
|
|
1941
|
-
messages
|
|
1942
|
-
};
|
|
1943
|
-
} catch (error) {
|
|
1944
|
-
capture.destroy();
|
|
1945
|
-
throw error;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
/**
|
|
1949
2146
|
* Create a self-driving, observable {@link SpinnerInterface} — a live activity spinner. `start()`
|
|
1950
2147
|
* arms a periodic timer that advances a glyph cycle, writing each `\r` + frame line to its sink and
|
|
1951
2148
|
* emitting it on `frame`; `success` / `failure` commit a final `✔` / `✖` line. The leading `\r` is the
|
|
@@ -2046,6 +2243,8 @@ var Logger = class {
|
|
|
2046
2243
|
#level;
|
|
2047
2244
|
#sink;
|
|
2048
2245
|
#styler;
|
|
2246
|
+
#theme;
|
|
2247
|
+
#format;
|
|
2049
2248
|
#limit;
|
|
2050
2249
|
#silent;
|
|
2051
2250
|
#entries = [];
|
|
@@ -2059,6 +2258,8 @@ var Logger = class {
|
|
|
2059
2258
|
if (options?.name !== void 0) this.name = options.name;
|
|
2060
2259
|
this.#sink = options?.sink ?? createConsoleSink();
|
|
2061
2260
|
this.#styler = options?.styler ?? createStyler();
|
|
2261
|
+
this.#theme = options?.theme ?? DEFAULT_THEME;
|
|
2262
|
+
this.#format = options?.format ?? formatRecord;
|
|
2062
2263
|
this.#limit = options?.limit ?? 1e3;
|
|
2063
2264
|
this.#silent = options?.silent ?? false;
|
|
2064
2265
|
}
|
|
@@ -2096,7 +2297,7 @@ var Logger = class {
|
|
|
2096
2297
|
this.#retain(record);
|
|
2097
2298
|
this.#emitter.emit("entry", record);
|
|
2098
2299
|
if (this.#silent) return;
|
|
2099
|
-
this.#sink.write(
|
|
2300
|
+
this.#sink.write(this.#format(record, this.#styler, this.#theme), level);
|
|
2100
2301
|
}
|
|
2101
2302
|
#record(level, message, data) {
|
|
2102
2303
|
return Object.freeze({
|
|
@@ -2138,6 +2339,7 @@ exports.DEFAULT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
|
|
|
2138
2339
|
exports.DEFAULT_LOG_LIMIT = DEFAULT_LOG_LIMIT;
|
|
2139
2340
|
exports.DEFAULT_PADDING = DEFAULT_PADDING;
|
|
2140
2341
|
exports.DEFAULT_SPINNER_INTERVAL = DEFAULT_SPINNER_INTERVAL;
|
|
2342
|
+
exports.DEFAULT_THEME = DEFAULT_THEME;
|
|
2141
2343
|
exports.DEFAULT_WIDTH = DEFAULT_WIDTH;
|
|
2142
2344
|
exports.EMPTY_STYLE = EMPTY_STYLE;
|
|
2143
2345
|
exports.ESC = ESC;
|
|
@@ -2159,8 +2361,6 @@ exports.STATUS_COLORS = STATUS_COLORS;
|
|
|
2159
2361
|
exports.STATUS_ICONS = STATUS_ICONS;
|
|
2160
2362
|
exports.STATUS_LEVELS = STATUS_LEVELS;
|
|
2161
2363
|
exports.Spinner = Spinner;
|
|
2162
|
-
exports.Styler = Styler;
|
|
2163
|
-
exports.TREE_CHARS = TREE_CHARS;
|
|
2164
2364
|
exports.align = align;
|
|
2165
2365
|
exports.cellAt = cellAt;
|
|
2166
2366
|
exports.createANSIRenderer = createANSIRenderer;
|
|
@@ -2172,10 +2372,12 @@ exports.createProgress = createProgress;
|
|
|
2172
2372
|
exports.createReporter = createReporter;
|
|
2173
2373
|
exports.createSpinner = createSpinner;
|
|
2174
2374
|
exports.createStyler = createStyler;
|
|
2375
|
+
exports.createTheme = createTheme;
|
|
2175
2376
|
exports.formatArgs = formatArgs;
|
|
2176
2377
|
exports.formatDuration = formatDuration;
|
|
2177
2378
|
exports.formatRecord = formatRecord;
|
|
2178
2379
|
exports.formatTime = formatTime;
|
|
2380
|
+
exports.freezeStyle = freezeStyle;
|
|
2179
2381
|
exports.isConsoleError = isConsoleError;
|
|
2180
2382
|
exports.meetsLevel = meetsLevel;
|
|
2181
2383
|
exports.paint = paint;
|