"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerStartCommand = registerStartCommand;
exports.isDaemonOlderThanCli = isDaemonOlderThanCli;
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const open_browser_js_1 = require("../open-browser.js");
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const daemon_discovery_js_1 = require("../../lib/daemon-discovery.js");
const port_utils_js_1 = require("../port-utils.js");
const runtime_env_js_1 = require("../runtime-env.js");
const shared_js_1 = require("../shared.js");
const ui_js_1 = require("../ui.js");
const update_js_1 = require("./update.js");
function registerStartCommand(program) {
    program
        .command('start')
        .option('--ui', 'Open browser UI after starting daemon')
        .option('--daemon-only', 'Skip cockpit (Next.js UI). Used by MCP auto-start.')
        .description('Start the Chorus daemon (PM2-style fork)')
        .action(async (options) => {
        try {
            const chorusDir = path_1.default.join(os_1.default.homedir(), '.chorus');
            // First-pass already-running check. If a daemon is up AND
            // (we don't need the cockpit OR the cockpit is also up),
            // bail out. Otherwise fall through to the upgrade path
            // below.
            //
            // `version_drifted` means alreadyRunningHealthy already
            // killed the stale daemon — we just fall through to the
            // normal spawn sequence below to bring up the new build.
            const alreadyHandled = await alreadyRunningHealthy(options.ui);
            if (alreadyHandled === 'satisfied')
                return;
            // Upgrade path: daemon is healthy but cockpit isn't running
            // and the user asked for --ui. Spawn just the cockpit and
            // update daemon.json. No need to acquire the start.lock —
            // we're not racing another start, just attaching the missing
            // process.
            if (alreadyHandled === 'cockpit_missing_ui_requested') {
                await spawnCockpitForExistingDaemon(chorusDir);
                return;
            }
            // Concurrent-start guard. Two MCP shims hitting auto-start
            // simultaneously would otherwise both pickPortPair → spawn
            // a daemon → write daemon.json. Whichever writes second
            // overwrites the first, leaving an orphan listening on a
            // forgotten port. Using O_EXCL on the lockfile means the
            // loser fails fast and falls through to alreadyRunningHealthy
            // on retry (after the winner finishes spawning).
            const acquired = acquireStartLock(chorusDir);
            if (!acquired) {
                // Another start is in flight. Poll briefly for daemon.json
                // to appear; if it shows up, we're done. If the lock is
                // stale (owner PID dead), reclaim it. Never blindly steal
                // the lock on timeout — the winner may just be slow.
                await waitForAnotherStartToWin();
                const second = await alreadyRunningHealthy(options.ui);
                if (second === 'satisfied')
                    return;
                if (second === 'cockpit_missing_ui_requested') {
                    await spawnCockpitForExistingDaemon(chorusDir);
                    return;
                }
                // No winner appeared. Check whether the lock owner is
                // actually dead before clearing — a slow but live winner
                // must NOT be elbowed aside or both processes will spawn.
                if (!isLockOwnerAlive(chorusDir)) {
                    clearStartLock(chorusDir);
                    if (!acquireStartLock(chorusDir)) {
                        throw new Error('Could not reclaim stale start lock. Try `chorus stop` then retry.');
                    }
                }
                else {
                    throw new Error('Another `chorus start` is still running. If this persists, run `chorus stop` then retry.');
                }
            }
            try {
                await reapOrphans();
                warnIfTmuxMissing();
                await captureAndPersistPath();
                const ports = await pickPortPair();
                await spawnDaemonAndCockpit(chorusDir, ports, {
                    daemonOnly: options.daemonOnly === true,
                });
                scheduleAutoOpenBrowser(options.ui, ports.cockpitPort);
            }
            finally {
                releaseStartLock(chorusDir);
            }
        }
        catch (error) {
            console.error('Failed to start daemon:', error);
            process.exit(1);
        }
    });
}
/**
 * Acquire ~/.chorus/start.lock with O_EXCL. Returns true on success;
 * false if another start has the lock. Stale-lock recovery is the
 * caller's responsibility.
 */
