@orkestrel/console 0.0.6 → 0.0.8

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.
@@ -287,17 +287,6 @@ var STATUS_LEVELS = Object.freeze([
287
287
  "info"
288
288
  ]);
289
289
  /**
290
- * The tree connectors {@link import('./helpers.js').renderTree} draws — the `├─` branch (a
291
- * non-last child), the `└─` corner (the last child), the `│ ` guide (carried down through an
292
- * earlier branch's descendants), and the ` ` gap (under a last branch). Frozen.
293
- */
294
- var TREE_CHARS = Object.freeze({
295
- branch: "├─ ",
296
- corner: "└─ ",
297
- guide: "│ ",
298
- gap: " "
299
- });
300
- /**
301
290
  * The default visible column width for the width-aware renderers — the separator rule and a
302
291
  * {@link import('./helpers.js').renderBox} with no explicit `width`, and the reporter's
303
292
  * `section` rule. A sane terminal default (80 columns); a caller overrides it per-call or via
@@ -413,6 +402,74 @@ var BAR_EMPTY = "░";
413
402
  * track is one inline element, not a full-width rule.
414
403
  */
415
404
  var DEFAULT_BAR_WIDTH = 30;
405
+ /**
406
+ * The default {@link Theme} — every role bound to its default {@link Style}, deeply frozen.
407
+ * The base {@link import('./factories.js').createTheme} merges over, and the theme every
408
+ * entity uses when none is supplied.
409
+ *
410
+ * @remarks
411
+ * - `levels` — each {@link LogLevel} label in its {@link LEVEL_COLORS} color, no attributes.
412
+ * - `statuses` — each {@link StatusLevel}'s {@link STATUS_ICONS} glyph in its
413
+ * {@link STATUS_COLORS} color.
414
+ * - `accent` — `cyan`: the spinner glyph, the progress fill, a step prefix.
415
+ * - `chrome` — `dim`: separators, box / table / tree frames, and a log line's timestamp /
416
+ * name / data surround. A color-free attribute, so chrome recedes on any background.
417
+ */
418
+ var DEFAULT_THEME = Object.freeze({
419
+ levels: Object.freeze({
420
+ debug: Object.freeze({
421
+ foreground: LEVEL_COLORS.debug,
422
+ attributes: EMPTY_STYLE.attributes
423
+ }),
424
+ info: Object.freeze({
425
+ foreground: LEVEL_COLORS.info,
426
+ attributes: EMPTY_STYLE.attributes
427
+ }),
428
+ warn: Object.freeze({
429
+ foreground: LEVEL_COLORS.warn,
430
+ attributes: EMPTY_STYLE.attributes
431
+ }),
432
+ error: Object.freeze({
433
+ foreground: LEVEL_COLORS.error,
434
+ attributes: EMPTY_STYLE.attributes
435
+ })
436
+ }),
437
+ statuses: Object.freeze({
438
+ success: Object.freeze({
439
+ icon: STATUS_ICONS.success,
440
+ style: Object.freeze({
441
+ foreground: STATUS_COLORS.success,
442
+ attributes: EMPTY_STYLE.attributes
443
+ })
444
+ }),
445
+ error: Object.freeze({
446
+ icon: STATUS_ICONS.error,
447
+ style: Object.freeze({
448
+ foreground: STATUS_COLORS.error,
449
+ attributes: EMPTY_STYLE.attributes
450
+ })
451
+ }),
452
+ warn: Object.freeze({
453
+ icon: STATUS_ICONS.warn,
454
+ style: Object.freeze({
455
+ foreground: STATUS_COLORS.warn,
456
+ attributes: EMPTY_STYLE.attributes
457
+ })
458
+ }),
459
+ info: Object.freeze({
460
+ icon: STATUS_ICONS.info,
461
+ style: Object.freeze({
462
+ foreground: STATUS_COLORS.info,
463
+ attributes: EMPTY_STYLE.attributes
464
+ })
465
+ })
466
+ }),
467
+ accent: Object.freeze({
468
+ foreground: "cyan",
469
+ attributes: EMPTY_STYLE.attributes
470
+ }),
471
+ chrome: Object.freeze({ attributes: Object.freeze(["dim"]) })
472
+ });
416
473
  //#endregion
417
474
  //#region src/core/errors.ts
