@orkestrel/process 0.0.1

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.
@@ -0,0 +1,717 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_buffer = require("node:buffer");
3
+ let node_child_process = require("node:child_process");
4
+ let node_path = require("node:path");
5
+ let _src_core = require("../core/index.cjs");
6
+ let node_readline = require("node:readline");
7
+ let node_string_decoder = require("node:string_decoder");
8
+ let _orkestrel_emitter = require("@orkestrel/emitter");
9
+ //#region src/server/helpers.ts
10
+ /**
11
+ * Signals one owned child process, or its detached process group on a POSIX host.
12
+ *
13
+ * @remarks
14
+ * On a POSIX host a child spawned `detached` leads its own process group, so signalling the
15
+ * negated pid reaches the whole tree. On Windows, or when no pid is available, the signal goes to
16
+ * the child directly. A throw during signalling is swallowed: the process can exit and win the
17
+ * signal race, and the child's close event remains the authoritative terminal state.
18
+ *
19
+ * @param child - The child process signal boundary
20
+ * @param signal - The signal to deliver
21
+ * @returns Nothing
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * killProcess(child, 'SIGTERM')
26
+ * ```
27
+ */
28
+ function killProcess(child, signal) {
29
+ try {
30
+ if (process.platform === "win32" || child.pid === void 0) {
31
+ child.kill(signal);
32
+ return;
33
+ }
34
+ process.kill(-child.pid, signal);
35
+ } catch {}
36
+ }
37
+ /**
38
+ * Trims a buffer to at most `limit` trailing bytes without splitting a UTF-8 sequence.
39
+ *
40
+ * @remarks
41
+ * Keeps the tail — the diagnostic end of a stderr stream. When the cut point lands inside a
42
+ * multibyte sequence, the start advances past the leading continuation bytes so the retained
43
+ * bytes always begin on a code-point boundary.
44
+ *
45
+ * @param buffer - The accumulated bytes
46
+ * @param limit - The maximum retained byte count
47
+ * @returns The trailing bytes, at most `limit` and never starting mid-sequence
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * trimTail(Buffer.from('hello'), 3) // <Buffer 6c 6c 6f>
52
+ * ```
53
+ */
54
+ function trimTail(buffer, limit) {
55
+ if (buffer.byteLength <= limit) return buffer;
56
+ let start = buffer.byteLength - limit;
57
+ while (start < buffer.byteLength) {
58
+ const byte = buffer[start];
59
+ if (byte === void 0 || (byte & 192) !== 128) break;
60
+ start += 1;
61
+ }
62
+ return node_buffer.Buffer.from(buffer.subarray(start));
63
+ }
64
+ /**
65
+ * Trims a buffer to at most `limit` leading bytes without splitting a UTF-8 sequence.
66
+ *
67
+ * @remarks
68
+ * Keeps the head — the captured start of a one-shot run's output. When the cut point lands inside
69
+ * a multibyte sequence, the end retreats to the sequence's start so the retained bytes always end
70
+ * on a code-point boundary and decode without a replacement character.
71
+ *
72
+ * @param buffer - The accumulated bytes
73
+ * @param limit - The maximum retained byte count
74
+ * @returns The leading bytes, at most `limit` and never ending mid-sequence
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * trimHead(Buffer.from('hello'), 3) // <Buffer 68 65 6c>
79
+ * ```
80
+ */
81
+ function trimHead(buffer, limit) {
82
+ if (buffer.byteLength <= limit) return buffer;
83
+ let end = limit;
84
+ while (end > 0) {
85
+ const byte = buffer[end];
86
+ if (byte === void 0 || (byte & 192) !== 128) break;
87
+ end -= 1;
88
+ }
89
+ return node_buffer.Buffer.from(buffer.subarray(0, end));
90
+ }
91
+ /**
92
+ * Checks whether a command file must be spawned through a shell on the current host.
93
+ *
94
+ * @remarks
95
+ * Node v22+ refuses to spawn a Windows `.cmd` or `.bat` batch file directly and returns `EINVAL`,
96
+ * and a bare command name such as `git` resolves through `PATHEXT` to a batch shim rather than an
97
+ * executable. Both cases run through the shell. A POSIX host, and a Windows path carrying a real
98
+ * executable extension such as `.exe`, spawn directly. When the result is `true`, treat the
99
+ * command file and arguments as trusted: shell invocation exposes shell metacharacters.
100
+ *
101
+ * @param file - The command executable name or path
102
+ * @returns True when the file must run through a shell; false when it spawns directly
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * requiresShell('git') // true on Windows, false on POSIX
107
+ * ```
108
+ */
109
+ function requiresShell(file) {
110
+ if (process.platform !== "win32") return false;
111
+ const lower = file.toLowerCase();
112
+ return lower.endsWith(".cmd") || lower.endsWith(".bat") || (0, node_path.extname)(file) === "";
113
+ }
114
+ /**
115
+ * Renders one command into its diagnostic command line.
116
+ *
117
+ * @param command - The executable and its argument vector
118
+ * @returns The space-joined command line, for a {@link RunResult} and error messages
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * commandLine({ file: 'git', arguments: ['status'] }) // 'git status'
123
+ * ```
124
+ */
125
+ function commandLine(command) {
126
+ return [command.file, ...command.arguments].join(" ");
127
+ }
128
+ /**
129
+ * Merges environment overrides over the current process environment.
130
+ *
131
+ * @remarks
132
+ * Later maps override earlier ones and an `undefined` value unsets a key. The base is the parent
133
+ * process environment, so a child inherits it unless a key is overridden or unset.
134
+ *
135
+ * @param base - The command's own environment overrides
136
+ * @param override - Per-invocation overrides applied last
137
+ * @returns The merged environment for a spawn
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * mergeEnvironment({ TOKEN: 'a' }, { TOKEN: undefined }) // TOKEN unset
142
+ * ```
143
+ */
144
+ function mergeEnvironment(base, override) {
145
+ return {
146
+ ...process.env,
147
+ ...base,
148
+ ...override
149
+ };
150
+ }
151
+ /**
152
+ * Builds one settled {@link RunResult} from a completed one-shot run's captured bytes and exit.
153
+ *
154
+ * @remarks
155
+ * `failed` is derived: a run failed when it timed out, was ended by a signal, or exited with a
156
+ * code other than `0` — a `null` code from a spawn fault is therefore a failure. Both output
157
+ * buffers are byte-bounded by `limit` before decoding.
158
+ *
159
+ * @param command - The diagnostic command line
160
+ * @param stdout - The captured standard output bytes
161
+ * @param stderr - The captured standard error bytes
162
+ * @param code - The exit code, or `null` when a signal or spawn fault ended the run
163
+ * @param signal - The terminating signal, or `null`
164
+ * @param timedOut - If `true`, the run's own timeout elapsed; if `false`, it did not
165
+ * @param limit - The maximum captured bytes for each stream
166
+ * @returns The frozen run outcome
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * buildRunResult('git status', Buffer.from('ok'), Buffer.alloc(0), 0, null, false, 1024)
171
+ * ```
172
+ */
173
+ function buildRunResult(command, stdout, stderr, code, signal, timedOut, limit) {
174
+ return Object.freeze({
175
+ command,
176
+ stdout: trimHead(stdout, limit).toString("utf8"),
177
+ stderr: trimHead(stderr, limit).toString("utf8"),
178
+ code,
179
+ signal,
180
+ failed: timedOut || signal !== null || code !== 0,
181
+ timedOut
182
+ });
183
+ }
184
+ /**
185
+ * Runs one command to completion, buffering its output, and settles with the outcome.
186
+ *
187
+ * @remarks
188
+ * The child is spawned through the cross-platform command resolver ({@link requiresShell}) and, on
189
+ * a POSIX host, detached so its whole process group can be terminated. Standard output and error
190
+ * are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}). A positive `timeout` arms a
191
+ * timer that terminates the child through `SIGTERM`, then `SIGKILL` after `grace`
192
+ * (default {@link PROCESS_GRACE}); aborting `signal` terminates the same way. The child's
193
+ * `environment` merges over the parent, then `options.environment` on top, and `options.input`
194
+ * overrides `command.input`. Unless `reject` is `false`, a failed run rejects with a
195
+ * {@link createRunError} carrying the {@link RunResult}.
196
+ *
197
+ * @param command - The executable, arguments, and optional environment and input
198
+ * @param options - Working directory, timeout, grace, signal, capture limit, and reject behavior
199
+ * @returns The settled run outcome
200
+ * @throws A {@link ProcessError} carrying the {@link RunResult} when the run failed and `reject` is not `false`
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * const result = await run({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
205
+ * ```
206
+ */
207
+ async function run(command, options) {
208
+ const limit = options?.limit ?? _src_core.PROCESS_OUTPUT;
209
+ const grace = options?.grace ?? _src_core.PROCESS_GRACE;
210
+ const timeout = options?.timeout ?? 0;
211
+ const reject = options?.reject ?? true;
212
+ const input = options?.input ?? command.input;
213
+ const line = commandLine(command);
214
+ const settled = Promise.withResolvers();
215
+ const terminate = new AbortController();
216
+ const cleanup = new AbortController();
217
+ let stdout = node_buffer.Buffer.alloc(0);
218
+ let stderr = node_buffer.Buffer.alloc(0);
219
+ let timedOut = false;
220
+ let killTimer;
221
+ let timeoutTimer;
222
+ let done = false;
223
+ const child = (0, node_child_process.spawn)(command.file, [...command.arguments], {
224
+ cwd: options?.workspace ?? process.cwd(),
225
+ detached: process.platform !== "win32",
226
+ env: mergeEnvironment(command.environment, options?.environment),
227
+ stdio: [
228
+ "pipe",
229
+ "pipe",
230
+ "pipe"
231
+ ],
232
+ shell: requiresShell(command.file),
233
+ windowsHide: true
234
+ });
235
+ terminate.signal.addEventListener("abort", () => {
236
+ killProcess(child, "SIGTERM");
237
+ killTimer = setTimeout(() => killProcess(child, "SIGKILL"), grace);
238
+ }, { once: true });
239
+ child.stdin.on("error", () => void 0);
240
+ child.stdout.on("data", (chunk) => {
241
+ if (node_buffer.Buffer.isBuffer(chunk)) stdout = trimHead(node_buffer.Buffer.concat([stdout, chunk]), limit);
242
+ });
243
+ child.stderr.on("data", (chunk) => {
244
+ if (node_buffer.Buffer.isBuffer(chunk)) stderr = trimHead(node_buffer.Buffer.concat([stderr, chunk]), limit);
245
+ });
246
+ child.once("error", () => {
247
+ if (done) return;
248
+ done = true;
249
+ cleanup.abort();
250
+ clearTimeout(killTimer);
251
+ clearTimeout(timeoutTimer);
252
+ settled.resolve(buildRunResult(line, stdout, stderr, null, null, false, limit));
253
+ });
254
+ child.once("close", (code, signal) => {
255
+ if (done) return;
256
+ done = true;
257
+ cleanup.abort();
258
+ clearTimeout(killTimer);
259
+ clearTimeout(timeoutTimer);
260
+ settled.resolve(buildRunResult(line, stdout, stderr, code, signal, timedOut, limit));
261
+ });
262
+ if (input !== void 0) child.stdin.write(input);
263
+ child.stdin.end();
264
+ if (timeout > 0) timeoutTimer = setTimeout(() => {
265
+ timedOut = true;
266
+ terminate.abort();
267
+ }, timeout);
268
+ if (options?.signal !== void 0) {
269
+ const signal = options.signal;
270
+ signal.addEventListener("abort", () => terminate.abort(), {
271
+ once: true,
272
+ signal: cleanup.signal
273
+ });
274
+ if (signal.aborted) terminate.abort();
275
+ }
276
+ const result = await settled.promise;
277
+ if (result.failed && reject) throw (0, _src_core.createRunError)(result);
278
+ return result;
279
+ }
280
+ /**
281
+ * Runs one command to completion synchronously, buffering its output, and returns the outcome.
282
+ *
283
+ * @remarks
284
+ * The synchronous counterpart of {@link run}. The child is spawned through the same cross-platform
285
+ * command resolver ({@link requiresShell}). Standard output and error are each byte-bounded by
286
+ * `limit` (default {@link PROCESS_OUTPUT}). A positive `timeout` is enforced by the host, which
287
+ * ends the child with `SIGKILL` when it elapses. The environment and input follow the same merge
288
+ * as {@link run}. Unless `reject` is `false`, a failed run throws a {@link createRunError} carrying
289
+ * the {@link RunResult}.
290
+ *
291
+ * @param command - The executable, arguments, and optional environment and input
292
+ * @param options - Working directory, timeout, signal, capture limit, and reject behavior
293
+ * @returns The run outcome
294
+ * @throws A {@link ProcessError} carrying the {@link RunResult} when the run failed and `reject` is not `false`
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * const result = runSync({ file: 'git', arguments: ['--version'] }, { reject: false })
299
+ * ```
300
+ */
301
+ function runSync(command, options) {
302
+ const limit = options?.limit ?? _src_core.PROCESS_OUTPUT;
303
+ const timeout = options?.timeout ?? 0;
304
+ const reject = options?.reject ?? true;
305
+ const input = options?.input ?? command.input;
306
+ const line = commandLine(command);
307
+ const outcome = (0, node_child_process.spawnSync)(command.file, [...command.arguments], {
308
+ cwd: options?.workspace ?? process.cwd(),
309
+ env: mergeEnvironment(command.environment, options?.environment),
310
+ encoding: "buffer",
311
+ maxBuffer: limit,
312
+ shell: requiresShell(command.file),
313
+ windowsHide: true,
314
+ ...input !== void 0 ? { input } : {},
315
+ ...options?.signal !== void 0 ? { signal: options.signal } : {},
316
+ ...timeout > 0 ? {
317
+ timeout,
318
+ killSignal: "SIGKILL"
319
+ } : {}
320
+ });
321
+ const error = outcome.error;
322
+ const timedOut = error !== void 0 && "code" in error && error.code === "ETIMEDOUT";
323
+ const result = buildRunResult(line, node_buffer.Buffer.isBuffer(outcome.stdout) ? outcome.stdout : node_buffer.Buffer.alloc(0), node_buffer.Buffer.isBuffer(outcome.stderr) ? outcome.stderr : node_buffer.Buffer.alloc(0), outcome.status, outcome.signal, timedOut, limit);
324
+ if (result.failed && reject) throw (0, _src_core.createRunError)(result);
325
+ return result;
326
+ }
327
+ //#endregion
328
+ //#region src/server/Process.ts
329
+ /**
330
+ * One supervised child process with eagerly framed output and bounded termination.
331
+ *
332
+ * @remarks
333
+ * Standard output is drained eagerly through `readline`, so `exit` resolves and a late consumer of
334
+ * `lines` still receives every framed line, including a final line written without a trailing
335
+ * newline. Standard error is decoded and forwarded live as the `stderr` event while a byte-bounded
336
+ * raw tail is retained as `evidence`. The typed `emitter` also carries the terminal `exit`,
337
+ * alongside the `exit` promise. `stop` terminates the child through `SIGTERM`, then `SIGKILL` after
338
+ * `grace`, and is idempotent; `destroy` stops the child and destroys the emitter last.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * import { Process } from '@orkestrel/process/server'
343
+ *
344
+ * const child = new Process({
345
+ * command: { file: 'node', arguments: ['--version'] },
346
+ * workspace: process.cwd(),
347
+ * grace: 5000,
348
+ * })
349
+ * const exit = await child.exit
350
+ * await child.destroy()
351
+ * ```
352
+ */
353
+ var Process = class {
354
+ #emitter;
355
+ #child;
356
+ #reader;
357
+ #grace;
358
+ #limit;
359
+ #decoder = new node_string_decoder.StringDecoder("utf8");
360
+ #exit = Promise.withResolvers();
361
+ #lines;
362
+ #buffer = [];
363
+ #waiters = [];
364
+ #signal;
365
+ #abort;
366
+ #tail = node_buffer.Buffer.alloc(0);
367
+ #closed = false;
368
+ #ended = false;
369
+ #stopping;
370
+ #ending;
371
+ /**
372
+ * Spawn one child process and begin eager stream capture.
373
+ *
374
+ * @param options - Command, workspace, termination, evidence, stdin, and observation settings
375
+ */
376
+ constructor(options) {
377
+ const on = options.on;
378
+ const error = options.error;
379
+ this.#emitter = new _orkestrel_emitter.Emitter({
380
+ ...on === void 0 ? {} : { on },
381
+ ...error === void 0 ? {} : { error }
382
+ });
383
+ this.#grace = options.grace;
384
+ this.#limit = options.evidence ?? _src_core.PROCESS_EVIDENCE;
385
+ this.#signal = options.signal;
386
+ this.#lines = Object.freeze({ [Symbol.asyncIterator]: this.#iterate.bind(this) });
387
+ this.#child = (0, node_child_process.spawn)(options.command.file, [...options.command.arguments], {
388
+ cwd: options.workspace,
389
+ detached: process.platform !== "win32",
390
+ env: mergeEnvironment(options.command.environment),
391
+ stdio: [
392
+ "pipe",
393
+ "pipe",
394
+ "pipe"
395
+ ],
396
+ windowsHide: true
397
+ });
398
+ this.#reader = (0, node_readline.createInterface)({
399
+ input: this.#child.stdout,
400
+ crlfDelay: Infinity
401
+ });
402
+ this.#reader.on("line", this.#push.bind(this));
403
+ this.#reader.once("close", this.#finish.bind(this));
404
+ this.#child.once("error", () => void 0);
405
+ this.#child.once("close", this.#close.bind(this));
406
+ this.#child.stdin.on("error", () => void 0);
407
+ this.#child.stderr.on("data", this.#retain.bind(this));
408
+ if (options.command.input !== void 0) this.#child.stdin.write(options.command.input);
409
+ if (options.writable !== true) this.#child.stdin.end();
410
+ if (this.#signal !== void 0) {
411
+ this.#abort = this.#terminate.bind(this);
412
+ this.#signal.addEventListener("abort", this.#abort, { once: true });
413
+ if (this.#signal.aborted) this.stop();
414
+ }
415
+ }
416
+ /** The typed lifecycle observation surface. */
417
+ get emitter() {
418
+ return this.#emitter;
419
+ }
420
+ /** The eagerly captured stdout lines, in arrival order, ending when the child's stdout closes. */
421
+ get lines() {
422
+ return this.#lines;
423
+ }
424
+ /** The decoded byte-bounded stderr tail. */
425
+ get evidence() {
426
+ return this.#tail.toString("utf8");
427
+ }
428
+ /** The terminal child state, observed once from the close event. */
429
+ get exit() {
430
+ return this.#exit.promise;
431
+ }
432
+ /**
433
+ * Write one line to the open standard-input channel.
434
+ *
435
+ * @param text - The line text without its trailing newline
436
+ * @returns True when the channel accepted the line without backpressure; false otherwise
437
+ */
438
+ send(text) {
439
+ if (this.#closed || this.#child.stdin.destroyed || this.#child.stdin.writableEnded) return false;
440
+ try {
441
+ return this.#child.stdin.write(`${text}\n`);
442
+ } catch {
443
+ return false;
444
+ }
445
+ }
446
+ /**
447
+ * Terminate the child process tree and await its observed exit.
448
+ *
449
+ * @returns A promise that resolves after bounded termination
450
+ */
451
+ stop() {
452
+ if (this.#stopping !== void 0) return this.#stopping;
453
+ this.#stopping = this.#kill();
454
+ return this.#stopping;
455
+ }
456
+ /**
457
+ * Stop the child and destroy the observation emitter.
458
+ *
459
+ * @returns The stable barrier shared by every call
460
+ */
461
+ destroy() {
462
+ if (this.#ending !== void 0) return this.#ending;
463
+ this.#ending = this.#teardown();
464
+ return this.#ending;
465
+ }
466
+ #iterate() {
467
+ return Object.freeze({ next: this.#next.bind(this) });
468
+ }
469
+ #next() {
470
+ const line = this.#buffer.shift();
471
+ if (line !== void 0) return Promise.resolve(Object.freeze({
472
+ done: false,
473
+ value: line
474
+ }));
475
+ if (this.#ended) return Promise.resolve(Object.freeze({
476
+ done: true,
477
+ value: void 0
478
+ }));
479
+ const waiter = Promise.withResolvers();
480
+ this.#waiters.push(waiter);
481
+ return waiter.promise;
482
+ }
483
+ #push(line) {
484
+ if (this.#ended) return;
485
+ const waiter = this.#waiters.shift();
486
+ if (waiter === void 0) {
487
+ this.#buffer.push(line);
488
+ return;
489
+ }
490
+ waiter.resolve(Object.freeze({
491
+ done: false,
492
+ value: line
493
+ }));
494
+ }
495
+ #finish() {
496
+ if (this.#ended) return;
497
+ this.#ended = true;
498
+ for (const waiter of this.#waiters) waiter.resolve(Object.freeze({
499
+ done: true,
500
+ value: void 0
501
+ }));
502
+ this.#waiters.length = 0;
503
+ }
504
+ #retain(chunk) {
505
+ if (!node_buffer.Buffer.isBuffer(chunk) || this.#closed) return;
506
+ this.#tail = trimTail(node_buffer.Buffer.concat([this.#tail, chunk]), this.#limit);
507
+ const text = this.#decoder.write(chunk);
508
+ if (text.length > 0) this.#emitter.emit("stderr", text);
509
+ }
510
+ #close(code, signal) {
511
+ if (this.#closed) return;
512
+ const suffix = this.#decoder.end();
513
+ if (suffix.length > 0) this.#emitter.emit("stderr", suffix);
514
+ this.#closed = true;
515
+ if (this.#signal !== void 0 && this.#abort !== void 0) this.#signal.removeEventListener("abort", this.#abort);
516
+ this.#reader.close();
517
+ const exit = Object.freeze({
518
+ code,
519
+ signal
520
+ });
521
+ this.#exit.resolve(exit);
522
+ this.#emitter.emit("exit", exit);
523
+ }
524
+ #terminate() {
525
+ this.stop();
526
+ }
527
+ async #kill() {
528
+ if (this.#closed) return;
529
+ killProcess(this.#child, "SIGTERM");
530
+ const grace = Promise.withResolvers();
531
+ const timer = setTimeout(grace.resolve, this.#grace);
532
+ await Promise.race([this.#exit.promise, grace.promise]);
533
+ clearTimeout(timer);
534
+ if (this.#closed) return;
535
+ killProcess(this.#child, "SIGKILL");
536
+ await this.#exit.promise;
537
+ }
538
+ async #teardown() {
539
+ await this.stop();
540
+ this.#emitter.destroy();
541
+ }
542
+ };
543
+ //#endregion
544
+ //#region src/server/ProcessManager.ts
545
+ /**
546
+ * A keyed registry of live supervised child processes.
547
+ *
548
+ * @remarks
549
+ * A child launched under an id joins the registry and emits `launch`; when it settles it removes
550
+ * itself and emits `exit`, so `count` and `processes` reflect only live children. `launch` throws
551
+ * a {@link createDuplicateError} when the id is already live — a spawn fault surfaces through the
552
+ * returned child's `exit`, never from `launch`. `destroy` stops every child, then destroys the
553
+ * registry emitter last.
554
+ *
555
+ * @example
556
+ * ```ts
557
+ * import { ProcessManager } from '@orkestrel/process/server'
558
+ *
559
+ * const manager = new ProcessManager()
560
+ * const child = manager.launch('build', {
561
+ * command: { file: 'node', arguments: ['build.js'] },
562
+ * workspace: process.cwd(),
563
+ * grace: 5000,
564
+ * })
565
+ * await child.exit
566
+ * await manager.destroy()
567
+ * ```
568
+ */
569
+ var ProcessManager = class {
570
+ #emitter;
571
+ #children = /* @__PURE__ */ new Map();
572
+ #ending;
573
+ /**
574
+ * Construct a process registry.
575
+ *
576
+ * @param options - Initial fleet-level observation hooks and listener-error handling
577
+ */
578
+ constructor(options) {
579
+ const on = options?.on;
580
+ const error = options?.error;
581
+ this.#emitter = new _orkestrel_emitter.Emitter({
582
+ ...on === void 0 ? {} : { on },
583
+ ...error === void 0 ? {} : { error }
584
+ });
585
+ }
586
+ /** The typed fleet-level observation surface. */
587
+ get emitter() {
588
+ return this.#emitter;
589
+ }
590
+ /** The number of live children. */
591
+ get count() {
592
+ return this.#children.size;
593
+ }
594
+ /**
595
+ * The live child under `id`, or `undefined` when none is.
596
+ *
597
+ * @param id - The registry key
598
+ * @returns The child, or `undefined`
599
+ */
600
+ process(id) {
601
+ return this.#children.get(id);
602
+ }
603
+ /**
604
+ * A snapshot of every live child.
605
+ *
606
+ * @returns The live children in launch order
607
+ */
608
+ processes() {
609
+ return [...this.#children.values()];
610
+ }
611
+ /**
612
+ * Spawn and register one child under `id`.
613
+ *
614
+ * @param id - The registry key, unique among live children
615
+ * @param options - The child construction options
616
+ * @returns The launched child
617
+ * @throws A {@link ProcessError} coded `duplicate` when `id` is already live
618
+ */
619
+ launch(id, options) {
620
+ if (this.#children.has(id)) throw (0, _src_core.createDuplicateError)(id);
621
+ const child = new Process(options);
622
+ this.#children.set(id, child);
623
+ child.emitter.on("exit", (exit) => this.#evict(id, child, exit));
624
+ this.#emitter.emit("launch", id);
625
+ return child;
626
+ }
627
+ stop(target) {
628
+ if (target === void 0) return this.#stopAll();
629
+ if (typeof target === "string") return this.#stopOne(target);
630
+ return this.#stopMany(target);
631
+ }
632
+ /**
633
+ * Stop every child, then destroy the registry emitter last.
634
+ *
635
+ * @returns The stable barrier shared by every call
636
+ */
637
+ destroy() {
638
+ if (this.#ending !== void 0) return this.#ending;
639
+ this.#ending = this.#teardown();
640
+ return this.#ending;
641
+ }
642
+ #evict(id, child, exit) {
643
+ if (this.#children.get(id) !== child) return;
644
+ this.#children.delete(id);
645
+ this.#emitter.emit("exit", id, exit);
646
+ }
647
+ async #stopOne(id) {
648
+ const child = this.#children.get(id);
649
+ if (child === void 0) return false;
650
+ await child.stop();
651
+ return true;
652
+ }
653
+ async #stopMany(ids) {
654
+ return (await Promise.all(ids.map((id) => this.#stopOne(id)))).every((ok) => ok);
655
+ }
656
+ async #stopAll() {
657
+ await Promise.all([...this.#children.values()].map((child) => child.stop()));
658
+ }
659
+ async #teardown() {
660
+ await Promise.all([...this.#children.values()].map((child) => child.destroy()));
661
+ this.#emitter.destroy();
662
+ }
663
+ };
664
+ //#endregion
665
+ //#region src/server/factories.ts
666
+ /**
667
+ * Creates one supervised child process.
668
+ *
669
+ * @param options - Command, workspace, termination, evidence, stdin, and observation settings
670
+ * @returns The launched {@link ProcessInterface}
671
+ *
672
+ * @example
673
+ * ```ts
674
+ * import { createProcess } from '@orkestrel/process/server'
675
+ *
676
+ * const child = createProcess({
677
+ * command: { file: 'node', arguments: ['--version'] },
678
+ * workspace: process.cwd(),
679
+ * grace: 5000,
680
+ * })
681
+ * ```
682
+ */
683
+ function createProcess(options) {
684
+ return new Process(options);
685
+ }
686
+ /**
687
+ * Creates one keyed registry of supervised child processes.
688
+ *
689
+ * @param options - Initial fleet-level observation hooks and listener-error handling
690
+ * @returns The {@link ProcessManagerInterface} registry
691
+ *
692
+ * @example
693
+ * ```ts
694
+ * import { createProcessManager } from '@orkestrel/process/server'
695
+ *
696
+ * const manager = createProcessManager()
697
+ * ```
698
+ */
699
+ function createProcessManager(options) {
700
+ return new ProcessManager(options);
701
+ }
702
+ //#endregion
703
+ exports.Process = Process;
704
+ exports.ProcessManager = ProcessManager;
705
+ exports.buildRunResult = buildRunResult;
706
+ exports.commandLine = commandLine;
707
+ exports.createProcess = createProcess;
708
+ exports.createProcessManager = createProcessManager;
709
+ exports.killProcess = killProcess;
710
+ exports.mergeEnvironment = mergeEnvironment;
711
+ exports.requiresShell = requiresShell;
712
+ exports.run = run;
713
+ exports.runSync = runSync;
714
+ exports.trimHead = trimHead;
715
+ exports.trimTail = trimTail;
716
+
717
+ //# sourceMappingURL=index.cjs.map