import { dirname, resolve } from "node:path";
import { createWriteStream } from "node:fs";
import { spawn } from "node:child_process";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
//#region src/contexts/process.ts
/**
* Whether the host supports POSIX process groups (the `detached: true` +
* `process.kill(-pid)` combination). Windows doesn't — its job-object
* model is shaped differently — so on win32 we fall back to killing the
* shell wrapper alone (matches pre-fix behavior; better than nothing).
*/
const SUPPORTS_PROCESS_GROUPS = process.platform !== "win32";
/**
* Default cap on captured stdout / stderr per child. Matches the
* pre-fix `child_process.exec` setting so existing callers see the
* same buffer envelope. Output beyond this is truncated and a
* marker is appended to stderr.
*/
const DEFAULT_MAX_BUFFER = 10 * 1024 * 1024;
/**
* How long `destroy()` waits for a SIGTERM'd background task to settle
* before escalating to SIGKILL and abandoning the wait. Matches the MCP
* connection's default `closeTimeout` so `agent.destroy()` stays bounded
* by a single grace period regardless of which leg is slowest.
*/
const DESTROY_SIGTERM_GRACE_MS = 5e3;
/**
* How long after the child's `exit` event we keep waiting for `close`
* before settling with the output collected so far.
*
* `close` fires only once every stdio pipe has closed — and a
* daemonized grandchild that inherited stdout/stderr (a dev server
* started with `&`, `nohup` without redirection, simulator helpers, …)
* keeps those pipes open indefinitely. Pre-fix, a foreground `exec`
* whose command finished in milliseconds would hang until its timeout,
* then `killProcessGroup` took the intentionally-backgrounded daemon
* down with it. The process itself is done at `exit`; this grace only
* exists to let normally-closing pipes flush their tail bytes.
*/
const EXIT_PIPE_DRAIN_GRACE_MS = 1500;
/**
* Sanitize a task id before it's joined into a filesystem path.
*
* We mint `bash_<n>` ids ourselves (no user input flows into the path
* for `ProcessContext`), so this is defensive — but third-party contexts
* MAY accept caller-provided ids, so the helper exists for them too.
* Anything that doesn't match the expected shape is rejected — never
* coerced — so the call site sees a clear error rather than a
* traversal-shaped path.
*/
const TASK_ID_RE = /^[a-z][\w-]*$/i;
function assertSafeTaskId(taskId) {
	if (!TASK_ID_RE.test(taskId)) throw new Error(`Invalid task id "${taskId}" — must match ${TASK_ID_RE}.`);
}
/**
* Format `date` as `YYYYMMDD-HHMMSS-mmm` in UTC.
*
* Pinned to UTC so the lexical sort of two timestamps always matches
* their chronological order (local time + DST does not). Used as the
* per-context suffix on background-task log filenames; see the field
* doc on `contextTimestamp` for the why.
*/
function formatContextTimestamp(date) {
	const pad2 = (n) => n.toString().padStart(2, "0");
	const pad3 = (n) => n.toString().padStart(3, "0");
	return `${date.getUTCFullYear()}${pad2(date.getUTCMonth() + 1)}${pad2(date.getUTCDate())}-${pad2(date.getUTCHours())}${pad2(date.getUTCMinutes())}${pad2(date.getUTCSeconds())}-${pad3(date.getUTCMilliseconds())}`;
}
function createProcessContext(config) {
	let counter = 0;
	const handles = /* @__PURE__ */ new Map();
	const defaultCwd = config?.cwd ?? process.cwd();
	const defaultEnv = config?.env;
	const destroyGraceMs = config?.destroyGraceMs ?? DESTROY_SIGTERM_GRACE_MS;
	/**
	* Per-context background-task registry. Entries live for the context's
	* lifetime — even after the child exits — so the model can read output
	* of completed tasks until `destroy()` tears everything down. Kept as
	* a plain Map (not a class) per code-quality checklist #7: no premature
	* abstraction. The state and the operations on it live inline.
	*/
	const tasks = /* @__PURE__ */ new Map();
	let taskCounter = 0;
	/**
	* Per-context UTC timestamp segment baked into every background task's
	* log filename. Same `taskCounter` value across two contexts (e.g. a
	* TUI restart) would otherwise re-open the SAME `bash_<n>.log` file —
	* we open with `flags: 'a'` so the new task would APPEND into the old
	* log, producing scrambled output. The timestamp guarantees each
	* context owns a distinct log filename without forcing the
	* model-facing task id (`bash_<n>`) to grow longer.
	*
	* Format: `YYYYMMDD-HHMMSS-mmm` in UTC.
	*   - Sortable (lexical sort = chronological sort).
	*   - Unambiguous (UTC sidesteps DST / locale shifts).
	*   - Filesystem-safe (digits + hyphens only).
	*   - Millisecond precision avoids same-second-restart collisions.
	*
	* The timestamp is computed once per context, NOT per task — all of a
	* context's tasks share the same suffix so a directory listing groups
	* cleanly by "which run produced these".
	*/
	const contextTimestamp = formatContextTimestamp(/* @__PURE__ */ new Date());
	/**
	* Last-resort orphan reaper. With `detached: true`, background tasks
	* are in their OWN process group — when the parent (zidane TUI) dies,
	* the OS does NOT send them SIGHUP and they keep running indefinitely.
	* The user's "Ctrl+C the TUI" intent is "stop everything I started",
	* not "leak a `sleep 60` into the background".
	*
	* `process.on('exit')` fires SYNCHRONOUSLY on `process.exit()` AND on
	* natural shutdown — exactly the seam we need. The handler can only
	* do synchronous work (Node ignores async), but `process.kill` IS
	* synchronous, so a SIGTERM-the-group sweep lands cleanly. The kill
	* is best-effort: already-dead children throw ESRCH (swallowed).
	*
	* Registered lazily on first `execBackground` so contexts that never
	* background a task don't pay the listener cost. Deregistered in
	* `destroy()` so reconstructed contexts don't accumulate listeners
	* (Node warns past 10 — a long-running session that switches sessions
	* frequently would otherwise hit that).
	*/
	let exitHandlerRegistered = false;
	const exitHandler = () => {
		for (const task of tasks.values()) {
			if (task.status !== "running") continue;
			const pid = task.child.pid;
			if (pid === void 0) continue;
			try {
				if (SUPPORTS_PROCESS_GROUPS) process.kill(-pid, "SIGTERM");
				else process.kill(pid, "SIGTERM");
			} catch {}
		}
	};
	return {
		type: "process",
		capabilities: {
			shell: true,
			filesystem: true,
			network: true,
			gpu: false,
			detachedTasks: "process-lifetime"
		},
		async spawn(overrides) {
			const id = `process-${++counter}`;
			const cwd = overrides?.cwd ?? defaultCwd;
			await mkdir(cwd, { recursive: true });
			const handle = {
				id,
				type: "process",
				cwd
			};
			handles.set(id, handle);
			return handle;
		},
		async exec(handle, command, options) {
			const cwd = options?.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
			if (options?.signal?.aborted) return {
				stdout: "",
				stderr: "aborted by signal before spawn",
				exitCode: 143
			};
			const timeoutMs = (options?.timeout ?? config?.limits?.timeout ?? 30) * 1e3;
			const maxBuffer = DEFAULT_MAX_BUFFER;
			return new Promise((resolveP) => {
				const child = spawn("/bin/sh", ["-c", command], {
					cwd,
					env: {
						...process.env,
						...defaultEnv,
						...options?.env
					},
					stdio: [
						"ignore",
						"pipe",
						"pipe"
					],
					detached: SUPPORTS_PROCESS_GROUPS
				});
				let stdout = "";
				let stderr = "";
				let stdoutBytes = 0;
				let stderrBytes = 0;
				let bufferTruncated = false;
				let timedOut = false;
				let killedByAbort = false;
				let settled = false;
				const appendCapped = (slot, chunk) => {
					const used = slot === "stdout" ? stdoutBytes : stderrBytes;
					if (used >= maxBuffer) {
						if (!bufferTruncated) {
							bufferTruncated = true;
							killProcessGroup(child, "SIGTERM");
						}
						return;
					}
					const room = maxBuffer - used;
					let piece = chunk;
					if (chunk.length > room) {
						let end = room;
						while (end > 0 && (chunk[end] & 192) === 128) end--;
						piece = chunk.subarray(0, end);
					}
					if (slot === "stdout") {
						stdout += piece.toString("utf8");
						stdoutBytes += piece.length;
					} else {
						stderr += piece.toString("utf8");
						stderrBytes += piece.length;
					}
					if (chunk.length > room) {
						bufferTruncated = true;
						killProcessGroup(child, "SIGTERM");
					}
				};
				child.stdout?.on("data", (chunk) => appendCapped("stdout", chunk));
				child.stderr?.on("data", (chunk) => appendCapped("stderr", chunk));
				const timeoutTimer = timeoutMs > 0 ? setTimeout(() => {
					timedOut = true;
					killProcessGroup(child, "SIGTERM");
				}, timeoutMs) : void 0;
				const onAbort = () => {
					killedByAbort = true;
					killProcessGroup(child, "SIGTERM");
				};
				const userSignal = options?.signal;
				if (userSignal) userSignal.addEventListener("abort", onAbort, { once: true });
				let exitGraceTimer;
				const settle = (exitCode, extraStderr) => {
					if (settled) return;
					settled = true;
					if (timeoutTimer) clearTimeout(timeoutTimer);
					if (exitGraceTimer) clearTimeout(exitGraceTimer);
					if (userSignal) userSignal.removeEventListener("abort", onAbort);
					const finalStderr = extraStderr ? stderr ? `${stderr}\n${extraStderr}` : extraStderr : stderr;
					resolveP({
						stdout,
						stderr: finalStderr,
						exitCode
					});
				};
				const settleFromStatus = (code, signal) => {
					if (killedByAbort) {
						settle(143, "aborted by signal");
						return;
					}
					if (timedOut) {
						settle(124, `command timed out after ${timeoutMs}ms`);
						return;
					}
					if (bufferTruncated) {
						settle(143, `output exceeded ${maxBuffer}-byte buffer; process killed`);
						return;
					}
					if (signal) {
						settle(143, `terminated by signal ${signal}`);
						return;
					}
					settle(typeof code === "number" ? code : 1);
				};
				child.on("error", (err) => {
					settle(1, err.message);
				});
				child.on("close", (code, signal) => settleFromStatus(code, signal));
				child.on("exit", (code, signal) => {
					exitGraceTimer = setTimeout(() => {
						if (settled) return;
						child.stdout?.destroy();
						child.stderr?.destroy();
						settleFromStatus(code, signal);
					}, EXIT_PIPE_DRAIN_GRACE_MS);
					exitGraceTimer.unref?.();
				});
			});
		},
		async readFile(handle, path) {
			return readFile(resolve(handle.cwd, path), "utf-8");
		},
		async readFileBinary(handle, path) {
			const buf = await readFile(resolve(handle.cwd, path));
			return new Uint8Array(buf);
		},
		async writeFile(handle, path, content) {
			const fullPath = resolve(handle.cwd, path);
			await mkdir(dirname(fullPath), { recursive: true });
			await writeFile(fullPath, content, "utf-8");
		},
		async listFiles(handle, path) {
			return readdir(resolve(handle.cwd, path));
		},
		async execBackground(handle, command, options) {
			const cwd = options.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
			await mkdir(options.outputDir, { recursive: true });
			const taskId = `bash_${++taskCounter}`;
			assertSafeTaskId(taskId);
			const outputPath = resolve(options.outputDir, `${taskId}.${contextTimestamp}.log`);
			if (!exitHandlerRegistered) {
				process.on("exit", exitHandler);
				exitHandlerRegistered = true;
			}
			const outputStream = createWriteStream(outputPath, { flags: "a" });
			outputStream.on("error", (err) => {
				if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} log stream error: ${err.message}\n`);
			});
			const child = spawn("/bin/sh", ["-c", command], {
				cwd,
				env: {
					...process.env,
					...defaultEnv,
					...options.env
				},
				stdio: [
					"ignore",
					"pipe",
					"pipe"
				],
				detached: SUPPORTS_PROCESS_GROUPS
			});
			const state = {
				taskId,
				handleId: handle.id,
				pid: child.pid ?? -1,
				command,
				cwd,
				startedAt: Date.now(),
				outputPath,
				outputStream,
				child,
				status: "running",
				bytesWritten: 0,
				settled: false,
				onExit: options.onExit
			};
			tasks.set(taskId, state);
			const maxOutputBytes = typeof options.maxOutputBytes === "number" && options.maxOutputBytes > 0 ? options.maxOutputBytes : void 0;
			let droppedBytes = 0;
			let capExhausted = false;
			const stallTimeoutMs = typeof options.stallTimeoutMs === "number" && options.stallTimeoutMs > 0 ? options.stallTimeoutMs : void 0;
			const onStall = options.onStall;
			let stallTimer;
			let lastOutputAt = Date.now();
			const armStallTimer = () => {
				if (!stallTimeoutMs || !onStall) return;
				if (stallTimer) clearTimeout(stallTimer);
				stallTimer = setTimeout(() => {
					stallTimer = void 0;
					if (state.settled) return;
					try {
						onStall({
							taskId,
							command,
							outputPath,
							stalledForMs: Date.now() - lastOutputAt,
							bytesWritten: state.bytesWritten
						});
					} catch (err) {
						if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} onStall threw: ${err instanceof Error ? err.message : String(err)}\n`);
					}
				}, stallTimeoutMs);
				stallTimer.unref?.();
			};
			const clearStallTimer = () => {
				if (stallTimer) {
					clearTimeout(stallTimer);
					stallTimer = void 0;
				}
			};
			armStallTimer();
			const appendChunk = (chunk) => {
				state.bytesWritten += chunk.length;
				lastOutputAt = Date.now();
				armStallTimer();
				if (maxOutputBytes !== void 0) {
					const onDisk = state.bytesWritten - droppedBytes - chunk.length;
					if (capExhausted || onDisk >= maxOutputBytes) {
						capExhausted = true;
						droppedBytes += chunk.length;
						return;
					}
					const room = maxOutputBytes - onDisk;
					if (chunk.length > room) {
						capExhausted = true;
						let end = room;
						while (end > 0 && (chunk[end] & 192) === 128) end--;
						droppedBytes += chunk.length - end;
						if (end > 0) outputStream.write(chunk.subarray(0, end));
						return;
					}
				}
				outputStream.write(chunk);
			};
			child.stdout?.on("data", (chunk) => appendChunk(chunk));
			child.stderr?.on("data", (chunk) => appendChunk(chunk));
			const settle = (cause, code, signal, errMessage) => {
				if (state.settled) return;
				state.settled = true;
				clearStallTimer();
				state.endedAt = Date.now();
				const status = signal === "SIGTERM" || state.killRequested ? "killed" : "exited";
				const exitCode = code !== null ? code : signal === "SIGTERM" ? 143 : signal ? 128 : 1;
				state.status = status;
				state.exitCode = exitCode;
				if (signal) state.signal = signal;
				if (errMessage) try {
					outputStream.write(`\n${errMessage}\n`);
				} catch {}
				if (droppedBytes > 0) try {
					outputStream.write(`\n<output-truncated bytes-dropped="${droppedBytes}"/>\n`);
				} catch {}
				outputStream.end(() => {
					try {
						state.onExit?.(stateToTaskExitInfo(state));
					} catch (err) {
						if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} onExit threw: ${err instanceof Error ? err.message : String(err)}\n`);
					}
				});
			};
			child.on("close", (code, signal) => settle("close", code, signal));
			child.on("error", (err) => settle("error", null, null, `[spawn error] ${err.message}`));
			child.on("exit", (code, signal) => {
				setTimeout(() => {
					if (state.settled) return;
					child.stdout?.destroy();
					child.stderr?.destroy();
					settle("close", code, signal);
				}, EXIT_PIPE_DRAIN_GRACE_MS).unref?.();
			});
			return {
				taskId,
				pid: state.pid,
				outputPath
			};
		},
		async killBackground(handle, taskId) {
			const state = tasks.get(taskId);
			if (!state || state.handleId !== handle.id) return null;
			if (state.status !== "running") return stateToTaskExitInfo(state);
			state.killRequested = true;
			const closed = new Promise((resolveP) => {
				if (state.settled) {
					resolveP();
					return;
				}
				const originalOnExit = state.onExit;
				state.onExit = (info) => {
					originalOnExit?.(info);
					resolveP();
				};
			});
			killProcessGroup(state.child, "SIGTERM");
			await closed;
			return stateToTaskExitInfo(state);
		},
		async waitBackground(handle, taskId, options) {
			const state = tasks.get(taskId);
			if (!state || state.handleId !== handle.id) return null;
			if (state.settled) return stateToTaskExitInfo(state);
			return new Promise((resolveP) => {
				let done = false;
				let timer;
				const signal = options?.signal;
				const finish = (value) => {
					if (done) return;
					done = true;
					if (timer) clearTimeout(timer);
					signal?.removeEventListener("abort", onAbort);
					resolveP(value);
				};
				function onAbort() {
					finish(null);
				}
				const originalOnExit = state.onExit;
				state.onExit = (info) => {
					originalOnExit?.(info);
					finish(stateToTaskExitInfo(state));
				};
				if (signal) {
					if (signal.aborted) {
						finish(null);
						return;
					}
					signal.addEventListener("abort", onAbort, { once: true });
				}
				const timeoutMs = options?.timeoutMs;
				if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0) {
					timer = setTimeout(finish, timeoutMs, null);
					timer.unref?.();
				}
			});
		},
		async reassignBackgroundTasks(fromHandle, toHandle, newOnExit) {
			if (fromHandle.id === toHandle.id) return [];
			const promoted = [];
			for (const state of tasks.values()) {
				if (state.handleId !== fromHandle.id || state.status !== "running") continue;
				state.handleId = toHandle.id;
				if (newOnExit) state.onExit = newOnExit;
				promoted.push(stateToTaskEntry(state));
			}
			return promoted;
		},
		async listBackground(handle) {
			return [...tasks.values()].filter((s) => s.handleId === handle.id).sort((a, b) => a.startedAt - b.startedAt).map(stateToTaskEntry);
		},
		async destroy(handle) {
			const survivors = [...tasks.values()].filter((s) => s.handleId === handle.id && !s.settled);
			await Promise.all(survivors.map(async (state) => {
				state.killRequested = true;
				await new Promise((resolveP) => {
					let graceTimer;
					const originalOnExit = state.onExit;
					state.onExit = (info) => {
						originalOnExit?.(info);
						if (graceTimer) clearTimeout(graceTimer);
						resolveP();
					};
					killProcessGroup(state.child, "SIGTERM");
					graceTimer = setTimeout(() => {
						killProcessGroup(state.child, "SIGKILL");
						state.outputStream.destroy();
						resolveP();
					}, destroyGraceMs);
					graceTimer.unref?.();
				});
			}));
			for (const [taskId, state] of tasks) if (state.handleId === handle.id) tasks.delete(taskId);
			handles.delete(handle.id);
			if (exitHandlerRegistered && handles.size === 0) {
				process.off("exit", exitHandler);
				exitHandlerRegistered = false;
			}
		}
	};
}
/**
* Send `signal` to the child's whole process group. Falls back to a
* single-process kill on Windows (no POSIX process groups). Shared
* across the foreground `exec` path, the background spawn / kill
* paths, and the shutdown-time orphan reaper — keeping one definition
* so the kill semantics can't drift between them.
*/
function killProcessGroup(child, signal) {
	const pid = child.pid;
	if (pid === void 0) return;
	try {
		if (SUPPORTS_PROCESS_GROUPS) process.kill(-pid, signal);
		else process.kill(pid, signal);
	} catch {}
}
/**
* Project a `TaskState` to the `TaskEntry` shape `listBackground` and
* `reassignBackgroundTasks` return. Single helper keeps the shape
* consistent across both call sites (and a future addition of fields
* to `TaskEntry` only needs to land here).
*/
function stateToTaskEntry(state) {
	return {
		taskId: state.taskId,
		pid: state.pid,
		command: state.command,
		cwd: state.cwd,
		startedAt: state.startedAt,
		...state.endedAt !== void 0 ? { endedAt: state.endedAt } : {},
		outputPath: state.outputPath,
		status: state.status,
		...state.exitCode !== void 0 ? { exitCode: state.exitCode } : {},
		...state.signal ? { signal: state.signal } : {},
		bytesWritten: state.bytesWritten
	};
}
/**
* Project a settled `TaskState` to the `TaskExitInfo` shape `settle()`
* fires from `onExit` and `killBackground` returns on the
* cached-exit path. Pre-condition: `state.status !== 'running'`
* (callers gate on this).
*/
function stateToTaskExitInfo(state) {
	return {
		taskId: state.taskId,
		status: state.status,
		exitCode: state.exitCode ?? 0,
		...state.signal ? { signal: state.signal } : {},
		outputPath: state.outputPath,
		durationMs: (state.endedAt ?? Date.now()) - state.startedAt,
		command: state.command
	};
}
//#endregion
//#region src/contexts/sandbox.ts
function createSandboxContext(provider) {
	const sandboxes = /* @__PURE__ */ new Map();
	function getSandboxId(handle) {
		const id = sandboxes.get(handle.id);
		if (!id) throw new Error(`Sandbox ${handle.id} not found`);
		return id;
	}
	return {
		type: "sandbox",
		capabilities: {
			shell: true,
			filesystem: true,
			network: true,
			gpu: false
		},
		async spawn(config) {
			const result = await provider.spawn(config ?? {});
			const handle = {
				id: result.id,
				type: "sandbox",
				cwd: result.cwd
			};
			sandboxes.set(handle.id, result.id);
			return handle;
		},
		async exec(handle, command, options) {
			return provider.exec(getSandboxId(handle), command, options);
		},
		async readFile(handle, path) {
			return provider.readFile(getSandboxId(handle), path);
		},
		async writeFile(handle, path, content) {
			return provider.writeFile(getSandboxId(handle), path, content);
		},
		async listFiles(handle, path) {
			return provider.listFiles(getSandboxId(handle), path);
		},
		async destroy(handle) {
			const id = sandboxes.get(handle.id);
			if (!id) return;
			await provider.destroy(id);
			sandboxes.delete(handle.id);
		}
	};
}
//#endregion
//#region src/contexts/types.ts
/**
* Effective {@link DetachedTasksCapability} of a context, with the
* backward-compatible inference for contexts that predate the field.
*/
function resolveDetachedTasksCapability(context) {
	return context.capabilities.detachedTasks ?? (context.execBackground ? "process-lifetime" : "none");
}
//#endregion
export { createSandboxContext as n, createProcessContext as r, resolveDetachedTasksCapability as t };

//# sourceMappingURL=contexts-BJVgG0LY.js.map