418
475
  /**
@@ -639,6 +696,27 @@ function width(text) {
639
696
  return [...strip(text)].length;
640
697
  }
641
698
  /**
699
+ * Snapshot and deeply freeze one {@link Style} value.
700
+ *
701
+ * @param style - The caller-owned style to snapshot
702
+ * @returns A frozen style record with an independently frozen attributes list
703
+ *
704
+ * @remarks
705
+ * The record spread captures accessor values once and preserves each present color channel.
706
+ * Copying `attributes` prevents later mutation of a caller-owned list from changing the result.
707
+ *
708
+ * @example
709
+ * ```ts
710
+ * freezeStyle({ foreground: 'red', attributes: ['bold'] })
711
+ * ```
712
+ */
713
+ function freezeStyle(style) {
714
+ return Object.freeze({
715
+ ...style,
716
+ attributes: Object.freeze([...style.attributes])
717
+ });
718
+ }
719
+ /**
642
720
  * Whether a record at `level` passes a logger gated at `threshold` — i.e. its severity is
643
721
  * at or above the threshold's.
644
722
  *
@@ -680,7 +758,7 @@ function formatTime(time) {
680
758
  *
681
759
  * @remarks
682
760
  * Layout: `{time} {LEVEL} {[name]} {message}{ data}` — the ISO timestamp (dimmed), the
683
- * upper-cased level label (colored by {@link LEVEL_COLORS} styling ORTHOGONAL to level),
761
+ * upper-cased level label (rendered through the theme's level role),
684
762
  * the originating logger's `name` in brackets (omitted when absent), the message, and the
685
763
  * structured `data` appended as compact JSON (omitted when absent / empty). Coloring flows
686
764
  * through the injected `styler`, so a disabled styler yields a plain line and a browser
@@ -689,19 +767,24 @@ function formatTime(time) {
689
767
  *
690
768
  * @param record - The {@link LogRecord} to render
691
769
  * @param styler - The {@link StylerInterface} the labels are colored through
770
+ * @param theme - The {@link Theme} supplying the level and chrome roles
692
771
  * @returns The formatted, styled line (no trailing newline — the sink's target adds it)
693
772
  *
694
773
  * @example
695
774
  * ```ts
696
- * formatRecord({ level: 'warn', message: 'low disk', time: 0, name: 'fs' }, createStyler())
775
+ * formatRecord(
776
+ * { level: 'warn', message: 'low disk', time: 0, name: 'fs' },
777
+ * createStyler(),
778
+ * DEFAULT_THEME,
779
+ * )
697
780
  * // '<dim>1970-01-01T00:00:00.000Z</> <yellow>WARN</> [fs] low disk'
698
781
  * ```
699
782
  */
