@orkestrel/console 0.0.10 → 0.0.12

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.
@@ -1,34 +1,28 @@
1
1
  import { StringDecoder } from "node:string_decoder";
2
2
  import { Emitter } from "@orkestrel/emitter";
3
- import { strip, stripControls } from "../core/index.js";
3
+ import { Retention, selectWriter, strip, stripControls } from "../core/index.js";
4
4
  //#region src/server/constants.ts
5
5
  /**
6
- * The two process streams a {@link import('./types.js').ProcessCaptureInterface} can intercept, in
6
+ * Lists the two process streams a {@link import('./types.js').ProcessCaptureInterface} can intercept, in
7
7
  * `stdout`-then-`stderr` order — the {@link StreamLevel} universe and the default configured set.
8
8
  */
9
9
  var STREAM_LEVELS = Object.freeze(["stdout", "stderr"]);
10
10
  /**
11
- * The default set of {@link StreamLevel}s a process capture patches when `options.levels` is omitted
12
- * — BOTH streams ({@link STREAM_LEVELS}). A consumer narrows it (e.g. just `['stderr']`) via
13
- * `options.levels`.
14
- */
15
- var DEFAULT_CAPTURE_LEVELS = STREAM_LEVELS;
16
- /**
17
- * The default bounded-buffer cap for a {@link import('./types.js').ProcessCaptureInterface} — at
11
+ * Sets the default bounded-buffer cap for a {@link import('./types.js').ProcessCaptureInterface} at
18
12
  * most this many recent {@link import('./types.js').CapturedChunk}s are retained per buffer (the
19
- * total buffer AND each per-stream bucket; oldest dropped first). Mirrors the core `Capture`'s
20
- * `DEFAULT_CAPTURE_LIMIT`; a consumer overrides it via `options.limit`.
13
+ * total buffer and each per-stream bucket; oldest dropped first). Mirrors the core `Capture`'s
14
+ * `DEFAULT_CAPTURE_LIMIT`; a consumer overrides it through `options.limit`.
21
15
  */
22
- var DEFAULT_CAPTURE_LIMIT = 1e3;
16
+ var DEFAULT_STREAM_LIMIT = 1e3;
23
17
  /**
24
- * The terminal width {@link import('./factories.js').createServerSink} reports through
25
- * {@link import('./types.js').ServerSinkInterface.columns} when the out stream is NOT a TTY (so
18
+ * Sets the terminal width {@link import('./factories.js').createServerSink} reports through
19
+ * {@link import('./types.js').ServerSinkInterface.columns} when the `stdout` stream is not a TTY (so
26
20
  * `.columns` is `undefined`) and no explicit `options.columns` was supplied — the conventional
27
21
  * 80-column default a non-interactive context (a pipe, a CI log) assumes.
28
22
  */
29
23
  var DEFAULT_COLUMNS = 80;
30
24
  /**
31
- * Each {@link StreamLevel}'s {@link LogLevel} for the optional sink forward — the projection a
25
+ * Maps each {@link StreamLevel} to its {@link LogLevel} for the optional sink forward — the projection a
32
26
  * process capture routes through when writing an intercepted chunk to a
33
27
  * {@link import('@src/core').SinkInterface}
34
28
  * (`sink.write(text, STREAM_LEVEL_MAP[level])`). `stderr` is conventionally the error/diagnostic
@@ -40,20 +34,20 @@ var STREAM_LEVEL_MAP = Object.freeze({
40
34
  stderr: "error"
41
35
  });
42
36
  //#endregion
43
- //#region src/server/helpers.ts
37
+ //#region src/server/validators.ts
44
38
  /**
45
- * Whether `value` is a usable {@link StreamTargetInterface} — a record with a callable `write`. A
46
- * total type guard (AGENTS §14): it NEVER throws and returns `false` for anything off-shape, so it
39
+ * Checks whether `value` is a usable {@link StreamTargetInterface} — a record with a callable `write`. A
40
+ * total type guard: it never throws and returns `false` for anything off-shape, so it
47
41
  * narrows the one unavoidable boundary (the real `process.stdout` / `process.stderr`, or a fake
48
42
  * stream a test injects) to the exact slice the sink + capture touch — no `as`.
49
43
  *
50
44
  * @remarks
51
45
  * Only `write` is required (the irreducible output method); `isTTY` and `columns` are optional on
52
46
  * {@link StreamTargetInterface}, so their absence does not disqualify a target — a piped stream
53
- * (no `isTTY`) is still a valid write target, just a non-terminal one.
47
+ * (no `isTTY`) is still a valid write target, only a non-terminal one.
54
48
  *
55
49
  * @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)
56
- * @returns `true` when `value` has a callable `write`
50
+ * @returns True if `value` has a callable `write`; false otherwise
57
51
  *
58
52
  * @example
59
53
  * ```ts
@@ -66,25 +60,38 @@ function isStreamTarget(value) {
66
60
  return typeof value === "object" && value !== null && "write" in value && typeof value.write === "function";
67
61
  }
68
62
  /**
69
- * The width in character cells of a stream target its live `columns` when it is a TTY, else the
70
- * non-interactive {@link DEFAULT_COLUMNS} fallback. The basis a {@link import('./types.js').ServerSinkInterface}
63
+ * Checks whether `encoding` is a {@link BufferEncoding} accepted by `Buffer.prototype.toString` a total
64
+ * guard used by {@link import('./helpers.js').decodeChunk} to honor a process-write `encoding`
65
+ * argument only when it is a real Node encoding (otherwise utf-8 is assumed).
66
+ *
67
+ * @param encoding - The candidate encoding (the second `write` argument, possibly a callback)
68
+ * @returns True if `encoding` names a supported buffer encoding; false otherwise
69
+ */
70
+ function isBufferEncoding(encoding) {
71
+ return typeof encoding === "string" && Buffer.isEncoding(encoding);
72
+ }
73
+ //#endregion
74
+ //#region src/server/helpers.ts
75
+ /**
76
+ * Infers the width in character cells of a stream target — its live `columns` when it is a TTY, else
77
+ * the non-interactive {@link DEFAULT_COLUMNS} fallback. The basis a {@link import('./types.js').ServerSinkInterface}
71
78
  * reports through `columns` so a `Reporter` / `Progress` can size its layout to the terminal.
72
79
  *
73
80
  * @remarks
74
- * Reads `target.columns` ON EACH CALL (so a getter-backed real stream reflects a live resize) and
81
+ * Reads `target.columns` on each call (so a getter-backed real stream reflects a live resize) and
75
82
  * accepts it only when it is a positive finite number; a missing / `0` / non-finite `columns` (a
76
83
  * piped, non-TTY stream) falls back to {@link DEFAULT_COLUMNS}. Total — never throws.
77
84
  *
78
85
  * @param target - The stream whose width to probe
79
86
  * @returns The terminal column count, or {@link DEFAULT_COLUMNS} when not a TTY
80
87
  */
