@deepseek-ai/dsh-subprocess-local 0.1.2-alpha.5 → 0.1.3-alpha.2

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.
@@ -0,0 +1,1623 @@
1
+ import { accessSync, chmodSync, closeSync, constants, existsSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
2
+ import { basename, dirname, extname, isAbsolute, join } from "node:path";
3
+ import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
4
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
5
+ import { randomBytes } from "node:crypto";
6
+ import { tmpdir } from "node:os";
7
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
+ import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
9
+ import koffi from "koffi";
10
+ import { getSystemErrorMessage, getSystemErrorName, inspect } from "node:util";
11
+ import { fileURLToPath } from "node:url";
12
+ //#region lib/types/managed-owner.js
13
+ /** Minimal managed-range ownership bound to one ordinary subprocess handle. */
14
+ /**
15
+ * Apply an optional abort bound to one shared wait promise.
16
+ * @param pending - managed-range wait shared by all callers.
17
+ * @param signal - optional caller cancellation signal.
18
+ * @returns whether the managed-range wait completed before cancellation.
19
+ */
20
+ async function waitWithAbort(pending, signal) {
21
+ if (signal?.aborted) {
22
+ pending.catch(() => {});
23
+ return false;
24
+ }
25
+ if (signal === void 0) {
26
+ await pending;
27
+ return true;
28
+ }
29
+ const aborted = Promise.withResolvers();
30
+ const onAbort = () => {
31
+ aborted.resolve(false);
32
+ };
33
+ signal.addEventListener("abort", onAbort, { once: true });
34
+ try {
35
+ return await Promise.race([pending.then(() => true), aborted.promise]);
36
+ } finally {
37
+ signal.removeEventListener("abort", onAbort);
38
+ }
39
+ }
40
+ //#endregion
41
+ //#region lib/types/windows-inspector.js
42
+ /**
43
+ * Windows process-table operations for terminal readiness, signalling, and
44
+ * teardown: Toolhelp32 snapshot enumeration with GetProcessTimes creation-time
45
+ * identity and process-handle wait-state liveness, the shell pid as a pseudo
46
+ * process group (Windows has no POSIX groups), and taskkill tree signalling.
47
+ * The koffi bindings load lazily so
48
+ * non-Windows processes never touch Win32 libraries; all decision logic takes
49
+ * an injectable internals boundary so suites can pin it on any host.
50
+ * @module dsh-subprocess-local/windows-inspector
51
+ */
52
+ /**
53
+ * Walk a process table from one root in children-first order, retaining only
54
+ * members whose start identity is readable (unreadable members are detector
55
+ * misses, exactly like an unreadable `/proc` entry on Linux).
56
+ * @param entries - the process table snapshot.
57
+ * @param rootPid - the tree root to descend from.
58
+ * @param started - creation-time identity resolver for one member.
59
+ * @returns the root and its current transitive descendants, children first.
60
+ */
61
+ function windowsProcessTree(entries, rootPid, started) {
62
+ const root = new Map(entries.map((entry) => [entry.pid, entry])).get(rootPid);
63
+ if (root === void 0) return [];
64
+ const byParent = /* @__PURE__ */ new Map();
65
+ for (const entry of entries) {
66
+ const children = byParent.get(entry.parentPid) ?? [];
67
+ children.push(entry);
68
+ byParent.set(entry.parentPid, children);
69
+ }
70
+ const visited = /* @__PURE__ */ new Set();
71
+ const result = [];
72
+ const visit = (entry) => {
73
+ if (visited.has(entry.pid)) return;
74
+ visited.add(entry.pid);
75
+ for (const child of byParent.get(entry.pid) ?? []) visit(child);
76
+ const identity = started(entry.pid);
77
+ if (identity !== void 0) result.push({
78
+ pid: entry.pid,
79
+ started: identity
80
+ });
81
+ };
82
+ visit(root);
83
+ return result;
84
+ }
85
+ /**
86
+ * Windows {@link ProcessInspector}. The shell pid stands in for a foreground
87
+ * process group: it is a stable pseudo-group that lets the prompt-marker
88
+ * readiness path compare foreground identities, while every actual signal
89
+ * targets the console-wide tree through taskkill (SIGINT is delivered by the
90
+ * terminal handle as a `\x03` input write and never reaches this layer).
91
+ */
92
+ var WindowsProcessInspector = class {
93
+ internals;
94
+ constructor(internals = defaultWindowsProcessInternals()) {
95
+ this.internals = internals;
96
+ }
97
+ foregroundPgid(shellPid) {
98
+ return shellPid;
99
+ }
100
+ isStdinWaiting(_pgid, _shellPid) {
101
+ return false;
102
+ }
103
+ isAlive(identity) {
104
+ const state = this.internals.processState(identity.pid);
105
+ return state?.active === true && state.started === identity.started;
106
+ }
107
+ snapshot() {
108
+ let entries;
109
+ return {
110
+ tree: (rootPid) => windowsProcessTree(entries ??= this.internals.snapshot(), rootPid, (pid) => this.internals.processState(pid)?.started),
111
+ session: () => [],
112
+ alive: (identity) => this.isAlive(identity)
113
+ };
114
+ }
115
+ signalGroup(pgid, signal) {
116
+ this.internals.taskkill(pgid, signal === "SIGKILL");
117
+ }
118
+ signalProcess(identity, signal) {
119
+ if (this.isAlive(identity)) this.internals.taskkill(identity.pid, signal === "SIGKILL");
120
+ }
121
+ };
122
+ /**
123
+ * Create the Windows process inspector.
124
+ * @param internals - injectable process operations; defaults to the koffi-backed table.
125
+ * @returns the Windows inspector.
126
+ */
127
+ function createWindowsProcessInspector(internals = defaultWindowsProcessInternals()) {
128
+ return new WindowsProcessInspector(internals);
129
+ }
130
+ /** Terminate one Windows process tree with taskkill, contained like POSIX group signalling. */
131
+ function taskkillTree(pid, force) {
132
+ if (pid <= 0) return;
133
+ spawnSync("taskkill", [
134
+ "/PID",
135
+ String(pid),
136
+ "/T",
137
+ ...force ? ["/F"] : []
138
+ ], {
139
+ stdio: "ignore",
140
+ windowsHide: true
141
+ });
142
+ }
143
+ /**
144
+ * True for NULL and INVALID_HANDLE_VALUE returns from Win32 handle APIs.
145
+ * @param value - a handle as koffi may hand it back (pointer, null, or 0n).
146
+ * @returns whether the value signals an invalid handle.
147
+ */
148
+ function isInvalidHandle(value) {
149
+ if (value === null || value === void 0) return true;
150
+ const asBigInt = value;
151
+ return asBigInt === 0n || asBigInt === 18446744073709551615n || asBigInt === -1n;
152
+ }
153
+ const PVOID = koffi.pointer("void");
154
+ /**
155
+ * Resolve the koffi Win32 struct types once. Registration is lazy and cached
156
+ * because koffi's type registry is global per process: test runners that
157
+ * re-evaluate this module (a hoisted `vi.mock` re-imports the graph) must not
158
+ * re-register the names.
159
+ */
160
+ function win32Structs() {
161
+ if (cachedStructs !== void 0) return cachedStructs;
162
+ const PROCESSENTRY32W = koffi.struct("PROCESSENTRY32W", {
163
+ dwSize: "uint32",
164
+ cntUsage: "uint32",
165
+ th32ProcessID: "uint32",
166
+ th32DefaultHeapID: PVOID,
167
+ th32ModuleID: "uint32",
168
+ cCntThreads: "uint32",
169
+ th32ParentProcessID: "uint32",
170
+ pcPriClassBase: "int32",
171
+ dwFlags: "uint32",
172
+ szExeFile: koffi.array("char16", 260)
173
+ });
174
+ const FILETIME = koffi.struct("FILETIME", {
175
+ dwLowDateTime: "uint32",
176
+ dwHighDateTime: "uint32"
177
+ });
178
+ /* v8 ignore start -- a layout-mismatch guard fires only on ABI breakage; the windows-native suites exercise the real struct. */
179
+ if (PROCESSENTRY32W.size !== 568) throw new Error(`PROCESSENTRY32W layout mismatch: koffi computed ${PROCESSENTRY32W.size}, Windows headers say 568`);
180
+ /* v8 ignore stop */
181
+ cachedStructs = {
182
+ PROCESSENTRY32W,
183
+ FILETIME
184
+ };
185
+ return cachedStructs;
186
+ }
187
+ let cachedStructs;
188
+ const TH32CS_SNAPPROCESS = 2;
189
+ const WAIT_OBJECT_0 = 0;
190
+ const WAIT_TIMEOUT = 258;
191
+ let cachedBindings;
192
+ /**
193
+ * Resolve the lazy Win32 bindings (throws the first binding failure, fail-closed).
194
+ * @returns the cached binding table.
195
+ */
196
+ function win32Bindings() {
197
+ if (cachedBindings !== void 0) return cachedBindings;
198
+ const { PROCESSENTRY32W, FILETIME } = win32Structs();
199
+ const kernel32 = koffi.load("kernel32.dll");
200
+ const bind = (name, result, args) => kernel32.func("__stdcall", name, result, args);
201
+ cachedBindings = {
202
+ createToolhelp32Snapshot: bind("CreateToolhelp32Snapshot", PVOID, ["uint32", "uint32"]),
203
+ process32FirstW: bind("Process32FirstW", "int", [PVOID, koffi.pointer(PROCESSENTRY32W)]),
204
+ process32NextW: bind("Process32NextW", "int", [PVOID, koffi.pointer(PROCESSENTRY32W)]),
205
+ openProcess: bind("OpenProcess", PVOID, [
206
+ "uint32",
207
+ "int",
208
+ "uint32"
209
+ ]),
210
+ getProcessTimes: bind("GetProcessTimes", "int", [
211
+ PVOID,
212
+ koffi.pointer(FILETIME),
213
+ koffi.pointer(FILETIME),
214
+ koffi.pointer(FILETIME),
215
+ koffi.pointer(FILETIME)
216
+ ]),
217
+ waitForSingleObject: bind("WaitForSingleObject", "uint32", [PVOID, "uint32"]),
218
+ closeHandle: bind("CloseHandle", "int", [PVOID])
219
+ };
220
+ return cachedBindings;
221
+ }
222
+ /**
223
+ * Allocate koffi memory as a branded {@link NativePtr}; koffi's TS types are
224
+ * `any`, so the cast goes through `unknown` to keep the unsafe surface here.
225
+ * @param type - the koffi type to allocate.
226
+ * @param count - element count.
227
+ * @returns the branded allocation pointer.
228
+ */
229
+ function allocNative(type, count) {
230
+ return koffi.alloc(type, count);
231
+ }
232
+ /** Enumerate the current process table through Toolhelp32. */
233
+ function snapshotWindowsProcesses(bindings) {
234
+ const { PROCESSENTRY32W } = win32Structs();
235
+ const snapshot = bindings.createToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
236
+ /* v8 ignore next -- an invalid snapshot for the process flag is not producible through the public API;
237
+ the guard mirrors POSIX's unreadable-proc tolerance and isInvalidHandle is unit-tested. */
238
+ if (isInvalidHandle(snapshot)) return [];
239
+ const entries = [];
240
+ try {
241
+ const entry = allocNative(PROCESSENTRY32W, 1);
242
+ koffi.encode(entry, "uint32", PROCESSENTRY32W.size);
243
+ let ok = bindings.process32FirstW(snapshot, entry);
244
+ while (ok !== 0) {
245
+ const record = koffi.decode(entry, PROCESSENTRY32W);
246
+ entries.push({
247
+ pid: record.th32ProcessID,
248
+ parentPid: record.th32ParentProcessID
249
+ });
250
+ ok = bindings.process32NextW(snapshot, entry);
251
+ }
252
+ } finally {
253
+ bindings.closeHandle(snapshot);
254
+ }
255
+ return entries;
256
+ }
257
+ /** Read one process's creation identity and current wait state. */
258
+ function windowsProcessState(bindings, pid) {
259
+ const { FILETIME } = win32Structs();
260
+ const handle = bindings.openProcess(1052672, 0, pid);
261
+ if (isInvalidHandle(handle)) return void 0;
262
+ try {
263
+ const creation = allocNative(FILETIME, 1);
264
+ const exit = allocNative(FILETIME, 1);
265
+ const kernel = allocNative(FILETIME, 1);
266
+ const user = allocNative(FILETIME, 1);
267
+ /* v8 ignore next -- a GetProcessTimes failure after a successful open races process exit and
268
+ cannot be staged deterministically; the absent-process path is covered and the caller
269
+ treats undefined as a detector miss. */
270
+ if (bindings.getProcessTimes(handle, creation, exit, kernel, user) === 0) return void 0;
271
+ const record = koffi.decode(creation, FILETIME);
272
+ const wait = bindings.waitForSingleObject(handle, 0);
273
+ /* v8 ignore next -- an opened process handle has exactly one of these two
274
+ zero-time wait states; an unexpected Win32 failure is an unreadable process. */
275
+ if (wait !== WAIT_OBJECT_0 && wait !== WAIT_TIMEOUT) return void 0;
276
+ return {
277
+ started: `${record.dwHighDateTime}:${record.dwLowDateTime}`,
278
+ active: wait === WAIT_TIMEOUT
279
+ };
280
+ } finally {
281
+ bindings.closeHandle(handle);
282
+ }
283
+ }
284
+ /** The koffi-backed default internals; bindings resolve lazily on first use. */
285
+ function defaultWindowsProcessInternals() {
286
+ return {
287
+ snapshot: () => snapshotWindowsProcesses(win32Bindings()),
288
+ processState: (pid) => windowsProcessState(win32Bindings(), pid),
289
+ taskkill: taskkillTree
290
+ };
291
+ }
292
+ //#endregion
293
+ //#region lib/types/process-inspector.js
294
+ /** Platform process-table inspection for terminal readiness, signals, and teardown. */
295
+ /* v8 ignore start -- thin OS bindings; injected logic is unit-tested and real platform composition exercises them. */
296
+ const DEFAULT_INTERNALS = {
297
+ readFile: (path) => readFileSync(path, "utf8"),
298
+ readDir: (path) => readdirSync(path),
299
+ readLink: (path) => readlinkSync(path, "utf8"),
300
+ stat: (path) => statSync(path),
301
+ open: (path) => openSync(path, "r"),
302
+ read: (fd, buffer, length, position) => readSync(fd, buffer, 0, length, position),
303
+ close: closeSync,
304
+ exec: (file, args) => execFileSync(file, args, { encoding: "utf8" }),
305
+ kill: (pid, signal) => process.kill(pid, signal)
306
+ };
307
+ /**
308
+ * Parse fields used from Linux `/proc/<pid>/stat`, including parenthesized comm text.
309
+ * @param text - complete stat line.
310
+ * @returns Parsed identity/group fields, or undefined for malformed input.
311
+ */
312
+ function parseProcStat(text) {
313
+ const open = text.indexOf("(");
314
+ const close = text.lastIndexOf(")");
315
+ if (open <= 0 || close <= open) return void 0;
316
+ const pid = Number(text.slice(0, open).trim());
317
+ const rest = text.slice(close + 2).trim().split(/\s+/);
318
+ const state = rest[0] || "";
319
+ const parentPid = Number(rest[1]);
320
+ const pgrp = Number(rest[2]);
321
+ const session = Number(rest[3]);
322
+ const ttyDevice = Number(rest[4]);
323
+ const tpgid = Number(rest[5]);
324
+ const started = rest[19];
325
+ if (![
326
+ pid,
327
+ parentPid,
328
+ pgrp,
329
+ session,
330
+ ttyDevice,
331
+ tpgid
332
+ ].every(Number.isSafeInteger) || state.length !== 1 || started === void 0) return void 0;
333
+ return {
334
+ pid,
335
+ parentPid,
336
+ pgrp,
337
+ session,
338
+ state,
339
+ ttyDevice,
340
+ tpgid,
341
+ started
342
+ };
343
+ }
344
+ function readLinuxStat(internals, pid) {
345
+ try {
346
+ return parseProcStat(internals.readFile(`/proc/${pid}/stat`));
347
+ } catch (_unreadableProcEntry) {
348
+ return;
349
+ }
350
+ }
351
+ function linuxDeviceNumber(value) {
352
+ return value >>> 0;
353
+ }
354
+ function readLinuxTerminalDevice(internals, pid, ttyDevice, tid) {
355
+ const terminalDevice = linuxDeviceNumber(ttyDevice);
356
+ if (terminalDevice === 0) return void 0;
357
+ const path = tid === void 0 ? `/proc/${pid}/fd/0` : `/proc/${pid}/task/${tid}/fd/0`;
358
+ try {
359
+ if (internals.readLink(path) === "/dev/tty") return terminalDevice;
360
+ const status = internals.stat(path);
361
+ return status.isCharacterDevice() && linuxDeviceNumber(status.rdev) === terminalDevice ? terminalDevice : void 0;
362
+ } catch (_unreadableStdinDevice) {
363
+ return;
364
+ }
365
+ }
366
+ /**
367
+ * Report whether a Linux process group has an executing member. `false`
368
+ * means the group contains only zombie/dead entries; `undefined` means the
369
+ * process table could not prove either outcome.
370
+ * @param processGroupId - POSIX process-group id to inspect.
371
+ * @param internals - injectable process-table operations.
372
+ * @returns Live-member presence, or `undefined` when unavailable/absent.
373
+ */
374
+ function linuxProcessGroupHasLiveMembers(processGroupId, internals = DEFAULT_INTERNALS) {
375
+ let entries;
376
+ try {
377
+ entries = internals.readDir("/proc");
378
+ } catch (_unreadableProcDirectory) {
379
+ return;
380
+ }
381
+ let matched = false;
382
+ for (const entry of entries) {
383
+ if (!/^\d+$/.test(entry)) continue;
384
+ const stat = readLinuxStat(internals, Number(entry));
385
+ if (stat?.pgrp !== processGroupId) continue;
386
+ matched = true;
387
+ if (!/^[ZXx]$/.test(stat.state)) return true;
388
+ }
389
+ return matched ? false : void 0;
390
+ }
391
+ function numericEntries(internals, path) {
392
+ try {
393
+ return internals.readDir(path).filter((entry) => /^\d+$/.test(entry)).map(Number);
394
+ } catch (_unreadableProcDirectory) {
395
+ return [];
396
+ }
397
+ }
398
+ function readSyscall(internals, pid, tid) {
399
+ try {
400
+ const text = internals.readFile(`/proc/${pid}/task/${tid}/syscall`).trim();
401
+ if (text === "running" || text.startsWith("-1 ")) return void 0;
402
+ const fields = text.split(/\s+/);
403
+ const number = Number(fields[0]);
404
+ const args = fields.slice(1, 7).map((field) => Number.parseInt(field, 16));
405
+ if (!Number.isSafeInteger(number) || args.some((value) => !Number.isSafeInteger(value))) return void 0;
406
+ return {
407
+ number,
408
+ args
409
+ };
410
+ } catch (_unreadableSyscall) {
411
+ return;
412
+ }
413
+ }
414
+ function readMemory(internals, pid, address, length) {
415
+ let fd;
416
+ try {
417
+ fd = internals.open(`/proc/${pid}/mem`);
418
+ const buffer = Buffer.alloc(length);
419
+ const count = internals.read(fd, buffer, length, address);
420
+ return buffer.subarray(0, count);
421
+ } catch (_unreadableProcessMemory) {
422
+ return;
423
+ } finally {
424
+ if (fd !== void 0) internals.close(fd);
425
+ }
426
+ }
427
+ function fdSetHasStdin(internals, pid, address) {
428
+ return address !== 0 && (readMemory(internals, pid, address, 8)?.[0] ?? 0) % 2 === 1;
429
+ }
430
+ function pollHasStdin(internals, pid, address, count) {
431
+ if (address === 0 || count <= 0) return false;
432
+ const memory = readMemory(internals, pid, address, Math.min(count, 1024) * 8);
433
+ if (memory === void 0) return false;
434
+ for (let offset = 0; offset + 8 <= memory.length; offset += 8) if (memory.readInt32LE(offset) === 0 && (memory.readInt16LE(offset + 4) & 1) !== 0) return true;
435
+ return false;
436
+ }
437
+ function epollHasStdin(internals, pid, tid, epfd) {
438
+ try {
439
+ return internals.readFile(`/proc/${pid}/task/${tid}/fdinfo/${epfd}`).split("\n").some((line) => /^tfd:\s+0\b/.test(line.trim()));
440
+ } catch (_unreadableFdInfo) {
441
+ return false;
442
+ }
443
+ }
444
+ const SYSCALLS = {
445
+ x64: {
446
+ read: 0,
447
+ select: 23,
448
+ pselect: 270,
449
+ poll: 7,
450
+ ppoll: 271,
451
+ epollWait: 232,
452
+ epollPwait: 281
453
+ },
454
+ arm64: {
455
+ read: 63,
456
+ pselect: 72,
457
+ ppoll: 73,
458
+ epollPwait: 22
459
+ }
460
+ };
461
+ const SUPPORTED_SYSCALL_TABLES = Object.values(SYSCALLS);
462
+ function linuxSyscallTables(arch) {
463
+ const primary = SYSCALLS[arch];
464
+ if (primary === void 0) return void 0;
465
+ return [primary, ...SUPPORTED_SYSCALL_TABLES.filter((table) => table !== primary)];
466
+ }
467
+ function syscallWaitsOnStdin(internals, pid, tid, syscall, tables) {
468
+ const [a0 = 0, a1 = 0, a2 = 0] = syscall.args;
469
+ for (const table of tables) {
470
+ if (syscall.number === table.read) return a0 === 0;
471
+ if (syscall.number === table.select || syscall.number === table.pselect) return a0 >= 1 && fdSetHasStdin(internals, pid, a1);
472
+ if (syscall.number === table.poll || syscall.number === table.ppoll) return a1 >= 1 && pollHasStdin(internals, pid, a0, a1);
473
+ if (syscall.number === table.epollWait || syscall.number === table.epollPwait) return a2 >= 1 && epollHasStdin(internals, pid, tid, a0);
474
+ }
475
+ return false;
476
+ }
477
+ var PosixProcessInspector = class {
478
+ internals;
479
+ constructor(internals) {
480
+ this.internals = internals;
481
+ }
482
+ signalGroup(pgid, signal) {
483
+ this.internals.kill(-pgid, signal);
484
+ }
485
+ signalProcess(identity, signal) {
486
+ if (this.isAlive(identity)) this.internals.kill(identity.pid, signal);
487
+ }
488
+ };
489
+ function quiescent(state) {
490
+ return state !== void 0 && /^[ZXx]$/.test(state);
491
+ }
492
+ var PosixProcessSnapshot = class {
493
+ rows;
494
+ byPid;
495
+ constructor(rows) {
496
+ this.rows = rows;
497
+ this.byPid = new Map(rows.map((row) => [row.pid, row]));
498
+ }
499
+ tree(rootPid) {
500
+ return processTree(this.rows, rootPid);
501
+ }
502
+ session(sessionId) {
503
+ return this.rows.flatMap((row) => row.session === sessionId ? [{
504
+ pid: row.pid,
505
+ started: row.started
506
+ }] : []);
507
+ }
508
+ alive(identity) {
509
+ const row = this.byPid.get(identity.pid);
510
+ return row?.started === identity.started && !quiescent(row.state);
511
+ }
512
+ };
513
+ function processTree(entries, rootPid) {
514
+ const root = new Map(entries.map((entry) => [entry.pid, entry])).get(rootPid);
515
+ if (root === void 0) return [];
516
+ const byParent = /* @__PURE__ */ new Map();
517
+ for (const entry of entries) {
518
+ const children = byParent.get(entry.parentPid) ?? [];
519
+ children.push(entry);
520
+ byParent.set(entry.parentPid, children);
521
+ }
522
+ const visited = /* @__PURE__ */ new Set();
523
+ const result = [];
524
+ const visit = (entry) => {
525
+ if (visited.has(entry.pid)) return;
526
+ visited.add(entry.pid);
527
+ for (const child of byParent.get(entry.pid) ?? []) visit(child);
528
+ result.push({
529
+ pid: entry.pid,
530
+ started: entry.started
531
+ });
532
+ };
533
+ visit(root);
534
+ return result;
535
+ }
536
+ var LinuxProcessInspector = class extends PosixProcessInspector {
537
+ arch;
538
+ constructor(arch, internals) {
539
+ super(internals);
540
+ this.arch = arch;
541
+ }
542
+ foregroundPgid(shellPid) {
543
+ const tpgid = readLinuxStat(this.internals, shellPid)?.tpgid;
544
+ return tpgid !== void 0 && tpgid > 0 ? tpgid : void 0;
545
+ }
546
+ isStdinWaiting(pgid, shellPid) {
547
+ const tables = linuxSyscallTables(this.arch);
548
+ if (tables === void 0) return false;
549
+ const shell = readLinuxStat(this.internals, shellPid);
550
+ if (shell === void 0) return false;
551
+ const terminalDevice = readLinuxTerminalDevice(this.internals, shellPid, shell.ttyDevice);
552
+ if (terminalDevice === void 0) return false;
553
+ for (const pid of numericEntries(this.internals, "/proc")) {
554
+ const process = readLinuxStat(this.internals, pid);
555
+ if (process?.pgrp !== pgid) continue;
556
+ for (const tid of numericEntries(this.internals, `/proc/${pid}/task`)) {
557
+ const syscall = readSyscall(this.internals, pid, tid);
558
+ if (syscall !== void 0 && syscallWaitsOnStdin(this.internals, pid, tid, syscall, tables) && readLinuxTerminalDevice(this.internals, pid, process.ttyDevice, tid) === terminalDevice) return true;
559
+ }
560
+ }
561
+ return false;
562
+ }
563
+ isAlive(identity) {
564
+ const stat = readLinuxStat(this.internals, identity.pid);
565
+ return stat?.started === identity.started && !quiescent(stat.state);
566
+ }
567
+ snapshot() {
568
+ return new PosixProcessSnapshot(numericEntries(this.internals, "/proc").flatMap((pid) => {
569
+ const stat = readLinuxStat(this.internals, pid);
570
+ return stat === void 0 ? [] : [{
571
+ pid,
572
+ parentPid: stat.parentPid,
573
+ started: stat.started,
574
+ session: stat.session,
575
+ state: stat.state
576
+ }];
577
+ }));
578
+ }
579
+ };
580
+ function macProcessTable(internals) {
581
+ return internals.exec("/bin/ps", ["-axo", "pid=,ppid=,lstart="]).split("\n").flatMap((line) => {
582
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
583
+ if (match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0) return [];
584
+ return [{
585
+ pid: Number(match[1]),
586
+ parentPid: Number(match[2]),
587
+ started: match[3],
588
+ session: void 0,
589
+ state: void 0
590
+ }];
591
+ });
592
+ }
593
+ var MacProcessInspector = class extends PosixProcessInspector {
594
+ foregroundPgid(shellPid) {
595
+ try {
596
+ const value = Number(this.internals.exec("/bin/ps", [
597
+ "-o",
598
+ "tpgid=",
599
+ "-p",
600
+ String(shellPid)
601
+ ]).trim());
602
+ return Number.isSafeInteger(value) && value > 0 ? value : void 0;
603
+ } catch (_missingProcess) {
604
+ return;
605
+ }
606
+ }
607
+ isStdinWaiting(_pgid, _shellPid) {
608
+ return false;
609
+ }
610
+ isAlive(identity) {
611
+ return macProcessTable(this.internals).some((entry) => entry.pid === identity.pid && entry.started === identity.started);
612
+ }
613
+ snapshot() {
614
+ return new PosixProcessSnapshot(macProcessTable(this.internals));
615
+ }
616
+ };
617
+ /**
618
+ * Create the supported platform inspector or fail at plugin load.
619
+ * @param platform - target Node platform.
620
+ * @param arch - target CPU architecture for Linux syscall numbers.
621
+ * @param internals - filesystem/process boundary, injectable for deterministic tests.
622
+ * @returns Platform process inspector.
623
+ */
624
+ function createProcessInspector(platform = process.platform, arch = process.arch, internals = DEFAULT_INTERNALS) {
625
+ if (platform === "linux") return new LinuxProcessInspector(arch, internals);
626
+ if (platform === "darwin") return new MacProcessInspector(internals);
627
+ if (platform === "win32") return createWindowsProcessInspector();
628
+ throw new Error(`subprocess-local: terminal inspection is unsupported on platform ${platform}`);
629
+ }
630
+ //#endregion
631
+ //#region lib/types/spawn.js
632
+ /**
633
+ * Process plumbing for the local subprocess service: ordinary process launch
634
+ * with per-stream stdio dispositions, tail-keep collection with spill
635
+ * files, provider-owned range signalling, and common termination scheduling.
636
+ * POSIX owners stage TERM before KILL; Windows owners terminate immediately.
637
+ * This layer reacts to an abort signal; callers own deadlines, teardown
638
+ * ladders, and cause classification.
639
+ * @module dsh-subprocess-local/spawn
640
+ */
641
+ /**
642
+ * Build a child environment: explicit caller entries override the scrubbed
643
+ * parent base using the target platform's environment-key semantics. A string
644
+ * deliberately restores or overrides an entry; an explicit `undefined`
645
+ * tombstone removes an ordinary ambient entry.
646
+ * @param extra - explicit caller entries and tombstones, merged after the scrub.
647
+ * @returns the environment to hand to `spawn` for the child process.
648
+ */
649
+ function childEnv(extra) {
650
+ const env = scrubbedParentEnv();
651
+ if (process.platform !== "win32") return {
652
+ ...env,
653
+ ...extra
654
+ };
655
+ let entries = Object.entries(env);
656
+ for (const [key, value] of Object.entries(extra ?? {})) {
657
+ const normalized = key.toUpperCase();
658
+ entries = entries.filter(([inherited]) => inherited.toUpperCase() !== normalized);
659
+ entries.push([key, value]);
660
+ }
661
+ return Object.fromEntries(entries);
662
+ }
663
+ /**
664
+ * Liveness-poll cadence for tree-exit waits. The timer stays ref'd: an
665
+ * awaited teardown must keep the event loop alive until the tree really
666
+ * exits, or the parent can exit while claiming quiescence and orphan the
667
+ * survivors it promised to reap.
668
+ */
669
+ function sleepTick() {
670
+ return setTimeout$1(15);
671
+ }
672
+ let spillCounter = 0;
673
+ let defaultSpillDir;
674
+ /**
675
+ * The default spill location: a private (0700) per-process directory under
676
+ * the OS tmpdir, created lazily. Predictable world-readable paths would let
677
+ * other local users read command output or pre-create symlinks. At a
678
+ * JavaScript-observable process exit the directory is removed only when it
679
+ * holds no completed spill file (spill files are retained as full-output
680
+ * recovery artifacts until an external cleanup).
681
+ */
682
+ function privateSpillDir() {
683
+ defaultSpillDir ??= mkdtempSync(join(tmpdir(), "dsh-subprocess-"));
684
+ return defaultSpillDir;
685
+ }
686
+ /* v8 ignore next 4 -- exit listeners run after the coverage dump; removal is verified by the CI /tmp residue measurement. */
687
+ process.once("exit", () => {
688
+ if (defaultSpillDir === void 0) return;
689
+ try {
690
+ rmdirSync(defaultSpillDir);
691
+ } catch {}
692
+ });
693
+ /**
694
+ * Prepare fallible output storage before starting a managed native process.
695
+ * @param internals - optional caller-owned spill directory.
696
+ * @returns binding inputs whose spill directory is ready for use.
697
+ */
698
+ function prepareManagedProcessBinding(internals = {}) {
699
+ return { spillDir: internals.spillDir ?? privateSpillDir() };
700
+ }
701
+ /**
702
+ * Collects one stream with a bounded in-memory tail. With a spill cap, on
703
+ * first overflow a spill file is created and every chunk (including those
704
+ * already collected) is appended there while the full stream remains within
705
+ * the cap; without one, only the in-memory tail is ever retained (the
706
+ * diagnostic-tail shape — a language server's stderr).
707
+ *
708
+ * Tail-keep rationale (pi/OpenCode): errors and final results cluster at the
709
+ * end of command output; the spill file covers the head.
710
+ */
711
+ var OutputCollector = class {
712
+ maxBytes;
713
+ maxSpillBytes;
714
+ label;
715
+ spillDir;
716
+ chunks = [];
717
+ bytes = 0;
718
+ dropped = false;
719
+ spillFd;
720
+ spillFile;
721
+ spillDisabled;
722
+ /** Total bytes ever pushed (not just retained). */
723
+ total = 0;
724
+ constructor(maxBytes, maxSpillBytes, label, spillDir) {
725
+ this.maxBytes = maxBytes;
726
+ this.maxSpillBytes = maxSpillBytes;
727
+ this.label = label;
728
+ this.spillDir = spillDir;
729
+ this.spillDisabled = maxSpillBytes === void 0;
730
+ }
731
+ /**
732
+ * Ingest one stream chunk, counting it toward the whole-stream total. On
733
+ * first overflow of the in-memory cap a spill file is opened (when spilling
734
+ * is enabled) and every chunk (already-collected ones included) is appended
735
+ * there from then on; the in-memory tail then drops whole chunks from its
736
+ * head (or the head of a single over-cap chunk) until it fits the cap again.
737
+ * @param chunk - the raw bytes from one stream 'data' event.
738
+ */
739
+ push(chunk) {
740
+ this.total += chunk.length;
741
+ const overflows = this.bytes + chunk.length > this.maxBytes;
742
+ if (!this.spillDisabled && (overflows || this.spillFd !== void 0)) this.spillAll(chunk);
743
+ this.chunks.push(chunk);
744
+ this.bytes += chunk.length;
745
+ while (this.bytes > this.maxBytes) {
746
+ const head = this.chunks[0];
747
+ const excess = this.bytes - this.maxBytes;
748
+ if (head.length <= excess) {
749
+ this.chunks.shift();
750
+ this.bytes -= head.length;
751
+ } else {
752
+ this.chunks[0] = head.subarray(excess);
753
+ this.bytes -= excess;
754
+ }
755
+ this.dropped = true;
756
+ }
757
+ }
758
+ /** Open the spill file lazily and append `chunk` (and any prior chunks once). */
759
+ spillAll(chunk) {
760
+ if (this.maxSpillBytes !== void 0 && this.total > this.maxSpillBytes) {
761
+ this.discardSpill();
762
+ return;
763
+ }
764
+ if (this.spillFd === void 0) {
765
+ this.spillFile = join(this.spillDir, `dsh-subprocess-${process.pid}-${++spillCounter}-${randomBytes(6).toString("hex")}-${this.label}.log`);
766
+ this.spillFd = openSync(this.spillFile, "wx", 384);
767
+ for (const prior of this.chunks) writeSync(this.spillFd, prior);
768
+ }
769
+ writeSync(this.spillFd, chunk);
770
+ }
771
+ /** Stop spilling and remove the file once it can no longer hold the complete stream. */
772
+ discardSpill() {
773
+ const fd = this.spillFd;
774
+ const file = this.spillFile;
775
+ this.spillFd = void 0;
776
+ this.spillFile = void 0;
777
+ this.spillDisabled = true;
778
+ if (fd !== void 0) try {
779
+ closeSync(fd);
780
+ } catch {
781
+ this.spillFd = fd;
782
+ }
783
+ if (file !== void 0) try {
784
+ unlinkSync(file);
785
+ } catch {}
786
+ }
787
+ /**
788
+ * Incremental read in whole-stream byte coordinates: returns everything
789
+ * pushed since `fromByte`. When `fromByte` has already slid out of the
790
+ * in-memory tail window, the read is `lossy` — it returns the whole
791
+ * retained tail and the gap is only recoverable from the spill file.
792
+ * @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
793
+ * @returns the delta text, the offset for the next read, the `lossy` flag, and the spill path when one was created.
794
+ */
795
+ readFrom(fromByte) {
796
+ const windowStart = this.total - this.bytes;
797
+ const buffer = Buffer.concat(this.chunks);
798
+ const lossy = fromByte < windowStart;
799
+ return {
800
+ text: (lossy ? buffer : buffer.subarray(fromByte - windowStart)).toString("utf8"),
801
+ nextOffset: this.total,
802
+ lossy,
803
+ ...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
804
+ };
805
+ }
806
+ /**
807
+ * Close the spill file once the stream has ended. A failed close (delayed
808
+ * writeback fault) stops advertising the spill path — the file may be
809
+ * missing its tail — while every in-memory read keeps working. Idempotent;
810
+ * the spawn path seals both collectors at settlement so reads after exit
811
+ * never point at a still-open file.
812
+ */
813
+ seal() {
814
+ if (this.spillFd === void 0) return;
815
+ try {
816
+ closeSync(this.spillFd);
817
+ } catch {
818
+ this.spillFile = void 0;
819
+ }
820
+ this.spillFd = void 0;
821
+ }
822
+ /**
823
+ * Seal the spill file and return the final output.
824
+ * @returns the final collected output: tail text, truncation flag, and the spill path when intact.
825
+ */
826
+ finalize() {
827
+ this.seal();
828
+ return {
829
+ text: Buffer.concat(this.chunks).toString("utf8"),
830
+ truncated: this.dropped,
831
+ ...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
832
+ };
833
+ }
834
+ };
835
+ /**
836
+ * Terminate one Windows process tree with `taskkill /T /F`. Contained like
837
+ * POSIX group signalling — delivery races tree exit, so an absent tree, a
838
+ * nonzero status, or a missing taskkill binary must not break idempotent
839
+ * teardown.
840
+ * @param pid - root process id, when the spawn published one.
841
+ */
842
+ function taskkillProcessTree(pid) {
843
+ if (pid === void 0 || pid <= 0) return;
844
+ spawnSync("taskkill", [
845
+ "/PID",
846
+ String(pid),
847
+ "/T",
848
+ "/F"
849
+ ], {
850
+ stdio: "ignore",
851
+ windowsHide: true
852
+ });
853
+ }
854
+ /**
855
+ * Signal a detached process tree with platform-correct semantics: POSIX
856
+ * signals the negative process-group id and falls back to the direct child
857
+ * when the group is gone; Windows terminates the tree via taskkill (any
858
+ * signal value force-terminates — Node maps signals to TerminateProcess).
859
+ */
860
+ function signalTree(platform, pid, sig, child, taskkill) {
861
+ /* v8 ignore next -- kill/terminate gate on treeAlive(), which is false without a pid; this guard protects direct callers only. */
862
+ if (pid === void 0) return;
863
+ if (platform === "win32") {
864
+ taskkill(pid);
865
+ return;
866
+ }
867
+ try {
868
+ process.kill(-pid, sig);
869
+ } catch {
870
+ /* v8 ignore start -- the fallback needs a live child whose group signal fails
871
+ (EPERM-style), which POSIX CI cannot stage; the swallow keeps teardown idempotent. */
872
+ try {
873
+ child.kill(sig);
874
+ } catch {}
875
+ }
876
+ }
877
+ /**
878
+ * Validate the synchronous portion of one ordinary spawn request.
879
+ * @param spec - exact target request.
880
+ * @throws when grace, cancellation, or argv is invalid before launch.
881
+ */
882
+ function validateSubprocessSpec(spec) {
883
+ if (!Number.isFinite(spec.graceMs) || spec.graceMs <= 0 || spec.graceMs > MAX_TIMER_DELAY_MS) throw new Error(`subprocess graceMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
884
+ if (spec.signal?.aborted) {
885
+ let reason = "aborted";
886
+ try {
887
+ reason = String(spec.signal.reason ?? reason);
888
+ } catch {}
889
+ throw new Error(`aborted before spawn: ${reason}`);
890
+ }
891
+ const [program] = spec.argv;
892
+ if (program === void 0 || program.length === 0) throw new Error("invalid argv: expected a non-empty program name at argv[0]");
893
+ }
894
+ function directChildResult(child) {
895
+ return new Promise((resolve, reject) => {
896
+ let completed = false;
897
+ child.once("error", (error) => {
898
+ /* v8 ignore next -- ChildProcess may report a later operational error after its
899
+ terminal exit event; the first terminal event owns the result. */
900
+ if (completed) return;
901
+ completed = true;
902
+ reject(error);
903
+ });
904
+ child.once("exit", (exitCode, signal) => {
905
+ /* v8 ignore next -- a spawn/kill error may be followed by exit; a Promise can publish only the first terminal event. */
906
+ if (completed) return;
907
+ completed = true;
908
+ resolve({
909
+ exitCode,
910
+ signal
911
+ });
912
+ });
913
+ });
914
+ }
915
+ function fallbackOwner(platform, pid, child, taskkill, linuxGroupHasLiveMembers, direct) {
916
+ let stopped = false;
917
+ let directSettled = false;
918
+ let observation;
919
+ direct.then(() => {
920
+ directSettled = true;
921
+ }, () => {
922
+ directSettled = true;
923
+ });
924
+ const alive = () => {
925
+ if (stopped || pid === void 0) return false;
926
+ if (platform === "win32") return child.exitCode === null && child.signalCode === null;
927
+ try {
928
+ process.kill(-pid, 0);
929
+ if (directSettled && platform === "linux" && linuxGroupHasLiveMembers(pid) === false) return false;
930
+ return true;
931
+ } catch (error) {
932
+ const code = error.code;
933
+ if (code === "ESRCH") return false;
934
+ /* v8 ignore start -- EPERM and non-POSIX negative-pid failures are platform defenses. */
935
+ if (code === "EPERM") return true;
936
+ return child.exitCode === null && child.signalCode === null;
937
+ }
938
+ };
939
+ return {
940
+ signal: (signal) => {
941
+ if (!alive()) {
942
+ stopped = true;
943
+ return;
944
+ }
945
+ signalTree(platform, pid, signal, child, taskkill);
946
+ },
947
+ waitForExit: async () => {
948
+ /* v8 ignore next -- bindManagedProcess memoizes this owner wait; the guard only
949
+ protects direct internal re-entry after signal() observed absence. */
950
+ if (stopped) return;
951
+ observation ??= (async () => {
952
+ while (alive()) await sleepTick();
953
+ stopped = true;
954
+ })();
955
+ await observation;
956
+ },
957
+ terminateForHostExit: () => {
958
+ if (stopped) return;
959
+ signalTree(platform, pid, "SIGKILL", child, taskkill);
960
+ }
961
+ };
962
+ }
963
+ /**
964
+ * Bind platform launch facts to the existing stdio, outcome, abort, and termination lifecycle.
965
+ * @param spec - fully resolved argv, cwd, stdio, grace, cancellation, environment.
966
+ * @param launch - platform streams, direct outcome, and managed-range owner.
967
+ * @param internals - test-only spill-directory override.
968
+ * @returns live subprocess handle.
969
+ */
970
+ function bindManagedProcess(spec, launch, internals = {}) {
971
+ const { spillDir } = prepareManagedProcessBinding(internals);
972
+ const { stdin, stdout, stderr } = launch;
973
+ const isCollect = (mode) => mode !== "pipe" && mode !== "inherit";
974
+ const outMode = spec.stdio.stdout;
975
+ const errMode = spec.stdio.stderr;
976
+ const stdinMode = spec.stdio.stdin;
977
+ const collectStream = (mode, stream, label) => {
978
+ if (!isCollect(mode) || stream === null) return void 0;
979
+ const collector = new OutputCollector(mode.maxBytes, mode.spill?.maxBytes, label, spillDir);
980
+ stream.on("data", (chunk) => {
981
+ collector.push(chunk);
982
+ });
983
+ return collector;
984
+ };
985
+ const stdoutCollector = collectStream(outMode, stdout, "stdout");
986
+ const stderrCollector = collectStream(errMode, stderr, "stderr");
987
+ const observeOutputStream = (mode, stream) => {
988
+ if (mode === "inherit" || stream === null || stream.readableEnded || stream.destroyed) return void 0;
989
+ return new Promise((resolve) => {
990
+ const settle = () => {
991
+ stream.off("end", settle);
992
+ stream.off("close", settle);
993
+ stream.off("error", settle);
994
+ resolve();
995
+ };
996
+ stream.once("end", settle);
997
+ stream.once("close", settle);
998
+ stream.once("error", settle);
999
+ });
1000
+ };
1001
+ const stdoutClosed = observeOutputStream(outMode, stdout);
1002
+ const stderrClosed = observeOutputStream(errMode, stderr);
1003
+ const outputStreamsClosed = Promise.all([stdoutClosed, stderrClosed]);
1004
+ const stopCollectors = () => {
1005
+ if (stdoutCollector !== void 0) stdout?.destroy();
1006
+ if (stderrCollector !== void 0) stderr?.destroy();
1007
+ stdoutCollector?.seal();
1008
+ stderrCollector?.seal();
1009
+ };
1010
+ let graceTimer;
1011
+ let terminationStarted = false;
1012
+ let rangeExitObserved = false;
1013
+ let rangeExitObservation;
1014
+ let settled = false;
1015
+ const scheduleOwnerCleanup = () => {
1016
+ if (launch.owner.cleanup === void 0) return false;
1017
+ queueMicrotask(() => {
1018
+ done.finally(() => {
1019
+ launch.owner.cleanup?.();
1020
+ }).catch(() => {});
1021
+ });
1022
+ return true;
1023
+ };
1024
+ /**
1025
+ * Start or reuse the handle's managed-range exit observer. A failed read
1026
+ * before direct settlement can be retried. Once direct settlement permits
1027
+ * cleanup, retain a failed observation because removing its private evidence
1028
+ * must not turn a later wait into a false success. The first confirmed
1029
+ * absence is the permanent no-more-signals boundary and cancels pending
1030
+ * escalation before stale identity can be used.
1031
+ */
1032
+ const observeRangeExit = () => {
1033
+ rangeExitObservation ??= (async () => {
1034
+ await launch.owner.waitForExit();
1035
+ rangeExitObserved = true;
1036
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
1037
+ graceTimer = void 0;
1038
+ spec.signal?.removeEventListener("abort", onAbort);
1039
+ scheduleOwnerCleanup();
1040
+ })().catch((error) => {
1041
+ if (!settled || !scheduleOwnerCleanup()) rangeExitObservation = void 0;
1042
+ throw error;
1043
+ });
1044
+ return rangeExitObservation;
1045
+ };
1046
+ const kill = (sig, cancellationReason) => {
1047
+ if (rangeExitObserved) return;
1048
+ launch.owner.signal(sig, cancellationReason);
1049
+ };
1050
+ const terminateWithReason = (cancellationReason) => {
1051
+ if (rangeExitObserved || terminationStarted) return;
1052
+ terminationStarted = true;
1053
+ observeRangeExit().catch(() => {});
1054
+ kill("SIGTERM", cancellationReason);
1055
+ graceTimer = setTimeout(() => {
1056
+ graceTimer = void 0;
1057
+ kill("SIGKILL");
1058
+ }, spec.graceMs);
1059
+ };
1060
+ const terminate = () => {
1061
+ terminateWithReason(/* @__PURE__ */ new Error("subprocess terminated before target start"));
1062
+ };
1063
+ const terminateForHostExit = () => {
1064
+ launch.owner.terminateForHostExit();
1065
+ };
1066
+ const onAbort = () => {
1067
+ terminateWithReason(spec.signal?.reason);
1068
+ };
1069
+ spec.signal?.addEventListener("abort", onAbort, { once: true });
1070
+ if (typeof stdinMode === "object" && stdin !== null) {
1071
+ stdin.on("error", () => {});
1072
+ stdin.end(stdinMode.data);
1073
+ }
1074
+ const done = new Promise((resolve, reject) => {
1075
+ let pipeDrainTimer;
1076
+ const settle = (outcome) => {
1077
+ if (settled) return;
1078
+ settled = true;
1079
+ stopCollectors();
1080
+ cleanup();
1081
+ resolve(outcome);
1082
+ };
1083
+ const fail = (error) => {
1084
+ settled = true;
1085
+ terminate();
1086
+ stopCollectors();
1087
+ cleanup();
1088
+ reject(error);
1089
+ };
1090
+ launch.direct.then((outcome) => {
1091
+ if (stdoutClosed === void 0 && stderrClosed === void 0) {
1092
+ settle(outcome);
1093
+ return;
1094
+ }
1095
+ pipeDrainTimer = setTimeout(() => {
1096
+ settle(outcome);
1097
+ }, spec.graceMs);
1098
+ outputStreamsClosed.then(() => {
1099
+ settle(outcome);
1100
+ });
1101
+ }, fail);
1102
+ function cleanup() {
1103
+ if (pipeDrainTimer !== void 0) clearTimeout(pipeDrainTimer);
1104
+ }
1105
+ });
1106
+ const waitForExit = async (signal) => {
1107
+ if (rangeExitObserved) return true;
1108
+ return waitWithAbort(observeRangeExit(), signal);
1109
+ };
1110
+ return {
1111
+ /* v8 ignore start -- pipe-mode streams exist on every conforming launch;
1112
+ the null-coalesces guard an internal adapter defect only. */
1113
+ stdin: stdinMode === "pipe" ? stdin ?? void 0 : void 0,
1114
+ stdout: outMode === "pipe" ? stdout ?? void 0 : void 0,
1115
+ stderr: errMode === "pipe" ? stderr ?? void 0 : void 0,
1116
+ /* v8 ignore stop */
1117
+ collected: {
1118
+ ...stdoutCollector !== void 0 ? { stdout: stdoutCollector } : {},
1119
+ ...stderrCollector !== void 0 ? { stderr: stderrCollector } : {}
1120
+ },
1121
+ done,
1122
+ terminate,
1123
+ terminateForHostExit,
1124
+ waitForExit
1125
+ };
1126
+ }
1127
+ /**
1128
+ * Spawn one detached PGID/taskkill fallback and bind the common lifecycle.
1129
+ * @param spec - fully resolved argv, cwd, stdio, grace, cancellation, environment.
1130
+ * @param internals - test-only spill-directory, platform, and taskkill overrides.
1131
+ * @returns live subprocess handle.
1132
+ */
1133
+ function spawnSubprocess(spec, internals = {}) {
1134
+ const binding = prepareManagedProcessBinding(internals);
1135
+ const platform = internals.platform ?? process.platform;
1136
+ const [program, ...args] = spec.argv;
1137
+ const child = (internals.spawn ?? spawn)(program, args, {
1138
+ cwd: spec.cwd,
1139
+ env: childEnv(spec.env),
1140
+ stdio: [
1141
+ spec.stdio.stdin === "ignore" ? "ignore" : "pipe",
1142
+ spec.stdio.stdout === "inherit" ? "inherit" : "pipe",
1143
+ spec.stdio.stderr === "inherit" ? "inherit" : "pipe"
1144
+ ],
1145
+ detached: platform !== "win32",
1146
+ windowsHide: platform === "win32"
1147
+ });
1148
+ const direct = directChildResult(child);
1149
+ const pid = child.pid;
1150
+ const owner = fallbackOwner(platform, pid, child, internals.taskkill ?? taskkillProcessTree, internals.linuxProcessGroupHasLiveMembers ?? linuxProcessGroupHasLiveMembers, direct);
1151
+ return bindManagedProcess(spec, {
1152
+ stdin: child.stdin,
1153
+ stdout: child.stdout,
1154
+ stderr: child.stderr,
1155
+ direct,
1156
+ owner
1157
+ }, binding);
1158
+ }
1159
+ //#endregion
1160
+ //#region lib/types/linux-execve.js
1161
+ /** Lazy libc execve and descriptor bindings used by the one-shot Linux bootstrap. */
1162
+ const STANDARD_FILE_DESCRIPTORS = [
1163
+ 0,
1164
+ 1,
1165
+ 2
1166
+ ];
1167
+ const F_GETFD = 1;
1168
+ const F_SETFD = 2;
1169
+ const FD_CLOEXEC = 1;
1170
+ let cachedExecve;
1171
+ function systemError(errno, syscall, path) {
1172
+ const uvError = -errno;
1173
+ const code = getSystemErrorName(uvError);
1174
+ const detail = getSystemErrorMessage(uvError);
1175
+ const subject = path === void 0 ? syscall : `${syscall} '${path}'`;
1176
+ const error = Object.assign(/* @__PURE__ */ new Error(`${code}: ${detail}, ${subject}`), {
1177
+ code,
1178
+ errno: uvError,
1179
+ syscall
1180
+ });
1181
+ return path === void 0 ? error : Object.assign(error, { path });
1182
+ }
1183
+ /**
1184
+ * Load libc's execve and fcntl symbols on first use and retain the native bindings.
1185
+ * @returns a process-replacing execve operation that throws Node-style errors on failure.
1186
+ */
1187
+ function loadLinuxExecve() {
1188
+ if (cachedExecve !== void 0) return cachedExecve;
1189
+ const libc = koffi.load(null);
1190
+ const nativeExecve = libc.func("int execve(const char *pathname, const char **argv, const char **envp)");
1191
+ const nativeFcntl = libc.func("int fcntl(int fd, int cmd, int arg)");
1192
+ cachedExecve = (file, argv, env) => {
1193
+ for (const fd of STANDARD_FILE_DESCRIPTORS) {
1194
+ const flags = nativeFcntl(fd, F_GETFD, 0);
1195
+ if (flags === -1) throw systemError(koffi.errno(), "fcntl");
1196
+ if ((flags & FD_CLOEXEC) === 0) continue;
1197
+ if (nativeFcntl(fd, F_SETFD, flags & -2) === -1) throw systemError(koffi.errno(), "fcntl");
1198
+ }
1199
+ nativeExecve(file, [...argv, null], [...Object.entries(env).map(([key, value]) => `${key}=${value}`), null]);
1200
+ throw systemError(koffi.errno(), "execve", file);
1201
+ };
1202
+ return cachedExecve;
1203
+ }
1204
+ //#endregion
1205
+ //#region lib/types/runner-protocol.js
1206
+ /** Closed private transports shared by the native subprocess runner. */
1207
+ function isRecord(value) {
1208
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1209
+ }
1210
+ function hasExactKeys(value, required, optional = []) {
1211
+ const allowed = new Set([...required, ...optional]);
1212
+ return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => allowed.has(key));
1213
+ }
1214
+ function isStringRecord(value) {
1215
+ return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
1216
+ }
1217
+ function isSerializedRunnerError(value) {
1218
+ if (!isRecord(value) || !hasExactKeys(value, ["name", "message"], [
1219
+ "code",
1220
+ "syscall",
1221
+ "path"
1222
+ ])) return false;
1223
+ return typeof value.name === "string" && typeof value.message === "string" && (value.code === void 0 || typeof value.code === "string") && (value.syscall === void 0 || typeof value.syscall === "string") && (value.path === void 0 || typeof value.path === "string");
1224
+ }
1225
+ function parseErrorResult(value) {
1226
+ if (!hasExactKeys(value, ["type", "error"]) || !isSerializedRunnerError(value.error)) throw new Error("subprocess runner emitted an invalid error result");
1227
+ if (value.type !== "error") throw new Error("subprocess runner emitted an unknown error result");
1228
+ return {
1229
+ type: "error",
1230
+ error: value.error
1231
+ };
1232
+ }
1233
+ /**
1234
+ * Create a private 0700 directory and one complete 0600 launch request.
1235
+ * @param request - target cwd and complete environment for the bootstrap.
1236
+ * @returns private paths owned by this launch.
1237
+ */
1238
+ function createLinuxLaunchFiles(request) {
1239
+ const directory = mkdtempSync(join(tmpdir(), "dsh-subprocess-launch-"));
1240
+ const files = {
1241
+ directory,
1242
+ requestPath: join(directory, "launch-request.json"),
1243
+ startupErrorPath: join(directory, "startup-error.json")
1244
+ };
1245
+ try {
1246
+ chmodSync(directory, 448);
1247
+ writeFileSync(files.requestPath, JSON.stringify(request), {
1248
+ flag: "wx",
1249
+ mode: 384
1250
+ });
1251
+ return files;
1252
+ } catch (error) {
1253
+ cleanupLinuxLaunchFiles(files);
1254
+ throw error;
1255
+ }
1256
+ }
1257
+ /**
1258
+ * Derive the only permitted startup-error path from an absolute request locator.
1259
+ * @param requestPath - absolute path to the private launch-request file.
1260
+ * @returns validated sibling paths for this launch.
1261
+ */
1262
+ function linuxLaunchFilesFromLocator(requestPath) {
1263
+ if (!isAbsolute(requestPath) || basename(requestPath) !== "launch-request.json") throw new Error("subprocess runner received an invalid Linux launch-request locator");
1264
+ const directory = dirname(requestPath);
1265
+ return {
1266
+ directory,
1267
+ requestPath,
1268
+ startupErrorPath: join(directory, "startup-error.json")
1269
+ };
1270
+ }
1271
+ /**
1272
+ * Strictly read and remove a one-shot Linux launch request.
1273
+ * @param requestPath - private launch-request path to consume.
1274
+ * @returns validated target cwd and environment.
1275
+ */
1276
+ function consumeLinuxLaunchRequest(requestPath) {
1277
+ const text = readFileSync(requestPath, "utf8");
1278
+ unlinkSync(requestPath);
1279
+ const value = JSON.parse(text);
1280
+ if (!isRecord(value) || !hasExactKeys(value, ["cwd", "env"]) || typeof value.cwd !== "string" || !isStringRecord(value.env)) throw new Error("subprocess runner received an invalid Linux launch request");
1281
+ return {
1282
+ cwd: value.cwd,
1283
+ env: value.env
1284
+ };
1285
+ }
1286
+ /**
1287
+ * Publish one strict 0600 Linux pre-exec error.
1288
+ * @param files - private paths for this launch.
1289
+ * @param error - bounded spawn or runner failure to publish.
1290
+ */
1291
+ function writeLinuxStartupError(files, error) {
1292
+ writeFileSync(files.startupErrorPath, JSON.stringify(error), {
1293
+ flag: "wx",
1294
+ mode: 384
1295
+ });
1296
+ }
1297
+ /**
1298
+ * Read the Linux pre-exec error, if the bootstrap published one.
1299
+ * @param path - expected startup-error path.
1300
+ * @returns the validated failure, or undefined when none was published.
1301
+ */
1302
+ function readLinuxStartupError(path) {
1303
+ if (!existsSync(path)) return void 0;
1304
+ const value = JSON.parse(readFileSync(path, "utf8"));
1305
+ if (!isRecord(value)) throw new Error("subprocess runner emitted an invalid startup error");
1306
+ return parseErrorResult(value);
1307
+ }
1308
+ /**
1309
+ * Strictly parse the single Windows start message.
1310
+ * @param value - untrusted IPC payload.
1311
+ * @returns validated target start request.
1312
+ */
1313
+ function parseWindowsStartRequest(value) {
1314
+ if (!isRecord(value) || !hasExactKeys(value, [
1315
+ "type",
1316
+ "cwd",
1317
+ "env"
1318
+ ]) || value.type !== "start" || typeof value.cwd !== "string" || !isStringRecord(value.env)) throw new Error("subprocess runner received an invalid Windows start request");
1319
+ return {
1320
+ type: "start",
1321
+ cwd: value.cwd,
1322
+ env: value.env
1323
+ };
1324
+ }
1325
+ /**
1326
+ * Return true only for the exact, payload-free Windows terminate control.
1327
+ * @param value - untrusted IPC payload.
1328
+ * @returns whether the payload is the exact terminate request.
1329
+ */
1330
+ function isWindowsTerminateRequest(value) {
1331
+ return isRecord(value) && hasExactKeys(value, ["type"]) && value.type === "terminate";
1332
+ }
1333
+ /**
1334
+ * Strictly parse one of the two Windows direct-result branches.
1335
+ * @param value - untrusted IPC payload.
1336
+ * @returns validated direct-result message.
1337
+ */
1338
+ function parseWindowsRunnerResult(value) {
1339
+ if (!isRecord(value) || typeof value.type !== "string") throw new Error("subprocess runner emitted an invalid Windows result");
1340
+ if (value.type === "error") return parseErrorResult(value);
1341
+ if (value.type === "target-exit") {
1342
+ const validExitCode = typeof value.exitCode === "number" && Number.isSafeInteger(value.exitCode) && value.exitCode >= 0;
1343
+ if (!hasExactKeys(value, ["type", "exitCode"]) || !validExitCode) throw new Error("subprocess runner emitted an invalid target-exit result");
1344
+ return {
1345
+ type: "target-exit",
1346
+ exitCode: value.exitCode
1347
+ };
1348
+ }
1349
+ throw new Error(`subprocess runner emitted an unknown Windows result: ${value.type}`);
1350
+ }
1351
+ /**
1352
+ * Convert an unknown failure into the bounded cross-process error record.
1353
+ * @param error - failure caught at the process boundary.
1354
+ * @returns bounded serializable error fields.
1355
+ */
1356
+ function serializeRunnerError(error) {
1357
+ const source = error instanceof Error ? error : new Error(String(error));
1358
+ const node = source;
1359
+ return {
1360
+ name: source.name,
1361
+ message: source.message,
1362
+ ...typeof node.code === "string" ? { code: node.code } : {},
1363
+ ...typeof node.syscall === "string" ? { syscall: node.syscall } : {},
1364
+ ...typeof node.path === "string" ? { path: node.path } : {}
1365
+ };
1366
+ }
1367
+ /**
1368
+ * Rebuild a Node-shaped Error from a strict runner record.
1369
+ * @param serialized - validated bounded error fields.
1370
+ * @returns reconstructed Error with supported Node fields.
1371
+ */
1372
+ function deserializeRunnerError(serialized) {
1373
+ const error = new Error(serialized.message);
1374
+ error.name = serialized.name;
1375
+ return Object.assign(error, {
1376
+ ...serialized.code === void 0 ? {} : { code: serialized.code },
1377
+ ...serialized.syscall === void 0 ? {} : { syscall: serialized.syscall },
1378
+ ...serialized.path === void 0 ? {} : { path: serialized.path }
1379
+ });
1380
+ }
1381
+ /**
1382
+ * Best-effort removal of only the private paths created for this Linux spawn.
1383
+ * @param files - exact private paths owned by this launch.
1384
+ */
1385
+ function cleanupLinuxLaunchFiles(files) {
1386
+ try {
1387
+ if (lstatSync(files.directory).isSymbolicLink()) {
1388
+ unlinkSync(files.directory);
1389
+ return;
1390
+ }
1391
+ for (const path of [files.requestPath, files.startupErrorPath]) try {
1392
+ unlinkSync(path);
1393
+ } catch (error) {
1394
+ if (error.code !== "ENOENT") throw error;
1395
+ }
1396
+ rmdirSync(files.directory);
1397
+ } catch {}
1398
+ }
1399
+ //#endregion
1400
+ //#region lib/types/runner-launch.js
1401
+ /** Parent-side invocation and bootstrap state for the private native runner. */
1402
+ /** The one private environment variable consumed before target state is restored. */
1403
+ const SUBPROCESS_RUNNER_ENV = "DSH_SUBPROCESS_RUNNER";
1404
+ /** Sentinel used by the packaged bootstrap for the Windows IPC runner. */
1405
+ const WINDOWS_RUNNER_SELECTION = "windows";
1406
+ const SOURCE_TSCONFIG_PATH = fileURLToPath(new URL("../../../../tsconfig.base.json", import.meta.url));
1407
+ const RUNNER_CONTROL_ENV_PREFIXES = ["NODE_", "TSX_"];
1408
+ /**
1409
+ * Resolve the source, built, or packaged entry that calls the same runner core.
1410
+ * @returns executable and arguments for the active runtime form.
1411
+ */
1412
+ function spawnRunnerInvocation() {
1413
+ if ("pkg" in process) return [process.execPath];
1414
+ /* v8 ignore next -- built-artifact smoke imports the emitted JavaScript runner entry;
1415
+ * source-unit coverage cannot change import.meta.url. */
1416
+ if (extname(fileURLToPath(import.meta.url)) !== ".ts") return [process.execPath, fileURLToPath(import.meta.resolve("@deepseek-ai/dsh-subprocess-local/runner"))];
1417
+ return [
1418
+ process.execPath,
1419
+ "--import",
1420
+ import.meta.resolve("tsx/esm"),
1421
+ fileURLToPath(new URL("./bin.ts", import.meta.url))
1422
+ ];
1423
+ }
1424
+ /**
1425
+ * Check the concrete runner executable and entry paths without executing a probe mode.
1426
+ * @param invocation - resolved executable and runner-entry arguments.
1427
+ * @returns whether every concrete executable or entry path is accessible.
1428
+ */
1429
+ function runnerInvocationAvailable(invocation = spawnRunnerInvocation()) {
1430
+ try {
1431
+ if (isAbsolute(invocation[0])) accessSync(invocation[0], constants.X_OK);
1432
+ const entry = invocation.at(-1);
1433
+ if (entry !== void 0 && entry !== invocation[0] && isAbsolute(entry)) accessSync(entry, constants.R_OK);
1434
+ return true;
1435
+ } catch {
1436
+ return false;
1437
+ }
1438
+ }
1439
+ /**
1440
+ * Build the bootstrap-safe environment; target overrides arrive through request/IPC.
1441
+ * @param selection - private runner selector or Linux launch-request locator.
1442
+ * @param invocation - resolved runner invocation whose source form needs the workspace paths map.
1443
+ * @returns environment for the runner before target state is restored.
1444
+ */
1445
+ function runnerEnvironment(selection, invocation) {
1446
+ const entry = invocation?.at(-1);
1447
+ const env = childEnv();
1448
+ for (const name of Object.keys(env)) {
1449
+ const normalized = name.toUpperCase();
1450
+ if (RUNNER_CONTROL_ENV_PREFIXES.some((prefix) => normalized.startsWith(prefix))) Reflect.deleteProperty(env, name);
1451
+ }
1452
+ return {
1453
+ ...env,
1454
+ [SUBPROCESS_RUNNER_ENV]: selection,
1455
+ SYSTEMD_LOG_TARGET: "null",
1456
+ ...entry?.endsWith(".ts") === true ? { TSX_TSCONFIG_PATH: SOURCE_TSCONFIG_PATH } : {}
1457
+ };
1458
+ }
1459
+ /**
1460
+ * Read and delete the private selector before importing or restoring target state.
1461
+ * @param env - mutable environment containing the private selector.
1462
+ * @returns the consumed selector, or undefined when no runner was requested.
1463
+ */
1464
+ function consumeRunnerSelection(env = process.env) {
1465
+ const selection = env[SUBPROCESS_RUNNER_ENV];
1466
+ Reflect.deleteProperty(env, SUBPROCESS_RUNNER_ENV);
1467
+ return selection;
1468
+ }
1469
+ /**
1470
+ * Require the private argv delimiter and at least one target argv entry.
1471
+ * @param argv - private runner arguments.
1472
+ * @returns copied target argv after the private delimiter.
1473
+ */
1474
+ function parseRunnerTargetArgv(argv) {
1475
+ if (argv[0] !== "--" || argv.length < 2) throw new Error("subprocess runner requires target argv after a private -- delimiter");
1476
+ return [...argv.slice(1)];
1477
+ }
1478
+ /**
1479
+ * Build direct Linux target stdio, or isolated Windows runner stdio with IPC
1480
+ * on fd 3 and target carriers on fd 4 through fd 6.
1481
+ * @param spec - ordinary subprocess request whose stdio modes are preserved.
1482
+ * @param ipc - whether to isolate the runner and add its private Node IPC descriptor.
1483
+ * @param stdinCarrier - runner fd 4 carrier; Windows ignore passes an opened null-device fd.
1484
+ * @returns child-process stdio options for the runner.
1485
+ */
1486
+ function runnerStdio(spec, ipc, stdinCarrier = "pipe") {
1487
+ const targetStdio = [
1488
+ spec.stdio.stdin === "ignore" ? "ignore" : "pipe",
1489
+ spec.stdio.stdout === "inherit" ? "inherit" : "pipe",
1490
+ spec.stdio.stderr === "inherit" ? "inherit" : "pipe"
1491
+ ];
1492
+ if (!ipc) return targetStdio;
1493
+ return [
1494
+ "ignore",
1495
+ "ignore",
1496
+ "ignore",
1497
+ "ipc",
1498
+ stdinCarrier,
1499
+ spec.stdio.stdout === "inherit" ? 1 : "pipe",
1500
+ spec.stdio.stderr === "inherit" ? 2 : "pipe"
1501
+ ];
1502
+ }
1503
+ function windowsEnvironmentValue(env, name) {
1504
+ for (const key of Object.keys(env).sort()) if (key.toUpperCase() === name) return env[key];
1505
+ }
1506
+ function executableCandidateExists(candidate) {
1507
+ try {
1508
+ return !statSync(candidate).isDirectory();
1509
+ } catch {
1510
+ try {
1511
+ const entry = lstatSync(candidate);
1512
+ return entry.isFile() || entry.isSymbolicLink();
1513
+ } catch {
1514
+ return false;
1515
+ }
1516
+ }
1517
+ }
1518
+ function windowsPathDirectories(path) {
1519
+ const directories = [];
1520
+ let start = 0;
1521
+ while (start < path.length) {
1522
+ if (path.charAt(start) === ";") {
1523
+ start += 1;
1524
+ continue;
1525
+ }
1526
+ const quote = path.charAt(start);
1527
+ const quoted = quote === "\"" || quote === "'";
1528
+ const quoteEnd = quoted ? path.indexOf(quote, start + 1) : -1;
1529
+ const separator = path.indexOf(";", quoted ? quoteEnd < 0 ? path.length : quoteEnd : start);
1530
+ const end = separator < 0 ? path.length : separator;
1531
+ let directory = path.slice(start, end);
1532
+ if (directory.startsWith("\"") || directory.startsWith("'")) directory = directory.slice(1);
1533
+ if (directory.endsWith("\"") || directory.endsWith("'")) directory = directory.slice(0, -1);
1534
+ if (directory.length > 0) directories.push(directory);
1535
+ start = end + 1;
1536
+ }
1537
+ return directories;
1538
+ }
1539
+ function windowsFileNameStart(command) {
1540
+ let start = command.length;
1541
+ while (start > 0 && !/[\\/:]/u.test(command.charAt(start - 1))) start -= 1;
1542
+ return start;
1543
+ }
1544
+ function windowsSearchPathJoin(directory, name, cwd) {
1545
+ let prefix = cwd;
1546
+ let adjustedDirectory = directory;
1547
+ const slash = (value) => value === "\\" || value === "/";
1548
+ if (directory.length > 2 && slash(directory.charAt(0)) && slash(directory.charAt(1))) prefix = "";
1549
+ else if (directory.length >= 1 && slash(directory.charAt(0))) prefix = cwd.slice(0, 2);
1550
+ else if (directory.length >= 2 && directory.charAt(1) === ":" && (directory.length < 3 || !slash(directory.charAt(2)))) if (cwd.length < 2 || cwd.slice(0, 2).toLowerCase() !== directory.slice(0, 2).toLowerCase()) prefix = "";
1551
+ else adjustedDirectory = directory.slice(2);
1552
+ else if (directory.length > 2 && directory.charAt(1) === ":") prefix = "";
1553
+ const append = (base, part) => {
1554
+ if (base.length === 0 || part.length === 0) return base + part;
1555
+ return /[\\/:]$/u.test(base) ? base + part : `${base}\\${part}`;
1556
+ };
1557
+ return append(append(prefix, adjustedDirectory), name);
1558
+ }
1559
+ function windowsExecutableNames(command, name) {
1560
+ const dot = name.indexOf(".");
1561
+ const hasExtension = dot >= 0 && dot < name.length - 1;
1562
+ const separator = name.endsWith(".") ? "" : ".";
1563
+ return [
1564
+ ...hasExtension ? [command] : [],
1565
+ `${command}${separator}com`,
1566
+ `${command}${separator}exe`
1567
+ ];
1568
+ }
1569
+ /**
1570
+ * Resolve the executable path with libuv/Node Windows spawn search order while
1571
+ * preserving the caller's original command-line argv entry separately.
1572
+ * @param command - original target argv[0].
1573
+ * @param cwd - final target working directory used for relative search roots.
1574
+ * @param env - final target environment containing the child PATH.
1575
+ * @param exists - injectable non-directory candidate probe used by tests.
1576
+ * @param currentEnv - runner environment supplying PATH fallback and cwd-search policy.
1577
+ * @returns a resolved application name suitable for `CreateProcessW`, or undefined when no candidate exists.
1578
+ */
1579
+ function resolveWindowsExecutable(command, cwd, env, exists = executableCandidateExists, currentEnv = process.env) {
1580
+ const nameStart = windowsFileNameStart(command);
1581
+ const directory = command.slice(0, nameStart);
1582
+ const name = command.slice(nameStart);
1583
+ const hasPath = nameStart !== 0;
1584
+ const roots = [];
1585
+ if (hasPath) roots.push(directory);
1586
+ else {
1587
+ if (windowsEnvironmentValue(currentEnv, "NODEFAULTCURRENTDIRECTORYINEXEPATH") === void 0) roots.push("");
1588
+ const path = windowsEnvironmentValue(env, "PATH") ?? windowsEnvironmentValue(currentEnv, "PATH") ?? "";
1589
+ roots.push(...windowsPathDirectories(path));
1590
+ }
1591
+ for (const root of roots) {
1592
+ const base = windowsSearchPathJoin(root, name, cwd);
1593
+ for (const candidate of windowsExecutableNames(base, name)) if (exists(candidate)) return candidate;
1594
+ }
1595
+ }
1596
+ function throwNullByteError(property, value, argument) {
1597
+ const subject = argument ? `The argument '${property}'` : `The property '${property}'`;
1598
+ const error = /* @__PURE__ */ new TypeError(`${subject} must be a string without null bytes. Received ${inspect(value)}`);
1599
+ Object.assign(error, { code: "ERR_INVALID_ARG_VALUE" });
1600
+ throw error;
1601
+ }
1602
+ function validateNoNullByte(property, value, argument = false) {
1603
+ if (value.includes("\0")) throwNullByteError(property, value, argument);
1604
+ }
1605
+ /**
1606
+ * Materialize and synchronously validate the final target environment.
1607
+ * @param spec - final target argv, cwd, and environment overrides.
1608
+ * @returns complete target environment after Node-equivalent validation.
1609
+ */
1610
+ function targetEnvironment(spec) {
1611
+ spec.argv.forEach((value, index) => {
1612
+ validateNoNullByte(index === 0 ? "file" : `args[${String(index - 1)}]`, value, true);
1613
+ });
1614
+ validateNoNullByte("options.cwd", spec.cwd);
1615
+ const env = Object.fromEntries(Object.entries(childEnv(spec.env)).filter((entry) => entry[1] !== void 0));
1616
+ for (const [key, value] of Object.entries(env)) {
1617
+ validateNoNullByte(`options.env['${key}']`, key);
1618
+ validateNoNullByte(`options.env['${key}']`, value);
1619
+ }
1620
+ return env;
1621
+ }
1622
+ //#endregion
1623
+ export { bindManagedProcess as C, validateSubprocessSpec as D, spawnSubprocess as E, createProcessInspector as O, loadLinuxExecve as S, prepareManagedProcessBinding as T, parseWindowsRunnerResult as _, resolveWindowsExecutable as a, serializeRunnerError as b, runnerStdio as c, cleanupLinuxLaunchFiles as d, consumeLinuxLaunchRequest as f, linuxLaunchFilesFromLocator as g, isWindowsTerminateRequest as h, parseRunnerTargetArgv as i, spawnRunnerInvocation as l, deserializeRunnerError as m, WINDOWS_RUNNER_SELECTION as n, runnerEnvironment as o, createLinuxLaunchFiles as p, consumeRunnerSelection as r, runnerInvocationAvailable as s, SUBPROCESS_RUNNER_ENV as t, targetEnvironment as u, parseWindowsStartRequest as v, childEnv as w, writeLinuxStartupError as x, readLinuxStartupError as y };