@orkestrel/console 0.0.11 → 0.0.13

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.
@@ -4,57 +4,56 @@ let _orkestrel_emitter = require("@orkestrel/emitter");
4
4
  let _src_core = require("../core/index.cjs");
5
5
  //#region src/server/constants.ts
6
6
  /**
7
- * The two process streams a {@link import('./types.js').ProcessCaptureInterface} can intercept, in
8
- * `stdout`-then-`stderr` order — the {@link StreamLevel} universe and the default configured set.
7
+ * Lists the process streams a {@link import('./types.js').ProcessCaptureInterface} can intercept,
8
+ * in `stdout`-then-`stderr` order — the {@link StreamLevel} universe and the default configured
9
+ * set.
9
10
  */
10
11
  var STREAM_LEVELS = Object.freeze(["stdout", "stderr"]);
11
12
  /**
12
- * The default set of {@link StreamLevel}s a process capture patches when `options.levels` is omitted
13
- * BOTH streams ({@link STREAM_LEVELS}). A consumer narrows it (e.g. just `['stderr']`) via
14
- * `options.levels`.
15
- */
16
- var DEFAULT_CAPTURE_LEVELS = STREAM_LEVELS;
17
- /**
18
- * The default bounded-buffer cap for a {@link import('./types.js').ProcessCaptureInterface} at
19
- * most this many recent {@link import('./types.js').CapturedChunk}s are retained per buffer (the
20
- * total buffer AND each per-stream bucket; oldest dropped first). Mirrors the core `Capture`'s
21
- * `DEFAULT_CAPTURE_LIMIT`; a consumer overrides it via `options.limit`.
13
+ * Sets the default bounded-buffer cap for a {@link import('./types.js').ProcessCaptureInterface}
14
+ * `1000`, so at most that many recent {@link import('./types.js').CapturedChunk}s are retained per
15
+ * buffer (the total buffer and each per-stream bucket; oldest dropped first) and retention is
16
+ * always bounded.
17
+ *
18
+ * @remarks
19
+ * It mirrors the core `Capture`'s `DEFAULT_CAPTURE_LIMIT`; a consumer overrides the cap through
20
+ * `options.limit`.
22
21
  */
23
- var DEFAULT_CAPTURE_LIMIT = 1e3;
22
+ var DEFAULT_STREAM_LIMIT = 1e3;
24
23
  /**
25
- * The terminal width {@link import('./factories.js').createServerSink} reports through
26
- * {@link import('./types.js').ServerSinkInterface.columns} when the out stream is NOT a TTY (so
27
- * `.columns` is `undefined`) and no explicit `options.columns` was supplied — the conventional
24
+ * Sets the terminal width {@link import('./factories.js').createServerSink} reports through
25
+ * {@link import('./types.js').ServerSinkInterface.columns} when the `stdout` stream is not a TTY
26
+ * (so `.columns` is `undefined`) and no explicit `options.columns` was supplied — the conventional
28
27
  * 80-column default a non-interactive context (a pipe, a CI log) assumes.
29
28
  */
30
29
  var DEFAULT_COLUMNS = 80;
31
30
  /**
32
- * Each {@link StreamLevel}'s {@link LogLevel} for the optional sink forward — the projection a
33
- * process capture routes through when writing an intercepted chunk to a
34
- * {@link import('@src/core').SinkInterface}
35
- * (`sink.write(text, STREAM_LEVEL_MAP[level])`). `stderr` is conventionally the error/diagnostic
36
- * stream → `error`; `stdout` is the normal output stream → `info`. The source of truth for the
37
- * stream-to-log projection (the server analogue of the core `CAPTURE_LEVEL_MAP`).
31
+ * Maps each {@link StreamLevel} to its {@link LogLevel} for the optional sink forward — the
32
+ * projection a process capture routes through when writing an intercepted chunk to a
33
+ * {@link import('@src/core').SinkInterface}. `sink.write(text, STREAM_LEVEL_MAP[level])` is the
34
+ * call this map backs. `stderr` is conventionally the error/diagnostic stream → `error`; `stdout`
35
+ * is the normal output stream → `info`. The source of truth for the stream-to-log projection (the
36
+ * server analogue of the core `CAPTURE_LEVEL_MAP`).
38
37
  */
39
38
  var STREAM_LEVEL_MAP = Object.freeze({
40
39
  stdout: "info",
41
40
  stderr: "error"
42
41
  });