function acquireStartLock(chorusDir) {
    fs_1.default.mkdirSync(chorusDir, { recursive: true });
    const lockPath = path_1.default.join(chorusDir, 'start.lock');
    try {
        // 'wx' = O_CREAT | O_EXCL | O_WRONLY. Atomic create-or-fail.
        const fd = fs_1.default.openSync(lockPath, 'wx');
        fs_1.default.writeSync(fd, String(process.pid));
        fs_1.default.closeSync(fd);
        return true;
    }
    catch {
        return false;
    }
}
function releaseStartLock(chorusDir) {
    const lockPath = path_1.default.join(chorusDir, 'start.lock');
    try {
        fs_1.default.unlinkSync(lockPath);
    }
    catch {
        /* already gone */
    }
}
function clearStartLock(chorusDir) {
    releaseStartLock(chorusDir);
}
/**
 * Read the PID stamped into start.lock and check if that process is
 * still alive. Used to distinguish a stale lock (winner crashed before
 * unlinking) from a slow-but-live winner. Pre-fix the loser would
 * blindly steal the lock after an 8s timeout, allowing both processes
 * to spawn daemons concurrently when the winner happened to be slow.
 */
function isLockOwnerAlive(chorusDir) {
    const lockPath = path_1.default.join(chorusDir, 'start.lock');
    try {
        const raw = fs_1.default.readFileSync(lockPath, 'utf-8').trim();
        const pid = Number.parseInt(raw, 10);
        if (!Number.isFinite(pid) || pid <= 0)
            return false;
        return (0, daemon_discovery_js_1.isPidAlive)(pid);
    }
    catch {
        // Lock file vanished between the failed acquire and this read —
        // treat as "not alive" so the caller retries acquisition cleanly.
        return false;
    }
}
/**
 * The lock-loser polls for daemon.json to appear, then defers to
 * alreadyRunningHealthy. Bounded wait — the winner's spawn-and-record
 * flow takes ~5s on healthy machines.
 */
async function waitForAnotherStartToWin() {
    const deadline = Date.now() + 8000;
    while (Date.now() < deadline) {
        const live = await (0, daemon_discovery_js_1.readLiveDaemonInfo)({ healthTimeoutMs: 500 });
        if (live)
            return;
        await new Promise((r) => setTimeout(r, 200));
    }
}
/**
 * Run the user's interactive shell once and stash $PATH so the daemon
 * (running in a non-interactive shell that skips .bashrc/.zshrc) can
 * find CLIs the user installed via official curl-bash scripts.
 */
async function captureAndPersistPath() {
    try {
        const { captureInteractivePath, persistCapturedPath } = await import('../../lib/runtime-path.js');
        const captured = captureInteractivePath();
        if (captured)
            await persistCapturedPath(captured);
    }
    catch {
        /* non-fatal */
    }
}
/**
 * Pure version comparator used for drift detection. Returns true when
 * `cliVersion` is strictly newer than `daemonVersion`.
 *
 * Adds two policies on top of plain numeric semver compare (which is
 * what `versionGreater` already does):
 *   1. Missing / null / empty daemon version → treated as older. A
 *      pre-0.7 daemon (no `version` field in daemon.json) gets
 *      restarted on the next `chorus start`, picking up whatever the
 *      currently-installed CLI is.
 *   2. Equal versions → not older. No restart when versions match.
 *
 * Pre-release suffixes (1.2.3-rc.1) are NOT handled — the underlying
 * `versionGreater` does naive split-on-dot + parseInt, so any non-numeric
 * tail compares as zero. The CLI only publishes plain semver to npm
 * latest, so this hasn't bitten us yet; flagged for follow-up if we
 * ever ship RC tags.
 */
function isDaemonOlderThanCli(daemonVersion, cliVersion) {
    if (!daemonVersion)
        return true;
    if (daemonVersion === cliVersion)
        return false;
    return (0, update_js_1.versionGreater)(cliVersion, daemonVersion);
}
/**
 * Tear down a drifted daemon in-process so start can immediately respawn
 * on the current CLI's code. Mirrors the kill path in `chorus stop` but
 * lives inline because chained `chorus stop && chorus start` invocations
 * are exactly the pain we're removing — if the user has to know to run
 * stop first, the auto-restart UX win is gone.
 *
 * Best-effort: failures during kill don't abort the start. The downstream
 * spawn picks fresh ports if the old ones are still bound, and the port
 * sweep in `reapOrphans` mops up.
 */
