import { spawn as spawnChildProcess } from "node:child_process";
export function createHostRunner(options = {}) {
    const runnerOptions = normalizeHostRunnerOptions(options);
    const detachedByDefault = runnerOptions.detached === true;
    return {
        name: "host",
        exec(inputSpec) {
            const spec = normalizeRunSpec(inputSpec);
            if (spec.signal?.aborted === true) {
                return {
                    pid: null,
                    stdin: null,
                    stdout: null,
                    stderr: null,
                    result: Promise.resolve({ exitCode: 1 }),
                    kill() { }
                };
            }
            const stdinMode = spec.stdin ?? "ignore";
            const stdoutMode = spec.stdout ?? "pipe";
            const stderrMode = spec.stderr ?? "pipe";
            const killProcessGroup = detachedByDefault || spec.killProcessGroup === true;
            const stdio = stdinMode === "inherit" && stdoutMode === "inherit" && stderrMode === "inherit"
                ? "inherit"
                : [stdinMode, stdoutMode, stderrMode];
            const child = spawnChildProcess(spec.command, spec.args ?? [], createNullRecord({
                cwd: spec.cwd,
                env: spec.env,
                stdio,
                ...(killProcessGroup ? { detached: true } : {})
            }));
            if (killProcessGroup) {
                child.unref();
            }
            const kill = (signal) => {
                if (killProcessGroup && process.platform !== "win32" && child.pid !== undefined) {
                    process.kill(-child.pid, signal);
                    return;
                }
                child.kill(signal);
            };
            let settled = false;
            let resolveResult = null;
            const result = new Promise((resolve) => {
                resolveResult = resolve;
            });
            const cleanupAbort = bindAbortSignal(spec.signal, () => {
                try {
                    kill("SIGTERM");
                }
                catch {
                    return;
                }
            });
            child.once("close", (code) => {
                if (settled)
                    return;
                settled = true;
                cleanupAbort();
                resolveResult?.({ exitCode: code ?? 1 });
            });
            child.once("error", () => {
                if (settled)
                    return;
                settled = true;
                cleanupAbort();
                resolveResult?.({ exitCode: 1 });
            });
            return {
                pid: child.pid ?? null,
                stdin: child.stdin,
                stdout: child.stdout,
                stderr: child.stderr,
                result,
                kill
            };
        }
    };
}
function normalizeHostRunnerOptions(options) {
    return createNullRecord({
        ...optionalOwnProperty(options, "detached")
    });
}
function normalizeRunSpec(spec) {
    return createNullRecord({
        command: getOwnProperty(spec, "command"),
        ...optionalOwnProperty(spec, "args"),
        ...optionalOwnProperty(spec, "cwd"),
        ...optionalOwnProperty(spec, "env"),
        ...optionalOwnProperty(spec, "stdin"),
        ...optionalOwnProperty(spec, "stdout"),
        ...optionalOwnProperty(spec, "stderr"),
        ...optionalOwnProperty(spec, "tty"),
        ...optionalOwnProperty(spec, "signal"),
        ...optionalOwnProperty(spec, "killProcessGroup")
    });
}
function optionalOwnProperty(value, name) {
    const property = getOwnProperty(value, name);
    return property === undefined ? {} : { [name]: property };
}
function getOwnProperty(value, name) {
    return hasOwnProperty(value, name) ? value[name] : undefined;
}
function hasOwnProperty(value, name) {
    return Object.prototype.hasOwnProperty.call(value, name);
}
function createNullRecord(value) {
    return Object.assign(Object.create(null), value);
}
function bindAbortSignal(signal, onAbort) {
    if (signal === undefined) {
        return () => { };
    }
    if (signal.aborted) {
        onAbort();
        return () => { };
    }
    signal.addEventListener("abort", onAbort, { once: true });
    return () => {
        signal.removeEventListener("abort", onAbort);
    };
}