43
42
  //#endregion
44
- //#region src/server/helpers.ts
43
+ //#region src/server/validators.ts
45
44
  /**
46
- * Whether `value` is a usable {@link StreamTargetInterface} — a record with a callable `write`. A
47
- * total type guard (AGENTS §14): it NEVER throws and returns `false` for anything off-shape, so it
45
+ * Checks whether `value` is a usable {@link StreamTargetInterface} — a record with a callable `write`. A
46
+ * total type guard: it never throws and returns `false` for anything off-shape, so it
48
47
  * narrows the one unavoidable boundary (the real `process.stdout` / `process.stderr`, or a fake
49
48
  * stream a test injects) to the exact slice the sink + capture touch — no `as`.
50
49
  *
51
50
  * @remarks
52
51
  * Only `write` is required (the irreducible output method); `isTTY` and `columns` are optional on
53
52
  * {@link StreamTargetInterface}, so their absence does not disqualify a target — a piped stream
54
- * (no `isTTY`) is still a valid write target, just a non-terminal one.
53
+ * (no `isTTY`) is still a valid write target, only a non-terminal one.
55
54
  *
56
55
  * @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)
57
- * @returns `true` when `value` has a callable `write`
56
+ * @returns True if `value` has a callable `write`; false otherwise
58
57
  *
59
58
  * @example
60
59
  * ```ts
@@ -67,36 +66,50 @@ function isStreamTarget(value) {
67
66
  return typeof value === "object" && value !== null && "write" in value && typeof value.write === "function";
68
67
  }
69
68
  /**
70
- * The width in character cells of a stream target its live `columns` when it is a TTY, else the
71
- * non-interactive {@link DEFAULT_COLUMNS} fallback. The basis a {@link import('./types.js').ServerSinkInterface}
72
- * reports through `columns` so a `Reporter` / `Progress` can size its layout to the terminal.
69
+ * Checks whether `encoding` is a {@link BufferEncoding} accepted by `Buffer.prototype.toString` a
70
+ * total guard used by {@link import('./helpers.js').decodeChunk} to honor a process-write
71
+ * `encoding` argument only when it is a real Node encoding (otherwise utf-8 is assumed).
72
+ *
73
+ * @param encoding - The candidate encoding (the second `write` argument, possibly a callback)
74
+ * @returns True if `encoding` names a supported buffer encoding; false otherwise
75
+ */
76
+ function isBufferEncoding(encoding) {
77
+ return typeof encoding === "string" && Buffer.isEncoding(encoding);
78
+ }
79
+ //#endregion
80
+ //#region src/server/helpers.ts
81
+ /**
82
+ * Infers the width in character cells of a stream target — its live `columns` when it is a TTY,
83
+ * else the non-interactive {@link DEFAULT_COLUMNS} fallback. The basis a
84
+ * {@link import('./types.js').ServerSinkInterface} reports through `columns` so a `Reporter` /
85
+ * `Progress` can size its layout to the terminal.
73
86
  *
74
87
  * @remarks
75
- * Reads `target.columns` ON EACH CALL (so a getter-backed real stream reflects a live resize) and
88
+ * Reads `target.columns` on each call (so a getter-backed real stream reflects a live resize) and
76
89
  * accepts it only when it is a positive finite number; a missing / `0` / non-finite `columns` (a
77
90
  * piped, non-TTY stream) falls back to {@link DEFAULT_COLUMNS}. Total — never throws.
78
91
  *
79
92
  * @param target - The stream whose width to probe
80
93
  * @returns The terminal column count, or {@link DEFAULT_COLUMNS} when not a TTY
81
94
  */