async function killDriftedDaemon(daemonPid, cockpitPid) {
    await (0, port_utils_js_1.killAndVerify)(daemonPid, 'Daemon');
    if (cockpitPid !== null && cockpitPid > 0) {
        await (0, port_utils_js_1.killAndVerify)(cockpitPid, 'Cockpit');
    }
    (0, daemon_discovery_js_1.clearDaemonInfo)();
}
/**
 * Detect a healthy chorus already running on this host before we try
 * to spawn another one. Returns:
 *   - 'satisfied': nothing to do (or just printed status + opened
 *     browser). Caller should return immediately.
 *   - 'cockpit_missing_ui_requested': the daemon is alive but was
 *     started in --daemon-only mode; user just asked for --ui. Caller
 *     should upgrade by spawning only the cockpit.
 *   - 'not_running': fall through to the normal start sequence.
 *
 * Cross-uid hardening: daemon.json lives in $HOME, so we only see
 * *our* daemon. A sudo-started daemon's daemon.json lives in root's
 * $HOME and is invisible to the unprivileged probe. The orphan reaper
 * handles that case via cmdline + cwd matching.
 */
async function alreadyRunningHealthy(uiFlag) {
    // Longer health timeout than runtime resolution: WSL loopback is
    // slow on cold start (3-4s observed). We'd rather wait 5s once than
    // mis-diagnose a healthy daemon and pile a second one on top.
    const live = await (0, daemon_discovery_js_1.readLiveDaemonInfo)({ healthTimeoutMs: 5000 });
    if (!live)
        return 'not_running';
    // Drift detection: the running daemon may have been spawned by an
    // older CLI build that the user has since upgraded via `chorus update`
    // or `npm install -g`. Without restart, the new code on disk is dead
    // weight. Detecting drift here closes the "I updated but `chorus start`
    // still says v0.8.38" pain.
    //
    // Asymmetric handling:
    //   - daemon older than CLI → restart automatically (the user
    //     just upgraded; they want the new code live)
    //   - daemon equal to CLI → no action ("already running")
    //   - daemon NEWER than CLI → warn but do not restart. Restarting
    //     would downgrade the daemon to whatever the older PATH `chorus`
    //     points at, which is almost certainly not what the user wants
    //     (it typically means they have two installs — sudo and nvm —
    //     and the wrong one is in PATH).
    if (isDaemonOlderThanCli(live.version, shared_js_1.pkg.version)) {
        console.log('');
        console.log((0, ui_js_1.header)(ui_js_1.sym.pointer, 'Daemon is on an older version — restarting', `daemon ${live.version || '(unknown)'} → CLI ${shared_js_1.pkg.version}`));
        await killDriftedDaemon(live.daemonPid, live.cockpitPid);
        console.log('');
        return 'version_drifted';
    }
    const cockpitRunning = live.cockpitPid !== null && (0, daemon_discovery_js_1.isPidAlive)(live.cockpitPid);
    // Upgrade path: daemon up, no cockpit, user wants --ui. Tell the
    // caller to handle this without printing anything; the cockpit
    // spawn will print the URL when it lands.
    if (uiFlag && !cockpitRunning) {
        return 'cockpit_missing_ui_requested';
    }
    console.log('');
    // Show both the daemon-recorded version AND the CLI version when
    // they differ (daemon newer case). Equal case shows once. Helps
    // users diagnose two-install confusion at a glance.
    const daemonVersionLabel = live.version
        ? live.version === shared_js_1.pkg.version
            ? `version ${shared_js_1.pkg.version}`
            : `daemon ${live.version} · CLI ${shared_js_1.pkg.version}`
        : `version ${shared_js_1.pkg.version}`;
    console.log((0, ui_js_1.header)(ui_js_1.sym.ok, 'Chorus is already running', daemonVersionLabel));
    if (live.version && live.version !== shared_js_1.pkg.version) {
        // Daemon newer than CLI (drift detection above already caught the
        // opposite case). Surface the install-path mismatch so the user
        // can fix it deliberately rather than guessing.
        console.log('');
        console.log(ui_js_1.c.dim(`   Daemon is newer than this CLI — multiple chorus installs on PATH?`));
        console.log(ui_js_1.c.dim(`   This CLI: ${(0, update_js_1.resolveChorusBinaryPath)() ?? '(unknown)'}`));
    }
    if (cockpitRunning) {
        const cockpitUrl = `http://127.0.0.1:${live.cockpitPort}`;
        console.log('');
        console.log(`   ${ui_js_1.c.gray('Open')}  ${ui_js_1.c.cyan(cockpitUrl)}`);
        const env = (0, runtime_env_js_1.detectRuntimeEnv)();
        if (env.hint) {
            console.log('');
            console.log((0, ui_js_1.tip)(env.hint));
        }
        console.log('');
        if (uiFlag && (0, runtime_env_js_1.shouldAutoOpenBrowser)(env)) {
            await (0, open_browser_js_1.openBrowser)(cockpitUrl);
        }
    }
    else {
        console.log('');
        console.log(ui_js_1.c.dim('   Daemon-only mode. Run `chorus start --ui` to bring up the cockpit.'));
        console.log('');
    }
    return 'satisfied';
}
/**
 * Bring up just the Next.js cockpit and re-write daemon.json with the
 * new cockpitPid. Used when a `--daemon-only` daemon is already running
 * and the user now passes `--ui` to attach the UI without restarting.
 */
