@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.
- package/dist/src/browser/index.d.ts +28 -21
- package/dist/src/browser/index.js +19 -66
- package/dist/src/core/index.cjs +280 -77
- package/dist/src/core/index.d.cts +221 -29
- package/dist/src/core/index.d.ts +221 -29
- package/dist/src/core/index.js +278 -77
- package/dist/src/server/index.cjs +47 -13
- package/dist/src/server/index.d.cts +48 -17
- package/dist/src/server/index.d.ts +48 -17
- package/dist/src/server/index.js +47 -14
- package/package.json +5 -5
- package/dist/src/browser/index.js.map +0 -1
- package/dist/src/core/index.cjs.map +0 -1
- package/dist/src/core/index.js.map +0 -1
- package/dist/src/server/index.cjs.map +0 -1
- package/dist/src/server/index.js.map +0 -1
|
@@ -84,6 +84,32 @@ function columnsOf(target) {
|
|
|
84
84
|
return 80;
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
+
* Infer whether one stream target should receive styled output. The result is a construction-time
|
|
88
|
+
* target fact for {@link import('./factories.js').createServerSink}; this helper is pure and never
|
|
89
|
+
* reads process globals itself.
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* A present `FORCE_COLOR` key has first precedence: only the exact value `'0'` disables styling.
|
|
93
|
+
* Next, a non-empty `NO_COLOR` disables styling. Otherwise styling follows
|
|
94
|
+
* `target.isTTY === true`.
|
|
95
|
+
*
|
|
96
|
+
* @param target - The stream target whose terminal capability is the fallback
|
|
97
|
+
* @param environment - The environment record supplying `FORCE_COLOR` and `NO_COLOR`
|
|
98
|
+
* @returns Whether output for the target should retain styling and control sequences
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* inferStyled({ write: () => true, isTTY: false }, { FORCE_COLOR: '1' }) // true
|
|
103
|
+
* inferStyled({ write: () => true, isTTY: true }, { NO_COLOR: '1' }) // false
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
function inferStyled(target, environment) {
|
|
107
|
+
if (Object.hasOwn(environment, "FORCE_COLOR")) return environment.FORCE_COLOR !== "0";
|
|
108
|
+
const disabled = environment.NO_COLOR;
|
|
109
|
+
if (disabled !== void 0 && disabled !== "") return false;
|
|
110
|
+
return target.isTTY === true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
87
113
|
* Decode one `process.stdout.write` / `process.stderr.write` chunk to a string — TOTAL, never
|
|
88
114
|
* throws (AGENTS §14). The process write signature accepts `string | Uint8Array` plus an optional
|
|
89
115
|
* encoding; the capture wrapper reuses this so intercepting a raw stream write can never crash the
|
|
@@ -277,10 +303,9 @@ var ProcessCapture = class {
|
|
|
277
303
|
/**
|
|
278
304
|
* Create the server TTY {@link ServerSinkInterface} — the C-g server output backend, the
|
|
279
305
|
* env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`
|
|
280
|
-
* routes by level to the process streams and
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* the ANSI to clean text when the stream is piped to a file or another process.
|
|
306
|
+
* routes by level to the process streams and uses construction-time styled facts: it sends ANSI
|
|
307
|
+
* straight to a styled target (with a leading `\r` overwriting a terminal line natively) but
|
|
308
|
+
* {@link import('@src/core').strip}s ANSI to clean text for a plain target.
|
|
284
309
|
*
|
|
285
310
|
* @param options - See {@link ServerSinkOptions}
|
|
286
311
|
* @returns A {@link ServerSinkInterface} — a {@link import('@src/core').SinkInterface} that also
|
|
@@ -290,11 +315,11 @@ var ProcessCapture = class {
|
|
|
290
315
|
* - **Routes by level.** `error` / `warn` → the error stream (`process.stderr` by default), every
|
|
291
316
|
* other level (and an omitted level) → the out stream (`process.stdout`) — the SAME routing as
|
|
292
317
|
* core's `createConsoleSink`, so a logger's `error` reaches `stderr`.
|
|
293
|
-
* - **
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
318
|
+
* - **Per-target styled facts.** At construction, each target uses `options.styled` when supplied;
|
|
319
|
+
* otherwise {@link inferStyled} applies the injected `environment` (default `process.env`) and
|
|
320
|
+
* then that target's `isTTY`.
|
|
321
|
+
* Writes use those stored facts, so `styled` and the out target's strip decision never disagree;
|
|
322
|
+
* the err target keeps its own fact internally.
|
|
298
323
|
* - **Width.** `columns` reflects the live `out.columns` (so it tracks a terminal resize), falling
|
|
299
324
|
* back to {@link import('./constants.js').DEFAULT_COLUMNS} when the out stream is not a TTY — or a
|
|
300
325
|
* fixed value when `options.columns` is supplied. Feed it to a `Reporter` / `Progress` `width`.
|
|
@@ -306,11 +331,12 @@ var ProcessCapture = class {
|
|
|
306
331
|
*
|
|
307
332
|
* @example
|
|
308
333
|
* ```ts
|
|
309
|
-
* import { createLogger, createReporter } from '@src/core'
|
|
334
|
+
* import { createLogger, createReporter, createStyler } from '@src/core'
|
|
310
335
|
* import { createServerSink } from '@src/server'
|
|
311
336
|
*
|
|
312
337
|
* const sink = createServerSink()
|
|
313
|
-
* const
|
|
338
|
+
* const styler = createStyler({ enabled: sink.styled })
|
|
339
|
+
* const logger = createLogger({ name: 'app', sink, styler })
|
|
314
340
|
* logger.error('boom') // → process.stderr, ANSI rendered on a TTY / stripped to a pipe
|
|
315
341
|
* const reporter = createReporter({ sink, width: sink.columns })
|
|
316
342
|
* ```
|
|
@@ -318,12 +344,19 @@ var ProcessCapture = class {
|
|
|
318
344
|
function createServerSink(options) {
|
|
319
345
|
const out = isStreamTarget(options?.out) ? options.out : process.stdout;
|
|
320
346
|
const err = isStreamTarget(options?.err) ? options.err : process.stderr;
|
|
347
|
+
const styled = options?.styled;
|
|
348
|
+
const environment = options?.environment ?? process.env;
|
|
349
|
+
const outStyled = styled ?? inferStyled(out, environment);
|
|
350
|
+
const errStyled = styled ?? inferStyled(err, environment);
|
|
321
351
|
const fixed = options?.columns;
|
|
322
352
|
return Object.freeze({
|
|
353
|
+
styled: outStyled,
|
|
323
354
|
write(text, level) {
|
|
324
|
-
const
|
|
355
|
+
const error = level === "error" || level === "warn";
|
|
356
|
+
const target = error ? err : out;
|
|
357
|
+
const keep = error ? errStyled : outStyled;
|
|
325
358
|
const line = text.startsWith("\r") ? text : `${text}\n`;
|
|
326
|
-
target.write(
|
|
359
|
+
target.write(keep ? line : (0, _src_core.stripControls)((0, _src_core.strip)(line)));
|
|
327
360
|
},
|
|
328
361
|
get columns() {
|
|
329
362
|
return typeof fixed === "number" ? fixed : columnsOf(out);
|
|
@@ -372,6 +405,7 @@ exports.columnsOf = columnsOf;
|
|
|
372
405
|
exports.createProcessCapture = createProcessCapture;
|
|
373
406
|
exports.createServerSink = createServerSink;
|
|
374
407
|
exports.decodeChunk = decodeChunk;
|
|
408
|
+
exports.inferStyled = inferStyled;
|
|
375
409
|
exports.isBufferEncoding = isBufferEncoding;
|
|
376
410
|
exports.isStreamTarget = isStreamTarget;
|
|
377
411
|
|
|
@@ -72,10 +72,9 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
72
72
|
/**
|
|
73
73
|
* Create the server TTY {@link ServerSinkInterface} — the C-g server output backend, the
|
|
74
74
|
* env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`
|
|
75
|
-
* routes by level to the process streams and
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* the ANSI to clean text when the stream is piped to a file or another process.
|
|
75
|
+
* routes by level to the process streams and uses construction-time styled facts: it sends ANSI
|
|
76
|
+
* straight to a styled target (with a leading `\r` overwriting a terminal line natively) but
|
|
77
|
+
* {@link import('@src/core').strip}s ANSI to clean text for a plain target.
|
|
79
78
|
*
|
|
80
79
|
* @param options - See {@link ServerSinkOptions}
|
|
81
80
|
* @returns A {@link ServerSinkInterface} — a {@link import('@src/core').SinkInterface} that also
|
|
@@ -85,11 +84,11 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
85
84
|
* - **Routes by level.** `error` / `warn` → the error stream (`process.stderr` by default), every
|
|
86
85
|
* other level (and an omitted level) → the out stream (`process.stdout`) — the SAME routing as
|
|
87
86
|
* core's `createConsoleSink`, so a logger's `error` reaches `stderr`.
|
|
88
|
-
* - **
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
87
|
+
* - **Per-target styled facts.** At construction, each target uses `options.styled` when supplied;
|
|
88
|
+
* otherwise {@link inferStyled} applies the injected `environment` (default `process.env`) and
|
|
89
|
+
* then that target's `isTTY`.
|
|
90
|
+
* Writes use those stored facts, so `styled` and the out target's strip decision never disagree;
|
|
91
|
+
* the err target keeps its own fact internally.
|
|
93
92
|
* - **Width.** `columns` reflects the live `out.columns` (so it tracks a terminal resize), falling
|
|
94
93
|
* back to {@link import('./constants.js').DEFAULT_COLUMNS} when the out stream is not a TTY — or a
|
|
95
94
|
* fixed value when `options.columns` is supplied. Feed it to a `Reporter` / `Progress` `width`.
|
|
@@ -101,11 +100,12 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
101
100
|
*
|
|
102
101
|
* @example
|
|
103
102
|
* ```ts
|
|
104
|
-
* import { createLogger, createReporter } from '@src/core'
|
|
103
|
+
* import { createLogger, createReporter, createStyler } from '@src/core'
|
|
105
104
|
* import { createServerSink } from '@src/server'
|
|
106
105
|
*
|
|
107
106
|
* const sink = createServerSink()
|
|
108
|
-
* const
|
|
107
|
+
* const styler = createStyler({ enabled: sink.styled })
|
|
108
|
+
* const logger = createLogger({ name: 'app', sink, styler })
|
|
109
109
|
* logger.error('boom') // → process.stderr, ANSI rendered on a TTY / stripped to a pipe
|
|
110
110
|
* const reporter = createReporter({ sink, width: sink.columns })
|
|
111
111
|
* ```
|
|
@@ -167,6 +167,28 @@ export declare const DEFAULT_CAPTURE_LIMIT = 1000;
|
|
|
167
167
|
*/
|
|
168
168
|
export declare const DEFAULT_COLUMNS = 80;
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Infer whether one stream target should receive styled output. The result is a construction-time
|
|
172
|
+
* target fact for {@link import('./factories.js').createServerSink}; this helper is pure and never
|
|
173
|
+
* reads process globals itself.
|
|
174
|
+
*
|
|
175
|
+
* @remarks
|
|
176
|
+
* A present `FORCE_COLOR` key has first precedence: only the exact value `'0'` disables styling.
|
|
177
|
+
* Next, a non-empty `NO_COLOR` disables styling. Otherwise styling follows
|
|
178
|
+
* `target.isTTY === true`.
|
|
179
|
+
*
|
|
180
|
+
* @param target - The stream target whose terminal capability is the fallback
|
|
181
|
+
* @param environment - The environment record supplying `FORCE_COLOR` and `NO_COLOR`
|
|
182
|
+
* @returns Whether output for the target should retain styling and control sequences
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* inferStyled({ write: () => true, isTTY: false }, { FORCE_COLOR: '1' }) // true
|
|
187
|
+
* inferStyled({ write: () => true, isTTY: true }, { NO_COLOR: '1' }) // false
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export declare function inferStyled(target: StreamTargetInterface, environment: Readonly<Record<string, string | undefined>>): boolean;
|
|
191
|
+
|
|
170
192
|
/**
|
|
171
193
|
* Whether `encoding` is a {@link BufferEncoding} accepted by `Buffer.prototype.toString` — a total
|
|
172
194
|
* guard used by {@link decodeChunk} to honor a process-write `encoding` argument only when it is a
|
|
@@ -354,13 +376,17 @@ export declare interface ProcessCaptureOptions {
|
|
|
354
376
|
* A {@link SinkInterface} that also exposes the target terminal's {@link columns} width — the shape
|
|
355
377
|
* {@link import('./factories.js').createServerSink} returns. It is a drop-in {@link SinkInterface}
|
|
356
378
|
* (so a `Logger` / `Reporter` / `Spinner` / `Progress` takes it as `sink`) whose extra `columns`
|
|
357
|
-
* getter lets a consumer size a `Reporter`'s layout to the live terminal.
|
|
379
|
+
* getter lets a consumer size a `Reporter`'s layout to the live terminal. Its `styled` fact lets
|
|
380
|
+
* the same consumer enable or disable its styler for the out target.
|
|
358
381
|
*
|
|
359
382
|
* @remarks
|
|
360
|
-
* `
|
|
361
|
-
*
|
|
383
|
+
* - `styled` is the `out` target's construction-time fact. The sink handles `err` through its own
|
|
384
|
+
* independently inferred fact because the two targets can differ.
|
|
385
|
+
* - `columns` is a getter, re-read on every access — so it reflects the CURRENT terminal width (a
|
|
386
|
+
* resize is observed) unless a fixed `options.columns` was supplied, in which case it is constant.
|
|
362
387
|
*/
|
|
363
388
|
export declare interface ServerSinkInterface extends SinkInterface {
|
|
389
|
+
readonly styled: boolean;
|
|
364
390
|
readonly columns: number;
|
|
365
391
|
}
|
|
366
392
|
|
|
@@ -372,6 +398,9 @@ export declare interface ServerSinkInterface extends SinkInterface {
|
|
|
372
398
|
* - `out` — the stream `info` / `debug` (and an omitted level) are written to; defaults to
|
|
373
399
|
* `process.stdout`. Any {@link StreamTargetInterface} is accepted, so a test injects a fake.
|
|
374
400
|
* - `err` — the stream `error` / `warn` are written to; defaults to `process.stderr`.
|
|
401
|
+
* - `styled` — an explicit styling decision for both targets. When omitted, each target infers its
|
|
402
|
+
* own fact from `FORCE_COLOR`, `NO_COLOR`, and `isTTY` at construction.
|
|
403
|
+
* - `environment` — the environment used for inference; defaults to `process.env`.
|
|
375
404
|
* - `columns` — an explicit width override for {@link ServerSinkInterface.columns}. When omitted,
|
|
376
405
|
* the sink reads the live `out.columns` (so it tracks a terminal resize), falling back to
|
|
377
406
|
* {@link import('./constants.js').DEFAULT_COLUMNS} when the out stream is not a TTY.
|
|
@@ -379,6 +408,8 @@ export declare interface ServerSinkInterface extends SinkInterface {
|
|
|
379
408
|
export declare interface ServerSinkOptions {
|
|
380
409
|
readonly out?: StreamTargetInterface;
|
|
381
410
|
readonly err?: StreamTargetInterface;
|
|
411
|
+
readonly styled?: boolean;
|
|
412
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
382
413
|
readonly columns?: number;
|
|
383
414
|
}
|
|
384
415
|
|
|
@@ -423,9 +454,9 @@ export declare type StreamLevel = 'stdout' | 'stderr';
|
|
|
423
454
|
* backpressure boolean (`false` when the kernel buffer is full). A `process` stream returns it;
|
|
424
455
|
* a fake may return `void` (read as truthy / no backpressure).
|
|
425
456
|
* - `isTTY` — present and `true` on a real terminal, absent / `false` when the stream is piped to a
|
|
426
|
-
* file or another process.
|
|
427
|
-
*
|
|
428
|
-
* text
|
|
457
|
+
* file or another process. When no explicit styling override exists, the sink reads it at
|
|
458
|
+
* construction to decide whether to keep ANSI or {@link import('@src/core').strip} it to clean
|
|
459
|
+
* text.
|
|
429
460
|
* - `columns` — the terminal width in character cells when the stream is a TTY, `undefined`
|
|
430
461
|
* otherwise; the sink surfaces it as {@link ServerSinkInterface.columns} so a consumer can feed a
|
|
431
462
|
* `Reporter` / `Progress` its render width.
|
|
@@ -72,10 +72,9 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
72
72
|
/**
|
|
73
73
|
* Create the server TTY {@link ServerSinkInterface} — the C-g server output backend, the
|
|
74
74
|
* env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`
|
|
75
|
-
* routes by level to the process streams and
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* the ANSI to clean text when the stream is piped to a file or another process.
|
|
75
|
+
* routes by level to the process streams and uses construction-time styled facts: it sends ANSI
|
|
76
|
+
* straight to a styled target (with a leading `\r` overwriting a terminal line natively) but
|
|
77
|
+
* {@link import('@src/core').strip}s ANSI to clean text for a plain target.
|
|
79
78
|
*
|
|
80
79
|
* @param options - See {@link ServerSinkOptions}
|
|
81
80
|
* @returns A {@link ServerSinkInterface} — a {@link import('@src/core').SinkInterface} that also
|
|
@@ -85,11 +84,11 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
85
84
|
* - **Routes by level.** `error` / `warn` → the error stream (`process.stderr` by default), every
|
|
86
85
|
* other level (and an omitted level) → the out stream (`process.stdout`) — the SAME routing as
|
|
87
86
|
* core's `createConsoleSink`, so a logger's `error` reaches `stderr`.
|
|
88
|
-
* - **
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
87
|
+
* - **Per-target styled facts.** At construction, each target uses `options.styled` when supplied;
|
|
88
|
+
* otherwise {@link inferStyled} applies the injected `environment` (default `process.env`) and
|
|
89
|
+
* then that target's `isTTY`.
|
|
90
|
+
* Writes use those stored facts, so `styled` and the out target's strip decision never disagree;
|
|
91
|
+
* the err target keeps its own fact internally.
|
|
93
92
|
* - **Width.** `columns` reflects the live `out.columns` (so it tracks a terminal resize), falling
|
|
94
93
|
* back to {@link import('./constants.js').DEFAULT_COLUMNS} when the out stream is not a TTY — or a
|
|
95
94
|
* fixed value when `options.columns` is supplied. Feed it to a `Reporter` / `Progress` `width`.
|
|
@@ -101,11 +100,12 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
101
100
|
*
|
|
102
101
|
* @example
|
|
103
102
|
* ```ts
|
|
104
|
-
* import { createLogger, createReporter } from '@src/core'
|
|
103
|
+
* import { createLogger, createReporter, createStyler } from '@src/core'
|
|
105
104
|
* import { createServerSink } from '@src/server'
|
|
106
105
|
*
|
|
107
106
|
* const sink = createServerSink()
|
|
108
|
-
* const
|
|
107
|
+
* const styler = createStyler({ enabled: sink.styled })
|
|
108
|
+
* const logger = createLogger({ name: 'app', sink, styler })
|
|
109
109
|
* logger.error('boom') // → process.stderr, ANSI rendered on a TTY / stripped to a pipe
|
|
110
110
|
* const reporter = createReporter({ sink, width: sink.columns })
|
|
111
111
|
* ```
|
|
@@ -167,6 +167,28 @@ export declare const DEFAULT_CAPTURE_LIMIT = 1000;
|
|
|
167
167
|
*/
|
|
168
168
|
export declare const DEFAULT_COLUMNS = 80;
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Infer whether one stream target should receive styled output. The result is a construction-time
|
|
172
|
+
* target fact for {@link import('./factories.js').createServerSink}; this helper is pure and never
|
|
173
|
+
* reads process globals itself.
|
|
174
|
+
*
|
|
175
|
+
* @remarks
|
|
176
|
+
* A present `FORCE_COLOR` key has first precedence: only the exact value `'0'` disables styling.
|
|
177
|
+
* Next, a non-empty `NO_COLOR` disables styling. Otherwise styling follows
|
|
178
|
+
* `target.isTTY === true`.
|
|
179
|
+
*
|
|
180
|
+
* @param target - The stream target whose terminal capability is the fallback
|
|
181
|
+
* @param environment - The environment record supplying `FORCE_COLOR` and `NO_COLOR`
|
|
182
|
+
* @returns Whether output for the target should retain styling and control sequences
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* inferStyled({ write: () => true, isTTY: false }, { FORCE_COLOR: '1' }) // true
|
|
187
|
+
* inferStyled({ write: () => true, isTTY: true }, { NO_COLOR: '1' }) // false
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export declare function inferStyled(target: StreamTargetInterface, environment: Readonly<Record<string, string | undefined>>): boolean;
|
|
191
|
+
|
|
170
192
|
/**
|
|
171
193
|
* Whether `encoding` is a {@link BufferEncoding} accepted by `Buffer.prototype.toString` — a total
|
|
172
194
|
* guard used by {@link decodeChunk} to honor a process-write `encoding` argument only when it is a
|
|
@@ -354,13 +376,17 @@ export declare interface ProcessCaptureOptions {
|
|
|
354
376
|
* A {@link SinkInterface} that also exposes the target terminal's {@link columns} width — the shape
|
|
355
377
|
* {@link import('./factories.js').createServerSink} returns. It is a drop-in {@link SinkInterface}
|
|
356
378
|
* (so a `Logger` / `Reporter` / `Spinner` / `Progress` takes it as `sink`) whose extra `columns`
|
|
357
|
-
* getter lets a consumer size a `Reporter`'s layout to the live terminal.
|
|
379
|
+
* getter lets a consumer size a `Reporter`'s layout to the live terminal. Its `styled` fact lets
|
|
380
|
+
* the same consumer enable or disable its styler for the out target.
|
|
358
381
|
*
|
|
359
382
|
* @remarks
|
|
360
|
-
* `
|
|
361
|
-
*
|
|
383
|
+
* - `styled` is the `out` target's construction-time fact. The sink handles `err` through its own
|
|
384
|
+
* independently inferred fact because the two targets can differ.
|
|
385
|
+
* - `columns` is a getter, re-read on every access — so it reflects the CURRENT terminal width (a
|
|
386
|
+
* resize is observed) unless a fixed `options.columns` was supplied, in which case it is constant.
|
|
362
387
|
*/
|
|
363
388
|
export declare interface ServerSinkInterface extends SinkInterface {
|
|
389
|
+
readonly styled: boolean;
|
|
364
390
|
readonly columns: number;
|
|
365
391
|
}
|
|
366
392
|
|
|
@@ -372,6 +398,9 @@ export declare interface ServerSinkInterface extends SinkInterface {
|
|
|
372
398
|
* - `out` — the stream `info` / `debug` (and an omitted level) are written to; defaults to
|
|
373
399
|
* `process.stdout`. Any {@link StreamTargetInterface} is accepted, so a test injects a fake.
|
|
374
400
|
* - `err` — the stream `error` / `warn` are written to; defaults to `process.stderr`.
|
|
401
|
+
* - `styled` — an explicit styling decision for both targets. When omitted, each target infers its
|
|
402
|
+
* own fact from `FORCE_COLOR`, `NO_COLOR`, and `isTTY` at construction.
|
|
403
|
+
* - `environment` — the environment used for inference; defaults to `process.env`.
|
|
375
404
|
* - `columns` — an explicit width override for {@link ServerSinkInterface.columns}. When omitted,
|
|
376
405
|
* the sink reads the live `out.columns` (so it tracks a terminal resize), falling back to
|
|
377
406
|
* {@link import('./constants.js').DEFAULT_COLUMNS} when the out stream is not a TTY.
|
|
@@ -379,6 +408,8 @@ export declare interface ServerSinkInterface extends SinkInterface {
|
|
|
379
408
|
export declare interface ServerSinkOptions {
|
|
380
409
|
readonly out?: StreamTargetInterface;
|
|
381
410
|
readonly err?: StreamTargetInterface;
|
|
411
|
+
readonly styled?: boolean;
|
|
412
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
382
413
|
readonly columns?: number;
|
|
383
414
|
}
|
|
384
415
|
|
|
@@ -423,9 +454,9 @@ export declare type StreamLevel = 'stdout' | 'stderr';
|
|
|
423
454
|
* backpressure boolean (`false` when the kernel buffer is full). A `process` stream returns it;
|
|
424
455
|
* a fake may return `void` (read as truthy / no backpressure).
|
|
425
456
|
* - `isTTY` — present and `true` on a real terminal, absent / `false` when the stream is piped to a
|
|
426
|
-
* file or another process.
|
|
427
|
-
*
|
|
428
|
-
* text
|
|
457
|
+
* file or another process. When no explicit styling override exists, the sink reads it at
|
|
458
|
+
* construction to decide whether to keep ANSI or {@link import('@src/core').strip} it to clean
|
|
459
|
+
* text.
|
|
429
460
|
* - `columns` — the terminal width in character cells when the stream is a TTY, `undefined`
|
|
430
461
|
* otherwise; the sink surfaces it as {@link ServerSinkInterface.columns} so a consumer can feed a
|
|
431
462
|
* `Reporter` / `Progress` its render width.
|
package/dist/src/server/index.js
CHANGED
|
@@ -83,6 +83,32 @@ function columnsOf(target) {
|
|
|
83
83
|
return 80;
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
+
* Infer whether one stream target should receive styled output. The result is a construction-time
|
|
87
|
+
* target fact for {@link import('./factories.js').createServerSink}; this helper is pure and never
|
|
88
|
+
* reads process globals itself.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* A present `FORCE_COLOR` key has first precedence: only the exact value `'0'` disables styling.
|
|
92
|
+
* Next, a non-empty `NO_COLOR` disables styling. Otherwise styling follows
|
|
93
|
+
* `target.isTTY === true`.
|
|
94
|
+
*
|
|
95
|
+
* @param target - The stream target whose terminal capability is the fallback
|
|
96
|
+
* @param environment - The environment record supplying `FORCE_COLOR` and `NO_COLOR`
|
|
97
|
+
* @returns Whether output for the target should retain styling and control sequences
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* inferStyled({ write: () => true, isTTY: false }, { FORCE_COLOR: '1' }) // true
|
|
102
|
+
* inferStyled({ write: () => true, isTTY: true }, { NO_COLOR: '1' }) // false
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
function inferStyled(target, environment) {
|
|
106
|
+
if (Object.hasOwn(environment, "FORCE_COLOR")) return environment.FORCE_COLOR !== "0";
|
|
107
|
+
const disabled = environment.NO_COLOR;
|
|
108
|
+
if (disabled !== void 0 && disabled !== "") return false;
|
|
109
|
+
return target.isTTY === true;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
86
112
|
* Decode one `process.stdout.write` / `process.stderr.write` chunk to a string — TOTAL, never
|
|
87
113
|
* throws (AGENTS §14). The process write signature accepts `string | Uint8Array` plus an optional
|
|
88
114
|
* encoding; the capture wrapper reuses this so intercepting a raw stream write can never crash the
|
|
@@ -276,10 +302,9 @@ var ProcessCapture = class {
|
|
|
276
302
|
/**
|
|
277
303
|
* Create the server TTY {@link ServerSinkInterface} — the C-g server output backend, the
|
|
278
304
|
* env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`
|
|
279
|
-
* routes by level to the process streams and
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* the ANSI to clean text when the stream is piped to a file or another process.
|
|
305
|
+
* routes by level to the process streams and uses construction-time styled facts: it sends ANSI
|
|
306
|
+
* straight to a styled target (with a leading `\r` overwriting a terminal line natively) but
|
|
307
|
+
* {@link import('@src/core').strip}s ANSI to clean text for a plain target.
|
|
283
308
|
*
|
|
284
309
|
* @param options - See {@link ServerSinkOptions}
|
|
285
310
|
* @returns A {@link ServerSinkInterface} — a {@link import('@src/core').SinkInterface} that also
|
|
@@ -289,11 +314,11 @@ var ProcessCapture = class {
|
|
|
289
314
|
* - **Routes by level.** `error` / `warn` → the error stream (`process.stderr` by default), every
|
|
290
315
|
* other level (and an omitted level) → the out stream (`process.stdout`) — the SAME routing as
|
|
291
316
|
* core's `createConsoleSink`, so a logger's `error` reaches `stderr`.
|
|
292
|
-
* - **
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
317
|
+
* - **Per-target styled facts.** At construction, each target uses `options.styled` when supplied;
|
|
318
|
+
* otherwise {@link inferStyled} applies the injected `environment` (default `process.env`) and
|
|
319
|
+
* then that target's `isTTY`.
|
|
320
|
+
* Writes use those stored facts, so `styled` and the out target's strip decision never disagree;
|
|
321
|
+
* the err target keeps its own fact internally.
|
|
297
322
|
* - **Width.** `columns` reflects the live `out.columns` (so it tracks a terminal resize), falling
|
|
298
323
|
* back to {@link import('./constants.js').DEFAULT_COLUMNS} when the out stream is not a TTY — or a
|
|
299
324
|
* fixed value when `options.columns` is supplied. Feed it to a `Reporter` / `Progress` `width`.
|
|
@@ -305,11 +330,12 @@ var ProcessCapture = class {
|
|
|
305
330
|
*
|
|
306
331
|
* @example
|
|
307
332
|
* ```ts
|
|
308
|
-
* import { createLogger, createReporter } from '@src/core'
|
|
333
|
+
* import { createLogger, createReporter, createStyler } from '@src/core'
|
|
309
334
|
* import { createServerSink } from '@src/server'
|
|
310
335
|
*
|
|
311
336
|
* const sink = createServerSink()
|
|
312
|
-
* const
|
|
337
|
+
* const styler = createStyler({ enabled: sink.styled })
|
|
338
|
+
* const logger = createLogger({ name: 'app', sink, styler })
|
|
313
339
|
* logger.error('boom') // → process.stderr, ANSI rendered on a TTY / stripped to a pipe
|
|
314
340
|
* const reporter = createReporter({ sink, width: sink.columns })
|
|
315
341
|
* ```
|
|
@@ -317,12 +343,19 @@ var ProcessCapture = class {
|
|
|
317
343
|
function createServerSink(options) {
|
|
318
344
|
const out = isStreamTarget(options?.out) ? options.out : process.stdout;
|
|
319
345
|
const err = isStreamTarget(options?.err) ? options.err : process.stderr;
|
|
346
|
+
const styled = options?.styled;
|
|
347
|
+
const environment = options?.environment ?? process.env;
|
|
348
|
+
const outStyled = styled ?? inferStyled(out, environment);
|
|
349
|
+
const errStyled = styled ?? inferStyled(err, environment);
|
|
320
350
|
const fixed = options?.columns;
|
|
321
351
|
return Object.freeze({
|
|
352
|
+
styled: outStyled,
|
|
322
353
|
write(text, level) {
|
|
323
|
-
const
|
|
354
|
+
const error = level === "error" || level === "warn";
|
|
355
|
+
const target = error ? err : out;
|
|
356
|
+
const keep = error ? errStyled : outStyled;
|
|
324
357
|
const line = text.startsWith("\r") ? text : `${text}\n`;
|
|
325
|
-
target.write(
|
|
358
|
+
target.write(keep ? line : stripControls(strip(line)));
|
|
326
359
|
},
|
|
327
360
|
get columns() {
|
|
328
361
|
return typeof fixed === "number" ? fixed : columnsOf(out);
|
|
@@ -361,6 +394,6 @@ function createProcessCapture(options) {
|
|
|
361
394
|
return new ProcessCapture(options);
|
|
362
395
|
}
|
|
363
396
|
//#endregion
|
|
364
|
-
export { DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, DEFAULT_COLUMNS, ProcessCapture, STREAM_LEVELS, STREAM_LEVEL_MAP, columnsOf, createProcessCapture, createServerSink, decodeChunk, isBufferEncoding, isStreamTarget };
|
|
397
|
+
export { DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, DEFAULT_COLUMNS, ProcessCapture, STREAM_LEVELS, STREAM_LEVEL_MAP, columnsOf, createProcessCapture, createServerSink, decodeChunk, inferStyled, isBufferEncoding, isStreamTarget };
|
|
365
398
|
|
|
366
399
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/console",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A typed console/terminal output toolkit for the @orkestrel line — logging, spinners, progress, and capture. Part of the @orkestrel line.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
@@ -87,14 +87,14 @@
|
|
|
87
87
|
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
|
-
"@orkestrel/contract": "^0.0.
|
|
91
|
-
"@orkestrel/emitter": "^0.0.
|
|
90
|
+
"@orkestrel/contract": "^0.0.12",
|
|
91
|
+
"@orkestrel/emitter": "^0.0.7"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@microsoft/api-extractor": "^7.58.12",
|
|
95
95
|
"@orkestrel/guide": "^0.0.11",
|
|
96
|
-
"@orkestrel/scaffold": "^0.0.
|
|
97
|
-
"@orkestrel/test": "^0.0.
|
|
96
|
+
"@orkestrel/scaffold": "^0.0.38",
|
|
97
|
+
"@orkestrel/test": "^0.0.6",
|
|
98
98
|
"@types/node": "^26.1.2",
|
|
99
99
|
"@vitest/browser-playwright": "^4.1.10",
|
|
100
100
|
"oxfmt": "^0.61.0",
|
|
@@ -1 +0,0 @@
|
|
|
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, 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. Each number→CSS row is written as a\n// COMPUTED key over core's code maps with its value read from the palette, so neither the\n// number↔name mapping nor a hex value is re-hardcoded here; `tests/src/browser/helpers.test.ts`\n// walks core's `COLORS` and fails if a row is missing. This file holds DATA only — a derivation\n// written as a callback would be module function syntax in a data-kind file (AGENTS §5).\n// The SGR-scan pattern is built from core's `ESC` so no control-character literal appears in\n// source. UPPER_SNAKE, deeply `Object.freeze`d, every member 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. The sink reads this while\n * scanning a run to translate a foreground code to CSS.\n *\n * @remarks\n * Every key is core's {@link FOREGROUND_CODES} entry and every value reads {@link COLOR_HEX}, so\n * neither the number↔name mapping nor the palette is duplicated here — this is a table of\n * references, not of literals. `tests/src/browser/helpers.test.ts` walks core's {@link COLORS} and\n * asserts this record covers every one of them, so a color added to core fails there rather than\n * going silently untranslated. Deeply frozen.\n */\nexport const FOREGROUND_CSS: Readonly<Record<number, string>> = Object.freeze({\n\t[FOREGROUND_CODES.black]: `color:${COLOR_HEX.black}`,\n\t[FOREGROUND_CODES.red]: `color:${COLOR_HEX.red}`,\n\t[FOREGROUND_CODES.green]: `color:${COLOR_HEX.green}`,\n\t[FOREGROUND_CODES.yellow]: `color:${COLOR_HEX.yellow}`,\n\t[FOREGROUND_CODES.blue]: `color:${COLOR_HEX.blue}`,\n\t[FOREGROUND_CODES.magenta]: `color:${COLOR_HEX.magenta}`,\n\t[FOREGROUND_CODES.cyan]: `color:${COLOR_HEX.cyan}`,\n\t[FOREGROUND_CODES.white]: `color:${COLOR_HEX.white}`,\n\t[FOREGROUND_CODES.brightBlack]: `color:${COLOR_HEX.brightBlack}`,\n\t[FOREGROUND_CODES.brightRed]: `color:${COLOR_HEX.brightRed}`,\n\t[FOREGROUND_CODES.brightGreen]: `color:${COLOR_HEX.brightGreen}`,\n\t[FOREGROUND_CODES.brightYellow]: `color:${COLOR_HEX.brightYellow}`,\n\t[FOREGROUND_CODES.brightBlue]: `color:${COLOR_HEX.brightBlue}`,\n\t[FOREGROUND_CODES.brightMagenta]: `color:${COLOR_HEX.brightMagenta}`,\n\t[FOREGROUND_CODES.brightCyan]: `color:${COLOR_HEX.brightCyan}`,\n\t[FOREGROUND_CODES.brightWhite]: `color:${COLOR_HEX.brightWhite}`,\n})\n\n/**\n * Each SGR BACKGROUND parameter (40–47 / 100–107) → its `background:<hex>` CSS. The sink reads this\n * while scanning a run to translate a background code to CSS.\n *\n * @remarks\n * Built the same way as {@link FOREGROUND_CSS} — keys from core's {@link BACKGROUND_CODES}, values\n * from {@link COLOR_HEX} — and covered by the same exhaustive walk over core's {@link COLORS} in\n * `tests/src/browser/helpers.test.ts`. Deeply frozen.\n */\nexport const BACKGROUND_CSS: Readonly<Record<number, string>> = Object.freeze({\n\t[BACKGROUND_CODES.black]: `background:${COLOR_HEX.black}`,\n\t[BACKGROUND_CODES.red]: `background:${COLOR_HEX.red}`,\n\t[BACKGROUND_CODES.green]: `background:${COLOR_HEX.green}`,\n\t[BACKGROUND_CODES.yellow]: `background:${COLOR_HEX.yellow}`,\n\t[BACKGROUND_CODES.blue]: `background:${COLOR_HEX.blue}`,\n\t[BACKGROUND_CODES.magenta]: `background:${COLOR_HEX.magenta}`,\n\t[BACKGROUND_CODES.cyan]: `background:${COLOR_HEX.cyan}`,\n\t[BACKGROUND_CODES.white]: `background:${COLOR_HEX.white}`,\n\t[BACKGROUND_CODES.brightBlack]: `background:${COLOR_HEX.brightBlack}`,\n\t[BACKGROUND_CODES.brightRed]: `background:${COLOR_HEX.brightRed}`,\n\t[BACKGROUND_CODES.brightGreen]: `background:${COLOR_HEX.brightGreen}`,\n\t[BACKGROUND_CODES.brightYellow]: `background:${COLOR_HEX.brightYellow}`,\n\t[BACKGROUND_CODES.brightBlue]: `background:${COLOR_HEX.brightBlue}`,\n\t[BACKGROUND_CODES.brightMagenta]: `background:${COLOR_HEX.brightMagenta}`,\n\t[BACKGROUND_CODES.brightCyan]: `background:${COLOR_HEX.brightCyan}`,\n\t[BACKGROUND_CODES.brightWhite]: `background:${COLOR_HEX.brightWhite}`,\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` carries immutable style snapshots while its local arrays assemble the final\n// `%c` output; only the standalone, reusable `escapePercent` / `parseParameters` utilities are\n// 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\tlet active: StyleAccumulator = Object.freeze({\n\t\tforeground: '',\n\t\tbackground: '',\n\t\tattributes: Object.freeze([]),\n\t})\n\tconst segments: string[] = []\n\tconst styles: string[] = []\n\tlet cursor = 0\n\tlet pending = ''\n\tlet match: RegExpExecArray | null = scanner.exec(text)\n\tif (match === null) return { format: escapePercent(text), styles: [] }\n\n\t// A null match is the final text boundary, so every visible run passes through one flush path.\n\twhile (true) {\n\t\tconst boundary = match === null ? text.length : match.index\n\t\tpending += escapePercent(text.slice(cursor, boundary))\n\t\tif (pending !== '') {\n\t\t\tsegments.push(`${DIRECTIVE}${pending}`)\n\t\t\tconst declarations = [...active.attributes]\n\t\t\tif (active.foreground !== '') declarations.push(active.foreground)\n\t\t\tif (active.background !== '') declarations.push(active.background)\n\t\t\tstyles.push(declarations.join(';'))\n\t\t\tpending = ''\n\t\t}\n\t\tif (match === null) break\n\n\t\t// Apply one SGR sequence by replacing the readonly accumulator. A reset clears every channel;\n\t\t// colors replace their channel; attributes accumulate once; unknown extensions are ignored.\n\t\tfor (const code of parseParameters(match[1] ?? '')) {\n\t\t\tif (code === RESET_CODE) {\n\t\t\t\tactive = Object.freeze({\n\t\t\t\t\tforeground: '',\n\t\t\t\t\tbackground: '',\n\t\t\t\t\tattributes: Object.freeze([]),\n\t\t\t\t})\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 = Object.freeze({ ...active, 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 = Object.freeze({ ...active, 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 = Object.freeze({\n\t\t\t\t\t...active,\n\t\t\t\t\tattributes: Object.freeze([...active.attributes, attribute]),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tcursor = match.index + match[0].length\n\t\tmatch = scanner.exec(text)\n\t}\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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"}
|