@orkestrel/process 0.0.8 → 0.0.9

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.
@@ -42,9 +42,12 @@ function trimTail(bytes, limit) {
42
42
  * Trims a buffer to at most `limit` leading bytes without splitting a UTF-8 sequence.
43
43
  *
44
44
  * @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.
45
+ * Keeps the head — the captured start of a one-shot run's output. The end retreats only when a
46
+ * sequence genuinely spans the cut: the byte at `limit` carries the continuation bit pattern, and a
47
+ * lead byte within the preceding three bytes declares a length that reaches it. The retained bytes
48
+ * then end on a code-point boundary and decode without a replacement character. A byte at `limit`
49
+ * that carries the continuation pattern without such a lead byte is invalid UTF-8 rather than a
50
+ * split sequence, so the full `limit` survives instead of a valid byte being dropped for it.
48
51
  *
49
52
  * @param bytes - The accumulated bytes
50
53
  * @param limit - The maximum retained byte count
@@ -58,13 +61,50 @@ function trimTail(bytes, limit) {
58
61
  function trimHead(bytes, limit) {
59
62
  const buffer = node_buffer.Buffer.isBuffer(bytes) ? bytes : node_buffer.Buffer.from(bytes);
60
63
  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;
64
+ const excluded = buffer[limit];
65
+ if (excluded === void 0 || (excluded & 192) !== 128) return node_buffer.Buffer.from(buffer.subarray(0, limit));
66
+ const floor = Math.max(0, limit - 3);
67
+ let start = limit - 1;
68
+ while (start >= floor) {
69
+ const lead = buffer[start];
70
+ if (lead !== void 0 && (lead & 192) !== 128) {
71
+ let span = 1;
72
+ if ((lead & 224) === 192) span = 2;
73
+ else if ((lead & 240) === 224) span = 3;
74
+ else if ((lead & 248) === 240) span = 4;
75
+ if (start + span > limit) return node_buffer.Buffer.from(buffer.subarray(0, start));
76
+ break;
77
+ }
78
+ start -= 1;
66
79
  }
67
- return node_buffer.Buffer.from(buffer.subarray(0, end));
80
+ return node_buffer.Buffer.from(buffer.subarray(0, limit));
81
+ }
82
+ /**
83
+ * Bounds one delivered stream chunk to the bytes a capture still has room for.
84
+ *
85
+ * @remarks
86
+ * A chunk that is not a buffer contributes nothing and reports `undefined` rather than throwing,
87
+ * because a stream `data` listener receives an `unknown` payload. The cut is byte-exact and might
88
+ * land inside a multibyte sequence, because {@link buildExecuteResult} performs the single
89
+ * code-point-boundary trim over the whole capture rather than over each chunk. Give the capture one
90
+ * byte more room than its limit, so that final trim can read the first excluded byte and retreat off
91
+ * a split sequence. A chunk that fits its room is returned as the delivered buffer itself rather
92
+ * than a copy, because a capture appending it copies again and never writes through it; a chunk that
93
+ * overruns its room returns a copy of the bytes that fit. Treat the return as read-only either way.
94
+ *
95
+ * @param chunk - The delivered chunk, ignored when it is not a buffer
96
+ * @param room - The bytes the capture still accepts
97
+ * @returns The bytes to retain, or `undefined` when the chunk contributes none
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * captureChunk(Buffer.from('hello'), 3) // <Buffer 68 65 6c>
102
+ * ```
103
+ */
104
+ function captureChunk(chunk, room) {
105
+ if (!node_buffer.Buffer.isBuffer(chunk) || room <= 0) return void 0;
106
+ if (chunk.byteLength <= room) return chunk;
107
+ return node_buffer.Buffer.from(chunk.subarray(0, room));
68
108
  }
69
109
  /**
70
110
  * Takes one owned frozen snapshot of a caller's command.
@@ -692,538 +732,536 @@ function buildExecuteResult(input) {
692
732
  truncated: input.truncated
693
733
  });
694
734
  }
695
- //#endregion
696
- //#region src/server/Supervisor.ts
697
735
  /**
698
- * Supervises one child process and reports each lifecycle moment to the face composing it.
736
+ * Runs one command to completion, buffering its output, and settles with the outcome.
699
737
  *
700
738
  * @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.
739
+ * The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
740
+ * child is detached, which is what lets its whole process group be terminated. Standard output and
741
+ * error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
742
+ * whether either stream exceeded it. Each capture keeps one byte beyond `limit`, so the single
743
+ * code-point-boundary trim in {@link buildExecuteResult} reads the first excluded byte and never
744
+ * delivers a split multibyte sequence. A positive `timeout` and an aborting `signal` both terminate the
745
+ * child through the same bounded stop, and only the earliest is recorded: a timeout reports
746
+ * `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
747
+ * a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
748
+ * covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
749
+ * rather than on process exit, so a descendant that inherited the child's stdio holds the run open
750
+ * after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
751
+ * the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
752
+ * overrides `command.input`. A host fault while writing that input terminates the run by design and
753
+ * marks its result failed; a strict rejection carries the host fault as its cause. This differs
754
+ * from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
755
+ * is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
756
+ * {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
757
+ * option or command string rejects before the child is spawned, because an async function cannot
758
+ * throw synchronously.
707
759
  *
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.
760
+ * @param command - The executable, arguments, and optional environment and input
761
+ * @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
762
+ * @returns The settled run outcome
763
+ * @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
764
  *
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.
765
+ * @example
766
+ * ```ts
767
+ * const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
768
+ * ```
725
769
  */
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
770
+ async function execute(command, options) {
771
+ const snapshot = snapshotCommand(command);
772
+ const optionEnvironment = options?.environment;
773
+ const optionWorkspace = options?.workspace;
774
+ const optionInput = options?.input;
775
+ const optionTimeout = options?.timeout;
776
+ const optionGrace = options?.grace;
777
+ const optionSignal = options?.signal;
778
+ const optionStrict = options?.strict;
779
+ const optionLimit = options?.limit;
780
+ validateCommand(snapshot);
781
+ validateEnvironment(optionEnvironment);
782
+ validateWorkspace(optionWorkspace);
783
+ validateTimer(optionTimeout, "option 'timeout'");
784
+ validateTimer(optionGrace, "option 'grace'");
785
+ validateBytes(optionLimit, "option 'limit'", 0);
786
+ const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
787
+ const grace = optionGrace ?? _src_core.PROCESS_GRACE;
788
+ const timeout = optionTimeout ?? 0;
789
+ const strict = optionStrict ?? true;
790
+ const text = optionInput ?? snapshot.input;
791
+ const line = formatCommand(snapshot);
792
+ const workspace = optionWorkspace ?? process.cwd();
793
+ const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
794
+ const plan = buildSpawn(snapshot, {
795
+ workspace,
796
+ environment
797
+ });
798
+ const settled = Promise.withResolvers();
799
+ const terminate = new AbortController();
800
+ const cleanup = new AbortController();
801
+ const finish = new AbortController();
802
+ const inputFailure = new AbortController();
803
+ const outChunks = [];
804
+ const errChunks = [];
805
+ let spawned = false;
806
+ let expired = false;
807
+ let aborted = false;
808
+ let outRetained = 0;
809
+ let errRetained = 0;
810
+ let cause;
811
+ let timeoutTimer;
812
+ let confirmTimer;
813
+ const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
814
+ cwd: workspace,
815
+ detached: process.platform !== "win32",
816
+ env: environment,
817
+ stdio: [
818
+ "pipe",
819
+ "pipe",
820
+ "pipe"
821
+ ],
822
+ windowsHide: true,
823
+ windowsVerbatimArguments: plan.verbatim
824
+ });
825
+ finish.signal.addEventListener("abort", () => {
826
+ clearTimeout(timeoutTimer);
827
+ clearTimeout(confirmTimer);
828
+ cleanup.abort();
829
+ settled.resolve(buildExecuteResult({
830
+ command: line,
831
+ stdout: node_buffer.Buffer.concat(outChunks),
832
+ stderr: node_buffer.Buffer.concat(errChunks),
833
+ code: child.exitCode,
834
+ signal: child.signalCode,
835
+ expired,
836
+ aborted,
837
+ truncated: outRetained > limit || errRetained > limit,
838
+ limit,
839
+ ...cause === void 0 ? {} : { cause }
840
+ }));
841
+ }, { once: true });
842
+ terminate.signal.addEventListener("abort", () => {
843
+ if (finish.signal.aborted) return;
844
+ clearTimeout(timeoutTimer);
845
+ cleanup.abort();
846
+ stopChild(child, grace, _src_core.PROCESS_CONFIRMATION).then(() => {
847
+ if (finish.signal.aborted) return;
848
+ confirmTimer = setTimeout(() => finish.abort(), _src_core.PROCESS_CONFIRMATION);
811
849
  });
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
880
- * does not prove that the child read them. An ordinary write settles when the kernel accepts it.
881
- * Only a full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and
882
- * every terminal teardown path settles pending writes. After `stop` or `destroy` begins, a later
883
- * call settles `false`, because teardown cannot confirm delivery for bytes it is about to
884
- * discard.
885
- *
886
- * @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`
888
- */
889
- deliver(bytes) {
890
- const stdin = this.#child.stdin;
891
- if (this.#settled || this.#stopping || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
892
- const settled = Promise.withResolvers();
893
- this.#writes.set(settled, void 0);
894
- try {
895
- stdin.write(bytes, this.#confirmWrite.bind(this, settled));
896
- } catch {
897
- this.#settleWrite(settled, false);
898
- return settled.promise;
899
- }
900
- if (this.#delivery > 0 && this.#writes.has(settled)) {
901
- const timer = setTimeout(() => this.#settleWrite(settled, false), this.#delivery);
902
- timer.unref();
903
- this.#writes.set(settled, timer);
904
- }
905
- return settled.promise;
906
- }
907
- /**
908
- * Closes the standard-input channel while leaving the child running.
909
- *
910
- * @remarks
911
- * Never rejects, and terminates nothing: no signal is sent, `stopping` stays false, no drain
912
- * window is armed, and the terminal moment does not arrive. The child observes end of input and
913
- * decides for itself what to do about it, which is what separates this from `stop`.
914
- *
915
- * Every call shares one barrier, and it resolves after the host flushes the writes it had already
916
- * accepted, or reports that it cannot. A `deliver` after this call resolves `false`, because the
917
- * host stops reporting the channel writable the moment it is ended. That same flag keeps the
918
- * ended channel quiet for its remaining life, so a later host fault on it settles pending writes
919
- * and reaches no face. After `stop` or `destroy` has begun, or after the terminal moment, the
920
- * call resolves and changes nothing, because the channel is already being discarded.
921
- *
922
- * @returns The stable barrier shared by every call
923
- */
924
- end() {
925
- if (this.#closure !== void 0) return this.#closure;
926
- this.#closure = this.#endInput();
927
- return this.#closure;
928
- }
929
- /**
930
- * Terminates the child process tree and reaches the terminal observation moment.
931
- *
932
- * @remarks
933
- * Never rejects, and every call shares one termination. After the termination sequence returns,
934
- * confirmed or not, it waits at most `drain` for the read channels to close, then cuts them off
935
- * and settles `exit` when they remain open. A cutoff reached before the native exit reports the
936
- * `code` and `signal` the host had recorded by then, which is `null` for a child still running.
937
- *
938
- * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
939
- */
940
- stop() {
941
- if (this.#termination !== void 0) return this.#termination;
942
- this.#termination = this.#kill();
943
- return this.#termination;
944
- }
945
- /**
946
- * Stops the child and releases the composing face after the terminal state freezes.
947
- *
948
- * @remarks
949
- * Always resolves, including when termination was never confirmed. Every call shares one barrier.
950
- *
951
- * @returns The stable barrier shared by every call
952
- */
953
- destroy() {
954
- if (this.#destruction !== void 0) return this.#destruction;
955
- this.#destruction = this.#conclude();
956
- return this.#destruction;
957
- }
958
- #retain(chunk) {
959
- if (!node_buffer.Buffer.isBuffer(chunk) || this.#settled) return;
960
- this.#tail = trimTail(node_buffer.Buffer.concat([this.#tail, chunk]), this.#evidence);
961
- const text = this.#decoder.write(chunk);
962
- if (text.length > 0) this.#chunk(text);
963
- }
964
- #complete() {
965
- if (this.#settled) return;
966
- this.#settle(true);
967
- }
968
- #expire() {
969
- this.#ending.resolve();
970
- if (this.#settled) return;
971
- this.#wait();
972
- }
973
- #wait() {
974
- this.#waiting ??= waitForClose(this.#child, this.#drain).then(() => {
975
- if (!this.#settled) this.#settle(false);
976
- });
977
- return this.#waiting;
978
- }
979
- #settle(drained) {
980
- const suffix = this.#decoder.end();
981
- if (suffix.length > 0) this.#chunk(suffix);
982
- this.#settled = true;
983
- this.#removeAbortListener();
984
- this.#close();
985
- const exit = Object.freeze({
986
- code: this.code,
987
- signal: this.signal,
988
- drained
850
+ }, { once: true });
851
+ inputFailure.signal.addEventListener("abort", () => {
852
+ if (finish.signal.aborted) return;
853
+ cause = inputFailure.signal.reason;
854
+ terminate.abort();
855
+ }, { once: true });
856
+ child.stdin.on("error", (error) => inputFailure.abort(error));
857
+ child.stdout.on("data", (chunk) => {
858
+ const captured = captureChunk(chunk, limit + 1 - outRetained);
859
+ if (captured === void 0) return;
860
+ outRetained += captured.byteLength;
861
+ outChunks.push(captured);
862
+ });
863
+ child.stderr.on("data", (chunk) => {
864
+ const captured = captureChunk(chunk, limit + 1 - errRetained);
865
+ if (captured === void 0) return;
866
+ errRetained += captured.byteLength;
867
+ errChunks.push(captured);
868
+ });
869
+ child.once("spawn", () => {
870
+ spawned = true;
871
+ });
872
+ child.once("error", (error) => {
873
+ if (spawned) return;
874
+ cause = error;
875
+ finish.abort();
876
+ });
877
+ child.once("close", () => finish.abort());
878
+ if (text !== void 0) child.stdin.write(text, (error) => {
879
+ if (error !== void 0 && error !== null) inputFailure.abort(error);
880
+ });
881
+ child.stdin.end();
882
+ if (timeout > 0) timeoutTimer = setTimeout(() => {
883
+ expired = true;
884
+ terminate.abort();
885
+ }, timeout);
886
+ if (optionSignal !== void 0) {
887
+ optionSignal.addEventListener("abort", () => {
888
+ aborted = true;
889
+ terminate.abort();
890
+ }, {
891
+ once: true,
892
+ signal: cleanup.signal
989
893
  });
990
- this.#exit.resolve(exit);
991
- this.#ending.resolve();
992
- this.#terminal(exit);
993
- this.#child.stdout.destroy();
994
- this.#child.stderr.destroy();
995
- }
996
- #terminate() {
997
- this.stop();
998
- }
999
- #removeAbortListener() {
1000
- if (this.#signal === void 0 || this.#abort === void 0) return;
1001
- this.#signal.removeEventListener("abort", this.#abort);
1002
- }
1003
- #confirmWrite(settled, error) {
1004
- if (error === void 0 || error === null) {
1005
- this.#settleWrite(settled, true);
1006
- return;
1007
- }
1008
- this.#failInputCallback(error);
1009
- }
1010
- #completeInput() {
1011
- this.#input -= 1;
1012
- }
1013
- #endInput() {
1014
- const stdin = this.#child.stdin;
1015
- if (this.#stopping || this.#settled || stdin.writableEnded || stdin.destroyed) return Promise.resolve();
1016
- const ended = Promise.withResolvers();
1017
- stdin.end(() => ended.resolve());
1018
- return ended.promise;
1019
- }
1020
- #settleWrite(settled, accepted) {
1021
- if (!this.#writes.has(settled)) return;
1022
- const timer = this.#writes.get(settled);
1023
- this.#writes.delete(settled);
1024
- clearTimeout(timer);
1025
- settled.resolve(accepted);
1026
- }
1027
- #settleWrites() {
1028
- for (const settled of this.#writes.keys()) this.#settleWrite(settled, false);
1029
- }
1030
- #failInputStream(cause) {
1031
- if (this.#input > 0) {
1032
- this.#settleWrites();
1033
- return;
1034
- }
1035
- this.#failInputCallback(cause);
1036
- }
1037
- #failInputCallback(cause) {
1038
- if (this.#child.stdin.writableEnded) {
1039
- this.#settleWrites();
1040
- return;
894
+ if (optionSignal.aborted) {
895
+ aborted = true;
896
+ terminate.abort();
1041
897
  }
1042
- if (this.#failure !== void 0 || this.#stopping) return;
1043
- this.#failure = cause;
1044
- this.#settleWrites();
1045
- this.#fault(new _src_core.ProcessError("The standard-input channel failed", {
1046
- code: "protocol",
1047
- cause
1048
- }));
1049
898
  }
1050
- async #kill() {
1051
- this.#stopping = true;
1052
- this.#relieve?.();
1053
- const confirmed = await stopChild(this.#child, this.#grace, _src_core.PROCESS_CONFIRMATION);
1054
- this.#settleWrites();
1055
- this.#child.stdin.destroy();
1056
- if (!this.#settled) await this.#wait();
1057
- if (!this.#settled) this.#settle(false);
1058
- return confirmed;
1059
- }
1060
- async #conclude() {
1061
- await this.stop();
1062
- this.#teardown();
899
+ const result = await settled.promise;
900
+ if (result.failed && strict) {
901
+ if (cause !== void 0 && cause === inputFailure.signal.reason) throw new _src_core.ProcessError(`Command '${result.command}' failed while writing standard input`, {
902
+ code: "input",
903
+ context: {
904
+ command: result.command,
905
+ code: result.code,
906
+ signal: result.signal
907
+ },
908
+ cause: inputFailure.signal.reason,
909
+ result
910
+ });
911
+ throw (0, _src_core.createExecuteError)(result, cause);
1063
912
  }
1064
- };
913
+ return result;
914
+ }
915
+ /**
916
+ * Runs one command to completion synchronously, buffering its output, and returns the outcome.
917
+ *
918
+ * @remarks
919
+ * The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
920
+ * shell. The host offers no cooperative termination window and no in-flight cancellation, so this
921
+ * contract carries neither. A positive `timeout` ends only the root process and can leave
922
+ * descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
923
+ * timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
924
+ * and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
925
+ * reports `truncated` without failing. The environment and input follow the same merge as
926
+ * {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
927
+ * carrying the {@link ExecuteResult}.
928
+ *
929
+ * @param command - The executable, arguments, and optional environment and input
930
+ * @param options - Working directory, timeout, capture limit, and failure delivery
931
+ * @returns The run outcome
932
+ * @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`
933
+ *
934
+ * @example
935
+ * ```ts
936
+ * const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
937
+ * ```
938
+ */
939
+ function executeSync(command, options) {
940
+ const snapshot = snapshotCommand(command);
941
+ const optionEnvironment = options?.environment;
942
+ const optionWorkspace = options?.workspace;
943
+ const optionInput = options?.input;
944
+ const optionTimeout = options?.timeout;
945
+ const optionStrict = options?.strict;
946
+ const optionLimit = options?.limit;
947
+ validateCommand(snapshot);
948
+ validateEnvironment(optionEnvironment);
949
+ validateWorkspace(optionWorkspace);
950
+ validateTimer(optionTimeout, "option 'timeout'");
951
+ validateBytes(optionLimit, "option 'limit'", 0);
952
+ const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
953
+ const timeout = optionTimeout ?? 0;
954
+ const strict = optionStrict ?? true;
955
+ const text = optionInput ?? snapshot.input;
956
+ const line = formatCommand(snapshot);
957
+ const workspace = optionWorkspace ?? process.cwd();
958
+ const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
959
+ const plan = buildSpawn(snapshot, {
960
+ workspace,
961
+ environment
962
+ });
963
+ const outcome = (0, node_child_process.spawnSync)(plan.file, [...plan.arguments], {
964
+ cwd: workspace,
965
+ env: environment,
966
+ encoding: "buffer",
967
+ maxBuffer: limit,
968
+ killSignal: "SIGKILL",
969
+ windowsHide: true,
970
+ windowsVerbatimArguments: plan.verbatim,
971
+ ...text !== void 0 ? { input: node_buffer.Buffer.from(text) } : {},
972
+ ...timeout > 0 ? { timeout } : {}
973
+ });
974
+ const error = outcome.error;
975
+ const fault = error !== void 0 && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : "";
976
+ const result = buildExecuteResult({
977
+ command: line,
978
+ stdout: node_buffer.Buffer.isBuffer(outcome.stdout) ? outcome.stdout : node_buffer.Buffer.alloc(0),
979
+ stderr: node_buffer.Buffer.isBuffer(outcome.stderr) ? outcome.stderr : node_buffer.Buffer.alloc(0),
980
+ code: outcome.status,
981
+ signal: outcome.signal,
982
+ expired: fault === "ETIMEDOUT",
983
+ aborted: false,
984
+ truncated: fault === "ENOBUFS",
985
+ limit,
986
+ ...error === void 0 ? {} : { cause: error }
987
+ });
988
+ if (result.failed && strict) throw (0, _src_core.createExecuteError)(result, error);
989
+ return result;
990
+ }
991
+ /**
992
+ * Spawns one command as a detached process and returns without waiting for it.
993
+ *
994
+ * @remarks
995
+ * The child owns no stdio and is unreferenced, so it outlives this process and nothing here observes
996
+ * its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
997
+ * fault is swallowed rather than crashing the caller; an invalid command is refused before anything
998
+ * is spawned.
999
+ *
1000
+ * @param command - The executable, arguments, and optional environment
1001
+ * @param options - The working directory the detached child starts in
1002
+ * @returns Nothing
1003
+ * @throws A {@link ProcessError} coded `invalid` when the working directory or a command string is malformed
1004
+ *
1005
+ * @example
1006
+ * ```ts
1007
+ * detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
1008
+ * ```
1009
+ */
1010
+ function detach(command, options) {
1011
+ const snapshot = snapshotCommand(command);
1012
+ const optionWorkspace = options?.workspace;
1013
+ validateCommand(snapshot);
1014
+ validateWorkspace(optionWorkspace);
1015
+ const workspace = optionWorkspace ?? process.cwd();
1016
+ const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment);
1017
+ const plan = buildSpawn(snapshot, {
1018
+ workspace,
1019
+ environment
1020
+ });
1021
+ const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
1022
+ cwd: workspace,
1023
+ detached: true,
1024
+ env: environment,
1025
+ stdio: "ignore",
1026
+ windowsHide: true,
1027
+ windowsVerbatimArguments: plan.verbatim
1028
+ });
1029
+ child.once("error", () => void 0);
1030
+ child.unref();
1031
+ }
1065
1032
  //#endregion
