import { closeSync, constants, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, statSync, unlinkSync, writeSync } from "node:fs";
import { access, stat } from "node:fs/promises";
import { delimiter, extname, isAbsolute, join, resolve } from "node:path";
import * as nodePty from "node-pty";
import { SubprocessRuntime, scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
import { execFileSync, spawn, spawnSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import { constants as constants$1, tmpdir } from "node:os";
import { setTimeout as setTimeout$1 } from "node:timers/promises";
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
import koffi from "koffi";
import { Buffer as Buffer$1 } from "node:buffer";
import { PassThrough } from "node:stream";
//#region lib/types/windows-inspector.js
/**
* Windows process-table operations for terminal readiness, signalling, and
* teardown: Toolhelp32 snapshot enumeration with GetProcessTimes creation-time
* identity and process-handle wait-state liveness, the shell pid as a pseudo
* process group (Windows has no POSIX groups), and taskkill tree signalling.
* The koffi bindings load lazily so
* non-Windows processes never touch Win32 libraries; all decision logic takes
* an injectable internals boundary so suites can pin it on any host.
* @module dsh-subprocess-local/windows-inspector
*/
/**
* Walk a process table from one root in children-first order, retaining only
* members whose start identity is readable (unreadable members are detector
* misses, exactly like an unreadable `/proc` entry on Linux).
* @param entries - the process table snapshot.
* @param rootPid - the tree root to descend from.
* @param started - creation-time identity resolver for one member.
* @returns the root and its current transitive descendants, children first.
*/
function windowsProcessTree(entries, rootPid, started) {
	const root = new Map(entries.map((entry) => [entry.pid, entry])).get(rootPid);
	if (root === void 0) return [];
	const byParent = /* @__PURE__ */ new Map();
	for (const entry of entries) {
		const children = byParent.get(entry.parentPid) ?? [];
		children.push(entry);
		byParent.set(entry.parentPid, children);
	}
	const visited = /* @__PURE__ */ new Set();
	const result = [];
	const visit = (entry) => {
		if (visited.has(entry.pid)) return;
		visited.add(entry.pid);
		for (const child of byParent.get(entry.pid) ?? []) visit(child);
		const identity = started(entry.pid);
		if (identity !== void 0) result.push({
			pid: entry.pid,
			started: identity
		});
	};
	visit(root);
	return result;
}
/**
* Windows {@link ProcessInspector}. The shell pid stands in for a foreground
* process group: it is a stable pseudo-group that lets the prompt-marker
* readiness path compare foreground identities, while every actual signal
* targets the console-wide tree through taskkill (SIGINT is delivered by the
* terminal handle as a `\x03` input write and never reaches this layer).
*/
var WindowsProcessInspector = class {
	internals;
	constructor(internals = defaultWindowsProcessInternals()) {
		this.internals = internals;
	}
	foregroundPgid(shellPid) {
		return shellPid;
	}
	isStdinWaiting(_pgid, _shellPid) {
		return false;
	}
	isAlive(identity) {
		const state = this.internals.processState(identity.pid);
		return state?.active === true && state.started === identity.started;
	}
	snapshot() {
		let entries;
		return {
			tree: (rootPid) => windowsProcessTree(entries ??= this.internals.snapshot(), rootPid, (pid) => this.internals.processState(pid)?.started),
			session: () => [],
			alive: (identity) => this.isAlive(identity)
		};
	}
	signalGroup(pgid, signal) {
		this.internals.taskkill(pgid, signal === "SIGKILL");
	}
	signalProcess(identity, signal) {
		if (this.isAlive(identity)) this.internals.taskkill(identity.pid, signal === "SIGKILL");
	}
};
/**
* Create the Windows process inspector.
* @param internals - injectable process operations; defaults to the koffi-backed table.
* @returns the Windows inspector.
*/
function createWindowsProcessInspector(internals = defaultWindowsProcessInternals()) {
	return new WindowsProcessInspector(internals);
}
/** Terminate one Windows process tree with taskkill, contained like POSIX group signalling. */
function taskkillTree(pid, force) {
	if (pid <= 0) return;
	spawnSync("taskkill", [
		"/PID",
		String(pid),
		"/T",
		...force ? ["/F"] : []
	], { stdio: "ignore" });
}
/**
* True for NULL and INVALID_HANDLE_VALUE returns from Win32 handle APIs.
* @param value - a handle as koffi may hand it back (pointer, null, or 0n).
* @returns whether the value signals an invalid handle.
*/
function isInvalidHandle(value) {
	if (value === null || value === void 0) return true;
	const asBigInt = value;
	return asBigInt === 0n || asBigInt === 18446744073709551615n || asBigInt === -1n;
}
const PVOID = koffi.pointer("void");
/**
* Resolve the koffi Win32 struct types once. Registration is lazy and cached
* because koffi's type registry is global per process: test runners that
* re-evaluate this module (a hoisted `vi.mock` re-imports the graph) must not
* re-register the names.
*/
function win32Structs() {
	if (cachedStructs !== void 0) return cachedStructs;
	const PROCESSENTRY32W = koffi.struct("PROCESSENTRY32W", {
		dwSize: "uint32",
		cntUsage: "uint32",
		th32ProcessID: "uint32",
		th32DefaultHeapID: PVOID,
		th32ModuleID: "uint32",
		cCntThreads: "uint32",
		th32ParentProcessID: "uint32",
		pcPriClassBase: "int32",
		dwFlags: "uint32",
		szExeFile: koffi.array("char16", 260)
	});
	const FILETIME = koffi.struct("FILETIME", {
		dwLowDateTime: "uint32",
		dwHighDateTime: "uint32"
	});
	/* v8 ignore start -- a layout-mismatch guard fires only on ABI breakage; the windows-native suites exercise the real struct. */
	if (PROCESSENTRY32W.size !== 568) throw new Error(`PROCESSENTRY32W layout mismatch: koffi computed ${PROCESSENTRY32W.size}, Windows headers say 568`);
	/* v8 ignore stop */
	cachedStructs = {
		PROCESSENTRY32W,
		FILETIME
	};
	return cachedStructs;
}
let cachedStructs;
const TH32CS_SNAPPROCESS = 2;
const WAIT_OBJECT_0 = 0;
const WAIT_TIMEOUT = 258;
let cachedBindings;
/**
* Resolve the lazy Win32 bindings (throws the first binding failure, fail-closed).
* @returns the cached binding table.
*/
function win32Bindings() {
	if (cachedBindings !== void 0) return cachedBindings;
	const { PROCESSENTRY32W, FILETIME } = win32Structs();
	const kernel32 = koffi.load("kernel32.dll");
	const bind = (name, result, args) => kernel32.func("__stdcall", name, result, args);
	cachedBindings = {
		createToolhelp32Snapshot: bind("CreateToolhelp32Snapshot", PVOID, ["uint32", "uint32"]),
		process32FirstW: bind("Process32FirstW", "int", [PVOID, koffi.pointer(PROCESSENTRY32W)]),
		process32NextW: bind("Process32NextW", "int", [PVOID, koffi.pointer(PROCESSENTRY32W)]),
		openProcess: bind("OpenProcess", PVOID, [
			"uint32",
			"int",
			"uint32"
		]),
		getProcessTimes: bind("GetProcessTimes", "int", [
			PVOID,
			koffi.pointer(FILETIME),
			koffi.pointer(FILETIME),
			koffi.pointer(FILETIME),
			koffi.pointer(FILETIME)
		]),
		waitForSingleObject: bind("WaitForSingleObject", "uint32", [PVOID, "uint32"]),
		closeHandle: bind("CloseHandle", "int", [PVOID])
	};
	return cachedBindings;
}
/**
* Allocate koffi memory as a branded {@link NativePtr}; koffi's TS types are
* `any`, so the cast goes through `unknown` to keep the unsafe surface here.
* @param type - the koffi type to allocate.
* @param count - element count.
* @returns the branded allocation pointer.
*/
function allocNative(type, count) {
	return koffi.alloc(type, count);
}
/** Enumerate the current process table through Toolhelp32. */
function snapshotWindowsProcesses(bindings) {
	const { PROCESSENTRY32W } = win32Structs();
	const snapshot = bindings.createToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	/* v8 ignore next -- an invalid snapshot for the process flag is not producible through the public API;
	the guard mirrors POSIX's unreadable-proc tolerance and isInvalidHandle is unit-tested. */
	if (isInvalidHandle(snapshot)) return [];
	const entries = [];
	try {
		const entry = allocNative(PROCESSENTRY32W, 1);
		koffi.encode(entry, "uint32", PROCESSENTRY32W.size);
		let ok = bindings.process32FirstW(snapshot, entry);
		while (ok !== 0) {
			const record = koffi.decode(entry, PROCESSENTRY32W);
			entries.push({
				pid: record.th32ProcessID,
				parentPid: record.th32ParentProcessID
			});
			ok = bindings.process32NextW(snapshot, entry);
		}
	} finally {
		bindings.closeHandle(snapshot);
	}
	return entries;
}
/** Read one process's creation identity and current wait state. */
function windowsProcessState(bindings, pid) {
	const { FILETIME } = win32Structs();
	const handle = bindings.openProcess(1052672, 0, pid);
	if (isInvalidHandle(handle)) return void 0;
	try {
		const creation = allocNative(FILETIME, 1);
		const exit = allocNative(FILETIME, 1);
		const kernel = allocNative(FILETIME, 1);
		const user = allocNative(FILETIME, 1);
		/* v8 ignore next -- a GetProcessTimes failure after a successful open races process exit and
		cannot be staged deterministically; the absent-process path is covered and the caller
		treats undefined as a detector miss. */
		if (bindings.getProcessTimes(handle, creation, exit, kernel, user) === 0) return void 0;
		const record = koffi.decode(creation, FILETIME);
		const wait = bindings.waitForSingleObject(handle, 0);
		/* v8 ignore next -- an opened process handle has exactly one of these two
		zero-time wait states; an unexpected Win32 failure is an unreadable process. */
		if (wait !== WAIT_OBJECT_0 && wait !== WAIT_TIMEOUT) return void 0;
		return {
			started: `${record.dwHighDateTime}:${record.dwLowDateTime}`,
			active: wait === WAIT_TIMEOUT
		};
	} finally {
		bindings.closeHandle(handle);
	}
}
/** The koffi-backed default internals; bindings resolve lazily on first use. */
function defaultWindowsProcessInternals() {
	return {
		snapshot: () => snapshotWindowsProcesses(win32Bindings()),
		processState: (pid) => windowsProcessState(win32Bindings(), pid),
		taskkill: taskkillTree
	};
}
//#endregion
//#region lib/types/process-inspector.js
/** Platform process-table inspection for terminal readiness, signals, and teardown. */
/* v8 ignore start -- thin OS bindings; injected logic is unit-tested and real platform composition exercises them. */
const DEFAULT_INTERNALS = {
	readFile: (path) => readFileSync(path, "utf8"),
	readDir: (path) => readdirSync(path),
	readLink: (path) => readlinkSync(path, "utf8"),
	stat: (path) => statSync(path),
	open: (path) => openSync(path, "r"),
	read: (fd, buffer, length, position) => readSync(fd, buffer, 0, length, position),
	close: closeSync,
	exec: (file, args) => execFileSync(file, args, { encoding: "utf8" }),
	kill: (pid, signal) => process.kill(pid, signal)
};
/**
* Parse fields used from Linux `/proc/<pid>/stat`, including parenthesized comm text.
* @param text - complete stat line.
* @returns Parsed identity/group fields, or undefined for malformed input.
*/
function parseProcStat(text) {
	const open = text.indexOf("(");
	const close = text.lastIndexOf(")");
	if (open <= 0 || close <= open) return void 0;
	const pid = Number(text.slice(0, open).trim());
	const rest = text.slice(close + 2).trim().split(/\s+/);
	const state = rest[0] || "";
	const parentPid = Number(rest[1]);
	const pgrp = Number(rest[2]);
	const session = Number(rest[3]);
	const ttyDevice = Number(rest[4]);
	const tpgid = Number(rest[5]);
	const started = rest[19];
	if (![
		pid,
		parentPid,
		pgrp,
		session,
		ttyDevice,
		tpgid
	].every(Number.isSafeInteger) || state.length !== 1 || started === void 0) return void 0;
	return {
		pid,
		parentPid,
		pgrp,
		session,
		state,
		ttyDevice,
		tpgid,
		started
	};
}
function readLinuxStat(internals, pid) {
	try {
		return parseProcStat(internals.readFile(`/proc/${pid}/stat`));
	} catch (_unreadableProcEntry) {
		return;
	}
}
function linuxDeviceNumber(value) {
	return value >>> 0;
}
function readLinuxTerminalDevice(internals, pid, ttyDevice, tid) {
	const terminalDevice = linuxDeviceNumber(ttyDevice);
	if (terminalDevice === 0) return void 0;
	const path = tid === void 0 ? `/proc/${pid}/fd/0` : `/proc/${pid}/task/${tid}/fd/0`;
	try {
		if (internals.readLink(path) === "/dev/tty") return terminalDevice;
		const status = internals.stat(path);
		return status.isCharacterDevice() && linuxDeviceNumber(status.rdev) === terminalDevice ? terminalDevice : void 0;
	} catch (_unreadableStdinDevice) {
		return;
	}
}
/**
* Report whether a Linux process group has an executing member. `false`
* means the group contains only zombie/dead entries; `undefined` means the
* process table could not prove either outcome.
* @param processGroupId - POSIX process-group id to inspect.
* @param internals - injectable process-table operations.
* @returns Live-member presence, or `undefined` when unavailable/absent.
*/
function linuxProcessGroupHasLiveMembers(processGroupId, internals = DEFAULT_INTERNALS) {
	let entries;
	try {
		entries = internals.readDir("/proc");
	} catch (_unreadableProcDirectory) {
		return;
	}
	let matched = false;
	for (const entry of entries) {
		if (!/^\d+$/.test(entry)) continue;
		const stat = readLinuxStat(internals, Number(entry));
		if (stat?.pgrp !== processGroupId) continue;
		matched = true;
		if (!/^[ZXx]$/.test(stat.state)) return true;
	}
	return matched ? false : void 0;
}
function numericEntries(internals, path) {
	try {
		return internals.readDir(path).filter((entry) => /^\d+$/.test(entry)).map(Number);
	} catch (_unreadableProcDirectory) {
		return [];
	}
}
function readSyscall(internals, pid, tid) {
	try {
		const text = internals.readFile(`/proc/${pid}/task/${tid}/syscall`).trim();
		if (text === "running" || text.startsWith("-1 ")) return void 0;
		const fields = text.split(/\s+/);
		const number = Number(fields[0]);
		const args = fields.slice(1, 7).map((field) => Number.parseInt(field, 16));
		if (!Number.isSafeInteger(number) || args.some((value) => !Number.isSafeInteger(value))) return void 0;
		return {
			number,
			args
		};
	} catch (_unreadableSyscall) {
		return;
	}
}
function readMemory(internals, pid, address, length) {
	let fd;
	try {
		fd = internals.open(`/proc/${pid}/mem`);
		const buffer = Buffer.alloc(length);
		const count = internals.read(fd, buffer, length, address);
		return buffer.subarray(0, count);
	} catch (_unreadableProcessMemory) {
		return;
	} finally {
		if (fd !== void 0) internals.close(fd);
	}
}
function fdSetHasStdin(internals, pid, address) {
	return address !== 0 && (readMemory(internals, pid, address, 8)?.[0] ?? 0) % 2 === 1;
}
function pollHasStdin(internals, pid, address, count) {
	if (address === 0 || count <= 0) return false;
	const memory = readMemory(internals, pid, address, Math.min(count, 1024) * 8);
	if (memory === void 0) return false;
	for (let offset = 0; offset + 8 <= memory.length; offset += 8) if (memory.readInt32LE(offset) === 0 && (memory.readInt16LE(offset + 4) & 1) !== 0) return true;
	return false;
}
function epollHasStdin(internals, pid, tid, epfd) {
	try {
		return internals.readFile(`/proc/${pid}/task/${tid}/fdinfo/${epfd}`).split("\n").some((line) => /^tfd:\s+0\b/.test(line.trim()));
	} catch (_unreadableFdInfo) {
		return false;
	}
}
const SYSCALLS = {
	x64: {
		read: 0,
		select: 23,
		pselect: 270,
		poll: 7,
		ppoll: 271,
		epollWait: 232,
		epollPwait: 281
	},
	arm64: {
		read: 63,
		pselect: 72,
		ppoll: 73,
		epollPwait: 22
	}
};
const SUPPORTED_SYSCALL_TABLES = Object.values(SYSCALLS);
function linuxSyscallTables(arch) {
	const primary = SYSCALLS[arch];
	if (primary === void 0) return void 0;
	return [primary, ...SUPPORTED_SYSCALL_TABLES.filter((table) => table !== primary)];
}
function syscallWaitsOnStdin(internals, pid, tid, syscall, tables) {
	const [a0 = 0, a1 = 0, a2 = 0] = syscall.args;
	for (const table of tables) {
		if (syscall.number === table.read) return a0 === 0;
		if (syscall.number === table.select || syscall.number === table.pselect) return a0 >= 1 && fdSetHasStdin(internals, pid, a1);
		if (syscall.number === table.poll || syscall.number === table.ppoll) return a1 >= 1 && pollHasStdin(internals, pid, a0, a1);
		if (syscall.number === table.epollWait || syscall.number === table.epollPwait) return a2 >= 1 && epollHasStdin(internals, pid, tid, a0);
	}
	return false;
}
var PosixProcessInspector = class {
	internals;
	constructor(internals) {
		this.internals = internals;
	}
	signalGroup(pgid, signal) {
		this.internals.kill(-pgid, signal);
	}
	signalProcess(identity, signal) {
		if (this.isAlive(identity)) this.internals.kill(identity.pid, signal);
	}
};
function quiescent(state) {
	return state !== void 0 && /^[ZXx]$/.test(state);
}
var PosixProcessSnapshot = class {
	rows;
	byPid;
	constructor(rows) {
		this.rows = rows;
		this.byPid = new Map(rows.map((row) => [row.pid, row]));
	}
	tree(rootPid) {
		return processTree(this.rows, rootPid);
	}
	session(sessionId) {
		return this.rows.flatMap((row) => row.session === sessionId ? [{
			pid: row.pid,
			started: row.started
		}] : []);
	}
	alive(identity) {
		const row = this.byPid.get(identity.pid);
		return row?.started === identity.started && !quiescent(row.state);
	}
};
function processTree(entries, rootPid) {
	const root = new Map(entries.map((entry) => [entry.pid, entry])).get(rootPid);
	if (root === void 0) return [];
	const byParent = /* @__PURE__ */ new Map();
	for (const entry of entries) {
		const children = byParent.get(entry.parentPid) ?? [];
		children.push(entry);
		byParent.set(entry.parentPid, children);
	}
	const visited = /* @__PURE__ */ new Set();
	const result = [];
	const visit = (entry) => {
		if (visited.has(entry.pid)) return;
		visited.add(entry.pid);
		for (const child of byParent.get(entry.pid) ?? []) visit(child);
		result.push({
			pid: entry.pid,
			started: entry.started
		});
	};
	visit(root);
	return result;
}
var LinuxProcessInspector = class extends PosixProcessInspector {
	arch;
	constructor(arch, internals) {
		super(internals);
		this.arch = arch;
	}
	foregroundPgid(shellPid) {
		const tpgid = readLinuxStat(this.internals, shellPid)?.tpgid;
		return tpgid !== void 0 && tpgid > 0 ? tpgid : void 0;
	}
	isStdinWaiting(pgid, shellPid) {
		const tables = linuxSyscallTables(this.arch);
		if (tables === void 0) return false;
		const shell = readLinuxStat(this.internals, shellPid);
		if (shell === void 0) return false;
		const terminalDevice = readLinuxTerminalDevice(this.internals, shellPid, shell.ttyDevice);
		if (terminalDevice === void 0) return false;
		for (const pid of numericEntries(this.internals, "/proc")) {
			const process = readLinuxStat(this.internals, pid);
			if (process?.pgrp !== pgid) continue;
			for (const tid of numericEntries(this.internals, `/proc/${pid}/task`)) {
				const syscall = readSyscall(this.internals, pid, tid);
				if (syscall !== void 0 && syscallWaitsOnStdin(this.internals, pid, tid, syscall, tables) && readLinuxTerminalDevice(this.internals, pid, process.ttyDevice, tid) === terminalDevice) return true;
			}
		}
		return false;
	}
	isAlive(identity) {
		const stat = readLinuxStat(this.internals, identity.pid);
		return stat?.started === identity.started && !quiescent(stat.state);
	}
	snapshot() {
		return new PosixProcessSnapshot(numericEntries(this.internals, "/proc").flatMap((pid) => {
			const stat = readLinuxStat(this.internals, pid);
			return stat === void 0 ? [] : [{
				pid,
				parentPid: stat.parentPid,
				started: stat.started,
				session: stat.session,
				state: stat.state
			}];
		}));
	}
};
function macProcessTable(internals) {
	return internals.exec("/bin/ps", ["-axo", "pid=,ppid=,lstart="]).split("\n").flatMap((line) => {
		const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
		if (match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0) return [];
		return [{
			pid: Number(match[1]),
			parentPid: Number(match[2]),
			started: match[3],
			session: void 0,
			state: void 0
		}];
	});
}
var MacProcessInspector = class extends PosixProcessInspector {
	foregroundPgid(shellPid) {
		try {
			const value = Number(this.internals.exec("/bin/ps", [
				"-o",
				"tpgid=",
				"-p",
				String(shellPid)
			]).trim());
			return Number.isSafeInteger(value) && value > 0 ? value : void 0;
		} catch (_missingProcess) {
			return;
		}
	}
	isStdinWaiting(_pgid, _shellPid) {
		return false;
	}
	isAlive(identity) {
		return macProcessTable(this.internals).some((entry) => entry.pid === identity.pid && entry.started === identity.started);
	}
	snapshot() {
		return new PosixProcessSnapshot(macProcessTable(this.internals));
	}
};
/**
* Create the supported platform inspector or fail at plugin load.
* @param platform - target Node platform.
* @param arch - target CPU architecture for Linux syscall numbers.
* @param internals - filesystem/process boundary, injectable for deterministic tests.
* @returns Platform process inspector.
*/
function createProcessInspector(platform = process.platform, arch = process.arch, internals = DEFAULT_INTERNALS) {
	if (platform === "linux") return new LinuxProcessInspector(arch, internals);
	if (platform === "darwin") return new MacProcessInspector(internals);
	if (platform === "win32") return createWindowsProcessInspector();
	throw new Error(`subprocess-local: terminal inspection is unsupported on platform ${platform}`);
}
//#endregion
//#region lib/types/spawn.js
/**
* Process plumbing for the local subprocess service: detached process-tree
* spawn with per-stream stdio dispositions, tail-keep collection with spill
* files, tree-scoped signalling (POSIX groups; Windows taskkill), and the
* SIGTERM→SIGKILL escalation. This layer reacts to an abort signal; callers
* own deadlines, teardown ladders, and cause classification.
* @module dsh-subprocess-local/spawn
*/
/**
* Build a child environment: explicit caller entries override the scrubbed
* parent base using the target platform's environment-key semantics. A string
* deliberately restores or overrides an entry; an explicit `undefined`
* tombstone removes an ordinary ambient entry.
* @param extra - explicit caller entries and tombstones, merged after the scrub.
* @returns the environment to hand to `spawn` for the child process.
*/
function childEnv(extra) {
	const env = scrubbedParentEnv();
	if (process.platform !== "win32") return {
		...env,
		...extra
	};
	let entries = Object.entries(env);
	for (const [key, value] of Object.entries(extra ?? {})) {
		const normalized = key.toUpperCase();
		entries = entries.filter(([inherited]) => inherited.toUpperCase() !== normalized);
		entries.push([key, value]);
	}
	return Object.fromEntries(entries);
}
/**
* Liveness-poll cadence for tree-exit waits. The timer stays ref'd: an
* awaited teardown must keep the event loop alive until the tree really
* exits, or the parent can exit while claiming quiescence and orphan the
* survivors it promised to reap.
*/
function sleepTick() {
	return setTimeout$1(15);
}
let spillCounter = 0;
let defaultSpillDir;
/**
* The default spill location: a private (0700) per-process directory under
* the OS tmpdir, created lazily. Predictable world-readable paths would let
* other local users read command output or pre-create symlinks.
*/
function privateSpillDir() {
	defaultSpillDir ??= mkdtempSync(join(tmpdir(), "dsh-subprocess-"));
	return defaultSpillDir;
}
/**
* Collects one stream with a bounded in-memory tail. With a spill cap, on
* first overflow a spill file is created and every chunk (including those
* already collected) is appended there while the full stream remains within
* the cap; without one, only the in-memory tail is ever retained (the
* diagnostic-tail shape — a language server's stderr).
*
* Tail-keep rationale (pi/OpenCode): errors and final results cluster at the
* end of command output; the spill file covers the head.
*/
var OutputCollector = class {
	maxBytes;
	maxSpillBytes;
	label;
	spillDir;
	chunks = [];
	bytes = 0;
	dropped = false;
	spillFd;
	spillFile;
	spillDisabled;
	/** Total bytes ever pushed (not just retained). */
	total = 0;
	constructor(maxBytes, maxSpillBytes, label, spillDir) {
		this.maxBytes = maxBytes;
		this.maxSpillBytes = maxSpillBytes;
		this.label = label;
		this.spillDir = spillDir;
		this.spillDisabled = maxSpillBytes === void 0;
	}
	/**
	* Ingest one stream chunk, counting it toward the whole-stream total. On
	* first overflow of the in-memory cap a spill file is opened (when spilling
	* is enabled) and every chunk (already-collected ones included) is appended
	* there from then on; the in-memory tail then drops whole chunks from its
	* head (or the head of a single over-cap chunk) until it fits the cap again.
	* @param chunk - the raw bytes from one stream 'data' event.
	*/
	push(chunk) {
		this.total += chunk.length;
		const overflows = this.bytes + chunk.length > this.maxBytes;
		if (!this.spillDisabled && (overflows || this.spillFd !== void 0)) this.spillAll(chunk);
		this.chunks.push(chunk);
		this.bytes += chunk.length;
		while (this.bytes > this.maxBytes) {
			const head = this.chunks[0];
			const excess = this.bytes - this.maxBytes;
			if (head.length <= excess) {
				this.chunks.shift();
				this.bytes -= head.length;
			} else {
				this.chunks[0] = head.subarray(excess);
				this.bytes -= excess;
			}
			this.dropped = true;
		}
	}
	/** Open the spill file lazily and append `chunk` (and any prior chunks once). */
	spillAll(chunk) {
		if (this.maxSpillBytes !== void 0 && this.total > this.maxSpillBytes) {
			this.discardSpill();
			return;
		}
		if (this.spillFd === void 0) {
			this.spillFile = join(this.spillDir, `dsh-subprocess-${process.pid}-${++spillCounter}-${randomBytes(6).toString("hex")}-${this.label}.log`);
			this.spillFd = openSync(this.spillFile, "wx", 384);
			for (const prior of this.chunks) writeSync(this.spillFd, prior);
		}
		writeSync(this.spillFd, chunk);
	}
	/** Stop spilling and remove the file once it can no longer hold the complete stream. */
	discardSpill() {
		const fd = this.spillFd;
		const file = this.spillFile;
		this.spillFd = void 0;
		this.spillFile = void 0;
		this.spillDisabled = true;
		if (fd !== void 0) try {
			closeSync(fd);
		} catch {
			this.spillFd = fd;
		}
		if (file !== void 0) try {
			unlinkSync(file);
		} catch {}
	}
	/**
	* Incremental read in whole-stream byte coordinates: returns everything
	* pushed since `fromByte`. When `fromByte` has already slid out of the
	* in-memory tail window, the read is `lossy` — it returns the whole
	* retained tail and the gap is only recoverable from the spill file.
	* @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
	* @returns the delta text, the offset for the next read, the `lossy` flag, and the spill path when one was created.
	*/
	readFrom(fromByte) {
		const windowStart = this.total - this.bytes;
		const buffer = Buffer.concat(this.chunks);
		const lossy = fromByte < windowStart;
		return {
			text: (lossy ? buffer : buffer.subarray(fromByte - windowStart)).toString("utf8"),
			nextOffset: this.total,
			lossy,
			...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
		};
	}
	/**
	* Close the spill file once the stream has ended. A failed close (delayed
	* writeback fault) stops advertising the spill path — the file may be
	* missing its tail — while every in-memory read keeps working. Idempotent;
	* the spawn path seals both collectors at settlement so reads after exit
	* never point at a still-open file.
	*/
	seal() {
		if (this.spillFd === void 0) return;
		try {
			closeSync(this.spillFd);
		} catch {
			this.spillFile = void 0;
		}
		this.spillFd = void 0;
	}
	/**
	* Seal the spill file and return the final output.
	* @returns the final collected output: tail text, truncation flag, and the spill path when intact.
	*/
	finalize() {
		this.seal();
		return {
			text: Buffer.concat(this.chunks).toString("utf8"),
			truncated: this.dropped,
			...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
		};
	}
};
/**
* Terminate one Windows process tree with `taskkill /T /F`. Contained like
* POSIX group signalling — delivery races tree exit, so an absent tree, a
* nonzero status, or a missing taskkill binary must not break idempotent
* teardown.
* @param pid - root process id; non-positive is a no-op.
*/
function taskkillProcessTree(pid) {
	if (pid <= 0) return;
	spawnSync("taskkill", [
		"/PID",
		String(pid),
		"/T",
		"/F"
	], { stdio: "ignore" });
}
/**
* Signal a detached process tree with platform-correct semantics: POSIX
* signals the negative process-group id and falls back to the direct child
* when the group is gone; Windows terminates the tree via taskkill (any
* signal value force-terminates — Node maps signals to TerminateProcess).
*/
function signalTree(platform, pid, sig, child, taskkill) {
	if (platform === "win32") {
		taskkill(pid);
		return;
	}
	/* v8 ignore next -- kill/terminate gate on treeAlive(), which is false for pid -1; this guard protects direct callers only. */
	if (pid <= 0) return;
	try {
		process.kill(-pid, sig);
	} catch {
		/* v8 ignore start -- the fallback needs a live child whose group signal fails
		(EPERM-style), which POSIX CI cannot stage; the swallow keeps teardown idempotent. */
		try {
			child.kill(sig);
		} catch {}
	}
}
/**
* Spawn one isolated detached process tree with the spec's per-stream stdio
* dispositions. Runtime exits resolve `done` as {@link SubprocessOutcome};
* only spawn failures reject.
* @param spec - fully resolved argv, cwd, stdio, grace, cancellation, environment.
* @param internals - test-only spill-directory, platform, and taskkill overrides.
* @returns live subprocess handle.
* @throws when `graceMs` cannot be represented by one Node timer.
*/
function spawnSubprocess(spec, internals = {}) {
	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}`);
	const spillDir = internals.spillDir ?? privateSpillDir();
	const platform = internals.platform ?? process.platform;
	const taskkill = internals.taskkill ?? taskkillProcessTree;
	const linuxGroupHasLiveMembers = internals.linuxProcessGroupHasLiveMembers ?? linuxProcessGroupHasLiveMembers;
	if (spec.signal?.aborted) throw new Error(`aborted before spawn: ${String(spec.signal.reason ?? "aborted")}`);
	const [program, ...args] = spec.argv;
	if (program === void 0 || program.length === 0) throw new Error("invalid argv: expected a non-empty program name at argv[0]");
	const isCollect = (mode) => mode !== "pipe" && mode !== "inherit";
	const outMode = spec.stdio.stdout;
	const errMode = spec.stdio.stderr;
	const stdinMode = spec.stdio.stdin;
	const env = childEnv(spec.env);
	const child = spawn(program, args, {
		cwd: spec.cwd,
		env,
		stdio: [
			stdinMode === "ignore" ? "ignore" : "pipe",
			outMode === "inherit" ? "inherit" : "pipe",
			errMode === "inherit" ? "inherit" : "pipe"
		],
		detached: platform !== "win32"
	});
	const collectStream = (mode, stream, label) => {
		if (!isCollect(mode) || stream === null) return void 0;
		const collector = new OutputCollector(mode.maxBytes, mode.spill?.maxBytes, label, spillDir);
		stream.on("data", (chunk) => {
			collector.push(chunk);
		});
		return collector;
	};
	const stdoutCollector = collectStream(outMode, child.stdout, "stdout");
	const stderrCollector = collectStream(errMode, child.stderr, "stderr");
	let graceTimer;
	let treeExitObserved = false;
	let treeExitObservation;
	let settled = false;
	const pid = child.pid ?? -1;
	/** Whether the detached tree's root (or POSIX group) is still alive. */
	const treeAlive = () => {
		/* v8 ignore next -- only a timer callback already queued when the observer settles can enter here;
		the guard is the final defense against probing an id after its tree was confirmed absent. */
		if (treeExitObserved) return false;
		if (pid <= 0) return false;
		if (platform === "win32") return child.exitCode === null && child.signalCode === null;
		try {
			process.kill(-pid, 0);
			if (settled && platform === "linux" && linuxGroupHasLiveMembers(pid) === false) return false;
			return true;
		} catch (error) {
			const code = error.code;
			/* v8 ignore next 2 -- POSIX reports an absent group as ESRCH; child-reaping timing
			makes observing the other arm platform-dependent. */
			if (code === "ESRCH") return false;
			/* v8 ignore start -- EPERM and non-POSIX negative-pid failures are platform defenses; CI runs
			tree-lifecycle tests on POSIX hosts where absence reports ESRCH. */
			if (code === "EPERM") return true;
			return child.exitCode === null && child.signalCode === null;
		}
	};
	/**
	* Start or reuse the handle's single whole-tree exit observer. The first
	* confirmed absence is a permanent no-more-signals boundary: it cancels a
	* pending escalation before this process-group id can be reused.
	*/
	const observeTreeExit = () => {
		treeExitObservation ??= (async () => {
			while (treeAlive()) await sleepTick();
			treeExitObserved = true;
			if (graceTimer !== void 0) clearTimeout(graceTimer);
			graceTimer = void 0;
		})();
		return treeExitObservation;
	};
	const kill = (sig) => {
		/* v8 ignore next -- the shared exit observer cancels the ordinary dead-tree timer;
		this remains the timer/death race guard and cannot be staged deterministically. */
		if (!treeAlive()) return;
		signalTree(platform, pid, sig, child, taskkill);
	};
	const terminate = () => {
		if (treeExitObserved || graceTimer !== void 0) return;
		observeTreeExit();
		if (treeExitObserved) return;
		kill("SIGTERM");
		graceTimer = setTimeout(() => {
			kill("SIGKILL");
		}, spec.graceMs);
	};
	const terminateForHostExit = () => {
		kill("SIGKILL");
	};
	const onAbort = () => {
		terminate();
	};
	spec.signal?.addEventListener("abort", onAbort, { once: true });
	if (typeof stdinMode === "object" && child.stdin !== null) {
		child.stdin.on("error", () => {});
		child.stdin.end(stdinMode.data);
	}
	const done = new Promise((resolve, reject) => {
		let pipeDrainTimer;
		const settle = (exitCode, signal) => {
			if (settled) return;
			settled = true;
			if (stdoutCollector !== void 0) child.stdout?.destroy();
			if (stderrCollector !== void 0) child.stderr?.destroy();
			stdoutCollector?.seal();
			stderrCollector?.seal();
			cleanup();
			resolve({
				exitCode,
				signal
			});
		};
		child.on("error", (error) => {
			settled = true;
			cleanup();
			reject(error);
		});
		child.on("exit", (exitCode, signal) => {
			pipeDrainTimer = setTimeout(() => {
				settle(exitCode, signal);
			}, spec.graceMs);
		});
		child.on("close", settle);
		function cleanup() {
			if (pipeDrainTimer !== void 0) clearTimeout(pipeDrainTimer);
			spec.signal?.removeEventListener("abort", onAbort);
		}
	});
	const waitForExit = async (signal) => {
		const observed = observeTreeExit();
		if (treeExitObserved) return true;
		if (signal?.aborted) return false;
		if (signal === void 0) {
			await observed;
			return true;
		}
		const aborted = Promise.withResolvers();
		const onAbort = () => {
			aborted.resolve(false);
		};
		signal.addEventListener("abort", onAbort, { once: true });
		/* v8 ignore next -- closes the event-loop race between the preceding aborted check and listener registration. */
		if (signal.aborted) onAbort();
		try {
			return await Promise.race([observed.then(() => true), aborted.promise]);
		} finally {
			signal.removeEventListener("abort", onAbort);
		}
	};
	return {
		pid,
		/* v8 ignore start -- pipe-mode fds exist on every spawn Node returns; the null-coalesces guard a nonconforming ChildProcess only. */
		stdin: stdinMode === "pipe" ? child.stdin ?? void 0 : void 0,
		stdout: outMode === "pipe" ? child.stdout ?? void 0 : void 0,
		stderr: errMode === "pipe" ? child.stderr ?? void 0 : void 0,
		/* v8 ignore stop */
		collected: {
			...stdoutCollector !== void 0 ? { stdout: stdoutCollector } : {},
			...stderrCollector !== void 0 ? { stderr: stderrCollector } : {}
		},
		done,
		terminate,
		terminateForHostExit,
		waitForExit
	};
}
//#endregion
//#region lib/types/terminal.js
/** Local node-pty terminal-process implementation for the subprocess seam. */
function delay(ms) {
	return new Promise((resolve) => setTimeout(resolve, ms));
}
function signalName(number) {
	if (number === void 0 || number === 0) return null;
	for (const [name, value] of Object.entries(constants$1.signals)) if (value === number) return name;
	return null;
}
/**
* A local terminal whose process-session ownership stays below the PTY backend.
* The seam's terminate() promise — no write, inspection, or signal in flight
* after settlement — holds here without operation tracking only because every
* handle call completes synchronously under the hood (node-pty write, ps-based
* inspection). A first genuinely asynchronous step in any handle call must add
* the tracking a remote provider needs.
*/
var LocalTerminalHandle = class {
	terminal;
	inspector;
	graceMs;
	platform;
	pid;
	output = new PassThrough();
	done;
	outcome = Promise.withResolvers();
	dataDisposable;
	exitDisposable;
	cleanup;
	exited = false;
	trackedDescendants = [];
	/** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */
	rootIdentity;
	/**
	* @param terminal - allocated node-pty process.
	* @param inspector - platform process/session operations.
	* @param graceMs - TERM-to-KILL and exit-wait grace.
	* @param platform - host platform; defaults to the running platform, injectable for deterministic tests.
	*/
	constructor(terminal, inspector, graceMs, platform = process.platform) {
		this.terminal = terminal;
		this.inspector = inspector;
		this.graceMs = graceMs;
		this.platform = platform;
		this.pid = terminal.pid;
		this.rootIdentity = inspector.snapshot().tree(this.pid).find((member) => member.pid === this.pid);
		this.done = this.outcome.promise;
		this.dataDisposable = terminal.onData((data) => {
			this.output.write(Buffer$1.from(data, "utf8"));
		});
		this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
			if (this.exited) return;
			this.exited = true;
			this.output.end();
			this.outcome.resolve({
				exitCode: exitSignal === void 0 || exitSignal === 0 ? exitCode : null,
				signal: signalName(exitSignal)
			});
		});
	}
	async write(data) {
		if (this.exited) throw new Error("terminal process has exited");
		this.terminal.write(data);
	}
	async inspectForeground() {
		this.descendants(this.inspector.snapshot());
		const processGroupId = this.inspector.foregroundPgid(this.pid);
		if (processGroupId === void 0) return void 0;
		return {
			processGroupId,
			inputWaiting: this.inspector.isStdinWaiting(processGroupId, this.pid)
		};
	}
	async signalForeground(signal) {
		const foreground = await this.inspectForeground();
		if (foreground === void 0) throw new Error(`cannot resolve foreground process group for terminal ${this.pid}`);
		if (signal === "SIGKILL" && foreground.processGroupId === this.pid) throw new Error("refusing to SIGKILL the terminal shell; terminate the terminal session instead");
		if (this.platform === "win32") {
			if (signal === "SIGINT") {
				this.terminal.write("");
				return foreground.processGroupId;
			}
			if (signal === "SIGTSTP" || signal === "SIGHUP") throw new Error(`signal ${signal} is unsupported on Windows; only SIGINT, SIGTERM, and SIGKILL are available`);
		}
		this.inspector.signalGroup(foreground.processGroupId, signal);
		return foreground.processGroupId;
	}
	terminate() {
		if (this.cleanup !== void 0) return this.cleanup;
		const cleanup = this.closeOnce();
		this.cleanup = cleanup;
		cleanup.catch(() => {
			this.cleanup = void 0;
		});
		return cleanup;
	}
	/**
	* Force-terminate the observable session synchronously during Node's exit
	* event. This does not claim quiescence and does not replace terminate().
	*/
	terminateForHostExit() {
		this.forceStopDescendants();
		this.forceStopShell();
		this.forceStopDescendants();
	}
	forceStopShell() {
		if (this.exited) return;
		if (this.rootIdentity !== void 0) {
			try {
				this.inspector.signalProcess(this.rootIdentity, "SIGKILL");
			} catch (_rootExitedDuringHostExit) {}
			return;
		}
		try {
			this.terminal.kill("SIGKILL");
		} catch (_unidentifiedShellExitedDuringHostExit) {}
	}
	survivors(members, observed) {
		return members.filter((member) => observed.alive(member));
	}
	descendants(observed) {
		const tree = observed.tree(this.pid);
		const root = tree.find((member) => member.pid === this.pid);
		const rootVerified = this.rootIdentity !== void 0 && root !== void 0 && root.started === this.rootIdentity.started;
		this.trackedDescendants = this.survivors(this.unionMembers(this.trackedDescendants, ...rootVerified ? [tree, observed.session(this.pid)] : []).filter((member) => member.pid !== this.pid), observed);
		return this.trackedDescendants;
	}
	async waitForMembers(members) {
		if (members.length === 0) return [];
		const until = Date.now() + this.graceMs;
		let survivors = this.survivors(members, this.inspector.snapshot());
		while (survivors.length > 0 && Date.now() < until) {
			await delay(Math.min(25, Math.max(1, until - Date.now())));
			survivors = this.survivors(members, this.inspector.snapshot());
		}
		return survivors;
	}
	signalMembers(members, signal) {
		for (const member of members) try {
			this.inspector.signalProcess(member, signal);
		} catch (_alreadyExitedDuringSignal) {}
	}
	forceStopDescendants() {
		let members = this.trackedDescendants;
		try {
			members = this.descendants(this.inspector.snapshot());
		} catch (_processTableUnavailableDuringHostExit) {}
		this.signalMembers(members, "SIGKILL");
	}
	unionMembers(...groups) {
		const members = [];
		const seen = /* @__PURE__ */ new Set();
		for (const group of groups) for (const member of group) {
			const key = `${member.pid}:${member.started}`;
			if (seen.has(key)) continue;
			seen.add(key);
			members.push(member);
		}
		return members;
	}
	async stopDescendants() {
		const captured = this.descendants(this.inspector.snapshot());
		this.signalMembers(captured, "SIGTERM");
		const capturedSurvivors = await this.waitForMembers(captured);
		const members = this.unionMembers(capturedSurvivors, this.descendants(this.inspector.snapshot()));
		this.signalMembers(members, "SIGKILL");
		const survivors = await this.waitForMembers(members);
		const observed = this.inspector.snapshot();
		return this.survivors(this.unionMembers(survivors, this.descendants(observed)), observed);
	}
	async stopShell() {
		if (this.platform === "win32") {
			await this.stopShellWindows();
			return;
		}
		if (!this.exited) {
			try {
				this.terminal.kill("SIGTERM");
			} catch (_topLevelAlreadyExitedDuringTerm) {}
			await Promise.race([this.done.then(() => void 0), delay(this.graceMs)]);
		}
		if (!this.exited) {
			try {
				this.terminal.kill("SIGKILL");
			} catch (_topLevelAlreadyExitedDuringKill) {}
			await Promise.race([this.done.then(() => void 0), delay(this.graceMs)]);
		}
		if (!this.exited) throw new Error(`terminal cleanup failed; surviving pid: ${this.pid}`);
	}
	async stopShellWindows() {
		const shellGone = () => this.exited || this.rootIdentity !== void 0 && !this.inspector.isAlive(this.rootIdentity);
		if (!shellGone() && this.rootIdentity !== void 0) {
			this.inspector.signalProcess(this.rootIdentity, "SIGTERM");
			await this.waitForWindowsShellExit();
		}
		if (!shellGone() && this.rootIdentity === void 0) {
			try {
				this.terminal.kill();
			} catch (_topLevelAlreadyExitedDuringKill) {}
			await Promise.race([this.done.then(() => void 0), delay(this.graceMs)]);
		}
		if (!shellGone() && this.rootIdentity !== void 0) {
			this.inspector.signalProcess(this.rootIdentity, "SIGKILL");
			await this.waitForWindowsShellExit();
		}
		if (!shellGone()) throw new Error(`terminal cleanup failed; surviving pid: ${this.pid}`);
	}
	async waitForWindowsShellExit() {
		const until = Date.now() + this.graceMs;
		while (!this.exited && Date.now() < until) {
			if (this.rootIdentity !== void 0 && !this.inspector.isAlive(this.rootIdentity)) return;
			await delay(Math.min(25, Math.max(1, until - Date.now())));
		}
	}
	async closeOnce() {
		let survivors = await this.stopDescendants();
		if (survivors.length > 0) throw new Error(`terminal cleanup failed; surviving pids: ${survivors.map((member) => member.pid).join(", ")}`);
		await this.stopShell();
		survivors = await this.stopDescendants();
		if (survivors.length > 0) throw new Error(`terminal cleanup failed; surviving pids: ${survivors.map((member) => member.pid).join(", ")}`);
		this.settleExitIfGone();
		this.dataDisposable.dispose();
		this.exitDisposable.dispose();
	}
	settleExitIfGone() {
		if (this.platform !== "win32") return;
		if (this.exited) return;
		/* v8 ignore next -- stopShellWindows() verified the shell is gone or threw;
		the identity re-check is a defensive fence for a future caller. */
		if (this.rootIdentity !== void 0 && this.inspector.isAlive(this.rootIdentity)) return;
		this.exited = true;
		this.output.end();
		this.outcome.resolve({
			exitCode: null,
			signal: null
		});
	}
};
//#endregion
//#region lib/types/index.js
/**
* Local Service Provider for the subprocess capability seam. Each spawn is a detached
* process tree with the spec's per-stream stdio dispositions. Normal disposal
* terminates and joins live trees; Node's synchronous exit phase force-stops
* any trees the service still owns. It has no config: every disposition and
* limit arrives on the spec, so the deployment-varying choices stay with the
* caller's config (the bash executor's, the LSP host's, …).
* @module @deepseek-ai/dsh-subprocess-local
*/
/**
* Local subprocess service: detached process trees, Node-shaped stdio
* dispositions (raw pipes, inherit, bounded tail-keep collection with spill
* files), credential-scrubbed environment, and tree-scoped signalling with
* SIGTERM→grace→SIGKILL escalation, plus synchronous final termination during
* JavaScript-observable host exit.
*/
var LocalSubprocessRuntime = class extends SubprocessRuntime {
	/** Live handles retained for normal disposal and synchronous host-exit finalization. */
	live = /* @__PURE__ */ new Set();
	/** Live terminals retained through normal quiescence or host-exit finalization. */
	terminals = /* @__PURE__ */ new Set();
	/** Test hook: spill and platform knobs forwarded to spawnSubprocess. */
	internals = {};
	/** Test hook for platform process inspection; production resolves lazily on terminal spawn. */
	terminalInspector;
	constructor(ctx) {
		super(ctx);
		ctx.effect(() => {
			const onHostExit = () => {
				this.terminateForHostExit();
			};
			process.prependListener("exit", onHostExit);
			return async () => {
				try {
					await this.disposeManagedProcesses();
				} finally {
					process.off("exit", onHostExit);
				}
			};
		}, "local subprocess teardown");
	}
	terminateForHostExit() {
		for (const handle of this.live) try {
			handle.terminateForHostExit();
		} catch (_ordinaryTreeTerminationFailed) {}
		for (const terminal of this.terminals) try {
			terminal.terminateForHostExit();
		} catch (_terminalTerminationFailed) {}
	}
	async disposeManagedProcesses() {
		const pending = [];
		for (const handle of this.live) {
			handle.terminate();
			pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()));
		}
		for (const terminal of this.terminals) pending.push(terminal.terminate());
		const failures = (await Promise.allSettled(pending)).flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
		if (failures.length > 0) this.terminateForHostExit();
		this.live.clear();
		this.terminals.clear();
		if (failures.length === 1) throw failures[0];
		if (failures.length > 1) throw new AggregateError(failures, "local subprocess teardown failed");
	}
	async resolveExecutable(command, env, signal) {
		if (command.length === 0) throw new Error("subprocess-local: executable must be non-empty");
		signal?.throwIfAborted();
		const environment = childEnv(env);
		const absolute = isAbsolute(command);
		if (!absolute && (command.includes("/") || process.platform === "win32" && command.includes("\\"))) throw new Error(`subprocess-local: command ${JSON.stringify(command)} is a relative path; use an absolute path or a bare PATH name`);
		const candidates = absolute ? [command] : this.executableCandidates(command, environment);
		for (const candidate of candidates) {
			signal?.throwIfAborted();
			try {
				if (!(await stat(candidate)).isFile()) continue;
				await access(candidate, constants.X_OK);
				signal?.throwIfAborted();
				return candidate;
			} catch {}
		}
		signal?.throwIfAborted();
		throw new Error(absolute ? `subprocess-local: command ${JSON.stringify(command)} is not an executable file` : `subprocess-local: command ${JSON.stringify(command)} was not found on PATH`);
	}
	executableCandidates(command, env) {
		const path = environmentValue(env, "PATH") ?? "";
		const extensions = process.platform === "win32" && extname(command) === "" ? (environmentValue(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
		return path.split(delimiter).flatMap((directory) => extensions.map((extension) => resolve(process.cwd(), directory, command + extension)));
	}
	spawn(spec) {
		const handle = spawnSubprocess(spec, this.internals);
		this.live.add(handle);
		const release = () => handle.waitForExit().then(() => {
			this.live.delete(handle);
		});
		handle.done.then(release, release);
		return handle;
	}
	async spawnTerminal(spec) {
		const file = spec.argv[0];
		if (file === void 0 || file.length === 0) throw new Error("subprocess-local: terminal argv must contain a program");
		spec.signal?.throwIfAborted();
		const options = {
			name: "dumb",
			rows: spec.rows,
			cols: spec.cols,
			cwd: spec.cwd,
			env: childEnv(spec.env)
		};
		const inspector = this.terminalInspector ?? createProcessInspector();
		const handle = new LocalTerminalHandle(nodePty.spawn(file, [...spec.argv.slice(1)], options), inspector, spec.graceMs);
		this.terminals.add(handle);
		const release = async () => {
			await handle.terminate();
			this.terminals.delete(handle);
		};
		handle.done.then(release, release).catch(() => {});
		return handle;
	}
};
/** Read a Windows environment key using the platform's case-insensitive semantics. */
function environmentValue(env, name) {
	const exact = env[name];
	if (exact !== void 0 || process.platform !== "win32") return exact;
	const normalized = name.toUpperCase();
	return Object.entries(env).find(([key]) => key.toUpperCase() === normalized)?.[1];
}
//#endregion
export { LocalSubprocessRuntime, LocalSubprocessRuntime as default };
