@orkestrel/console 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Orkestrel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @orkestrel/console
2
+
3
+ A unified output-control system for the `@orkestrel` line — one
4
+ environment-agnostic engine composing five concerns over a shared substrate:
5
+ a **style engine** (`Styler` + `ANSIRenderer`, style as data), **structured
6
+ logging** (`Logger`, `LoggerManager`), **narrative reporting** (`Reporter`),
7
+ **console & stream capture** (`Capture`, `ProcessCapture`), and **live
8
+ animations** (`Spinner`, `Progress`). Built to sit beside `@orkestrel/emitter`
9
+ (observable lifecycle), reusing it as it takes shape.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @orkestrel/console
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ - Node.js >= 24
20
+ - Core is ESM; the `./server` subpath ships dual ESM+CJS builds; `./browser`
21
+ is ESM-only
22
+
23
+ ## Usage
24
+
25
+ The same code retargets to any environment by swapping the `sink`:
26
+
27
+ ```ts
28
+ import { createLogger, createReporter, createSpinner } from '@src/core'
29
+
30
+ const logger = createLogger({ name: 'http', level: 'info' }) // ANSI to the console by default
31
+ logger.info('request', { method: 'GET', path: '/' }) // a styled, leveled line + an `entry` event
32
+ logger.emitter.on('entry', (record) => archive(record)) // the transport seam — file / JSON / remote
33
+
34
+ const reporter = createReporter()
35
+ reporter.section('Build')
36
+ reporter.step('bundling', { index: 2, total: 5 }) // [2/5] bundling
37
+ reporter.status('success', 'built in 1.2s') // ✔ built in 1.2s
38
+
39
+ const spinner = createSpinner({ message: 'deploying' })
40
+ spinner.start() // a self-driving glyph cycle, `\r`-redrawn by an overwrite-capable sink
41
+ spinner.success('deployed') // ✔ deployed — the timer cleared, the line committed
42
+ ```
43
+
44
+ Style is data — a `Style` is a frozen record rendered through a swappable
45
+ `RendererInterface` (`ANSIRenderer` by default):
46
+
47
+ ```ts
48
+ import { createStyler } from '@src/core'
49
+
50
+ const styler = createStyler()
51
+ console.log(styler.red.bold('hi')) // renders through the injected renderer
52
+ ```
53
+
54
+ Take control of `console.*` on the read side with `Capture`:
55
+
56
+ ```ts
57
+ import { createCapture } from '@src/core'
58
+
59
+ const capture = createCapture({ mirror: true })
60
+ capture.start()
61
+ console.log('hello')
62
+ capture.messages() // [{ level: 'log', text: 'hello', time: ... }]
63
+ capture.stop()
64
+ ```
65
+
66
+ On the server, `ProcessCapture` takes over the whole `process` output surface
67
+ (direct `process.stdout`/`stderr` writes, not just `console.*`):
68
+
69
+ ```ts
70
+ import { createProcessCapture } from '@src/server'
71
+
72
+ const capture = createProcessCapture({ levels: ['stderr'], mirror: true })
73
+ capture.start()
74
+ ```
75
+
76
+ ## Guide
77
+
78
+ See [guides/src/console.md](./guides/src/console.md) for the full documented
79
+ surface — styling, logging, reporting, capture, and animations.
80
+
81
+ ## Package
82
+
83
+ Published as three environment-scoped entry points per the `exports` field
84
+ in `package.json`: `.` (the shared, environment-agnostic core engine, the
85
+ default ANSI renderer, and the console sink), `./server` (adds the server
86
+ sink and `ProcessCapture`), and `./browser` (adds the browser sink
87
+ translating ANSI to `console.log('%c…', css)`). Core and `./server` ship
88
+ dual ESM+CJS builds; `./browser` is ESM-only.
89
+
90
+ ## License
91
+
92
+ MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
@@ -0,0 +1,214 @@
1
+ import { Color } from '../core/index.js';
2
+ import { SinkInterface } from '../core/index.js';
3
+
4
+ /**
5
+ * Translate an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a
6
+ * `%c`-segmented format string and the parallel array of CSS declarations, so a DevTools console
7
+ * renders the SAME styling a terminal would (the C-f sink calls `console[method](format, ...styles)`).
8
+ *
9
+ * @remarks
10
+ * - **SGR runs → `%c` segments.** The text is scanned for SGR sequences ({@link SGR_PATTERN} —
11
+ * `ESC[…m`); each delimits a run. A run carrying VISIBLE text emits one `%c` directive plus that
12
+ * text into `format` and the run's accumulated CSS into `styles`, so the browser switches style at
13
+ * each `%c`. Foreground / background / attribute codes accumulate; the reset code (`0`, or a bare
14
+ * `ESC[m`) clears the accumulated style back to none. A later color of the same channel REPLACES
15
+ * the earlier one; an attribute is added once. Non-SGR escapes (cursor / erase / OSC) are not style
16
+ * and are left in the text verbatim.
17
+ * - **`%`-safe.** Every LITERAL `%` in the text is doubled to `%%` so the console never treats it as
18
+ * a directive — only the `%c`s this function inserts are real directives. So `format`'s real `%c`
19
+ * count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.
20
+ * - **Plain text short-circuits.** A string with NO SGR sequence yields `{ format: <escaped text>,
21
+ * styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).
22
+ * - **Pure + total.** Same input → same output; it never throws on any string (adversarial escapes,
23
+ * lone `ESC`, unterminated sequences all fall through as literal text).
24
+ *
25
+ * @param text - Any string, ANSI-styled or plain
26
+ * @returns The `%c` format string + parallel CSS array ({@link ConsoleOutput})
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * ansiToConsole('\x1b[31mred\x1b[0m') // { format: '%cred', styles: ['color:#cd0000'] }
31
+ * ansiToConsole('plain') // { format: 'plain', styles: [] }
32
+ * ansiToConsole('50%') // { format: '50%%', styles: [] }
33
+ * ```
34
+ */
35
+ export declare function ansiToConsole(text: string): ConsoleOutput;
36
+
37
+ /**
38
+ * Each text-{@link Attribute}'s SGR "on" number → its equivalent CSS declaration — the browser
39
+ * counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2 →
40
+ * `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,
41
+ * `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`). Keyed by the SGR
42
+ * NUMBER (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a parameter up directly
43
+ * while scanning a run.
44
+ *
45
+ * @remarks
46
+ * `inverse` (SGR 7) has no faithful single-declaration CSS equivalent (it swaps the fore/back inks,
47
+ * which depends on the live colors); it maps to a best-effort `filter:invert(100%)` — documented as
48
+ * approximate, never silently dropped. Deeply frozen.
49
+ */
50
+ export declare const ATTRIBUTE_CSS: Readonly<Record<number, string>>;
51
+
52
+ /**
53
+ * Each SGR BACKGROUND parameter (40–47 / 100–107) → its `background:<hex>` CSS, derived by walking
54
+ * core's {@link COLORS} against {@link BACKGROUND_CODES} and {@link COLOR_HEX}. The sink reads this
55
+ * while scanning a run to translate a background code to CSS.
56
+ */
57
+ export declare const BACKGROUND_CSS: Readonly<Record<number, string>>;
58
+
59
+ /**
60
+ * Each named {@link Color}'s hex value — the 16 standard terminal colors a browser DevTools
61
+ * console renders the SAME {@link Color} names as. The source of truth for the BROWSER color
62
+ * axis: the ANSI renderer maps a `Color` name to an SGR number, and this maps the same name to
63
+ * the CSS color the `%c` sink paints with, so a browser shows the same 16 colors a terminal does.
64
+ *
65
+ * @remarks
66
+ * The conventional VGA/xterm 16-color palette (the base 8 plus their bright variants); `default`
67
+ * is intentionally absent (it leaves the console's own ink and emits no CSS). Deeply frozen.
68
+ */
69
+ export declare const COLOR_HEX: Readonly<Record<Exclude<Color, 'default'>, string>>;
70
+
71
+ /**
72
+ * The `console.log`-ready output {@link import('./helpers.js').ansiToConsole} produces from
73
+ * an ANSI-styled string — a format string of `%c`-prefixed segments and the parallel array
74
+ * of CSS declarations, ready to spread into a browser `console` call as
75
+ * `console.log(format, ...styles)`.
76
+ *
77
+ * @remarks
78
+ * - `format` — the text with each styled run prefixed by one `%c` directive (the directive
79
+ * the browser console consumes to switch the active style) and every LITERAL `%` doubled to
80
+ * `%%` so it is not mistaken for a directive. A plain (no-ANSI) input yields the text
81
+ * verbatim with NO `%c` and an empty `styles` (still `%`-escaped).
82
+ * - `styles` — one CSS declaration string per `%c` in `format`, in order: the browser applies
83
+ * `styles[n]` from the n-th `%c` onward. Each entry is the accumulated style for that run
84
+ * (an SGR reset clears it back to `''`). `format`'s `%c` count always equals `styles.length`,
85
+ * so the spread `console.log(format, ...styles)` lines up exactly.
86
+ */
87
+ export declare interface ConsoleOutput {
88
+ readonly format: string;
89
+ readonly styles: readonly string[];
90
+ }
91
+
92
+ /**
93
+ * Create the browser `%c` {@link SinkInterface} — the C-f browser output backend. `write(text, level?)`
94
+ * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)
95
+ * via {@link ansiToConsole}, so a DevTools console renders the SAME styling a terminal does. Drop it in
96
+ * as a logger / reporter / spinner sink (`createLogger({ sink: createBrowserSink() })`) to retarget the
97
+ * core output to the browser console with no change to the core.
98
+ *
99
+ * @returns A browser `%c` {@link SinkInterface}
100
+ *
101
+ * @remarks
102
+ * - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and
103
+ * re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}
104
+ * — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.
105
+ * - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an
106
+ * omitted level) → `console.log` — the SAME routing as core's `createConsoleSink`, so a logger's level
107
+ * reaches the matching DevTools stream.
108
+ * - **Animation degrade (locked).** A browser console cannot overwrite a line, so a `text` beginning with
109
+ * a carriage return `\r` (a spinner / progress redraw) has the leading `\r` STRIPPED and is written as a
110
+ * fresh, non-overwriting line — the locked browser degrade. Only a LEADING `\r` is stripped; an interior
111
+ * one is left to the console.
112
+ * - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` / `console.error` AT
113
+ * CREATION and writes through those references, so a later `Capture` that PATCHES `console.*` can never
114
+ * feed this sink's output back into itself (the no-capture-loop principle, AGENTS / the core sink's
115
+ * precedent). Create the sink (or the logger) BEFORE installing a capture.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * import { createLogger } from '../core/index.js'
120
+ * import { createBrowserSink } from '@src/browser'
121
+ *
122
+ * const logger = createLogger({ name: 'app', sink: createBrowserSink() })
123
+ * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools
124
+ * ```
125
+ */
126
+ export declare function createBrowserSink(): SinkInterface;
127
+
128
+ /**
129
+ * The browser console directive that switches the active style — one `%c` prefixes every styled run
130
+ * in the {@link import('./types.js').ConsoleOutput} format string, consuming the next entry of the
131
+ * parallel CSS array. The single source of truth for the directive token.
132
+ */
133
+ export declare const DIRECTIVE = "%c";
134
+
135
+ /**
136
+ * Double every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from
137
+ * reading a stray `%` (e.g. in `50%` or `%s`) as a format directive. The single escape the
138
+ * {@link ansiToConsole} translation applies to every text segment before assembling the format
139
+ * string (so only the `%c`s it inserts are real directives).
140
+ *
141
+ * @param text - A literal text segment (no inserted directives)
142
+ * @returns `text` with each `%` doubled
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * escapePercent('100% done') // '100%% done'
147
+ * ```
148
+ */
149
+ export declare function escapePercent(text: string): string;
150
+
151
+ /**
152
+ * Each SGR FOREGROUND parameter (30–37 / 90–97) → its `color:<hex>` CSS, derived by walking core's
153
+ * {@link COLORS} against {@link FOREGROUND_CODES} and {@link COLOR_HEX} (so the number↔name mapping
154
+ * stays in core, never duplicated here). The sink reads this while scanning a run to translate a
155
+ * foreground code to CSS. A pure build-once expression producing a frozen record (the
156
+ * `DEFAULT_CAPTURE_LEVELS = CAPTURE_LEVELS` precedent — derived data, not new data).
157
+ */
158
+ export declare const FOREGROUND_CSS: Readonly<Record<number, string>>;
159
+
160
+ /**
161
+ * Parse an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})
162
+ * into its numeric codes — `'1;31'` → `[1, 31]`. An EMPTY list (a bare `ESC[m`) yields `[0]`, since
163
+ * the SGR spec treats a parameterless sequence as a reset; an empty field within a list (`'1;;4'`)
164
+ * likewise counts as a `0` reset, matching the spec.
165
+ *
166
+ * @param parameters - The raw `;`-separated parameter string (the regex capture)
167
+ * @returns The parsed SGR codes (a parameterless / empty field becoming `0`)
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * parseParameters('1;31') // [1, 31]
172
+ * parseParameters('') // [0]
173
+ * ```
174
+ */
175
+ export declare function parseParameters(parameters: string): readonly number[];
176
+
177
+ /**
178
+ * Matches one SGR sequence (`ESC[ <params> m`) and CAPTURES its `;`-separated numeric parameters —
179
+ * the subset of ANSI {@link import('../core/index.js').strip} cares about that carries STYLE (color /
180
+ * attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks
181
+ * every SGR run in a string; built from core's {@link ESC} so no control-character literal appears
182
+ * in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,
183
+ * which the spec treats as a reset).
184
+ *
185
+ * @remarks
186
+ * A global `RegExp` carries a mutable `lastIndex`; a scan builds a FRESH `RegExp` from this one's
187
+ * `source` + `flags` rather than reuse this instance, so concurrent scans never collide. This is the
188
+ * canonical definition, not a shared scanner.
189
+ */
190
+ export declare const SGR_PATTERN: RegExp;
191
+
192
+ /**
193
+ * The mutable accumulator {@link import('./helpers.js').ansiToConsole} carries across a run while
194
+ * translating SGR codes to CSS — a single `foreground` and `background` declaration (each channel
195
+ * REPLACEABLE by a later color of the same channel) plus an ordered, de-duplicated list of attribute
196
+ * declarations. An SGR reset empties all three; {@link import('./helpers.js').ansiToConsole} folds
197
+ * it into the `;`-joined CSS string a run emits.
198
+ *
199
+ * @remarks
200
+ * Mutable BY DESIGN — it is internal scan state the scanner updates in place per SGR sequence (the
201
+ * one place this surface departs from the `readonly` default, AGENTS §11), never a returned value. A
202
+ * channel holds the FULL CSS declaration (`'color:#cd0000'`, not a bare hex), or `''` when unset.
203
+ * - `foreground` — the current `color:<hex>` declaration, or `''` (unset / post-reset).
204
+ * - `background` — the current `background:<hex>` declaration, or `''`.
205
+ * - `attributes` — the active text-effect declarations in insertion order (`'font-weight:bold'`, …),
206
+ * each present at most once.
207
+ */
208
+ export declare interface StyleAccumulator {
209
+ foreground: string;
210
+ background: string;
211
+ attributes: string[];
212
+ }
213
+
214
+ export { }
@@ -0,0 +1,275 @@
1
+ import { ATTRIBUTE_CODES, BACKGROUND_CODES, COLORS, ESC, FOREGROUND_CODES, RESET_CODE } from "../core/index.js";
2
+ //#region src/browser/constants.ts
3
+ /**
4
+ * Each named {@link Color}'s hex value — the 16 standard terminal colors a browser DevTools
5
+ * console renders the SAME {@link Color} names as. The source of truth for the BROWSER color
6
+ * axis: the ANSI renderer maps a `Color` name to an SGR number, and this maps the same name to
7
+ * the CSS color the `%c` sink paints with, so a browser shows the same 16 colors a terminal does.
8
+ *
9
+ * @remarks
10
+ * The conventional VGA/xterm 16-color palette (the base 8 plus their bright variants); `default`
11
+ * is intentionally absent (it leaves the console's own ink and emits no CSS). Deeply frozen.
12
+ */
13
+ var COLOR_HEX = Object.freeze({
14
+ black: "#000000",
15
+ red: "#cd0000",
16
+ green: "#00cd00",
17
+ yellow: "#cdcd00",
18
+ blue: "#0000ee",
19
+ magenta: "#cd00cd",
20
+ cyan: "#00cdcd",
21
+ white: "#e5e5e5",
22
+ brightBlack: "#7f7f7f",
23
+ brightRed: "#ff0000",
24
+ brightGreen: "#00ff00",
25
+ brightYellow: "#ffff00",
26
+ brightBlue: "#5c5cff",
27
+ brightMagenta: "#ff00ff",
28
+ brightCyan: "#00ffff",
29
+ brightWhite: "#ffffff"
30
+ });
31
+ /**
32
+ * Each text-{@link Attribute}'s SGR "on" number → its equivalent CSS declaration — the browser
33
+ * counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2 →
34
+ * `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,
35
+ * `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`). Keyed by the SGR
36
+ * NUMBER (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a parameter up directly
37
+ * while scanning a run.
38
+ *
39
+ * @remarks
40
+ * `inverse` (SGR 7) has no faithful single-declaration CSS equivalent (it swaps the fore/back inks,
41
+ * which depends on the live colors); it maps to a best-effort `filter:invert(100%)` — documented as
42
+ * approximate, never silently dropped. Deeply frozen.
43
+ */
44
+ var ATTRIBUTE_CSS = Object.freeze({
45
+ [ATTRIBUTE_CODES.bold]: "font-weight:bold",
46
+ [ATTRIBUTE_CODES.dim]: "opacity:0.6",
47
+ [ATTRIBUTE_CODES.italic]: "font-style:italic",
48
+ [ATTRIBUTE_CODES.underline]: "text-decoration:underline",
49
+ [ATTRIBUTE_CODES.inverse]: "filter:invert(100%)",
50
+ [ATTRIBUTE_CODES.strikethrough]: "text-decoration:line-through"
51
+ });
52
+ /**
53
+ * Each SGR FOREGROUND parameter (30–37 / 90–97) → its `color:<hex>` CSS, derived by walking core's
54
+ * {@link COLORS} against {@link FOREGROUND_CODES} and {@link COLOR_HEX} (so the number↔name mapping
55
+ * stays in core, never duplicated here). The sink reads this while scanning a run to translate a
56
+ * foreground code to CSS. A pure build-once expression producing a frozen record (the
57
+ * `DEFAULT_CAPTURE_LEVELS = CAPTURE_LEVELS` precedent — derived data, not new data).
58
+ */
59
+ var FOREGROUND_CSS = Object.freeze(Object.fromEntries(COLORS.map((color) => [FOREGROUND_CODES[color], `color:${COLOR_HEX[color]}`])));
60
+ /**
61
+ * Each SGR BACKGROUND parameter (40–47 / 100–107) → its `background:<hex>` CSS, derived by walking
62
+ * core's {@link COLORS} against {@link BACKGROUND_CODES} and {@link COLOR_HEX}. The sink reads this
63
+ * while scanning a run to translate a background code to CSS.
64
+ */
65
+ var BACKGROUND_CSS = Object.freeze(Object.fromEntries(COLORS.map((color) => [BACKGROUND_CODES[color], `background:${COLOR_HEX[color]}`])));
66
+ /**
67
+ * The browser console directive that switches the active style — one `%c` prefixes every styled run
68
+ * in the {@link import('./types.js').ConsoleOutput} format string, consuming the next entry of the
69
+ * parallel CSS array. The single source of truth for the directive token.
70
+ */
71
+ var DIRECTIVE = "%c";
72
+ /**
73
+ * Matches one SGR sequence (`ESC[ <params> m`) and CAPTURES its `;`-separated numeric parameters —
74
+ * the subset of ANSI {@link import('@src/core').strip} cares about that carries STYLE (color /
75
+ * attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks
76
+ * every SGR run in a string; built from core's {@link ESC} so no control-character literal appears
77
+ * in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,
78
+ * which the spec treats as a reset).
79
+ *
80
+ * @remarks
81
+ * A global `RegExp` carries a mutable `lastIndex`; a scan builds a FRESH `RegExp` from this one's
82
+ * `source` + `flags` rather than reuse this instance, so concurrent scans never collide. This is the
83
+ * canonical definition, not a shared scanner.
84
+ */
85
+ var SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
86
+ //#endregion
87
+ //#region src/browser/helpers.ts
88
+ /**
89
+ * Translate an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a
90
+ * `%c`-segmented format string and the parallel array of CSS declarations, so a DevTools console
91
+ * renders the SAME styling a terminal would (the C-f sink calls `console[method](format, ...styles)`).
92
+ *
93
+ * @remarks
94
+ * - **SGR runs → `%c` segments.** The text is scanned for SGR sequences ({@link SGR_PATTERN} —
95
+ * `ESC[…m`); each delimits a run. A run carrying VISIBLE text emits one `%c` directive plus that
96
+ * text into `format` and the run's accumulated CSS into `styles`, so the browser switches style at
97
+ * each `%c`. Foreground / background / attribute codes accumulate; the reset code (`0`, or a bare
98
+ * `ESC[m`) clears the accumulated style back to none. A later color of the same channel REPLACES
99
+ * the earlier one; an attribute is added once. Non-SGR escapes (cursor / erase / OSC) are not style
100
+ * and are left in the text verbatim.
101
+ * - **`%`-safe.** Every LITERAL `%` in the text is doubled to `%%` so the console never treats it as
102
+ * a directive — only the `%c`s this function inserts are real directives. So `format`'s real `%c`
103
+ * count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.
104
+ * - **Plain text short-circuits.** A string with NO SGR sequence yields `{ format: <escaped text>,
105
+ * styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).
106
+ * - **Pure + total.** Same input → same output; it never throws on any string (adversarial escapes,
107
+ * lone `ESC`, unterminated sequences all fall through as literal text).
108
+ *
109
+ * @param text - Any string, ANSI-styled or plain
110
+ * @returns The `%c` format string + parallel CSS array ({@link ConsoleOutput})
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * ansiToConsole('\x1b[31mred\x1b[0m') // { format: '%cred', styles: ['color:#cd0000'] }
115
+ * ansiToConsole('plain') // { format: 'plain', styles: [] }
116
+ * ansiToConsole('50%') // { format: '50%%', styles: [] }
117
+ * ```
118
+ */
119
+ function ansiToConsole(text) {
120
+ const scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags);
121
+ const active = {
122
+ foreground: "",
123
+ background: "",
124
+ attributes: []
125
+ };
126
+ const segments = [];
127
+ const styles = [];
128
+ let styled = false;
129
+ let cursor = 0;
130
+ let pending = "";
131
+ const apply = (codes) => {
132
+ for (const code of codes) {
133
+ if (code === RESET_CODE) {
134
+ active.foreground = "";
135
+ active.background = "";
136
+ active.attributes.length = 0;
137
+ continue;
138
+ }
139
+ const foreground = FOREGROUND_CSS[code];
140
+ if (foreground !== void 0) {
141
+ active.foreground = foreground;
142
+ continue;
143
+ }
144
+ const background = BACKGROUND_CSS[code];
145
+ if (background !== void 0) {
146
+ active.background = background;
147
+ continue;
148
+ }
149
+ const attribute = ATTRIBUTE_CSS[code];
150
+ if (attribute !== void 0 && !active.attributes.includes(attribute)) active.attributes.push(attribute);
151
+ }
152
+ };
153
+ const serialize = () => {
154
+ const declarations = [...active.attributes];
155
+ if (active.foreground !== "") declarations.push(active.foreground);
156
+ if (active.background !== "") declarations.push(active.background);
157
+ return declarations.join(";");
158
+ };
159
+ const flush = () => {
160
+ if (pending === "") return;
161
+ segments.push(`%c${pending}`);
162
+ styles.push(serialize());
163
+ pending = "";
164
+ };
165
+ for (let match = scanner.exec(text); match !== null; match = scanner.exec(text)) {
166
+ styled = true;
167
+ pending += escapePercent(text.slice(cursor, match.index));
168
+ flush();
169
+ apply(parseParameters(match[1] ?? ""));
170
+ cursor = match.index + match[0].length;
171
+ }
172
+ if (!styled) return {
173
+ format: escapePercent(text),
174
+ styles: []
175
+ };
176
+ pending += escapePercent(text.slice(cursor));
177
+ flush();
178
+ return {
179
+ format: segments.join(""),
180
+ styles
181
+ };
182
+ }
183
+ /**
184
+ * Double every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from
185
+ * reading a stray `%` (e.g. in `50%` or `%s`) as a format directive. The single escape the
186
+ * {@link ansiToConsole} translation applies to every text segment before assembling the format
187
+ * string (so only the `%c`s it inserts are real directives).
188
+ *
189
+ * @param text - A literal text segment (no inserted directives)
190
+ * @returns `text` with each `%` doubled
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * escapePercent('100% done') // '100%% done'
195
+ * ```
196
+ */
197
+ function escapePercent(text) {
198
+ return text.replace(/%/g, "%%");
199
+ }
200
+ /**
201
+ * Parse an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})
202
+ * into its numeric codes — `'1;31'` → `[1, 31]`. An EMPTY list (a bare `ESC[m`) yields `[0]`, since
203
+ * the SGR spec treats a parameterless sequence as a reset; an empty field within a list (`'1;;4'`)
204
+ * likewise counts as a `0` reset, matching the spec.
205
+ *
206
+ * @param parameters - The raw `;`-separated parameter string (the regex capture)
207
+ * @returns The parsed SGR codes (a parameterless / empty field becoming `0`)
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * parseParameters('1;31') // [1, 31]
212
+ * parseParameters('') // [0]
213
+ * ```
214
+ */
215
+ function parseParameters(parameters) {
216
+ if (parameters === "") return [RESET_CODE];
217
+ return parameters.split(";").map((field) => field === "" ? RESET_CODE : Number(field));
218
+ }
219
+ //#endregion
220
+ //#region src/browser/factories.ts
221
+ /**
222
+ * Create the browser `%c` {@link SinkInterface} — the C-f browser output backend. `write(text, level?)`
223
+ * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)
224
+ * via {@link ansiToConsole}, so a DevTools console renders the SAME styling a terminal does. Drop it in
225
+ * as a logger / reporter / spinner sink (`createLogger({ sink: createBrowserSink() })`) to retarget the
226
+ * core output to the browser console with no change to the core.
227
+ *
228
+ * @returns A browser `%c` {@link SinkInterface}
229
+ *
230
+ * @remarks
231
+ * - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and
232
+ * re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}
233
+ * — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.
234
+ * - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an
235
+ * omitted level) → `console.log` — the SAME routing as core's `createConsoleSink`, so a logger's level
236
+ * reaches the matching DevTools stream.
237
+ * - **Animation degrade (locked).** A browser console cannot overwrite a line, so a `text` beginning with
238
+ * a carriage return `\r` (a spinner / progress redraw) has the leading `\r` STRIPPED and is written as a
239
+ * fresh, non-overwriting line — the locked browser degrade. Only a LEADING `\r` is stripped; an interior
240
+ * one is left to the console.
241
+ * - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` / `console.error` AT
242
+ * CREATION and writes through those references, so a later `Capture` that PATCHES `console.*` can never
243
+ * feed this sink's output back into itself (the no-capture-loop principle, AGENTS / the core sink's
244
+ * precedent). Create the sink (or the logger) BEFORE installing a capture.
245
+ *
246
+ * @example
247
+ * ```ts
248
+ * import { createLogger } from '@src/core'
249
+ * import { createBrowserSink } from '@src/browser'
250
+ *
251
+ * const logger = createLogger({ name: 'app', sink: createBrowserSink() })
252
+ * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools
253
+ * ```
254
+ */
255
+ function createBrowserSink() {
256
+ const log = console.log.bind(console);
257
+ const warn = console.warn.bind(console);
258
+ const error = console.error.bind(console);
259
+ return { write(text, level) {
260
+ const { format, styles } = ansiToConsole(text.startsWith("\r") ? text.slice(1) : text);
261
+ if (level === "error") {
262
+ error(format, ...styles);
263
+ return;
264
+ }
265
+ if (level === "warn") {
266
+ warn(format, ...styles);
267
+ return;
268
+ }
269
+ log(format, ...styles);
270
+ } };
271
+ }
272
+ //#endregion
273
+ export { ATTRIBUTE_CSS, BACKGROUND_CSS, COLOR_HEX, DIRECTIVE, FOREGROUND_CSS, SGR_PATTERN, ansiToConsole, createBrowserSink, escapePercent, parseParameters };
274
+
275
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/constants.ts","../../../src/browser/helpers.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { Color } from '@src/core'\nimport { ATTRIBUTE_CODES, BACKGROUND_CODES, COLORS, ESC, FOREGROUND_CODES } from '@src/core'\n\n// The SGR → CSS translation DATA the browser sink maps ANSI runs through (the C-f branch).\n// The core `src/core/console` is the source of truth for the SGR NUMBERS (which code is which\n// color / attribute); this module owns only the BROWSER-side mapping — a named-color → hex\n// palette and each SGR number → its CSS declaration — and DERIVES the number→CSS lookups by\n// walking core's `COLORS` against its code maps + the palette, so the number↔name mapping is\n// never re-hardcoded here. The SGR-scan pattern is built from core's `ESC` so no control-\n// character literal appears in source. UPPER_SNAKE, deeply `Object.freeze`d, every member\n// exported (AGENTS §5).\n\n/**\n * Each named {@link Color}'s hex value — the 16 standard terminal colors a browser DevTools\n * console renders the SAME {@link Color} names as. The source of truth for the BROWSER color\n * axis: the ANSI renderer maps a `Color` name to an SGR number, and this maps the same name to\n * the CSS color the `%c` sink paints with, so a browser shows the same 16 colors a terminal does.\n *\n * @remarks\n * The conventional VGA/xterm 16-color palette (the base 8 plus their bright variants); `default`\n * is intentionally absent (it leaves the console's own ink and emits no CSS). Deeply frozen.\n */\nexport const COLOR_HEX: Readonly<Record<Exclude<Color, 'default'>, string>> = Object.freeze({\n\tblack: '#000000',\n\tred: '#cd0000',\n\tgreen: '#00cd00',\n\tyellow: '#cdcd00',\n\tblue: '#0000ee',\n\tmagenta: '#cd00cd',\n\tcyan: '#00cdcd',\n\twhite: '#e5e5e5',\n\tbrightBlack: '#7f7f7f',\n\tbrightRed: '#ff0000',\n\tbrightGreen: '#00ff00',\n\tbrightYellow: '#ffff00',\n\tbrightBlue: '#5c5cff',\n\tbrightMagenta: '#ff00ff',\n\tbrightCyan: '#00ffff',\n\tbrightWhite: '#ffffff',\n})\n\n/**\n * Each text-{@link Attribute}'s SGR \"on\" number → its equivalent CSS declaration — the browser\n * counterpart to the terminal's SGR text effects (`bold` 1 → `font-weight:bold`, `dim` 2 →\n * `opacity:0.6`, `italic` 3 → `font-style:italic`, `underline` 4 → `text-decoration:underline`,\n * `inverse` 7 → best-effort, `strikethrough` 9 → `text-decoration:line-through`). Keyed by the SGR\n * NUMBER (derived from core's {@link ATTRIBUTE_CODES}) so the sink looks a parameter up directly\n * while scanning a run.\n *\n * @remarks\n * `inverse` (SGR 7) has no faithful single-declaration CSS equivalent (it swaps the fore/back inks,\n * which depends on the live colors); it maps to a best-effort `filter:invert(100%)` — documented as\n * approximate, never silently dropped. Deeply frozen.\n */\nexport const ATTRIBUTE_CSS: Readonly<Record<number, string>> = Object.freeze({\n\t[ATTRIBUTE_CODES.bold]: 'font-weight:bold',\n\t[ATTRIBUTE_CODES.dim]: 'opacity:0.6',\n\t[ATTRIBUTE_CODES.italic]: 'font-style:italic',\n\t[ATTRIBUTE_CODES.underline]: 'text-decoration:underline',\n\t[ATTRIBUTE_CODES.inverse]: 'filter:invert(100%)',\n\t[ATTRIBUTE_CODES.strikethrough]: 'text-decoration:line-through',\n})\n\n/**\n * Each SGR FOREGROUND parameter (30–37 / 90–97) → its `color:<hex>` CSS, derived by walking core's\n * {@link COLORS} against {@link FOREGROUND_CODES} and {@link COLOR_HEX} (so the number↔name mapping\n * stays in core, never duplicated here). The sink reads this while scanning a run to translate a\n * foreground code to CSS. A pure build-once expression producing a frozen record (the\n * `DEFAULT_CAPTURE_LEVELS = CAPTURE_LEVELS` precedent — derived data, not new data).\n */\nexport const FOREGROUND_CSS: Readonly<Record<number, string>> = Object.freeze(\n\tObject.fromEntries(COLORS.map((color) => [FOREGROUND_CODES[color], `color:${COLOR_HEX[color]}`])),\n)\n\n/**\n * Each SGR BACKGROUND parameter (40–47 / 100–107) → its `background:<hex>` CSS, derived by walking\n * core's {@link COLORS} against {@link BACKGROUND_CODES} and {@link COLOR_HEX}. The sink reads this\n * while scanning a run to translate a background code to CSS.\n */\nexport const BACKGROUND_CSS: Readonly<Record<number, string>> = Object.freeze(\n\tObject.fromEntries(\n\t\tCOLORS.map((color) => [BACKGROUND_CODES[color], `background:${COLOR_HEX[color]}`]),\n\t),\n)\n\n/**\n * The browser console directive that switches the active style — one `%c` prefixes every styled run\n * in the {@link import('./types.js').ConsoleOutput} format string, consuming the next entry of the\n * parallel CSS array. The single source of truth for the directive token.\n */\nexport const DIRECTIVE = '%c'\n\n/**\n * Matches one SGR sequence (`ESC[ <params> m`) and CAPTURES its `;`-separated numeric parameters —\n * the subset of ANSI {@link import('@src/core').strip} cares about that carries STYLE (color /\n * attribute / reset), as opposed to cursor / erase / OSC sequences. Global, so the scanner walks\n * every SGR run in a string; built from core's {@link ESC} so no control-character literal appears\n * in source (the codebase idiom). The capture group is the parameter list (`''` for a bare `ESC[m`,\n * which the spec treats as a reset).\n *\n * @remarks\n * A global `RegExp` carries a mutable `lastIndex`; a scan builds a FRESH `RegExp` from this one's\n * `source` + `flags` rather than reuse this instance, so concurrent scans never collide. This is the\n * canonical definition, not a shared scanner.\n */\nexport const SGR_PATTERN = new RegExp(`${ESC}\\\\[([0-9;]*)m`, 'g')\n","import type { ConsoleOutput, StyleAccumulator } from './types.js'\nimport { RESET_CODE } from '@src/core'\nimport {\n\tATTRIBUTE_CSS,\n\tBACKGROUND_CSS,\n\tDIRECTIVE,\n\tFOREGROUND_CSS,\n\tSGR_PATTERN,\n} from './constants.js'\n\n// The pure, browser-only translation behind the `%c` console sink (the C-f branch). The core\n// styler / Logger / Reporter emit ANSI-styled STRINGS; a DevTools console can't render ANSI but\n// can style via `console.log('%ctext', 'css')`, so `ansiToConsole` parses the SGR runs in the\n// incoming text and re-emits them as a `%c`-ready format string + parallel CSS array — the\n// translation happens at the OUTPUT boundary, leaving the core unchanged. Pure + total + `%`-safe.\n// `ansiToConsole`'s scan glue (apply / serialize / flush over its own `active` / `segments` /\n// `styles` state) lives as local closures inside it (AGENTS §5); only the standalone, reusable\n// `escapePercent` / `parseParameters` utilities are exported alongside it.\n\n/**\n * Translate an ANSI-styled string into a browser `console.log`-ready {@link ConsoleOutput} — a\n * `%c`-segmented format string and the parallel array of CSS declarations, so a DevTools console\n * renders the SAME styling a terminal would (the C-f sink calls `console[method](format, ...styles)`).\n *\n * @remarks\n * - **SGR runs → `%c` segments.** The text is scanned for SGR sequences ({@link SGR_PATTERN} —\n * `ESC[…m`); each delimits a run. A run carrying VISIBLE text emits one `%c` directive plus that\n * text into `format` and the run's accumulated CSS into `styles`, so the browser switches style at\n * each `%c`. Foreground / background / attribute codes accumulate; the reset code (`0`, or a bare\n * `ESC[m`) clears the accumulated style back to none. A later color of the same channel REPLACES\n * the earlier one; an attribute is added once. Non-SGR escapes (cursor / erase / OSC) are not style\n * and are left in the text verbatim.\n * - **`%`-safe.** Every LITERAL `%` in the text is doubled to `%%` so the console never treats it as\n * a directive — only the `%c`s this function inserts are real directives. So `format`'s real `%c`\n * count always equals `styles.length`, and `console.log(format, ...styles)` lines up exactly.\n * - **Plain text short-circuits.** A string with NO SGR sequence yields `{ format: <escaped text>,\n * styles: [] }` — no `%c`, no styles (the text is still `%`-escaped).\n * - **Pure + total.** Same input → same output; it never throws on any string (adversarial escapes,\n * lone `ESC`, unterminated sequences all fall through as literal text).\n *\n * @param text - Any string, ANSI-styled or plain\n * @returns The `%c` format string + parallel CSS array ({@link ConsoleOutput})\n *\n * @example\n * ```ts\n * ansiToConsole('\\x1b[31mred\\x1b[0m') // { format: '%cred', styles: ['color:#cd0000'] }\n * ansiToConsole('plain') // { format: 'plain', styles: [] }\n * ansiToConsole('50%') // { format: '50%%', styles: [] }\n * ```\n */\nexport function ansiToConsole(text: string): ConsoleOutput {\n\tconst scanner = new RegExp(SGR_PATTERN.source, SGR_PATTERN.flags)\n\t// The accumulated active style across a run — a separate foreground / background declaration\n\t// (each channel REPLACEABLE) plus an ordered, de-duplicated list of attribute declarations. An\n\t// SGR reset empties all three. Serialized to a `;`-joined CSS string per emitted run.\n\tconst active: StyleAccumulator = { foreground: '', background: '', attributes: [] }\n\tconst segments: string[] = []\n\tconst styles: string[] = []\n\tlet styled = false\n\tlet cursor = 0\n\tlet pending = ''\n\n\t// Apply one SGR sequence's `codes` to `active` (in place): a reset clears every channel; a\n\t// foreground / background code REPLACES that channel; an attribute is added once (idempotent).\n\t// An unrecognized code (a 256-color / truecolor extension this layer doesn't map) is ignored,\n\t// never raised — keeping the translation total.\n\tconst apply = (codes: readonly number[]): void => {\n\t\tfor (const code of codes) {\n\t\t\tif (code === RESET_CODE) {\n\t\t\t\tactive.foreground = ''\n\t\t\t\tactive.background = ''\n\t\t\t\tactive.attributes.length = 0\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst foreground = FOREGROUND_CSS[code]\n\t\t\tif (foreground !== undefined) {\n\t\t\t\tactive.foreground = foreground\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst background = BACKGROUND_CSS[code]\n\t\t\tif (background !== undefined) {\n\t\t\t\tactive.background = background\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst attribute = ATTRIBUTE_CSS[code]\n\t\t\tif (attribute !== undefined && !active.attributes.includes(attribute)) {\n\t\t\t\tactive.attributes.push(attribute)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Serialize `active` into one `;`-joined CSS declaration string — attributes (insertion order),\n\t// then foreground, then background, mirroring the renderer's stable code order; an empty style\n\t// (post-reset / nothing accumulated) serializes to `''`.\n\tconst serialize = (): string => {\n\t\tconst declarations = [...active.attributes]\n\t\tif (active.foreground !== '') declarations.push(active.foreground)\n\t\tif (active.background !== '') declarations.push(active.background)\n\t\treturn declarations.join(';')\n\t}\n\n\t// Push `pending` (the run's already-escaped text) as one `%c` segment paired with the run's\n\t// current CSS, then clear `pending`. An EMPTY run is dropped (a style change with no visible text\n\t// emits no `%c`), keeping `format`'s `%c` count exactly equal to `styles.length`.\n\tconst flush = (): void => {\n\t\tif (pending === '') return\n\t\tsegments.push(`${DIRECTIVE}${pending}`)\n\t\tstyles.push(serialize())\n\t\tpending = ''\n\t}\n\n\tfor (let match = scanner.exec(text); match !== null; match = scanner.exec(text)) {\n\t\tstyled = true\n\t\tpending += escapePercent(text.slice(cursor, match.index))\n\t\tflush()\n\t\tapply(parseParameters(match[1] ?? ''))\n\t\tcursor = match.index + match[0].length\n\t}\n\tif (!styled) return { format: escapePercent(text), styles: [] }\n\tpending += escapePercent(text.slice(cursor))\n\tflush()\n\treturn { format: segments.join(''), styles }\n}\n\n/**\n * Double every literal `%` in `text` to `%%` — the `%`-escape that keeps a browser console from\n * reading a stray `%` (e.g. in `50%` or `%s`) as a format directive. The single escape the\n * {@link ansiToConsole} translation applies to every text segment before assembling the format\n * string (so only the `%c`s it inserts are real directives).\n *\n * @param text - A literal text segment (no inserted directives)\n * @returns `text` with each `%` doubled\n *\n * @example\n * ```ts\n * escapePercent('100% done') // '100%% done'\n * ```\n */\nexport function escapePercent(text: string): string {\n\treturn text.replace(/%/g, '%%')\n}\n\n/**\n * Parse an SGR parameter list (the `;`-separated numeric string captured by {@link SGR_PATTERN})\n * into its numeric codes — `'1;31'` → `[1, 31]`. An EMPTY list (a bare `ESC[m`) yields `[0]`, since\n * the SGR spec treats a parameterless sequence as a reset; an empty field within a list (`'1;;4'`)\n * likewise counts as a `0` reset, matching the spec.\n *\n * @param parameters - The raw `;`-separated parameter string (the regex capture)\n * @returns The parsed SGR codes (a parameterless / empty field becoming `0`)\n *\n * @example\n * ```ts\n * parseParameters('1;31') // [1, 31]\n * parseParameters('') // [0]\n * ```\n */\nexport function parseParameters(parameters: string): readonly number[] {\n\tif (parameters === '') return [RESET_CODE]\n\treturn parameters.split(';').map((field) => (field === '' ? RESET_CODE : Number(field)))\n}\n","import type { LogLevel, SinkInterface } from '@src/core'\nimport { ansiToConsole } from './helpers.js'\n\n// The browser `%c` console sink (the C-f branch) — the platform-bound backend that satisfies core's\n// `SinkInterface` in a browser DevTools console. The core styler / Logger / Reporter emit ANSI-styled\n// STRINGS; a DevTools console can't render ANSI but CAN style via `console.log('%ctext', 'css')`, so\n// this sink translates the incoming ANSI runs into a `%c` call at the OUTPUT boundary (the env-split\n// rule: core owns the contract + universal logic, the browser provides the platform backend). A thin\n// stateless adapter, so a frozen-object factory — like core's `createConsoleSink` — not a class\n// (AGENTS §5). `SinkInterface` / `LogLevel` are IMPORTED from `@src/core`, never redeclared.\n\n/**\n * Create the browser `%c` {@link SinkInterface} — the C-f browser output backend. `write(text, level?)`\n * translates the ANSI-styled `text` into a browser `console` call (`console[method](format, ...styles)`)\n * via {@link ansiToConsole}, so a DevTools console renders the SAME styling a terminal does. Drop it in\n * as a logger / reporter / spinner sink (`createLogger({ sink: createBrowserSink() })`) to retarget the\n * core output to the browser console with no change to the core.\n *\n * @returns A browser `%c` {@link SinkInterface}\n *\n * @remarks\n * - **ANSI → `%c` at the sink.** The core produces ANSI strings; this sink parses the SGR runs and\n * re-emits them as a `console.log`-ready `%c` format string + parallel CSS array ({@link ansiToConsole}\n * — pure, total, and `%`-safe), so the styling survives the trip to a console that can't render ANSI.\n * - **Routes by level.** `error` → `console.error`, `warn` → `console.warn`, every other level (and an\n * omitted level) → `console.log` — the SAME routing as core's `createConsoleSink`, so a logger's level\n * reaches the matching DevTools stream.\n * - **Animation degrade (locked).** A browser console cannot overwrite a line, so a `text` beginning with\n * a carriage return `\\r` (a spinner / progress redraw) has the leading `\\r` STRIPPED and is written as a\n * fresh, non-overwriting line — the locked browser degrade. Only a LEADING `\\r` is stripped; an interior\n * one is left to the console.\n * - **Snapshotted — no capture loop.** It captures `console.log` / `console.warn` / `console.error` AT\n * CREATION and writes through those references, so a later `Capture` that PATCHES `console.*` can never\n * feed this sink's output back into itself (the no-capture-loop principle, AGENTS / the core sink's\n * precedent). Create the sink (or the logger) BEFORE installing a capture.\n *\n * @example\n * ```ts\n * import { createLogger } from '@src/core'\n * import { createBrowserSink } from '@src/browser'\n *\n * const logger = createLogger({ name: 'app', sink: createBrowserSink() })\n * logger.error('boom') // → console.error('%c…', 'color:#cd0000;…') in DevTools\n * ```\n */\nexport function createBrowserSink(): SinkInterface {\n\t// Snapshot the three console writers NOW — bound to their `console` receiver — so a later patch of\n\t// `console.*` (by Capture) can never reach this sink's output (no capture loop), exactly as core's\n\t// `createConsoleSink` does.\n\tconst log = console.log.bind(console)\n\tconst warn = console.warn.bind(console)\n\tconst error = console.error.bind(console)\n\treturn {\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\t// Degrade the animation redraw first: a leading `\\r` can't overwrite a line in a browser\n\t\t\t// console, so drop it and write a fresh, non-overwriting line (the locked decision).\n\t\t\tconst line = text.startsWith('\\r') ? text.slice(1) : text\n\t\t\tconst { format, styles } = ansiToConsole(line)\n\t\t\tif (level === 'error') {\n\t\t\t\terror(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (level === 'warn') {\n\t\t\t\twarn(format, ...styles)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog(format, ...styles)\n\t\t},\n\t}\n}\n"],"mappings":"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"}