1066
- //#region src/server/Process.ts
1033
+ //#region src/server/Supervisor.ts
1067
1034
  /**
1068
- * Supervises one child while keeping every observation channel aligned at termination.
1035
+ * Supervises one child process and reports each lifecycle moment to the face composing it.
1069
1036
  *
1070
1037
  * @remarks
1071
- * The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read
1072
- * the host child directly, so they expose native exit as soon as the host records it. `settled`,
1073
- * `exit`, `evidence`, and `lines` reach one terminal moment after the read channels close or the
1074
- * bounded `drain` window cuts them off. That window is armed by the child's native exit and by a
1075
- * termination this package initiated, so every way a child can end reaches the moment. The terminal
1076
- * routine freezes `evidence` and ends `lines` before it destroys stdout, stderr, or the emitter, so
1077
- * the push channel never disappears while a pull channel can still change or wait indefinitely.
1078
- *
1079
- * Standard output is framed through `readline` into a single-consumer stream. Lines already framed
1080
- * and queued remain available through the terminal moment, while bytes arriving after a requested
1081
- * termination may be cut off. Standard error is forwarded through the live `stderr` event and
1082
- * retained as a byte-bounded raw tail until that same moment. `stop` ends the child tree and the
1083
- * observation surfaces. `destroy` performs that stop and then destroys the emitter after the
1084
- * frozen terminal state exists.
1038
+ * This is the engine every supervised-child face shares. It owns the eager spawn, the byte-bounded
1039
+ * stderr tail and its decoded chunks, the standard-input channel and its bounded confirmation, the
1040
+ * termination sequence, and the progression from the child's native exit through the bounded `drain`
1041
+ * window to one terminal moment. It owns no output framing and no observation surface: a face
1042
+ * attaches its own consumer to `stdout`, frames bytes however its contract promises, and owns the
1043
+ * emitter and event map its consumers subscribe to.
1085
1044
  *
1086
- * @example
1087
- * ```ts
1088
- * import { Process } from '@orkestrel/process/server'
1045
+ * The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read the
1046
+ * host child directly, so they expose native exit as soon as the host records it, and `ending`
1047
+ * settles at that same moment. `settled`, `exit`, and `evidence` reach one terminal moment after the
1048
+ * read channels close or the bounded `drain` window cuts them off. That window is armed by the
1049
+ * child's native exit and by a termination this package initiated, so every way a child can end
1050
+ * reaches the moment. The terminal routine hands the face its `close` and `terminal` moments before
1051
+ * it destroys stdout and stderr, so a face's own pull surfaces are final while its read ends still
1052
+ * exist.
1089
1053
  *
1090
- * const child = new Process({
1091
- * command: { file: 'node', arguments: ['--version'] },
1092
- * workspace: process.cwd(),
1093
- * })
1094
- * const exit = await child.exit
1095
- * await child.destroy()
1096
- * ```
1054
+ * The face is a set of callbacks rather than an emitter, because each face publishes a different
1055
+ * event map over the same moments. `chunk` and `fault` carry the live stderr text and the child or
1056
+ * channel faults, `relieve` reports that a termination began so the face can release whatever
1057
+ * backpressure it holds, `close` ends the face's read pipeline at the terminal moment, `terminal`
1058
+ * carries the frozen {@link ProcessExit}, and `teardown` releases the face's own surface after the
1059
+ * termination completes. `relieve` is optional because a face that never pauses the child's output
1060
+ * holds no backpressure to release, and a face with nothing to do there would otherwise declare an
1061
+ * empty method to say so.
1097
1062
  */