81
- function columnsOf(target) {
88
+ function inferColumns(target) {
82
89
  const columns = target.columns;
83
90
  if (typeof columns === "number" && Number.isFinite(columns) && columns > 0) return columns;
84
91
  return 80;
85
92
  }
86
93
  /**
87
- * Infer whether one stream target should receive styled output. The result is a construction-time
94
+ * Infers whether one stream target receives styled output. The result is a construction-time
88
95
  * target fact for {@link import('./factories.js').createServerSink}; this helper is pure and never
89
96
  * reads process globals itself.
90
97
  *
@@ -95,7 +102,7 @@ function columnsOf(target) {
95
102
  *
96
103
  * @param target - The stream target whose terminal capability is the fallback
97
104
  * @param environment - The environment record supplying `FORCE_COLOR` and `NO_COLOR`
98
- * @returns Whether output for the target should retain styling and control sequences
105
+ * @returns True if output for the target retains styling and control sequences; false otherwise
99
106
  *
100
107
  * @example
101
108
  * ```ts
@@ -110,8 +117,8 @@ function inferStyled(target, environment) {
110
117
  return target.isTTY === true;
111
118
  }
112
119
  /**
113
- * Decode one `process.stdout.write` / `process.stderr.write` chunk to a string — TOTAL, never
114
- * throws (AGENTS §14). The process write signature accepts `string | Uint8Array` plus an optional
120
+ * Decodes one `process.stdout.write` / `process.stderr.write` chunk to a string — total, never
121
+ * throws. The process write signature accepts `string | Uint8Array` plus an optional
115
122
  * encoding; the capture wrapper reuses this so intercepting a raw stream write can never crash the
116
123
  * host (a throw inside `process.stdout.write` would take the program down).
117
124
  *
@@ -120,12 +127,12 @@ function inferStyled(target, environment) {
120
127
  * and `process.stdout.write('text')` all pass a string).
121
128
  * - A `Buffer` chunk is decoded with the supplied `encoding` when it is a recognized
122
129
  * {@link BufferEncoding} (`process` write supports `'utf8'` / `'hex'` / `'base64'` / …), defaulting
123
- * to `'utf8'`; a bare `Uint8Array` is decoded via `TextDecoder` (always utf-8 — the `encoding`
124
- * argument applies ONLY to a `Buffer`, never a plain `Uint8Array`).
130
+ * to `'utf8'`; a bare `Uint8Array` is decoded through `TextDecoder` (always utf-8 — the `encoding`
131
+ * argument applies only to a `Buffer`, never a plain `Uint8Array`).
125
132
  * - Anything else is coerced with `String(chunk)` (a number / object / bigint / symbol a misbehaving
126
133
  * writer hands the stream). The coercion is itself guarded: a value whose `toString` /
127
134
  * `Symbol.toPrimitive` throws yields the stable `'[unprintable]'` placeholder. So the helper is
128
- * TOTAL on every input — it always yields SOME string, never an exception (a throw here would
135
+ * total on every input — it always yields some string, never an exception (a throw here would
129
136
  * escape into `process.*.write` and crash the host).
130
137
  *
131
138
  * @param chunk - The chunk passed to the stream's `write`
@@ -149,42 +156,31 @@ function decodeChunk(chunk, encoding) {
149
156
  return "[unprintable]";
150
157
  }
151
158
  }
152
- /**
153
- * Whether `encoding` is a {@link BufferEncoding} accepted by `Buffer.prototype.toString` — a total
154
- * guard used by {@link decodeChunk} to honor a process-write `encoding` argument only when it is a
155
- * real Node encoding (otherwise utf-8 is assumed).
156
- *
157
- * @param encoding - The candidate encoding (the second `write` argument, possibly a callback)
158
- * @returns `true` when `encoding` names a supported buffer encoding
159
- */
160
- function isBufferEncoding(encoding) {
161
- return typeof encoding === "string" && Buffer.isEncoding(encoding);
162
- }
163
159
  //#endregion
164
160
  //#region src/server/ProcessCapture.ts
165
161
  /**
166
- * An observable interceptor of the RAW process output streams (AGENTS §13) — it takes control of
167
- * `process.stdout.write` / `process.stderr.write` on the WRITE side. While `active`, every write to
162
+ * Implements an observable interceptor of the raw process output streams — it takes control of
163
+ * `process.stdout.write` / `process.stderr.write` on the write side. While `active`, every write to
168
164
  * a configured {@link StreamLevel} is captured as a frozen {@link CapturedChunk}, buffered (total +
169
- * per-stream, bounded), emitted on `capture`, and — per options — mirrored to the real stream and/or
170
- * forwarded to a {@link SinkInterface}.
165
+ * per-stream, bounded), emitted on `capture`, and — per options — mirrored to the real stream,
166
+ * forwarded to a {@link SinkInterface}, or both.
171
167
  *
172
168
  * @remarks
173
169
  * Where the core `Capture` patches `console.*` (the high-level read side), this patches the
174
- * low-level stream `write`, so it owns ALL server output: a direct `process.stdout.write`, a
170
+ * low-level stream `write`, so it owns all server output: a direct `process.stdout.write`, a
175
171
  * third-party library's writes, a child-process pipe — not only `console.*`.
176
172
  *
177
- * - **Snapshot-at-start (the no-capture-loop principle).** `start()` snapshots the CURRENT
173
+ * - **Snapshot-at-start (the no-capture-loop principle).** `start()` snapshots the current
178
174
  * `process[stream].write` for each configured level, then installs the wrappers. The mirror
179
175
  * replays through that snapshot (bound to its stream) — so a server sink created from the same
180
- * streams BEFORE the capture is never re-captured: this catches OTHER writers, not the mirror's
176
+ * streams before the capture is never re-captured: this catches other writers, not the mirror's
181
177
  * own replay. Create your sinks before installing a capture.
182
- * - **Idempotent + PROCESS-GLOBAL + NON-REENTRANT.** `start()` while `active` is a no-op (never
178
+ * - **Idempotent + process-global + non-reentrant.** `start()` while `active` is a no-op (never
183
179
  * double-patches — that would snapshot the wrapper as the "original" and break restore); `stop()`
184
- * while inactive is a no-op. It patches the ONE global `process`, so at most ONE process capture
180
+ * while inactive is a no-op. It patches the one global `process`, so at most one process capture
185
181
  * may be active at a time — two concurrently would interleave buffers and clobber each other's
186
182
  * restore.
187
- * - **The wrapper NEVER throws and passes backpressure through.** A throw inside
183
+ * - **The wrapper never throws and passes backpressure through.** A throw inside
188
184
  * `process.stdout.write` would crash the host, so the wrapper decodes each chunk totally (a byte
189
185
  * chunk through the per-level streaming decoder below, everything else through the total
190
186
  * {@link decodeChunk}), and returns the snapshot-original's `boolean` when mirroring (so a caller's
@@ -195,18 +191,18 @@ function isBufferEncoding(encoding) {
195
191
  * child-process pipe, a library, or OS buffering all produce this — carries its partial bytes to
196
192
  * the next write instead of decoding each half to `U+FFFD`. `stop()` flushes each decoder once, so
197
193
  * a codepoint left half-written at stop is still surfaced. A `string` chunk is already text and
198
- * passes through; an explicit NON-utf-8 buffer encoding (`latin1` / `hex` / `base64` / …) names a
194
+ * passes through; an explicit non-utf-8 buffer encoding (`latin1` / `hex` / `base64` / …) names a
199
195
  * self-contained per-write decode and is honored one-shot through {@link decodeChunk}.
200
196
  * - **Bounded buffers.** The total buffer and each per-stream bucket are each capped at `limit`
201
197
  * (oldest dropped first), never unbounded — the same retention precedent as the core `Capture`.
202
- * - **Lifecycle (§10).** `start` / `stop` toggle interception (emitting `start` / `stop`);
203
- * `destroy()` stops (restoring the PRISTINE `write`) then destroys the emitter.
198
+ * - **Lifecycle.** `start` / `stop` toggle interception (emitting `start` / `stop`);
199
+ * `destroy()` stops (restoring the pristine `write`) then destroys the emitter.
204
200
  *
205
201
  * @example
206
202
  * ```ts
207
203
  * const capture = new ProcessCapture({ levels: ['stderr'], mirror: true })
208
204
  * capture.start()
209
- * process.stderr.write('a library diagnostic\n') // captured AND still written to the terminal
205
+ * process.stderr.write('a library diagnostic\n') // captured and still written to the terminal
210
206
  * capture.messages('stderr') // [{ level: 'stderr', text: 'a library diagnostic\n', time: … }]
211
207
  * capture.stop() // process.stderr.write restored
212
208
  * ```
@@ -216,9 +212,7 @@ var ProcessCapture = class {
216
212
  #levels;
217
213
  #mirror;
218
214
  #sink;
219
- #limit;
220
- #messages = [];
221
- #buckets = /* @__PURE__ */ new Map();
215
+ #retention;
222
216
  #originals = /* @__PURE__ */ new Map();
223
217
  #decoders = /* @__PURE__ */ new Map();
224
218
  #active = false;
@@ -227,11 +221,10 @@ var ProcessCapture = class {
227
221
  ...options?.on !== void 0 ? { on: options.on } : {},
228
222
  ...options?.error !== void 0 ? { error: options.error } : {}
229
223
  });
230
- this.#levels = options?.levels ?? DEFAULT_CAPTURE_LEVELS;
224
+ this.#levels = options?.levels ?? STREAM_LEVELS;
231
225
  this.#mirror = options?.mirror ?? false;
232
226
  this.#sink = options?.sink;
233
- this.#limit = options?.limit ?? 1e3;
234
- for (const level of this.#levels) this.#buckets.set(level, []);
227
+ this.#retention = new Retention(this.#levels, options?.limit ?? 1e3);
235
228
  }