82
- function columnsOf(target) {
95
+ function inferColumns(target) {
83
96
  const columns = target.columns;
84
97
  if (typeof columns === "number" && Number.isFinite(columns) && columns > 0) return columns;
85
98
  return 80;
86
99
  }
87
100
  /**
88
- * Infer whether one stream target should receive styled output. The result is a construction-time
89
- * target fact for {@link import('./factories.js').createServerSink}; this helper is pure and never
90
- * reads process globals itself.
101
+ * Infers whether one stream target receives styled output a present `FORCE_COLOR` first, then a
102
+ * non-empty `NO_COLOR`, then `target.isTTY === true`.
91
103
  *
92
104
  * @remarks
93
- * A present `FORCE_COLOR` key has first precedence: only the exact value `'0'` disables styling.
94
- * Next, a non-empty `NO_COLOR` disables styling. Otherwise styling follows
95
- * `target.isTTY === true`.
105
+ * The result is a construction-time target fact for
106
+ * {@link import('./factories.js').createServerSink}; this helper is pure and
107
+ * never reads process globals itself. Under `FORCE_COLOR` only the exact value `'0'` disables
108
+ * styling.
96
109
  *
97
110
  * @param target - The stream target whose terminal capability is the fallback
98
111
  * @param environment - The environment record supplying `FORCE_COLOR` and `NO_COLOR`
99
- * @returns Whether output for the target should retain styling and control sequences
112
+ * @returns True if output for the target retains styling and control sequences; false otherwise
100
113
  *
101
114
  * @example
102
115
  * ```ts
@@ -111,8 +124,8 @@ function inferStyled(target, environment) {
111
124
  return target.isTTY === true;
112
125
  }
113
126
  /**
114
- * Decode one `process.stdout.write` / `process.stderr.write` chunk to a string — TOTAL, never
115
- * throws (AGENTS §14). The process write signature accepts `string | Uint8Array` plus an optional
127
+ * Decodes one `process.stdout.write` / `process.stderr.write` chunk to a string — total, never
128
+ * throws. The process write signature accepts `string | Uint8Array` plus an optional
116
129
  * encoding; the capture wrapper reuses this so intercepting a raw stream write can never crash the
117
130
  * host (a throw inside `process.stdout.write` would take the program down).
118
131
  *
@@ -121,12 +134,12 @@ function inferStyled(target, environment) {
121
134
  * and `process.stdout.write('text')` all pass a string).
122
135
  * - A `Buffer` chunk is decoded with the supplied `encoding` when it is a recognized
123
136
  * {@link BufferEncoding} (`process` write supports `'utf8'` / `'hex'` / `'base64'` / …), defaulting
124
- * to `'utf8'`; a bare `Uint8Array` is decoded via `TextDecoder` (always utf-8 — the `encoding`
125
- * argument applies ONLY to a `Buffer`, never a plain `Uint8Array`).
137
+ * to `'utf8'`; a bare `Uint8Array` is decoded through `TextDecoder` (always utf-8 — the `encoding`
138
+ * argument applies only to a `Buffer`, never a plain `Uint8Array`).
126
139
  * - Anything else is coerced with `String(chunk)` (a number / object / bigint / symbol a misbehaving
127
140
  * writer hands the stream). The coercion is itself guarded: a value whose `toString` /
128
141
  * `Symbol.toPrimitive` throws yields the stable `'[unprintable]'` placeholder. So the helper is
129
- * TOTAL on every input — it always yields SOME string, never an exception (a throw here would
142
+ * total on every input — it always yields some string, never an exception (a throw here would
130
143
  * escape into `process.*.write` and crash the host).
131
144
  *
132
145
  * @param chunk - The chunk passed to the stream's `write`
@@ -150,42 +163,31 @@ function decodeChunk(chunk, encoding) {
150
163
  return "[unprintable]";
151
164
  }
152
165
  }
153
- /**
154
- * Whether `encoding` is a {@link BufferEncoding} accepted by `Buffer.prototype.toString` — a total
155
- * guard used by {@link decodeChunk} to honor a process-write `encoding` argument only when it is a
156
- * real Node encoding (otherwise utf-8 is assumed).
157
- *
158
- * @param encoding - The candidate encoding (the second `write` argument, possibly a callback)
159
- * @returns `true` when `encoding` names a supported buffer encoding
160
- */
161
- function isBufferEncoding(encoding) {
162
- return typeof encoding === "string" && Buffer.isEncoding(encoding);
163
- }
164
166
  //#endregion
165
167
  //#region src/server/ProcessCapture.ts
166
168
  /**
167
- * An observable interceptor of the RAW process output streams (AGENTS §13) — it takes control of
168
- * `process.stdout.write` / `process.stderr.write` on the WRITE side. While `active`, every write to
169
+ * Implements an observable interceptor of the raw process output streams — it takes control of
170
+ * `process.stdout.write` / `process.stderr.write` on the write side. While `active`, every write to
169
171
  * a configured {@link StreamLevel} is captured as a frozen {@link CapturedChunk}, buffered (total +
170
- * per-stream, bounded), emitted on `capture`, and — per options — mirrored to the real stream and/or
171
- * forwarded to a {@link SinkInterface}.
172
+ * per-stream, bounded), emitted on `capture`, and — per options — mirrored to the real stream,
173
+ * forwarded to a {@link SinkInterface}, or both.
172
174
  *
173
175
  * @remarks
174
176
  * Where the core `Capture` patches `console.*` (the high-level read side), this patches the
175
- * low-level stream `write`, so it owns ALL server output: a direct `process.stdout.write`, a
177
+ * low-level stream `write`, so it owns all server output: a direct `process.stdout.write`, a
176
178
  * third-party library's writes, a child-process pipe — not only `console.*`.
177
179
  *
178
- * - **Snapshot-at-start (the no-capture-loop principle).** `start()` snapshots the CURRENT
180
+ * - **Snapshot-at-start (the no-capture-loop principle).** `start()` snapshots the current
179
181
  * `process[stream].write` for each configured level, then installs the wrappers. The mirror
180
182
  * replays through that snapshot (bound to its stream) — so a server sink created from the same
181
- * streams BEFORE the capture is never re-captured: this catches OTHER writers, not the mirror's
183
+ * streams before the capture is never re-captured: this catches other writers, not the mirror's
182
184
  * own replay. Create your sinks before installing a capture.
183
- * - **Idempotent + PROCESS-GLOBAL + NON-REENTRANT.** `start()` while `active` is a no-op (never
185
+ * - **Idempotent + process-global + non-reentrant.** `start()` while `active` is a no-op (never
184
186
  * double-patches — that would snapshot the wrapper as the "original" and break restore); `stop()`
185
- * while inactive is a no-op. It patches the ONE global `process`, so at most ONE process capture
187
+ * while inactive is a no-op. It patches the one global `process`, so at most one process capture
186
188
  * may be active at a time — two concurrently would interleave buffers and clobber each other's
187
189
  * restore.
188
- * - **The wrapper NEVER throws and passes backpressure through.** A throw inside
190
+ * - **The wrapper never throws and passes backpressure through.** A throw inside
189
191
  * `process.stdout.write` would crash the host, so the wrapper decodes each chunk totally (a byte
190
192
  * chunk through the per-level streaming decoder below, everything else through the total
191
193
  * {@link decodeChunk}), and returns the snapshot-original's `boolean` when mirroring (so a caller's
@@ -196,18 +198,18 @@ function isBufferEncoding(encoding) {
196
198
  * child-process pipe, a library, or OS buffering all produce this — carries its partial bytes to
197
199
  * the next write instead of decoding each half to `U+FFFD`. `stop()` flushes each decoder once, so
198
200
  * a codepoint left half-written at stop is still surfaced. A `string` chunk is already text and
199
- * passes through; an explicit NON-utf-8 buffer encoding (`latin1` / `hex` / `base64` / …) names a
201
+ * passes through; an explicit non-utf-8 buffer encoding (`latin1` / `hex` / `base64` / …) names a
200
202
  * self-contained per-write decode and is honored one-shot through {@link decodeChunk}.
201
203
  * - **Bounded buffers.** The total buffer and each per-stream bucket are each capped at `limit`
202
204
  * (oldest dropped first), never unbounded — the same retention precedent as the core `Capture`.
203
- * - **Lifecycle (§10).** `start` / `stop` toggle interception (emitting `start` / `stop`);
204
- * `destroy()` stops (restoring the PRISTINE `write`) then destroys the emitter.
205
+ * - **Lifecycle.** `start` / `stop` toggle interception (emitting `start` / `stop`);
206
+ * `destroy()` stops (restoring the pristine `write`) then destroys the emitter.
205
207
  *
206
208
  * @example
207
209
  * ```ts
208
210
  * const capture = new ProcessCapture({ levels: ['stderr'], mirror: true })
209
211
  * capture.start()
210
- * process.stderr.write('a library diagnostic\n') // captured AND still written to the terminal
212
+ * process.stderr.write('a library diagnostic\n') // captured and still written to the terminal
211
213
  * capture.messages('stderr') // [{ level: 'stderr', text: 'a library diagnostic\n', time: … }]
212
214
  * capture.stop() // process.stderr.write restored
213
215
  * ```
@@ -217,9 +219,7 @@ var ProcessCapture = class {
217
219
  #levels;
218
220
  #mirror;
219
221
  #sink;
220
- #limit;
221
- #messages = [];
222
- #buckets = /* @__PURE__ */ new Map();
222
+ #retention;
223
223
  #originals = /* @__PURE__ */ new Map();
224
224
  #decoders = /* @__PURE__ */ new Map();
225
225
  #active = false;
@@ -228,11 +228,10 @@ var ProcessCapture = class {
228
228
  ...options?.on !== void 0 ? { on: options.on } : {},
229
229
  ...options?.error !== void 0 ? { error: options.error } : {}
230
230
  });
