/**
 * run-cmd.mjs — D-05 in-process command harness.
 *
 * `runCmd(cmdFn, args, opts)` invokes a Layer 1 command function in-process
 * (no subprocess), captures stdout + stderr, traps any process.exit calls
 * (via a try/finally restore), stubs `process.stdin.isTTY` /
 * `process.stdout.isTTY` to `false` for the trapped invocation duration, and
 * returns:
 *
 *     { result, stdout, stderr, exitCode }
 *
 * `result` is the cmd function's return value (typically `{envelope, exitCode}`
 * after the migration; `null` if the cmd called process.exit before returning).
 * `stdout` / `stderr` are the captured `*.write` payloads (string). `exitCode`
 * is the effective exit code: the trapped exit code if process.exit was called,
 * else `result.exitCode`, else 0.
 *
 * D-05 (d) decision: strictExit DEFAULTS TO TRUE — the harness throws a
 * strict-mode-violation Error any time a Layer 1 cmd calls process.exit.
 * Legacy tests being migrated opt OUT explicitly per-test with
 * `{ strictExit: false }` until the underlying cmd is rewritten. After Wave 5
 * (subtask 25) zero `strictExit: false` opt-outs remain in __tests__/.
 *
 * Restoration: process.exit, process.stdout.write, process.stderr.write, and
 * the two `isTTY` bits are saved at entry and restored unconditionally in a
 * try/finally. A test that crashes mid-call still leaves the global state
 * byte-identical for the next test in the file.
 *
 * Cache resets (F3 subtask 1): by default the harness clears the two
 * path-keyed module-graph caches that can leak across cmd invocations within
 * a single vitest worker — `skill-discover._discoverCache` and the
 * `agent-identity` identity cache. The `gate/registry` and
 * `consistency/registry` registries are NOT cleared by default because their
 * built-in entries are populated by barrel imports that ESM evaluates exactly
 * ONCE per process — clearing them would leave subsequent cmd invocations
 * looking at an empty registry. Tests that specifically need an empty
 * registry import `__resetCaches()` from those modules directly.
 */
import { __resetCaches as __resetAgentIdentityCaches } from '../cli/coordination/agent-identity.mjs';
import { __resetCaches as __resetSkillDiscoverCaches } from '../cli/route/skill-discover.mjs';
class ExitTrap extends Error {
    constructor(code) {
        super(`apt-tools cmd called process.exit(${code})`);
        this.code = code;
    }
}
/**
 * @param {Function} cmdFn — the Layer 1 cmd function (sync or async).
 * @param {unknown[]} [args=[]] — argv-shaped positional args.
 * @param {{ strictExit?: boolean, resetCaches?: boolean, env?: Record<string, string> }} [opts]
 * @returns {Promise<{ result: unknown, stdout: string, stderr: string, exitCode: number }>}
 */
export async function runCmd(cmdFn, args = [], opts = {}) {
    const { strictExit = true, resetCaches = true } = opts;
    if (resetCaches) {
        __resetSkillDiscoverCaches();
        __resetAgentIdentityCaches();
    }
    let trappedExit = null;
    const origExit = process.exit;
    // Save the unbound originals — assignment to process.stdout.write replaces
    // the property; restoration of the original reference returns the global
    // to byte-identical state (Function-identity check in self-tests).
    const origWrite = process.stdout.write;
    const origErrWrite = process.stderr.write;
    const origStdinIsTTY = process.stdin.isTTY;
    const origStdoutIsTTY = process.stdout.isTTY;
    const stdoutBuf = [];
    const stderrBuf = [];
    process.exit = (code) => {
        trappedExit = typeof code === 'number' ? code : 0;
        if (strictExit) {
            throw new Error(`process.exit(${trappedExit}) called inside cmdFn — strict-mode violation. ` +
                `Layer 1 commands must return { envelope, exitCode } instead of calling process.exit. ` +
                `If this cmd is mid-migration, pass { strictExit: false } to runCmd to opt out per-test.`);
        }
        // Lenient mode — unwind the cmdFn's call stack so the harness can
        // observe the trapped code via the catch block below.
        throw new ExitTrap(trappedExit);
    };
    process.stdout.write = (chunk, ..._rest) => {
        stdoutBuf.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
        return true;
    };
    process.stderr.write = (chunk, ..._rest) => {
        stderrBuf.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
        return true;
    };
    // F3 subtask 1 (Q3): defensively force the non-interactive branch for any
    // cmd that gates on isTTY (init.mjs:127,167,1013 + cmdState's TTY sidecar).
    // Under vitest workers isTTY is usually `undefined`, but stubbing to `false`
    // removes the Node-version drift risk.
    process.stdin.isTTY = false;
    process.stdout.isTTY = false;
    try {
        const result = await cmdFn(...args);
        const exitCode = trappedExit !== null
            ? trappedExit
            : typeof result === 'object' && result !== null && typeof result.exitCode === 'number'
                ? result.exitCode
                : 0;
        return { result, stdout: stdoutBuf.join(''), stderr: stderrBuf.join(''), exitCode };
    }
    catch (e) {
        if (e instanceof ExitTrap) {
            return {
                result: null,
                stdout: stdoutBuf.join(''),
                stderr: stderrBuf.join(''),
                exitCode: e.code,
            };
        }
        // Re-throw non-trap errors (including the strict-mode-violation Error
        // thrown above) so tests can assert via expect(...).rejects.
        throw e;
    }
    finally {
        process.exit = origExit;
        process.stdout.write = origWrite;
        process.stderr.write = origErrWrite;
        process.stdin.isTTY = origStdinIsTTY;
        process.stdout.isTTY = origStdoutIsTTY;
    }
}
//# sourceMappingURL=run-cmd.mjs.map