1098
- var Process = class {
1099
- #emitter;
1100
- #engine;
1101
- #reader;
1102
- #backlog;
1103
- #lines;
1104
- #queue = [];
1105
- #waiters = [];
1106
- #head = 0;
1107
- #pending = 0;
1108
- #requested = false;
1109
- #paused = false;
1110
- #truncated = false;
1111
- #ended = false;
1063
+ var Supervisor = class {
1064
+ #chunk;
1065
+ #fault;
1066
+ #relieve;
1067
+ #close;
1068
+ #terminal;
1069
+ #teardown;
1070
+ #child;
1071
+ #grace;
1072
+ #drain;
1073
+ #evidence;
1074
+ #delivery;
1075
+ #decoder = new node_string_decoder.StringDecoder("utf8");
1076
+ #exit = Promise.withResolvers();
1077
+ #ending = Promise.withResolvers();
1078
+ #signal;
1079
+ #abort;
1080
+ #writes = /* @__PURE__ */ new Map();
1081
+ #tail = node_buffer.Buffer.alloc(0);
1082
+ #stopping = false;
1083
+ #settled = false;
1084
+ #input = 0;
1085
+ #failure;
1086
+ #waiting;
1087
+ #closure;
1088
+ #termination;
1089
+ #destruction;
1112
1090
  /**
1113
- * Spawn one child process and begin stream capture.
1091
+ * Spawn one child process and begin standard-error capture.
1114
1092
  *
1115
- * @param options - Command, workspace, termination, capture, stdin, and observation settings
1093
+ * @remarks
1094
+ * The face's callbacks are captured before anything is read or spawned, so the first moment the
1095
+ * child can produce already has somewhere to go. `options` supplies the command, workspace,
1096
+ * termination, capture, and standard-input settings; a face's own settings, such as an output
1097
+ * retention bound, are read and validated by that face before it constructs this engine, so an
1098
+ * invalid one refuses construction while nothing has been spawned.
1099
+ *
1100
+ * @param options - Command, workspace, termination, capture, and stdin settings
1101
+ * @param face - The composing face's callbacks for each lifecycle moment
1116
1102
  * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
1117
1103
  */
1118
- constructor(options) {
1104
+ constructor(options, face) {
1105
+ this.#chunk = face.chunk;
1106
+ this.#fault = face.fault;
1107
+ this.#relieve = face.relieve;
1108
+ this.#close = face.close;
1109
+ this.#terminal = face.terminal;
1110
+ this.#teardown = face.teardown;
1119
1111
  const source = options.command;
1120
1112
  const workspace = options.workspace;
1121
1113
  const grace = options.grace;
1122
1114
  const drain = options.drain;
1123
1115
  const evidence = options.evidence;
1124
- const backlog = options.backlog;
1125
1116
  const delivery = options.delivery;
1126
1117
  const writable = options.writable;
1127
1118
  const signal = options.signal;
1128
- const on = options.on;
1129
- const error = options.error;
1130
1119
  const command = snapshotCommand(source);
1120
+ const input = command.input;
1131
1121
  validateCommand(command);
1132
1122
  validateWorkspace(workspace);
1133
1123
  validateTimer(grace, "option 'grace'");
1134
1124
  validateTimer(drain, "option 'drain'");
1135
1125
  validateTimer(delivery, "option 'delivery'");
1136
1126
  validateBytes(evidence, "option 'evidence'", 0);
1137
- validateBytes(backlog, "option 'backlog'", 1);
1138
- this.#emitter = new _orkestrel_emitter.Emitter({
1139
- ...on === void 0 ? {} : { on },
1140
- ...error === void 0 ? {} : { error }
1141
- });
1142
- this.#backlog = backlog ?? _src_core.PROCESS_BACKLOG;
1143
- this.#lines = Object.freeze({ [Symbol.asyncIterator]: this.#iterate.bind(this) });
1144
- this.#engine = new Supervisor({
1145
- command,
1127
+ this.#grace = grace ?? _src_core.PROCESS_GRACE;
1128
+ this.#drain = drain ?? _src_core.PROCESS_DRAIN;
1129
+ this.#evidence = evidence ?? _src_core.PROCESS_EVIDENCE;
1130
+ this.#delivery = delivery ?? 0;
1131
+ this.#signal = signal;
1132
+ const childEnvironment = mergeEnvironment(command.isolated === true, command.environment);
1133
+ const plan = buildSpawn(command, {
1146
1134
  workspace,
1147
- ...grace === void 0 ? {} : { grace },
1148
- ...drain === void 0 ? {} : { drain },
1149
- ...evidence === void 0 ? {} : { evidence },
1150
- ...delivery === void 0 ? {} : { delivery },
1151
- ...writable === void 0 ? {} : { writable },
1152
- ...signal === void 0 ? {} : { signal }
1153
- }, {
1154
- chunk: this.#reportStderr.bind(this),
1155
- fault: this.#reportFault.bind(this),
1156
- relieve: this.#releaseBackpressure.bind(this),
1157
- close: this.#closeReader.bind(this),
1158
- terminal: this.#reportExit.bind(this),
1159
- teardown: this.#emitter.destroy.bind(this.#emitter)
1135
+ environment: childEnvironment
1160
1136
  });
1161
- this.#reader = (0, node_readline.createInterface)({
1162
- input: this.#engine.stdout,
1163
- crlfDelay: Infinity
1137
+ this.#child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
1138
+ cwd: workspace,
1139
+ detached: process.platform !== "win32",
1140
+ env: childEnvironment,
1141
+ stdio: [
1142
+ "pipe",
1143
+ "pipe",
1144
+ "pipe"
1145
+ ],
1146
+ windowsHide: true,
1147
+ windowsVerbatimArguments: plan.verbatim
1164
1148
  });
