import fs from "node:fs";
import fsp from "node:fs/promises";
import http from "node:http";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { execFile, spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { resolveMeIdentityHash } from "../identity/meIdentity.js";
import { normalizeNamespaceConstant } from "../namespace/identity.js";
const DEFAULT_PORT_START = 8161;
const DEFAULT_PORT_END = 8999;
export function getMonadsHome() {
    return path.resolve(process.env.MONADS_HOME || path.join(os.homedir(), ".monad", "monads"));
}
export function normalizeMonadName(input) {
    const normalized = String(input || "")
        .trim()
        .toLowerCase()
        .replace(/[^a-z0-9._-]+/g, "-")
        .replace(/^-+|-+$/g, "");
    return normalized || `monad-${Date.now().toString(36)}`;
}
function resolveDefaultRootspace() {
    return normalizeNamespaceConstant(process.env.MONAD_ROOTSPACE ||
        process.env.ME_NAMESPACE ||
        process.env.MONAD_SELF_IDENTITY ||
        process.env.MONAD_SELF_HOSTNAME ||
        os.hostname()) || "monad.local";
}
export function getMonadRuntimeDir(name) {
    return path.join(getMonadsHome(), normalizeMonadName(name));
}
function getRecordPath(name) {
    return path.join(getMonadRuntimeDir(name), "monad.json");
}
function pidAlive(pid) {
    if (!Number.isInteger(pid) || pid <= 0)
        return false;
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (error) {
        return error?.code === "EPERM";
    }
}
async function findListeningPid(port) {
    if (!Number.isInteger(port) || port <= 0)
        return null;
    return new Promise((resolve) => {
        execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], (error, stdout) => {
            if (error) {
                resolve(error.code === 1 ? null : undefined);
                return;
            }
            const pid = Number(String(stdout || "").trim().split(/\s+/)[0]);
            resolve(Number.isInteger(pid) && pid > 0 ? pid : null);
        });
    });
}
async function ensureDir(dir) {
    await fsp.mkdir(dir, { recursive: true });
}
async function readJsonFile(filePath) {
    try {
        return JSON.parse(await fsp.readFile(filePath, "utf8"));
    }
    catch {
        return null;
    }
}
async function writeRecord(record) {
    await ensureDir(record.runtimeDir);
    await fsp.writeFile(getRecordPath(record.name), `${JSON.stringify(record, null, 2)}\n`, "utf8");
}
function resolveSafeMonadRuntimeDir(name) {
    const home = path.resolve(getMonadsHome());
    const runtimeDir = path.resolve(getMonadRuntimeDir(name));
    const relative = path.relative(home, runtimeDir);
    if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
        throw new Error(`Refusing to delete unsafe Monad runtime directory: ${runtimeDir}`);
    }
    return runtimeDir;
}
async function waitForPidExit(pid, timeoutMs) {
    const started = Date.now();
    while (pidAlive(pid) && Date.now() - started < timeoutMs) {
        await new Promise((resolve) => setTimeout(resolve, 200));
    }
    return !pidAlive(pid);
}
async function terminateRecordProcess(input) {
    if (!pidAlive(input.record.pid))
        return true;
    process.kill(input.record.pid, input.signal || "SIGTERM");
    if (await waitForPidExit(input.record.pid, input.timeoutMs ?? 5000))
        return true;
    if (!input.forceKill)
        return false;
    process.kill(input.record.pid, "SIGKILL");
    return waitForPidExit(input.record.pid, 2000);
}
function normalizeRecord(record) {
    if (!record)
        return null;
    const legacyNameNamespace = normalizeNamespaceConstant(`${record.name}.local`);
    const recordedNamespace = normalizeNamespaceConstant(record.namespace || record.identity);
    const namespace = !record.namespace && recordedNamespace === legacyNameNamespace
        ? resolveDefaultRootspace()
        : normalizeNamespaceConstant(record.namespace || record.identity || resolveDefaultRootspace());
    return {
        ...record,
        namespace,
        identity: namespace,
        surface: record.surface || record.name,
    };
}
export async function readMonadRecord(name) {
    return normalizeRecord(await readJsonFile(getRecordPath(name)));
}
export async function listMonadRecords() {
    const home = getMonadsHome();
    try {
        const entries = await fsp.readdir(home, { withFileTypes: true });
        const records = await Promise.all(entries
            .filter((entry) => entry.isDirectory())
            .map((entry) => readMonadRecord(entry.name)));
        return records
            .filter((record) => Boolean(record))
            .sort((a, b) => a.name.localeCompare(b.name));
    }
    catch {
        return [];
    }
}
export async function listRunningMonads() {
    const statuses = await Promise.all((await listMonadRecords()).map(getMonadStatus));
    return statuses.filter((status) => status.healthy && status.status === "running");
}
async function isPortFree(port) {
    const ownerPid = await findListeningPid(port);
    if (typeof ownerPid === "number")
        return false;
    return new Promise((resolve) => {
        const server = net.createServer();
        server.once("error", () => resolve(false));
        server.once("listening", () => {
            server.close(() => resolve(true));
        });
        server.listen(port);
    });
}
async function findFreePort(preferred) {
    if (preferred && await isPortFree(preferred))
        return preferred;
    for (let port = DEFAULT_PORT_START; port <= DEFAULT_PORT_END; port += 1) {
        if (await isPortFree(port))
            return port;
    }
    throw new Error(`No free Monad port found in ${DEFAULT_PORT_START}-${DEFAULT_PORT_END}`);
}
function resolveServerEntry(devMode = false) {
    const here = path.dirname(fileURLToPath(import.meta.url));
    // In dev mode prefer the TypeScript source so tsx --watch picks up changes.
    const candidates = devMode
        ? [
            path.resolve(here, "../../../server.ts"), // monorepo source (most useful)
            path.resolve(here, "../../server.ts"), // packaged .ts fallback
            path.resolve(here, "../../server.js"), // compiled fallback
        ]
        : [
            path.resolve(here, "../../server.js"),
            path.resolve(here, "../../server.ts"),
            path.resolve(here, "../../../server.ts"),
        ];
    const found = candidates.find((candidate) => fs.existsSync(candidate));
    if (!found)
        throw new Error("Could not locate monad.ai server entry.");
    return found;
}
function resolveTsxBin(packageRoot) {
    const candidates = [
        path.resolve(packageRoot, "node_modules/.bin/tsx"),
        path.resolve(packageRoot, "../node_modules/.bin/tsx"),
        path.resolve(packageRoot, "../../node_modules/.bin/tsx"),
    ];
    return candidates.find((p) => fs.existsSync(p));
}
function resolvePackageRoot() {
    const here = path.dirname(fileURLToPath(import.meta.url));
    const candidates = [
        path.resolve(here, "../../.."),
        path.resolve(here, "../.."),
    ];
    const found = candidates.find((candidate) => fs.existsSync(path.join(candidate, "package.json")));
    if (!found)
        throw new Error("Could not locate monad.ai package root.");
    return found;
}
function existingPath(...segments) {
    const resolved = path.resolve(...segments);
    return fs.existsSync(resolved) ? resolved : undefined;
}
function requestJson(url, timeoutMs = 1000) {
    return new Promise((resolve, reject) => {
        const req = http.get(url, { timeout: timeoutMs }, (res) => {
            let body = "";
            res.setEncoding("utf8");
            res.on("data", (chunk) => {
                body += chunk;
            });
            res.on("end", () => {
                if ((res.statusCode || 500) >= 400) {
                    reject(new Error(`HTTP ${res.statusCode}`));
                    return;
                }
                try {
                    resolve(JSON.parse(body));
                }
                catch {
                    resolve(body);
                }
            });
        });
        req.on("timeout", () => {
            req.destroy(new Error("timeout"));
        });
        req.on("error", reject);
    });
}
async function waitForHealthy(endpoint, timeoutMs = 6000) {
    const started = Date.now();
    while (Date.now() - started < timeoutMs) {
        try {
            await requestJson(`${endpoint}/__surface`, 750);
            return true;
        }
        catch {
            await new Promise((resolve) => setTimeout(resolve, 250));
        }
    }
    return false;
}
export async function getMonadStatus(record) {
    const alive = pidAlive(record.pid);
    if (!alive) {
        const restingStatus = record.status === "paused" || record.status === "stopped" ? record.status : "dead";
        return {
            record,
            pidAlive: false,
            healthy: false,
            status: restingStatus,
        };
    }
    const listenerPid = await findListeningPid(record.port);
    if (listenerPid === null) {
        return {
            record,
            pidAlive: true,
            healthy: false,
            status: record.status === "starting" ? "starting" : "dead",
            error: `Port ${record.port} is not listening.`,
        };
    }
    if (typeof listenerPid === "number" && listenerPid !== record.pid) {
        // In dev mode tsx watch forks a child process that owns the port.
        // The recorded PID is the tsx watcher (parent); that's still alive — treat as running.
        if (!record.dev) {
            return {
                record,
                pidAlive: true,
                healthy: false,
                status: "dead",
                error: `Port ${record.port} is owned by PID ${listenerPid}, not PID ${record.pid}.`,
            };
        }
    }
    try {
        const surface = await requestJson(`${record.endpoint}/__surface`, 1000);
        return {
            record,
            pidAlive: true,
            healthy: true,
            status: "running",
            surface,
        };
    }
    catch (error) {
        return {
            record,
            pidAlive: true,
            healthy: false,
            status: "starting",
            error: error?.message || String(error),
        };
    }
}
export async function startMonadProcess(options = {}) {
    const name = normalizeMonadName(options.name);
    const existing = await readMonadRecord(name);
    if (existing && pidAlive(existing.pid)) {
        throw new Error(`Monad "${name}" is already running on port ${existing.port}.`);
    }
    const runtimeDir = getMonadRuntimeDir(name);
    const port = await findFreePort(options.port ?? existing?.port);
    const namespace = normalizeNamespaceConstant(options.namespace || existing?.namespace || resolveDefaultRootspace());
    const identity = namespace;
    const surface = name;
    const endpoint = `http://127.0.0.1:${port}`;
    const stateDir = path.join(runtimeDir, "state");
    const claimDir = path.join(runtimeDir, "claims");
    const selfConfigPath = path.join(runtimeDir, "self.json");
    const stdoutLog = path.join(runtimeDir, "stdout.log");
    const stderrLog = path.join(runtimeDir, "stderr.log");
    const packageRoot = resolvePackageRoot();
    const cwd = path.resolve(options.cwd || existing?.cwd || packageRoot);
    const now = new Date().toISOString();
    const repoRoot = path.resolve(packageRoot, "../../..");
    await ensureDir(runtimeDir);
    await ensureDir(stateDir);
    await ensureDir(claimDir);
    const out = fs.openSync(stdoutLog, "w");
    const err = fs.openSync(stderrLog, "w");
    const env = {
        ...process.env,
        PORT: String(port),
        // Fallback seed is anchored to the namespace, not the monad instance name.
        // Two monads serving the same namespace must share the same SEED so they
        // can read each other's kernel state. The instance name (haiku, iphone…)
        // belongs in MONAD_NAME, never in the namespace authority key.
        SEED: options.seed || process.env.SEED || process.env.ME_SEED || `monad-local:${namespace}`,
        ME_NAMESPACE: namespace,
        ME_STATE_DIR: stateDir,
        MONAD_CLAIM_DIR: claimDir,
        MONAD_SELF_CONFIG_PATH: selfConfigPath,
        MONAD_SELF_IDENTITY: namespace,
        MONAD_SELF_HOSTNAME: namespace,
        MONAD_SELF_ENDPOINT: endpoint,
        MONAD_SELF_TAGS: `local,monad,${name},surface:${name}`,
        MONAD_NAME: name,
        MONAD_SURFACE: surface,
        MONAD_ROOTSPACE: namespace,
        MONAD_INDEX_PATH: existingPath(packageRoot, "../index.html"),
        MONAD_ROUTES_PATH: existingPath(packageRoot, "../routes.js"),
        GUI_PKG_DIST_DIR: existingPath(repoRoot, "packages/GUI/npm/dist"),
        ME_PKG_DIST_DIR: existingPath(repoRoot, "me/npm/dist"),
        CLEAKER_PKG_DIST_DIR: existingPath(packageRoot, "../../cleaker/npm/dist"),
        LOCAL_REACT_UMD_DIR: existingPath(repoRoot, "packages/GUI/npm/node_modules/react/umd"),
        LOCAL_REACTDOM_UMD_DIR: existingPath(repoRoot, "packages/GUI/npm/node_modules/react-dom/umd"),
    };
    const devMode = options.dev === true;
    const entry = resolveServerEntry(devMode);
    const usesTsx = devMode && entry.endsWith(".ts");
    const tsxBin = usesTsx ? resolveTsxBin(packageRoot) : undefined;
    if (devMode && !tsxBin) {
        // tsx not found — warn and fall back to compiled entry
        const compiledEntry = resolveServerEntry(false);
        process.stderr.write(`[monads] --dev: tsx not found under ${packageRoot}; falling back to ${compiledEntry}\n`);
    }
    const execBin = usesTsx && tsxBin ? tsxBin : process.execPath;
    const execArgs = usesTsx && tsxBin ? ["watch", entry] : [entry];
    const child = spawn(execBin, execArgs, {
        cwd,
        env,
        detached: true,
        stdio: ["ignore", out, err],
    });
    child.unref();
    fs.closeSync(out);
    fs.closeSync(err);
    const record = {
        name,
        identity_hash: resolveMeIdentityHash(env.SEED),
        identity,
        namespace,
        surface,
        port,
        pid: child.pid || 0,
        endpoint,
        cwd,
        startedAt: now,
        updatedAt: now,
        status: "starting",
        runtimeDir,
        stateDir,
        claimDir,
        selfConfigPath,
        stdoutLog,
        stderrLog,
        ...(devMode ? { dev: true } : {}),
    };
    await writeRecord(record);
    // tsx needs extra time to compile TypeScript on first boot — give it more headroom.
    const healthy = await waitForHealthy(endpoint, devMode ? 20000 : 6000);
    const updated = {
        ...record,
        status: healthy ? "running" : "starting",
        updatedAt: new Date().toISOString(),
    };
    await writeRecord(updated);
    return getMonadStatus(updated);
}
export async function stopMonadProcess(name, options = {}) {
    const normalized = normalizeMonadName(name);
    const record = await readMonadRecord(normalized);
    if (!record)
        throw new Error(`Monad "${normalized}" was not found.`);
    await terminateRecordProcess({
        record,
        signal: options.signal,
        timeoutMs: options.timeoutMs,
    });
    const updated = {
        ...record,
        status: options.status || "stopped",
        updatedAt: new Date().toISOString(),
    };
    await writeRecord(updated);
    return getMonadStatus(updated);
}
export async function pauseMonadProcess(name) {
    return stopMonadProcess(name, { status: "paused" });
}
export async function startExistingMonadProcess(name, options = {}) {
    const normalized = normalizeMonadName(name);
    const existing = await readMonadRecord(normalized);
    if (!existing)
        throw new Error(`Monad "${normalized}" was not found.`);
    return startMonadProcess({
        name: existing.name,
        port: options.port ?? existing.port,
        namespace: options.namespace || existing.namespace,
        cwd: options.cwd || existing.cwd,
        seed: options.seed,
        dev: existing.dev,
    });
}
export async function resumeMonadProcess(name, options = {}) {
    return startExistingMonadProcess(name, options);
}
export async function restartMonadProcess(name, options = {}) {
    const normalized = normalizeMonadName(name);
    const existing = await readMonadRecord(normalized);
    if (!existing)
        throw new Error(`Monad "${normalized}" was not found.`);
    const status = await getMonadStatus(existing);
    if (status.pidAlive)
        await stopMonadProcess(existing.name);
    return startMonadProcess({
        name: existing.name,
        port: options.port ?? existing.port,
        namespace: options.namespace || existing.namespace,
        cwd: options.cwd || existing.cwd,
        seed: options.seed,
        dev: existing.dev,
    });
}
export async function deleteMonadProcess(name) {
    const normalized = normalizeMonadName(name);
    const record = await readMonadRecord(normalized);
    if (!record)
        throw new Error(`Monad "${normalized}" was not found.`);
    const runtimeDir = resolveSafeMonadRuntimeDir(normalized);
    const stopped = await terminateRecordProcess({ record, forceKill: true });
    if (!stopped) {
        throw new Error(`Monad "${normalized}" could not be stopped before delete.`);
    }
    await fsp.rm(runtimeDir, { recursive: true, force: true });
    return { record, runtimeDir, deleted: true };
}
export async function readLogTail(record, stream = "stdout", lines = 80) {
    const logPath = stream === "stdout" ? record.stdoutLog : record.stderrLog;
    try {
        const content = await fsp.readFile(logPath, "utf8");
        return content.split(/\r?\n/).slice(-lines).join("\n").trim();
    }
    catch {
        return "";
    }
}
async function printInitialTail(logPath, output, lines) {
    try {
        const content = await fsp.readFile(logPath, "utf8");
        const tail = content.split(/\r?\n/).slice(-lines).join("\n").trim();
        if (tail) {
            output.write(tail);
            if (!tail.endsWith("\n"))
                output.write("\n");
        }
        return Buffer.byteLength(content);
    }
    catch {
        return 0;
    }
}
function watchLogFile(input) {
    let offset = input.offset;
    let reading = false;
    const tick = async () => {
        if (reading || input.signal?.aborted)
            return;
        reading = true;
        try {
            const stat = await fsp.stat(input.logPath);
            if (stat.size < offset)
                offset = 0;
            if (stat.size > offset) {
                await new Promise((resolve) => {
                    const stream = fs.createReadStream(input.logPath, {
                        start: offset,
                        end: stat.size - 1,
                        encoding: "utf8",
                    });
                    stream.on("data", (chunk) => input.output.write(chunk));
                    stream.on("error", () => resolve());
                    stream.on("end", () => resolve());
                });
                offset = stat.size;
            }
        }
        catch {
            // The log file may not exist yet during startup. Keep watching.
        }
        finally {
            reading = false;
        }
    };
    const timer = setInterval(tick, input.intervalMs);
    void tick();
    return timer;
}
function buildPacFile(proxyPort) {
    return `function FindProxyForURL(url, host) {
  // .monad  → monad instances by name (haiku.monad, iphone.monad, …)
  // .netget → netget admin/control plane by hostname (suis-macbook-air.netget, …)
  if (dnsDomainIs(host, ".monad") || host === "local.monad" ||
      dnsDomainIs(host, ".netget") || host === "local.netget") {
    return "PROXY 127.0.0.1:${proxyPort}";
  }
  return "DIRECT";
}`;
}
function buildStatusHtml(monads, proxyPort) {
    const rows = monads.length === 0
        ? `<tr><td colspan="4" style="color:#888;text-align:center">No monads running. Start one: <code>monads start &lt;name&gt;</code></td></tr>`
        : monads
            .map((s) => `<tr>
              <td><a href="http://${s.record.name}.monad" target="_blank">${s.record.name}.monad</a></td>
              <td><a href="${s.record.endpoint}" target="_blank">${s.record.endpoint}</a></td>
              <td style="font-size:0.9em;color:#555">${s.record.namespace}</td>
              <td style="color:#2a2">online</td>
            </tr>`)
            .join("");
    return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Monads</title>
<style>
  body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; color: #111; }
  h1 { font-size: 1.4rem; margin-bottom: 1rem; }
  table { width: 100%; border-collapse: collapse; }
  th { text-align: left; padding: 6px 10px; background: #f4f4f4; font-size: 0.85rem; }
  td { padding: 6px 10px; border-top: 1px solid #eee; font-size: 0.9rem; }
  a { color: #0070f3; text-decoration: none; }
  a:hover { text-decoration: underline; }
  .note { margin-top: 1.5rem; font-size: 0.85rem; color: #555; background: #fafafa; padding: 0.8rem 1rem; border-radius: 6px; }
  code { background: #f0f0f0; padding: 1px 4px; border-radius: 3px; font-size: 0.9em; }
</style>
</head>
<body>
<h1>Monads</h1>
<table>
<thead><tr><th>URL</th><th>Endpoint</th><th>Namespace</th><th>Status</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<div class="note">
  <strong>local.monad</strong> → first running monad<br>
  <strong>name.monad</strong> → monad by name<br><br>
  PAC file: <code>http://127.0.0.1:${proxyPort}/proxy.pac</code>
</div>
</body>
</html>`;
}
export async function startMonadProxy(options = {}) {
    const proxyPort = options.port ?? 8160;
    const server = http.createServer(async (req, res) => {
        const hostRaw = String(req.headers.host || "").toLowerCase();
        const host = hostRaw.split(":")[0] ?? "";
        const url = req.url || "/";
        if (host === "127.0.0.1" || host === "localhost") {
            if (url === "/proxy.pac") {
                res.writeHead(200, { "Content-Type": "application/x-ns-proxy-autoconfig" });
                res.end(buildPacFile(proxyPort));
                return;
            }
            const running = await listRunningMonads();
            res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
            res.end(buildStatusHtml(running, proxyPort));
            return;
        }
        const monadMatch = host.match(/^(.+)\.monad$/);
        if (!monadMatch) {
            res.writeHead(400, { "Content-Type": "text/plain" });
            res.end("Expected a *.monad host.");
            return;
        }
        const name = monadMatch[1];
        let targetPort = null;
        if (name === "local") {
            const running = await listRunningMonads();
            targetPort = running[0]?.record.port ?? null;
        }
        else if (name) {
            const record = await readMonadRecord(name);
            if (record && pidAlive(record.pid))
                targetPort = record.port;
        }
        if (!targetPort) {
            res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
            res.end(`<h2>Monad not found: ${name}</h2>` +
                `<p>Is <strong>${name}</strong> running? Try: <code>monads start ${name}</code></p>` +
                `<p><a href="http://127.0.0.1:${proxyPort}/">Dashboard</a></p>`);
            return;
        }
        const proxyReq = http.request({
            hostname: "127.0.0.1",
            port: targetPort,
            path: url,
            method: req.method,
            headers: { ...req.headers, host: `127.0.0.1:${targetPort}` },
        }, (proxyRes) => {
            res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
            proxyRes.pipe(res, { end: true });
        });
        proxyReq.on("error", (err) => {
            if (!res.headersSent)
                res.writeHead(502, { "Content-Type": "text/plain" });
            res.end(`Gateway error: ${err.message}`);
        });
        req.pipe(proxyReq, { end: true });
    });
    server.on("error", (err) => {
        if (err.code === "EADDRINUSE") {
            process.stderr.write(`Port ${proxyPort} is already in use. Try: monads proxy --port 8161\n`);
        }
        else {
            process.stderr.write(`Proxy error: ${err.message}\n`);
        }
        process.exit(1);
    });
    await new Promise((resolve) => {
        server.listen(proxyPort, "127.0.0.1", () => {
            process.stdout.write(`\nMonad gateway running at http://127.0.0.1:${proxyPort}/\n` +
                `\nBrowser setup (one time):\n` +
                `  Safari / Chrome / Firefox proxy settings → Automatic proxy configuration:\n` +
                `  URL: http://127.0.0.1:${proxyPort}/proxy.pac\n` +
                `\nThen open:\n` +
                `  http://local.monad       → first running monad\n` +
                `  http://name.monad        → monad by name\n` +
                `\nPress Ctrl+C to stop.\n`);
        });
        const stop = () => server.close(() => resolve());
        process.once("SIGINT", stop);
        process.once("SIGTERM", stop);
    });
}
export async function followMonadLogs(record, options = {}) {
    const lines = options.lines || 80;
    const intervalMs = options.intervalMs || 250;
    const includeStderr = options.includeStderr !== false;
    const stdoutOffset = await printInitialTail(record.stdoutLog, process.stdout, lines);
    let stderrOffset = 0;
    if (includeStderr) {
        const stderr = await readLogTail(record, "stderr", lines);
        if (stderr) {
            process.stderr.write(`\n[stderr]\n${stderr}\n`);
            try {
                stderrOffset = (await fsp.stat(record.stderrLog)).size;
            }
            catch {
                stderrOffset = 0;
            }
        }
    }
    await new Promise((resolve) => {
        const timers = [
            watchLogFile({
                logPath: record.stdoutLog,
                output: process.stdout,
                offset: stdoutOffset,
                intervalMs,
                signal: options.signal,
            }),
        ];
        if (includeStderr) {
            timers.push(watchLogFile({
                logPath: record.stderrLog,
                output: process.stderr,
                offset: stderrOffset,
                intervalMs,
                signal: options.signal,
            }));
        }
        const cleanup = () => {
            for (const timer of timers)
                clearInterval(timer);
            options.signal?.removeEventListener("abort", cleanup);
            resolve();
        };
        if (options.signal?.aborted) {
            cleanup();
            return;
        }
        options.signal?.addEventListener("abort", cleanup, { once: true });
    });
}
