@orkestrel/process 0.0.4 → 0.0.6

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.
@@ -593,17 +593,41 @@ function waitForExit(child, timeout) {
593
593
  return settled.promise.finally(() => clearTimeout(timer));
594
594
  }
595
595
  /**
596
+ * Waits for one child process's streams to close, bounded by a deadline.
597
+ *
598
+ * @param child - The child boundary
599
+ * @param timeout - The milliseconds to wait before giving up
600
+ * @returns True when the host close arrived inside the deadline; false otherwise
601
+ *
602
+ * @example
603
+ * ```ts
604
+ * const closed = await waitForClose(child, 5_000)
605
+ * ```
606
+ */
607
+ function waitForClose(child, timeout) {
608
+ const closed = Promise.withResolvers();
609
+ const expired = Promise.withResolvers();
610
+ const timer = setTimeout(() => expired.resolve(false), timeout);
611
+ child.once("close", closed.resolve);
612
+ return Promise.race([closed.promise.then(() => true), expired.promise]).finally(() => {
613
+ clearTimeout(timer);
614
+ child.off("close", closed.resolve);
615
+ });
616
+ }
617
+ /**
596
618
  * Terminates one child process tree and reports whether its native exit was observed.
597
619
  *
598
620
  * @remarks
599
- * Windows ends the whole tree at once through {@link killTree}, falling back to a direct kill after
600
- * the utility fails, because the host has no cooperative termination to offer. A POSIX host signals
601
- * the process group `SIGTERM`, falls back to the direct child when no group owns its pid, waits
602
- * `grace`, then sends `SIGKILL` through the same route. No signal is initiated after the native exit
603
- * is observed; the window between initiating a signal and the host delivering it belongs to the
604
- * operating system. `confirm` bounds each awaited step rather than the call as a whole: on Windows
605
- * it bounds the `taskkill` call and then the final wait, and on a POSIX host `grace` bounds the
606
- * cooperative wait and `confirm` the final one. Never rejects.
621
+ * An already-exited child is returned on before any route to its pid runs, because the host has
622
+ * reaped that number and may have handed it to another process. Windows ends the whole tree at once
623
+ * through {@link killTree}, falling back to a direct kill after the utility fails, because the host
624
+ * has no cooperative termination to offer. A POSIX host signals the process group `SIGTERM`, falls
625
+ * back to the direct child when no group owns its pid, waits `grace`, then sends `SIGKILL` through
626
+ * the same route. No signal is initiated after the native exit is observed; the window between
627
+ * initiating a signal and the host delivering it belongs to the operating system. `confirm` bounds
628
+ * each awaited step rather than the call as a whole: on Windows it bounds the `taskkill` call and
629
+ * then the final wait, and on a POSIX host `grace` bounds the cooperative wait and `confirm` the
630
+ * final one. Never rejects.
607
631
  *
608
632
  * @param child - The child boundary
609
633
  * @param grace - The cooperative POSIX window in milliseconds between `SIGTERM` and `SIGKILL`
@@ -671,26 +695,23 @@ function buildExecuteResult(input) {
671
695
  //#endregion
672
696
  //#region src/server/Process.ts
673
697
  /**
674
- * One supervised child process with framed output, a bounded backlog, and bounded termination.
698
+ * Supervises one child while keeping every observation channel aligned at termination.
675
699
  *
676
700
  * @remarks
677
- * Standard output is framed through `readline` into a single-consumer stream: each line goes to
678
- * exactly one waiting iterator, so concurrent iterators split the output rather than each receiving
679
- * all of it. While no consumer has ever requested an iterator, stdout keeps draining so `exit` still
680
- * resolves, and retention stops at the `backlog` mark: a
681
- * consumer attaching later receives the retained head, a gap, then the live stream. After an iterator
682
- * has been requested, stdout pauses at the mark and resumes at half of it, so that consumer loses
683
- * nothing before termination and the child feels real backpressure. The mark is soft — the ordinary backlog can
684
- * overshoot it by the line that crossed it plus the rest of its delivered chunk. Termination never
685
- * reapplies backpressure, and retained lines are capped at twice `backlog`; later lines are dropped
686
- * and `truncated` reports the omission. Standard error is decoded and forwarded live as the `stderr`
687
- * event while a byte-bounded raw tail is retained as `evidence`. The typed `emitter` also carries the
688
- * child `error` cause on a spawn fault and the terminal `exit`, alongside the `exit` promise. `pid`,
689
- * `code`, and `signal` read the spawned child directly, so the native exit is readable before the
690
- * `exit` promise settles on stdio close. `stop` ends the whole tree and reports whether the native
691
- * exit was observed; `destroy` stops the child, destroys stdin so every pending `send` settles, and
692
- * destroys the emitter last. `destroy` resolving does not imply the child's stdio has closed, so
693
- * `exit` and `lines` can still be outstanding when a descendant holds an inherited pipe.
701
+ * The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read
702
+ * the host child directly, so they expose native exit as soon as the host records it. `settled`,
703
+ * `exit`, `evidence`, and `lines` reach one terminal moment after the read channels close or the
704
+ * bounded `drain` window cuts them off. That window is armed by the child's native exit and by a
705
+ * termination this package initiated, so every way a child can end reaches the moment. The terminal
706
+ * routine freezes `evidence` and ends `lines` before it destroys stdout, stderr, or the emitter, so
707
+ * the push channel never disappears while a pull channel can still change or wait indefinitely.
708
+ *
709
+ * Standard output is framed through `readline` into a single-consumer stream. Lines already framed
710
+ * and queued remain available through the terminal moment, while bytes arriving after a requested
711
+ * termination may be cut off. Standard error is forwarded through the live `stderr` event and
712
+ * retained as a byte-bounded raw tail until that same moment. `stop` ends the child tree and the
713
+ * observation surfaces. `destroy` performs that stop and then destroys the emitter after the
714
+ * frozen terminal state exists.
694
715
  *
695
716
  * @example
696
717
  * ```ts
@@ -709,8 +730,10 @@ var Process = class {
709
730
  #child;
710
731
  #reader;
711
732
  #grace;
733
+ #drain;
712
734
  #evidence;
713
735
  #backlog;
736
+ #delivery;
714
737
  #decoder = new node_string_decoder.StringDecoder("utf8");
715
738
  #exit = Promise.withResolvers();
716
739
  #lines;
@@ -718,16 +741,20 @@ var Process = class {
718
741
  #waiters = [];
719
742
  #signal;
720
743
  #abort;
744
+ #writes = /* @__PURE__ */ new Map();
721
745
  #tail = node_buffer.Buffer.alloc(0);