async function spawnCockpitForExistingDaemon(chorusDir) {
    const live = await (0, daemon_discovery_js_1.readLiveDaemonInfo)({ healthTimeoutMs: 5000 });
    if (!live) {
        // Edge case: daemon died between the alreadyRunningHealthy check
        // and this call. Caller's own logic will pick it up on retry.
        throw new Error('Daemon disappeared while attaching cockpit. Retry `chorus start`.');
    }
    const packageRoot = path_1.default.resolve(__dirname, '..', '..', '..');
    const nextEntry = path_1.default.resolve(packageRoot, 'node_modules', 'next', 'dist', 'bin', 'next');
    if (!fs_1.default.existsSync(nextEntry) ||
        !fs_1.default.existsSync(path_1.default.join(packageRoot, '.next'))) {
        console.log('');
        console.log(ui_js_1.c.red('  ✗ Cockpit UI not found. Try `npm install -g chorus-codes` to repair.'));
        console.log('');
        return;
    }
    const logsDir = path_1.default.join(chorusDir, 'logs');
    fs_1.default.mkdirSync(logsDir, { recursive: true });
    const webLogPath = path_1.default.join(logsDir, 'web.log');
    const webLogFd = fs_1.default.openSync(webLogPath, 'a');
    const webPidFile = path_1.default.join(chorusDir, 'web.pid');
    const webChild = (0, child_process_1.spawn)('node', [
        nextEntry,
        'start',
        '-p',
        String(live.cockpitPort),
        '-H',
        '127.0.0.1',
    ], {
        cwd: packageRoot,
        detached: true,
        stdio: ['ignore', webLogFd, webLogFd],
        env: {
            ...process.env,
            CHORUS_DAEMON_URL: `http://127.0.0.1:${live.daemonPort}`,
            PORT: String(live.cockpitPort),
        },
    });
    if (!webChild.pid) {
        throw new Error('Failed to spawn cockpit process');
    }
    fs_1.default.writeFileSync(webPidFile, webChild.pid.toString());
    webChild.unref();
    // Update daemon.json so future stops know about the cockpit PID.
    (0, daemon_discovery_js_1.writeDaemonInfo)({
        schemaVersion: 1,
        daemonPort: live.daemonPort,
        cockpitPort: live.cockpitPort,
        daemonPid: live.daemonPid,
        cockpitPid: webChild.pid,
        startedAt: live.startedAt,
        version: live.version,
    });
    console.log('');
    console.log((0, ui_js_1.header)(ui_js_1.sym.ok, 'Cockpit attached', `cockpit PID ${webChild.pid}`));
    const cockpitUrl = `http://127.0.0.1:${live.cockpitPort}`;
    console.log('');
    console.log(`   ${ui_js_1.c.gray('Open')}  ${ui_js_1.c.cyan(cockpitUrl)}`);
    const env = (0, runtime_env_js_1.detectRuntimeEnv)();
    if (env.hint) {
        console.log('');
        console.log((0, ui_js_1.tip)(env.hint));
    }
    // Cockpit just got a new PID. Same port-forward staleness concern
    // as a full daemon restart.
    const restart = (0, runtime_env_js_1.remoteRestartHint)(env);
    if (restart) {
        console.log('');
        console.log((0, ui_js_1.tip)(restart));
    }
    console.log('');
    if ((0, runtime_env_js_1.shouldAutoOpenBrowser)(env)) {
        await (0, open_browser_js_1.openBrowser)(cockpitUrl);
    }
}
/**
 * Pick a free (daemon, cockpit) port pair. Honours CHORUS_DAEMON_PORT
 * and CHORUS_COCKPIT_PORT env overrides as the *preferred* starting
 * point — the walk still fires off them if taken. If the walk
 * exhausts, exit with the same actionable diagnostic the v0.7 reaper
 * used.
 */