700
- function formatRecord(record, styler) {
701
- const time = styler.dim(formatTime(record.time));
702
- const label = styler[LEVEL_COLORS[record.level]](record.level.toUpperCase());
703
- const name = record.name === void 0 ? "" : ` ${styler.dim(`[${record.name}]`)}`;
704
- const data = record.data === void 0 || Object.keys(record.data).length === 0 ? "" : ` ${styler.dim(JSON.stringify(record.data))}`;
783
+ function formatRecord(record, styler, theme) {
784
+ const time = styler.render(theme.chrome, formatTime(record.time));
785
+ const label = styler.render(theme.levels[record.level], record.level.toUpperCase());
786
+ const name = record.name === void 0 ? "" : ` ${styler.render(theme.chrome, `[${record.name}]`)}`;
787
+ const data = record.data === void 0 || Object.keys(record.data).length === 0 ? "" : ` ${styler.render(theme.chrome, JSON.stringify(record.data))}`;
705
788
  return `${time} ${label}${name} ${record.message}${data}`;
706
789
  }
707
790
  /**
@@ -768,10 +851,12 @@ function formatDuration(ms) {
768
851
  *
769
852
  * @param styler - The {@link StylerInterface} to color with, or `undefined` for no styling
770
853
  * @param text - The glyphs / text to color
854
+ * @param style - An optional {@link Style} to render by value instead of the styler's chain
771
855
  * @returns `styler(text)` when a styler is given, else `text` unchanged
772
856
  */
773
- function paint(styler, text) {
774
- return styler === void 0 ? text : styler(text);
857
+ function paint(styler, text, style) {
858
+ if (styler === void 0) return text;
859
+ return style === void 0 ? styler(text) : styler.render(style, text);
775
860
  }
776
861
  /**
777
862
  * Repeat `unit` until it fills exactly `count` VISIBLE columns, trimming a trailing partial
@@ -837,12 +922,12 @@ function cellAt(row, index) {
837
922
  function renderSeparator(options) {
838
923
  const total = options.width ?? 80;
839
924
  const fill = options.fill ?? "─";
840
- if (options.title === void 0) return paint(options.styler, repeatTo(fill, total));
841
- const gapped = ` ${paint(options.styler, options.title)} `;
925
+ if (options.title === void 0) return paint(options.styler, repeatTo(fill, total), options.style);
926
+ const gapped = ` ${paint(options.styler, options.title, options.style)} `;
842
927
  const room = total - width(options.title) - 2;
843
928
  if (room <= 0) return gapped;
844
929
  const left = Math.floor(room / 2);
845
- return `${paint(options.styler, repeatTo(fill, left))}${gapped}${paint(options.styler, repeatTo(fill, room - left))}`;
930
+ return `${paint(options.styler, repeatTo(fill, left), options.style)}${gapped}${paint(options.styler, repeatTo(fill, room - left), options.style)}`;
846
931
  }
847
932
  /**
848
933
  * Render `content` framed in box-drawing characters, optionally captioned, width-aware so
@@ -874,18 +959,18 @@ function renderBox(options) {
874
959
  const budget = options.width === void 0 ? 0 : options.width - 2 - padding * 2;
875
960
  const inner = lines.reduce((max, line) => Math.max(max, width(line)), Math.max(0, titleRoom, budget));
876
961
  const gutter = " ".repeat(padding);
877
- const bar = paint(styler, chars.vertical);
962
+ const bar = paint(styler, chars.vertical, options.style);
878
963
  const span = inner + padding * 2;
879
964
  let top;
880
- if (options.title === void 0) top = paint(styler, `${chars.topLeft}${repeatTo(chars.horizontal, span)}${chars.topRight}`);
965
+ if (options.title === void 0) top = paint(styler, `${chars.topLeft}${repeatTo(chars.horizontal, span)}${chars.topRight}`, options.style);
881
966
  else {
882
967
  const caption = ` ${options.title} `;
883
968
  const room = span - width(caption);
884
- const lead = paint(styler, repeatTo(chars.horizontal, 1));
885
- const rest = room - 1 <= 0 ? "" : paint(styler, repeatTo(chars.horizontal, room - 1));
886
- top = `${paint(styler, chars.topLeft)}${lead}${paint(styler, caption)}${rest}${paint(styler, chars.topRight)}`;
969
+ const lead = paint(styler, repeatTo(chars.horizontal, 1), options.style);
970
+ const rest = room - 1 <= 0 ? "" : paint(styler, repeatTo(chars.horizontal, room - 1), options.style);
971
+ top = `${paint(styler, chars.topLeft, options.style)}${lead}${paint(styler, caption, options.style)}${rest}${paint(styler, chars.topRight, options.style)}`;
887
972
  }
888
- const bottom = paint(styler, `${chars.bottomLeft}${repeatTo(chars.horizontal, inner + padding * 2)}${chars.bottomRight}`);
973
+ const bottom = paint(styler, `${chars.bottomLeft}${repeatTo(chars.horizontal, inner + padding * 2)}${chars.bottomRight}`, options.style);
889
974
  const body = lines.map((line) => `${bar}${gutter}${align(line, inner)}${gutter}${bar}`);
890
975
  return [
891
976
  top,
@@ -920,14 +1005,14 @@ function renderTable(options) {
920
1005
  const columns = options.columns;
921
1006
  const widths = columns.map((column, index) => options.rows.reduce((max, row) => Math.max(max, width(cellAt(row, index))), width(column.label)));
922
1007
  const aligns = columns.map((column) => column.align ?? "left");
923
- const renderedRows = [columns.map((column) => paint(styler, column.label)), ...options.rows.map((row) => columns.map((_column, index) => cellAt(row, index)))].map((cells) => {
924
- const bar = paint(styler, chars.vertical);
1008
+ const renderedRows = [columns.map((column) => paint(styler, column.label, options.style)), ...options.rows.map((row) => columns.map((_column, index) => cellAt(row, index)))].map((cells) => {
1009
+ const bar = paint(styler, chars.vertical, options.style);
925
1010
  return `${bar}${cells.map((cell, index) => ` ${align(cell, widths[index] ?? width(cell), aligns[index] ?? "left")} `).join(bar)}${bar}`;
926
1011
  });
927
1012
  const segments = widths.map((columnWidth) => repeatTo(chars.horizontal, columnWidth + 2));
928
- const top = paint(styler, `${chars.topLeft}${segments.join(chars.teeDown)}${chars.topRight}`);
929
- const rule = paint(styler, `${chars.teeRight}${segments.join(chars.cross)}${chars.teeLeft}`);
930
- const bottom = paint(styler, `${chars.bottomLeft}${segments.join(chars.teeUp)}${chars.bottomRight}`);
1013
+ const top = paint(styler, `${chars.topLeft}${segments.join(chars.teeDown)}${chars.topRight}`, options.style);
1014
+ const rule = paint(styler, `${chars.teeRight}${segments.join(chars.cross)}${chars.teeLeft}`, options.style);
1015
+ const bottom = paint(styler, `${chars.bottomLeft}${segments.join(chars.teeUp)}${chars.bottomRight}`, options.style);
931
1016
  return [
932
1017
  top,
933
1018
  ...renderedRows.slice(0, 1),
@@ -942,7 +1027,7 @@ function renderTable(options) {
942
1027
  *
943
1028
  * @remarks
944
1029
  * The `root` label is the unindented first line; its descendants are drawn beneath it with
945
- * {@link TREE_CHARS} — `├─ ` before each child but the last, `└─ ` before the last, and the
1030
+ * {@link BORDER_CHARS} — `├─ ` before each child but the last, `└─ ` before the last, and the
946
1031
  * carried prefix using `│ ` under an ancestor that still has later siblings or ` ` under a
947
1032
  * last ancestor (so the guides line up exactly under the branch they descend from). Node
948
1033
  * labels are written as given (an already-styled label is honored); `options.styler` colors
@@ -960,7 +1045,11 @@ function renderTable(options) {
960
1045
  * ```
961
1046
  */
962
1047
  function renderTree(options) {
963
- return [options.root.label, ...renderTreeChildren(options.root.children ?? [], "", options.styler)].join("\n");
1048
+ const border = options.border ?? "single";
1049
+ return [options.root.label, ...renderTreeChildren(options.root.children ?? [], "", {
1050
+ ...options,
1051
+ border
1052
+ })].join("\n");
964
1053
  }
965
1054
  /**
966
1055
  * Render the connector-prefixed lines for a {@link TreeNode} list — the recursive core
@@ -974,22 +1063,27 @@ function renderTree(options) {
974
1063
  *
975
1064
  * @param nodes - The sibling {@link TreeNode}s to render at this depth
976
1065
  * @param prefix - The guide/gap string carried in from the ancestor chain (`''` at the root)
977
- * @param styler - The {@link StylerInterface} connectors are colored through, when supplied
1066
+ * @param options - The required `border` selection plus optional connector `styler` and
1067
+ * by-value `style`
978
1068
  * @returns The rendered lines for `nodes` and all their descendants
979
1069
  *
980
1070
  * @example
981
1071
  * ```ts
982
- * renderTreeChildren([{ label: 'a' }, { label: 'b' }], '')
1072
+ * renderTreeChildren([{ label: 'a' }, { label: 'b' }], '', { border: 'single' })
983
1073
  * // ['├─ a', '└─ b']
984
1074
  * ```
985
1075
  */
986
- function renderTreeChildren(nodes, prefix, styler) {
1076
+ function renderTreeChildren(nodes, prefix, options) {
1077
+ const chars = BORDER_CHARS[options.border];
1078
+ const branch = `${chars.teeRight}${chars.horizontal} `;
1079
+ const corner = `${chars.bottomLeft}${chars.horizontal} `;
1080
+ const guide = `${chars.vertical} `;
987
1081
  const lines = [];
988
1082
  nodes.forEach((node, index) => {
989
1083
  const last = index === nodes.length - 1;
990
- lines.push(`${prefix}${paint(styler, last ? TREE_CHARS.corner : TREE_CHARS.branch)}${node.label}`);
991
- const carry = `${prefix}${paint(styler, last ? TREE_CHARS.gap : TREE_CHARS.guide)}`;
992
- lines.push(...renderTreeChildren(node.children ?? [], carry, styler));
1084
+ lines.push(`${prefix}${paint(options.styler, last ? corner : branch, options.style)}${node.label}`);
1085
+ const carry = `${prefix}${paint(options.styler, last ? " " : guide, options.style)}`;
1086
+ lines.push(...renderTreeChildren(node.children ?? [], carry, options));
993
1087
  });
994
1088
  return lines;
995
1089
  }
@@ -1096,7 +1190,7 @@ function renderBar(options) {
1096
1190
  const current = options.total <= 0 ? 0 : Math.max(0, Math.min(options.total, options.current));
1097
1191
  const fraction = options.total <= 0 ? 1 : current / options.total;
1098
1192
  const filledCells = Math.round(fraction * track);
1099
- return `${`${paint(options.styler, repeatTo(fill, filledCells))}${repeatTo(empty, track - filledCells)}`} ${Math.round(fraction * 100)}% (${current}/${options.total})`;
1193
+ return `${`${paint(options.styler, repeatTo(fill, filledCells), options.style)}${repeatTo(empty, track - filledCells)}`} ${Math.round(fraction * 100)}% (${current}/${options.total})`;
1100
1194
  }
1101
1195
  /**
1102
1196
  * Run `fn` with the global `console.*` captured for its duration, returning the function's `value`
@@ -1220,17 +1314,19 @@ var ANSIRenderer = class {
1220
1314
  * @remarks
1221
1315
  * - **Registry (§9).** Loggers live in an insertion-ordered `Map` keyed by `name`.
1222
1316
  * `register(name, options?)` mints a {@link Logger} named `name` — the manager's default
1223
- * `level` / `sink` / `styler` / `limit` / `silent` flow in unless `options` OVERRIDES them
1317
+ * `level` / `sink` / `styler` / `theme` / `format` / `limit` / `silent` flow in unless
1318
+ * `options` OVERRIDES them
1224
1319
  * (`name` is always the registry key, so any `options.name` is ignored) — stores it (a
1225
1320
  * re-`register` of the same name OVERWRITES, last write wins), and returns it. `count` is
1226
1321
  * the map size, `logger(name)` looks one up, `loggers()` lists them in insertion order.
1227
1322
  * - **Removal (§9.2).** `remove()` clears ALL, `remove(name)` drops ONE (`true` if present),
1228
- * `remove(names)` drops a batch (`true` if any was removed). `clear()` empties the registry.
1323
+ * `remove(names)` drops a batch (`true` if any was removed).
1229
1324
  * (Removal does NOT `destroy` the returned loggers — a caller still holding one keeps using
1230
1325
  * it; the manager simply stops tracking it.)
1231
1326
  * - **Fan-out.** `debug` / `info` / `warn` / `error(message, data?)` forward the one call to
1232
- * EVERY registered logger; each gates / emits / writes per its own `level` and `sink`. A
1233
- * fan-out over an empty registry is a no-op.
1327
+ * every registered logger in insertion order; each gates / emits / writes per its own `level`
1328
+ * and `sink`. A formatter throw is a programmer error and propagates, stopping the remaining
1329
+ * loggers for that call. A fan-out over an empty registry is a no-op.
1234
1330
  * - **Event-free.** No emitter, no events — the manager is a pure registry; observability is
1235
1331
  * per-{@link Logger}.
1236
1332
  *
@@ -1248,12 +1344,16 @@ var LoggerManager = class {
1248
1344
  #level;
1249
1345
  #sink;
1250
1346
  #styler;
1347
+ #theme;
1348
+ #format;
1251
1349
  #limit;
1252
1350
  #silent;
1253
1351
  constructor(options) {
1254
1352
  this.#level = options?.level;
1255
1353
  this.#sink = options?.sink;
1256
1354
  this.#styler = options?.styler;
1355
+ this.#theme = options?.theme;
1356
+ this.#format = options?.format;
1257
1357
  this.#limit = options?.limit;
1258
1358
  this.#silent = options?.silent;
1259
1359
  }
@@ -1265,6 +1365,8 @@ var LoggerManager = class {
1265
1365
  ...this.#level !== void 0 ? { level: this.#level } : {},
1266
1366
  ...this.#sink !== void 0 ? { sink: this.#sink } : {},
1267
1367
  ...this.#styler !== void 0 ? { styler: this.#styler } : {},
1368
+ ...this.#theme !== void 0 ? { theme: this.#theme } : {},
1369
+ ...this.#format !== void 0 ? { format: this.#format } : {},
1268
1370
  ...this.#limit !== void 0 ? { limit: this.#limit } : {},
1269
1371
  ...this.#silent !== void 0 ? { silent: this.#silent } : {},
1270
1372
  ...options,
@@ -1303,9 +1405,6 @@ var LoggerManager = class {
1303
1405
  }
1304
1406
  return this.#loggers.delete(names);
1305
1407
  }
1306
- clear() {
1307
- this.#loggers.clear();
1308
- }
1309
1408
  };
1310
1409
  //#endregion
1311
1410
  //#region src/core/Progress.ts
@@ -1341,8 +1440,11 @@ var Progress = class {
1341
1440
  #emitter;
1342
1441
  #total;
1343
1442
  #width;
1443
+ #fill;
1444
+ #empty;
1344
1445
  #sink;
1345
1446
  #styler;
1447
+ #theme;
1346
1448
  #message;
1347
1449
  #current = 0;
1348
1450
  #active = true;
@@ -1354,8 +1456,11 @@ var Progress = class {
1354
1456
  });
1355
1457
  this.#total = options.total;
1356
1458
  this.#width = options.width ?? 30;
1459
+ this.#fill = options.fill;
1460
+ this.#empty = options.empty;
1357
1461
  this.#sink = options.sink ?? createConsoleSink();
1358
1462
  this.#styler = options.styler ?? createStyler();
1463
+ this.#theme = options.theme ?? DEFAULT_THEME;
1359
1464
  this.#message = options.message ?? "";
1360
1465
  }
1361
1466
  get emitter() {
@@ -1408,7 +1513,10 @@ var Progress = class {
1408
1513
  current: this.#current,
1409
1514
  total: this.#total,
1410
1515
  width: this.#width,
1411
- styler: this.#styler.cyan
1516
+ ...this.#fill === void 0 ? {} : { fill: this.#fill },
1517
+ ...this.#empty === void 0 ? {} : { empty: this.#empty },
1518
+ styler: this.#styler,
1519
+ style: this.#theme.accent
1412
1520
  });
1413
1521
  const line = this.#message === "" ? bar : `${bar} ${this.#message}`;
1414
1522
  this.#sink.write(`\r${line}${final ? "\n" : ""}`, level);
@@ -1429,7 +1537,7 @@ var Progress = class {
1429
1537
  * capture (the capture chunk), no level retention (the logger). Just format + write.
1430
1538
  * - **`status` is a narrative OUTCOME, not a log level.** Its {@link StatusLevel} (`success` /
1431
1539
  * `error` / `warn` / `info`) is distinct from {@link import('./types.js').LogLevel}: an icon
1432
- * ({@link STATUS_ICONS}) + a color ({@link STATUS_COLORS}), with `error` routed to the sink's
1540
+ * supplied theme status icon + style, with `error` routed to the sink's
1433
1541
  * error stream (the `level` hint forwarded to {@link SinkInterface.write}) — there is no
1434
1542
  * gating and no severity ordering.
1435
1543
  * - **Width-aware.** `section` (and a `box` with no explicit `width`) lay out to the reporter's
@@ -1449,49 +1557,45 @@ var Progress = class {
1449
1557
  var Reporter = class {
1450
1558
  #sink;
1451
1559
  #styler;
1560
+ #theme;
1452
1561
  #width;
1453
1562
  constructor(options) {
1454
1563
  this.#sink = options?.sink ?? createConsoleSink();
1455
1564
  this.#styler = options?.styler ?? createStyler();
1565
+ this.#theme = options?.theme ?? DEFAULT_THEME;
1456
1566
  this.#width = options?.width ?? 80;
1457
1567
  }
1458
1568
  section(title) {
1459
1569
  this.#sink.write(renderSeparator({
1460
1570
  title,
1461
1571
  width: this.#width,
1462
- styler: this.#styler.dim
1572
+ styler: this.#styler,
1573
+ style: this.#theme.chrome
1463
1574
  }));
1464
1575
  }
1465
1576
  step(message, position) {
1466
- const prefix = position === void 0 ? "" : `${this.#styler.cyan(`[${position.index}/${position.total}]`)} `;
1577
+ const prefix = position === void 0 ? "" : `${this.#styler.render(this.#theme.accent, `[${position.index}/${position.total}]`)} `;
1467
1578
  this.#sink.write(`${prefix}${message}`);
1468
1579
  }
1469
1580
  timing(label, ms) {
1470
- this.#sink.write(`${label} ${this.#styler.dim(`… ${formatDuration(ms)}`)}`);
1581
+ this.#sink.write(`${label} ${this.#styler.render(this.#theme.chrome, `… ${formatDuration(ms)}`)}`);
1471
1582
  }
1472
1583
  status(level, message) {
1473
- const color = this.#styler[STATUS_COLORS[level]];
1474
- const line = `${color(STATUS_ICONS[level])} ${color(message)}`;
1584
+ const status = this.#theme.statuses[level];
1585
+ const line = `${this.#styler.render(status.style, status.icon)} ${this.#styler.render(status.style, message)}`;
1475
1586
  this.#sink.write(line, level === "error" ? "error" : void 0);
1476
1587
  }
1477
1588
  table(options) {
1478
- this.#sink.write(renderTable({
1479
- styler: this.#styler.dim,
1480
- ...options
1481
- }));
1589
+ this.#sink.write(renderTable(this.#resolveStyle(options)));
1482
1590
  }
1483
1591
  tree(options) {
1484
- this.#sink.write(renderTree({
1485
- styler: this.#styler.dim,
1486
- ...options
1487
- }));
1592
+ this.#sink.write(renderTree(this.#resolveStyle(options)));
1488
1593
  }
1489
1594
  box(options) {
1490
- this.#sink.write(renderBox({
1595
+ this.#sink.write(renderBox(this.#resolveStyle({
1491
1596
  width: this.#width,
1492
- styler: this.#styler.dim,
1493
1597
  ...options
1494
- }));
1598
+ })));
1495
1599
  }
1496
1600
  line(text) {
1497
1601
  this.#sink.write(text);
@@ -1499,6 +1603,13 @@ var Reporter = class {
1499
1603
  blank(count = 1) {
1500
1604
  for (let index = 0; index < count; index += 1) this.#sink.write("");
1501
1605
  }
1606
+ #resolveStyle(options) {
1607
+ return {
1608
+ styler: this.#styler,
1609
+ ...options.styler === void 0 && options.style === void 0 ? { style: this.#theme.chrome } : {},
1610
+ ...options
1611
+ };
1612
+ }
1502
1613
  };
1503
1614
  //#endregion
1504
1615
  //#region src/core/Spinner.ts
@@ -1522,7 +1633,7 @@ var Reporter = class {
1522
1633
  * - **Idempotent `start`.** A {@link start} while already `active` is a no-op (it never arms a second
1523
1634
  * timer).
1524
1635
  * - **Outcome lines.** {@link success} / {@link failure} clear the timer then write + emit a FINAL line —
1525
- * the {@link STATUS_ICONS} `✔` / `✖` (colored via {@link STATUS_COLORS}) + the message — terminated
1636
+ * the supplied theme status icon + style (`✔` / `✖` by default) + the message — terminated
1526
1637
  * by a newline (the activity is over; the line is committed, not overwritten). {@link failure} routes to
1527
1638
  * the sink's error stream.
1528
1639
  * - **Lifecycle (§10).** {@link stop} clears the timer and LEAVES the current line; {@link destroy}
@@ -1543,6 +1654,7 @@ var Spinner = class {
1543
1654
  #interval;
1544
1655
  #sink;
1545
1656
  #styler;
1657
+ #theme;
1546
1658
  #message;
1547
1659
  #handle;
1548
1660
  #index = 0;
@@ -1556,6 +1668,7 @@ var Spinner = class {
1556
1668
  this.#interval = options?.interval ?? 80;
1557
1669
  this.#sink = options?.sink ?? createConsoleSink();
1558
1670
  this.#styler = options?.styler ?? createStyler();
1671
+ this.#theme = options?.theme ?? DEFAULT_THEME;
1559
1672
  this.#message = options?.message ?? "";
1560
1673
  }
1561
1674
  get emitter() {
@@ -1601,13 +1714,13 @@ var Spinner = class {
1601
1714
  this.stop();
1602
1715
  const text = message ?? this.#message;
1603
1716
  if (message !== void 0) this.#message = message;
1604
- const color = this.#styler[STATUS_COLORS[level]];
1605
- const line = `${color(STATUS_ICONS[level])} ${color(text)}`;
1717
+ const status = this.#theme.statuses[level];
1718
+ const line = `${this.#styler.render(status.style, status.icon)} ${this.#styler.render(status.style, text)}`;
1606
1719
  this.#emitter.emit("frame", line);
1607
1720
  this.#sink.write(`\r${line}\n`, level === "error" ? "error" : void 0);
1608
1721
  }
1609
1722
  #line() {
1610
- const glyph = this.#styler.cyan(this.#frames[this.#index] ?? "");
1723
+ const glyph = this.#styler.render(this.#theme.accent, this.#frames[this.#index] ?? "");
1611
1724
  return this.#message === "" ? glyph : `${glyph} ${this.#message}`;
1612
1725
  }
1613
1726
  #paint(line) {
@@ -1635,6 +1748,9 @@ var Spinner = class {
1635
1748
  * - **Immutable.** `#foreground` and `#attribute` return a fresh `Styler` (the style is
1636
1749
  * rebuilt, never mutated). A later color of the same channel WINS (last write); a
1637
1750
  * repeated attribute is idempotent (de-duplicated, order preserved).
1751
+ * - **Styling by value.** `render(style, text)` merges a {@link Style} over the accumulated
1752
+ * one and renders that — the same precedence a chain applies, reached with DATA instead of
1753
+ * accessor names. It is how a {@link import('./types.js').Theme} role is drawn.
1638
1754
  * - **`enabled` switch.** When `false`, the render function returns text VERBATIM — no
1639
1755
  * renderer call, no escape codes (for a non-TTY / `NO_COLOR` / piped output).
1640
1756
  * - **Event-free** — a pure styling primitive (AGENTS §13), like `Scheduler`.
@@ -1670,7 +1786,7 @@ var Styler = class Styler {
1670
1786
  * type assertion is used (AGENTS §1 / §14 — narrow, never assert).
1671
1787
  */
1672
1788
  get surface() {
1673
- const render = this.#render.bind(this);
1789
+ const callable = this.#render.bind(this);
1674
1790
  const descriptors = {
1675
1791
  style: {
1676
1792
  value: this.#style,
@@ -1679,6 +1795,10 @@ var Styler = class Styler {
1679
1795
  enabled: {
1680
1796
  value: this.#enabled,
1681
1797
  enumerable: true
1798
+ },
1799
+ render: {
1800
+ value: this.render.bind(this),
1801
+ enumerable: true
1682
1802
  }
1683
1803
  };
1684
1804
  for (const color of COLORS) descriptors[color] = {
@@ -1689,13 +1809,44 @@ var Styler = class Styler {
1689
1809
  get: this.#attributeSurface.bind(this, attribute),
1690
1810
  enumerable: true
1691
1811
  };
1692
- const surface = Object.defineProperties(render, descriptors);
1812
+ const surface = Object.defineProperties(callable, descriptors);
1693
1813
  if (this.#isSurface(surface)) return surface;
1694
1814
  throw new ConsoleError("INVARIANT", "console: styler surface construction is incomplete");
1695
1815
  }
1816
+ /**
1817
+ * Render `text` in `style` merged OVER the accumulated style — the by-value door beside
1818
+ * the accessor chain, and how a {@link import('./types.js').Theme} role is applied.
1819
+ *
1820
+ * @param style - The style to overlay; its colors win over the accumulated ones and its
1821
+ * attributes join them (de-duplicated, the accumulated ones first)
1822
+ * @param text - The text to wrap
1823
+ * @returns The rendered text — verbatim when `enabled` is `false`, and (by the
1824
+ * {@link RendererInterface} contract) when the merged style or `text` is empty
1825
+ *
1826
+ * @example
1827
+ * ```ts
1828
+ * import { createStyler, DEFAULT_THEME } from '@src/core'
1829
+ *
1830
+ * const styler = createStyler()
1831
+ * styler.render(DEFAULT_THEME.levels.warn, 'WARN') // yellow
1832
+ * styler.bold.render(DEFAULT_THEME.chrome, '│') // dim, over the accumulated bold
1833
+ * ```
1834
+ */
1835
+ render(style, text) {
1836
+ return this.#enabled ? this.#renderer.render(this.#merge(style), text) : text;
1837
+ }
1696
1838
  #render(text) {
1697
1839
  return this.#enabled ? this.#renderer.render(this.#style, text) : text;
1698
1840
  }
1841
+ #merge(style) {
1842
+ const attributes = [...this.#style.attributes];
1843
+ for (const attribute of style.attributes) if (!attributes.includes(attribute)) attributes.push(attribute);
1844
+ return Object.freeze({
1845
+ ...this.#style,
1846
+ ...style,
1847
+ attributes: Object.freeze(attributes)
1848
+ });
1849
+ }
1699
1850
  #foregroundSurface(color) {
1700
1851
  return this.#foreground(color).surface;
1701
1852
  }
@@ -1703,7 +1854,7 @@ var Styler = class Styler {
1703
1854
  return this.#attribute(attribute).surface;
1704
1855
  }
1705
1856
  #isSurface(value) {
1706
- return typeof value === "function" && "style" in value && "enabled" in value && "red" in value && "bold" in value;
1857
+ return typeof value === "function" && "style" in value && "enabled" in value && "render" in value && "red" in value && "bold" in value;
1707
1858
  }
1708
1859
  #foreground(color) {
1709
1860
  return new Styler(this.#renderer, this.#enabled, Object.freeze({
@@ -1773,6 +1924,52 @@ function createStyler(options) {
1773
1924
  return new Styler(options?.renderer ?? new ANSIRenderer(), options?.enabled ?? true, EMPTY_STYLE).surface;
1774
1925
  }
1775
1926
  /**
1927
+ * Create a {@link Theme} — the app-wide semantic style vocabulary, merged role by role over
1928
+ * {@link DEFAULT_THEME}. Hand one theme to a logger / reporter / spinner / progress and every
1929
+ * surface speaks it; omit `options` for the defaults.
1930
+ *
1931
+ * @param options - See {@link ThemeOptions}
1932
+ * @returns A frozen {@link Theme}
1933
+ *
1934
+ * @remarks
1935
+ * - **Merges per ROLE, not per theme.** An omitted role keeps its default, and `levels` /
1936
+ * `statuses` merge per ENTRY — `{ levels: { warn: … } }` restyles the `warn` label and
1937
+ * leaves `debug` / `info` / `error` untouched.
1938
+ * - **Frozen and shareable.** The factory snapshots and deep-freezes every style leaf. The
1939
+ * returned theme and its `levels` / `statuses` records are frozen. Each status record is also
1940
+ * copied and frozen. One theme is therefore safely shared across every entity.
1941
+ *
1942
+ * @example
1943
+ * ```ts
1944
+ * import { createStyler, createTheme } from '@src/core'
1945
+ *
1946
+ * const styler = createStyler()
1947
+ * const theme = createTheme({
1948
+ * levels: { warn: styler.brightYellow.bold.style }, // only the warn label changes
1949
+ * accent: styler.magenta.style, // spinner glyph, progress fill, step prefix
1950
+ * })
1951
+ * theme.levels.error // still the default red
1952
+ * ```
1953
+ */
1954
+ function createTheme(options) {
1955
+ const levels = { ...DEFAULT_THEME.levels };
1956
+ for (const level of LEVELS) levels[level] = freezeStyle(options?.levels?.[level] ?? DEFAULT_THEME.levels[level]);
1957
+ const statuses = { ...DEFAULT_THEME.statuses };
1958
+ for (const status of STATUS_LEVELS) {
1959
+ const source = options?.statuses?.[status] ?? DEFAULT_THEME.statuses[status];
1960
+ statuses[status] = Object.freeze({
1961
+ icon: source.icon,
1962
+ style: freezeStyle(source.style)
1963
+ });
1964
+ }
1965
+ return Object.freeze({
1966
+ levels: Object.freeze(levels),
1967
+ statuses: Object.freeze(statuses),
1968
+ accent: freezeStyle(options?.accent ?? DEFAULT_THEME.accent),
1969
+ chrome: freezeStyle(options?.chrome ?? DEFAULT_THEME.chrome)
1970
+ });
1971
+ }
1972
+ /**
1776
1973
  * Create the default {@link SinkInterface} — a console sink that routes by level and writes
1777
1974
  * through the `console` methods SNAPSHOTTED at creation. The default output target behind
1778
1975
  * {@link createLogger}.
@@ -2045,6 +2242,8 @@ var Logger = class {
2045
2242
  #level;
2046
2243
  #sink;
2047
2244
  #styler;
2245
+ #theme;
2246
+ #format;
2048
2247
  #limit;
2049
2248
  #silent;
2050
2249
  #entries = [];
@@ -2058,6 +2257,8 @@ var Logger = class {
2058
2257
  if (options?.name !== void 0) this.name = options.name;
2059
2258
  this.#sink = options?.sink ?? createConsoleSink();
2060
2259
  this.#styler = options?.styler ?? createStyler();
2260
+ this.#theme = options?.theme ?? DEFAULT_THEME;
2261
+ this.#format = options?.format ?? formatRecord;
2061
2262
  this.#limit = options?.limit ?? 1e3;
2062
2263
  this.#silent = options?.silent ?? false;
2063
2264
  }
@@ -2095,7 +2296,7 @@ var Logger = class {
2095
2296
  this.#retain(record);
2096
2297
  this.#emitter.emit("entry", record);
2097
2298
  if (this.#silent) return;
2098
- this.#sink.write(formatRecord(record, this.#styler), level);
2299
+ this.#sink.write(this.#format(record, this.#styler, this.#theme), level);
2099
2300
  }
2100
2301
  #record(level, message, data) {
2101
2302
  return Object.freeze({
@@ -2112,6 +2313,6 @@ var Logger = class {
2112
2313
  }
2113
2314
  };
2114
2315
  //#endregion
2115
- export { ANSIRenderer, ANSI_PATTERN, ATTRIBUTES, ATTRIBUTE_CODES, BACKGROUND_CODES, BAR_EMPTY, BAR_FILL, BEL, BORDER_CHARS, CAPTURE_LEVELS, CAPTURE_LEVEL_MAP, COLORS, CONTROL_PATTERN, CSI, Capture, ConsoleError, DEFAULT_ALIGN, DEFAULT_BAR_WIDTH, DEFAULT_BORDER, DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_LIMIT, DEFAULT_PADDING, DEFAULT_SPINNER_INTERVAL, DEFAULT_WIDTH, EMPTY_STYLE, ESC, FOREGROUND_CODES, LEVELS, LEVEL_COLORS, LEVEL_SEVERITY, Logger, LoggerManager, Progress, RESET, RESET_CODE, Reporter, SECOND_MS, SEPARATOR_FILL, SEPARATOR_TITLE_GAP, SPINNER_FRAMES, STATUS_COLORS, STATUS_ICONS, STATUS_LEVELS, Spinner, TREE_CHARS, align, cellAt, createANSIRenderer, createCapture, createConsoleSink, createLogger, createLoggerManager, createProgress, createReporter, createSpinner, createStyler, formatArgs, formatDuration, formatRecord, formatTime, isConsoleError, meetsLevel, paint, renderBar, renderBox, renderSeparator, renderTable, renderTree, renderTreeChildren, repeatTo, stringifyValue, strip, stripControls, width, withCapture };
2316
+ export { ANSIRenderer, ANSI_PATTERN, ATTRIBUTES, ATTRIBUTE_CODES, BACKGROUND_CODES, BAR_EMPTY, BAR_FILL, BEL, BORDER_CHARS, CAPTURE_LEVELS, CAPTURE_LEVEL_MAP, COLORS, CONTROL_PATTERN, CSI, Capture, ConsoleError, DEFAULT_ALIGN, DEFAULT_BAR_WIDTH, DEFAULT_BORDER, DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_LIMIT, DEFAULT_PADDING, DEFAULT_SPINNER_INTERVAL, DEFAULT_THEME, DEFAULT_WIDTH, EMPTY_STYLE, ESC, FOREGROUND_CODES, LEVELS, LEVEL_COLORS, LEVEL_SEVERITY, Logger, LoggerManager, Progress, RESET, RESET_CODE, Reporter, SECOND_MS, SEPARATOR_FILL, SEPARATOR_TITLE_GAP, SPINNER_FRAMES, STATUS_COLORS, STATUS_ICONS, STATUS_LEVELS, Spinner, align, cellAt, createANSIRenderer, createCapture, createConsoleSink, createLogger, createLoggerManager, createProgress, createReporter, createSpinner, createStyler, createTheme, formatArgs, formatDuration, formatRecord, formatTime, freezeStyle, isConsoleError, meetsLevel, paint, renderBar, renderBox, renderSeparator, renderTable, renderTree, renderTreeChildren, repeatTo, stringifyValue, strip, stripControls, width, withCapture };
2116
2317
 
2117
2318
  //# sourceMappingURL=index.js.map