@orkestrel/process 0.0.8 → 0.0.10

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.
@@ -8,6 +8,42 @@ import { PROCESS_BACKLOG, PROCESS_CONFIRMATION, PROCESS_DRAIN, PROCESS_EVIDENCE,
8
8
  import { createInterface } from "node:readline";
9
9
  import { Emitter } from "@orkestrel/emitter";
10
10
  import { StringDecoder } from "node:string_decoder";
11
+ //#region src/server/cloners.ts
12
+ /**
13
+ * Takes one owned frozen snapshot of a caller's command.
14
+ *
15
+ * @remarks
16
+ * Every public entry point snapshots before it validates, so the object validated is the object
17
+ * spawned. Each property is read exactly once, because reading one runs the caller's own getter: a
18
+ * command whose `file` changes between reads would otherwise validate one executable and spawn
19
+ * another. The argument vector and the environment record are copied and frozen, so a caller
20
+ * mutating either after the call cannot reach the spawn. An absent optional stays absent rather than
21
+ * becoming an explicit `undefined`.
22
+ *
23
+ * @param command - The caller's command, whose properties may be getters
24
+ * @returns A frozen command carrying the values read at this instant
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * snapshotCommand({ file: 'git', arguments: ['status'] }) // { file: 'git', arguments: ['status'] }
29
+ * ```
30
+ */
31
+ function snapshotCommand(command) {
32
+ const file = command.file;
33
+ const argumentsList = Object.freeze([...command.arguments]);
34
+ const sourceEnvironment = command.environment;
35
+ const environment = sourceEnvironment === void 0 ? void 0 : Object.freeze({ ...sourceEnvironment });
36
+ const input = command.input;
37
+ const isolated = command.isolated;
38
+ return Object.freeze({
39
+ file,
40
+ arguments: argumentsList,
41
+ ...environment === void 0 ? {} : { environment },
42
+ ...input === void 0 ? {} : { input },
43
+ ...isolated === void 0 ? {} : { isolated }
44
+ });
45
+ }
46
+ //#endregion
11
47
  //#region src/server/helpers.ts
12
48
  /**
13
49
  * Trims a buffer to at most `limit` trailing bytes without splitting a UTF-8 sequence.
@@ -41,9 +77,12 @@ function trimTail(bytes, limit) {
41
77
  * Trims a buffer to at most `limit` leading bytes without splitting a UTF-8 sequence.
42
78
  *
43
79
  * @remarks
44
- * Keeps the head — the captured start of a one-shot run's output. When the cut point lands inside
45
- * a multibyte sequence, the end retreats to the sequence's start so the retained bytes always end
46
- * on a code-point boundary and decode without a replacement character.
80
+ * Keeps the head — the captured start of a one-shot run's output. The end retreats only when a
81
+ * sequence genuinely spans the cut: the byte at `limit` carries the continuation bit pattern, and a
82
+ * lead byte within the preceding three bytes declares a length that reaches it. The retained bytes
83
+ * then end on a code-point boundary and decode without a replacement character. A byte at `limit`
84
+ * that carries the continuation pattern without such a lead byte is invalid UTF-8 rather than a
85
+ * split sequence, so the full `limit` survives instead of a valid byte being dropped for it.
47
86
  *
48
87
  * @param bytes - The accumulated bytes
49
88
  * @param limit - The maximum retained byte count
@@ -57,47 +96,50 @@ function trimTail(bytes, limit) {
57
96
  function trimHead(bytes, limit) {
58
97
  const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
59
98
  if (buffer.byteLength <= limit) return buffer;
60
- let end = limit;
61
- while (end > 0) {
62
- const byte = buffer[end];
63
- if (byte === void 0 || (byte & 192) !== 128) break;
64
- end -= 1;
99
+ const excluded = buffer[limit];
100
+ if (excluded === void 0 || (excluded & 192) !== 128) return Buffer.from(buffer.subarray(0, limit));
101
+ const floor = Math.max(0, limit - 3);
102
+ let start = limit - 1;
103
+ while (start >= floor) {
104
+ const lead = buffer[start];
105
+ if (lead !== void 0 && (lead & 192) !== 128) {
106
+ let span = 1;
107
+ if ((lead & 224) === 192) span = 2;
108
+ else if ((lead & 240) === 224) span = 3;
109
+ else if ((lead & 248) === 240) span = 4;
110
+ if (start + span > limit) return Buffer.from(buffer.subarray(0, start));
111
+ break;
112
+ }
113
+ start -= 1;
65
114
  }
66
- return Buffer.from(buffer.subarray(0, end));
115
+ return Buffer.from(buffer.subarray(0, limit));
67
116
  }
68
117
  /**
69
- * Takes one owned frozen snapshot of a caller's command.
118
+ * Bounds one delivered stream chunk to the bytes a capture still has room for.
70
119
  *
71
120
  * @remarks
72
- * Every public entry point snapshots before it validates, so the object validated is the object
73
- * spawned. Each property is read exactly once, because reading one runs the caller's own getter: a
74
- * command whose `file` changes between reads would otherwise validate one executable and spawn
75
- * another. The argument vector and the environment record are copied and frozen, so a caller
76
- * mutating either after the call cannot reach the spawn. An absent optional stays absent rather than
77
- * becoming an explicit `undefined`.
78
- *
79
- * @param command - The caller's command, whose properties may be getters
80
- * @returns A frozen command carrying the values read at this instant
121
+ * A chunk that is not a buffer contributes nothing and reports `undefined` rather than throwing,
122
+ * because a stream `data` listener receives an `unknown` payload. The cut is byte-exact and might
123
+ * land inside a multibyte sequence, because {@link buildExecuteResult} performs the single
124
+ * code-point-boundary trim over the whole capture rather than over each chunk. Give the capture one
125
+ * byte more room than its limit, so that final trim can read the first excluded byte and retreat off
126
+ * a split sequence. A chunk that fits its room is returned as the delivered buffer itself rather
127
+ * than a copy, because a capture appending it copies again and never writes through it; a chunk that
128
+ * overruns its room returns a copy of the bytes that fit. Treat the return as read-only either way.
129
+ *
130
+ * @param chunk - The delivered chunk, ignored when it is not a buffer
131
+ * @param room - The bytes the capture still accepts
132
+ * @returns The bytes to retain, or `undefined` when the chunk contributes none
81
133
  *
82
134
  * @example
83
135
  * ```ts
84
- * snapshotCommand({ file: 'git', arguments: ['status'] }) // { file: 'git', arguments: ['status'] }
136
+ * captureChunk(Buffer.from('hello'), 3) // <Buffer 68 65 6c>
85
137
  * ```
86
138
  */
87
- function snapshotCommand(command) {
88
- const file = command.file;
89
- const argumentsList = Object.freeze([...command.arguments]);
90
- const sourceEnvironment = command.environment;
91
- const environment = sourceEnvironment === void 0 ? void 0 : Object.freeze({ ...sourceEnvironment });
92
- const input = command.input;
93
- const isolated = command.isolated;
94
- return Object.freeze({
95
- file,
96
- arguments: argumentsList,
97
- ...environment === void 0 ? {} : { environment },
98
- ...input === void 0 ? {} : { input },
99
- ...isolated === void 0 ? {} : { isolated }
100
- });
139
+ function captureChunk(chunk, room) {
140
+ if (!Buffer.isBuffer(chunk) || room <= 0) return void 0;
141
+ if (chunk.byteLength <= room) return chunk;
142
+ return Buffer.from(chunk.subarray(0, room));
101
143
  }
102
144
  /**
103
145
  * Renders one command into its diagnostic command line.
@@ -216,7 +258,7 @@ function mergeEnvironment(isolated, base, override) {
216
258
  * command. An unreadable or malformed path reports `false` rather than throwing.
217
259
  *
218
260
  * @param target - The absolute or relative path to inspect
219
- * @returns True when the path resolves to a regular file; false otherwise
261
+ * @returns True if the path resolves to a regular file; false otherwise
220
262
  *
221
263
  * @example
222
264
  * ```ts
@@ -489,7 +531,7 @@ function validateWorkspace(workspace) {
489
531
  * initiated against a pid the host may have reused.
490
532
  *
491
533
  * @param child - The child boundary
492
- * @returns True when the process has exited; false while it is live
534
+ * @returns True if the process has exited; false otherwise
493
535
  *
494
536
  * @example
495
537
  * ```ts
@@ -545,7 +587,7 @@ function killProcess(child, signal) {
545
587
  *
546
588
  * @param pid - The root process id of the tree to end
547
589
  * @param timeout - The milliseconds the utility is given before it is killed
548
- * @returns True when the utility reported success; false when it failed, was unavailable, or was cut off
590
+ * @returns True if the utility reported success; false otherwise (it failed, was unavailable, or was cut off)
549
591
  *
550
592
  * @example
551
593
  * ```ts
@@ -596,7 +638,7 @@ function waitForExit(child, timeout) {
596
638
  *
597
639
  * @param child - The child boundary
598
640
  * @param timeout - The milliseconds to wait before giving up
599
- * @returns True when the host close arrived inside the deadline; false otherwise
641
+ * @returns True if the host close arrived inside the deadline; false otherwise
600
642
  *
601
643
  * @example
602
644
  * ```ts
@@ -631,7 +673,7 @@ function waitForClose(child, timeout) {
631
673
  * @param child - The child boundary
632
674
  * @param grace - The cooperative POSIX window in milliseconds between `SIGTERM` and `SIGKILL`
633
675
  * @param confirm - The milliseconds the native exit is awaited after the final kill
634
- * @returns True when the native exit was observed; false when `confirm` elapsed without it
676
+ * @returns True if the native exit was observed; false otherwise (`confirm` elapsed without it)
635
677
  *
636
678
  * @example
637
679
  * ```ts
@@ -691,191 +733,507 @@ function buildExecuteResult(input) {
691
733
  truncated: input.truncated
692
734
  });
693
735
  }
694
- //#endregion
695
- //#region src/server/Supervisor.ts
696
736
  /**
697
- * Supervises one child process and reports each lifecycle moment to the face composing it.
737
+ * Runs one command to completion, buffering its output, and settles with the outcome.
698
738
  *
699
739
  * @remarks
700
- * This is the engine every supervised-child face shares. It owns the eager spawn, the byte-bounded
701
- * stderr tail and its decoded chunks, the standard-input channel and its bounded confirmation, the
702
- * termination sequence, and the progression from the child's native exit through the bounded `drain`
703
- * window to one terminal moment. It owns no output framing and no observation surface: a face
704
- * attaches its own consumer to `stdout`, frames bytes however its contract promises, and owns the
705
- * emitter and event map its consumers subscribe to.
740
+ * The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
741
+ * child is detached, which is what lets its whole process group be terminated. Standard output and
742
+ * error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
743
+ * whether either stream exceeded it. Each capture keeps one byte beyond `limit`, so the single
744
+ * code-point-boundary trim in {@link buildExecuteResult} reads the first excluded byte and never
745
+ * delivers a split multibyte sequence. A positive `timeout` and an aborting `signal` both terminate the
746
+ * child through the same bounded stop, and only the earliest is recorded: a timeout reports
747
+ * `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
748
+ * a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
749
+ * covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
750
+ * rather than on process exit, so a descendant that inherited the child's stdio holds the run open
751
+ * after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
752
+ * the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
753
+ * overrides `command.input`. A host fault while writing that input terminates the run by design and
754
+ * marks its result failed; a strict rejection carries the host fault as its cause. This differs
755
+ * from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
756
+ * is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
757
+ * {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
758
+ * option or command string rejects before the child is spawned, because an async function cannot
759
+ * throw synchronously.
706
760
  *
707
- * The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read the
708
- * host child directly, so they expose native exit as soon as the host records it, and `ending`
709
- * settles at that same moment. `settled`, `exit`, and `evidence` reach one terminal moment after the
710
- * read channels close or the bounded `drain` window cuts them off. That window is armed by the
711
- * child's native exit and by a termination this package initiated, so every way a child can end
712
- * reaches the moment. The terminal routine hands the face its `close` and `terminal` moments before
713
- * it destroys stdout and stderr, so a face's own pull surfaces are final while its read ends still
714
- * exist.
761
+ * @param command - The executable, arguments, and optional environment and input
762
+ * @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
763
+ * @returns The settled run outcome
764
+ * @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
715
765
  *
716
- * The face is a set of callbacks rather than an emitter, because each face publishes a different
717
- * event map over the same moments. `chunk` and `fault` carry the live stderr text and the child or
718
- * channel faults, `relieve` reports that a termination began so the face can release whatever
719
- * backpressure it holds, `close` ends the face's read pipeline at the terminal moment, `terminal`
720
- * carries the frozen {@link ProcessExit}, and `teardown` releases the face's own surface after the
721
- * termination completes. `relieve` is optional because a face that never pauses the child's output
722
- * holds no backpressure to release, and a face with nothing to do there would otherwise declare an
723
- * empty method to say so.
766
+ * @example
767
+ * ```ts
768
+ * const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
769
+ * ```
724
770
  */
725
- var Supervisor = class {
726
- #chunk;
727
- #fault;
728
- #relieve;
729
- #close;
730
- #terminal;
731
- #teardown;
732
- #child;
733
- #grace;
734
- #drain;
735
- #evidence;
736
- #delivery;
737
- #decoder = new StringDecoder("utf8");
738
- #exit = Promise.withResolvers();
739
- #ending = Promise.withResolvers();
740
- #signal;
741
- #abort;
742
- #writes = /* @__PURE__ */ new Map();
743
- #tail = Buffer.alloc(0);
744
- #stopping = false;
745
- #settled = false;
746
- #input = 0;
747
- #failure;
748
- #waiting;
749
- #closure;
750
- #termination;
751
- #destruction;
752
- /**
753
- * Spawn one child process and begin standard-error capture.
754
- *
755
- * @remarks
756
- * The face's callbacks are captured before anything is read or spawned, so the first moment the
757
- * child can produce already has somewhere to go. `options` supplies the command, workspace,
758
- * termination, capture, and standard-input settings; a face's own settings, such as an output
759
- * retention bound, are read and validated by that face before it constructs this engine, so an
760
- * invalid one refuses construction while nothing has been spawned.
761
- *
762
- * @param options - Command, workspace, termination, capture, and stdin settings
763
- * @param face - The composing face's callbacks for each lifecycle moment
764
- * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
765
- */
766
- constructor(options, face) {
767
- this.#chunk = face.chunk;
768
- this.#fault = face.fault;
769
- this.#relieve = face.relieve;
770
- this.#close = face.close;
771
- this.#terminal = face.terminal;
772
- this.#teardown = face.teardown;
773
- const source = options.command;
774
- const workspace = options.workspace;
775
- const grace = options.grace;
776
- const drain = options.drain;
777
- const evidence = options.evidence;
778
- const delivery = options.delivery;
779
- const writable = options.writable;
780
- const signal = options.signal;
781
- const command = snapshotCommand(source);
782
- const input = command.input;
783
- validateCommand(command);
784
- validateWorkspace(workspace);
785
- validateTimer(grace, "option 'grace'");
786
- validateTimer(drain, "option 'drain'");
787
- validateTimer(delivery, "option 'delivery'");
788
- validateBytes(evidence, "option 'evidence'", 0);
789
- this.#grace = grace ?? PROCESS_GRACE;
790
- this.#drain = drain ?? PROCESS_DRAIN;
791
- this.#evidence = evidence ?? PROCESS_EVIDENCE;
792
- this.#delivery = delivery ?? 0;
793
- this.#signal = signal;
794
- const childEnvironment = mergeEnvironment(command.isolated === true, command.environment);
795
- const plan = buildSpawn(command, {
796
- workspace,
797
- environment: childEnvironment
798
- });
799
- this.#child = spawn(plan.file, [...plan.arguments], {
800
- cwd: workspace,
801
- detached: process.platform !== "win32",
802
- env: childEnvironment,
803
- stdio: [
804
- "pipe",
805
- "pipe",
806
- "pipe"
807
- ],
808
- windowsHide: true,
809
- windowsVerbatimArguments: plan.verbatim
810
- });
811
- this.#child.once("error", this.#fault);
812
- this.#child.once("exit", this.#expire.bind(this));
813
- this.#child.once("close", this.#complete.bind(this));
814
- this.#child.stdin.on("error", this.#failInputStream.bind(this));
815
- this.#child.stderr.on("data", this.#retain.bind(this));
816
- if (input !== void 0) {
817
- this.#input += 1;
818
- this.#child.stdin.write(input, this.#completeInput.bind(this));
819
- }
820
- if (writable !== true) {
821
- this.#input += 1;
822
- this.#child.stdin.end(this.#completeInput.bind(this));
823
- }
824
- if (signal !== void 0) {
825
- this.#abort = this.#terminate.bind(this);
826
- signal.addEventListener("abort", this.#abort, { once: true });
827
- if (signal.aborted) this.stop();
828
- }
829
- }
830
- /** The child's standard-output stream, for the composing face to attach its own consumer to. */
831
- get stdout() {
832
- return this.#child.stdout;
833
- }
834
- /** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
835
- get pid() {
836
- return this.#child.pid;
837
- }
838
- /** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
839
- get code() {
840
- return this.#child.exitCode;
841
- }
842
- /** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
843
- get signal() {
844
- return this.#child.signalCode;
845
- }
846
- /** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
847
- get evidence() {
848
- return this.#tail.toString("utf8");
849
- }
850
- /** True after the terminal moment arrived and `exit` settled. */
851
- get settled() {
852
- return this.#settled;
853
- }
854
- /** True after termination began, including after the terminal moment. */
855
- get stopping() {
856
- return this.#stopping;
857
- }
858
- /**
859
- * The child's own ending, awaited without the terminal moment's drain window.
860
- *
861
- * @remarks
862
- * Never rejects, and resolves no value: `code` and `signal` carry the terminal facts as soon as
863
- * the host records them. It settles at the native exit, and at the terminal moment for a child
864
- * whose spawn produced no native exit at all, so it can never outlive the supervision.
865
- */
866
- get ending() {
867
- return this.#ending.promise;
868
- }
869
- /** The terminal child state, observed once after stream close or the drain cutoff. */
870
- get exit() {
871
- return this.#exit.promise;
872
- }
873
- /**
874
- * Writes raw bytes to the open standard-input channel.
875
- *
876
- * @remarks
877
- * Never rejects, and adds no framing: a face composes whatever terminator its contract promises
878
- * before it calls this. `true` means the host accepted the bytes without reporting a fault; it
771
+ async function execute(command, options) {
772
+ const snapshot = snapshotCommand(command);
773
+ const optionEnvironment = options?.environment;
774
+ const optionWorkspace = options?.workspace;
775
+ const optionInput = options?.input;
776
+ const optionTimeout = options?.timeout;
777
+ const optionGrace = options?.grace;
778
+ const optionSignal = options?.signal;
779
+ const optionStrict = options?.strict;
780
+ const optionLimit = options?.limit;
781
+ validateCommand(snapshot);
782
+ validateEnvironment(optionEnvironment);
783
+ validateWorkspace(optionWorkspace);
784
+ validateTimer(optionTimeout, "option 'timeout'");
785
+ validateTimer(optionGrace, "option 'grace'");
786
+ validateBytes(optionLimit, "option 'limit'", 0);
787
+ const limit = optionLimit ?? PROCESS_OUTPUT;
788
+ const grace = optionGrace ?? PROCESS_GRACE;
789
+ const timeout = optionTimeout ?? 0;
790
+ const strict = optionStrict ?? true;
791
+ const text = optionInput ?? snapshot.input;
792
+ const line = formatCommand(snapshot);
793
+ const workspace = optionWorkspace ?? process.cwd();
794
+ const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
795
+ const plan = buildSpawn(snapshot, {
796
+ workspace,
797
+ environment
798
+ });
799
+ const settled = Promise.withResolvers();
800
+ const terminate = new AbortController();
801
+ const cleanup = new AbortController();
802
+ const finish = new AbortController();
803
+ const inputFailure = new AbortController();
804
+ const outChunks = [];
805
+ const errChunks = [];
806
+ let spawned = false;
807
+ let expired = false;
808
+ let aborted = false;
809
+ let outRetained = 0;
810
+ let errRetained = 0;
811
+ let cause;
812
+ let timeoutTimer;
813
+ let confirmTimer;
814
+ const child = spawn(plan.file, [...plan.arguments], {
815
+ cwd: workspace,
816
+ detached: process.platform !== "win32",
817
+ env: environment,
818
+ stdio: [
819
+ "pipe",
820
+ "pipe",
821
+ "pipe"
822
+ ],
823
+ windowsHide: true,
824
+ windowsVerbatimArguments: plan.verbatim
825
+ });
826
+ finish.signal.addEventListener("abort", () => {
827
+ clearTimeout(timeoutTimer);
828
+ clearTimeout(confirmTimer);
829
+ cleanup.abort();
830
+ settled.resolve(buildExecuteResult({
831
+ command: line,
832
+ stdout: Buffer.concat(outChunks),
833
+ stderr: Buffer.concat(errChunks),
834
+ code: child.exitCode,
835
+ signal: child.signalCode,
836
+ expired,
837
+ aborted,
838
+ truncated: outRetained > limit || errRetained > limit,
839
+ limit,
840
+ ...cause === void 0 ? {} : { cause }
841
+ }));
842
+ }, { once: true });
843
+ terminate.signal.addEventListener("abort", () => {
844
+ if (finish.signal.aborted) return;
845
+ clearTimeout(timeoutTimer);
846
+ cleanup.abort();
847
+ stopChild(child, grace, PROCESS_CONFIRMATION).then(() => {
848
+ if (finish.signal.aborted) return;
849
+ confirmTimer = setTimeout(() => finish.abort(), PROCESS_CONFIRMATION);
850
+ });
851
+ }, { once: true });
852
+ inputFailure.signal.addEventListener("abort", () => {
853
+ if (finish.signal.aborted) return;
854
+ cause = inputFailure.signal.reason;
855
+ terminate.abort();
856
+ }, { once: true });
857
+ child.stdin.on("error", (error) => inputFailure.abort(error));
858
+ child.stdout.on("data", (chunk) => {
859
+ const captured = captureChunk(chunk, limit + 1 - outRetained);
860
+ if (captured === void 0) return;
861
+ outRetained += captured.byteLength;
862
+ outChunks.push(captured);
863
+ });
864
+ child.stderr.on("data", (chunk) => {
865
+ const captured = captureChunk(chunk, limit + 1 - errRetained);
866
+ if (captured === void 0) return;
867
+ errRetained += captured.byteLength;
868
+ errChunks.push(captured);
869
+ });
870
+ child.once("spawn", () => {
871
+ spawned = true;
872
+ });
873
+ child.once("error", (error) => {
874
+ if (spawned) return;
875
+ cause = error;
876
+ finish.abort();
877
+ });
878
+ child.once("close", () => finish.abort());
879
+ if (text !== void 0) child.stdin.write(text, (error) => {
880
+ if (error !== void 0 && error !== null) inputFailure.abort(error);
881
+ });
882
+ child.stdin.end();
883
+ if (timeout > 0) timeoutTimer = setTimeout(() => {
884
+ expired = true;
885
+ terminate.abort();
886
+ }, timeout);
887
+ if (optionSignal !== void 0) {
888
+ optionSignal.addEventListener("abort", () => {
889
+ aborted = true;
890
+ terminate.abort();
891
+ }, {
892
+ once: true,
893
+ signal: cleanup.signal
894
+ });
895
+ if (optionSignal.aborted) {
896
+ aborted = true;
897
+ terminate.abort();
898
+ }
899
+ }
900
+ const result = await settled.promise;
901
+ if (result.failed && strict) {
902
+ if (cause !== void 0 && cause === inputFailure.signal.reason) throw new ProcessError(`Command '${result.command}' failed while writing standard input`, {
903
+ code: "input",
904
+ context: {
905
+ command: result.command,
906
+ code: result.code,
907
+ signal: result.signal
908
+ },
909
+ cause: inputFailure.signal.reason,
910
+ result
911
+ });
912
+ throw createExecuteError(result, cause);
913
+ }
914
+ return result;
915
+ }
916
+ /**
917
+ * Runs one command to completion synchronously, buffering its output, and returns the outcome.
918
+ *
919
+ * @remarks
920
+ * The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
921
+ * shell. The host offers no cooperative termination window and no in-flight cancellation, so this
922
+ * contract carries neither. A positive `timeout` ends only the root process and can leave
923
+ * descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
924
+ * timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
925
+ * and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
926
+ * reports `truncated` without failing. The environment and input follow the same merge as
927
+ * {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
928
+ * carrying the {@link ExecuteResult}.
929
+ *
930
+ * @param command - The executable, arguments, and optional environment and input
931
+ * @param options - Working directory, timeout, capture limit, and failure delivery
932
+ * @returns The run outcome
933
+ * @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
934
+ *
935
+ * @example
936
+ * ```ts
937
+ * const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
938
+ * ```
939
+ */
940
+ function executeSync(command, options) {
941
+ const snapshot = snapshotCommand(command);
942
+ const optionEnvironment = options?.environment;
943
+ const optionWorkspace = options?.workspace;
944
+ const optionInput = options?.input;
945
+ const optionTimeout = options?.timeout;
946
+ const optionStrict = options?.strict;
947
+ const optionLimit = options?.limit;
948
+ validateCommand(snapshot);
949
+ validateEnvironment(optionEnvironment);
950
+ validateWorkspace(optionWorkspace);
951
+ validateTimer(optionTimeout, "option 'timeout'");
952
+ validateBytes(optionLimit, "option 'limit'", 0);
953
+ const limit = optionLimit ?? PROCESS_OUTPUT;
954
+ const timeout = optionTimeout ?? 0;
955
+ const strict = optionStrict ?? true;
956
+ const text = optionInput ?? snapshot.input;
957
+ const line = formatCommand(snapshot);
958
+ const workspace = optionWorkspace ?? process.cwd();
959
+ const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
960
+ const plan = buildSpawn(snapshot, {
961
+ workspace,
962
+ environment
963
+ });
964
+ const outcome = spawnSync(plan.file, [...plan.arguments], {
965
+ cwd: workspace,
966
+ env: environment,
967
+ encoding: "buffer",
968
+ maxBuffer: limit,
969
+ killSignal: "SIGKILL",
970
+ windowsHide: true,
971
+ windowsVerbatimArguments: plan.verbatim,
972
+ ...text !== void 0 ? { input: Buffer.from(text) } : {},
973
+ ...timeout > 0 ? { timeout } : {}
974
+ });
975
+ const error = outcome.error;
976
+ const fault = error !== void 0 && "code" in error && isString(error.code) ? error.code : "";
977
+ const result = buildExecuteResult({
978
+ command: line,
979
+ stdout: Buffer.isBuffer(outcome.stdout) ? outcome.stdout : Buffer.alloc(0),
980
+ stderr: Buffer.isBuffer(outcome.stderr) ? outcome.stderr : Buffer.alloc(0),
981
+ code: outcome.status,
982
+ signal: outcome.signal,
983
+ expired: fault === "ETIMEDOUT",
984
+ aborted: false,
985
+ truncated: fault === "ENOBUFS",
986
+ limit,
987
+ ...error === void 0 ? {} : { cause: error }
988
+ });
989
+ if (result.failed && strict) throw createExecuteError(result, error);
990
+ return result;
991
+ }
992
+ /**
993
+ * Spawns one command as a detached process and returns without waiting for it.
994
+ *
995
+ * @remarks
996
+ * The child owns no stdio and is unreferenced, so it outlives this process and nothing here observes
997
+ * its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
998
+ * fault is swallowed rather than crashing the caller; an invalid command is refused before anything
999
+ * is spawned.
1000
+ *
1001
+ * @param command - The executable, arguments, and optional environment
1002
+ * @param options - The working directory the detached child starts in
1003
+ * @returns Nothing
1004
+ * @throws A {@link ProcessError} coded `invalid` when the working directory or a command string is malformed
1005
+ *
1006
+ * @example
1007
+ * ```ts
1008
+ * detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
1009
+ * ```
1010
+ */
1011
+ function detach(command, options) {
1012
+ const snapshot = snapshotCommand(command);
1013
+ const optionWorkspace = options?.workspace;
1014
+ validateCommand(snapshot);
1015
+ validateWorkspace(optionWorkspace);
1016
+ const workspace = optionWorkspace ?? process.cwd();
1017
+ const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment);
1018
+ const plan = buildSpawn(snapshot, {
1019
+ workspace,
1020
+ environment
1021
+ });
1022
+ const child = spawn(plan.file, [...plan.arguments], {
1023
+ cwd: workspace,
1024
+ detached: true,
1025
+ env: environment,
1026
+ stdio: "ignore",
1027
+ windowsHide: true,
1028
+ windowsVerbatimArguments: plan.verbatim
1029
+ });
1030
+ child.once("error", () => void 0);
1031
+ child.unref();
1032
+ }
1033
+ //#endregion
1034
+ //#region src/server/processes/Supervisor.ts
1035
+ /**
1036
+ * Supervises one child process and reports each lifecycle moment to the face composing it.
1037
+ *
1038
+ * @remarks
1039
+ * This is the engine every supervised-child face shares. It owns the eager spawn, the byte-bounded
1040
+ * stderr tail and its decoded chunks, the standard-input channel and its bounded confirmation, the
1041
+ * termination sequence, and the progression from the child's native exit through the bounded `drain`
1042
+ * window to one terminal moment. It owns no output framing and no observation surface: a face
1043
+ * attaches its own consumer to `stdout`, frames bytes however its contract promises, and owns the
1044
+ * emitter and event map its consumers subscribe to.
1045
+ *
1046
+ * The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read the
1047
+ * host child directly, so they expose native exit as soon as the host records it, and `ending`
1048
+ * settles at that same moment. `settled`, `exit`, and `evidence` reach one terminal moment after the
1049
+ * read channels close or the bounded `drain` window cuts them off. That window is armed by the
1050
+ * child's native exit and by a termination this package initiated, so every way a child can end
1051
+ * reaches the moment. The terminal routine hands the face its `close` and `terminal` moments before
1052
+ * it destroys stdout and stderr, so a face's own pull surfaces are final while its read ends still
1053
+ * exist.
1054
+ *
1055
+ * The face is a set of callbacks rather than an emitter, because each face publishes a different
1056
+ * event map over the same moments. `chunk` and `fault` carry the live stderr text and the child or
1057
+ * channel faults, `relieve` reports that a termination began so the face can release whatever
1058
+ * backpressure it holds, `close` ends the face's read pipeline at the terminal moment, `terminal`
1059
+ * carries the frozen {@link ProcessExit}, and `teardown` releases the face's own surface after the
1060
+ * termination completes. `relieve` is optional because a face that never pauses the child's output
1061
+ * holds no backpressure to release, and a face with nothing to do there would otherwise declare an
1062
+ * empty method to say so.
1063
+ *
1064
+ * @example
1065
+ * ```ts
1066
+ * import { Supervisor } from '@orkestrel/process/server'
1067
+ *
1068
+ * const engine = new Supervisor(
1069
+ * { command: { file: 'node', arguments: ['--version'] }, workspace: process.cwd() },
1070
+ * {
1071
+ * chunk: () => undefined,
1072
+ * fault: () => undefined,
1073
+ * close: () => undefined,
1074
+ * terminal: () => undefined,
1075
+ * teardown: () => undefined,
1076
+ * },
1077
+ * )
1078
+ * const exit = await engine.exit
1079
+ * exit.code // 0
1080
+ * await engine.destroy()
1081
+ * ```
1082
+ */
1083
+ var Supervisor = class {
1084
+ #chunk;
1085
+ #fault;
1086
+ #relieve;
1087
+ #close;
1088
+ #terminal;
1089
+ #teardown;
1090
+ #child;
1091
+ #grace;
1092
+ #drain;
1093
+ #evidence;
1094
+ #delivery;
1095
+ #decoder = new StringDecoder("utf8");
1096
+ #exit = Promise.withResolvers();
1097
+ #ending = Promise.withResolvers();
1098
+ #signal;
1099
+ #abort;
1100
+ #writes = /* @__PURE__ */ new Map();
1101
+ #tail = Buffer.alloc(0);
1102
+ #stopping = false;
1103
+ #settled = false;
1104
+ #input = 0;
1105
+ #failure;
1106
+ #waiting;
1107
+ #closure;
1108
+ #termination;
1109
+ #destruction;
1110
+ /**
1111
+ * Spawns one child process and begins standard-error capture.
1112
+ *
1113
+ * @remarks
1114
+ * The face's callbacks are captured before anything is read or spawned, so the first moment the
1115
+ * child can produce already has somewhere to go. `options` supplies the command, workspace,
1116
+ * termination, capture, and standard-input settings; a face's own settings, such as an output
1117
+ * retention bound, are read and validated by that face before it constructs this engine, so an
1118
+ * invalid one refuses construction while nothing has been spawned.
1119
+ *
1120
+ * @param options - Command, workspace, termination, capture, and stdin settings
1121
+ * @param face - The composing face's callbacks for each lifecycle moment
1122
+ * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
1123
+ */
1124
+ constructor(options, face) {
1125
+ this.#chunk = face.chunk;
1126
+ this.#fault = face.fault;
1127
+ this.#relieve = face.relieve;
1128
+ this.#close = face.close;
1129
+ this.#terminal = face.terminal;
1130
+ this.#teardown = face.teardown;
1131
+ const source = options.command;
1132
+ const workspace = options.workspace;
1133
+ const grace = options.grace;
1134
+ const drain = options.drain;
1135
+ const evidence = options.evidence;
1136
+ const delivery = options.delivery;
1137
+ const writable = options.writable;
1138
+ const signal = options.signal;
1139
+ const command = snapshotCommand(source);
1140
+ const input = command.input;
1141
+ validateCommand(command);
1142
+ validateWorkspace(workspace);
1143
+ validateTimer(grace, "option 'grace'");
1144
+ validateTimer(drain, "option 'drain'");
1145
+ validateTimer(delivery, "option 'delivery'");
1146
+ validateBytes(evidence, "option 'evidence'", 0);
1147
+ this.#grace = grace ?? PROCESS_GRACE;
1148
+ this.#drain = drain ?? PROCESS_DRAIN;
1149
+ this.#evidence = evidence ?? PROCESS_EVIDENCE;
1150
+ this.#delivery = delivery ?? 0;
1151
+ this.#signal = signal;
1152
+ const childEnvironment = mergeEnvironment(command.isolated === true, command.environment);
1153
+ const plan = buildSpawn(command, {
1154
+ workspace,
1155
+ environment: childEnvironment
1156
+ });
1157
+ this.#child = spawn(plan.file, [...plan.arguments], {
1158
+ cwd: workspace,
1159
+ detached: process.platform !== "win32",
1160
+ env: childEnvironment,
1161
+ stdio: [
1162
+ "pipe",
1163
+ "pipe",
1164
+ "pipe"
1165
+ ],
1166
+ windowsHide: true,
1167
+ windowsVerbatimArguments: plan.verbatim
1168
+ });
1169
+ this.#child.once("error", this.#fault);
1170
+ this.#child.once("exit", this.#expire.bind(this));
1171
+ this.#child.once("close", this.#complete.bind(this));
1172
+ this.#child.stdin.on("error", this.#failInputStream.bind(this));
1173
+ this.#child.stderr.on("data", this.#retain.bind(this));
1174
+ if (input !== void 0) {
1175
+ this.#input += 1;
1176
+ this.#child.stdin.write(input, this.#completeInput.bind(this));
1177
+ }
1178
+ if (writable !== true) {
1179
+ this.#input += 1;
1180
+ this.#child.stdin.end(this.#completeInput.bind(this));
1181
+ }
1182
+ if (signal !== void 0) {
1183
+ this.#abort = this.#terminate.bind(this);
1184
+ signal.addEventListener("abort", this.#abort, { once: true });
1185
+ if (signal.aborted) this.stop();
1186
+ }
1187
+ }
1188
+ /** Holds the child's standard-output stream, for the composing face to attach its own consumer to. */
1189
+ get stdout() {
1190
+ return this.#child.stdout;
1191
+ }
1192
+ /** Holds the host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
1193
+ get pid() {
1194
+ return this.#child.pid;
1195
+ }
1196
+ /** Holds the exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
1197
+ get code() {
1198
+ return this.#child.exitCode;
1199
+ }
1200
+ /** Holds the terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
1201
+ get signal() {
1202
+ return this.#child.signalCode;
1203
+ }
1204
+ /** Holds the decoded byte-bounded stderr tail, frozen at the terminal moment. */
1205
+ get evidence() {
1206
+ return this.#tail.toString("utf8");
1207
+ }
1208
+ /** Reports whether the terminal moment arrived and `exit` settled. */
1209
+ get settled() {
1210
+ return this.#settled;
1211
+ }
1212
+ /** Reports whether termination began, including after the terminal moment. */
1213
+ get stopping() {
1214
+ return this.#stopping;
1215
+ }
1216
+ /**
1217
+ * Settles at the child's own ending, awaited without the terminal moment's drain window.
1218
+ *
1219
+ * @remarks
1220
+ * Never rejects, and resolves no value: `code` and `signal` carry the terminal facts as soon as
1221
+ * the host records them. It settles at the native exit, and at the terminal moment for a child
1222
+ * whose spawn produced no native exit at all, so it can never outlive the supervision.
1223
+ */
1224
+ get ending() {
1225
+ return this.#ending.promise;
1226
+ }
1227
+ /** Settles with the terminal child state, observed once after stream close or the drain cutoff. */
1228
+ get exit() {
1229
+ return this.#exit.promise;
1230
+ }
1231
+ /**
1232
+ * Writes raw bytes to the open standard-input channel.
1233
+ *
1234
+ * @remarks
1235
+ * Never rejects, and adds no framing: a face composes whatever terminator its contract promises
1236
+ * before it calls this. `true` means the host accepted the bytes without reporting a fault; it
879
1237
  * does not prove that the child read them. An ordinary write settles when the kernel accepts it.
880
1238
  * Only a full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and
881
1239
  * every terminal teardown path settles pending writes. After `stop` or `destroy` begins, a later
@@ -883,7 +1241,30 @@ var Supervisor = class {
883
1241
  * discard.
884
1242
  *
885
1243
  * @param bytes - The payload to write, already framed by the caller
886
- * @returns True when the host accepted the bytes without reporting a fault; false when the channel was closed, destroyed, ended, failed, or remained unconfirmed through `delivery`
1244
+ * @returns True if the host accepted the bytes without reporting a fault; false otherwise (the channel was closed, destroyed, ended, failed, or the write remained unconfirmed through `delivery`)
1245
+ *
1246
+ * @example
1247
+ * ```ts
1248
+ * import { Supervisor } from '@orkestrel/process/server'
1249
+ *
1250
+ * const engine = new Supervisor(
1251
+ * {
1252
+ * command: { file: 'node', arguments: ['-e', 'process.stdin.pipe(process.stdout)'] },
1253
+ * workspace: process.cwd(),
1254
+ * writable: true,
1255
+ * },
1256
+ * {
1257
+ * chunk: () => undefined,
1258
+ * fault: () => undefined,
1259
+ * close: () => undefined,
1260
+ * terminal: () => undefined,
1261
+ * teardown: () => undefined,
1262
+ * },
1263
+ * )
1264
+ * const accepted = await engine.deliver(new TextEncoder().encode('ping\n'))
1265
+ * accepted // true
1266
+ * await engine.destroy()
1267
+ * ```
887
1268
  */
888
1269
  deliver(bytes) {
889
1270
  const stdin = this.#child.stdin;
@@ -934,7 +1315,7 @@ var Supervisor = class {
934
1315
  * and settles `exit` when they remain open. A cutoff reached before the native exit reports the
935
1316
  * `code` and `signal` the host had recorded by then, which is `null` for a child still running.
936
1317
  *
937
- * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
1318
+ * @returns True if the child's native exit was observed; false otherwise (the confirmation deadline elapsed without it)
938
1319
  */
939
1320
  stop() {
940
1321
  if (this.#termination !== void 0) return this.#termination;
@@ -1062,7 +1443,7 @@ var Supervisor = class {
1062
1443
  }
1063
1444
  };
1064
1445
  //#endregion
1065
- //#region src/server/Process.ts
1446
+ //#region src/server/processes/Process.ts
1066
1447
  /**
1067
1448
  * Supervises one child while keeping every observation channel aligned at termination.
1068
1449
  *
@@ -1109,7 +1490,7 @@ var Process = class {
1109
1490
  #truncated = false;
1110
1491
  #ended = false;
1111
1492
  /**
1112
- * Spawn one child process and begin stream capture.
1493
+ * Spawns one child process and begins stream capture.
1113
1494
  *
1114
1495
  * @param options - Command, workspace, termination, capture, stdin, and observation settings
1115
1496
  * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
@@ -1164,48 +1545,48 @@ var Process = class {
1164
1545
  this.#reader.on("line", this.#push.bind(this));
1165
1546
  this.#reader.once("close", this.#finish.bind(this));
1166
1547
  }
1167
- /** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
1548
+ /** Holds the host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
1168
1549
  get pid() {
1169
1550
  return this.#engine.pid;
1170
1551
  }
1171
- /** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
1552
+ /** Holds the exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
1172
1553
  get code() {
1173
1554
  return this.#engine.code;
1174
1555
  }
1175
- /** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
1556
+ /** Holds the terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
1176
1557
  get signal() {
1177
1558
  return this.#engine.signal;
1178
1559
  }
1179
- /** The typed lifecycle observation surface. */
1560
+ /** Holds the typed lifecycle observation surface. */
1180
1561
  get emitter() {
1181
1562
  return this.#emitter;
1182
1563
  }
1183
- /** The captured stdout lines, in arrival order, ending after queued lines at the terminal moment. */
1564
+ /** Yields the captured stdout lines, in arrival order, ending after queued lines at the terminal moment. */
1184
1565
  get lines() {
1185
1566
  return this.#lines;
1186
1567
  }
1187
- /** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
1568
+ /** Holds the decoded byte-bounded stderr tail, frozen at the terminal moment. */
1188
1569
  get evidence() {
1189
1570
  return this.#engine.evidence;
1190
1571
  }
1191
- /** True when the `lines` stream omitted output after a retention bound was reached. */
1572
+ /** Reports whether the `lines` stream omitted output after a retention bound was reached. */
1192
1573
  get truncated() {
1193
1574
  return this.#truncated;
1194
1575
  }
1195
- /** True after the terminal moment arrived and `exit` settled. */
1576
+ /** Reports whether the terminal moment arrived and `exit` settled. */
1196
1577
  get settled() {
1197
1578
  return this.#engine.settled;
1198
1579
  }
1199
- /** True after termination began, including after the terminal moment. */
1580
+ /** Reports whether termination began, including after the terminal moment. */
1200
1581
  get stopping() {
1201
1582
  return this.#engine.stopping;
1202
1583
  }
1203
- /** The terminal child state, observed once after stream close or the drain cutoff. */
1584
+ /** Settles with the terminal child state, observed once after stream close or the drain cutoff. */
1204
1585
  get exit() {
1205
1586
  return this.#engine.exit;
1206
1587
  }
1207
1588
  /**
1208
- * Write one line to the open standard-input channel.
1589
+ * Writes one line to the open standard-input channel.
1209
1590
  *
1210
1591
  * @remarks
1211
1592
  * Never rejects. `true` means the host accepted the bytes without reporting a fault; it does not
@@ -1219,7 +1600,7 @@ var Process = class {
1219
1600
  * for bytes the package is about to discard.
1220
1601
  *
1221
1602
  * @param text - The line text without its trailing newline
1222
- * @returns True when the host accepted the bytes without reporting a fault; false when the channel was closed, destroyed, ended, failed, or remained unconfirmed through `delivery`
1603
+ * @returns True if the host accepted the bytes without reporting a fault; false otherwise (the channel was closed, destroyed, ended, failed, or the write remained unconfirmed through `delivery`)
1223
1604
  */
1224
1605
  send(text) {
1225
1606
  return this.#engine.deliver(Buffer.from(`${text}\n`, "utf8"));
@@ -1233,7 +1614,7 @@ var Process = class {
1233
1614
  * and settles `exit` when they remain open. A cutoff reached before the native exit reports the
1234
1615
  * `code` and `signal` the host had recorded by then, which is `null` for a child still running.
1235
1616
  *
1236
- * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
1617
+ * @returns True if the child's native exit was observed; false otherwise (the confirmation deadline elapsed without it)
1237
1618
  */
1238
1619
  stop() {
1239
1620
  return this.#engine.stop();
@@ -1351,9 +1732,9 @@ var Process = class {
1351
1732
  }
1352
1733
  };
1353
1734
  //#endregion
1354
- //#region src/server/ProcessManager.ts
1735
+ //#region src/server/processes/ProcessManager.ts
1355
1736
  /**
1356
- * A keyed registry of live supervised child processes.
1737
+ * Represents a keyed registry of live supervised child processes.
1357
1738
  *
1358
1739
  * @remarks
1359
1740
  * A child launched under an id joins the registry and emits `launch`; when it settles it removes
@@ -1389,7 +1770,7 @@ var ProcessManager = class {
1389
1770
  #destroying = false;
1390
1771
  #ending;
1391
1772
  /**
1392
- * Construct a process registry.
1773
+ * Constructs a process registry.
1393
1774
  *
1394
1775
  * @param options - Initial fleet-level observation hooks and listener-error handling
1395
1776
  */
@@ -1401,16 +1782,16 @@ var ProcessManager = class {
1401
1782
  ...error === void 0 ? {} : { error }
1402
1783
  });
1403
1784
  }
1404
- /** The typed fleet-level observation surface. */
1785
+ /** Holds the typed fleet-level observation surface. */
1405
1786
  get emitter() {
1406
1787
  return this.#emitter;
1407
1788
  }
1408
- /** The number of live children. */
1789
+ /** Counts the live children. */
1409
1790
  get count() {
1410
1791
  return this.#children.size;
1411
1792
  }
1412
1793
  /**
1413
- * The live child under `id`, or `undefined` when none is.
1794
+ * Returns the live child under `id`, or `undefined` when none is.
1414
1795
  *
1415
1796
  * @param id - The registry key
1416
1797
  * @returns The child, or `undefined`
@@ -1419,7 +1800,7 @@ var ProcessManager = class {
1419
1800
  return this.#children.get(id);
1420
1801
  }
1421
1802
  /**
1422
- * A snapshot of every live child.
1803
+ * Returns a snapshot of every live child.
1423
1804
  *
1424
1805
  * @returns The live children in launch order
1425
1806
  */
@@ -1427,7 +1808,7 @@ var ProcessManager = class {
1427
1808
  return [...this.#children.values()];
1428
1809
  }
1429
1810
  /**
1430
- * Spawn and register one child under `id`.
1811
+ * Spawns and registers one child under `id`.
1431
1812
  *
1432
1813
  * @remarks
1433
1814
  * {@link Process} reads every option before it spawns, so a caller's own option getter runs while
@@ -1465,7 +1846,7 @@ var ProcessManager = class {
1465
1846
  return this.#stopMany(target);
1466
1847
  }
1467
1848
  /**
1468
- * Stop every child, then destroy the registry emitter last.
1849
+ * Stops every child, then destroys the registry emitter last.
1469
1850
  *
1470
1851
  * @remarks
1471
1852
  * Always resolves, and refuses a later `launch` with a {@link createProtocolError}. Each child is
@@ -1507,591 +1888,254 @@ var ProcessManager = class {
1507
1888
  }
1508
1889
  async #teardown() {
1509
1890
  await Promise.allSettled([...this.#children.values()].map((child) => child.destroy()));
1510
- while (this.#orphans.size > 0) {
1511
- const orphans = [...this.#orphans];
1512
- this.#orphans.clear();
1513
- await Promise.allSettled(orphans.map((child) => child.destroy()));
1514
- }
1515
- this.#children.clear();
1516
- this.#ids.clear();
1517
- this.#emitter.destroy();
1518
- }
1519
- };
1520
- //#endregion
1521
- //#region src/server/Session.ts
1522
- /**
1523
- * Supervises one child and publishes its standard output as raw bytes.
1524
- *
1525
- * @remarks
1526
- * This is the transport face of the supervision engine `Process` also composes. It frames nothing,
1527
- * retains nothing, and decodes nothing on the output side: every chunk the host delivers becomes one
1528
- * `stdout` event carrying that chunk's bytes, in arrival order, so a consumer speaking a
1529
- * length-prefixed or delimiter-framed protocol reads exactly what the child wrote and frames it
1530
- * itself. Standard error is decoded and forwarded as `stderr`, with the same byte-bounded `evidence`
1531
- * tail a `Process` retains, because a diagnostic stream is read rather than parsed.
1532
- *
1533
- * Each emitted chunk is this session's own copy, and a plain `Uint8Array` rather than the host buffer
1534
- * the read produced, so a consumer keeps it, mutates it, concatenates it, and reads its whole backing
1535
- * buffer without reaching memory the host still manages and without depending on a host type.
1536
- *
1537
- * The standard-input channel is open from the spawn, and `end` closes it without terminating
1538
- * anything. That is the member a cooperative protocol reaches for: the child observes end of input
1539
- * and exits on its own, which `ending` reports. `stop` and `destroy` remain the terminations, and
1540
- * `exit` remains the terminal moment — the native exit plus at most `drain` while a descendant holds
1541
- * the inherited read ends.
1542
- *
1543
- * @example
1544
- * ```ts
1545
- * import { Session } from '@orkestrel/process/server'
1546
- *
1547
- * const session = new Session({
1548
- * command: { file: 'node', arguments: ['-e', 'process.stdin.pipe(process.stdout)'] },
1549
- * workspace: process.cwd(),
1550
- * })
1551
- * ```
1552
- */
1553
- var Session = class {
1554
- #emitter;
1555
- #engine;
1556
- #output;
1557
- /**
1558
- * Spawn one child process and begin byte capture.
1559
- *
1560
- * @param options - Command, workspace, termination, evidence, stdin, and observation settings
1561
- * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
1562
- */
1563
- constructor(options) {
1564
- const command = options.command;
1565
- const workspace = options.workspace;
1566
- const grace = options.grace;
1567
- const drain = options.drain;
1568
- const evidence = options.evidence;
1569
- const delivery = options.delivery;
1570
- const signal = options.signal;
1571
- const on = options.on;
1572
- const error = options.error;
1573
- this.#emitter = new Emitter({
1574
- ...on === void 0 ? {} : { on },
1575
- ...error === void 0 ? {} : { error }
1576
- });
1577
- this.#output = this.#publish.bind(this);
1578
- this.#engine = new Supervisor({
1579
- command,
1580
- workspace,
1581
- writable: true,
1582
- ...grace === void 0 ? {} : { grace },
1583
- ...drain === void 0 ? {} : { drain },
1584
- ...evidence === void 0 ? {} : { evidence },
1585
- ...delivery === void 0 ? {} : { delivery },
1586
- ...signal === void 0 ? {} : { signal }
1587
- }, {
1588
- chunk: this.#reportStderr.bind(this),
1589
- fault: this.#reportFault.bind(this),
1590
- close: this.#closeOutput.bind(this),
1591
- terminal: this.#reportExit.bind(this),
1592
- teardown: this.#emitter.destroy.bind(this.#emitter)
1593
- });
1594
- this.#engine.stdout.on("data", this.#output);
1595
- }
1596
- /** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
1597
- get pid() {
1598
- return this.#engine.pid;
1599
- }
1600
- /** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
1601
- get code() {
1602
- return this.#engine.code;
1603
- }
1604
- /** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
1605
- get signal() {
1606
- return this.#engine.signal;
1607
- }
1608
- /** The typed lifecycle observation surface. */
1609
- get emitter() {
1610
- return this.#emitter;
1611
- }
1612
- /** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
1613
- get evidence() {
1614
- return this.#engine.evidence;
1615
- }
1616
- /** True after the terminal moment arrived and `exit` settled. */
1617
- get settled() {
1618
- return this.#engine.settled;
1619
- }
1620
- /** True after termination began, including after the terminal moment. `end` never turns it true. */
1621
- get stopping() {
1622
- return this.#engine.stopping;
1623
- }
1624
- /** The child's own ending, awaited without the terminal moment's drain window. */
1625
- get ending() {
1626
- return this.#engine.ending;
1627
- }
1628
- /** The terminal child state, observed once after stream close or the drain cutoff. */
1629
- get exit() {
1630
- return this.#engine.exit;
1631
- }
1632
- /**
1633
- * Writes raw bytes to the open standard-input channel.
1634
- *
1635
- * @remarks
1636
- * Never rejects, and appends no terminator: the child receives exactly these bytes. `true` means
1637
- * the host accepted them without reporting a fault; it does not prove that the child read them.
1638
- * The channel can queue the payload, so treat the array as owned by the channel until the
1639
- * returned promise settles.
1640
- *
1641
- * @param bytes - The payload to write, already framed by the caller
1642
- * @returns True when the host accepted the bytes without reporting a fault; false when the channel was closed, destroyed, ended, failed, or remained unconfirmed through `delivery`
1643
- */
1644
- write(bytes) {
1645
- return this.#engine.deliver(bytes);
1646
- }
1647
- /**
1648
- * Closes the standard-input channel and leaves the child running.
1649
- *
1650
- * @remarks
1651
- * Never rejects and terminates nothing: `stopping` stays false and the terminal moment does not
1652
- * arrive. Every call shares one barrier, which resolves after the host flushes the writes it had
1653
- * already accepted. A later `write` resolves `false`.
1654
- *
1655
- * @returns The stable barrier shared by every call
1656
- */
1657
- end() {
1658
- return this.#engine.end();
1659
- }
1660
- /**
1661
- * Terminates the child process tree and reaches the terminal observation moment.
1662
- *
1663
- * @remarks
1664
- * Never rejects, and every call shares one termination bounded by `grace` and `drain`.
1665
- *
1666
- * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
1667
- */
1668
- stop() {
1669
- return this.#engine.stop();
1670
- }
1671
- /**
1672
- * Stops the child and destroys the observation emitter after the terminal state freezes.
1673
- *
1674
- * @remarks
1675
- * Always resolves, including when termination was never confirmed. Every call shares one barrier.
1676
- *
1677
- * @returns The stable barrier shared by every call
1678
- */
1679
- destroy() {
1680
- return this.#engine.destroy();
1681
- }
1682
- #publish(chunk) {
1683
- if (!Buffer.isBuffer(chunk) || this.#engine.settled) return;
1684
- this.#emitter.emit("stdout", new Uint8Array(chunk));
1685
- }
1686
- #reportStderr(chunk) {
1687
- this.#emitter.emit("stderr", chunk);
1688
- }
1689
- #reportFault(cause) {
1690
- this.#emitter.emit("error", cause);
1691
- }
1692
- #reportExit(exit) {
1693
- this.#emitter.emit("exit", exit);
1694
- }
1695
- #closeOutput() {
1696
- this.#engine.stdout.off("data", this.#output);
1891
+ while (this.#orphans.size > 0) {
1892
+ const orphans = [...this.#orphans];
1893
+ this.#orphans.clear();
1894
+ await Promise.allSettled(orphans.map((child) => child.destroy()));
1895
+ }
1896
+ this.#children.clear();
1897
+ this.#ids.clear();
1898
+ this.#emitter.destroy();
1697
1899
  }
