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