@orkestrel/process 0.0.15 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ import { PROCESS_BACKLOG, PROCESS_CONFIRMATION, PROCESS_DRAIN, PROCESS_EVIDENCE,
8
8
  import { createInterface } from "node:readline";
9
9
  import { Emitter } from "@orkestrel/emitter";
10
10
  import { StringDecoder } from "node:string_decoder";
11
- //#region src/server/cloners.ts
11
+ // src/server/cloners.ts
12
12
  /**
13
13
  * Takes one owned frozen snapshot of a caller's command.
14
14
  *
@@ -43,8 +43,7 @@ function snapshotCommand(command) {
43
43
  ...isolated === void 0 ? {} : { isolated }
44
44
  });
45
45
  }
46
- //#endregion
47
- //#region src/server/helpers.ts
46
+ // src/server/helpers.ts
48
47
  /**
49
48
  * Trims a buffer to at most `limit` trailing bytes without splitting a UTF-8 sequence.
50
49
  *
@@ -65,13 +64,13 @@ function snapshotCommand(command) {
65
64
  function trimTail(bytes, limit) {
66
65
  const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
67
66
  if (buffer.byteLength <= limit) return buffer;
68
- let start = buffer.byteLength - limit;
69
- while (start < buffer.byteLength) {
67
+ const offset = buffer.byteLength - limit;
68
+ if (!(offset < buffer.byteLength)) return Buffer.from(buffer.subarray(offset));
69
+ for (const start of Array.from({ length: Math.ceil(buffer.byteLength - offset) }, (_, index) => offset + index)) {
70
70
  const byte = buffer[start];
71
- if (byte === void 0 || (byte & 192) !== 128) break;
72
- start += 1;
71
+ if (byte === void 0 || (byte & 192) !== 128) return Buffer.from(buffer.subarray(start));
73
72
  }
74
- return Buffer.from(buffer.subarray(start));
73
+ return Buffer.from(buffer.subarray(buffer.byteLength));
75
74
  }
76
75
  /**
77
76
  * Trims a buffer to at most `limit` leading bytes without splitting a UTF-8 sequence.
@@ -99,18 +98,12 @@ function trimHead(bytes, limit) {
99
98
  const excluded = buffer[limit];
100
99
  if (excluded === void 0 || (excluded & 192) !== 128) return Buffer.from(buffer.subarray(0, limit));
101
100
  const floor = Math.max(0, limit - 3);
102
- let start = limit - 1;
103
- while (start >= floor) {
101
+ for (const start of Array.from({ length: Math.max(0, Math.floor(limit - floor)) }, (_, index) => limit - 1 - index)) {
104
102
  const lead = buffer[start];
105
103
  if (lead !== void 0 && (lead & 192) !== 128) {
106
- let span = 1;
107
- if ((lead & 224) === 192) span = 2;
108
- else if ((lead & 240) === 224) span = 3;
109
- else if ((lead & 248) === 240) span = 4;
110
- if (start + span > limit) return Buffer.from(buffer.subarray(0, start));
104
+ if (start + ((lead & 224) === 192 ? 2 : (lead & 240) === 224 ? 3 : (lead & 248) === 240 ? 4 : 1) > limit) return Buffer.from(buffer.subarray(0, start));
111
105
  break;
112
106
  }
113
- start -= 1;
114
107
  }
115
108
  return Buffer.from(buffer.subarray(0, limit));
116
109
  }
@@ -552,6 +545,27 @@ function isExited(child) {
552
545
  return child.exitCode !== null || child.signalCode !== null;
553
546
  }
554
547
  /**
548
+ * Reports whether a process or POSIX process group still exists.
549
+ *
550
+ * @param pid - A positive process id, or a negative POSIX process-group id
551
+ * @returns True when the host confirms existence or refuses permission; false for ESRCH; undefined for another host failure
552
+ * @throws A {@link ProcessError} coded `invalid` for an unsafe integer, zero, minus one, or a negative id on Windows
553
+ * @remarks Negative ids probe POSIX process groups. The `process.platform` branch refuses negative ids on Windows, where the host supports positive process ids only.
554
+ * @example
555
+ * ```ts
556
+ * inspectProcess(process.pid) // true
557
+ * ```
558
+ */
559
+ function inspectProcess(pid) {
560
+ if (!Number.isSafeInteger(pid) || pid === 0 || pid === -1 || process.platform === "win32" && pid < 0) throw createInvalidError("process id", pid);
561
+ try {
562
+ return process.kill(pid, 0);
563
+ } catch (error) {
564
+ const code = isError(error) && "code" in error ? error.code : void 0;
565
+ return code === "ESRCH" ? false : code === "EPERM" ? true : void 0;
566
+ }
567
+ }
568
+ /**
555
569
  * Signals one owned child process, or its detached process group on a POSIX host.
556
570
  *
557
571
  * @remarks
@@ -744,262 +758,6 @@ function buildExecuteResult(input) {
744
758
  });
745
759
  }
746
760
  /**
747
- * Runs one command to completion, buffering its output, and settles with the outcome.
748
- *
749
- * @remarks
750
- * The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
751
- * child is detached, which is what lets its whole process group be terminated. Standard output and
752
- * error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
753
- * whether either stream exceeded it. Each capture keeps one byte beyond `limit`, so the single
754
- * code-point-boundary trim in {@link buildExecuteResult} reads the first excluded byte and never
755
- * delivers a split multibyte sequence. A positive `timeout` and an aborting `signal` both terminate the
756
- * child through the same bounded stop, and only the earliest is recorded: a timeout reports
757
- * `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
758
- * a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
759
- * covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
760
- * rather than on process exit, so a descendant that inherited the child's stdio holds the run open
761
- * after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
762
- * the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
763
- * overrides `command.input`. A host fault while writing that input terminates the run by design and
764
- * marks its result failed; a strict rejection carries the host fault as its cause. This differs
765
- * from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
766
- * is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
767
- * {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
768
- * option or command string rejects before the child is spawned, because an async function cannot
769
- * throw synchronously.
770
- *
771
- * @param command - The executable, arguments, and optional environment and input
772
- * @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
773
- * @returns The settled run outcome
774
- * @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`
775
- *
776
- * @example
777
- * ```ts
778
- * const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
779
- * ```
780
- */
781
- async function execute(command, options) {
782
- const snapshot = snapshotCommand(command);
783
- const optionEnvironment = options?.environment;
784
- const optionWorkspace = options?.workspace;
785
- const optionInput = options?.input;
786
- const optionTimeout = options?.timeout;
787
- const optionGrace = options?.grace;
788
- const optionSignal = options?.signal;
789
- const optionStrict = options?.strict;
790
- const optionLimit = options?.limit;
791
- validateCommand(snapshot);
792
- validateEnvironment(optionEnvironment);
793
- validateWorkspace(optionWorkspace);
794
- validateTimer(optionTimeout, "option 'timeout'");
795
- validateTimer(optionGrace, "option 'grace'");
796
- validateBytes(optionLimit, "option 'limit'", 0);
797
- const limit = optionLimit ?? PROCESS_OUTPUT;
798
- const grace = optionGrace ?? PROCESS_GRACE;
799
- const timeout = optionTimeout ?? 0;
800
- const strict = optionStrict ?? true;
801
- const text = optionInput ?? snapshot.input;
802
- const line = formatCommand(snapshot);
803
- const workspace = optionWorkspace ?? process.cwd();
804
- const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
805
- const plan = buildSpawn(snapshot, {
806
- workspace,
807
- environment
808
- });
809
- const settled = Promise.withResolvers();
810
- const terminate = new AbortController();
811
- const cleanup = new AbortController();
812
- const finish = new AbortController();
813
- const inputFailure = new AbortController();
814
- const outChunks = [];
815
- const errChunks = [];
816
- let spawned = false;
817
- let expired = false;
818
- let aborted = false;
819
- let outRetained = 0;
820
- let errRetained = 0;
821
- let cause;
822
- let timeoutTimer;
823
- let confirmTimer;
824
- const child = spawn(plan.file, [...plan.arguments], {
825
- cwd: workspace,
826
- detached: process.platform !== "win32",
827
- env: environment,
828
- stdio: [
829
- "pipe",
830
- "pipe",
831
- "pipe"
832
- ],
833
- windowsHide: true,
834
- windowsVerbatimArguments: plan.verbatim
835
- });
836
- finish.signal.addEventListener("abort", () => {
837
- clearTimeout(timeoutTimer);
838
- clearTimeout(confirmTimer);
839
- cleanup.abort();
840
- settled.resolve(buildExecuteResult({
841
- command: line,
842
- stdout: Buffer.concat(outChunks),
843
- stderr: Buffer.concat(errChunks),
844
- code: child.exitCode,
845
- signal: child.signalCode,
846
- expired,
847
- aborted,
848
- truncated: outRetained > limit || errRetained > limit,
849
- limit,
850
- ...cause === void 0 ? {} : { cause }
851
- }));
852
- }, { once: true });
853
- terminate.signal.addEventListener("abort", () => {
854
- if (finish.signal.aborted) return;
855
- clearTimeout(timeoutTimer);
856
- cleanup.abort();
857
- stopChild(child, grace, PROCESS_CONFIRMATION).then(() => {
858
- if (finish.signal.aborted) return;
859
- confirmTimer = setTimeout(() => finish.abort(), PROCESS_CONFIRMATION);
860
- });
861
- }, { once: true });
862
- inputFailure.signal.addEventListener("abort", () => {
863
- if (finish.signal.aborted) return;
864
- cause = inputFailure.signal.reason;
865
- terminate.abort();
866
- }, { once: true });
867
- child.stdin.on("error", (error) => inputFailure.abort(error));
868
- child.stdout.on("data", (chunk) => {
869
- const captured = captureChunk(chunk, limit + 1 - outRetained);
870
- if (captured === void 0) return;
871
- outRetained += captured.byteLength;
872
- outChunks.push(captured);
873
- });
874
- child.stderr.on("data", (chunk) => {
875
- const captured = captureChunk(chunk, limit + 1 - errRetained);
876
- if (captured === void 0) return;
877
- errRetained += captured.byteLength;
878
- errChunks.push(captured);
879
- });
880
- child.once("spawn", () => {
881
- spawned = true;
882
- });
883
- child.once("error", (error) => {
884
- if (spawned) return;
885
- cause = error;
886
- finish.abort();
887
- });
888
- child.once("close", () => finish.abort());
889
- if (text !== void 0) child.stdin.write(text, (error) => {
890
- if (error !== void 0 && error !== null) inputFailure.abort(error);
891
- });
892
- child.stdin.end();
893
- if (timeout > 0) timeoutTimer = setTimeout(() => {
894
- expired = true;
895
- terminate.abort();
896
- }, timeout);
897
- if (optionSignal !== void 0) {
898
- optionSignal.addEventListener("abort", () => {
899
- aborted = true;
900
- terminate.abort();
901
- }, {
902
- once: true,
903
- signal: cleanup.signal
904
- });
905
- if (optionSignal.aborted) {
906
- aborted = true;
907
- terminate.abort();
908
- }
909
- }
910
- const result = await settled.promise;
911
- if (result.failed && strict) {
912
- if (cause !== void 0 && cause === inputFailure.signal.reason) throw new ProcessError(`Command '${result.command}' failed while writing standard input`, {
913
- code: "input",
914
- context: {
915
- command: result.command,
916
- code: result.code,
917
- signal: result.signal
918
- },
919
- cause: inputFailure.signal.reason,
920
- result
921
- });
922
- throw createExecuteError(result, cause);
923
- }
924
- return result;
925
- }
926
- /**
927
- * Runs one command to completion synchronously, buffering its output, and returns the outcome.
928
- *
929
- * @remarks
930
- * The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
931
- * shell. The host offers no cooperative termination window and no in-flight cancellation, so this
932
- * contract carries neither. A positive `timeout` ends only the root process and can leave
933
- * descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
934
- * timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
935
- * and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
936
- * reports `truncated` without failing. The environment and input follow the same merge as
937
- * {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
938
- * carrying the {@link ExecuteResult}.
939
- *
940
- * @param command - The executable, arguments, and optional environment and input
941
- * @param options - Working directory, timeout, capture limit, and failure delivery
942
- * @returns The run outcome
943
- * @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`
944
- *
945
- * @example
946
- * ```ts
947
- * const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
948
- * ```
949
- */
950
- function executeSync(command, options) {
951
- const snapshot = snapshotCommand(command);
952
- const optionEnvironment = options?.environment;
953
- const optionWorkspace = options?.workspace;
954
- const optionInput = options?.input;
955
- const optionTimeout = options?.timeout;
956
- const optionStrict = options?.strict;
957
- const optionLimit = options?.limit;
958
- validateCommand(snapshot);
959
- validateEnvironment(optionEnvironment);
960
- validateWorkspace(optionWorkspace);
961
- validateTimer(optionTimeout, "option 'timeout'");
962
- validateBytes(optionLimit, "option 'limit'", 0);
963
- const limit = optionLimit ?? PROCESS_OUTPUT;
964
- const timeout = optionTimeout ?? 0;
965
- const strict = optionStrict ?? true;
966
- const text = optionInput ?? snapshot.input;
967
- const line = formatCommand(snapshot);
968
- const workspace = optionWorkspace ?? process.cwd();
969
- const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
970
- const plan = buildSpawn(snapshot, {
971
- workspace,
972
- environment
973
- });
974
- const outcome = spawnSync(plan.file, [...plan.arguments], {
975
- cwd: workspace,
976
- env: environment,
977
- encoding: "buffer",
978
- maxBuffer: limit,
979
- killSignal: "SIGKILL",
980
- windowsHide: true,
981
- windowsVerbatimArguments: plan.verbatim,
982
- ...text !== void 0 ? { input: Buffer.from(text) } : {},
983
- ...timeout > 0 ? { timeout } : {}
984
- });
985
- const error = outcome.error;
986
- const fault = error !== void 0 && "code" in error && isString(error.code) ? error.code : "";
987
- const result = buildExecuteResult({
988
- command: line,
989
- stdout: Buffer.isBuffer(outcome.stdout) ? outcome.stdout : Buffer.alloc(0),
990
- stderr: Buffer.isBuffer(outcome.stderr) ? outcome.stderr : Buffer.alloc(0),
991
- code: outcome.status,
992
- signal: outcome.signal,
993
- expired: fault === "ETIMEDOUT",
994
- aborted: false,
995
- truncated: fault === "ENOBUFS",
996
- limit,
997
- ...error === void 0 ? {} : { cause: error }
998
- });
999
- if (result.failed && strict) throw createExecuteError(result, error);
1000
- return result;
1001
- }
1002
- /**
1003
761
  * Spawns one command as a detached process and returns without waiting for it.
1004
762
  *
1005
763
  * @remarks
@@ -1040,8 +798,7 @@ function detach(command, options) {
1040
798
  child.once("error", () => void 0);
1041
799
  child.unref();
1042
800
  }
1043
- //#endregion
1044
- //#region src/server/processes/Supervisor.ts
801
+ // src/server/processes/Supervisor.ts
1045
802
  /**
1046
803
  * Supervises one child process and reports each lifecycle moment to the face composing it.
1047
804
  *
@@ -1054,7 +811,7 @@ function detach(command, options) {
1054
811
  * emitter and event map its consumers subscribe to.
1055
812
  *
1056
813
  * The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read the
1057
- * host child directly, so they expose native exit as soon as the host records it, and `ending`
814
+ * host child directly, so they expose native exit as soon as the host records it, and `ended`
1058
815
  * settles at that same moment. `settled`, `exit`, and `evidence` reach one terminal moment after the
1059
816
  * read channels close or the bounded `drain` window cuts them off. That window is armed by the
1060
817
  * child's native exit and by a termination this package initiated, so every way a child can end
@@ -1104,12 +861,12 @@ var Supervisor = class {
1104
861
  #delivery;
1105
862
  #decoder = new StringDecoder("utf8");
1106
863
  #exit = Promise.withResolvers();
1107
- #ending = Promise.withResolvers();
864
+ #ended = Promise.withResolvers();
1108
865
  #signal;
1109
866
  #abort;
1110
867
  #writes = /* @__PURE__ */ new Map();
1111
868
  #tail = Buffer.alloc(0);
1112
- #stopping = false;
869
+ #terminated = false;
1113
870
  #settled = false;
1114
871
  #input = 0;
1115
872
  #failure;
@@ -1220,8 +977,8 @@ var Supervisor = class {
1220
977
  return this.#settled;
1221
978
  }
1222
979
  /** Reports whether termination began, including after the terminal moment. */
1223
- get stopping() {
1224
- return this.#stopping;
980
+ get terminated() {
981
+ return this.#terminated;
1225
982
  }
1226
983
  /**
1227
984
  * Settles at the child's own ending, awaited without the terminal moment's drain window.
@@ -1231,8 +988,8 @@ var Supervisor = class {
1231
988
  * the host records them. It settles at the native exit, and at the terminal moment for a child
1232
989
  * whose spawn produced no native exit at all, so it can never outlive the supervision.
1233
990
  */
1234
- get ending() {
1235
- return this.#ending.promise;
991
+ get ended() {
992
+ return this.#ended.promise;
1236
993
  }
1237
994
  /** Settles with the terminal child state, observed once after stream close or the drain cutoff. */
1238
995
  get exit() {
@@ -1278,7 +1035,7 @@ var Supervisor = class {
1278
1035
  */
1279
1036
  deliver(bytes) {
1280
1037
  const stdin = this.#child.stdin;
1281
- if (this.#settled || this.#stopping || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
1038
+ if (this.#settled || this.#terminated || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
1282
1039
  const settled = Promise.withResolvers();
1283
1040
  this.#writes.set(settled, void 0);
1284
1041
  try {
@@ -1298,7 +1055,7 @@ var Supervisor = class {
1298
1055
  * Closes the standard-input channel while leaving the child running.
1299
1056
  *
1300
1057
  * @remarks
1301
- * Never rejects, and terminates nothing: no signal is sent, `stopping` stays false, no drain
1058
+ * Never rejects, and terminates nothing: no signal is sent, `terminated` stays false, no drain
1302
1059
  * window is armed, and the terminal moment does not arrive. The child observes end of input and
1303
1060
  * decides for itself what to do about it, which is what separates this from `stop`.
1304
1061
  *
@@ -1356,7 +1113,7 @@ var Supervisor = class {
1356
1113
  this.#settle(true);
1357
1114
  }
1358
1115
  #expire() {
1359
- this.#ending.resolve();
1116
+ this.#ended.resolve();
1360
1117
  if (this.#settled) return;
1361
1118
  this.#wait();
1362
1119
  }
@@ -1378,7 +1135,7 @@ var Supervisor = class {
1378
1135
  drained
1379
1136
  });
1380
1137
  this.#exit.resolve(exit);
1381
- this.#ending.resolve();
1138
+ this.#ended.resolve();
1382
1139
  this.#terminal(exit);
1383
1140
  this.#child.stdout.destroy();
1384
1141
  this.#child.stderr.destroy();
@@ -1402,7 +1159,7 @@ var Supervisor = class {
1402
1159
  }
1403
1160
  #endInput() {
1404
1161
  const stdin = this.#child.stdin;
1405
- if (this.#stopping || this.#settled || stdin.writableEnded || stdin.destroyed) return Promise.resolve();
1162
+ if (this.#terminated || this.#settled || stdin.writableEnded || stdin.destroyed) return Promise.resolve();
1406
1163
  const ended = Promise.withResolvers();
1407
1164
  stdin.end(() => ended.resolve());
1408
1165
  return ended.promise;
@@ -1429,7 +1186,7 @@ var Supervisor = class {
1429
1186
  this.#settleWrites();
1430
1187
  return;
1431
1188
  }
1432
- if (this.#failure !== void 0 || this.#stopping) return;
1189
+ if (this.#failure !== void 0 || this.#terminated) return;
1433
1190
  this.#failure = cause;
1434
1191
  this.#settleWrites();
1435
1192
  this.#fault(new ProcessError("The standard-input channel failed", {
@@ -1438,7 +1195,7 @@ var Supervisor = class {
1438
1195
  }));
1439
1196
  }
1440
1197
  async #kill() {
1441
- this.#stopping = true;
1198
+ this.#terminated = true;
1442
1199
  this.#relieve?.();
1443
1200
  const confirmed = await stopChild(this.#child, this.#grace, PROCESS_CONFIRMATION);
1444
1201
  this.#settleWrites();
@@ -1452,8 +1209,7 @@ var Supervisor = class {
1452
1209
  this.#teardown();
1453
1210
  }
1454
1211
  };
1455
- //#endregion
1456
- //#region src/server/processes/Process.ts
1212
+ // src/server/processes/Process.ts
1457
1213
  /**
1458
1214
  * Supervises one child, frames its standard output into lines under a bounded backlog, and keeps
1459
1215
  * every observation channel aligned at termination.
@@ -1589,8 +1345,8 @@ var Process = class {
1589
1345
  return this.#engine.settled;
1590
1346
  }
1591
1347
  /** Reports whether termination began, including after the terminal moment. */
1592
- get stopping() {
1593
- return this.#engine.stopping;
1348
+ get terminated() {
1349
+ return this.#engine.terminated;
1594
1350
  }
1595
1351
  /** Settles with the terminal child state, observed once after stream close or the drain cutoff. */
1596
1352
  get exit() {
@@ -1684,9 +1440,9 @@ var Process = class {
1684
1440
  return;
1685
1441
  }
1686
1442
  const bytes = Buffer.byteLength(line) + 1;
1687
- const stopping = this.#engine.stopping;
1688
- const limit = stopping ? this.#backlog * 2 : this.#backlog;
1689
- if ((!this.#requested || stopping) && this.#pending + bytes > limit) {
1443
+ const terminated = this.#engine.terminated;
1444
+ const limit = terminated ? this.#backlog * 2 : this.#backlog;
1445
+ if ((!this.#requested || terminated) && this.#pending + bytes > limit) {
1690
1446
  this.#truncated = true;
1691
1447
  return;
1692
1448
  }
@@ -1695,14 +1451,14 @@ var Process = class {
1695
1451
  this.#restrain();
1696
1452
  }
1697
1453
  #restrain() {
1698
- if (this.#paused || this.#engine.stopping || !this.#requested) return;
1454
+ if (this.#paused || this.#engine.terminated || !this.#requested) return;
1699
1455
  if (this.#pending < this.#backlog) return;
1700
1456
  this.#paused = true;
1701
1457
  this.#reader.pause();
1702
1458
  }
1703
1459
  #relieve() {
1704
1460
  if (!this.#paused || this.#ended) return;
1705
- if (!this.#engine.stopping && this.#pending > this.#backlog / 2) return;
1461
+ if (!this.#engine.terminated && this.#pending > this.#backlog / 2) return;
1706
1462
  this.#paused = false;
1707
1463
  this.#reader.resume();
1708
1464
  }
@@ -1742,8 +1498,7 @@ var Process = class {
1742
1498
  }
1743
1499
  }
1744
1500
  };
1745
- //#endregion
1746
- //#region src/server/processes/ProcessManager.ts
1501
+ // src/server/processes/ProcessManager.ts
1747
1502
  /**
1748
1503
  * Launches supervised children under caller-chosen ids, evicts each one as it settles, and destroys
1749
1504
  * every live child on teardown.
@@ -1780,7 +1535,7 @@ var ProcessManager = class {
1780
1535
  #ids = /* @__PURE__ */ new Set();
1781
1536
  #orphans = /* @__PURE__ */ new Set();
1782
1537
  #destroying = false;
1783
- #ending;
1538
+ #ended;
1784
1539
  /**
1785
1540
  * Constructs a process registry.
1786
1541
  *
@@ -1868,10 +1623,10 @@ var ProcessManager = class {
1868
1623
  * @returns The stable barrier shared by every call
1869
1624
  */
1870
1625
  destroy() {
1871
- if (this.#ending !== void 0) return this.#ending;
1626
+ if (this.#ended !== void 0) return this.#ended;
1872
1627
  this.#destroying = true;
1873
- this.#ending = this.#teardown();
1874
- return this.#ending;
1628
+ this.#ended = this.#teardown();
1629
+ return this.#ended;
1875
1630
  }
1876
1631
  #construct(id, options) {
1877
1632
  try {
@@ -1910,8 +1665,7 @@ var ProcessManager = class {
1910
1665
  this.#emitter.destroy();
1911
1666
  }
1912
1667
  };
1913
- //#endregion
1914
- //#region src/server/processes/Session.ts
1668
+ // src/server/processes/Session.ts
1915
1669
  /**
1916
1670
  * Supervises one child and publishes its standard output as raw bytes.
1917
1671
  *
@@ -1929,7 +1683,7 @@ var ProcessManager = class {
1929
1683
  *
1930
1684
  * The standard-input channel is open from the spawn, and `end` closes it without terminating
1931
1685
  * anything. That is the member a cooperative protocol reaches for: the child observes end of input
1932
- * and exits on its own, which `ending` reports. `stop` and `destroy` remain the terminations, and
1686
+ * and exits on its own, which `ended` reports. `stop` and `destroy` remain the terminations, and
1933
1687
  * `exit` remains the terminal moment — the native exit plus at most `drain` while a descendant holds
1934
1688
  * the inherited read ends.
1935
1689
  *
@@ -2011,12 +1765,12 @@ var Session = class {
2011
1765
  return this.#engine.settled;
2012
1766
  }
2013
1767
  /** Reports whether termination began, including after the terminal moment. `end` never turns it true. */
2014
- get stopping() {
2015
- return this.#engine.stopping;
1768
+ get terminated() {
1769
+ return this.#engine.terminated;
2016
1770
  }
2017
1771
  /** Settles at the child's own ending, awaited without the terminal moment's drain window. */
2018
- get ending() {
2019
- return this.#engine.ending;
1772
+ get ended() {
1773
+ return this.#engine.ended;
2020
1774
  }
2021
1775
  /** Settles with the terminal child state, observed once after stream close or the drain cutoff. */
2022
1776
  get exit() {
@@ -2041,7 +1795,7 @@ var Session = class {
2041
1795
  * Closes the standard-input channel and leaves the child running.
2042
1796
  *
2043
1797
  * @remarks
2044
- * Never rejects and terminates nothing: `stopping` stays false and the terminal moment does not
1798
+ * Never rejects and terminates nothing: `terminated` stays false and the terminal moment does not
2045
1799
  * arrive. Every call shares one barrier, which resolves after the host flushes the writes it had
2046
1800
  * already accepted. A later `write` resolves `false`.
2047
1801
  *
@@ -2089,8 +1843,7 @@ var Session = class {
2089
1843
  this.#engine.stdout.off("data", this.#output);
2090
1844
  }
2091
1845
  };
2092
- //#endregion
2093
- //#region src/server/factories.ts
1846
+ // src/server/factories.ts
2094
1847
  /**
2095
1848
  * Creates one supervised child process and returns it as a {@link ProcessInterface}, so a caller
2096
1849
  * holds the published contract rather than the `Process` class.
@@ -2159,7 +1912,284 @@ function createSession(options) {
2159
1912
  function createProcessManager(options) {
2160
1913
  return new ProcessManager(options);
2161
1914
  }
2162
- //#endregion
2163
- export { Process, ProcessManager, Session, Supervisor, buildExecutableCandidates, buildExecuteResult, buildPlatformSpawn, buildSpawn, captureChunk, createProcess, createProcessManager, createSession, detach, execute, executeSync, formatCommand, isExited, isFile, killProcess, killTree, mergeEnvironment, mergePlatformEnvironment, quoteArgument, readPlatformVariable, readVariable, resolveExecutable, snapshotCommand, stopChild, trimHead, trimTail, validateBytes, validateCommand, validateEnvironment, validateText, validateTimer, validateWorkspace, waitForClose, waitForExit };
1915
+ // src/server/processes/Execution.ts
1916
+ /** Owns the child, capture, cancellation, and terminal state of one buffered execution. */
1917
+ var Execution = class {
1918
+ #child;
1919
+ #settled;
1920
+ #terminate;
1921
+ #cleanup;
1922
+ #finish;
1923
+ #inputFailure;
1924
+ #outChunks = [];
1925
+ #errChunks = [];
1926
+ #strict;
1927
+ #spawned = false;
1928
+ #expired = false;
1929
+ #aborted = false;
1930
+ #outRetained = 0;
1931
+ #errRetained = 0;
1932
+ #cause;
1933
+ #timeoutTimer;
1934
+ #confirmTimer;
1935
+ constructor(command, options) {
1936
+ const snapshot = snapshotCommand(command);
1937
+ const optionEnvironment = options?.environment;
1938
+ const optionWorkspace = options?.workspace;
1939
+ const optionInput = options?.input;
1940
+ const optionTimeout = options?.timeout;
1941
+ const optionGrace = options?.grace;
1942
+ const optionSignal = options?.signal;
1943
+ const optionStrict = options?.strict;
1944
+ const optionLimit = options?.limit;
1945
+ validateCommand(snapshot);
1946
+ validateEnvironment(optionEnvironment);
1947
+ validateWorkspace(optionWorkspace);
1948
+ validateTimer(optionTimeout, "option 'timeout'");
1949
+ validateTimer(optionGrace, "option 'grace'");
1950
+ validateBytes(optionLimit, "option 'limit'", 0);
1951
+ const limit = optionLimit ?? PROCESS_OUTPUT;
1952
+ const grace = optionGrace ?? PROCESS_GRACE;
1953
+ const timeout = optionTimeout ?? 0;
1954
+ this.#strict = optionStrict ?? true;
1955
+ const text = optionInput ?? snapshot.input;
1956
+ const line = formatCommand(snapshot);
1957
+ const workspace = optionWorkspace ?? process.cwd();
1958
+ const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
1959
+ const plan = buildSpawn(snapshot, {
1960
+ workspace,
1961
+ environment
1962
+ });
1963
+ this.#settled = Promise.withResolvers();
1964
+ this.#terminate = new AbortController();
1965
+ this.#cleanup = new AbortController();
1966
+ this.#finish = new AbortController();
1967
+ this.#inputFailure = new AbortController();
1968
+ this.#child = spawn(plan.file, [...plan.arguments], {
1969
+ cwd: workspace,
1970
+ detached: process.platform !== "win32",
1971
+ env: environment,
1972
+ stdio: [
1973
+ "pipe",
1974
+ "pipe",
1975
+ "pipe"
1976
+ ],
1977
+ windowsHide: true,
1978
+ windowsVerbatimArguments: plan.verbatim
1979
+ });
1980
+ this.#finish.signal.addEventListener("abort", () => {
1981
+ clearTimeout(this.#timeoutTimer);
1982
+ clearTimeout(this.#confirmTimer);
1983
+ this.#cleanup.abort();
1984
+ this.#settled.resolve(buildExecuteResult({
1985
+ command: line,
1986
+ stdout: Buffer.concat(this.#outChunks),
1987
+ stderr: Buffer.concat(this.#errChunks),
1988
+ code: this.#child.exitCode,
1989
+ signal: this.#child.signalCode,
1990
+ expired: this.#expired,
1991
+ aborted: this.#aborted,
1992
+ truncated: this.#outRetained > limit || this.#errRetained > limit,
1993
+ limit,
1994
+ ...this.#cause === void 0 ? {} : { cause: this.#cause }
1995
+ }));
1996
+ }, { once: true });
1997
+ this.#terminate.signal.addEventListener("abort", () => {
1998
+ if (this.#finish.signal.aborted) return;
1999
+ clearTimeout(this.#timeoutTimer);
2000
+ this.#cleanup.abort();
2001
+ stopChild(this.#child, grace, PROCESS_CONFIRMATION).then(() => {
2002
+ if (this.#finish.signal.aborted) return;
2003
+ this.#confirmTimer = setTimeout(() => this.#finish.abort(), PROCESS_CONFIRMATION);
2004
+ });
2005
+ }, { once: true });
2006
+ this.#inputFailure.signal.addEventListener("abort", () => {
2007
+ if (this.#finish.signal.aborted) return;
2008
+ this.#cause = this.#inputFailure.signal.reason;
2009
+ this.#terminate.abort();
2010
+ }, { once: true });
2011
+ this.#child.stdin.on("error", (error) => this.#inputFailure.abort(error));
2012
+ this.#child.stdout.on("data", (chunk) => {
2013
+ const captured = captureChunk(chunk, limit + 1 - this.#outRetained);
2014
+ if (captured === void 0) return;
2015
+ this.#outRetained += captured.byteLength;
2016
+ this.#outChunks.push(captured);
2017
+ });
2018
+ this.#child.stderr.on("data", (chunk) => {
2019
+ const captured = captureChunk(chunk, limit + 1 - this.#errRetained);
2020
+ if (captured === void 0) return;
2021
+ this.#errRetained += captured.byteLength;
2022
+ this.#errChunks.push(captured);
2023
+ });
2024
+ this.#child.once("spawn", () => {
2025
+ this.#spawned = true;
2026
+ });
2027
+ this.#child.once("error", (error) => {
2028
+ if (this.#spawned) return;
2029
+ this.#cause = error;
2030
+ this.#finish.abort();
2031
+ });
2032
+ this.#child.once("close", () => this.#finish.abort());
2033
+ if (text !== void 0) this.#child.stdin.write(text, (error) => {
2034
+ if (error !== void 0 && error !== null) this.#inputFailure.abort(error);
2035
+ });
2036
+ this.#child.stdin.end();
2037
+ if (timeout > 0) this.#timeoutTimer = setTimeout(() => {
2038
+ this.#expired = true;
2039
+ this.#terminate.abort();
2040
+ }, timeout);
2041
+ if (optionSignal !== void 0) {
2042
+ optionSignal.addEventListener("abort", () => {
2043
+ this.#aborted = true;
2044
+ this.#terminate.abort();
2045
+ }, {
2046
+ once: true,
2047
+ signal: this.#cleanup.signal
2048
+ });
2049
+ if (optionSignal.aborted) {
2050
+ this.#aborted = true;
2051
+ this.#terminate.abort();
2052
+ }
2053
+ }
2054
+ }
2055
+ get settled() {
2056
+ return this.#settled.promise;
2057
+ }
2058
+ read(result) {
2059
+ if (result.failed && this.#strict) {
2060
+ if (this.#cause !== void 0 && this.#cause === this.#inputFailure.signal.reason) throw new ProcessError(`Command '${result.command}' failed while writing standard input`, {
2061
+ code: "input",
2062
+ context: {
2063
+ command: result.command,
2064
+ code: result.code,
2065
+ signal: result.signal
2066
+ },
2067
+ cause: this.#inputFailure.signal.reason,
2068
+ result
2069
+ });
2070
+ throw createExecuteError(result, this.#cause);
2071
+ }
2072
+ return result;
2073
+ }
2074
+ };
2075
+ // src/server/execution/execute.ts
2076
+ /**
2077
+ * Runs one command to completion, buffering its output, and settles with the outcome.
2078
+ *
2079
+ * @remarks
2080
+ * The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
2081
+ * child is detached, which is what lets its whole process group be terminated. Standard output and
2082
+ * error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
2083
+ * whether either stream exceeded it. Each capture keeps one byte beyond `limit`, so the single
2084
+ * code-point-boundary trim in {@link buildExecuteResult} reads the first excluded byte and never
2085
+ * delivers a split multibyte sequence. A positive `timeout` and an aborting `signal` both terminate the
2086
+ * child through the same bounded stop, and only the earliest is recorded: a timeout reports
2087
+ * `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
2088
+ * a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
2089
+ * covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
2090
+ * rather than on process exit, so a descendant that inherited the child's stdio holds the run open
2091
+ * after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
2092
+ * the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
2093
+ * overrides `command.input`. A host fault while writing that input terminates the run by design and
2094
+ * marks its result failed; a strict rejection carries the host fault as its cause. This differs
2095
+ * from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
2096
+ * is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
2097
+ * {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
2098
+ * option or command string rejects before the child is spawned, because an async function cannot
2099
+ * throw synchronously.
2100
+ *
2101
+ * @param command - The executable, arguments, and optional environment and input
2102
+ * @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
2103
+ * @returns The settled run outcome
2104
+ * @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`
2105
+ *
2106
+ * @example
2107
+ * ```ts
2108
+ * const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
2109
+ * ```
2110
+ */
2111
+ async function execute(command, options) {
2112
+ const execution = new Execution(command, options);
2113
+ const result = await execution.settled;
2114
+ return execution.read(result);
2115
+ }
2116
+ // src/server/execution/executeSync.ts
2117
+ /**
2118
+ * Runs one command to completion synchronously, buffering its output, and returns the outcome.
2119
+ *
2120
+ * @remarks
2121
+ * The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
2122
+ * shell. The host offers no cooperative termination window and no in-flight cancellation, so this
2123
+ * contract carries neither. A positive `timeout` ends only the root process and can leave
2124
+ * descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
2125
+ * timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
2126
+ * and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
2127
+ * reports `truncated` without failing. The environment and input follow the same merge as
2128
+ * {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
2129
+ * carrying the {@link ExecuteResult}.
2130
+ *
2131
+ * @param command - The executable, arguments, and optional environment and input
2132
+ * @param options - Working directory, timeout, capture limit, and failure delivery
2133
+ * @returns The run outcome
2134
+ * @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`
2135
+ *
2136
+ * @example
2137
+ * ```ts
2138
+ * const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
2139
+ * ```
2140
+ */
2141
+ function executeSync(command, options) {
2142
+ const snapshot = snapshotCommand(command);
2143
+ const optionEnvironment = options?.environment;
2144
+ const optionWorkspace = options?.workspace;
2145
+ const optionInput = options?.input;
2146
+ const optionTimeout = options?.timeout;
2147
+ const optionStrict = options?.strict;
2148
+ const optionLimit = options?.limit;
2149
+ validateCommand(snapshot);
2150
+ validateEnvironment(optionEnvironment);
2151
+ validateWorkspace(optionWorkspace);
2152
+ validateTimer(optionTimeout, "option 'timeout'");
2153
+ validateBytes(optionLimit, "option 'limit'", 0);
2154
+ const limit = optionLimit ?? PROCESS_OUTPUT;
2155
+ const timeout = optionTimeout ?? 0;
2156
+ const strict = optionStrict ?? true;
2157
+ const text = optionInput ?? snapshot.input;
2158
+ const line = formatCommand(snapshot);
2159
+ const workspace = optionWorkspace ?? process.cwd();
2160
+ const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
2161
+ const plan = buildSpawn(snapshot, {
2162
+ workspace,
2163
+ environment
2164
+ });
2165
+ const outcome = spawnSync(plan.file, [...plan.arguments], {
2166
+ cwd: workspace,
2167
+ env: environment,
2168
+ encoding: "buffer",
2169
+ maxBuffer: limit,
2170
+ killSignal: "SIGKILL",
2171
+ windowsHide: true,
2172
+ windowsVerbatimArguments: plan.verbatim,
2173
+ ...text !== void 0 ? { input: Buffer.from(text) } : {},
2174
+ ...timeout > 0 ? { timeout } : {}
2175
+ });
2176
+ const error = outcome.error;
2177
+ const fault = error !== void 0 && "code" in error && isString(error.code) ? error.code : "";
2178
+ const result = buildExecuteResult({
2179
+ command: line,
2180
+ stdout: Buffer.isBuffer(outcome.stdout) ? outcome.stdout : Buffer.alloc(0),
2181
+ stderr: Buffer.isBuffer(outcome.stderr) ? outcome.stderr : Buffer.alloc(0),
2182
+ code: outcome.status,
2183
+ signal: outcome.signal,
2184
+ expired: fault === "ETIMEDOUT",
2185
+ aborted: false,
2186
+ truncated: fault === "ENOBUFS",
2187
+ limit,
2188
+ ...error === void 0 ? {} : { cause: error }
2189
+ });
2190
+ if (result.failed && strict) throw createExecuteError(result, error);
2191
+ return result;
2192
+ }
2193
+ export { Process, ProcessManager, Session, Supervisor, buildExecutableCandidates, buildExecuteResult, buildPlatformSpawn, buildSpawn, captureChunk, createProcess, createProcessManager, createSession, detach, execute, executeSync, formatCommand, inspectProcess, isExited, isFile, killProcess, killTree, mergeEnvironment, mergePlatformEnvironment, quoteArgument, readPlatformVariable, readVariable, resolveExecutable, snapshotCommand, stopChild, trimHead, trimTail, validateBytes, validateCommand, validateEnvironment, validateText, validateTimer, validateWorkspace, waitForClose, waitForExit };
2164
2194
 
2165
2195
  //# sourceMappingURL=index.js.map