1698
1900
  };
1699
1901
  //#endregion
1700
- //#region src/server/factories.ts
1902
+ //#region src/server/processes/Session.ts
1701
1903
  /**
1702
- * Creates one supervised child process.
1703
- *
1704
- * @param options - Command, workspace, termination, evidence, stdin, and observation settings
1705
- * @returns The launched {@link ProcessInterface}
1904
+ * Supervises one child and publishes its standard output as raw bytes.
1706
1905
  *
1707
- * @example
1708
- * ```ts
1709
- * import { createProcess } from '@orkestrel/process/server'
1906
+ * @remarks
1907
+ * This is the transport face of the supervision engine `Process` also composes. It frames nothing,
1908
+ * retains nothing, and decodes nothing on the output side: every chunk the host delivers becomes one
1909
+ * `stdout` event carrying that chunk's bytes, in arrival order, so a consumer speaking a
1910
+ * length-prefixed or delimiter-framed protocol reads exactly what the child wrote and frames it
1911
+ * itself. Standard error is decoded and forwarded as `stderr`, with the same byte-bounded `evidence`
1912
+ * tail a `Process` retains, because a diagnostic stream is read rather than parsed.
1710
1913
  *
1711
- * const child = createProcess({
1712
- * command: { file: 'node', arguments: ['--version'] },
1713
- * workspace: process.cwd(),
1714
- * grace: 5000,
1715
- * })
1716
- * ```
1717
- */
1718
- function createProcess(options) {
1719
- return new Process(options);
1720
- }
1721
- /**
1722
- * Creates one raw byte session over a supervised child process.
1914
+ * Each emitted chunk is this session's own copy, and a plain `Uint8Array` rather than the host buffer
1915
+ * the read produced, so a consumer keeps it, mutates it, concatenates it, and reads its whole backing
1916
+ * buffer without reaching memory the host still manages and without depending on a host type.
1723
1917
  *
1724
- * @param options - Command, workspace, termination, evidence, stdin, and observation settings
1725
- * @returns The launched {@link SessionInterface}
1918
+ * The standard-input channel is open from the spawn, and `end` closes it without terminating
1919
+ * anything. That is the member a cooperative protocol reaches for: the child observes end of input
1920
+ * and exits on its own, which `ending` reports. `stop` and `destroy` remain the terminations, and
1921
+ * `exit` remains the terminal moment — the native exit plus at most `drain` while a descendant holds
1922
+ * the inherited read ends.
1726
1923
  *
1727
1924
  * @example
1728
1925
  * ```ts
1729
- * import { createSession } from '@orkestrel/process/server'
1926
+ * import { Session } from '@orkestrel/process/server'
1730
1927
  *
1731
- * const session = createSession({
1732
- * command: { file: 'node', arguments: ['server.js', '--stdio'] },
1928
+ * const session = new Session({
1929
+ * command: { file: 'node', arguments: ['-e', 'process.stdin.pipe(process.stdout)'] },
1733
1930
  * workspace: process.cwd(),
1734
- * grace: 5000,
1735
1931
  * })
1736
1932
  * ```
1737
1933
  */