async function pickPortPair() {
    const preferredDaemon = parseEnvPort('CHORUS_DAEMON_PORT', daemon_discovery_js_1.DEFAULT_DAEMON_PORT);
    const preferredCockpit = parseEnvPort('CHORUS_COCKPIT_PORT', daemon_discovery_js_1.DEFAULT_COCKPIT_PORT);
    const daemonPort = await (0, daemon_discovery_js_1.pickFreePort)(preferredDaemon, daemon_discovery_js_1.DAEMON_PORT_RANGE, port_utils_js_1.isPortInUse);
    if (daemonPort === null) {
        failPortWalk('daemon', preferredDaemon, daemon_discovery_js_1.DAEMON_PORT_RANGE);
    }
    const cockpitPort = await (0, daemon_discovery_js_1.pickFreePort)(preferredCockpit, daemon_discovery_js_1.COCKPIT_PORT_RANGE, port_utils_js_1.isPortInUse);
    if (cockpitPort === null) {
        failPortWalk('cockpit', preferredCockpit, daemon_discovery_js_1.COCKPIT_PORT_RANGE);
    }
    return { daemonPort: daemonPort, cockpitPort: cockpitPort };
}
function parseEnvPort(name, fallback) {
    const raw = process.env[name];
    if (!raw)
        return fallback;
    const n = Number.parseInt(raw, 10);
    return Number.isFinite(n) && n > 0 && n < 65536 ? n : fallback;
}
function failPortWalk(label, start, range) {
    const end = start + range - 1;
    console.log('');
    console.log((0, ui_js_1.header)(ui_js_1.sym.err, `No free ${label} port in range :${start}–:${end}`, 'every candidate port is held by another process'));
    console.log('');
    console.log(ui_js_1.c.dim('  Find what owns these ports:'));
    for (let p = start; p <= end; p += 1) {
        console.log(`    sudo lsof -iTCP:${p} -sTCP:LISTEN`);
    }
    console.log('');
    console.log(ui_js_1.c.dim('  Or pick a different starting port:'));
    console.log(`    CHORUS_${label.toUpperCase()}_PORT=<port> chorus start`);
    console.log('');
    process.exit(1);
}
/**
 * Pre-spawn orphan reap. Sweeps the *default* daemon + cockpit ports
 * because that's where v0.7 daemons would have bound — we want to
 * absorb stale v0.7 processes during the v0.8 transition. The picker
 * still walks past whatever survives, so the reap is best-effort
 * cleanup, not a prerequisite.
 *
 * Foreign-process guard: only reap PIDs whose cmdline looks like a
 * chorus daemon/cockpit. If something else is bound, refuse to kill
 * it and ask the user to free the port — same behaviour as v0.7.
 */
