/**
 * agent_bridge — `opensquid agent-bridge {start|stop|status|restart|run-foreground}` CLI (WAB.7).
 *
 * Authoritative spec: `docs/tasks/T-warm-agent-chat-bridge.md` WAB.7 §"CLI".
 *
 * Shape: start spawns detached `run-foreground`; stop/status read pid file +
 * `kill -0` liveness; restart = stop + wait for pid removal + start;
 * run-foreground is the actual daemon entry (constructs AgentBridgeDaemon,
 * starts it, then awaits a never-resolving promise — signal handlers in
 * daemon.ts call shutdown() + process.exit(0)).
 *
 * Resolution chains:
 *   projectUuid: OPENSQUID_PROJECT_UUID → cwd walk for `.opensquid/project.json` → hard-fail
 *   packRoot:    OPENSQUID_PACK_ROOT → `~/.opensquid/packs/default/` (loadPack throws on missing manifest)
 *
 * Exit codes: 0 = success (including not-running for stop/status), 1 =
 * generic failure, 2 = `start` invoked while another daemon is running.
 *
 * Imports from: node:child_process, node:fs/promises, node:path, commander,
 *   ./daemon.js.
 * Imported by: src/cli.ts (registers `agent-bridge` parent verb).
 */
import { spawn } from 'node:child_process';
import { readFile, rm } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { GENERAL_UMBRELLA, loadChannelsConfig, resolveUmbrellaForCwd, } from '../../channels/routing.js';
import { OPENSQUID_HOME, resolveProjectUuid } from '../paths.js';
import { AgentBridgeDaemon, agentBridgePidPath, resolveGeneralPackRoot, resolvePackRootFromEnv, } from './daemon.js';
const DEFAULTS = {
    out: (chunk) => {
        process.stdout.write(chunk);
    },
    err: (chunk) => {
        process.stderr.write(chunk);
    },
    exit: (code) => {
        process.exitCode = code;
    },
    spawnFn: (cmd, args, opts) => {
        const child = spawn(cmd, args, { detached: opts.detached, stdio: 'ignore' });
        return { ...(child.pid !== undefined ? { pid: child.pid } : {}), unref: () => child.unref() };
    },
    killFn: (pid, sig) => {
        process.kill(pid, sig);
    },
    waitForever: () => new Promise(() => undefined),
};
/** Register the `agent-bridge` parent verb + every subcommand. */
export function registerAgentBridge(parent, deps = {}) {
    const out = deps.out ?? DEFAULTS.out;
    const err = deps.err ?? DEFAULTS.err;
    const exit = deps.exit ?? DEFAULTS.exit;
    const env = deps.env ?? process.env;
    const cwd = deps.cwd ?? (() => process.cwd());
    const spawnFn = deps.spawnFn ?? DEFAULTS.spawnFn;
    const killFn = deps.killFn ?? DEFAULTS.killFn;
    const waitForever = deps.waitForever ?? DEFAULTS.waitForever;
    const Ctor = deps.daemonCtor ?? AgentBridgeDaemon;
    const group = parent
        .command('agent-bridge')
        .description('Warm-pool chat-agent daemon (start | stop | status | restart | run-foreground)');
    group
        .command('status')
        .description('Report agent-bridge daemon status (running pid or "not running").')
        .action(async () => {
        const result = await readLivePid(env, killFn);
        if (result.alive) {
            out(`agent-bridge: running (pid=${String(result.pid)})\n`);
        }
        else if (result.stalePid !== undefined) {
            out(`agent-bridge: not running (stale pid=${String(result.stalePid)} at ${pidPathFor(env)})\n`);
        }
        else {
            out(`agent-bridge: not running (no pid file at ${pidPathFor(env)})\n`);
        }
    });
    const doSpawn = (label, general = false) => {
        const argv1 = process.argv[1] ?? 'opensquid';
        const fgArgs = ['agent-bridge', 'run-foreground', ...(general ? ['--general'] : [])];
        const child = spawnFn(process.execPath, [resolve(argv1), ...fgArgs], {
            detached: true,
        });
        child.unref();
        if (child.pid === undefined) {
            err('agent-bridge: spawn returned no pid\n');
            exit(1);
            return;
        }
        out(`agent-bridge: ${label} (pid=${String(child.pid)})\n`);
    };
    group
        .command('start')
        .description('Spawn the agent-bridge daemon detached and return.')
        // CAT.6: `--general` spawns the PROJECT-LESS general session (umbrella=general,
        // the built-in `general` pack) instead of the cwd's project-scoped daemon.
        .option('--general', 'run the project-less general session (DM + General + All)')
        .action(async (opts) => {
        const live = await readLivePid(env, killFn);
        if (live.alive) {
            err(`agent-bridge: already running (pid=${String(live.pid)})\n`);
            exit(2);
            return;
        }
        if (live.stalePid !== undefined) {
            await rm(pidPathFor(env), { force: true }).catch(() => undefined);
        }
        doSpawn('spawned', opts.general === true);
    });
    group
        .command('stop')
        .description('Send SIGTERM to the running agent-bridge daemon.')
        .action(async () => {
        const live = await readLivePid(env, killFn);
        if (!live.alive) {
            if (live.stalePid !== undefined) {
                await rm(pidPathFor(env), { force: true }).catch(() => undefined);
            }
            out('agent-bridge: not running\n');
            return;
        }
        try {
            killFn(live.pid, 'SIGTERM');
            out(`agent-bridge: SIGTERM -> pid ${String(live.pid)}\n`);
        }
        catch (e) {
            err(`agent-bridge: kill failed: ${e instanceof Error ? e.message : String(e)}\n`);
            exit(1);
        }
    });
    group
        .command('restart')
        .description('Stop + brief wait for pid file removal + start.')
        .action(async () => {
        const live = await readLivePid(env, killFn);
        if (live.alive) {
            try {
                killFn(live.pid, 'SIGTERM');
            }
            catch {
                /* already gone */
            }
            // 2s is generous — start's stale-pid cleanup handles the miss too.
            await waitForPidfileGone(env, 2000);
        }
        doSpawn('restarted');
    });
    group
        .command('run-foreground')
        .description('Run the daemon in the current process (foreground; used by `start` spawn).')
        // CAT.6: project-less general session — umbrella=general, the built-in
        // `general` pack, no project uuid (umbrella id is the scope identity).
        .option('--general', 'run the project-less general session (DM + General + All)')
        .action(async (opts) => {
        // CAT.6 — PROJECT-LESS general path. No cwd→project resolution: the
        // umbrella id `general` is the scope identity, the daemon runs
        // projectLess, and the built-in `general` pack is the responder.
        if (opts.general === true) {
            const generalPackRoot = resolveGeneralPackRoot(env);
            const daemon = new Ctor({
                projectUuid: '',
                packRoot: generalPackRoot,
                umbrellaId: GENERAL_UMBRELLA,
                projectLess: true,
            });
            try {
                await daemon.start();
            }
            catch (e) {
                err(`agent-bridge: start failed: ${e instanceof Error ? e.message : String(e)}\n`);
                exit(1);
                return;
            }
            out(`agent-bridge: running (general, umbrella=${GENERAL_UMBRELLA}, pack=${generalPackRoot})\n`);
            await waitForever();
            return;
        }
        const projectUuid = await resolveProjectUuid({ cwd: cwd(), env });
        if (projectUuid === null) {
            err('agent-bridge: project UUID not found. ' +
                'Set OPENSQUID_PROJECT_UUID or run `opensquid setup chat` to create `.opensquid/project.json`.\n');
            exit(1);
            return;
        }
        const packRoot = resolvePackRootFromEnv(env);
        // CAT.5: resolve cwd → umbrella so the daemon runs umbrella-keyed (headless
        // lease handoff). A missing/absent channels.json or an unclaimed cwd leaves
        // umbrellaId undefined → the daemon falls back to legacy per-project keying.
        const umbrellaId = await resolveUmbrellaForCwdSafe(cwd());
        const daemon = new Ctor({
            projectUuid,
            packRoot,
            ...(umbrellaId !== null ? { umbrellaId } : {}),
        });
        try {
            await daemon.start();
        }
        catch (e) {
            err(`agent-bridge: start failed: ${e instanceof Error ? e.message : String(e)}\n`);
            exit(1);
            return;
        }
        out(`agent-bridge: running (project=${projectUuid}, pack=${packRoot})\n`);
        // Block until SIGTERM/SIGINT — the signal handlers in daemon.ts
        // call shutdown() + process.exit(0). The waitForever Promise gives
        // tests a seam to terminate the foreground process deterministically.
        await waitForever();
    });
    return group;
}
function pidPathFor(env) {
    const home = env.OPENSQUID_HOME ?? OPENSQUID_HOME();
    return join(home, 'agent-bridge.pid');
}
/** Read pid file + `kill -0` liveness check. */
async function readLivePid(env, killFn) {
    const p = pidPathFor(env);
    let raw;
    try {
        raw = await readFile(p, 'utf8');
    }
    catch {
        return { alive: false };
    }
    const pid = parseInt(raw.trim(), 10);
    if (!Number.isFinite(pid) || pid <= 0)
        return { alive: false };
    try {
        killFn(pid, 0);
        return { alive: true, pid };
    }
    catch (e) {
        // EPERM = process exists but owned by someone else (still counts alive).
        const code = e.code;
        if (code === 'EPERM')
            return { alive: true, pid };
        return { alive: false, stalePid: pid };
    }
}
/** Wait up to `timeoutMs` for the agent-bridge pidfile to disappear. */
async function waitForPidfileGone(env, timeoutMs) {
    const p = pidPathFor(env);
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        try {
            await readFile(p, 'utf8');
        }
        catch {
            return;
        }
        await new Promise((r) => setTimeout(r, 50));
    }
}
/**
 * Resolve a cwd → umbrella id via `channels.json` (CAT.5). Returns null when
 * there is no config, no umbrella claims the cwd, or any read error — the
 * daemon then runs legacy per-project keyed (no headless lease handoff). Never
 * throws: a broken routing config must not block the daemon from booting.
 */
async function resolveUmbrellaForCwdSafe(cwd) {
    try {
        const cfg = await loadChannelsConfig();
        if (cfg === null)
            return null;
        return resolveUmbrellaForCwd(cfg, cwd);
    }
    catch {
        return null;
    }
}
/** Re-exported for the parent CLI registration in src/cli.ts. */
export { agentBridgePidPath };
//# sourceMappingURL=cli.js.map