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