231
- this.#levels = options?.levels ?? DEFAULT_CAPTURE_LEVELS;
231
+ this.#levels = options?.levels ?? STREAM_LEVELS;
232
232
  this.#mirror = options?.mirror ?? false;
233
233
  this.#sink = options?.sink;
234
- this.#limit = options?.limit ?? 1e3;
235
- for (const level of this.#levels) this.#buckets.set(level, []);
234
+ this.#retention = new _src_core.Retention(this.#levels, options?.limit ?? 1e3);
236
235
  }
237
236
  get emitter() {
238
237
  return this.#emitter;
@@ -262,12 +261,11 @@ var ProcessCapture = class {
262
261
  this.#emitter.emit("stop");
263
262
  }
264
263
  messages(level) {
265
- if (level === void 0) return [...this.#messages];
266
- return [...this.#buckets.get(level) ?? []];
264
+ if (level === void 0) return this.#retention.records();
265
+ return this.#retention.records(level);
267
266
  }
268
267
  clear() {
269
- this.#messages.length = 0;
270
- for (const bucket of this.#buckets.values()) bucket.length = 0;
268
+ this.#retention.clear();
271
269
  }
272
270
  destroy() {
273
271
  this.stop();
@@ -306,7 +304,7 @@ var ProcessCapture = class {
306
304
  text,
307
305
  time: Date.now()
308
306
  });
309
- this.#retain(message);
307
+ this.#retention.add(message);
310
308
  this.#emitter.emit("capture", message);
311
309
  if (this.#sink !== void 0) try {
312
310
  this.#sink.write(message.text, STREAM_LEVEL_MAP[level]);
@@ -319,24 +317,14 @@ var ProcessCapture = class {
319
317
  }
320
318
  this.#decoders.clear();
321
319
  }
322
- #retain(message) {
323
- this.#push(this.#messages, message);
324
- const bucket = this.#buckets.get(message.level);
325
- if (bucket !== void 0) this.#push(bucket, message);
326
- }
327
- #push(buffer, message) {
328
- buffer.push(message);
329
- if (buffer.length > this.#limit) buffer.shift();
330
- }
331
320
  };
332
321
  //#endregion
333
322
  //#region src/server/factories.ts
334
323
  /**
335
- * Create the server TTY {@link ServerSinkInterface} — the C-g server output backend, the
336
- * env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`
337
- * routes by level to the process streams and uses construction-time styled facts: it sends ANSI
338
- * straight to a styled target (with a leading `\r` overwriting a terminal line natively) but
339
- * {@link import('@src/core').strip}s ANSI to clean text for a plain target.
324
+ * Creates the server TTY {@link ServerSinkInterface} — the server output backend, whose
325
+ * `write(text, level?)` routes by level to the process streams and uses construction-time styled
326
+ * facts: it sends ANSI straight to a styled target (with a leading `\r` overwriting a terminal
327
+ * line natively) but {@link import('@src/core').strip}s ANSI to clean text for a plain target.
340
328
  *
341
329
  * @param options - See {@link ServerSinkOptions}
342
330
  * @returns A {@link ServerSinkInterface} — a {@link import('@src/core').SinkInterface} that also
@@ -344,37 +332,52 @@ var ProcessCapture = class {
344
332
  *
345
333
  * @remarks
346
334
  * - **Routes by level.** `error` / `warn` → the error stream (`process.stderr` by default), every
347
- * other level (and an omitted level) → the out stream (`process.stdout`) — the SAME routing as
348
- * core's `createConsoleSink`, so a logger's `error` reaches `stderr`.
335
+ * other level (and an omitted level) → the `stdout` stream (`process.stdout`) — the same routing
336
+ * as core's `createConsoleSink`, so a logger's `error` reaches `stderr`. Both call the one
337
+ * {@link import('@src/core').selectWriter} leaf, which is what keeps them identical.
349
338
  * - **Per-target styled facts.** At construction, each target uses `options.styled` when supplied;
350
339
  * otherwise {@link inferStyled} applies the injected `environment` (default `process.env`) and
351
340
  * then that target's `isTTY`.
352
- * Writes use those stored facts, so `styled` and the out target's strip decision never disagree;
353
- * the err target keeps its own fact internally.
354
- * - **Width.** `columns` reflects the live `out.columns` (so it tracks a terminal resize), falling
355
- * back to {@link import('./constants.js').DEFAULT_COLUMNS} when the out stream is not a TTY — or a
356
- * fixed value when `options.columns` is supplied. Feed it to a `Reporter` / `Progress` `width`.
357
- * - **Injectable + guard-narrowed.** `options.out` / `options.err` default to `process.stdout` /
358
- * `process.stderr` but accept ANY {@link import('./types.js').StreamTargetInterface}, resolved
359
- * through {@link isStreamTarget} (AGENTS §14 — narrow the boundary, never `as`), so a test drives
341
+ * Writes use those stored facts, so `styled` and the `stdout` target's strip decision never
342
+ * disagree; the `stderr` target keeps its own fact internally.
343
+ * - **Width.** `columns` reflects the live `stdout.columns` (so it tracks a terminal resize),
344
+ * falling back to {@link import('./constants.js').DEFAULT_COLUMNS} when the `stdout` stream is not
345
+ * a TTY — or a fixed value when `options.columns` is supplied. Feed it to a `Reporter` /
346
+ * `Progress` `width`.
347
+ * - **Injectable + guard-narrowed.** `options.stdout` / `options.stderr` default to `process.stdout`
348
+ * / `process.stderr` but accept any {@link import('./types.js').StreamTargetInterface}, resolved
349
+ * through {@link isStreamTarget} (narrow the boundary, never `as`), so a test drives
360
350
  * the sink (and the isTTY-strip path) with a fake stream that never touches the real process
361
351
  * streams.
362
352
  *
363
- * @example
353
+ * @example The server — a TTY sink and a process capture
364
354
  * ```ts
365
- * import { createLogger, createReporter, createStyler } from '@src/core'
366
- * import { createServerSink } from '@src/server'
355
+ * import { createStyler, Logger, Reporter } from '@orkestrel/console'
356
+ * import { createServerSink, ProcessCapture } from '@orkestrel/console/server'
357
+ *
358
+ * const sink = createServerSink() // FORCE_COLOR, then NO_COLOR, then isTTY — per target, at construction
359
+ * const styler = createStyler({ enabled: sink.styled }) // keep generated ANSI paired with the sink's stdout stripping
360
+ * const logger = new Logger({ name: 'server', sink, styler })
361
+ * logger.error('boom') // → process.stderr (the error stream)
362
+ * const reporter = new Reporter({ sink, width: sink.columns }) // size the layout to the live terminal
367
363
  *
368
- * const sink = createServerSink()
369
- * const styler = createStyler({ enabled: sink.styled })
370
- * const logger = createLogger({ name: 'app', sink, styler })
371
- * logger.error('boom') // → process.stderr, ANSI rendered on a TTY / stripped to a pipe
372
- * const reporter = createReporter({ sink, width: sink.columns })
364
+ * // `styled` overrides the inference outright — for a CI log that renders ANSI off a TTY, say.
365
+ * const forced = createServerSink({ styled: true })
366
+ * forced.styled // true, whatever the environment and the streams say
367
+ *
368
+ * // Own every output path a direct process.stdout.write, library output, child-process pipes:
369
+ * const capture = new ProcessCapture({ levels: ['stderr'], mirror: true })
370
+ * capture.start()
371
+ * process.stderr.write('a library diagnostic\n') // captured and still shown (mirror: true)
372
+ * capture.messages('stderr') // [{ level: 'stderr', text: 'a library diagnostic\n', time: … }]
373
+ * capture.clear() // drop buffered chunks; interception is unaffected
374
+ * capture.stop()
375
+ * capture.destroy() // stop() then tear down the emitter
373
376
  * ```
374
377
  */
375
378
  function createServerSink(options) {
376
- const out = isStreamTarget(options?.out) ? options.out : process.stdout;
377
- const err = isStreamTarget(options?.err) ? options.err : process.stderr;
379
+ const out = isStreamTarget(options?.stdout) ? options.stdout : process.stdout;
380
+ const err = isStreamTarget(options?.stderr) ? options.stderr : process.stderr;
378
381
  const styled = options?.styled;
379
382
  const environment = options?.environment ?? process.env;
380
383
  const outStyled = styled ?? inferStyled(out, environment);
@@ -383,59 +386,33 @@ function createServerSink(options) {
383
386
  return Object.freeze({
384
387
  styled: outStyled,
385
388
  write(text, level) {
386
- const error = level === "error" || level === "warn";
387
- const target = error ? err : out;
388
- const keep = error ? errStyled : outStyled;
389
+ const target = (0, _src_core.selectWriter)(level, {
390
+ log: out,
391
+ warn: err,
392
+ error: err
393
+ });
394
+ const keep = (0, _src_core.selectWriter)(level, {
395
+ log: outStyled,
396
+ warn: errStyled,
397
+ error: errStyled
398
+ });
389
399
  const line = text.startsWith("\r") ? text : `${text}\n`;
390
400
  target.write(keep ? line : (0, _src_core.stripControls)((0, _src_core.strip)(line)));
391
401
  },
392
402
  get columns() {
393
- return typeof fixed === "number" ? fixed : columnsOf(out);
403
+ return typeof fixed === "number" ? fixed : inferColumns(out);
394
404
  }
395
405
  });
396
406
  }
397
- /**
398
- * Create an observable {@link ProcessCaptureInterface} — the server "own ALL output" capture. It
399
- * intercepts the RAW `process.stdout.write` / `process.stderr.write` (not just `console.*`, which is
400
- * the core `Capture`), so it catches direct `process` writes, library output, and child-process
401
- * pipes. Each intercepted write becomes a frozen {@link import('./types.js').CapturedChunk},
402
- * buffered (bounded, per-stream) and emitted on `capture`; per options it is mirrored back to the
403
- * real stream and/or forwarded to a {@link import('@src/core').SinkInterface}.
404
- *
405
- * @param options - See {@link ProcessCaptureOptions}
406
- * @returns A {@link ProcessCaptureInterface}
407
- *
408
- * @remarks
409
- * - **The wrapper never throws and passes backpressure through** — a throw in `process.stdout.write`
410
- * would crash the host, so chunks are decoded totally and the original's `boolean` is returned.
411
- * - **Snapshot-at-start + non-reentrant + process-global** — `start()` snapshots and swaps the
412
- * pristine `write`; `stop()` restores the EXACT original. At most ONE may be active at a time.
413
- * Create any server sink BEFORE installing a capture so the mirror's replay is not re-captured.
414
- *
415
- * @example
416
- * ```ts
417
- * import { createProcessCapture } from '@src/server'
418
- *
419
- * const capture = createProcessCapture({ levels: ['stderr'], mirror: true })
420
- * capture.start()
421
- * process.stderr.write('a library diagnostic\n') // captured AND still shown
422
- * capture.stop()
423
- * ```
424
- */
425
- function createProcessCapture(options) {
426
- return new ProcessCapture(options);
427
- }
428
407
  //#endregion
429
- exports.DEFAULT_CAPTURE_LEVELS = DEFAULT_CAPTURE_LEVELS;
430
- exports.DEFAULT_CAPTURE_LIMIT = DEFAULT_CAPTURE_LIMIT;
431
408
  exports.DEFAULT_COLUMNS = DEFAULT_COLUMNS;
409
+ exports.DEFAULT_STREAM_LIMIT = DEFAULT_STREAM_LIMIT;
432
410
  exports.ProcessCapture = ProcessCapture;
433
411
  exports.STREAM_LEVELS = STREAM_LEVELS;
434
412
  exports.STREAM_LEVEL_MAP = STREAM_LEVEL_MAP;
435
- exports.columnsOf = columnsOf;
436
- exports.createProcessCapture = createProcessCapture;
437
413
  exports.createServerSink = createServerSink;
438
414
  exports.decodeChunk = decodeChunk;
415
+ exports.inferColumns = inferColumns;
439
416
  exports.inferStyled = inferStyled;
440
417
  exports.isBufferEncoding = isBufferEncoding;
441
418
  exports.isStreamTarget = isStreamTarget;