1165
- this.#reader.on("line", this.#push.bind(this));
1166
- this.#reader.once("close", this.#finish.bind(this));
1149
+ this.#child.once("error", this.#fault);
1150
+ this.#child.once("exit", this.#expire.bind(this));
1151
+ this.#child.once("close", this.#complete.bind(this));
1152
+ this.#child.stdin.on("error", this.#failInputStream.bind(this));
1153
+ this.#child.stderr.on("data", this.#retain.bind(this));
1154
+ if (input !== void 0) {
1155
+ this.#input += 1;
1156
+ this.#child.stdin.write(input, this.#completeInput.bind(this));
1157
+ }
1158
+ if (writable !== true) {
1159
+ this.#input += 1;
1160
+ this.#child.stdin.end(this.#completeInput.bind(this));
1161
+ }
1162
+ if (signal !== void 0) {
1163
+ this.#abort = this.#terminate.bind(this);
1164
+ signal.addEventListener("abort", this.#abort, { once: true });
1165
+ if (signal.aborted) this.stop();
1166
+ }
1167
+ }
1168
+ /** The child's standard-output stream, for the composing face to attach its own consumer to. */
1169
+ get stdout() {
1170
+ return this.#child.stdout;
1167
1171
  }
1168
1172
  /** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
1169
1173
  get pid() {
1170
- return this.#engine.pid;
1174
+ return this.#child.pid;
1171
1175
  }
1172
1176
  /** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
1173
1177
  get code() {
1174
- return this.#engine.code;
1178
+ return this.#child.exitCode;
1175
1179
  }
1176
1180
  /** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
1177
1181
  get signal() {
1178
- return this.#engine.signal;
1179
- }
1180
- /** The typed lifecycle observation surface. */
1181
- get emitter() {
1182
- return this.#emitter;
1183
- }
1184
- /** The captured stdout lines, in arrival order, ending after queued lines at the terminal moment. */
1185
- get lines() {
1186
- return this.#lines;
1182
+ return this.#child.signalCode;
1187
1183
  }
1188
1184
  /** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
1189
1185
  get evidence() {
1190
- return this.#engine.evidence;
1191
- }
1192
- /** True when the `lines` stream omitted output after a retention bound was reached. */
1193
- get truncated() {
1194
- return this.#truncated;
1186
+ return this.#tail.toString("utf8");
1195
1187
  }
1196
1188
  /** True after the terminal moment arrived and `exit` settled. */
1197
1189
  get settled() {
1198
- return this.#engine.settled;
1190
+ return this.#settled;
1199
1191
  }
1200
1192
  /** True after termination began, including after the terminal moment. */
1201
1193
  get stopping() {
1202
- return this.#engine.stopping;
1194
+ return this.#stopping;
1195
+ }
1196
+ /**
1197
+ * The child's own ending, awaited without the terminal moment's drain window.
1198
+ *
1199
+ * @remarks
1200
+ * Never rejects, and resolves no value: `code` and `signal` carry the terminal facts as soon as
1201
+ * the host records them. It settles at the native exit, and at the terminal moment for a child
1202
+ * whose spawn produced no native exit at all, so it can never outlive the supervision.
1203
+ */
1204
+ get ending() {
1205
+ return this.#ending.promise;
1203
1206
  }
1204
1207
  /** The terminal child state, observed once after stream close or the drain cutoff. */
1205
1208
  get exit() {
1206
- return this.#engine.exit;
1209
+ return this.#exit.promise;
1207
1210
  }
