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