722
746
  #head = 0;
723
747
  #pending = 0;
724
748
  #requested = false;
725
749
  #paused = false;
726
750
  #truncated = false;
727
- #terminating = false;
728
- #closed = false;
751
+ #stopping = false;
752
+ #settled = false;
729
753
  #ended = false;
730
- #stopping;
754
+ #input = 0;
755
+ #failure;
756
+ #waiting;
757
+ #termination;
731
758
  #ending;
732
759
  /**
733
760
  * Spawn one child process and begin stream capture.
@@ -739,8 +766,10 @@ var Process = class {
739
766
  const source = options.command;
740
767
  const workspace = options.workspace;
741
768
  const grace = options.grace;
769
+ const drain = options.drain;
742
770
  const evidence = options.evidence;
743
771
  const backlog = options.backlog;
772
+ const delivery = options.delivery;
744
773
  const writable = options.writable;
745
774
  const signal = options.signal;
746
775
  const on = options.on;
@@ -750,6 +779,8 @@ var Process = class {
750
779
  validateCommand(command);
751
780
  validateWorkspace(workspace);
752
781
  validateTimer(grace, "option 'grace'");
782
+ validateTimer(drain, "option 'drain'");
783
+ validateTimer(delivery, "option 'delivery'");
753
784
  validateBytes(evidence, "option 'evidence'", 0);
754
785
  validateBytes(backlog, "option 'backlog'", 1);
755
786
  this.#emitter = new _orkestrel_emitter.Emitter({
@@ -757,8 +788,10 @@ var Process = class {
757
788
  ...error === void 0 ? {} : { error }
758
789
  });
759
790
  this.#grace = grace ?? _src_core.PROCESS_GRACE;
791
+ this.#drain = drain ?? _src_core.PROCESS_DRAIN;
760
792
  this.#evidence = evidence ?? _src_core.PROCESS_EVIDENCE;
761
793
  this.#backlog = backlog ?? _src_core.PROCESS_BACKLOG;
794
+ this.#delivery = delivery ?? 0;
762
795
  this.#signal = signal;
763
796
  this.#lines = Object.freeze({ [Symbol.asyncIterator]: this.#iterate.bind(this) });
764
797
  const childEnvironment = mergeEnvironment(command.isolated === true, command.environment);
@@ -785,11 +818,18 @@ var Process = class {
785
818
  this.#reader.on("line", this.#push.bind(this));
786
819
  this.#reader.once("close", this.#finish.bind(this));
787
820
  this.#child.once("error", (cause) => this.#emitter.emit("error", cause));
821
+ this.#child.once("exit", this.#expire.bind(this));
788
822
  this.#child.once("close", this.#close.bind(this));
789
- this.#child.stdin.on("error", () => void 0);
823
+ this.#child.stdin.on("error", (cause) => this.#failInputStream(cause));
790
824
  this.#child.stderr.on("data", this.#retain.bind(this));
791
- if (input !== void 0) this.#child.stdin.write(input);
792
- if (writable !== true) this.#child.stdin.end();
825
+ if (input !== void 0) {
826
+ this.#input += 1;
827
+ this.#child.stdin.write(input, this.#completeInput.bind(this));
828
+ }
829
+ if (writable !== true) {
830
+ this.#input += 1;
831
+ this.#child.stdin.end(this.#completeInput.bind(this));
832
+ }
793
833
  if (signal !== void 0) {
794
834
  this.#abort = this.#terminate.bind(this);
795
835
  signal.addEventListener("abort", this.#abort, { once: true });
@@ -812,11 +852,11 @@ var Process = class {
812
852
  get emitter() {
813
853
  return this.#emitter;
814
854
  }
815
- /** The captured stdout lines, in arrival order, ending when the child's stdout closes. */
855
+ /** The captured stdout lines, in arrival order, ending after queued lines at the terminal moment. */
816
856
  get lines() {
817
857
  return this.#lines;
818
858
  }