1208
1211
  /**
1209
- * Write one line to the open standard-input channel.
1212
+ * Writes raw bytes to the open standard-input channel.
1213
+ *
1214
+ * @remarks
1215
+ * Never rejects, and adds no framing: a face composes whatever terminator its contract promises
1216
+ * before it calls this. `true` means the host accepted the bytes without reporting a fault; it
1217
+ * does not prove that the child read them. An ordinary write settles when the kernel accepts it.
1218
+ * Only a full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and
1219
+ * every terminal teardown path settles pending writes. After `stop` or `destroy` begins, a later
1220
+ * call settles `false`, because teardown cannot confirm delivery for bytes it is about to
1221
+ * discard.
1222
+ *
1223
+ * @param bytes - The payload to write, already framed by the caller
1224
+ * @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`
1225
+ */
1226
+ deliver(bytes) {
1227
+ const stdin = this.#child.stdin;
1228
+ if (this.#settled || this.#stopping || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
1229
+ const settled = Promise.withResolvers();
1230
+ this.#writes.set(settled, void 0);
1231
+ try {
1232
+ stdin.write(bytes, this.#confirmWrite.bind(this, settled));
1233
+ } catch {
1234
+ this.#settleWrite(settled, false);
1235
+ return settled.promise;
1236
+ }
1237
+ if (this.#delivery > 0 && this.#writes.has(settled)) {
1238
+ const timer = setTimeout(() => this.#settleWrite(settled, false), this.#delivery);
1239
+ timer.unref();
1240
+ this.#writes.set(settled, timer);
1241
+ }
1242
+ return settled.promise;
1243
+ }
1244
+ /**
1245
+ * Closes the standard-input channel while leaving the child running.
1246
+ *
1247
+ * @remarks
1248
+ * Never rejects, and terminates nothing: no signal is sent, `stopping` stays false, no drain
1249
+ * window is armed, and the terminal moment does not arrive. The child observes end of input and
1250
+ * decides for itself what to do about it, which is what separates this from `stop`.
1210
1251
  *
1211
- * @remarks
1212
- * Never rejects. `true` means the host accepted the bytes without reporting a fault; it does not
1213
- * prove that the child read them. An ordinary write settles when the kernel accepts it. Only a
1214
- * full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and every
1215
- * terminal teardown path settles pending writes. On Windows 11 with Node v24.18.1, measured on
1216
- * 2026-08-21, a child that closes its own file descriptor 0 can leave the parent pipe writable:
1217
- * the write can settle `true` without a callback error or a stream error while the child remains
1218
- * alive. After `stop` or `destroy` begins, a later call settles `false`. Version 0.0.4 could settle
1219
- * that call `true` before teardown destroyed the pipe; returning `false` avoids claiming delivery
1220
- * for bytes the package is about to discard.
1252
+ * Every call shares one barrier, and it resolves after the host flushes the writes it had already
1253
+ * accepted, or reports that it cannot. A `deliver` after this call resolves `false`, because the
1254
+ * host stops reporting the channel writable the moment it is ended. That same flag keeps the
1255
+ * ended channel quiet for its remaining life, so a later host fault on it settles pending writes
1256
+ * and reaches no face. After `stop` or `destroy` has begun, or after the terminal moment, the
1257
+ * call resolves and changes nothing, because the channel is already being discarded.
1221
1258
  *
1222
- * @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`
1259
+ * @returns The stable barrier shared by every call
1224
1260
  */
1225
- send(text) {
1226
- return this.#engine.deliver(node_buffer.Buffer.from(`${text}\n`, "utf8"));
1261
+ end() {
1262
+ if (this.#closure !== void 0) return this.#closure;
1263
+ this.#closure = this.#endInput();
1264
+ return this.#closure;
1227
1265
  }
1228
1266
  /**
1229
1267
  * Terminates the child process tree and reaches the terminal observation moment.
@@ -1237,10 +1275,12 @@ var Process = class {
1237
1275
  * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
1238
1276
  */
1239
1277
  stop() {
1240
- return this.#engine.stop();
1278
+ if (this.#termination !== void 0) return this.#termination;
1279
+ this.#termination = this.#kill();
1280
+ return this.#termination;
1241
1281
  }
1242
1282
  /**
1243
- * Stops the child and destroys the observation emitter after the terminal state freezes.
1283
+ * Stops the child and releases the composing face after the terminal state freezes.
1244
1284
  *
1245
1285
  * @remarks
1246
1286
  * Always resolves, including when termination was never confirmed. Every call shares one barrier.
@@ -1248,351 +1288,219 @@ var Process = class {
1248
1288
  * @returns The stable barrier shared by every call
1249
1289
  */
1250
1290
  destroy() {
1251
- return this.#engine.destroy();
1252
- }
1253
- #iterate() {
1254
- this.#requested = true;
1255
- return Object.freeze({ next: this.#next.bind(this) });
1256
- }
1257
- #next() {
1258
- const line = this.#take();
1259
- if (line !== void 0) return Promise.resolve(Object.freeze({
1260
- done: false,
1261
- value: line
1262
- }));
1263
- if (this.#ended) return Promise.resolve(Object.freeze({
1264
- done: true,
1265
- value: void 0
1266
- }));
1267
- const waiter = Promise.withResolvers();
1268
- this.#waiters.push(waiter);
1269
- return waiter.promise;
1270
- }
1271
- #take() {
1272
- if (this.#head >= this.#queue.length) return void 0;
1273
- const line = this.#queue[this.#head];
1274
- this.#queue[this.#head] = "";
1275
- this.#head += 1;
1276
- if (this.#head >= this.#queue.length) {
1277
- this.#queue.length = 0;
1278
- this.#head = 0;
1279
- }
1280
- if (line === void 0) return void 0;
1281
- this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
1282
- this.#relieve();
1283
- return line;
1284
- }
1285
- #push(line) {
1286
- if (this.#ended) return;
1287
- const waiter = this.#waiters.shift();
1288
- if (waiter !== void 0) {
1289
- waiter.resolve(Object.freeze({
1290
- done: false,
1291
- value: line
1292
- }));
1293
- return;
1294
- }
1295
- const bytes = node_buffer.Buffer.byteLength(line) + 1;
1296
- const stopping = this.#engine.stopping;
1297
- const limit = stopping ? this.#backlog * 2 : this.#backlog;
1298
- if ((!this.#requested || stopping) && this.#pending + bytes > limit) {
1299
- this.#truncated = true;
1300
- return;
1301
- }
1302
- this.#queue.push(line);
1303
- this.#pending += bytes;
1304
- this.#restrain();
1305
- }
1306
- #restrain() {
1307
- if (this.#paused || this.#engine.stopping || !this.#requested) return;
1308
- if (this.#pending < this.#backlog) return;
1309
- this.#paused = true;
1310
- this.#reader.pause();
1311
- }
1312
- #relieve() {
1313
- if (!this.#paused || this.#ended) return;
1314
- if (!this.#engine.stopping && this.#pending > this.#backlog / 2) return;
1315
- this.#paused = false;
1316
- this.#reader.resume();
1317
- }
1318
- #finish() {
1319
- if (this.#ended) return;
1320
- this.#ended = true;
1321
- this.#paused = false;
1322
- for (const waiter of this.#waiters) waiter.resolve(Object.freeze({
1323
- done: true,
1324
- value: void 0
1325
- }));
1326
- this.#waiters.length = 0;
1327
- }
1328
- #reportStderr(chunk) {
1329
- this.#emitter.emit("stderr", chunk);
1330
- }
1331
- #reportFault(cause) {
1332
- this.#emitter.emit("error", cause);
1333
- }
1334
- #reportExit(exit) {
1335
- this.#emitter.emit("exit", exit);
1336
- }
1337
- #closeReader() {
1338
- this.#reader.close();
1339
- }
1340
- #releaseBackpressure() {
1341
- this.#capBacklog();
1342
- this.#relieve();
1343
- }
1344
- #capBacklog() {
1345
- const limit = this.#backlog * 2;
1346
- while (this.#pending > limit && this.#queue.length > this.#head) {
1347
- const line = this.#queue.pop();
1348
- if (line === void 0) break;
1349
- this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
1350
- this.#truncated = true;
1351
- }
1352
- }
1353
- };
1354
- //#endregion
1355
- //#region src/server/ProcessManager.ts
1356
- /**
1357
- * A keyed registry of live supervised child processes.
1358
- *
1359
- * @remarks
1360
- * A child launched under an id joins the registry and emits `launch`; when it settles it removes
1361
- * itself and emits `exit`, so `count` and `processes` reflect only live children. The id is reserved
1362
- * before the child is constructed and released when construction throws, so a refused launch never
1363
- * strands its key. Eviction follows the child's own `exit` promise, which no listener can forge, so
1364
- * it lands one microtask after the child's public `exit` event: a listener on that event still sees
1365
- * the child registered. `launch` throws a {@link createDuplicateError} when the id is already live
1366
- * and a {@link createProtocolError} after `destroy` has begun, including when the caller's own option
1367
- * getter begins that teardown mid-construction — a spawn fault surfaces through the returned child's
1368
- * `exit`, never from `launch`. `destroy` awaits every child's own teardown, including the teardown
1369
- * of a child a lost race refused, which destroys each child's observation emitter and leaves every
1370
- * subscription on it silently inert, then destroys the registry emitter last.
1371
- *
1372
- * @example
1373
- * ```ts
1374
- * import { ProcessManager } from '@orkestrel/process/server'
1375
- *
1376
- * const manager = new ProcessManager()
1377
- * const child = manager.launch('build', {
1378
- * command: { file: 'node', arguments: ['build.js'] },
1379
- * workspace: process.cwd(),
1380
- * })
1381
- * await child.exit
1382
- * await manager.destroy()
1383
- * ```
1384
- */
1385
- var ProcessManager = class {
1386
- #emitter;
1387
- #children = /* @__PURE__ */ new Map();
1388
- #ids = /* @__PURE__ */ new Set();
1389
- #orphans = /* @__PURE__ */ new Set();
1390
- #destroying = false;
1391
- #ending;
1392
- /**
1393
- * Construct a process registry.
1394
- *
1395
- * @param options - Initial fleet-level observation hooks and listener-error handling
1396
- */
1397
- constructor(options) {
1398
- const on = options?.on;
1399
- const error = options?.error;
1400
- this.#emitter = new _orkestrel_emitter.Emitter({
1401
- ...on === void 0 ? {} : { on },
1402
- ...error === void 0 ? {} : { error }
1403
- });
1404
- }
1405
- /** The typed fleet-level observation surface. */
1406
- get emitter() {
1407
- return this.#emitter;
1291
+ if (this.#destruction !== void 0) return this.#destruction;
1292
+ this.#destruction = this.#conclude();
1293
+ return this.#destruction;
1408
1294
  }
1409
- /** The number of live children. */
1410
- get count() {
1411
- return this.#children.size;
1295
+ #retain(chunk) {
1296
+ if (!node_buffer.Buffer.isBuffer(chunk) || this.#settled) return;
1297
+ this.#tail = trimTail(node_buffer.Buffer.concat([this.#tail, chunk]), this.#evidence);
1298
+ const text = this.#decoder.write(chunk);
1299
+ if (text.length > 0) this.#chunk(text);
1412
1300
  }
1413
- /**
1414
- * The live child under `id`, or `undefined` when none is.
1415
- *
1416
- * @param id - The registry key
1417
- * @returns The child, or `undefined`
1418
- */
1419
- process(id) {
1420
- return this.#children.get(id);
1301
+ #complete() {
1302
+ if (this.#settled) return;
1303
+ this.#settle(true);
1421
1304
  }
1422
- /**
1423
- * A snapshot of every live child.
1424
- *
1425
- * @returns The live children in launch order
1426
- */
1427
- processes() {
1428
- return [...this.#children.values()];
1305
+ #expire() {
1306
+ this.#ending.resolve();
1307
+ if (this.#settled) return;
1308
+ this.#wait();
1429
1309
  }
1430
- /**
1431
- * Spawn and register one child under `id`.
1432
- *
1433
- * @remarks
1434
- * {@link Process} reads every option before it spawns, so a caller's own option getter runs while
1435
- * nothing has started and a throw from one strands no process. A getter that begins `destroy`
1436
- * without throwing is the one remaining race: the child is already spawned, so the launch is
1437
- * refused with a {@link createProtocolError} and that child is destroyed rather than adopted, its
1438
- * teardown bounded by `grace` plus the confirmation window. The `protocol` refusal throws
1439
- * synchronously, and the `destroy` barrier covers that teardown, so the refused child reaches its
1440
- * terminal moment before the barrier resolves.
1441
- *
1442
- * @param id - The registry key, unique among live children
1443
- * @param options - The child construction options
1444
- * @returns The launched child
1445
- * @throws A {@link ProcessError} coded `duplicate` when `id` is already live, `protocol` when the registry is being destroyed, or `invalid` when an option or command string is malformed
1446
- */
1447
- launch(id, options) {
1448
- if (this.#destroying) throw (0, _src_core.createProtocolError)(id);
1449
- if (this.#ids.has(id)) throw (0, _src_core.createDuplicateError)(id);
1450
- this.#ids.add(id);
1451
- const child = this.#construct(id, options);
1452
- if (this.#destroying) {
1453
- this.#ids.delete(id);
1454
- this.#orphans.add(child);
1455
- child.destroy();
1456
- throw (0, _src_core.createProtocolError)(id);
1457
- }
1458
- this.#children.set(id, child);
1459
- child.exit.then((exit) => this.#evict(id, child, exit));
1460
- this.#emitter.emit("launch", id);
1461
- return child;
1310
+ #wait() {
1311
+ this.#waiting ??= waitForClose(this.#child, this.#drain).then(() => {
1312
+ if (!this.#settled) this.#settle(false);
1313
+ });
1314
+ return this.#waiting;
1462
1315
  }
1463
- stop(target) {
1464
- if (target === void 0) return this.#stopAll();
1465
- if (typeof target === "string") return this.#stopOne(target);
1466
- return this.#stopMany(target);
1316
+ #settle(drained) {
1317
+ const suffix = this.#decoder.end();
1318
+ if (suffix.length > 0) this.#chunk(suffix);
1319
+ this.#settled = true;
1320
+ this.#removeAbortListener();
1321
+ this.#close();
1322
+ const exit = Object.freeze({
1323
+ code: this.code,
1324
+ signal: this.signal,
1325
+ drained
1326
+ });
1327
+ this.#exit.resolve(exit);
1328
+ this.#ending.resolve();
1329
+ this.#terminal(exit);
1330
+ this.#child.stdout.destroy();
1331
+ this.#child.stderr.destroy();
1467
1332
  }
1468
- /**
1469
- * Stop every child, then destroy the registry emitter last.
1470
- *
1471
- * @remarks
1472
- * Always resolves, and refuses a later `launch` with a {@link createProtocolError}. Each child is
1473
- * destroyed rather than merely stopped, so its own observation emitter is destroyed too and every
1474
- * subscription on it goes silently inert.
1475
- *
1476
- * @returns The stable barrier shared by every call
1477
- */
1478
- destroy() {
1479
- if (this.#ending !== void 0) return this.#ending;
1480
- this.#destroying = true;
1481
- this.#ending = this.#teardown();
1482
- return this.#ending;
1333
+ #terminate() {
1334
+ this.stop();
1483
1335
  }
1484
- #construct(id, options) {
1485
- try {
1486
- return new Process(options);
1487
- } catch (error) {
1488
- this.#ids.delete(id);
1489
- throw error;
1336
+ #removeAbortListener() {
1337
+ if (this.#signal === void 0 || this.#abort === void 0) return;
1338
+ this.#signal.removeEventListener("abort", this.#abort);
1339
+ }
1340
+ #confirmWrite(settled, error) {
1341
+ if (error === void 0 || error === null) {
1342
+ this.#settleWrite(settled, true);
1343
+ return;
1490
1344
  }
1345
+ this.#failInputCallback(error);
1491
1346
  }
1492
- #evict(id, child, exit) {
1493
- if (this.#children.get(id) !== child) return;
1494
- this.#children.delete(id);
1495
- this.#ids.delete(id);
1496
- this.#emitter.emit("exit", id, exit);
1347
+ #completeInput() {
1348
+ this.#input -= 1;
1497
1349
  }
1498
- async #stopOne(id) {
1499
- const child = this.#children.get(id);
1500
- if (child === void 0) return false;
1501
- return child.stop();
1350
+ #endInput() {
1351
+ const stdin = this.#child.stdin;
1352
+ if (this.#stopping || this.#settled || stdin.writableEnded || stdin.destroyed) return Promise.resolve();
1353
+ const ended = Promise.withResolvers();
1354
+ stdin.end(() => ended.resolve());
1355
+ return ended.promise;
1502
1356
  }
1503
- async #stopMany(ids) {
1504
- return (await Promise.all(ids.map((id) => this.#stopOne(id)))).every((ok) => ok);
1357
+ #settleWrite(settled, accepted) {
1358
+ if (!this.#writes.has(settled)) return;
1359
+ const timer = this.#writes.get(settled);
1360
+ this.#writes.delete(settled);
1361
+ clearTimeout(timer);
1362
+ settled.resolve(accepted);
1505
1363
  }
1506
- async #stopAll() {
1507
- await Promise.allSettled([...this.#children.values()].map((child) => child.stop()));
1364
+ #settleWrites() {
1365
+ for (const settled of this.#writes.keys()) this.#settleWrite(settled, false);
1508
1366
  }
1509
- async #teardown() {
1510
- 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()));
1367
+ #failInputStream(cause) {
1368
+ if (this.#input > 0) {
1369
+ this.#settleWrites();
1370
+ return;
1515
1371
  }
1516
- this.#children.clear();
1517
- this.#ids.clear();
1518
- this.#emitter.destroy();
1372
+ this.#failInputCallback(cause);
1373
+ }
1374
+ #failInputCallback(cause) {
1375
+ if (this.#child.stdin.writableEnded) {
1376
+ this.#settleWrites();
1377
+ return;
1378
+ }
1379
+ if (this.#failure !== void 0 || this.#stopping) return;
1380
+ this.#failure = cause;
1381
+ this.#settleWrites();
1382
+ this.#fault(new _src_core.ProcessError("The standard-input channel failed", {
1383
+ code: "protocol",
1384
+ cause
1385
+ }));
1386
+ }
1387
+ async #kill() {
1388
+ this.#stopping = true;
1389
+ this.#relieve?.();
1390
+ const confirmed = await stopChild(this.#child, this.#grace, _src_core.PROCESS_CONFIRMATION);
1391
+ this.#settleWrites();
1392
+ this.#child.stdin.destroy();
1393
+ if (!this.#settled) await this.#wait();
1394
+ if (!this.#settled) this.#settle(false);
1395
+ return confirmed;
1396
+ }
1397
+ async #conclude() {
1398
+ await this.stop();
1399
+ this.#teardown();
1519
1400
  }
1520
1401
  };
1521
1402
  //#endregion
1522
- //#region src/server/Session.ts
1403
+ //#region src/server/Process.ts
1523
1404
  /**
1524
- * Supervises one child and publishes its standard output as raw bytes.
1405
+ * Supervises one child while keeping every observation channel aligned at termination.
1525
1406
  *
1526
1407
  * @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.
1408
+ * The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read
1409
+ * the host child directly, so they expose native exit as soon as the host records it. `settled`,
1410
+ * `exit`, `evidence`, and `lines` reach one terminal moment after the read channels close or the
1411
+ * bounded `drain` window cuts them off. That window is armed by the child's native exit and by a
1412
+ * termination this package initiated, so every way a child can end reaches the moment. The terminal
1413
+ * routine freezes `evidence` and ends `lines` before it destroys stdout, stderr, or the emitter, so
1414
+ * the push channel never disappears while a pull channel can still change or wait indefinitely.
1537
1415
  *
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.
1416
+ * Standard output is framed through `readline` into a single-consumer stream. Lines already framed
1417
+ * and queued remain available through the terminal moment, while bytes arriving after a requested
1418
+ * termination may be cut off. Standard error is forwarded through the live `stderr` event and
1419
+ * retained as a byte-bounded raw tail until that same moment. `stop` ends the child tree and the
1420
+ * observation surfaces. `destroy` performs that stop and then destroys the emitter after the
1421
+ * frozen terminal state exists.
1543
1422
  *
1544
1423
  * @example
1545
1424
  * ```ts
1546
- * import { Session } from '@orkestrel/process/server'
1425
+ * import { Process } from '@orkestrel/process/server'
1547
1426
  *
1548
- * const session = new Session({
1549
- * command: { file: 'node', arguments: ['-e', 'process.stdin.pipe(process.stdout)'] },
1427
+ * const child = new Process({
1428
+ * command: { file: 'node', arguments: ['--version'] },
1550
1429
  * workspace: process.cwd(),
1551
1430
  * })
1431
+ * const exit = await child.exit
1432
+ * await child.destroy()
1552
1433
  * ```
1553
1434
  */
1554
- var Session = class {
1435
+ var Process = class {
1555
1436
  #emitter;
1556
1437
  #engine;
1557
- #output;
1438
+ #reader;
1439
+ #backlog;
1440
+ #lines;
1441
+ #queue = [];
1442
+ #waiters = [];
1443
+ #head = 0;
1444
+ #pending = 0;
1445
+ #requested = false;
1446
+ #paused = false;
1447
+ #truncated = false;
1448
+ #ended = false;
1558
1449
  /**
1559
- * Spawn one child process and begin byte capture.
1450
+ * Spawn one child process and begin stream capture.
1560
1451
  *
1561
- * @param options - Command, workspace, termination, evidence, stdin, and observation settings
1452
+ * @param options - Command, workspace, termination, capture, stdin, and observation settings
1562
1453
  * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
1563
1454
  */
1564
1455
  constructor(options) {
1565
- const command = options.command;
1456
+ const source = options.command;
1566
1457
  const workspace = options.workspace;
1567
1458
  const grace = options.grace;
1568
1459
  const drain = options.drain;
1569
1460
  const evidence = options.evidence;
1461
+ const backlog = options.backlog;
1570
1462
  const delivery = options.delivery;
1463
+ const writable = options.writable;
1571
1464
  const signal = options.signal;
1572
1465
  const on = options.on;
1573
1466
  const error = options.error;
1467
+ const command = snapshotCommand(source);
1468
+ validateCommand(command);
1469
+ validateWorkspace(workspace);
1470
+ validateTimer(grace, "option 'grace'");
1471
+ validateTimer(drain, "option 'drain'");
1472
+ validateTimer(delivery, "option 'delivery'");
1473
+ validateBytes(evidence, "option 'evidence'", 0);
1474
+ validateBytes(backlog, "option 'backlog'", 1);
1574
1475
  this.#emitter = new _orkestrel_emitter.Emitter({
1575
1476
  ...on === void 0 ? {} : { on },
1576
1477
  ...error === void 0 ? {} : { error }
1577
1478
  });
1578
- this.#output = this.#publish.bind(this);
1479
+ this.#backlog = backlog ?? _src_core.PROCESS_BACKLOG;
1480
+ this.#lines = Object.freeze({ [Symbol.asyncIterator]: this.#iterate.bind(this) });
1579
1481
  this.#engine = new Supervisor({
1580
1482
  command,
1581
1483
  workspace,
1582
- writable: true,
1583
1484
  ...grace === void 0 ? {} : { grace },
1584
1485
  ...drain === void 0 ? {} : { drain },
1585
1486
  ...evidence === void 0 ? {} : { evidence },
1586
1487
  ...delivery === void 0 ? {} : { delivery },
1488
+ ...writable === void 0 ? {} : { writable },
1587
1489
  ...signal === void 0 ? {} : { signal }
1588
1490
  }, {
1589
1491
  chunk: this.#reportStderr.bind(this),
1590
1492
  fault: this.#reportFault.bind(this),
1591
- close: this.#closeOutput.bind(this),
1493
+ relieve: this.#releaseBackpressure.bind(this),
1494
+ close: this.#closeReader.bind(this),
1592
1495
  terminal: this.#reportExit.bind(this),
1593
1496
  teardown: this.#emitter.destroy.bind(this.#emitter)
1594
1497
  });
1595
- this.#engine.stdout.on("data", this.#output);
1498
+ this.#reader = (0, node_readline.createInterface)({
1499
+ input: this.#engine.stdout,
1500
+ crlfDelay: Infinity
1501
+ });
1502
+ this.#reader.on("line", this.#push.bind(this));
1503
+ this.#reader.once("close", this.#finish.bind(this));
1596
1504
  }
1597
1505
  /** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
1598
1506
  get pid() {
@@ -1610,59 +1518,58 @@ var Session = class {
1610
1518
  get emitter() {
1611
1519
  return this.#emitter;
1612
1520
  }
1521
+ /** The captured stdout lines, in arrival order, ending after queued lines at the terminal moment. */
1522
+ get lines() {
1523
+ return this.#lines;
1524
+ }
1613
1525
  /** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
1614
1526
  get evidence() {
1615
1527
  return this.#engine.evidence;
1616
1528
  }
1529
+ /** True when the `lines` stream omitted output after a retention bound was reached. */
1530
+ get truncated() {
1531
+ return this.#truncated;
1532
+ }
1617
1533
  /** True after the terminal moment arrived and `exit` settled. */
1618
1534
  get settled() {
1619
1535
  return this.#engine.settled;
1620
1536
  }
1621
- /** True after termination began, including after the terminal moment. `end` never turns it true. */
1537
+ /** True after termination began, including after the terminal moment. */
1622
1538
  get stopping() {
1623
1539
  return this.#engine.stopping;
1624
1540
  }
1625
- /** The child's own ending, awaited without the terminal moment's drain window. */
1626
- get ending() {
1627
- return this.#engine.ending;
1628
- }
1629
1541
  /** The terminal child state, observed once after stream close or the drain cutoff. */
1630
1542
  get exit() {
1631
1543
  return this.#engine.exit;
1632
1544
  }
1633
1545
  /**
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.
1546
+ * Write one line to the open standard-input channel.
1650
1547
  *
1651
1548
  * @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`.
1549
+ * Never rejects. `true` means the host accepted the bytes without reporting a fault; it does not
1550
+ * prove that the child read them. An ordinary write settles when the kernel accepts it. Only a
1551
+ * full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and every
1552
+ * terminal teardown path settles pending writes. On Windows 11 with Node v24.18.1, measured on
1553
+ * 2026-08-21, a child that closes its own file descriptor 0 can leave the parent pipe writable:
1554
+ * the write can settle `true` without a callback error or a stream error while the child remains
1555
+ * alive. After `stop` or `destroy` begins, a later call settles `false`. Version 0.0.4 could settle
1556
+ * that call `true` before teardown destroyed the pipe; returning `false` avoids claiming delivery
1557
+ * for bytes the package is about to discard.
1655
1558
  *
1656
- * @returns The stable barrier shared by every call
1559
+ * @param text - The line text without its trailing newline
1560
+ * @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`
1657
1561
  */
1658
- end() {
1659
- return this.#engine.end();
1562
+ send(text) {
1563
+ return this.#engine.deliver(node_buffer.Buffer.from(`${text}\n`, "utf8"));
1660
1564
  }
1661
1565
  /**
1662
1566
  * Terminates the child process tree and reaches the terminal observation moment.
1663
1567
  *
1664
1568
  * @remarks
1665
- * Never rejects, and every call shares one termination bounded by `grace` and `drain`.
1569
+ * Never rejects, and every call shares one termination. After the termination sequence returns,
1570
+ * confirmed or not, it waits at most `drain` for the read channels to close, then cuts them off
1571
+ * and settles `exit` when they remain open. A cutoff reached before the native exit reports the
1572
+ * `code` and `signal` the host had recorded by then, which is `null` for a child still running.
1666
1573
  *
1667
1574
  * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
1668
1575
  */
@@ -1680,9 +1587,80 @@ var Session = class {
1680
1587
  destroy() {
1681
1588
  return this.#engine.destroy();
1682
1589
  }
1683
- #publish(chunk) {
1684
- if (!node_buffer.Buffer.isBuffer(chunk) || this.#engine.settled) return;
1685
- this.#emitter.emit("stdout", new Uint8Array(chunk));
1590
+ #iterate() {
1591
+ this.#requested = true;
1592
+ return Object.freeze({ next: this.#next.bind(this) });
1593
+ }
1594
+ #next() {
1595
+ const line = this.#take();
1596
+ if (line !== void 0) return Promise.resolve(Object.freeze({
1597
+ done: false,
1598
+ value: line
1599
+ }));
1600
+ if (this.#ended) return Promise.resolve(Object.freeze({
1601
+ done: true,
1602
+ value: void 0
1603
+ }));
1604
+ const waiter = Promise.withResolvers();
1605
+ this.#waiters.push(waiter);
1606
+ return waiter.promise;
1607
+ }
1608
+ #take() {
1609
+ if (this.#head >= this.#queue.length) return void 0;
1610
+ const line = this.#queue[this.#head];
1611
+ this.#queue[this.#head] = "";
1612
+ this.#head += 1;
1613
+ if (this.#head >= this.#queue.length) {
1614
+ this.#queue.length = 0;
1615
+ this.#head = 0;
1616
+ }
1617
+ if (line === void 0) return void 0;
1618
+ this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
1619
+ this.#relieve();
1620
+ return line;
1621
+ }
1622
+ #push(line) {
1623
+ if (this.#ended) return;
1624
+ const waiter = this.#waiters.shift();
1625
+ if (waiter !== void 0) {
1626
+ waiter.resolve(Object.freeze({
1627
+ done: false,
1628
+ value: line
1629
+ }));
1630
+ return;
1631
+ }
1632
+ const bytes = node_buffer.Buffer.byteLength(line) + 1;
1633
+ const stopping = this.#engine.stopping;
1634
+ const limit = stopping ? this.#backlog * 2 : this.#backlog;
1635
+ if ((!this.#requested || stopping) && this.#pending + bytes > limit) {
1636
+ this.#truncated = true;
1637
+ return;
1638
+ }
1639
+ this.#queue.push(line);
1640
+ this.#pending += bytes;
1641
+ this.#restrain();
1642
+ }
1643
+ #restrain() {
1644
+ if (this.#paused || this.#engine.stopping || !this.#requested) return;
1645
+ if (this.#pending < this.#backlog) return;
1646
+ this.#paused = true;
1647
+ this.#reader.pause();
1648
+ }
1649
+ #relieve() {
1650
+ if (!this.#paused || this.#ended) return;
1651
+ if (!this.#engine.stopping && this.#pending > this.#backlog / 2) return;
1652
+ this.#paused = false;
1653
+ this.#reader.resume();
1654
+ }
1655
+ #finish() {
1656
+ if (this.#ended) return;
1657
+ this.#ended = true;
1658
+ this.#paused = false;
1659
+ for (const waiter of this.#waiters) waiter.resolve(Object.freeze({
1660
+ done: true,
1661
+ value: void 0
1662
+ }));
1663
+ this.#waiters.length = 0;
1686
1664
  }
1687
1665
  #reportStderr(chunk) {
1688
1666
  this.#emitter.emit("stderr", chunk);
@@ -1693,414 +1671,436 @@ var Session = class {
1693
1671
  #reportExit(exit) {
1694
1672
  this.#emitter.emit("exit", exit);
1695
1673
  }
1696
- #closeOutput() {
1697
- this.#engine.stdout.off("data", this.#output);
1674
+ #closeReader() {
1675
+ this.#reader.close();
1676
+ }
1677
+ #releaseBackpressure() {
1678
+ this.#capBacklog();
1679
+ this.#relieve();
1680
+ }
1681
+ #capBacklog() {
1682
+ const limit = this.#backlog * 2;
1683
+ while (this.#pending > limit && this.#queue.length > this.#head) {
1684
+ const line = this.#queue.pop();
1685
+ if (line === void 0) break;
1686
+ this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
1687
+ this.#truncated = true;
1688
+ }
1698
1689
  }
1699
1690
  };
1700
1691
  //#endregion
1701
- //#region src/server/factories.ts
1702
- /**
1703
- * Creates one supervised child process.
1704
- *
1705
- * @param options - Command, workspace, termination, evidence, stdin, and observation settings
1706
- * @returns The launched {@link ProcessInterface}
1707
- *
1708
- * @example
1709
- * ```ts
1710
- * import { createProcess } from '@orkestrel/process/server'
1711
- *
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
- }
1692
+ //#region src/server/ProcessManager.ts
1722
1693
  /**
1723
- * Creates one raw byte session over a supervised child process.
1694
+ * A keyed registry of live supervised child processes.
1724
1695
  *
1725
- * @param options - Command, workspace, termination, evidence, stdin, and observation settings
1726
- * @returns The launched {@link SessionInterface}
1696
+ * @remarks
1697
+ * A child launched under an id joins the registry and emits `launch`; when it settles it removes
1698
+ * itself and emits `exit`, so `count` and `processes` reflect only live children. The id is reserved
1699
+ * before the child is constructed and released when construction throws, so a refused launch never
1700
+ * strands its key. Eviction follows the child's own `exit` promise, which no listener can forge, so
1701
+ * it lands one microtask after the child's public `exit` event: a listener on that event still sees
1702
+ * the child registered. `launch` throws a {@link createDuplicateError} when the id is already live
1703
+ * and a {@link createProtocolError} after `destroy` has begun, including when the caller's own option
1704
+ * getter begins that teardown mid-construction — a spawn fault surfaces through the returned child's
1705
+ * `exit`, never from `launch`. `destroy` awaits every child's own teardown, including the teardown
1706
+ * of a child a lost race refused, which destroys each child's observation emitter and leaves every
1707
+ * subscription on it silently inert, then destroys the registry emitter last.
1727
1708
  *
1728
1709
  * @example
1729
1710
  * ```ts
1730
- * import { createSession } from '@orkestrel/process/server'
1711
+ * import { ProcessManager } from '@orkestrel/process/server'
1731
1712
  *
1732
- * const session = createSession({
1733
- * command: { file: 'node', arguments: ['server.js', '--stdio'] },
1713
+ * const manager = new ProcessManager()
1714
+ * const child = manager.launch('build', {
1715
+ * command: { file: 'node', arguments: ['build.js'] },
1734
1716
  * workspace: process.cwd(),
1735
- * grace: 5000,
1736
1717
  * })
1718
+ * await child.exit
1719
+ * await manager.destroy()
1737
1720
  * ```
1738
1721
  */
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;
1722
+ var ProcessManager = class {
1723
+ #emitter;
1724
+ #children = /* @__PURE__ */ new Map();
1725
+ #ids = /* @__PURE__ */ new Set();
1726
+ #orphans = /* @__PURE__ */ new Set();
1727
+ #destroying = false;
1728
+ #ending;
1729
+ /**
1730
+ * Construct a process registry.
1731
+ *
1732
+ * @param options - Initial fleet-level observation hooks and listener-error handling
1733
+ */
1734
+ constructor(options) {
1735
+ const on = options?.on;
1736
+ const error = options?.error;
1737
+ this.#emitter = new _orkestrel_emitter.Emitter({
1738
+ ...on === void 0 ? {} : { on },
1739
+ ...error === void 0 ? {} : { error }
1740
+ });
1741
+ }
1742
+ /** The typed fleet-level observation surface. */
1743
+ get emitter() {
1744
+ return this.#emitter;
1745
+ }
1746
+ /** The number of live children. */
1747
+ get count() {
1748
+ return this.#children.size;
1749
+ }
1750
+ /**
1751
+ * The live child under `id`, or `undefined` when none is.
1752
+ *
1753
+ * @param id - The registry key
1754
+ * @returns The child, or `undefined`
1755
+ */
1756
+ process(id) {
1757
+ return this.#children.get(id);
1758
+ }
1759
+ /**
1760
+ * A snapshot of every live child.
1761
+ *
1762
+ * @returns The live children in launch order
1763
+ */
1764
+ processes() {
1765
+ return [...this.#children.values()];
1766
+ }
1767
+ /**
1768
+ * Spawn and register one child under `id`.
1769
+ *
1770
+ * @remarks
1771
+ * {@link Process} reads every option before it spawns, so a caller's own option getter runs while
1772
+ * nothing has started and a throw from one strands no process. A getter that begins `destroy`
1773
+ * without throwing is the one remaining race: the child is already spawned, so the launch is
1774
+ * refused with a {@link createProtocolError} and that child is destroyed rather than adopted, its
1775
+ * teardown bounded by `grace` plus the confirmation window. The `protocol` refusal throws
1776
+ * synchronously, and the `destroy` barrier covers that teardown, so the refused child reaches its
1777
+ * terminal moment before the barrier resolves.
1778
+ *
1779
+ * @param id - The registry key, unique among live children
1780
+ * @param options - The child construction options
1781
+ * @returns The launched child
1782
+ * @throws A {@link ProcessError} coded `duplicate` when `id` is already live, `protocol` when the registry is being destroyed, or `invalid` when an option or command string is malformed
1783
+ */
1784
+ launch(id, options) {
1785
+ if (this.#destroying) throw (0, _src_core.createProtocolError)(id);
1786
+ if (this.#ids.has(id)) throw (0, _src_core.createDuplicateError)(id);
1787
+ this.#ids.add(id);
1788
+ const child = this.#construct(id, options);
1789
+ if (this.#destroying) {
1790
+ this.#ids.delete(id);
1791
+ this.#orphans.add(child);
1792
+ child.destroy();
1793
+ throw (0, _src_core.createProtocolError)(id);
1794
+ }
1795
+ this.#children.set(id, child);
1796
+ child.exit.then((exit) => this.#evict(id, child, exit));
1797
+ this.#emitter.emit("launch", id);
1798
+ return child;
1776
1799
  }
1777
- /** The bytes retained from the stream head. */
1778
- get retained() {
1779
- return this.#retained;
1800
+ stop(target) {
1801
+ if (target === void 0) return this.#stopAll();
1802
+ if (typeof target === "string") return this.#stopOne(target);
1803
+ return this.#stopMany(target);
1780
1804
  }
1781
1805
  /**
1782
- * Retains a delivered chunk within a byte limit.
1806
+ * Stop every child, then destroy the registry emitter last.
1807
+ *
1808
+ * @remarks
1809
+ * Always resolves, and refuses a later `launch` with a {@link createProtocolError}. Each child is
1810
+ * destroyed rather than merely stopped, so its own observation emitter is destroyed too and every
1811
+ * subscription on it goes silently inert.
1783
1812
  *
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
1813
+ * @returns The stable barrier shared by every call
1787
1814
  */
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;
1815
+ destroy() {
1816
+ if (this.#ending !== void 0) return this.#ending;
1817
+ this.#destroying = true;
1818
+ this.#ending = this.#teardown();
1819
+ return this.#ending;
1820
+ }
1821
+ #construct(id, options) {
1822
+ try {
1823
+ return new Process(options);
1824
+ } catch (error) {
1825
+ this.#ids.delete(id);
1826
+ throw error;
1827
+ }
1828
+ }
1829
+ #evict(id, child, exit) {
1830
+ if (this.#children.get(id) !== child) return;
1831
+ this.#children.delete(id);
1832
+ this.#ids.delete(id);
1833
+ this.#emitter.emit("exit", id, exit);
1834
+ }
1835
+ async #stopOne(id) {
1836
+ const child = this.#children.get(id);
1837
+ if (child === void 0) return false;
1838
+ return child.stop();
1839
+ }
1840
+ async #stopMany(ids) {
1841
+ return (await Promise.all(ids.map((id) => this.#stopOne(id)))).every((ok) => ok);
1842
+ }
1843
+ async #stopAll() {
1844
+ await Promise.allSettled([...this.#children.values()].map((child) => child.stop()));
1845
+ }
1846
+ async #teardown() {
1847
+ await Promise.allSettled([...this.#children.values()].map((child) => child.destroy()));
1848
+ while (this.#orphans.size > 0) {
1849
+ const orphans = [...this.#orphans];
1850
+ this.#orphans.clear();
1851
+ await Promise.allSettled(orphans.map((child) => child.destroy()));
1852
+ }
1853
+ this.#children.clear();
1854
+ this.#ids.clear();
1855
+ this.#emitter.destroy();
1796
1856
  }
1797
1857
  };
1798
1858
  //#endregion
1799
- //#region src/server/execution/execute.ts
1859
+ //#region src/server/Session.ts
1800
1860
  /**
1801
- * Runs one command to completion, buffering its output, and settles with the outcome.
1861
+ * Supervises one child and publishes its standard output as raw bytes.
1802
1862
  *
1803
1863
  * @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.
1864
+ * This is the transport face of the supervision engine `Process` also composes. It frames nothing,
1865
+ * retains nothing, and decodes nothing on the output side: every chunk the host delivers becomes one
1866
+ * `stdout` event carrying that chunk's bytes, in arrival order, so a consumer speaking a
1867
+ * length-prefixed or delimiter-framed protocol reads exactly what the child wrote and frames it
1868
+ * itself. Standard error is decoded and forwarded as `stderr`, with the same byte-bounded `evidence`
1869
+ * tail a `Process` retains, because a diagnostic stream is read rather than parsed.
1822
1870
  *
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`
1871
+ * Each emitted chunk is this session's own copy, and a plain `Uint8Array` rather than the host buffer
1872
+ * the read produced, so a consumer keeps it, mutates it, concatenates it, and reads its whole backing
1873
+ * buffer without reaching memory the host still manages and without depending on a host type.
1874
+ *
1875
+ * The standard-input channel is open from the spawn, and `end` closes it without terminating
1876
+ * anything. That is the member a cooperative protocol reaches for: the child observes end of input
1877
+ * and exits on its own, which `ending` reports. `stop` and `destroy` remain the terminations, and
1878
+ * `exit` remains the terminal moment — the native exit plus at most `drain` while a descendant holds
1879
+ * the inherited read ends.
1827
1880
  *
1828
1881
  * @example
1829
1882
  * ```ts
1830
- * const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
1883
+ * import { Session } from '@orkestrel/process/server'
1884
+ *
1885
+ * const session = new Session({
1886
+ * command: { file: 'node', arguments: ['-e', 'process.stdin.pipe(process.stdout)'] },
1887
+ * workspace: process.cwd(),
1888
+ * })
1831
1889
  * ```
1832
1890
  */
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);
1912
- });
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();
1949
- }, {
1950
- once: true,
1951
- signal: cleanup.signal
1891
+ var Session = class {
1892
+ #emitter;
1893
+ #engine;
1894
+ #output;
1895
+ /**
1896
+ * Spawn one child process and begin byte capture.
1897
+ *
1898
+ * @param options - Command, workspace, termination, evidence, stdin, and observation settings
1899
+ * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
1900
+ */
1901
+ constructor(options) {
1902
+ const command = options.command;
1903
+ const workspace = options.workspace;
1904
+ const grace = options.grace;
1905
+ const drain = options.drain;
1906
+ const evidence = options.evidence;
1907
+ const delivery = options.delivery;
1908
+ const signal = options.signal;
1909
+ const on = options.on;
1910
+ const error = options.error;
1911
+ this.#emitter = new _orkestrel_emitter.Emitter({
1912
+ ...on === void 0 ? {} : { on },
1913
+ ...error === void 0 ? {} : { error }
1952
1914
  });
1953
- if (optionSignal.aborted) {
1954
- aborted = true;
1955
- terminate.abort();
1956
- }
1957
- }
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
1915
+ this.#output = this.#publish.bind(this);
1916
+ this.#engine = new Supervisor({
1917
+ command,
1918
+ workspace,
1919
+ writable: true,
1920
+ ...grace === void 0 ? {} : { grace },
1921
+ ...drain === void 0 ? {} : { drain },
1922
+ ...evidence === void 0 ? {} : { evidence },
1923
+ ...delivery === void 0 ? {} : { delivery },
1924
+ ...signal === void 0 ? {} : { signal }
1925
+ }, {
1926
+ chunk: this.#reportStderr.bind(this),
1927
+ fault: this.#reportFault.bind(this),
1928
+ close: this.#closeOutput.bind(this),
1929
+ terminal: this.#reportExit.bind(this),
1930
+ teardown: this.#emitter.destroy.bind(this.#emitter)
1969
1931
  });
1970
- throw (0, _src_core.createExecuteError)(result, cause);
1932
+ this.#engine.stdout.on("data", this.#output);
1971
1933
  }
1972
- return result;
1973
- }
1934
+ /** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
1935
+ get pid() {
1936
+ return this.#engine.pid;
1937
+ }
1938
+ /** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
1939
+ get code() {
1940
+ return this.#engine.code;
1941
+ }
1942
+ /** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
1943
+ get signal() {
1944
+ return this.#engine.signal;
1945
+ }
1946
+ /** The typed lifecycle observation surface. */
1947
+ get emitter() {
1948
+ return this.#emitter;
1949
+ }
1950
+ /** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
1951
+ get evidence() {
1952
+ return this.#engine.evidence;
1953
+ }
1954
+ /** True after the terminal moment arrived and `exit` settled. */
1955
+ get settled() {
1956
+ return this.#engine.settled;
1957
+ }
1958
+ /** True after termination began, including after the terminal moment. `end` never turns it true. */
1959
+ get stopping() {
1960
+ return this.#engine.stopping;
1961
+ }
1962
+ /** The child's own ending, awaited without the terminal moment's drain window. */
1963
+ get ending() {
1964
+ return this.#engine.ending;
1965
+ }
1966
+ /** The terminal child state, observed once after stream close or the drain cutoff. */
1967
+ get exit() {
1968
+ return this.#engine.exit;
1969
+ }
1970
+ /**
1971
+ * Writes raw bytes to the open standard-input channel.
1972
+ *
1973
+ * @remarks
1974
+ * Never rejects, and appends no terminator: the child receives exactly these bytes. `true` means
1975
+ * the host accepted them without reporting a fault; it does not prove that the child read them.
1976
+ * The channel can queue the payload, so treat the array as owned by the channel until the
1977
+ * returned promise settles.
1978
+ *
1979
+ * @param bytes - The payload to write, already framed by the caller
1980
+ * @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`
1981
+ */
1982
+ write(bytes) {
1983
+ return this.#engine.deliver(bytes);
1984
+ }
1985
+ /**
1986
+ * Closes the standard-input channel and leaves the child running.
1987
+ *
1988
+ * @remarks
1989
+ * Never rejects and terminates nothing: `stopping` stays false and the terminal moment does not
1990
+ * arrive. Every call shares one barrier, which resolves after the host flushes the writes it had
1991
+ * already accepted. A later `write` resolves `false`.
1992
+ *
1993
+ * @returns The stable barrier shared by every call
1994
+ */
1995
+ end() {
1996
+ return this.#engine.end();
1997
+ }
1998
+ /**
1999
+ * Terminates the child process tree and reaches the terminal observation moment.
2000
+ *
2001
+ * @remarks
2002
+ * Never rejects, and every call shares one termination bounded by `grace` and `drain`.
2003
+ *
2004
+ * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
2005
+ */
2006
+ stop() {
2007
+ return this.#engine.stop();
2008
+ }
2009
+ /**
2010
+ * Stops the child and destroys the observation emitter after the terminal state freezes.
2011
+ *
2012
+ * @remarks
2013
+ * Always resolves, including when termination was never confirmed. Every call shares one barrier.
2014
+ *
2015
+ * @returns The stable barrier shared by every call
2016
+ */
2017
+ destroy() {
2018
+ return this.#engine.destroy();
2019
+ }
2020
+ #publish(chunk) {
2021
+ if (!node_buffer.Buffer.isBuffer(chunk) || this.#engine.settled) return;
2022
+ this.#emitter.emit("stdout", new Uint8Array(chunk));
2023
+ }
2024
+ #reportStderr(chunk) {
2025
+ this.#emitter.emit("stderr", chunk);
2026
+ }
2027
+ #reportFault(cause) {
2028
+ this.#emitter.emit("error", cause);
2029
+ }
2030
+ #reportExit(exit) {
2031
+ this.#emitter.emit("exit", exit);
2032
+ }
2033
+ #closeOutput() {
2034
+ this.#engine.stdout.off("data", this.#output);
2035
+ }
2036
+ };
1974
2037
  //#endregion
