@orkestrel/console 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/browser/index.d.ts +11 -11
- package/dist/src/browser/index.js +39 -36
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +66 -54
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1 -1
- package/dist/src/core/index.d.ts +1 -1
- package/dist/src/core/index.js +66 -54
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +6 -3
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +11 -11
- package/dist/src/server/index.d.ts +11 -11
- package/dist/src/server/index.js +6 -3
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -15
|
@@ -188,8 +188,8 @@ var ProcessCapture = class {
|
|
|
188
188
|
#active = false;
|
|
189
189
|
constructor(options) {
|
|
190
190
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
191
|
-
on: options
|
|
192
|
-
error: options
|
|
191
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
192
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
193
193
|
});
|
|
194
194
|
this.#levels = options?.levels ?? DEFAULT_CAPTURE_LEVELS;
|
|
195
195
|
this.#mirror = options?.mirror ?? false;
|
|
@@ -211,7 +211,7 @@ var ProcessCapture = class {
|
|
|
211
211
|
const original = stream.write;
|
|
212
212
|
this.#originals.set(level, original);
|
|
213
213
|
const mirror = original.bind(stream);
|
|
214
|
-
stream.write =
|
|
214
|
+
stream.write = this.#captureWrite.bind(this, level, mirror);
|
|
215
215
|
}
|
|
216
216
|
this.#emitter.emit("start");
|
|
217
217
|
}
|
|
@@ -237,6 +237,9 @@ var ProcessCapture = class {
|
|
|
237
237
|
#stream(level) {
|
|
238
238
|
return process[level];
|
|
239
239
|
}
|
|
240
|
+
#captureWrite(level, mirror, chunk, encoding, callback) {
|
|
241
|
+
return this.#intercept(level, chunk, encoding, callback, mirror);
|
|
242
|
+
}
|
|
240
243
|
#intercept(level, chunk, encoding, callback, mirror) {
|
|
241
244
|
const message = this.#capture(level, chunk, encoding);
|
|
242
245
|
this.#retain(message);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["#emitter","#levels","#mirror","#sink","#limit","#messages","#buckets","#originals","#active","#stream","#intercept","#capture","#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).\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 * 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 { Emitter } from '@orkestrel/emitter'\nimport { DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, STREAM_LEVEL_MAP } from './constants.js'\nimport { decodeChunk } 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 the chunk 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 * - **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#active = false\n\n\tconstructor(options?: ProcessCaptureOptions) {\n\t\tthis.#emitter = new Emitter<ProcessCaptureEventMap>({ on: options?.on, error: options?.error })\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 = (\n\t\t\t\tchunk: string | Uint8Array,\n\t\t\t\tencoding?: BufferEncoding | StreamWriteCallback,\n\t\t\t\tcallback?: StreamWriteCallback,\n\t\t\t): boolean => this.#intercept(level, chunk, encoding, callback, mirror)\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\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// The wrapper body behind every patched stream write: build the frozen chunk record, buffer it\n\t// (total + per-stream, bounded), emit `capture`, then — per options — forward to the sink and\n\t// mirror to the real stream. NEVER throws (decodeChunk is total; the emitter isolates listeners);\n\t// the program's own write is replayed through `mirror` (the bound snapshot original) only when the\n\t// `mirror` option is set, and the original's backpressure boolean is returned. Capture-only\n\t// returns `true` (output is swallowed into the buffer, so the kernel buffer never fills). The\n\t// chunk is decoded using `encoding` only (a callback in that slot is ignored for decode); the\n\t// `encoding` / `callback` tail is then forwarded to the mirror BRANCHED on whether the 2nd arg\n\t// is the callback or an encoding (the two Node overloads), so a caller's 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\tconst message = this.#capture(level, chunk, encoding)\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\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// Build the immutable, serializable captured chunk — the chunk decoded to text (total, never\n\t// throws — see decodeChunk; a callback in the encoding slot is ignored, falling back to utf-8),\n\t// stamped with the capture instant. Frozen so a consumer (or the `capture` listener) can never\n\t// mutate it after the fact.\n\t#capture(\n\t\tlevel: StreamLevel,\n\t\tchunk: string | Uint8Array,\n\t\tencoding: BufferEncoding | StreamWriteCallback | undefined,\n\t): CapturedChunk {\n\t\treturn Object.freeze({ level, text: decodeChunk(chunk, encoding), time: Date.now() })\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, 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 is isTTY-aware: it sends ANSI straight to a terminal\n * (which renders it, with a leading `\\r` overwriting the line natively — that is how the C-e\n * animations become a LIVE redraw here, with no extra code) but {@link import('@src/core').strip}s\n * the ANSI to clean text when the stream is piped to a file or another process.\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 * - **isTTY-aware ANSI.** For each write, if the TARGET stream is a TTY the styled `text` is written\n * VERBATIM (the terminal renders the ANSI, and a leading `\\r` overwrites the current line — live\n * animations for free); if it is NOT a TTY (a pipe / redirect to a log file), the ANSI is stripped\n * so the file gets clean text. The decision is per-stream and per-write, re-read live, so it stays\n * correct if a stream's TTY-ness ever differs between out and err.\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 } from '@src/core'\n * import { createServerSink } from '@src/server'\n *\n * const sink = createServerSink()\n * const logger = createLogger({ name: 'app', sink })\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 fixed = options?.columns\n\treturn Object.freeze({\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\tconst target = level === 'error' || level === 'warn' ? err : out\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// On a TTY, write the ANSI verbatim (rendered; a leading `\\r` overwrites natively); off a\n\t\t\t// TTY (a pipe / file), strip ANSI + C0 control codes so the sink delivers clean text.\n\t\t\t// Re-read `isTTY` per write.\n\t\t\ttarget.write(target.isTTY === true ? 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.cjs","names":["#emitter","#levels","#mirror","#sink","#limit","#messages","#buckets","#originals","#active","#stream","#captureWrite","#intercept","#capture","#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).\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 * 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 { Emitter } from '@orkestrel/emitter'\nimport { DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, STREAM_LEVEL_MAP } from './constants.js'\nimport { decodeChunk } 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 the chunk 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 * - **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#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}\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\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: build the frozen chunk record, buffer it\n\t// (total + per-stream, bounded), emit `capture`, then — per options — forward to the sink and\n\t// mirror to the real stream. NEVER throws (decodeChunk is total; the emitter isolates listeners);\n\t// the program's own write is replayed through `mirror` (the bound snapshot original) only when the\n\t// `mirror` option is set, and the original's backpressure boolean is returned. Capture-only\n\t// returns `true` (output is swallowed into the buffer, so the kernel buffer never fills). The\n\t// chunk is decoded using `encoding` only (a callback in that slot is ignored for decode); the\n\t// `encoding` / `callback` tail is then forwarded to the mirror BRANCHED on whether the 2nd arg\n\t// is the callback or an encoding (the two Node overloads), so a caller's 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\tconst message = this.#capture(level, chunk, encoding)\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\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// Build the immutable, serializable captured chunk — the chunk decoded to text (total, never\n\t// throws — see decodeChunk; a callback in the encoding slot is ignored, falling back to utf-8),\n\t// stamped with the capture instant. Frozen so a consumer (or the `capture` listener) can never\n\t// mutate it after the fact.\n\t#capture(\n\t\tlevel: StreamLevel,\n\t\tchunk: string | Uint8Array,\n\t\tencoding: BufferEncoding | StreamWriteCallback | undefined,\n\t): CapturedChunk {\n\t\treturn Object.freeze({ level, text: decodeChunk(chunk, encoding), time: Date.now() })\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, 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 is isTTY-aware: it sends ANSI straight to a terminal\n * (which renders it, with a leading `\\r` overwriting the line natively — that is how the C-e\n * animations become a LIVE redraw here, with no extra code) but {@link import('@src/core').strip}s\n * the ANSI to clean text when the stream is piped to a file or another process.\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 * - **isTTY-aware ANSI.** For each write, if the TARGET stream is a TTY the styled `text` is written\n * VERBATIM (the terminal renders the ANSI, and a leading `\\r` overwrites the current line — live\n * animations for free); if it is NOT a TTY (a pipe / redirect to a log file), the ANSI is stripped\n * so the file gets clean text. The decision is per-stream and per-write, re-read live, so it stays\n * correct if a stream's TTY-ness ever differs between out and err.\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 } from '@src/core'\n * import { createServerSink } from '@src/server'\n *\n * const sink = createServerSink()\n * const logger = createLogger({ name: 'app', sink })\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 fixed = options?.columns\n\treturn Object.freeze({\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\tconst target = level === 'error' || level === 'warn' ? err : out\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// On a TTY, write the ANSI verbatim (rendered; a leading `\\r` overwrites natively); off a\n\t\t\t// TTY (a pipe / file), strip ANSI + C0 control codes so the sink delivers clean text.\n\t\t\t// Re-read `isTTY` per write.\n\t\t\ttarget.write(target.isTTY === true ? 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,8 +1,8 @@
|
|
|
1
1
|
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
2
2
|
import { EmitterHooks } from '@orkestrel/emitter';
|
|
3
3
|
import { EmitterInterface } from '@orkestrel/emitter';
|
|
4
|
-
import { LogLevel } from '../core/index.
|
|
5
|
-
import { SinkInterface } from '../core/index.
|
|
4
|
+
import { LogLevel } from '../core/index.ts';
|
|
5
|
+
import { SinkInterface } from '../core/index.ts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* One intercepted process-stream write — the immutable, serializable record a
|
|
@@ -45,7 +45,7 @@ export declare function columnsOf(target: StreamTargetInterface): number;
|
|
|
45
45
|
* the core `Capture`), so it catches direct `process` writes, library output, and child-process
|
|
46
46
|
* pipes. Each intercepted write becomes a frozen {@link import('./types.js').CapturedChunk},
|
|
47
47
|
* buffered (bounded, per-stream) and emitted on `capture`; per options it is mirrored back to the
|
|
48
|
-
* real stream and/or forwarded to a {@link import('
|
|
48
|
+
* real stream and/or forwarded to a {@link import('@src/core').SinkInterface}.
|
|
49
49
|
*
|
|
50
50
|
* @param options - See {@link ProcessCaptureOptions}
|
|
51
51
|
* @returns A {@link ProcessCaptureInterface}
|
|
@@ -74,11 +74,11 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
74
74
|
* env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`
|
|
75
75
|
* routes by level to the process streams and is isTTY-aware: it sends ANSI straight to a terminal
|
|
76
76
|
* (which renders it, with a leading `\r` overwriting the line natively — that is how the C-e
|
|
77
|
-
* animations become a LIVE redraw here, with no extra code) but {@link import('
|
|
77
|
+
* animations become a LIVE redraw here, with no extra code) but {@link import('@src/core').strip}s
|
|
78
78
|
* the ANSI to clean text when the stream is piped to a file or another process.
|
|
79
79
|
*
|
|
80
80
|
* @param options - See {@link ServerSinkOptions}
|
|
81
|
-
* @returns A {@link ServerSinkInterface} — a {@link import('
|
|
81
|
+
* @returns A {@link ServerSinkInterface} — a {@link import('@src/core').SinkInterface} that also
|
|
82
82
|
* exposes the terminal `columns` width
|
|
83
83
|
*
|
|
84
84
|
* @remarks
|
|
@@ -101,7 +101,7 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
101
101
|
*
|
|
102
102
|
* @example
|
|
103
103
|
* ```ts
|
|
104
|
-
* import { createLogger, createReporter } from '
|
|
104
|
+
* import { createLogger, createReporter } from '@src/core'
|
|
105
105
|
* import { createServerSink } from '@src/server'
|
|
106
106
|
*
|
|
107
107
|
* const sink = createServerSink()
|
|
@@ -335,7 +335,7 @@ export declare interface ProcessCaptureInterface {
|
|
|
335
335
|
* `write` (bound to its stream), so the output still reaches the terminal while being captured;
|
|
336
336
|
* defaults to `false` (capture-only, the program's output is swallowed into the buffer).
|
|
337
337
|
* - `sink` — an optional {@link SinkInterface} each intercepted chunk is also written to
|
|
338
|
-
* (`sink.write(text, level)` with the {@link StreamLevel} mapped to a {@link import('
|
|
338
|
+
* (`sink.write(text, level)` with the {@link StreamLevel} mapped to a {@link import('@src/core').LogLevel} via
|
|
339
339
|
* {@link import('./constants.js').STREAM_LEVEL_MAP}), to tee captured output into the logging
|
|
340
340
|
* pipeline / a file. Absent by default.
|
|
341
341
|
* - `limit` — the bounded-buffer cap (total AND each per-stream bucket); defaults to
|
|
@@ -385,7 +385,7 @@ export declare interface ServerSinkOptions {
|
|
|
385
385
|
/**
|
|
386
386
|
* Each {@link StreamLevel}'s {@link LogLevel} for the optional sink forward — the projection a
|
|
387
387
|
* process capture routes through when writing an intercepted chunk to a
|
|
388
|
-
* {@link import('
|
|
388
|
+
* {@link import('@src/core').SinkInterface}
|
|
389
389
|
* (`sink.write(text, STREAM_LEVEL_MAP[level])`). `stderr` is conventionally the error/diagnostic
|
|
390
390
|
* stream → `error`; `stdout` is the normal output stream → `info`. The source of truth for the
|
|
391
391
|
* stream-to-log projection (the server analogue of the core `CAPTURE_LEVEL_MAP`).
|
|
@@ -403,9 +403,9 @@ export declare const STREAM_LEVELS: readonly StreamLevel[];
|
|
|
403
403
|
* {@link ProcessCaptureInterface}, the server analogue of the core `Capture`'s `CaptureLevel`.
|
|
404
404
|
*
|
|
405
405
|
* @remarks
|
|
406
|
-
* DISTINCT from {@link import('
|
|
406
|
+
* DISTINCT from {@link import('@src/core').LogLevel}: a `StreamLevel` names the ORIGINATING process stream
|
|
407
407
|
* (`process.stdout` vs `process.stderr`), not a severity. It is a named value family (it indexes
|
|
408
|
-
* {@link import('./constants.js').STREAM_LEVEL_MAP} to a {@link import('
|
|
408
|
+
* {@link import('./constants.js').STREAM_LEVEL_MAP} to a {@link import('@src/core').LogLevel} for the optional sink
|
|
409
409
|
* forward), never a binary toggle — so it stays a union (AGENTS §4.4).
|
|
410
410
|
*/
|
|
411
411
|
export declare type StreamLevel = 'stdout' | 'stderr';
|
|
@@ -424,7 +424,7 @@ export declare type StreamLevel = 'stdout' | 'stderr';
|
|
|
424
424
|
* a fake may return `void` (read as truthy / no backpressure).
|
|
425
425
|
* - `isTTY` — present and `true` on a real terminal, absent / `false` when the stream is piped to a
|
|
426
426
|
* file or another process. The sink reads it to decide whether to keep ANSI (a terminal renders
|
|
427
|
-
* it, and a leading `\r` overwrites natively) or {@link import('
|
|
427
|
+
* it, and a leading `\r` overwrites natively) or {@link import('@src/core').strip} it to clean
|
|
428
428
|
* text (a log file should not carry escape codes).
|
|
429
429
|
* - `columns` — the terminal width in character cells when the stream is a TTY, `undefined`
|
|
430
430
|
* otherwise; the sink surfaces it as {@link ServerSinkInterface.columns} so a consumer can feed a
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
2
2
|
import { EmitterHooks } from '@orkestrel/emitter';
|
|
3
3
|
import { EmitterInterface } from '@orkestrel/emitter';
|
|
4
|
-
import { LogLevel } from '../core/index.
|
|
5
|
-
import { SinkInterface } from '../core/index.
|
|
4
|
+
import { LogLevel } from '../core/index.ts';
|
|
5
|
+
import { SinkInterface } from '../core/index.ts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* One intercepted process-stream write — the immutable, serializable record a
|
|
@@ -45,7 +45,7 @@ export declare function columnsOf(target: StreamTargetInterface): number;
|
|
|
45
45
|
* the core `Capture`), so it catches direct `process` writes, library output, and child-process
|
|
46
46
|
* pipes. Each intercepted write becomes a frozen {@link import('./types.js').CapturedChunk},
|
|
47
47
|
* buffered (bounded, per-stream) and emitted on `capture`; per options it is mirrored back to the
|
|
48
|
-
* real stream and/or forwarded to a {@link import('
|
|
48
|
+
* real stream and/or forwarded to a {@link import('@src/core').SinkInterface}.
|
|
49
49
|
*
|
|
50
50
|
* @param options - See {@link ProcessCaptureOptions}
|
|
51
51
|
* @returns A {@link ProcessCaptureInterface}
|
|
@@ -74,11 +74,11 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
74
74
|
* env-symmetric sibling of `createBrowserSink` / core's `createConsoleSink`. `write(text, level?)`
|
|
75
75
|
* routes by level to the process streams and is isTTY-aware: it sends ANSI straight to a terminal
|
|
76
76
|
* (which renders it, with a leading `\r` overwriting the line natively — that is how the C-e
|
|
77
|
-
* animations become a LIVE redraw here, with no extra code) but {@link import('
|
|
77
|
+
* animations become a LIVE redraw here, with no extra code) but {@link import('@src/core').strip}s
|
|
78
78
|
* the ANSI to clean text when the stream is piped to a file or another process.
|
|
79
79
|
*
|
|
80
80
|
* @param options - See {@link ServerSinkOptions}
|
|
81
|
-
* @returns A {@link ServerSinkInterface} — a {@link import('
|
|
81
|
+
* @returns A {@link ServerSinkInterface} — a {@link import('@src/core').SinkInterface} that also
|
|
82
82
|
* exposes the terminal `columns` width
|
|
83
83
|
*
|
|
84
84
|
* @remarks
|
|
@@ -101,7 +101,7 @@ export declare function createProcessCapture(options?: ProcessCaptureOptions): P
|
|
|
101
101
|
*
|
|
102
102
|
* @example
|
|
103
103
|
* ```ts
|
|
104
|
-
* import { createLogger, createReporter } from '
|
|
104
|
+
* import { createLogger, createReporter } from '@src/core'
|
|
105
105
|
* import { createServerSink } from '@src/server'
|
|
106
106
|
*
|
|
107
107
|
* const sink = createServerSink()
|
|
@@ -335,7 +335,7 @@ export declare interface ProcessCaptureInterface {
|
|
|
335
335
|
* `write` (bound to its stream), so the output still reaches the terminal while being captured;
|
|
336
336
|
* defaults to `false` (capture-only, the program's output is swallowed into the buffer).
|
|
337
337
|
* - `sink` — an optional {@link SinkInterface} each intercepted chunk is also written to
|
|
338
|
-
* (`sink.write(text, level)` with the {@link StreamLevel} mapped to a {@link import('
|
|
338
|
+
* (`sink.write(text, level)` with the {@link StreamLevel} mapped to a {@link import('@src/core').LogLevel} via
|
|
339
339
|
* {@link import('./constants.js').STREAM_LEVEL_MAP}), to tee captured output into the logging
|
|
340
340
|
* pipeline / a file. Absent by default.
|
|
341
341
|
* - `limit` — the bounded-buffer cap (total AND each per-stream bucket); defaults to
|
|
@@ -385,7 +385,7 @@ export declare interface ServerSinkOptions {
|
|
|
385
385
|
/**
|
|
386
386
|
* Each {@link StreamLevel}'s {@link LogLevel} for the optional sink forward — the projection a
|
|
387
387
|
* process capture routes through when writing an intercepted chunk to a
|
|
388
|
-
* {@link import('
|
|
388
|
+
* {@link import('@src/core').SinkInterface}
|
|
389
389
|
* (`sink.write(text, STREAM_LEVEL_MAP[level])`). `stderr` is conventionally the error/diagnostic
|
|
390
390
|
* stream → `error`; `stdout` is the normal output stream → `info`. The source of truth for the
|
|
391
391
|
* stream-to-log projection (the server analogue of the core `CAPTURE_LEVEL_MAP`).
|
|
@@ -403,9 +403,9 @@ export declare const STREAM_LEVELS: readonly StreamLevel[];
|
|
|
403
403
|
* {@link ProcessCaptureInterface}, the server analogue of the core `Capture`'s `CaptureLevel`.
|
|
404
404
|
*
|
|
405
405
|
* @remarks
|
|
406
|
-
* DISTINCT from {@link import('
|
|
406
|
+
* DISTINCT from {@link import('@src/core').LogLevel}: a `StreamLevel` names the ORIGINATING process stream
|
|
407
407
|
* (`process.stdout` vs `process.stderr`), not a severity. It is a named value family (it indexes
|
|
408
|
-
* {@link import('./constants.js').STREAM_LEVEL_MAP} to a {@link import('
|
|
408
|
+
* {@link import('./constants.js').STREAM_LEVEL_MAP} to a {@link import('@src/core').LogLevel} for the optional sink
|
|
409
409
|
* forward), never a binary toggle — so it stays a union (AGENTS §4.4).
|
|
410
410
|
*/
|
|
411
411
|
export declare type StreamLevel = 'stdout' | 'stderr';
|
|
@@ -424,7 +424,7 @@ export declare type StreamLevel = 'stdout' | 'stderr';
|
|
|
424
424
|
* a fake may return `void` (read as truthy / no backpressure).
|
|
425
425
|
* - `isTTY` — present and `true` on a real terminal, absent / `false` when the stream is piped to a
|
|
426
426
|
* file or another process. The sink reads it to decide whether to keep ANSI (a terminal renders
|
|
427
|
-
* it, and a leading `\r` overwrites natively) or {@link import('
|
|
427
|
+
* it, and a leading `\r` overwrites natively) or {@link import('@src/core').strip} it to clean
|
|
428
428
|
* text (a log file should not carry escape codes).
|
|
429
429
|
* - `columns` — the terminal width in character cells when the stream is a TTY, `undefined`
|
|
430
430
|
* otherwise; the sink surfaces it as {@link ServerSinkInterface.columns} so a consumer can feed a
|
package/dist/src/server/index.js
CHANGED
|
@@ -187,8 +187,8 @@ var ProcessCapture = class {
|
|
|
187
187
|
#active = false;
|
|
188
188
|
constructor(options) {
|
|
189
189
|
this.#emitter = new Emitter({
|
|
190
|
-
on: options
|
|
191
|
-
error: options
|
|
190
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
191
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
192
192
|
});
|
|
193
193
|
this.#levels = options?.levels ?? DEFAULT_CAPTURE_LEVELS;
|
|
194
194
|
this.#mirror = options?.mirror ?? false;
|
|
@@ -210,7 +210,7 @@ var ProcessCapture = class {
|
|
|
210
210
|
const original = stream.write;
|
|
211
211
|
this.#originals.set(level, original);
|
|
212
212
|
const mirror = original.bind(stream);
|
|
213
|
-
stream.write =
|
|
213
|
+
stream.write = this.#captureWrite.bind(this, level, mirror);
|
|
214
214
|
}
|
|
215
215
|
this.#emitter.emit("start");
|
|
216
216
|
}
|
|
@@ -236,6 +236,9 @@ var ProcessCapture = class {
|
|
|
236
236
|
#stream(level) {
|
|
237
237
|
return process[level];
|
|
238
238
|
}
|
|
239
|
+
#captureWrite(level, mirror, chunk, encoding, callback) {
|
|
240
|
+
return this.#intercept(level, chunk, encoding, callback, mirror);
|
|
241
|
+
}
|
|
239
242
|
#intercept(level, chunk, encoding, callback, mirror) {
|
|
240
243
|
const message = this.#capture(level, chunk, encoding);
|
|
241
244
|
this.#retain(message);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#emitter","#levels","#mirror","#sink","#limit","#messages","#buckets","#originals","#active","#stream","#intercept","#capture","#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).\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 * 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 { Emitter } from '@orkestrel/emitter'\nimport { DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, STREAM_LEVEL_MAP } from './constants.js'\nimport { decodeChunk } 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 the chunk 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 * - **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#active = false\n\n\tconstructor(options?: ProcessCaptureOptions) {\n\t\tthis.#emitter = new Emitter<ProcessCaptureEventMap>({ on: options?.on, error: options?.error })\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 = (\n\t\t\t\tchunk: string | Uint8Array,\n\t\t\t\tencoding?: BufferEncoding | StreamWriteCallback,\n\t\t\t\tcallback?: StreamWriteCallback,\n\t\t\t): boolean => this.#intercept(level, chunk, encoding, callback, mirror)\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\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// The wrapper body behind every patched stream write: build the frozen chunk record, buffer it\n\t// (total + per-stream, bounded), emit `capture`, then — per options — forward to the sink and\n\t// mirror to the real stream. NEVER throws (decodeChunk is total; the emitter isolates listeners);\n\t// the program's own write is replayed through `mirror` (the bound snapshot original) only when the\n\t// `mirror` option is set, and the original's backpressure boolean is returned. Capture-only\n\t// returns `true` (output is swallowed into the buffer, so the kernel buffer never fills). The\n\t// chunk is decoded using `encoding` only (a callback in that slot is ignored for decode); the\n\t// `encoding` / `callback` tail is then forwarded to the mirror BRANCHED on whether the 2nd arg\n\t// is the callback or an encoding (the two Node overloads), so a caller's 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\tconst message = this.#capture(level, chunk, encoding)\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\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// Build the immutable, serializable captured chunk — the chunk decoded to text (total, never\n\t// throws — see decodeChunk; a callback in the encoding slot is ignored, falling back to utf-8),\n\t// stamped with the capture instant. Frozen so a consumer (or the `capture` listener) can never\n\t// mutate it after the fact.\n\t#capture(\n\t\tlevel: StreamLevel,\n\t\tchunk: string | Uint8Array,\n\t\tencoding: BufferEncoding | StreamWriteCallback | undefined,\n\t): CapturedChunk {\n\t\treturn Object.freeze({ level, text: decodeChunk(chunk, encoding), time: Date.now() })\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, 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 is isTTY-aware: it sends ANSI straight to a terminal\n * (which renders it, with a leading `\\r` overwriting the line natively — that is how the C-e\n * animations become a LIVE redraw here, with no extra code) but {@link import('@src/core').strip}s\n * the ANSI to clean text when the stream is piped to a file or another process.\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 * - **isTTY-aware ANSI.** For each write, if the TARGET stream is a TTY the styled `text` is written\n * VERBATIM (the terminal renders the ANSI, and a leading `\\r` overwrites the current line — live\n * animations for free); if it is NOT a TTY (a pipe / redirect to a log file), the ANSI is stripped\n * so the file gets clean text. The decision is per-stream and per-write, re-read live, so it stays\n * correct if a stream's TTY-ness ever differs between out and err.\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 } from '@src/core'\n * import { createServerSink } from '@src/server'\n *\n * const sink = createServerSink()\n * const logger = createLogger({ name: 'app', sink })\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 fixed = options?.columns\n\treturn Object.freeze({\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\tconst target = level === 'error' || level === 'warn' ? err : out\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// On a TTY, write the ANSI verbatim (rendered; a leading `\\r` overwrites natively); off a\n\t\t\t// TTY (a pipe / file), strip ANSI + C0 control codes so the sink delivers clean text.\n\t\t\t// Re-read `isTTY` per write.\n\t\t\ttarget.write(target.isTTY === true ? 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":["#emitter","#levels","#mirror","#sink","#limit","#messages","#buckets","#originals","#active","#stream","#captureWrite","#intercept","#capture","#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).\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 * 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 { Emitter } from '@orkestrel/emitter'\nimport { DEFAULT_CAPTURE_LEVELS, DEFAULT_CAPTURE_LIMIT, STREAM_LEVEL_MAP } from './constants.js'\nimport { decodeChunk } 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 the chunk 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 * - **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#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}\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\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: build the frozen chunk record, buffer it\n\t// (total + per-stream, bounded), emit `capture`, then — per options — forward to the sink and\n\t// mirror to the real stream. NEVER throws (decodeChunk is total; the emitter isolates listeners);\n\t// the program's own write is replayed through `mirror` (the bound snapshot original) only when the\n\t// `mirror` option is set, and the original's backpressure boolean is returned. Capture-only\n\t// returns `true` (output is swallowed into the buffer, so the kernel buffer never fills). The\n\t// chunk is decoded using `encoding` only (a callback in that slot is ignored for decode); the\n\t// `encoding` / `callback` tail is then forwarded to the mirror BRANCHED on whether the 2nd arg\n\t// is the callback or an encoding (the two Node overloads), so a caller's 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\tconst message = this.#capture(level, chunk, encoding)\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\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// Build the immutable, serializable captured chunk — the chunk decoded to text (total, never\n\t// throws — see decodeChunk; a callback in the encoding slot is ignored, falling back to utf-8),\n\t// stamped with the capture instant. Frozen so a consumer (or the `capture` listener) can never\n\t// mutate it after the fact.\n\t#capture(\n\t\tlevel: StreamLevel,\n\t\tchunk: string | Uint8Array,\n\t\tencoding: BufferEncoding | StreamWriteCallback | undefined,\n\t): CapturedChunk {\n\t\treturn Object.freeze({ level, text: decodeChunk(chunk, encoding), time: Date.now() })\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, 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 is isTTY-aware: it sends ANSI straight to a terminal\n * (which renders it, with a leading `\\r` overwriting the line natively — that is how the C-e\n * animations become a LIVE redraw here, with no extra code) but {@link import('@src/core').strip}s\n * the ANSI to clean text when the stream is piped to a file or another process.\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 * - **isTTY-aware ANSI.** For each write, if the TARGET stream is a TTY the styled `text` is written\n * VERBATIM (the terminal renders the ANSI, and a leading `\\r` overwrites the current line — live\n * animations for free); if it is NOT a TTY (a pipe / redirect to a log file), the ANSI is stripped\n * so the file gets clean text. The decision is per-stream and per-write, re-read live, so it stays\n * correct if a stream's TTY-ness ever differs between out and err.\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 } from '@src/core'\n * import { createServerSink } from '@src/server'\n *\n * const sink = createServerSink()\n * const logger = createLogger({ name: 'app', sink })\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 fixed = options?.columns\n\treturn Object.freeze({\n\t\twrite(text: string, level?: LogLevel): void {\n\t\t\tconst target = level === 'error' || level === 'warn' ? err : out\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// On a TTY, write the ANSI verbatim (rendered; a leading `\\r` overwrites natively); off a\n\t\t\t// TTY (a pipe / file), strip ANSI + C0 control codes so the sink delivers clean text.\n\t\t\t// Re-read `isTTY` per write.\n\t\t\ttarget.write(target.isTTY === true ? 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/console",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "A typed console/terminal output toolkit for the @orkestrel line — logging, spinners, progress, and capture. Part of the @orkestrel line.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"url": "git+https://github.com/orkestrel/console.git"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
-
"dist",
|
|
22
|
+
"dist/src",
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"type": "module",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"access": "public"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
|
-
"clean": "node -e \"
|
|
62
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
63
63
|
"copy": "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
|
|
64
|
-
"
|
|
65
|
-
"lint": "oxlint --config .oxlintrc.json --fix .",
|
|
64
|
+
"scaffold": "scaffold",
|
|
65
|
+
"lint": "oxlint --config .oxlintrc.json --fix --deny-warnings .",
|
|
66
66
|
"check": "tsc --noEmit --project tsconfig.json && npm run check:src",
|
|
67
67
|
"check:src": "npm run check:src:core && npm run check:src:browser && npm run check:src:server",
|
|
68
68
|
"check:src:core": "tsc --noEmit -p configs/src/tsconfig.core.json",
|
|
@@ -70,12 +70,13 @@
|
|
|
70
70
|
"check:src:server": "tsc --noEmit -p configs/src/tsconfig.server.json",
|
|
71
71
|
"format": "oxfmt --config .oxfmtrc.json --write .",
|
|
72
72
|
"format:check": "oxfmt --config .oxfmtrc.json --check .",
|
|
73
|
-
"lint:check": "oxlint --config .oxlintrc.json .",
|
|
74
|
-
"test": "npm run test:src && npm run test:guides",
|
|
73
|
+
"lint:check": "oxlint --config .oxlintrc.json --deny-warnings .",
|
|
74
|
+
"test": "npm run test:src && npm run test:policy && npm run test:guides",
|
|
75
75
|
"test:src": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core --project src:browser --project src:server",
|
|
76
76
|
"test:src:core": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
|
|
77
77
|
"test:src:browser": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:browser",
|
|
78
78
|
"test:src:server": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:server",
|
|
79
|
+
"test:policy": "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy",
|
|
79
80
|
"test:guides": "vitest run --config vite.config.ts --reporter=dot --project guides",
|
|
80
81
|
"build": "npm run clean && npm run build:src",
|
|
81
82
|
"build:src": "npm run build:src:core && npm run build:src:browser && npm run build:src:server",
|
|
@@ -85,22 +86,24 @@
|
|
|
85
86
|
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
|
|
86
87
|
},
|
|
87
88
|
"dependencies": {
|
|
88
|
-
"@orkestrel/contract": "^0.0.
|
|
89
|
-
"@orkestrel/emitter": "^0.0.
|
|
89
|
+
"@orkestrel/contract": "^0.0.11",
|
|
90
|
+
"@orkestrel/emitter": "^0.0.6"
|
|
90
91
|
},
|
|
91
92
|
"devDependencies": {
|
|
92
|
-
"@microsoft/api-extractor": "^7.58.
|
|
93
|
-
"@orkestrel/guide": "^0.0.
|
|
94
|
-
"@
|
|
93
|
+
"@microsoft/api-extractor": "^7.58.12",
|
|
94
|
+
"@orkestrel/guide": "^0.0.9",
|
|
95
|
+
"@orkestrel/scaffold": "^0.0.26",
|
|
96
|
+
"@types/node": "^26.1.2",
|
|
95
97
|
"@vitest/browser-playwright": "^4.1.10",
|
|
96
|
-
"oxfmt": "^0.
|
|
97
|
-
"oxlint": "^1.
|
|
98
|
+
"oxfmt": "^0.61.0",
|
|
99
|
+
"oxlint": "^1.76.0",
|
|
100
|
+
"playwright": "^1.62.0",
|
|
98
101
|
"typescript": "^6.0.3",
|
|
99
102
|
"vite": "^8.1.5",
|
|
100
103
|
"vite-plugin-dts": "^5.0.3",
|
|
101
104
|
"vitest": "^4.1.10"
|
|
102
105
|
},
|
|
103
106
|
"engines": {
|
|
104
|
-
"node": ">=22"
|
|
107
|
+
"node": ">=22.12.0"
|
|
105
108
|
}
|
|
106
109
|
}
|