819
- /** The decoded byte-bounded stderr tail. */
859
+ /** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
820
860
  get evidence() {
821
861
  return this.#tail.toString("utf8");
822
862
  }
@@ -824,7 +864,15 @@ var Process = class {
824
864
  get truncated() {
825
865
  return this.#truncated;
826
866
  }
827
- /** The terminal child state, observed once from the close event. */
867
+ /** True after the terminal moment arrived and `exit` settled. */
868
+ get settled() {
869
+ return this.#settled;
870
+ }
871
+ /** True after termination began, including after the terminal moment. */
872
+ get stopping() {
873
+ return this.#stopping;
874
+ }
875
+ /** The terminal child state, observed once after stream close or the drain cutoff. */
828
876
  get exit() {
829
877
  return this.#exit.promise;
830
878
  }
@@ -832,51 +880,64 @@ var Process = class {
832
880
  * Write one line to the open standard-input channel.
833
881
  *
834
882
  * @remarks
835
- * Never rejects. The promise settles when the host reports the line handled, so a line written to
836
- * a child that is not reading stays pending until the child drains it or the channel closes. Every
837
- * terminal teardown path destroys stdin, so a pending write always settles by teardown; a caller
838
- * that needs its own deadline arms a timer and calls `stop`.
883
+ * Never rejects. `true` means the host accepted the bytes without reporting a fault; it does not
884
+ * prove that the child read them. An ordinary write settles when the kernel accepts it. Only a
885
+ * full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and every
886
+ * terminal teardown path settles pending writes. On Windows 11 with Node v24.18.1, measured on
887
+ * 2026-08-21, a child that closes its own file descriptor 0 can leave the parent pipe writable:
888
+ * the write can settle `true` without a callback error or a stream error while the child remains
889
+ * alive. After `stop` or `destroy` begins, a later call settles `false`. Version 0.0.4 could settle
890
+ * that call `true` before teardown destroyed the pipe; returning `false` avoids claiming delivery
891
+ * for bytes the package is about to discard.
839
892
  *
840
893
  * @param text - The line text without its trailing newline
841
- * @returns True when the line reached the host without error; false when the channel was closed, destroyed, ended, or the write failed
894
+ * @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`
842
895
  */
843
896
  send(text) {
844
897
  const stdin = this.#child.stdin;
845
- if (this.#closed || !stdin.writable) return Promise.resolve(false);
898
+ if (this.#settled || this.#stopping || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
846
899
  const settled = Promise.withResolvers();
900
+ this.#writes.set(settled, void 0);
847
901
  try {
848
- stdin.write(`${text}\n`, (error) => {
849
- settled.resolve(error === void 0 || error === null);
850
- });
902
+ stdin.write(`${text}\n`, this.#confirmWrite.bind(this, settled));
851
903
  } catch {
852
- return Promise.resolve(false);
904
+ this.#settleWrite(settled, false);
905
+ return settled.promise;
906
+ }
907
+ if (this.#delivery > 0 && this.#writes.has(settled)) {
908
+ const timer = setTimeout(() => this.#settleWrite(settled, false), this.#delivery);
909
+ timer.unref();
910
+ this.#writes.set(settled, timer);
853
911
  }
854
912
  return settled.promise;
855
913
  }
856
914
  /**
857
- * Terminate the child process tree and await its observed exit.
915
+ * Terminates the child process tree and reaches the terminal observation moment.
858
916
  *
859
917
  * @remarks
860
- * Never rejects, and every call shares one termination.
918
+ * Never rejects, and every call shares one termination. After the termination sequence returns,
919
+ * confirmed or not, it waits at most `drain` for the read channels to close, then cuts them off
920
+ * and settles `exit` when they remain open. A cutoff reached before the native exit reports the
921
+ * `code` and `signal` the host had recorded by then, which is `null` for a child still running.
861
922
  *
862
923
  * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
863
924
  */
864
925
  stop() {
865
- if (this.#stopping !== void 0) return this.#stopping;
866
- this.#stopping = this.#kill();
867
- return this.#stopping;
926
+ if (this.#termination !== void 0) return this.#termination;
927
+ this.#termination = this.#kill();
928
+ return this.#termination;
868
929
  }
869
930
  /**
870
- * Stop the child, close its standard-input channel, and destroy the observation emitter.
931
+ * Stops the child and destroys the observation emitter after the terminal state freezes.
871
932
  *
872
933
  * @remarks
873
- * Always resolves, including when termination was never confirmed.
934
+ * Always resolves, including when termination was never confirmed. Every call shares one barrier.
874
935
  *
875
936
  * @returns The stable barrier shared by every call
876
937
  */
877
938
  destroy() {
878
939
  if (this.#ending !== void 0) return this.#ending;
879
- this.#ending = this.#teardown();
940
+ this.#ending = this.#end();
880
941
  return this.#ending;
881
942
  }
882
943
  #iterate() {
@@ -922,8 +983,8 @@ var Process = class {
922
983
  return;
923
984
  }
924
985
  const bytes = node_buffer.Buffer.byteLength(line) + 1;
925
- const limit = this.#terminating ? this.#backlog * 2 : this.#backlog;
926
- if ((!this.#requested || this.#terminating) && this.#pending + bytes > limit) {
986
+ const limit = this.#stopping ? this.#backlog * 2 : this.#backlog;
987
+ if ((!this.#requested || this.#stopping) && this.#pending + bytes > limit) {
927
988
  this.#truncated = true;
928
989
  return;
929
990
  }
@@ -932,14 +993,14 @@ var Process = class {
932
993
  this.#restrain();
933
994
  }
934
995
  #restrain() {
935
- if (this.#paused || this.#terminating || !this.#requested) return;
996
+ if (this.#paused || this.#stopping || !this.#requested) return;
936
997
  if (this.#pending < this.#backlog) return;
937
998
  this.#paused = true;
938
999
  this.#reader.pause();
939
1000
  }
940
1001
  #relieve() {
941
1002
  if (!this.#paused || this.#ended) return;
942
- if (!this.#terminating && this.#pending > this.#backlog / 2) return;
1003
+ if (!this.#stopping && this.#pending > this.#backlog / 2) return;
943
1004
  this.#paused = false;
944
1005
  this.#reader.resume();
945
1006
  }
@@ -954,24 +1015,40 @@ var Process = class {
954
1015
  this.#waiters.length = 0;
955
1016
  }
956
1017
  #retain(chunk) {
957
- if (!node_buffer.Buffer.isBuffer(chunk) || this.#closed) return;
1018
+ if (!node_buffer.Buffer.isBuffer(chunk) || this.#settled) return;
958
1019
  this.#tail = trimTail(node_buffer.Buffer.concat([this.#tail, chunk]), this.#evidence);
959
1020
  const text = this.#decoder.write(chunk);
960
1021
  if (text.length > 0) this.#emitter.emit("stderr", text);
961
1022
  }
962
- #close(code, signal) {
963
- if (this.#closed) return;
1023
+ #close() {
1024
+ if (this.#settled) return;
1025
+ this.#settle(true);
1026
+ }
1027
+ #expire() {
1028
+ if (this.#settled) return;
1029
+ this.#wait();
1030
+ }
1031
+ #wait() {
1032
+ this.#waiting ??= waitForClose(this.#child, this.#drain).then(() => {
1033
+ if (!this.#settled) this.#settle(false);
1034
+ });
1035
+ return this.#waiting;
1036
+ }
1037
+ #settle(drained) {
964
1038
  const suffix = this.#decoder.end();
965
1039
  if (suffix.length > 0) this.#emitter.emit("stderr", suffix);
966
- this.#closed = true;
1040
+ this.#settled = true;
967
1041
  this.#removeAbortListener();
968
1042
  this.#reader.close();
969
1043
  const exit = Object.freeze({
970
- code,
971
- signal
1044
+ code: this.code,
1045
+ signal: this.signal,
1046
+ drained
972
1047
  });
973
1048
  this.#exit.resolve(exit);
974
1049
  this.#emitter.emit("exit", exit);
1050
+ this.#child.stdout.destroy();
1051
+ this.#child.stderr.destroy();
975
1052
  }
976
1053
  #terminate() {
977
1054
  this.stop();
@@ -980,6 +1057,42 @@ var Process = class {
980
1057
  if (this.#signal === void 0 || this.#abort === void 0) return;
981
1058
  this.#signal.removeEventListener("abort", this.#abort);
982
1059
  }
1060
+ #confirmWrite(settled, error) {
1061
+ if (error === void 0 || error === null) {
1062
+ this.#settleWrite(settled, true);
1063
+ return;
1064
+ }
1065
+ this.#failInputCallback(error);
1066
+ }
1067
+ #completeInput() {
1068
+ this.#input -= 1;
1069
+ }
1070
+ #settleWrite(settled, accepted) {
1071
+ if (!this.#writes.has(settled)) return;
1072
+ const timer = this.#writes.get(settled);
1073
+ this.#writes.delete(settled);
1074
+ clearTimeout(timer);
1075
+ settled.resolve(accepted);
1076
+ }
1077
+ #settleWrites() {
1078
+ for (const settled of this.#writes.keys()) this.#settleWrite(settled, false);
1079
+ }
1080
+ #failInputStream(cause) {
1081
+ if (this.#child.stdin.writableEnded || this.#input > 0) {
1082
+ this.#settleWrites();
1083
+ return;
1084
+ }
1085
+ this.#failInputCallback(cause);
1086
+ }
1087
+ #failInputCallback(cause) {
1088
+ if (this.#failure !== void 0 || this.#stopping) return;
1089
+ this.#failure = cause;
1090
+ this.#settleWrites();
1091
+ this.#emitter.emit("error", new _src_core.ProcessError("The standard-input channel failed", {
1092
+ code: "protocol",
1093
+ cause
1094
+ }));
1095
+ }
983
1096
  #capBacklog() {
984
1097
  const limit = this.#backlog * 2;
985
1098
  while (this.#pending > limit && this.#queue.length > this.#head) {
@@ -990,15 +1103,17 @@ var Process = class {
990
1103
  }
991
1104
  }
992
1105
  async #kill() {
993
- this.#terminating = true;
1106
+ this.#stopping = true;
994
1107
  this.#capBacklog();
995
1108
  this.#relieve();
996
1109
  const confirmed = await stopChild(this.#child, this.#grace, _src_core.PROCESS_CONFIRMATION);
1110
+ this.#settleWrites();
997
1111
  this.#child.stdin.destroy();
1112
+ if (!this.#settled) await this.#wait();
1113
+ if (!this.#settled) this.#settle(false);
998
1114
  return confirmed;
999
1115
  }
1000
- async #teardown() {
1001
- this.#removeAbortListener();
1116
+ async #end() {
1002
1117
  await this.stop();
1003
1118
  this.#emitter.destroy();
1004
1119
  }
@@ -1017,9 +1132,9 @@ var Process = class {
1017
1132
  * the child registered. `launch` throws a {@link createDuplicateError} when the id is already live
1018
1133
  * and a {@link createProtocolError} after `destroy` has begun, including when the caller's own option
1019
1134
  * getter begins that teardown mid-construction — a spawn fault surfaces through the returned child's
1020
- * `exit`, never from `launch`. `destroy` awaits every child's own teardown, which destroys each
1021
- * child's observation emitter and leaves every subscription on it silently inert, then destroys the
1022
- * registry emitter last.
1135
+ * `exit`, never from `launch`. `destroy` awaits every child's own teardown, including the teardown
1136
+ * of a child a lost race refused, which destroys each child's observation emitter and leaves every
1137
+ * subscription on it silently inert, then destroys the registry emitter last.
1023
1138
  *
1024
1139
  * @example
1025
1140
  * ```ts
@@ -1038,6 +1153,7 @@ var ProcessManager = class {
1038
1153
  #emitter;
1039
1154
  #children = /* @__PURE__ */ new Map();
1040
1155
  #ids = /* @__PURE__ */ new Set();
1156
+ #orphans = /* @__PURE__ */ new Set();
1041
1157
  #destroying = false;
1042
1158
  #ending;
1043
1159
  /**
@@ -1084,11 +1200,11 @@ var ProcessManager = class {
1084
1200
  * @remarks
1085
1201
  * {@link Process} reads every option before it spawns, so a caller's own option getter runs while
1086
1202
  * nothing has started and a throw from one strands no process. A getter that begins `destroy`
1087
- * without throwing is the one remaining race, and it leaves a bounded residual: the child is
1088
- * already spawned, so the launch is refused with a {@link createProtocolError} and that child is
1089
- * torn down asynchronously, bounded by `grace` plus the confirmation window. The `protocol`
1090
- * refusal throws synchronously before the `destroy` barrier settles. The barrier does not cover
1091
- * that child's asynchronous teardown.
1203
+ * without throwing is the one remaining race: the child is already spawned, so the launch is
1204
+ * refused with a {@link createProtocolError} and that child is destroyed rather than adopted, its
1205
+ * teardown bounded by `grace` plus the confirmation window. The `protocol` refusal throws
1206
+ * synchronously, and the `destroy` barrier covers that teardown, so the refused child reaches its
1207
+ * terminal moment before the barrier resolves.
1092
1208
  *
1093
1209
  * @param id - The registry key, unique among live children
1094
1210
  * @param options - The child construction options
@@ -1102,6 +1218,7 @@ var ProcessManager = class {
1102
1218
  const child = this.#construct(id, options);
1103
1219
  if (this.#destroying) {
1104
1220
  this.#ids.delete(id);
1221
+ this.#orphans.add(child);
1105
1222
  child.destroy();
1106
1223
  throw (0, _src_core.createProtocolError)(id);
1107
1224
  }
@@ -1158,6 +1275,11 @@ var ProcessManager = class {
1158
1275
  }
1159
1276
  async #teardown() {
1160
1277
  await Promise.allSettled([...this.#children.values()].map((child) => child.destroy()));
1278
+ while (this.#orphans.size > 0) {
1279
+ const orphans = [...this.#orphans];
1280
+ this.#orphans.clear();
1281
+ await Promise.allSettled(orphans.map((child) => child.destroy()));
1282
+ }
1161
1283
  this.#children.clear();
1162
1284
  this.#ids.clear();
1163
1285
  this.#emitter.destroy();
@@ -1258,9 +1380,13 @@ var Retention = class {
1258
1380
  * rather than on process exit, so a descendant that inherited the child's stdio holds the run open
1259
1381
  * after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
1260
1382
  * the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
1261
- * overrides `command.input`. Unless `strict` is `false`, a failed run rejects with a
1262
- * {@link createExecuteError} carrying the {@link ExecuteResult}. An invalid option or command string rejects
1263
- * before the child is spawned, because an async function cannot throw synchronously.
1383
+ * overrides `command.input`. A host fault while writing that input terminates the run by design and
1384
+ * marks its result failed; a strict rejection carries the host fault as its cause. This differs
1385
+ * from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
1386
+ * is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
1387
+ * {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
1388
+ * option or command string rejects before the child is spawned, because an async function cannot
1389
+ * throw synchronously.
1264
1390
  *
1265
1391
  * @param command - The executable, arguments, and optional environment and input
1266
1392
  * @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
@@ -1304,6 +1430,7 @@ async function execute(command, options) {
1304
1430
  const terminate = new AbortController();
1305
1431
  const cleanup = new AbortController();
1306
1432
  const finish = new AbortController();
1433
+ const inputFailure = new AbortController();
1307
1434
  const outChunks = [];
1308
1435
  const errChunks = [];
1309
1436
  const outRetention = new Retention();
@@ -1344,6 +1471,7 @@ async function execute(command, options) {
1344
1471
  }));
1345
1472
  }, { once: true });
1346
1473
  terminate.signal.addEventListener("abort", () => {
1474
+ if (finish.signal.aborted) return;
1347
1475
  clearTimeout(timeoutTimer);
1348
1476
  cleanup.abort();
1349
1477
  stopChild(child, grace, _src_core.PROCESS_CONFIRMATION).then(() => {
@@ -1351,7 +1479,12 @@ async function execute(command, options) {
1351
1479
  confirmTimer = setTimeout(() => finish.abort(), _src_core.PROCESS_CONFIRMATION);
1352
1480
  });
1353
1481
  }, { once: true });
1354
- child.stdin.on("error", () => void 0);
1482
+ inputFailure.signal.addEventListener("abort", () => {
1483
+ if (finish.signal.aborted) return;
1484
+ cause = inputFailure.signal.reason;
1485
+ terminate.abort();
1486
+ }, { once: true });
1487
+ child.stdin.on("error", (error) => inputFailure.abort(error));
1355
1488
  child.stdout.on("data", (chunk) => {
1356
1489
  const retained = outRetention.retain(chunk, limit);
1357
1490
  if (retained !== void 0) outChunks.push(retained);
@@ -1369,7 +1502,9 @@ async function execute(command, options) {
1369
1502
  finish.abort();
1370
1503
  });
1371
1504
  child.once("close", () => finish.abort());
1372
- if (text !== void 0) child.stdin.write(text);
1505
+ if (text !== void 0) child.stdin.write(text, (error) => {
1506
+ if (error !== void 0 && error !== null) inputFailure.abort(error);
1507
+ });
1373
1508
  child.stdin.end();
1374
1509
  if (timeout > 0) timeoutTimer = setTimeout(() => {
1375
1510
  expired = true;
@@ -1389,7 +1524,19 @@ async function execute(command, options) {
1389
1524
  }
1390
1525
  }
1391
1526
  const result = await settled.promise;
1392
- if (result.failed && strict) throw (0, _src_core.createExecuteError)(result, cause);
1527
+ if (result.failed && strict) {
1528
+ if (cause !== void 0 && cause === inputFailure.signal.reason) throw new _src_core.ProcessError(`Command '${result.command}' failed while writing standard input`, {
1529
+ code: "input",
1530
+ context: {
1531
+ command: result.command,
1532
+ code: result.code,
1533
+ signal: result.signal
1534
+ },
1535
+ cause: inputFailure.signal.reason,
1536
+ result
1537
+ });
1538
+ throw (0, _src_core.createExecuteError)(result, cause);
1539
+ }
1393
1540
  return result;
1394
1541
  }
1395
1542
  //#endregion
@@ -1547,6 +1694,7 @@ exports.validateEnvironment = validateEnvironment;
1547
1694
  exports.validateText = validateText;
1548
1695
  exports.validateTimer = validateTimer;
1549
1696
  exports.validateWorkspace = validateWorkspace;
1697
+ exports.waitForClose = waitForClose;
1550
1698
  exports.waitForExit = waitForExit;
1551
1699
 
1552
1700
  //# sourceMappingURL=index.cjs.map