async function reapOrphans() {
    for (const [port, label] of [
        [daemon_discovery_js_1.DEFAULT_DAEMON_PORT, 'daemon'],
        [daemon_discovery_js_1.DEFAULT_COCKPIT_PORT, 'cockpit'],
    ]) {
        if (!(await (0, port_utils_js_1.isPortInUse)(port)))
            continue;
        let pids = (0, port_utils_js_1.findPidsOnPort)(port);
        let needsSudoToKill = false;
        if (pids.length === 0) {
            pids = (0, port_utils_js_1.findPidsOnPortWithSudo)(port);
            needsSudoToKill = pids.length > 0;
        }
        if (pids.length === 0) {
            // Couldn't see who owns the default port — the picker will walk
            // past it. Don't fail; just note it.
            console.log('');
            console.log(ui_js_1.c.dim(`  ${ui_js_1.sym.info} Port :${port} is in use but the owner isn't visible. Will pick the next free port.`));
            continue;
        }
        for (const pid of pids) {
            const { match, cmdline } = (0, port_utils_js_1.pidLooksLikeChorus)(pid);
            if (!match) {
                // Foreign process on the default port — let the picker walk
                // past it. Don't fail.
                console.log('');
                console.log(ui_js_1.c.dim(`  ${ui_js_1.sym.info} Port :${port} is held by ${cmdline ?? `PID ${pid}`} — will pick the next free port.`));
                continue;
            }
            const dead = needsSudoToKill
                ? await (0, port_utils_js_1.killWithSudoAndVerify)(pid, `${label} orphan`)
                : await (0, port_utils_js_1.killAndVerify)(pid, `${label} orphan`);
            if (dead) {
                console.log(`  ${ui_js_1.sym.ok} reaped ${label} orphan on :${port} ${ui_js_1.c.dim(`(PID ${pid}${needsSudoToKill ? ', cross-uid via sudo' : ''})`)}`);
            }
        }
    }
}
/**
 * Default transport is 'headless' (no tmux needed). tmux is the
 * OPTIONAL backup mode for users who want to attach to a live voice
 * session and take over / watch step-by-step / hand off mid-run.
 */