1738
- function createSession(options) {
1739
- return new Session(options);
1740
- }
1741
- /**
1742
- * Creates one keyed registry of supervised child processes.
1743
- *
1744
- * @param options - Initial fleet-level observation hooks and listener-error handling
1745
- * @returns The {@link ProcessManagerInterface} registry
1746
- *
1747
- * @example
1748
- * ```ts
1749
- * import { createProcessManager } from '@orkestrel/process/server'
1750
- *
1751
- * const manager = createProcessManager()
1752
- * ```
1753
- */
1754
- function createProcessManager(options) {
1755
- return new ProcessManager(options);
1756
- }
1757
- //#endregion
1758
- //#region src/server/Retention.ts
1759
- /**
1760
- * Accumulates byte totals while retaining a bounded stream head.
1761
- *
1762
- * @example
1763
- * ```ts
1764
- * const retention = new Retention()
1765
- * retention.retain(Buffer.from('hello'), 3)?.toString('utf8') // 'hel'
1766
- * retention.retained // 3
1767
- * ```
1768
- */
1769
- var Retention = class {
1770
- #delivered = 0;
1771
- #retained = 0;
1772
- /** The bytes delivered by the stream. */
1773
- get delivered() {
1774
- return this.#delivered;
1775
- }
1776
- /** The bytes retained from the stream head. */
1777
- get retained() {
1778
- return this.#retained;
1779
- }
1934
+ var Session = class {
1935
+ #emitter;
1936
+ #engine;
1937
+ #output;
1780
1938
  /**
1781
- * Retains a delivered chunk within a byte limit.
1939
+ * Spawns one child process and begins byte capture.
1782
1940
  *
1783
- * @param chunk - The delivered chunk, ignored when it is not a buffer
1784
- * @param limit - The maximum retained byte count
1785
- * @returns The retained slice, or `undefined` when the chunk contributes no retained bytes
1941
+ * @param options - Command, workspace, termination, evidence, stdin, and observation settings
1942
+ * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
1786
1943
  */
1787
- retain(chunk, limit) {
1788
- if (!Buffer.isBuffer(chunk)) return void 0;
1789
- this.#delivered += chunk.byteLength;
1790
- const room = limit - this.#retained;
1791
- if (room <= 0) return void 0;
1792
- const slice = chunk.byteLength <= room ? chunk : Buffer.from(chunk.subarray(0, room));
1793
- this.#retained += slice.byteLength;
1794
- return slice;
1795
- }
1796
- };
1797
- //#endregion
1798
- //#region src/server/execution/execute.ts
1799
- /**
1800
- * Runs one command to completion, buffering its output, and settles with the outcome.
1801
- *
1802
- * @remarks
1803
- * The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
1804
- * child is detached, which is what lets its whole process group be terminated. Standard output and
1805
- * error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
1806
- * whether either stream exceeded it. A positive `timeout` and an aborting `signal` both terminate the
1807
- * child through the same bounded stop, and only the earliest is recorded: a timeout reports
1808
- * `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
1809
- * a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
1810
- * covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
1811
- * rather than on process exit, so a descendant that inherited the child's stdio holds the run open
1812
- * after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
1813
- * the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
1814
- * overrides `command.input`. A host fault while writing that input terminates the run by design and
1815
- * marks its result failed; a strict rejection carries the host fault as its cause. This differs
1816
- * from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
1817
- * is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
1818
- * {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
1819
- * option or command string rejects before the child is spawned, because an async function cannot
1820
- * throw synchronously.
1821
- *
1822
- * @param command - The executable, arguments, and optional environment and input
1823
- * @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
1824
- * @returns The settled run outcome
1825
- * @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
1826
- *
1827
- * @example
1828
- * ```ts
1829
- * const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
1830
- * ```
1831
- */
1832
- async function execute(command, options) {
1833
- const snapshot = snapshotCommand(command);
1834
- const optionEnvironment = options?.environment;
1835
- const optionWorkspace = options?.workspace;
1836
- const optionInput = options?.input;
1837
- const optionTimeout = options?.timeout;
1838
- const optionGrace = options?.grace;
1839
- const optionSignal = options?.signal;
1840
- const optionStrict = options?.strict;
1841
- const optionLimit = options?.limit;
1842
- validateCommand(snapshot);
1843
- validateEnvironment(optionEnvironment);
1844
- validateWorkspace(optionWorkspace);
1845
- validateTimer(optionTimeout, "option 'timeout'");
1846
- validateTimer(optionGrace, "option 'grace'");
1847
- validateBytes(optionLimit, "option 'limit'", 0);
1848
- const limit = optionLimit ?? PROCESS_OUTPUT;
1849
- const grace = optionGrace ?? PROCESS_GRACE;
1850
- const timeout = optionTimeout ?? 0;
1851
- const strict = optionStrict ?? true;
1852
- const text = optionInput ?? snapshot.input;
1853
- const line = formatCommand(snapshot);
1854
- const workspace = optionWorkspace ?? process.cwd();
1855
- const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
1856
- const plan = buildSpawn(snapshot, {
1857
- workspace,
1858
- environment
1859
- });
1860
- const settled = Promise.withResolvers();
1861
- const terminate = new AbortController();
1862
- const cleanup = new AbortController();
1863
- const finish = new AbortController();
1864
- const inputFailure = new AbortController();
1865
- const outChunks = [];
1866
- const errChunks = [];
1867
- const outRetention = new Retention();
1868
- const errRetention = new Retention();
1869
- let spawned = false;
1870
- let expired = false;
1871
- let aborted = false;
1872
- let cause;
1873
- let timeoutTimer;
1874
- let confirmTimer;
1875
- const child = spawn(plan.file, [...plan.arguments], {
1876
- cwd: workspace,
1877
- detached: process.platform !== "win32",
1878
- env: environment,
1879
- stdio: [
1880
- "pipe",
1881
- "pipe",
1882
- "pipe"
1883
- ],
1884
- windowsHide: true,
1885
- windowsVerbatimArguments: plan.verbatim
1886
- });
1887
- finish.signal.addEventListener("abort", () => {
1888
- clearTimeout(timeoutTimer);
1889
- clearTimeout(confirmTimer);
1890
- cleanup.abort();
1891
- settled.resolve(buildExecuteResult({
1892
- command: line,
1893
- stdout: Buffer.concat(outChunks),
1894
- stderr: Buffer.concat(errChunks),
1895
- code: child.exitCode,
1896
- signal: child.signalCode,
1897
- expired,
1898
- aborted,
1899
- truncated: outRetention.delivered > limit || errRetention.delivered > limit,
1900
- limit,
1901
- ...cause === void 0 ? {} : { cause }
1902
- }));
1903
- }, { once: true });
1904
- terminate.signal.addEventListener("abort", () => {
1905
- if (finish.signal.aborted) return;
1906
- clearTimeout(timeoutTimer);
1907
- cleanup.abort();
1908
- stopChild(child, grace, PROCESS_CONFIRMATION).then(() => {
1909
- if (finish.signal.aborted) return;
1910
- confirmTimer = setTimeout(() => finish.abort(), PROCESS_CONFIRMATION);
1944
+ constructor(options) {
1945
+ const command = options.command;
1946
+ const workspace = options.workspace;
1947
+ const grace = options.grace;
1948
+ const drain = options.drain;
1949
+ const evidence = options.evidence;
1950
+ const delivery = options.delivery;
1951
+ const signal = options.signal;
1952
+ const on = options.on;
1953
+ const error = options.error;
1954
+ this.#emitter = new Emitter({
1955
+ ...on === void 0 ? {} : { on },
1956
+ ...error === void 0 ? {} : { error }
1911
1957
  });
1912
- }, { once: true });
1913
- inputFailure.signal.addEventListener("abort", () => {
1914
- if (finish.signal.aborted) return;
1915
- cause = inputFailure.signal.reason;
1916
- terminate.abort();
1917
- }, { once: true });
1918
- child.stdin.on("error", (error) => inputFailure.abort(error));
1919
- child.stdout.on("data", (chunk) => {
1920
- const retained = outRetention.retain(chunk, limit);
1921
- if (retained !== void 0) outChunks.push(retained);
1922
- });
1923
- child.stderr.on("data", (chunk) => {
1924
- const retained = errRetention.retain(chunk, limit);
1925
- if (retained !== void 0) errChunks.push(retained);
1926
- });
1927
- child.once("spawn", () => {
1928
- spawned = true;
1929
- });
1930
- child.once("error", (error) => {
1931
- if (spawned) return;
1932
- cause = error;
1933
- finish.abort();
1934
- });
1935
- child.once("close", () => finish.abort());
1936
- if (text !== void 0) child.stdin.write(text, (error) => {
1937
- if (error !== void 0 && error !== null) inputFailure.abort(error);
1938
- });
1939
- child.stdin.end();
1940
- if (timeout > 0) timeoutTimer = setTimeout(() => {
1941
- expired = true;
1942
- terminate.abort();
1943
- }, timeout);
1944
- if (optionSignal !== void 0) {
1945
- optionSignal.addEventListener("abort", () => {
1946
- aborted = true;
1947
- terminate.abort();
1958
+ this.#output = this.#publish.bind(this);
1959
+ this.#engine = new Supervisor({
1960
+ command,
1961
+ workspace,
1962
+ writable: true,
1963
+ ...grace === void 0 ? {} : { grace },
1964
+ ...drain === void 0 ? {} : { drain },
1965
+ ...evidence === void 0 ? {} : { evidence },
1966
+ ...delivery === void 0 ? {} : { delivery },
1967
+ ...signal === void 0 ? {} : { signal }
1948
1968
  }, {
1949
- once: true,
1950
- signal: cleanup.signal
1969
+ chunk: this.#reportStderr.bind(this),
1970
+ fault: this.#reportFault.bind(this),
1971
+ close: this.#closeOutput.bind(this),
1972
+ terminal: this.#reportExit.bind(this),
1973
+ teardown: this.#emitter.destroy.bind(this.#emitter)
1951
1974
  });
1952
- if (optionSignal.aborted) {
1953
- aborted = true;
1954
- terminate.abort();
1955
- }
1975
+ this.#engine.stdout.on("data", this.#output);
1956
1976
  }
1957
- const result = await settled.promise;
1958
- if (result.failed && strict) {
1959
- if (cause !== void 0 && cause === inputFailure.signal.reason) throw new ProcessError(`Command '${result.command}' failed while writing standard input`, {
1960
- code: "input",
1961
- context: {
1962
- command: result.command,
1963
- code: result.code,
1964
- signal: result.signal
1965
- },
1966
- cause: inputFailure.signal.reason,
1967
- result
1968
- });
1969
- throw createExecuteError(result, cause);
1977
+ /** Holds the host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
1978
+ get pid() {
1979
+ return this.#engine.pid;
1970
1980
  }
1971
- return result;
1972
- }
1981
+ /** Holds the exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
1982
+ get code() {
1983
+ return this.#engine.code;
1984
+ }
1985
+ /** Holds the terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
1986
+ get signal() {
1987
+ return this.#engine.signal;
1988
+ }
1989
+ /** Holds the typed lifecycle observation surface. */
1990
+ get emitter() {
1991
+ return this.#emitter;
1992
+ }
1993
+ /** Holds the decoded byte-bounded stderr tail, frozen at the terminal moment. */
1994
+ get evidence() {
1995
+ return this.#engine.evidence;
1996
+ }
1997
+ /** Reports whether the terminal moment arrived and `exit` settled. */
1998
+ get settled() {
1999
+ return this.#engine.settled;
2000
+ }
2001
+ /** Reports whether termination began, including after the terminal moment. `end` never turns it true. */
2002
+ get stopping() {
2003
+ return this.#engine.stopping;
2004
+ }
2005
+ /** Settles at the child's own ending, awaited without the terminal moment's drain window. */
2006
+ get ending() {
2007
+ return this.#engine.ending;
2008
+ }
2009
+ /** Settles with the terminal child state, observed once after stream close or the drain cutoff. */
2010
+ get exit() {
2011
+ return this.#engine.exit;
2012
+ }
2013
+ /**
2014
+ * Writes raw bytes to the open standard-input channel.
2015
+ *
2016
+ * @remarks
2017
+ * Never rejects, and appends no terminator: the child receives exactly these bytes. `true` means
2018
+ * the host accepted them without reporting a fault; it does not prove that the child read them.
2019
+ * The channel can queue the payload, so treat the array as owned by the channel until the
2020
+ * returned promise settles.
2021
+ *
2022
+ * @param bytes - The payload to write, already framed by the caller
2023
+ * @returns True if the host accepted the bytes without reporting a fault; false otherwise (the channel was closed, destroyed, ended, failed, or the write remained unconfirmed through `delivery`)
2024
+ */
2025
+ write(bytes) {
2026
+ return this.#engine.deliver(bytes);
2027
+ }
2028
+ /**
2029
+ * Closes the standard-input channel and leaves the child running.
2030
+ *
2031
+ * @remarks
2032
+ * Never rejects and terminates nothing: `stopping` stays false and the terminal moment does not
2033
+ * arrive. Every call shares one barrier, which resolves after the host flushes the writes it had
2034
+ * already accepted. A later `write` resolves `false`.
2035
+ *
2036
+ * @returns The stable barrier shared by every call
2037
+ */
2038
+ end() {
2039
+ return this.#engine.end();
2040
+ }
2041
+ /**
2042
+ * Terminates the child process tree and reaches the terminal observation moment.
2043
+ *
2044
+ * @remarks
2045
+ * Never rejects, and every call shares one termination bounded by `grace` and `drain`.
2046
+ *
2047
+ * @returns True if the child's native exit was observed; false otherwise (the confirmation deadline elapsed without it)
2048
+ */
2049
+ stop() {
2050
+ return this.#engine.stop();
2051
+ }
2052
+ /**
2053
+ * Stops the child and destroys the observation emitter after the terminal state freezes.
2054
+ *
2055
+ * @remarks
2056
+ * Always resolves, including when termination was never confirmed. Every call shares one barrier.
2057
+ *
2058
+ * @returns The stable barrier shared by every call
2059
+ */
2060
+ destroy() {
2061
+ return this.#engine.destroy();
2062
+ }
2063
+ #publish(chunk) {
2064
+ if (!Buffer.isBuffer(chunk) || this.#engine.settled) return;
2065
+ this.#emitter.emit("stdout", new Uint8Array(chunk));
2066
+ }
2067
+ #reportStderr(chunk) {
2068
+ this.#emitter.emit("stderr", chunk);
2069
+ }
2070
+ #reportFault(cause) {
2071
+ this.#emitter.emit("error", cause);
2072
+ }
2073
+ #reportExit(exit) {
2074
+ this.#emitter.emit("exit", exit);
2075
+ }
2076
+ #closeOutput() {
2077
+ this.#engine.stdout.off("data", this.#output);
2078
+ }
2079
+ };
1973
2080
  //#endregion
