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