function warnIfTmuxMissing() {
    try {
        (0, child_process_1.execFileSync)('tmux', ['-V'], { stdio: 'ignore' });
    }
    catch {
        console.log('');
        console.log(ui_js_1.c.dim(`  ${ui_js_1.sym.info} tmux not detected. Chorus runs headless by default — this is fine.`));
        console.log(ui_js_1.c.dim('    Optional backup mode: install tmux, then open'));
        console.log(ui_js_1.c.dim('    /settings#transport in the cockpit and pick "Tmux — attach & take over".'));
        console.log(ui_js_1.c.dim('    `tmux attach -t <name>` lets you watch step-by-step or take over mid-run.'));
        console.log(ui_js_1.c.dim('    macOS: brew install tmux · Ubuntu/Debian: apt install tmux · Fedora: dnf install tmux'));
        console.log('');
    }
}
async function spawnDaemonAndCockpit(chorusDir, ports, options = { daemonOnly: false }) {
    const daemonJs = path_1.default.resolve(__dirname, '..', '..', 'daemon', 'index.js');
    const daemonTs = path_1.default.resolve(__dirname, '..', '..', '..', 'src', 'daemon', 'index.ts');
    const useCompiled = fs_1.default.existsSync(daemonJs);
    const daemonPath = useCompiled ? daemonJs : daemonTs;
    const spawnArgs = useCompiled ? [daemonPath] : ['-r', 'tsx/cjs', daemonPath];
    fs_1.default.mkdirSync(chorusDir, { recursive: true });
    const logsDir = path_1.default.join(chorusDir, 'logs');
    fs_1.default.mkdirSync(logsDir, { recursive: true });
    const daemonLogPath = path_1.default.join(logsDir, 'daemon.log');
    const daemonLogFd = fs_1.default.openSync(daemonLogPath, 'a');
    const child = (0, child_process_1.spawn)('node', spawnArgs, {
        detached: true,
        stdio: ['ignore', daemonLogFd, daemonLogFd],
        env: {
            ...process.env,
            CHORUS_DAEMON_PORT: String(ports.daemonPort),
            CHORUS_COCKPIT_PORT: String(ports.cockpitPort),
        },
    });
    if (!child.pid) {
        throw new Error('Failed to spawn daemon process');
    }
    const pidFile = path_1.default.join(chorusDir, 'daemon.pid');
    fs_1.default.writeFileSync(pidFile, child.pid.toString());
    const packageRoot = path_1.default.resolve(__dirname, '..', '..', '..');
    const nextEntry = path_1.default.resolve(packageRoot, 'node_modules', 'next', 'dist', 'bin', 'next');
    const webPidFile = path_1.default.join(chorusDir, 'web.pid');
    let cockpitPid = null;
    if (options.daemonOnly) {
        // Skip cockpit spawn — used by MCP auto-start where the user
        // hasn't asked for the UI. Daemon API alone is enough for the
        // editor to make tool calls.
    }
    else if (fs_1.default.existsSync(nextEntry) &&
        fs_1.default.existsSync(path_1.default.join(packageRoot, '.next'))) {
        const webLogPath = path_1.default.join(logsDir, 'web.log');
        const webLogFd = fs_1.default.openSync(webLogPath, 'a');
        const webChild = (0, child_process_1.spawn)('node', [
            nextEntry,
            'start',
            '-p',
            String(ports.cockpitPort),
            '-H',
            '127.0.0.1',
        ], {
            cwd: packageRoot,
            detached: true,
            stdio: ['ignore', webLogFd, webLogFd],
            env: {
                ...process.env,
                // Tell the cockpit's server-side proxy where the daemon is.
                // Without this the proxy would fall through to the legacy
                // 7707 default and miss our shifted port.
                CHORUS_DAEMON_URL: `http://127.0.0.1:${ports.daemonPort}`,
                PORT: String(ports.cockpitPort),
            },
        });
        if (webChild.pid) {
            fs_1.default.writeFileSync(webPidFile, webChild.pid.toString());
            cockpitPid = webChild.pid;
            webChild.unref();
        }
    }
    else {
        console.log('');
        console.log(ui_js_1.c.red('  ✗ Cockpit UI not found.'));
        if (fs_1.default.existsSync(path_1.default.join(packageRoot, 'src'))) {
            console.log(ui_js_1.c.dim('    This looks like a dev checkout. Build it once:'));
            console.log(`    ${ui_js_1.c.bold('pnpm install && pnpm build')}`);
        }
        else {
            console.log(ui_js_1.c.dim('    The published install should ship a built UI. Try reinstalling:'));
            console.log(`    ${ui_js_1.c.bold('npm install -g chorus-codes')}`);
        }
        console.log(ui_js_1.c.dim(`    The daemon API is still up on port ${ports.daemonPort} if you only need MCP.`));
        console.log('');
    }
    // Wait for the daemon to answer health, THEN write daemon.json.
    // Must await: the parent CLI process exits as soon as this function
    // returns; if we fire-and-forget, the file never gets written and
    // every consumer falls back to defaults forever.
    await waitForDaemonListenerThenRecord(ports, child.pid, cockpitPid);
    child.unref();
    console.log('');
    console.log((0, ui_js_1.header)(ui_js_1.sym.ok, `Chorus started v${shared_js_1.pkg.version}`, `daemon PID ${child.pid}`));
    // Path of the resolved binary helps users diagnose multi-install
    // confusion (sudo npm install vs nvm-managed npm). Quiet by default
    // — only printed when running from a global install location, since
    // dev checkouts already know what binary they're running.
    const binPath = (0, update_js_1.resolveChorusBinaryPath)();
    if (binPath && binPath.includes('node_modules')) {
        console.log(`   ${ui_js_1.c.dim('from')}  ${ui_js_1.c.dim(binPath)}`);
    }
    if (!options.daemonOnly) {
        const cockpitUrl = `http://127.0.0.1:${ports.cockpitPort}`;
        console.log('');
        console.log(`   ${ui_js_1.c.gray('Open')}  ${ui_js_1.c.cyan(cockpitUrl)}`);
        const env = (0, runtime_env_js_1.detectRuntimeEnv)();
        if (env.hint) {
            console.log('');
            console.log((0, ui_js_1.tip)(env.hint));
        }
        // Fresh daemon spawn => new PID. On remote-dev hosts the editor's
        // port-forward proxy may still be bound to the old PID, so the
        // browser shows blank-page / connection-refused. Tell the user how
        // to fix it before they hit it.
        const restart = (0, runtime_env_js_1.remoteRestartHint)(env);
        if (restart) {
            console.log('');
            console.log((0, ui_js_1.tip)(restart));
        }
    }
    console.log('');
    // Async update check — fires in the background so the start path is
    // never blocked on a network call. Prints a one-line nudge after
    // the start banner if a newer version is on npm. Failures (offline,
    // registry timeout, etc.) silently skip — never burden the user with
    // "couldn't check for updates" noise on a healthy start.
    void checkForUpdate();
}
async function checkForUpdate() {
    try {
        const latest = await (0, update_js_1.fetchLatestVersion)();
        if (!latest)
            return;
        if (!(0, update_js_1.versionGreater)(latest, shared_js_1.pkg.version))
            return;
        console.log(`   ${ui_js_1.c.dim('•')} ${ui_js_1.c.cyan(`chorus ${latest}`)} ${ui_js_1.c.dim('is available — run')} ${ui_js_1.c.cyan('chorus update')}`);
        console.log('');
    }
    catch {
        /* never block a healthy start on a network failure */
    }
}
/**
 * Poll the daemon's health endpoint up to 5 seconds, then write
 * daemon.json once it responds. Background fire-and-forget — the
 * caller has already returned to the user; we just need to make sure
 * the file is in place before any consumer reads it.
 *
 * If the daemon never comes up, write the file anyway with the recorded
 * ports — better stale data than no data, since the next `chorus start`
 * will detect the dead daemon via PID liveness and overwrite.
 */