1975
- //#region src/server/execution/executeSync.ts
2038
+ //#region src/server/factories.ts
1976
2039
  /**
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}.
2040
+ * Creates one supervised child process.
1989
2041
  *
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`
2042
+ * @param options - Command, workspace, termination, evidence, stdin, and observation settings
2043
+ * @returns The launched {@link ProcessInterface}
1994
2044
  *
1995
2045
  * @example
1996
2046
  * ```ts
1997
- * const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
2047
+ * import { createProcess } from '@orkestrel/process/server'
2048
+ *
2049
+ * const child = createProcess({
2050
+ * command: { file: 'node', arguments: ['--version'] },
2051
+ * workspace: process.cwd(),
2052
+ * grace: 5000,
2053
+ * })
1998
2054
  * ```
1999
2055
  */
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;
2056
+ function createProcess(options) {
2057
+ return new Process(options);
2051
2058
  }
2052
- //#endregion
2053
- //#region src/server/execution/detach.ts
2054
2059
  /**
2055
- * Spawns one command as a detached process and returns without waiting for it.
2060
+ * Creates one raw byte session over a supervised child process.
2056
2061
  *
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.
2062
+ * @param options - Command, workspace, termination, evidence, stdin, and observation settings
2063
+ * @returns The launched {@link SessionInterface}
2062
2064
  *
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
2065
+ * @example
2066
+ * ```ts
2067
+ * import { createSession } from '@orkestrel/process/server'
2068
+ *
2069
+ * const session = createSession({
2070
+ * command: { file: 'node', arguments: ['server.js', '--stdio'] },
2071
+ * workspace: process.cwd(),
2072
+ * grace: 5000,
2073
+ * })
2074
+ * ```
2075
+ */
2076
+ function createSession(options) {
2077
+ return new Session(options);
2078
+ }
2079
+ /**
2080
+ * Creates one keyed registry of supervised child processes.
2081
+ *
2082
+ * @param options - Initial fleet-level observation hooks and listener-error handling
2083
+ * @returns The {@link ProcessManagerInterface} registry
2067
2084
  *
2068
2085
  * @example
2069
2086
  * ```ts
2070
- * detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
2087
+ * import { createProcessManager } from '@orkestrel/process/server'
2088
+ *
2089
+ * const manager = createProcessManager()
2071
2090
  * ```
2072
2091
  */
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();
2092
+ function createProcessManager(options) {
2093
+ return new ProcessManager(options);
2094
2094
  }
2095
2095
  //#endregion
2096
2096
  exports.Process = Process;
2097
2097
  exports.ProcessManager = ProcessManager;
2098
- exports.Retention = Retention;
2099
2098
  exports.Session = Session;
2100
2099
  exports.buildExecutableCandidates = buildExecutableCandidates;
2101
2100
  exports.buildExecuteResult = buildExecuteResult;
2102
2101
  exports.buildPlatformSpawn = buildPlatformSpawn;
2103
2102
  exports.buildSpawn = buildSpawn;
2103
+ exports.captureChunk = captureChunk;
2104
2104
  exports.createProcess = createProcess;
2105
2105
  exports.createProcessManager = createProcessManager;
2106
2106
  exports.createSession = createSession;