1974
- //#region src/server/execution/executeSync.ts
2081
+ //#region src/server/factories.ts
1975
2082
  /**
1976
- * Runs one command to completion synchronously, buffering its output, and returns the outcome.
1977
- *
1978
- * @remarks
1979
- * The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
1980
- * shell. The host offers no cooperative termination window and no in-flight cancellation, so this
1981
- * contract carries neither. A positive `timeout` ends only the root process and can leave
1982
- * descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
1983
- * timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
1984
- * and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
1985
- * reports `truncated` without failing. The environment and input follow the same merge as
1986
- * {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
1987
- * carrying the {@link ExecuteResult}.
2083
+ * Creates one supervised child process.
1988
2084
  *
1989
- * @param command - The executable, arguments, and optional environment and input
1990
- * @param options - Working directory, timeout, capture limit, and failure delivery
1991
- * @returns The run outcome
1992
- * @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
2085
+ * @param options - Command, workspace, termination, evidence, stdin, and observation settings
2086
+ * @returns The launched {@link ProcessInterface}
1993
2087
  *
1994
2088
  * @example
1995
2089
  * ```ts
1996
- * const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
2090
+ * import { createProcess } from '@orkestrel/process/server'
2091
+ *
2092
+ * const child = createProcess({
2093
+ * command: { file: 'node', arguments: ['--version'] },
2094
+ * workspace: process.cwd(),
2095
+ * grace: 5000,
2096
+ * })
1997
2097
  * ```
1998
2098
  */