async function waitForDaemonListenerThenRecord(ports, daemonPid, cockpitPid) {
    const deadline = Date.now() + 5000;
    while (Date.now() < deadline) {
        try {
            const ac = new AbortController();
            const timer = setTimeout(() => ac.abort(), 500);
            const res = await fetch(`http://127.0.0.1:${ports.daemonPort}/api/v1/health`, { signal: ac.signal });
            clearTimeout(timer);
            if (res.ok)
                break;
        }
        catch {
            /* not up yet */
        }
        if (!(0, daemon_discovery_js_1.isPidAlive)(daemonPid)) {
            // Spawned process died before the listener came up — bail rather
            // than write a junk daemon.json.
            return;
        }
        await new Promise((r) => setTimeout(r, 100));
    }
    (0, daemon_discovery_js_1.writeDaemonInfo)({
        schemaVersion: 1,
        daemonPort: ports.daemonPort,
        cockpitPort: ports.cockpitPort,
        daemonPid,
        cockpitPid,
        startedAt: new Date().toISOString(),
        version: shared_js_1.pkg.version,
    });
}
function scheduleAutoOpenBrowser(uiFlag, cockpitPort) {
    setTimeout(() => {
        if (!uiFlag || !(0, runtime_env_js_1.shouldAutoOpenBrowser)((0, runtime_env_js_1.detectRuntimeEnv)()))
            return;
        // Catch the rejection here — a fire-and-forget setTimeout would
        // surface an unhandled rejection on hosts where `open` can't find
        // a browser (headless boxes, exotic envs).
        (0, open_browser_js_1.openBrowser)(`http://127.0.0.1:${cockpitPort}`).catch(() => {
            // Best-effort browser open; ignore failures silently — the cockpit
            // URL was already printed to the user above.
        });
    }, 1000);
}
//# sourceMappingURL=start.js.map