236
229
  get emitter() {
237
230
  return this.#emitter;
@@ -261,12 +254,11 @@ var ProcessCapture = class {
261
254
  this.#emitter.emit("stop");
262
255
  }
263
256
  messages(level) {
264
- if (level === void 0) return [...this.#messages];
265
- return [...this.#buckets.get(level) ?? []];
257
+ if (level === void 0) return this.#retention.records();
258
+ return this.#retention.records(level);
266
259
  }
267
260
  clear() {
268
- this.#messages.length = 0;
269
- for (const bucket of this.#buckets.values()) bucket.length = 0;
261
+ this.#retention.clear();
270
262
  }
271
263
  destroy() {
272
264
  this.stop();
@@ -305,7 +297,7 @@ var ProcessCapture = class {
305
297
  text,
306
298
  time: Date.now()
307
299
  });
308
- this.#retain(message);
300
+ this.#retention.add(message);
309
301
  this.#emitter.emit("capture", message);
310
302
  if (this.#sink !== void 0) try {
311
303
  this.#sink.write(message.text, STREAM_LEVEL_MAP[level]);
@@ -318,20 +310,11 @@ var ProcessCapture = class {
318
310
  }
319
311
  this.#decoders.clear();
320
312
  }
321
- #retain(message) {
322
- this.#push(this.#messages, message);
323
- const bucket = this.#buckets.get(message.level);
324
- if (bucket !== void 0) this.#push(bucket, message);
325
- }
326
- #push(buffer, message) {
327
- buffer.push(message);
328
- if (buffer.length > this.#limit) buffer.shift();
329
- }
330
313
  };
331
314
  //#endregion
332
315
  //#region src/server/factories.ts
333
316
  /**
334
- * Create the server TTY {@link ServerSinkInterface} — the C-g server output backend, the
317
+ * Creates the server TTY {@link ServerSinkInterface} — the server output backend, the
335
318
  * env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`
336
319
  * routes by level to the process streams and uses construction-time styled facts: it sends ANSI
337
320
  * straight to a styled target (with a leading `\r` overwriting a terminal line natively) but
@@ -343,37 +326,39 @@ var ProcessCapture = class {
343
326
  *
344
327
  * @remarks
345
328
  * - **Routes by level.** `error` / `warn` → the error stream (`process.stderr` by default), every
346
- * other level (and an omitted level) → the out stream (`process.stdout`) — the SAME routing as
347
- * core's `createConsoleSink`, so a logger's `error` reaches `stderr`.
329
+ * other level (and an omitted level) → the `stdout` stream (`process.stdout`) — the same routing
330
+ * as core's `createConsoleSink`, so a logger's `error` reaches `stderr`. Both call the one
331
+ * {@link import('@src/core').selectWriter} leaf, which is what keeps them identical.
348
332
  * - **Per-target styled facts.** At construction, each target uses `options.styled` when supplied;
349
333
  * otherwise {@link inferStyled} applies the injected `environment` (default `process.env`) and
350
334
  * then that target's `isTTY`.
351
- * Writes use those stored facts, so `styled` and the out target's strip decision never disagree;
352
- * the err target keeps its own fact internally.
353
- * - **Width.** `columns` reflects the live `out.columns` (so it tracks a terminal resize), falling
354
- * back to {@link import('./constants.js').DEFAULT_COLUMNS} when the out stream is not a TTY — or a
355
- * fixed value when `options.columns` is supplied. Feed it to a `Reporter` / `Progress` `width`.
356
- * - **Injectable + guard-narrowed.** `options.out` / `options.err` default to `process.stdout` /
357
- * `process.stderr` but accept ANY {@link import('./types.js').StreamTargetInterface}, resolved
358
- * through {@link isStreamTarget} (AGENTS §14 — narrow the boundary, never `as`), so a test drives
335
+ * Writes use those stored facts, so `styled` and the `stdout` target's strip decision never
336
+ * disagree; the `stderr` target keeps its own fact internally.
337
+ * - **Width.** `columns` reflects the live `stdout.columns` (so it tracks a terminal resize),
338
+ * falling back to {@link import('./constants.js').DEFAULT_COLUMNS} when the `stdout` stream is not
339
+ * a TTY — or a fixed value when `options.columns` is supplied. Feed it to a `Reporter` /
340
+ * `Progress` `width`.
341
+ * - **Injectable + guard-narrowed.** `options.stdout` / `options.stderr` default to `process.stdout`
342
+ * / `process.stderr` but accept any {@link import('./types.js').StreamTargetInterface}, resolved
343
+ * through {@link isStreamTarget} (narrow the boundary, never `as`), so a test drives
359
344
  * the sink (and the isTTY-strip path) with a fake stream that never touches the real process
360
345
  * streams.
361
346
  *
362
347
  * @example
363
348
  * ```ts
364
- * import { createLogger, createReporter, createStyler } from '@src/core'
365
- * import { createServerSink } from '@src/server'
349
+ * import { createStyler, Logger, Reporter } from '@orkestrel/console'
350
+ * import { createServerSink } from '@orkestrel/console/server'
366
351
  *
367
352
  * const sink = createServerSink()
368
353
  * const styler = createStyler({ enabled: sink.styled })
369
- * const logger = createLogger({ name: 'app', sink, styler })
354
+ * const logger = new Logger({ name: 'app', sink, styler })
370
355
  * logger.error('boom') // → process.stderr, ANSI rendered on a TTY / stripped to a pipe
371
- * const reporter = createReporter({ sink, width: sink.columns })
356
+ * const reporter = new Reporter({ sink, width: sink.columns })
372
357
  * ```
373
358
  */
374
359
  function createServerSink(options) {
375
- const out = isStreamTarget(options?.out) ? options.out : process.stdout;
376
- const err = isStreamTarget(options?.err) ? options.err : process.stderr;
360
+ const out = isStreamTarget(options?.stdout) ? options.stdout : process.stdout;
361
+ const err = isStreamTarget(options?.stderr) ? options.stderr : process.stderr;
377
362
  const styled = options?.styled;
378
363
  const environment = options?.environment ?? process.env;
379
364
  const outStyled = styled ?? inferStyled(out, environment);
@@ -382,49 +367,25 @@ function createServerSink(options) {
382
367
  return Object.freeze({
383
368
  styled: outStyled,
384
369
  write(text, level) {
385
- const error = level === "error" || level === "warn";
386
- const target = error ? err : out;
387
- const keep = error ? errStyled : outStyled;
370
+ const target = selectWriter(level, {
371
+ log: out,
372
+ warn: err,
373
+ error: err
374
+ });
375
+ const keep = selectWriter(level, {
376
+ log: outStyled,
377
+ warn: errStyled,
378
+ error: errStyled
379
+ });
388
380
  const line = text.startsWith("\r") ? text : `${text}\n`;
389
381
  target.write(keep ? line : stripControls(strip(line)));
390
382
  },
391
383
  get columns() {
392
- return typeof fixed === "number" ? fixed : columnsOf(out);
384
+ return typeof fixed === "number" ? fixed : inferColumns(out);
393
385
  }
394
386
  });
395
387
  }
396
- /**
397
- * Create an observable {@link ProcessCaptureInterface} — the server "own ALL output" capture. It
398
- * intercepts the RAW `process.stdout.write` / `process.stderr.write` (not just `console.*`, which is
399
- * the core `Capture`), so it catches direct `process` writes, library output, and child-process
400
- * pipes. Each intercepted write becomes a frozen {@link import('./types.js').CapturedChunk},
401
- * buffered (bounded, per-stream) and emitted on `capture`; per options it is mirrored back to the
402
- * real stream and/or forwarded to a {@link import('@src/core').SinkInterface}.
403
- *
404
- * @param options - See {@link ProcessCaptureOptions}
405
- * @returns A {@link ProcessCaptureInterface}
406
- *
407
- * @remarks
408
- * - **The wrapper never throws and passes backpressure through** — a throw in `process.stdout.write`
409
- * would crash the host, so chunks are decoded totally and the original's `boolean` is returned.
410
- * - **Snapshot-at-start + non-reentrant + process-global** — `start()` snapshots and swaps the
411
- * pristine `write`; `stop()` restores the EXACT original. At most ONE may be active at a time.
412
- * Create any server sink BEFORE installing a capture so the mirror's replay is not re-captured.
413
- *
414
- * @example
415
- * ```ts
416
- * import { createProcessCapture } from '@src/server'
417
- *
418
- * const capture = createProcessCapture({ levels: ['stderr'], mirror: true })
419
- * capture.start()
420
- * process.stderr.write('a library diagnostic\n') // captured AND still shown
421
- * capture.stop()
422
- * ```
423
- */
424
- function createProcessCapture(options) {
425
- return new ProcessCapture(options);
426
- }
427
388
  //#endregion
428
- export { DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, DEFAULT_COLUMNS, ProcessCapture, STREAM_LEVELS, STREAM_LEVEL_MAP, columnsOf, createProcessCapture, createServerSink, decodeChunk, inferStyled, isBufferEncoding, isStreamTarget };
389
+ export { DEFAULT_COLUMNS, DEFAULT_STREAM_LIMIT, ProcessCapture, STREAM_LEVELS, STREAM_LEVEL_MAP, createServerSink, decodeChunk, inferColumns, inferStyled, isBufferEncoding, isStreamTarget };
429
390
 
430
391
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#emitter","#levels","#mirror","#sink","#limit","#messages","#buckets","#originals","#decoders","#active","#stream","#captureWrite","#flush","#intercept","#record","#decode","#streams","#retain","#push"],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/ProcessCapture.ts","../../../src/server/factories.ts"],"sourcesContent":["// Server-console constants (the C-g branch) — UPPER_SNAKE, `Object.freeze`d data. The kind-pure home\n// for every module-scope constant the sink + process capture use (AGENTS §5): the default stream\n// set, the buffer cap, the no-TTY column fallback, and the stream→log-level projection.\n\nimport type { LogLevel } from '@src/core'\nimport type { StreamLevel } from './types.js'\n\n/**\n * The two process streams a {@link import('./types.js').ProcessCaptureInterface} can intercept, in\n * `stdout`-then-`stderr` order — the {@link StreamLevel} universe and the default configured set.\n */\nexport const STREAM_LEVELS: readonly StreamLevel[] = Object.freeze(['stdout', 'stderr'])\n\n/**\n * The default set of {@link StreamLevel}s a process capture patches when `options.levels` is omitted\n * — BOTH streams ({@link STREAM_LEVELS}). A consumer narrows it (e.g. just `['stderr']`) via\n * `options.levels`.\n */\nexport const DEFAULT_CAPTURE_LEVELS: readonly StreamLevel[] = STREAM_LEVELS\n\n/**\n * The default bounded-buffer cap for a {@link import('./types.js').ProcessCaptureInterface} — at\n * most this many recent {@link import('./types.js').CapturedChunk}s are retained per buffer (the\n * total buffer AND each per-stream bucket; oldest dropped first). Mirrors the core `Capture`'s\n * `DEFAULT_CAPTURE_LIMIT`; a consumer overrides it via `options.limit`.\n */\nexport const DEFAULT_CAPTURE_LIMIT = 1000\n\n/**\n * The terminal width {@link import('./factories.js').createServerSink} reports through\n * {@link import('./types.js').ServerSinkInterface.columns} when the out stream is NOT a TTY (so\n * `.columns` is `undefined`) and no explicit `options.columns` was supplied — the conventional\n * 80-column default a non-interactive context (a pipe, a CI log) assumes.\n */\nexport const DEFAULT_COLUMNS = 80\n\n/**\n * Each {@link StreamLevel}'s {@link LogLevel} for the optional sink forward — the projection a\n * process capture routes through when writing an intercepted chunk to a\n * {@link import('@src/core').SinkInterface}\n * (`sink.write(text, STREAM_LEVEL_MAP[level])`). `stderr` is conventionally the error/diagnostic\n * stream → `error`; `stdout` is the normal output stream → `info`. The source of truth for the\n * stream-to-log projection (the server analogue of the core `CAPTURE_LEVEL_MAP`).\n */\nexport const STREAM_LEVEL_MAP: Readonly<Record<StreamLevel, LogLevel>> = Object.freeze({\n\tstdout: 'info',\n\tstderr: 'error',\n})\n","// Pure helpers for the C-g server-console branch (AGENTS §5 — every function here is exported and\n// unit-tested). Total utilities: the stream-target boundary guard (narrow `process.stdout` / any\n// injected target without `as`, §14), the TTY column probe, and the total chunk→text decoder (with\n// its encoding guard) the process-capture wrapper reuses so intercepting `process.*.write` can never\n// throw (§14), plus pure color-environment inference for the server sink.\n\nimport type { StreamTargetInterface } from './types.js'\nimport { DEFAULT_COLUMNS } from './constants.js'\n\n/**\n * Whether `value` is a usable {@link StreamTargetInterface} — a record with a callable `write`. A\n * total type guard (AGENTS §14): it NEVER throws and returns `false` for anything off-shape, so it\n * narrows the one unavoidable boundary (the real `process.stdout` / `process.stderr`, or a fake\n * stream a test injects) to the exact slice the sink + capture touch — no `as`.\n *\n * @remarks\n * Only `write` is required (the irreducible output method); `isTTY` and `columns` are optional on\n * {@link StreamTargetInterface}, so their absence does not disqualify a target — a piped stream\n * (no `isTTY`) is still a valid write target, just a non-terminal one.\n *\n * @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)\n * @returns `true` when `value` has a callable `write`\n *\n * @example\n * ```ts\n * isStreamTarget(process.stdout) // true\n * isStreamTarget({ write: () => true }) // true\n * isStreamTarget({}) // false (no write)\n * ```\n */\nexport function isStreamTarget(value: unknown): value is StreamTargetInterface {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t'write' in value &&\n\t\ttypeof value.write === 'function'\n\t)\n}\n\n/**\n * The width in character cells of a stream target — its live `columns` when it is a TTY, else the\n * non-interactive {@link DEFAULT_COLUMNS} fallback. The basis a {@link import('./types.js').ServerSinkInterface}\n * reports through `columns` so a `Reporter` / `Progress` can size its layout to the terminal.\n *\n * @remarks\n * Reads `target.columns` ON EACH CALL (so a getter-backed real stream reflects a live resize) and\n * accepts it only when it is a positive finite number; a missing / `0` / non-finite `columns` (a\n * piped, non-TTY stream) falls back to {@link DEFAULT_COLUMNS}. Total — never throws.\n *\n * @param target - The stream whose width to probe\n * @returns The terminal column count, or {@link DEFAULT_COLUMNS} when not a TTY\n */\nexport function columnsOf(target: StreamTargetInterface): number {\n\tconst columns = target.columns\n\tif (typeof columns === 'number' && Number.isFinite(columns) && columns > 0) return columns\n\treturn DEFAULT_COLUMNS\n}\n\n/**\n * Infer whether one stream target should receive styled output. The result is a construction-time\n * target fact for {@link import('./factories.js').createServerSink}; this helper is pure and never\n * reads process globals itself.\n *\n * @remarks\n * A present `FORCE_COLOR` key has first precedence: only the exact value `'0'` disables styling.\n * Next, a non-empty `NO_COLOR` disables styling. Otherwise styling follows\n * `target.isTTY === true`.\n *\n * @param target - The stream target whose terminal capability is the fallback\n * @param environment - The environment record supplying `FORCE_COLOR` and `NO_COLOR`\n * @returns Whether output for the target should retain styling and control sequences\n *\n * @example\n * ```ts\n * inferStyled({ write: () => true, isTTY: false }, { FORCE_COLOR: '1' }) // true\n * inferStyled({ write: () => true, isTTY: true }, { NO_COLOR: '1' }) // false\n * ```\n */\nexport function inferStyled(\n\ttarget: StreamTargetInterface,\n\tenvironment: Readonly<Record<string, string | undefined>>,\n): boolean {\n\tif (Object.hasOwn(environment, 'FORCE_COLOR')) return environment.FORCE_COLOR !== '0'\n\tconst disabled = environment.NO_COLOR\n\tif (disabled !== undefined && disabled !== '') return false\n\treturn target.isTTY === true\n}\n\n/**\n * Decode one `process.stdout.write` / `process.stderr.write` chunk to a string — TOTAL, never\n * throws (AGENTS §14). The process write signature accepts `string | Uint8Array` plus an optional\n * encoding; the capture wrapper reuses this so intercepting a raw stream write can never crash the\n * host (a throw inside `process.stdout.write` would take the program down).\n *\n * @remarks\n * - A `string` chunk is returned verbatim — the common case (`console.log`, most library output,\n * and `process.stdout.write('text')` all pass a string).\n * - A `Buffer` chunk is decoded with the supplied `encoding` when it is a recognized\n * {@link BufferEncoding} (`process` write supports `'utf8'` / `'hex'` / `'base64'` / …), defaulting\n * to `'utf8'`; a bare `Uint8Array` is decoded via `TextDecoder` (always utf-8 — the `encoding`\n * argument applies ONLY to a `Buffer`, never a plain `Uint8Array`).\n * - Anything else is coerced with `String(chunk)` (a number / object / bigint / symbol a misbehaving\n * writer hands the stream). The coercion is itself guarded: a value whose `toString` /\n * `Symbol.toPrimitive` throws yields the stable `'[unprintable]'` placeholder. So the helper is\n * TOTAL on every input — it always yields SOME string, never an exception (a throw here would\n * escape into `process.*.write` and crash the host).\n *\n * @param chunk - The chunk passed to the stream's `write`\n * @param encoding - The optional encoding argument passed alongside the chunk\n * @returns The chunk as text\n *\n * @example\n * ```ts\n * decodeChunk('hi') // 'hi'\n * decodeChunk(Buffer.from('hi')) // 'hi'\n * decodeChunk(new Uint8Array([104, 105])) // 'hi'\n * ```\n */\nexport function decodeChunk(chunk: unknown, encoding?: unknown): string {\n\tif (typeof chunk === 'string') return chunk\n\ttry {\n\t\tif (Buffer.isBuffer(chunk)) {\n\t\t\treturn chunk.toString(isBufferEncoding(encoding) ? encoding : 'utf8')\n\t\t}\n\t\tif (chunk instanceof Uint8Array) return new TextDecoder().decode(chunk)\n\t\t// The String() coercion is inside the try too: a value with a hostile `toString` /\n\t\t// `Symbol.toPrimitive` would otherwise throw HERE and escape into `process.*.write`, crashing\n\t\t// the host — the exact failure this total decoder exists to prevent (§14). Guard it.\n\t\treturn String(chunk)\n\t} catch {\n\t\t// Any decode / coercion failure yields a stable placeholder — the helper is total on EVERY\n\t\t// input (the kind a misbehaving writer could hand the patched stream), never an exception.\n\t\treturn '[unprintable]'\n\t}\n}\n\n/**\n * Whether `encoding` is a {@link BufferEncoding} accepted by `Buffer.prototype.toString` — a total\n * guard used by {@link decodeChunk} to honor a process-write `encoding` argument only when it is a\n * real Node encoding (otherwise utf-8 is assumed).\n *\n * @param encoding - The candidate encoding (the second `write` argument, possibly a callback)\n * @returns `true` when `encoding` names a supported buffer encoding\n */\nexport function isBufferEncoding(encoding: unknown): encoding is BufferEncoding {\n\treturn typeof encoding === 'string' && Buffer.isEncoding(encoding)\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type {\n\tCapturedChunk,\n\tProcessCaptureEventMap,\n\tProcessCaptureInterface,\n\tProcessCaptureOptions,\n\tStreamLevel,\n\tStreamWriteCallback,\n\tStreamWriteFunction,\n} from './types.js'\nimport type { SinkInterface } from '@src/core'\nimport { StringDecoder } from 'node:string_decoder'\nimport { Emitter } from '@orkestrel/emitter'\nimport { DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, STREAM_LEVEL_MAP } from './constants.js'\nimport { decodeChunk, isBufferEncoding } from './helpers.js'\n\n/**\n * An observable interceptor of the RAW process output streams (AGENTS §13) — it takes control of\n * `process.stdout.write` / `process.stderr.write` on the WRITE side. While `active`, every write to\n * a configured {@link StreamLevel} is captured as a frozen {@link CapturedChunk}, buffered (total +\n * per-stream, bounded), emitted on `capture`, and — per options — mirrored to the real stream and/or\n * forwarded to a {@link SinkInterface}.\n *\n * @remarks\n * Where the core `Capture` patches `console.*` (the high-level read side), this patches the\n * low-level stream `write`, so it owns ALL server output: a direct `process.stdout.write`, a\n * third-party library's writes, a child-process pipe — not only `console.*`.\n *\n * - **Snapshot-at-start (the no-capture-loop principle).** `start()` snapshots the CURRENT\n * `process[stream].write` for each configured level, then installs the wrappers. The mirror\n * replays through that snapshot (bound to its stream) — so a server sink created from the same\n * streams BEFORE the capture is never re-captured: this catches OTHER writers, not the mirror's\n * own replay. Create your sinks before installing a capture.\n * - **Idempotent + PROCESS-GLOBAL + NON-REENTRANT.** `start()` while `active` is a no-op (never\n * double-patches — that would snapshot the wrapper as the \"original\" and break restore); `stop()`\n * while inactive is a no-op. It patches the ONE global `process`, so at most ONE process capture\n * may be active at a time — two concurrently would interleave buffers and clobber each other's\n * restore.\n * - **The wrapper NEVER throws and passes backpressure through.** A throw inside\n * `process.stdout.write` would crash the host, so the wrapper decodes each chunk totally (a byte\n * chunk through the per-level streaming decoder below, everything else through the total\n * {@link decodeChunk}), and returns the snapshot-original's `boolean` when mirroring (so a caller's\n * `write` backpressure handling still works) or `true` when capture-only (the buffer never fills).\n * - **Streaming UTF-8 decode (no split-codepoint corruption).** `start()` gives each configured\n * {@link StreamLevel} a fresh persistent `StringDecoder`. A byte chunk with utf-8 or an omitted\n * encoding decodes through it, so a multibyte codepoint split across two `write` byte chunks — a\n * child-process pipe, a library, or OS buffering all produce this — carries its partial bytes to\n * the next write instead of decoding each half to `U+FFFD`. `stop()` flushes each decoder once, so\n * a codepoint left half-written at stop is still surfaced. A `string` chunk is already text and\n * passes through; an explicit NON-utf-8 buffer encoding (`latin1` / `hex` / `base64` / …) names a\n * self-contained per-write decode and is honored one-shot through {@link decodeChunk}.\n * - **Bounded buffers.** The total buffer and each per-stream bucket are each capped at `limit`\n * (oldest dropped first), never unbounded — the same retention precedent as the core `Capture`.\n * - **Lifecycle (§10).** `start` / `stop` toggle interception (emitting `start` / `stop`);\n * `destroy()` stops (restoring the PRISTINE `write`) then destroys the emitter.\n *\n * @example\n * ```ts\n * const capture = new ProcessCapture({ levels: ['stderr'], mirror: true })\n * capture.start()\n * process.stderr.write('a library diagnostic\\n') // captured AND still written to the terminal\n * capture.messages('stderr') // [{ level: 'stderr', text: 'a library diagnostic\\n', time: … }]\n * capture.stop() // process.stderr.write restored\n * ```\n */\nexport class ProcessCapture implements ProcessCaptureInterface {\n\t// The PUSH observation surface (§13) — owned, never inherited. The emitter isolates a listener\n\t// throw (routing it to the `error` handler), so a buggy `capture` listener can never escape into\n\t// the host program's `process.*.write` call.\n\treadonly #emitter: Emitter<ProcessCaptureEventMap>\n\treadonly #levels: readonly StreamLevel[]\n\treadonly #mirror: boolean\n\treadonly #sink: SinkInterface | undefined\n\treadonly #limit: number\n\t// The bounded total buffer — every captured chunk, oldest first, capped at #limit.\n\treadonly #messages: CapturedChunk[] = []\n\t// The bounded per-stream buckets — one capped buffer per configured StreamLevel.\n\treadonly #buckets = new Map<StreamLevel, CapturedChunk[]>()\n\t// The snapshot-original `write` references, captured at start() and restored at stop(); empty\n\t// while inactive. The presence of an entry is what `active` reads.\n\treadonly #originals = new Map<StreamLevel, StreamWriteFunction>()\n\t// One PERSISTENT streaming utf-8 decoder per configured level, created fresh in start() and\n\t// flushed + cleared in stop(). It carries a multibyte codepoint split across successive byte\n\t// writes so each half is not decoded to U+FFFD; empty while inactive.\n\treadonly #decoders = new Map<StreamLevel, StringDecoder>()\n\t#active = false\n\n\tconstructor(options?: ProcessCaptureOptions) {\n\t\tthis.#emitter = new Emitter<ProcessCaptureEventMap>({\n\t\t\t...(options?.on !== undefined ? { on: options.on } : {}),\n\t\t\t...(options?.error !== undefined ? { error: options.error } : {}),\n\t\t})\n\t\tthis.#levels = options?.levels ?? DEFAULT_CAPTURE_LEVELS\n\t\tthis.#mirror = options?.mirror ?? false\n\t\tthis.#sink = options?.sink\n\t\tthis.#limit = options?.limit ?? DEFAULT_CAPTURE_LIMIT\n\t\tfor (const level of this.#levels) this.#buckets.set(level, [])\n\t}\n\n\tget emitter(): EmitterInterface<ProcessCaptureEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget active(): boolean {\n\t\treturn this.#active\n\t}\n\n\tstart(): void {\n\t\t// Idempotent — never double-patch an already-active capture (that would snapshot the wrappers\n\t\t// as the \"originals\" and break restore).\n\t\tif (this.#active) return\n\t\tthis.#active = true\n\t\tfor (const level of this.#levels) {\n\t\t\tconst stream = this.#stream(level)\n\t\t\t// Snapshot the CURRENT write reference BEFORE replacing it — stop() restores EXACTLY this\n\t\t\t// reference, leaving the stream pristine (the wrapper is never snapshotted as the original).\n\t\t\tconst original = stream.write\n\t\t\tthis.#originals.set(level, original)\n\t\t\t// The mirror target is the snapshot original BOUND to its stream, computed once here — so a\n\t\t\t// mirrored write reaches the real method with its proper receiver, through the snapshot and\n\t\t\t// never the live (patched) `write` (no capture loop). The restore reference stays the pristine\n\t\t\t// unbound `original` above; only the mirror uses the bound form.\n\t\t\tconst mirror = original.bind(stream)\n\t\t\t// The replacement matches the Node `write` overload shape exactly — `(chunk, encoding?, cb?)`\n\t\t\t// where the 2nd arg is either a `BufferEncoding` or the completion callback — so it assigns to\n\t\t\t// the stream's `write` slot AND its args forward cleanly to `mirror` (no `as`, no untyped\n\t\t\t// spread).\n\t\t\tstream.write = this.#captureWrite.bind(this, level, mirror)\n\t\t\t// A fresh streaming decoder per cycle — a stop → start pair starts clean, never carrying a\n\t\t\t// stale partial byte from a prior capture into the new one.\n\t\t\tthis.#decoders.set(level, new StringDecoder('utf8'))\n\t\t}\n\t\tthis.#emitter.emit('start')\n\t}\n\n\tstop(): void {\n\t\t// Safe when not active — nothing to restore.\n\t\tif (!this.#active) return\n\t\tthis.#active = false\n\t\tfor (const [level, original] of this.#originals) this.#stream(level).write = original\n\t\tthis.#originals.clear()\n\t\t// Drain any trailing partial codepoint from each streaming decoder BEFORE the `stop` signal, so\n\t\t// a codepoint left half-written at stop is captured once rather than dropped.\n\t\tthis.#flush()\n\t\tthis.#emitter.emit('stop')\n\t}\n\n\tmessages(): readonly CapturedChunk[]\n\tmessages(level: StreamLevel): readonly CapturedChunk[]\n\tmessages(level?: StreamLevel): readonly CapturedChunk[] {\n\t\tif (level === undefined) return [...this.#messages]\n\t\treturn [...(this.#buckets.get(level) ?? [])]\n\t}\n\n\tclear(): void {\n\t\tthis.#messages.length = 0\n\t\tfor (const bucket of this.#buckets.values()) bucket.length = 0\n\t}\n\n\tdestroy(): void {\n\t\tthis.stop()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// The global WriteStream for a StreamLevel — `process[level]` indexes it directly, since a\n\t// StreamLevel IS the `process` property key (`'stdout'` / `'stderr'`); no `as`, no lookup map.\n\t#stream(level: StreamLevel): NodeJS.WriteStream {\n\t\treturn process[level]\n\t}\n\n\t// Adapt the patched stream's write signature to #intercept. Binding level and the pristine\n\t// mirror in start() leaves the canonical chunk / encoding / callback parameters.\n\t#captureWrite(\n\t\tlevel: StreamLevel,\n\t\tmirror: StreamWriteFunction,\n\t\tchunk: string | Uint8Array,\n\t\tencoding?: BufferEncoding | StreamWriteCallback,\n\t\tcallback?: StreamWriteCallback,\n\t): boolean {\n\t\treturn this.#intercept(level, chunk, encoding, callback, mirror)\n\t}\n\n\t// The wrapper body behind every patched stream write: decode the chunk to text (#decode — total,\n\t// streaming for byte chunks), record it (buffer bounded, emit `capture`, forward to the sink),\n\t// then — per options — mirror to the real stream. NEVER throws (#decode is total; the emitter\n\t// isolates listeners); the program's own write is replayed through `mirror` (the bound snapshot\n\t// original) only when the `mirror` option is set, and the original's backpressure boolean is\n\t// returned. Capture-only returns `true` (output is swallowed into the buffer, so the kernel buffer\n\t// never fills). The RAW chunk (not the decoded text) is what mirrors, so the terminal still\n\t// receives the exact bytes; the `encoding` / `callback` tail is forwarded to the mirror BRANCHED\n\t// on whether the 2nd arg is the callback or an encoding (the two Node overloads), so a caller's\n\t// completion callback fires.\n\t#intercept(\n\t\tlevel: StreamLevel,\n\t\tchunk: string | Uint8Array,\n\t\tencoding: BufferEncoding | StreamWriteCallback | undefined,\n\t\tcallback: StreamWriteCallback | undefined,\n\t\tmirror: StreamWriteFunction,\n\t): boolean {\n\t\tthis.#record(level, this.#decode(level, chunk, encoding))\n\t\tif (!this.#mirror) {\n\t\t\t// Capture-only: the write never reaches the real stream, so fire the caller's completion\n\t\t\t// callback asynchronously (matching Node's own async completion semantics) rather than\n\t\t\t// silently dropping it — both call shapes (`write(chunk, cb)` and `write(chunk, encoding, cb)`)\n\t\t\t// are covered.\n\t\t\tconst done = typeof encoding === 'function' ? encoding : callback\n\t\t\tif (done !== undefined) queueMicrotask(() => done())\n\t\t\treturn true\n\t\t}\n\t\t// `write(chunk, cb)` when the 2nd arg is the callback; `write(chunk, encoding, cb)` otherwise —\n\t\t// matching the two Node overloads so the forward stays typed.\n\t\tif (typeof encoding === 'function') return mirror(chunk, encoding)\n\t\treturn mirror(chunk, encoding, callback)\n\t}\n\n\t// Decode one write chunk to text — TOTAL, never throws (a throw here would escape into the patched\n\t// process.*.write and crash the host). A `string` chunk is already text and passes through. A byte\n\t// chunk with utf-8 / an omitted encoding / a callback in the encoding slot streams through the\n\t// level's persistent decoder, carrying a codepoint split across writes; an explicit NON-utf-8\n\t// buffer encoding is self-contained per write and decoded one-shot through decodeChunk.\n\t#decode(\n\t\tlevel: StreamLevel,\n\t\tchunk: string | Uint8Array,\n\t\tencoding: BufferEncoding | StreamWriteCallback | undefined,\n\t): string {\n\t\tif (typeof chunk === 'string') return chunk\n\t\tconst decoder = this.#decoders.get(level)\n\t\tif (decoder !== undefined && this.#streams(encoding)) {\n\t\t\treturn decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))\n\t\t}\n\t\treturn decodeChunk(chunk, encoding)\n\t}\n\n\t// Whether a byte chunk with this encoding routes through the persistent streaming decoder rather\n\t// than the one-shot decodeChunk. TRUE for an omitted encoding, a callback in the slot, an\n\t// unrecognized string (all utf-8 by decodeChunk's own fallback), or an explicit utf-8; FALSE only\n\t// for a recognized NON-utf-8 buffer encoding, whose per-write bytes are self-contained.\n\t#streams(encoding: BufferEncoding | StreamWriteCallback | undefined): boolean {\n\t\tif (!isBufferEncoding(encoding)) return true\n\t\tconst normalized = encoding.toLowerCase()\n\t\treturn normalized === 'utf8' || normalized === 'utf-8'\n\t}\n\n\t// Build the immutable, serializable captured chunk from already-decoded text, stamp it with the\n\t// capture instant, buffer it (total + per-stream, bounded), emit `capture`, then forward it to the\n\t// sink. Frozen so a consumer (or the `capture` listener) can never mutate it. NEVER throws into the\n\t// patched stream — the sink is a best-effort tee and the emitter isolates a listener throw.\n\t#record(level: StreamLevel, text: string): void {\n\t\tconst message: CapturedChunk = Object.freeze({ level, text, time: Date.now() })\n\t\tthis.#retain(message)\n\t\tthis.#emitter.emit('capture', message)\n\t\tif (this.#sink !== undefined) {\n\t\t\ttry {\n\t\t\t\tthis.#sink.write(message.text, STREAM_LEVEL_MAP[level])\n\t\t\t} catch {\n\t\t\t\t// The sink is a best-effort tee; the wrapper NEVER throws into the patched global stream —\n\t\t\t\t// a broken/throwing sink must not crash the host's process.stdout / process.stderr write.\n\t\t\t}\n\t\t}\n\t}\n\n\t// Drain each streaming decoder's trailing partial codepoint ONCE — `decoder.end()` emits the\n\t// pending bytes' final text (U+FFFD for a genuinely truncated sequence) — so a codepoint left\n\t// half-written at stop is recorded, not silently dropped. An empty flush adds no record. Clears\n\t// the decoders so the next start() begins clean.\n\t#flush(): void {\n\t\tfor (const [level, decoder] of this.#decoders) {\n\t\t\tconst text = decoder.end()\n\t\t\tif (text !== '') this.#record(level, text)\n\t\t}\n\t\tthis.#decoders.clear()\n\t}\n\n\t// Push onto the total buffer and the stream's bucket, evicting the oldest of each when at\n\t// capacity — both stay capped at #limit, never growing without bound.\n\t#retain(message: CapturedChunk): void {\n\t\tthis.#push(this.#messages, message)\n\t\tconst bucket = this.#buckets.get(message.level)\n\t\tif (bucket !== undefined) this.#push(bucket, message)\n\t}\n\n\t// Bounded push — append, then drop the oldest while over the cap.\n\t#push(buffer: CapturedChunk[], message: CapturedChunk): void {\n\t\tbuffer.push(message)\n\t\tif (buffer.length > this.#limit) buffer.shift()\n\t}\n}\n","import type { LogLevel } from '@src/core'\nimport type {\n\tProcessCaptureInterface,\n\tProcessCaptureOptions,\n\tServerSinkInterface,\n\tServerSinkOptions,\n} from './types.js'\nimport { strip, stripControls } from '@src/core'\nimport { ProcessCapture } from './ProcessCapture.js'\nimport { columnsOf, inferStyled, isStreamTarget } from './helpers.js'\n\n/**\n * Create the server TTY {@link ServerSinkInterface} — the C-g server output backend, the\n * env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`\n * routes by level to the process streams and uses construction-time styled facts: it sends ANSI\n * straight to a styled target (with a leading `\\r` overwriting a terminal line natively) but\n * {@link import('@src/core').strip}s ANSI to clean text for a plain target.\n *\n * @param options - See {@link ServerSinkOptions}\n * @returns A {@link ServerSinkInterface} — a {@link import('@src/core').SinkInterface} that also\n * exposes the terminal `columns` width\n *\n * @remarks\n * - **Routes by level.** `error` / `warn` → the error stream (`process.stderr` by default), every\n * other level (and an omitted level) → the out stream (`process.stdout`) — the SAME routing as\n * core's `createConsoleSink`, so a logger's `error` reaches `stderr`.\n * - **Per-target styled facts.** At construction, each target uses `options.styled` when supplied;\n * otherwise {@link inferStyled} applies the injected `environment` (default `process.env`) and\n * then that target's `isTTY`.\n * Writes use those stored facts, so `styled` and the out target's strip decision never disagree;\n * the err target keeps its own fact internally.\n * - **Width.** `columns` reflects the live `out.columns` (so it tracks a terminal resize), falling\n * back to {@link import('./constants.js').DEFAULT_COLUMNS} when the out stream is not a TTY — or a\n * fixed value when `options.columns` is supplied. Feed it to a `Reporter` / `Progress` `width`.\n * - **Injectable + guard-narrowed.** `options.out` / `options.err` default to `process.stdout` /\n * `process.stderr` but accept ANY {@link import('./types.js').StreamTargetInterface}, resolved\n * through {@link isStreamTarget} (AGENTS §14 — narrow the boundary, never `as`), so a test drives\n * the sink (and the isTTY-strip path) with a fake stream that never touches the real process\n * streams.\n *\n * @example\n * ```ts\n * import { createLogger, createReporter, createStyler } from '@src/core'\n * import { createServerSink } from '@src/server'\n *\n * const sink = createServerSink()\n * const styler = createStyler({ enabled: sink.styled })\n * const logger = createLogger({ name: 'app', sink, styler })\n * logger.error('boom') // → process.stderr, ANSI rendered on a TTY / stripped to a pipe\n * const reporter = createReporter({ sink, width: sink.columns })\n * ```\n */\nexport function createServerSink(options?: ServerSinkOptions): ServerSinkInterface {\n\t// Resolve each target through the guard (§14): a present, well-shaped injected stream is used as\n\t// is; otherwise the real process stream — no `as`, and an `undefined` option falls through to the\n\t// default. `out` carries info/debug, `err` carries error/warn.\n\tconst out = isStreamTarget(options?.out) ? options.out : process.stdout\n\tconst err = isStreamTarget(options?.err) ? options.err : process.stderr\n\tconst styled = options?.styled\n\tconst environment = options?.environment ?? process.env\n\tconst outStyled = styled ?? inferStyled(out, environment)\n\tconst errStyled = styled ?? inferStyled(err, environment)\n\tconst fixed = options?.columns\n\treturn Object.freeze({\n\t\tstyled: outStyled,\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\tconst error = level === 'error' || level === 'warn'\n\t\t\tconst target = error ? err : out\n\t\t\tconst keep = error ? errStyled : outStyled\n\t\t\t// A leading `\\r` marks an in-place redraw frame (Spinner/Progress), which carries its own\n\t\t\t// line endings and is written verbatim; every other (line-oriented) write gets exactly one\n\t\t\t// trailing `\\n` appended here, matching `console.log`'s newline-terminated behavior.\n\t\t\tconst framed = text.startsWith('\\r')\n\t\t\tconst line = framed ? text : `${text}\\n`\n\t\t\t// A styled target receives the line verbatim; a plain target receives visible text only.\n\t\t\ttarget.write(keep ? line : stripControls(strip(line)))\n\t\t},\n\t\tget columns(): number {\n\t\t\t// A fixed override wins; otherwise the live out-stream width (tracks a resize), with the\n\t\t\t// non-TTY fallback inside columnsOf.\n\t\t\treturn typeof fixed === 'number' ? fixed : columnsOf(out)\n\t\t},\n\t})\n}\n\n/**\n * Create an observable {@link ProcessCaptureInterface} — the server \"own ALL output\" capture. It\n * intercepts the RAW `process.stdout.write` / `process.stderr.write` (not just `console.*`, which is\n * the core `Capture`), so it catches direct `process` writes, library output, and child-process\n * pipes. Each intercepted write becomes a frozen {@link import('./types.js').CapturedChunk},\n * buffered (bounded, per-stream) and emitted on `capture`; per options it is mirrored back to the\n * real stream and/or forwarded to a {@link import('@src/core').SinkInterface}.\n *\n * @param options - See {@link ProcessCaptureOptions}\n * @returns A {@link ProcessCaptureInterface}\n *\n * @remarks\n * - **The wrapper never throws and passes backpressure through** — a throw in `process.stdout.write`\n * would crash the host, so chunks are decoded totally and the original's `boolean` is returned.\n * - **Snapshot-at-start + non-reentrant + process-global** — `start()` snapshots and swaps the\n * pristine `write`; `stop()` restores the EXACT original. At most ONE may be active at a time.\n * Create any server sink BEFORE installing a capture so the mirror's replay is not re-captured.\n *\n * @example\n * ```ts\n * import { createProcessCapture } from '@src/server'\n *\n * const capture = createProcessCapture({ levels: ['stderr'], mirror: true })\n * capture.start()\n * process.stderr.write('a library diagnostic\\n') // captured AND still shown\n * capture.stop()\n * ```\n */\nexport function createProcessCapture(options?: ProcessCaptureOptions): ProcessCaptureInterface {\n\treturn new ProcessCapture(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/validators.ts","../../../src/server/helpers.ts","../../../src/server/ProcessCapture.ts","../../../src/server/factories.ts"],"sourcesContent":["// Server-console constants (the server branch) — UPPER_SNAKE, `Object.freeze`d data. The kind-pure home\n// for every module-scope constant the sink + process capture use: the default stream\n// set, the buffer cap, the no-TTY column fallback, and the stream→log-level projection.\n\nimport type { LogLevel } from '@src/core'\nimport type { StreamLevel } from './types.js'\n\n/**\n * Lists the two process streams a {@link import('./types.js').ProcessCaptureInterface} can intercept, in\n * `stdout`-then-`stderr` order — the {@link StreamLevel} universe and the default configured set.\n */\nexport const STREAM_LEVELS: readonly StreamLevel[] = Object.freeze(['stdout', 'stderr'])\n\n/**\n * Sets the default bounded-buffer cap for a {@link import('./types.js').ProcessCaptureInterface} — at\n * most this many recent {@link import('./types.js').CapturedChunk}s are retained per buffer (the\n * total buffer and each per-stream bucket; oldest dropped first). Mirrors the core `Capture`'s\n * `DEFAULT_CAPTURE_LIMIT`; a consumer overrides it through `options.limit`.\n */\nexport const DEFAULT_STREAM_LIMIT = 1000\n\n/**\n * Sets the terminal width {@link import('./factories.js').createServerSink} reports through\n * {@link import('./types.js').ServerSinkInterface.columns} when the `stdout` stream is not a TTY (so\n * `.columns` is `undefined`) and no explicit `options.columns` was supplied — the conventional\n * 80-column default a non-interactive context (a pipe, a CI log) assumes.\n */\nexport const DEFAULT_COLUMNS = 80\n\n/**\n * Maps each {@link StreamLevel} to its {@link LogLevel} for the optional sink forward — the projection a\n * process capture routes through when writing an intercepted chunk to a\n * {@link import('@src/core').SinkInterface}\n * (`sink.write(text, STREAM_LEVEL_MAP[level])`). `stderr` is conventionally the error/diagnostic\n * stream → `error`; `stdout` is the normal output stream → `info`. The source of truth for the\n * stream-to-log projection (the server analogue of the core `CAPTURE_LEVEL_MAP`).\n */\nexport const STREAM_LEVEL_MAP: Readonly<Record<StreamLevel, LogLevel>> = Object.freeze({\n\tstdout: 'info',\n\tstderr: 'error',\n})\n","// Total boundary guards for the server-console branch; every guard here is exported and unit-tested.\n// They narrow the two unavoidable boundaries the sink and the process capture cross — an injected or\n// real stream target, and the encoding argument a `process.*.write` carries — so neither surface\n// needs a type assertion and neither can throw on adversarial input.\n\nimport type { StreamTargetInterface } from './types.js'\n\n/**\n * Checks whether `value` is a usable {@link StreamTargetInterface} — a record with a callable `write`. A\n * total type guard: it never throws and returns `false` for anything off-shape, so it\n * narrows the one unavoidable boundary (the real `process.stdout` / `process.stderr`, or a fake\n * stream a test injects) to the exact slice the sink + capture touch — no `as`.\n *\n * @remarks\n * Only `write` is required (the irreducible output method); `isTTY` and `columns` are optional on\n * {@link StreamTargetInterface}, so their absence does not disqualify a target — a piped stream\n * (no `isTTY`) is still a valid write target, only a non-terminal one.\n *\n * @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)\n * @returns True if `value` has a callable `write`; false otherwise\n *\n * @example\n * ```ts\n * isStreamTarget(process.stdout) // true\n * isStreamTarget({ write: () => true }) // true\n * isStreamTarget({}) // false (no write)\n * ```\n */\nexport function isStreamTarget(value: unknown): value is StreamTargetInterface {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t'write' in value &&\n\t\ttypeof value.write === 'function'\n\t)\n}\n\n/**\n * Checks whether `encoding` is a {@link BufferEncoding} accepted by `Buffer.prototype.toString` — a total\n * guard used by {@link import('./helpers.js').decodeChunk} to honor a process-write `encoding`\n * argument only when it is a real Node encoding (otherwise utf-8 is assumed).\n *\n * @param encoding - The candidate encoding (the second `write` argument, possibly a callback)\n * @returns True if `encoding` names a supported buffer encoding; false otherwise\n */\nexport function isBufferEncoding(encoding: unknown): encoding is BufferEncoding {\n\treturn typeof encoding === 'string' && Buffer.isEncoding(encoding)\n}\n","// Pure helpers for the server-console branch; every function here is exported and unit-tested.\n// Total utilities: the TTY column probe, the total chunk→text decoder the process-capture wrapper\n// reuses so intercepting `process.*.write` can never throw, and pure color-environment inference\n// for the server sink. The boundary guards live in `validators.ts`.\n\nimport type { StreamTargetInterface } from './types.js'\nimport { DEFAULT_COLUMNS } from './constants.js'\nimport { isBufferEncoding } from './validators.js'\n\n/**\n * Infers the width in character cells of a stream target — its live `columns` when it is a TTY, else\n * the non-interactive {@link DEFAULT_COLUMNS} fallback. The basis a {@link import('./types.js').ServerSinkInterface}\n * reports through `columns` so a `Reporter` / `Progress` can size its layout to the terminal.\n *\n * @remarks\n * Reads `target.columns` on each call (so a getter-backed real stream reflects a live resize) and\n * accepts it only when it is a positive finite number; a missing / `0` / non-finite `columns` (a\n * piped, non-TTY stream) falls back to {@link DEFAULT_COLUMNS}. Total — never throws.\n *\n * @param target - The stream whose width to probe\n * @returns The terminal column count, or {@link DEFAULT_COLUMNS} when not a TTY\n */\nexport function inferColumns(target: StreamTargetInterface): number {\n\tconst columns = target.columns\n\tif (typeof columns === 'number' && Number.isFinite(columns) && columns > 0) return columns\n\treturn DEFAULT_COLUMNS\n}\n\n/**\n * Infers whether one stream target receives styled output. The result is a construction-time\n * target fact for {@link import('./factories.js').createServerSink}; this helper is pure and never\n * reads process globals itself.\n *\n * @remarks\n * A present `FORCE_COLOR` key has first precedence: only the exact value `'0'` disables styling.\n * Next, a non-empty `NO_COLOR` disables styling. Otherwise styling follows\n * `target.isTTY === true`.\n *\n * @param target - The stream target whose terminal capability is the fallback\n * @param environment - The environment record supplying `FORCE_COLOR` and `NO_COLOR`\n * @returns True if output for the target retains styling and control sequences; false otherwise\n *\n * @example\n * ```ts\n * inferStyled({ write: () => true, isTTY: false }, { FORCE_COLOR: '1' }) // true\n * inferStyled({ write: () => true, isTTY: true }, { NO_COLOR: '1' }) // false\n * ```\n */\nexport function inferStyled(\n\ttarget: StreamTargetInterface,\n\tenvironment: Readonly<Record<string, string | undefined>>,\n): boolean {\n\tif (Object.hasOwn(environment, 'FORCE_COLOR')) return environment.FORCE_COLOR !== '0'\n\tconst disabled = environment.NO_COLOR\n\tif (disabled !== undefined && disabled !== '') return false\n\treturn target.isTTY === true\n}\n\n/**\n * Decodes one `process.stdout.write` / `process.stderr.write` chunk to a string — total, never\n * throws. The process write signature accepts `string | Uint8Array` plus an optional\n * encoding; the capture wrapper reuses this so intercepting a raw stream write can never crash the\n * host (a throw inside `process.stdout.write` would take the program down).\n *\n * @remarks\n * - A `string` chunk is returned verbatim — the common case (`console.log`, most library output,\n * and `process.stdout.write('text')` all pass a string).\n * - A `Buffer` chunk is decoded with the supplied `encoding` when it is a recognized\n * {@link BufferEncoding} (`process` write supports `'utf8'` / `'hex'` / `'base64'` / …), defaulting\n * to `'utf8'`; a bare `Uint8Array` is decoded through `TextDecoder` (always utf-8 — the `encoding`\n * argument applies only to a `Buffer`, never a plain `Uint8Array`).\n * - Anything else is coerced with `String(chunk)` (a number / object / bigint / symbol a misbehaving\n * writer hands the stream). The coercion is itself guarded: a value whose `toString` /\n * `Symbol.toPrimitive` throws yields the stable `'[unprintable]'` placeholder. So the helper is\n * total on every input — it always yields some string, never an exception (a throw here would\n * escape into `process.*.write` and crash the host).\n *\n * @param chunk - The chunk passed to the stream's `write`\n * @param encoding - The optional encoding argument passed alongside the chunk\n * @returns The chunk as text\n *\n * @example\n * ```ts\n * decodeChunk('hi') // 'hi'\n * decodeChunk(Buffer.from('hi')) // 'hi'\n * decodeChunk(new Uint8Array([104, 105])) // 'hi'\n * ```\n */\nexport function decodeChunk(chunk: unknown, encoding?: unknown): string {\n\tif (typeof chunk === 'string') return chunk\n\ttry {\n\t\tif (Buffer.isBuffer(chunk)) {\n\t\t\treturn chunk.toString(isBufferEncoding(encoding) ? encoding : 'utf8')\n\t\t}\n\t\tif (chunk instanceof Uint8Array) return new TextDecoder().decode(chunk)\n\t\t// The String() coercion is inside the try too: a value with a hostile `toString` /\n\t\t// `Symbol.toPrimitive` would otherwise throw here and escape into `process.*.write`, crashing\n\t\t// the host — the exact failure this total decoder exists to prevent. Guard it.\n\t\treturn String(chunk)\n\t} catch {\n\t\t// Any decode / coercion failure yields a stable placeholder — the helper is total on every\n\t\t// input (the kind a misbehaving writer could hand the patched stream), never an exception.\n\t\treturn '[unprintable]'\n\t}\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type {\n\tCapturedChunk,\n\tProcessCaptureEventMap,\n\tProcessCaptureInterface,\n\tProcessCaptureOptions,\n\tStreamLevel,\n\tStreamWriteCallback,\n\tStreamWriteFunction,\n} from './types.js'\nimport type { RetentionInterface, SinkInterface } from '@src/core'\nimport { StringDecoder } from 'node:string_decoder'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Retention } from '@src/core'\nimport { DEFAULT_STREAM_LIMIT, STREAM_LEVEL_MAP, STREAM_LEVELS } from './constants.js'\nimport { decodeChunk } from './helpers.js'\nimport { isBufferEncoding } from './validators.js'\n\n/**\n * Implements an observable interceptor of the raw process output streams — it takes control of\n * `process.stdout.write` / `process.stderr.write` on the write side. While `active`, every write to\n * a configured {@link StreamLevel} is captured as a frozen {@link CapturedChunk}, buffered (total +\n * per-stream, bounded), emitted on `capture`, and — per options — mirrored to the real stream,\n * forwarded to a {@link SinkInterface}, or both.\n *\n * @remarks\n * Where the core `Capture` patches `console.*` (the high-level read side), this patches the\n * low-level stream `write`, so it owns all server output: a direct `process.stdout.write`, a\n * third-party library's writes, a child-process pipe — not only `console.*`.\n *\n * - **Snapshot-at-start (the no-capture-loop principle).** `start()` snapshots the current\n * `process[stream].write` for each configured level, then installs the wrappers. The mirror\n * replays through that snapshot (bound to its stream) — so a server sink created from the same\n * streams before the capture is never re-captured: this catches other writers, not the mirror's\n * own replay. Create your sinks before installing a capture.\n * - **Idempotent + process-global + non-reentrant.** `start()` while `active` is a no-op (never\n * double-patches — that would snapshot the wrapper as the \"original\" and break restore); `stop()`\n * while inactive is a no-op. It patches the one global `process`, so at most one process capture\n * may be active at a time — two concurrently would interleave buffers and clobber each other's\n * restore.\n * - **The wrapper never throws and passes backpressure through.** A throw inside\n * `process.stdout.write` would crash the host, so the wrapper decodes each chunk totally (a byte\n * chunk through the per-level streaming decoder below, everything else through the total\n * {@link decodeChunk}), and returns the snapshot-original's `boolean` when mirroring (so a caller's\n * `write` backpressure handling still works) or `true` when capture-only (the buffer never fills).\n * - **Streaming UTF-8 decode (no split-codepoint corruption).** `start()` gives each configured\n * {@link StreamLevel} a fresh persistent `StringDecoder`. A byte chunk with utf-8 or an omitted\n * encoding decodes through it, so a multibyte codepoint split across two `write` byte chunks — a\n * child-process pipe, a library, or OS buffering all produce this — carries its partial bytes to\n * the next write instead of decoding each half to `U+FFFD`. `stop()` flushes each decoder once, so\n * a codepoint left half-written at stop is still surfaced. A `string` chunk is already text and\n * passes through; an explicit non-utf-8 buffer encoding (`latin1` / `hex` / `base64` / …) names a\n * self-contained per-write decode and is honored one-shot through {@link decodeChunk}.\n * - **Bounded buffers.** The total buffer and each per-stream bucket are each capped at `limit`\n * (oldest dropped first), never unbounded — the same retention precedent as the core `Capture`.\n * - **Lifecycle.** `start` / `stop` toggle interception (emitting `start` / `stop`);\n * `destroy()` stops (restoring the pristine `write`) then destroys the emitter.\n *\n * @example\n * ```ts\n * const capture = new ProcessCapture({ levels: ['stderr'], mirror: true })\n * capture.start()\n * process.stderr.write('a library diagnostic\\n') // captured and still written to the terminal\n * capture.messages('stderr') // [{ level: 'stderr', text: 'a library diagnostic\\n', time: … }]\n * capture.stop() // process.stderr.write restored\n * ```\n */\nexport class ProcessCapture implements ProcessCaptureInterface {\n\t// The push observation surface — owned, never inherited. The emitter isolates a listener\n\t// throw (routing it to the `error` handler), so a buggy `capture` listener can never escape into\n\t// the host program's `process.*.write` call.\n\treadonly #emitter: Emitter<ProcessCaptureEventMap>\n\treadonly #levels: readonly StreamLevel[]\n\treadonly #mirror: boolean\n\treadonly #sink: SinkInterface | undefined\n\t// The bounded buffers — the total one and one per configured StreamLevel — owned by the shared\n\t// core retention engine the console `Capture` composes too.\n\treadonly #retention: RetentionInterface<CapturedChunk>\n\t// The snapshot-original `write` references, captured at start() and restored at stop(); empty\n\t// while inactive.\n\treadonly #originals = new Map<StreamLevel, StreamWriteFunction>()\n\t// One persistent streaming utf-8 decoder per configured level, created fresh in start() and\n\t// flushed + cleared in stop(). It carries a multibyte codepoint split across successive byte\n\t// writes so each half is not decoded to U+FFFD; empty while inactive.\n\treadonly #decoders = new Map<StreamLevel, StringDecoder>()\n\t// Stored rather than derived from #originals: an empty `levels` list patches no stream, so a\n\t// started capture configured with no level leaves #originals empty while still being active.\n\t#active = false\n\n\tconstructor(options?: ProcessCaptureOptions) {\n\t\tthis.#emitter = new Emitter<ProcessCaptureEventMap>({\n\t\t\t...(options?.on !== undefined ? { on: options.on } : {}),\n\t\t\t...(options?.error !== undefined ? { error: options.error } : {}),\n\t\t})\n\t\tthis.#levels = options?.levels ?? STREAM_LEVELS\n\t\tthis.#mirror = options?.mirror ?? false\n\t\tthis.#sink = options?.sink\n\t\tthis.#retention = new Retention<CapturedChunk>(\n\t\t\tthis.#levels,\n\t\t\toptions?.limit ?? DEFAULT_STREAM_LIMIT,\n\t\t)\n\t}\n\n\tget emitter(): EmitterInterface<ProcessCaptureEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget active(): boolean {\n\t\treturn this.#active\n\t}\n\n\tstart(): void {\n\t\t// Idempotent — never double-patch an already-active capture (that would snapshot the wrappers\n\t\t// as the \"originals\" and break restore).\n\t\tif (this.#active) return\n\t\tthis.#active = true\n\t\tfor (const level of this.#levels) {\n\t\t\tconst stream = this.#stream(level)\n\t\t\t// Snapshot the current write reference before replacing it — stop() restores exactly this\n\t\t\t// reference, leaving the stream pristine (the wrapper is never snapshotted as the original).\n\t\t\tconst original = stream.write\n\t\t\tthis.#originals.set(level, original)\n\t\t\t// The mirror target is the snapshot original bound to its stream, computed once here — so a\n\t\t\t// mirrored write reaches the real method with its proper receiver, through the snapshot and\n\t\t\t// never the live (patched) `write` (no capture loop). The restore reference stays the pristine\n\t\t\t// unbound `original` above; only the mirror uses the bound form.\n\t\t\tconst mirror = original.bind(stream)\n\t\t\t// The replacement matches the Node `write` overload shape exactly — `(chunk, encoding?, cb?)`\n\t\t\t// where the 2nd arg is either a `BufferEncoding` or the completion callback — so it assigns to\n\t\t\t// the stream's `write` slot and its args forward cleanly to `mirror` (no `as`, no untyped\n\t\t\t// spread).\n\t\t\tstream.write = this.#captureWrite.bind(this, level, mirror)\n\t\t\t// A fresh streaming decoder per cycle — a stop → start pair starts clean, never carrying a\n\t\t\t// stale partial byte from a prior capture into the new one.\n\t\t\tthis.#decoders.set(level, new StringDecoder('utf8'))\n\t\t}\n\t\tthis.#emitter.emit('start')\n\t}\n\n\tstop(): void {\n\t\t// Safe when not active — nothing to restore.\n\t\tif (!this.#active) return\n\t\tthis.#active = false\n\t\tfor (const [level, original] of this.#originals) this.#stream(level).write = original\n\t\tthis.#originals.clear()\n\t\t// Drain any trailing partial codepoint from each streaming decoder before the `stop` signal, so\n\t\t// a codepoint left half-written at stop is captured once rather than dropped.\n\t\tthis.#flush()\n\t\tthis.#emitter.emit('stop')\n\t}\n\n\tmessages(): readonly CapturedChunk[]\n\tmessages(level: StreamLevel): readonly CapturedChunk[]\n\tmessages(level?: StreamLevel): readonly CapturedChunk[] {\n\t\tif (level === undefined) return this.#retention.records()\n\t\treturn this.#retention.records(level)\n\t}\n\n\tclear(): void {\n\t\tthis.#retention.clear()\n\t}\n\n\tdestroy(): void {\n\t\tthis.stop()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// The global WriteStream for a StreamLevel — `process[level]` indexes it directly, since a\n\t// StreamLevel is the `process` property key (`'stdout'` / `'stderr'`); no `as`, no lookup map.\n\t#stream(level: StreamLevel): NodeJS.WriteStream {\n\t\treturn process[level]\n\t}\n\n\t// Adapt the patched stream's write signature to #intercept. Binding level and the pristine\n\t// mirror in start() leaves the canonical chunk / encoding / callback parameters.\n\t#captureWrite(\n\t\tlevel: StreamLevel,\n\t\tmirror: StreamWriteFunction,\n\t\tchunk: string | Uint8Array,\n\t\tencoding?: BufferEncoding | StreamWriteCallback,\n\t\tcallback?: StreamWriteCallback,\n\t): boolean {\n\t\treturn this.#intercept(level, chunk, encoding, callback, mirror)\n\t}\n\n\t// The wrapper body behind every patched stream write: decode the chunk to text (#decode — total,\n\t// streaming for byte chunks), record it (buffer bounded, emit `capture`, forward to the sink),\n\t// then — per options — mirror to the real stream. Never throws (#decode is total; the emitter\n\t// isolates listeners); the program's own write is replayed through `mirror` (the bound snapshot\n\t// original) only when the `mirror` option is set, and the original's backpressure boolean is\n\t// returned. Capture-only returns `true` (output is swallowed into the buffer, so the kernel buffer\n\t// never fills). The raw chunk (not the decoded text) is what mirrors, so the terminal still\n\t// receives the exact bytes; the `encoding` / `callback` tail is forwarded to the mirror branched\n\t// on whether the 2nd arg is the callback or an encoding (the two Node overloads), so a caller's\n\t// completion callback fires.\n\t#intercept(\n\t\tlevel: StreamLevel,\n\t\tchunk: string | Uint8Array,\n\t\tencoding: BufferEncoding | StreamWriteCallback | undefined,\n\t\tcallback: StreamWriteCallback | undefined,\n\t\tmirror: StreamWriteFunction,\n\t): boolean {\n\t\tthis.#record(level, this.#decode(level, chunk, encoding))\n\t\tif (!this.#mirror) {\n\t\t\t// Capture-only: the write never reaches the real stream, so fire the caller's completion\n\t\t\t// callback asynchronously (matching Node's own async completion semantics) rather than\n\t\t\t// silently dropping it — both call shapes (`write(chunk, cb)` and `write(chunk, encoding, cb)`)\n\t\t\t// are covered.\n\t\t\tconst done = typeof encoding === 'function' ? encoding : callback\n\t\t\tif (done !== undefined) queueMicrotask(() => done())\n\t\t\treturn true\n\t\t}\n\t\t// `write(chunk, cb)` when the 2nd arg is the callback; `write(chunk, encoding, cb)` otherwise —\n\t\t// matching the two Node overloads so the forward stays typed.\n\t\tif (typeof encoding === 'function') return mirror(chunk, encoding)\n\t\treturn mirror(chunk, encoding, callback)\n\t}\n\n\t// Decode one write chunk to text — total, never throws (a throw here would escape into the patched\n\t// process.*.write and crash the host). A `string` chunk is already text and passes through. A byte\n\t// chunk with utf-8 / an omitted encoding / a callback in the encoding slot streams through the\n\t// level's persistent decoder, carrying a codepoint split across writes; an explicit non-utf-8\n\t// buffer encoding is self-contained per write and decoded one-shot through decodeChunk.\n\t#decode(\n\t\tlevel: StreamLevel,\n\t\tchunk: string | Uint8Array,\n\t\tencoding: BufferEncoding | StreamWriteCallback | undefined,\n\t): string {\n\t\tif (typeof chunk === 'string') return chunk\n\t\tconst decoder = this.#decoders.get(level)\n\t\tif (decoder !== undefined && this.#streams(encoding)) {\n\t\t\treturn decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))\n\t\t}\n\t\treturn decodeChunk(chunk, encoding)\n\t}\n\n\t// Whether a byte chunk with this encoding routes through the persistent streaming decoder rather\n\t// than the one-shot decodeChunk. True for an omitted encoding, a callback in the slot, an\n\t// unrecognized string (all utf-8 by decodeChunk's own fallback), or an explicit utf-8; false only\n\t// for a recognized non-utf-8 buffer encoding, whose per-write bytes are self-contained.\n\t#streams(encoding: BufferEncoding | StreamWriteCallback | undefined): boolean {\n\t\tif (!isBufferEncoding(encoding)) return true\n\t\tconst normalized = encoding.toLowerCase()\n\t\treturn normalized === 'utf8' || normalized === 'utf-8'\n\t}\n\n\t// Build the immutable, serializable captured chunk from already-decoded text, stamp it with the\n\t// capture instant, buffer it (total + per-stream, bounded), emit `capture`, then forward it to the\n\t// sink. Frozen so a consumer (or the `capture` listener) can never mutate it. Never throws into the\n\t// patched stream — the sink is a best-effort tee and the emitter isolates a listener throw.\n\t#record(level: StreamLevel, text: string): void {\n\t\tconst message: CapturedChunk = Object.freeze({ level, text, time: Date.now() })\n\t\tthis.#retention.add(message)\n\t\tthis.#emitter.emit('capture', message)\n\t\tif (this.#sink !== undefined) {\n\t\t\ttry {\n\t\t\t\tthis.#sink.write(message.text, STREAM_LEVEL_MAP[level])\n\t\t\t} catch {\n\t\t\t\t// The sink is a best-effort tee; the wrapper never throws into the patched global stream —\n\t\t\t\t// a broken/throwing sink must not crash the host's process.stdout / process.stderr write.\n\t\t\t}\n\t\t}\n\t}\n\n\t// Drain each streaming decoder's trailing partial codepoint once — `decoder.end()` emits the\n\t// pending bytes' final text (U+FFFD for a genuinely truncated sequence) — so a codepoint left\n\t// half-written at stop is recorded, not silently dropped. An empty flush adds no record. Clears\n\t// the decoders so the next start() begins clean.\n\t#flush(): void {\n\t\tfor (const [level, decoder] of this.#decoders) {\n\t\t\tconst text = decoder.end()\n\t\t\tif (text !== '') this.#record(level, text)\n\t\t}\n\t\tthis.#decoders.clear()\n\t}\n}\n","import type { LogLevel } from '@src/core'\nimport type { ServerSinkInterface, ServerSinkOptions } from './types.js'\nimport { selectWriter, strip, stripControls } from '@src/core'\nimport { inferColumns, inferStyled } from './helpers.js'\nimport { isStreamTarget } from './validators.js'\n\n/**\n * Creates the server TTY {@link ServerSinkInterface} — the server output backend, the\n * env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`\n * routes by level to the process streams and uses construction-time styled facts: it sends ANSI\n * straight to a styled target (with a leading `\\r` overwriting a terminal line natively) but\n * {@link import('@src/core').strip}s ANSI to clean text for a plain target.\n *\n * @param options - See {@link ServerSinkOptions}\n * @returns A {@link ServerSinkInterface} — a {@link import('@src/core').SinkInterface} that also\n * exposes the terminal `columns` width\n *\n * @remarks\n * - **Routes by level.** `error` / `warn` → the error stream (`process.stderr` by default), every\n * other level (and an omitted level) → the `stdout` stream (`process.stdout`) — the same routing\n * as core's `createConsoleSink`, so a logger's `error` reaches `stderr`. Both call the one\n * {@link import('@src/core').selectWriter} leaf, which is what keeps them identical.\n * - **Per-target styled facts.** At construction, each target uses `options.styled` when supplied;\n * otherwise {@link inferStyled} applies the injected `environment` (default `process.env`) and\n * then that target's `isTTY`.\n * Writes use those stored facts, so `styled` and the `stdout` target's strip decision never\n * disagree; the `stderr` target keeps its own fact internally.\n * - **Width.** `columns` reflects the live `stdout.columns` (so it tracks a terminal resize),\n * falling back to {@link import('./constants.js').DEFAULT_COLUMNS} when the `stdout` stream is not\n * a TTY — or a fixed value when `options.columns` is supplied. Feed it to a `Reporter` /\n * `Progress` `width`.\n * - **Injectable + guard-narrowed.** `options.stdout` / `options.stderr` default to `process.stdout`\n * / `process.stderr` but accept any {@link import('./types.js').StreamTargetInterface}, resolved\n * through {@link isStreamTarget} (narrow the boundary, never `as`), so a test drives\n * the sink (and the isTTY-strip path) with a fake stream that never touches the real process\n * streams.\n *\n * @example\n * ```ts\n * import { createStyler, Logger, Reporter } from '@orkestrel/console'\n * import { createServerSink } from '@orkestrel/console/server'\n *\n * const sink = createServerSink()\n * const styler = createStyler({ enabled: sink.styled })\n * const logger = new Logger({ name: 'app', sink, styler })\n * logger.error('boom') // → process.stderr, ANSI rendered on a TTY / stripped to a pipe\n * const reporter = new Reporter({ sink, width: sink.columns })\n * ```\n */\nexport function createServerSink(options?: ServerSinkOptions): ServerSinkInterface {\n\t// Resolve each target through the guard: a present, well-shaped injected stream is used as\n\t// is; otherwise the real process stream — no `as`, and an `undefined` option falls through to the\n\t// default. `stdout` carries info/debug, `stderr` carries error/warn.\n\tconst out = isStreamTarget(options?.stdout) ? options.stdout : process.stdout\n\tconst err = isStreamTarget(options?.stderr) ? options.stderr : process.stderr\n\tconst styled = options?.styled\n\tconst environment = options?.environment ?? process.env\n\tconst outStyled = styled ?? inferStyled(out, environment)\n\tconst errStyled = styled ?? inferStyled(err, environment)\n\tconst fixed = options?.columns\n\treturn Object.freeze({\n\t\tstyled: outStyled,\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\t// The one shared routing leaf picks the target and its styled fact together; `warn` shares\n\t\t\t// the error stream here, matching `console.warn` writing to stderr in core.\n\t\t\tconst target = selectWriter(level, { log: out, warn: err, error: err })\n\t\t\tconst keep = selectWriter(level, { log: outStyled, warn: errStyled, error: errStyled })\n\t\t\t// A leading `\\r` marks an in-place redraw frame (Spinner/Progress), which carries its own\n\t\t\t// line endings and is written verbatim; every other (line-oriented) write gets exactly one\n\t\t\t// trailing `\\n` appended here, matching `console.log`'s newline-terminated behavior.\n\t\t\tconst framed = text.startsWith('\\r')\n\t\t\tconst line = framed ? text : `${text}\\n`\n\t\t\t// A styled target receives the line verbatim; a plain target receives visible text only.\n\t\t\ttarget.write(keep ? line : stripControls(strip(line)))\n\t\t},\n\t\tget columns(): number {\n\t\t\t// A fixed override wins; otherwise the live stdout-stream width (tracks a resize), with the\n\t\t\t// non-TTY fallback inside inferColumns.\n\t\t\treturn typeof fixed === 'number' ? fixed : inferColumns(out)\n\t\t},\n\t})\n}\n"],"mappings":"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"}