1999
- function executeSync(command, options) {
2000
- const snapshot = snapshotCommand(command);
2001
- const optionEnvironment = options?.environment;
2002
- const optionWorkspace = options?.workspace;
2003
- const optionInput = options?.input;
2004
- const optionTimeout = options?.timeout;
2005
- const optionStrict = options?.strict;
2006
- const optionLimit = options?.limit;
2007
- validateCommand(snapshot);
2008
- validateEnvironment(optionEnvironment);
2009
- validateWorkspace(optionWorkspace);
2010
- validateTimer(optionTimeout, "option 'timeout'");
2011
- validateBytes(optionLimit, "option 'limit'", 0);
2012
- const limit = optionLimit ?? PROCESS_OUTPUT;
2013
- const timeout = optionTimeout ?? 0;
2014
- const strict = optionStrict ?? true;
2015
- const text = optionInput ?? snapshot.input;
2016
- const line = formatCommand(snapshot);
2017
- const workspace = optionWorkspace ?? process.cwd();
2018
- const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
2019
- const plan = buildSpawn(snapshot, {
2020
- workspace,
2021
- environment
2022
- });
2023
- const outcome = spawnSync(plan.file, [...plan.arguments], {
2024
- cwd: workspace,
2025
- env: environment,
2026
- encoding: "buffer",
2027
- maxBuffer: limit,
2028
- killSignal: "SIGKILL",
2029
- windowsHide: true,
2030
- windowsVerbatimArguments: plan.verbatim,
2031
- ...text !== void 0 ? { input: Buffer.from(text) } : {},
2032
- ...timeout > 0 ? { timeout } : {}
2033
- });
2034
- const error = outcome.error;
2035
- const fault = error !== void 0 && "code" in error && isString(error.code) ? error.code : "";
2036
- const result = buildExecuteResult({
2037
- command: line,
2038
- stdout: Buffer.isBuffer(outcome.stdout) ? outcome.stdout : Buffer.alloc(0),
2039
- stderr: Buffer.isBuffer(outcome.stderr) ? outcome.stderr : Buffer.alloc(0),
2040
- code: outcome.status,
2041
- signal: outcome.signal,
2042
- expired: fault === "ETIMEDOUT",
2043
- aborted: false,
2044
- truncated: fault === "ENOBUFS",
2045
- limit,
2046
- ...error === void 0 ? {} : { cause: error }
2047
- });
2048
- if (result.failed && strict) throw createExecuteError(result, error);
2049
- return result;
2099
+ function createProcess(options) {
2100
+ return new Process(options);
2050
2101
  }
2051
- //#endregion
2052
- //#region src/server/execution/detach.ts
2053
2102
  /**
2054
- * Spawns one command as a detached process and returns without waiting for it.
2103
+ * Creates one raw byte session over a supervised child process.
2055
2104
  *
2056
- * @remarks
2057
- * The child owns no stdio and is unreferenced, so it outlives this process and nothing here observes
2058
- * its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
2059
- * fault is swallowed rather than crashing the caller; an invalid command is refused before anything
2060
- * is spawned.
2105
+ * @param options - Command, workspace, termination, evidence, stdin, and observation settings
2106
+ * @returns The launched {@link SessionInterface}
2061
2107
  *
2062
- * @param command - The executable, arguments, and optional environment
2063
- * @param options - The working directory the detached child starts in
2064
- * @returns Nothing
2065
- * @throws A {@link ProcessError} coded `invalid` when the working directory or a command string is malformed
2108
+ * @example
2109
+ * ```ts
2110
+ * import { createSession } from '@orkestrel/process/server'
2111
+ *
2112
+ * const session = createSession({
2113
+ * command: { file: 'node', arguments: ['server.js', '--stdio'] },
2114
+ * workspace: process.cwd(),
2115
+ * grace: 5000,
2116
+ * })
2117
+ * ```
2118
+ */
2119
+ function createSession(options) {
2120
+ return new Session(options);
2121
+ }
2122
+ /**
2123
+ * Creates one keyed registry of supervised child processes.
2124
+ *
2125
+ * @param options - Initial fleet-level observation hooks and listener-error handling
2126
+ * @returns The {@link ProcessManagerInterface} registry
2066
2127
  *
2067
2128
  * @example
2068
2129
  * ```ts
2069
- * detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
2130
+ * import { createProcessManager } from '@orkestrel/process/server'
2131
+ *
2132
+ * const manager = createProcessManager()
2070
2133
  * ```
2071
2134
  */
2072
- function detach(command, options) {
2073
- const snapshot = snapshotCommand(command);
2074
- const optionWorkspace = options?.workspace;
2075
- validateCommand(snapshot);
2076
- validateWorkspace(optionWorkspace);
2077
- const workspace = optionWorkspace ?? process.cwd();
2078
- const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment);
2079
- const plan = buildSpawn(snapshot, {
2080
- workspace,
2081
- environment
2082
- });
2083
- const child = spawn(plan.file, [...plan.arguments], {
2084
- cwd: workspace,
2085
- detached: true,
2086
- env: environment,
2087
- stdio: "ignore",
2088
- windowsHide: true,
2089
- windowsVerbatimArguments: plan.verbatim
2090
- });
2091
- child.once("error", () => void 0);
2092
- child.unref();
2135
+ function createProcessManager(options) {
2136
+ return new ProcessManager(options);
2093
2137
  }
2094
2138
  //#endregion
2095
- export { Process, ProcessManager, Retention, Session, buildExecutableCandidates, buildExecuteResult, buildPlatformSpawn, buildSpawn, createProcess, createProcessManager, createSession, detach, execute, executeSync, formatCommand, isExited, isFile, killProcess, killTree, mergeEnvironment, mergePlatformEnvironment, quoteArgument, readPlatformVariable, readVariable, resolveExecutable, snapshotCommand, stopChild, trimHead, trimTail, validateBytes, validateCommand, validateEnvironment, validateText, validateTimer, validateWorkspace, waitForClose, waitForExit };
2139
+ export { Process, ProcessManager, Session, Supervisor, buildExecutableCandidates, buildExecuteResult, buildPlatformSpawn, buildSpawn, captureChunk, createProcess, createProcessManager, createSession, detach, execute, executeSync, formatCommand, isExited, isFile, killProcess, killTree, mergeEnvironment, mergePlatformEnvironment, quoteArgument, readPlatformVariable, readVariable, resolveExecutable, snapshotCommand, stopChild, trimHead, trimTail, validateBytes, validateCommand, validateEnvironment, validateText, validateTimer, validateWorkspace, waitForClose, waitForExit };
2096
2140
 
2097
2141
  //# sourceMappingURL=index.js.map