#!/usr/bin/env node
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { setProjectRoot, bobbitStateDir, migrateFromLegacyPiDir, globalAgentDir } from "./bobbit-dir.js";
import { scaffoldBobbitDir } from "./scaffold.js";
import { stageBundledBinaries } from "./binaries.js";
import { resolveSystemPromptPath } from "./agent/system-prompt.js";
import { loadOrCreateToken, readToken } from "./auth/token.js";
import { ensureTlsCert } from "./auth/tls.js";
import { loadDesecConfig, updateDesecIp } from "./auth/desec.js";
import { createGateway } from "./server.js";
import { loopbackForBind } from "./cli-loopback.js";
export { loopbackForBind };
/** Find the NordLynx (NordVPN mesh) interface IPv4 address, or null if not found. */
function findNordLynxIp() {
    const interfaces = os.networkInterfaces();
    for (const [name, addrs] of Object.entries(interfaces)) {
        if (!addrs)
            continue;
        if (!name.toLowerCase().includes("nordlynx"))
            continue;
        for (const addr of addrs) {
            if (addr.family === "IPv4" && !addr.internal) {
                return addr.address;
            }
        }
    }
    return null;
}
function parseArgs(argv) {
    const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : NaN;
    const result = {
        host: "", // resolved after parsing
        port: !isNaN(envPort) ? envPort : 3001,
        portExplicit: !isNaN(envPort),
        cwd: process.cwd(),
        newToken: false,
        showToken: false,
        noUi: false,
        tls: true, // on by default
        tlsExplicit: false,
        forceAuth: false,
    };
    for (let i = 0; i < argv.length; i++) {
        switch (argv[i]) {
            case "--host":
                result.host = argv[++i];
                break;
            case "--port":
                result.port = parseInt(argv[++i], 10);
                result.portExplicit = true;
                break;
            case "--cwd":
                result.cwd = path.resolve(argv[++i]);
                break;
            case "--new-token":
                result.newToken = true;
                break;
            case "--show-token":
                result.showToken = true;
                break;
            case "--static":
                result.staticDir = path.resolve(argv[++i]);
                break;
            case "--agent-cli":
                result.agentCliPath = path.resolve(argv[++i]);
                break;
            case "--no-ui":
                result.noUi = true;
                break;
            case "--auth":
                result.forceAuth = true;
                break;
            case "--tls":
                result.tls = true;
                result.tlsExplicit = true;
                break;
            case "--no-tls":
                result.tls = false;
                result.tlsExplicit = true;
                break;
            case "--nord": {
                const nordIp = findNordLynxIp();
                if (nordIp) {
                    result.host = nordIp;
                }
                else {
                    console.error("No NordLynx interface found. Is NordVPN meshnet active?");
                    process.exit(1);
                }
                break;
            }
        }
    }
    // Auto-detect embedded UI (dist/ui/) unless --no-ui or explicit --static
    if (!result.noUi && !result.staticDir) {
        const __dirname = path.dirname(fileURLToPath(import.meta.url));
        const embeddedUi = path.join(__dirname, "..", "ui");
        if (fs.existsSync(path.join(embeddedUi, "index.html"))) {
            result.staticDir = embeddedUi;
        }
    }
    return result;
}
async function main() {
    const args = parseArgs(process.argv.slice(2));
    // --show-token: print token and exit
    if (args.showToken) {
        const token = readToken();
        if (token) {
            console.log(token);
        }
        else {
            console.error("No token found. Run the gateway first to generate one.");
            process.exit(1);
        }
        return;
    }
    // Default to localhost unless --host or --nord was given
    if (!args.host) {
        args.host = "localhost";
    }
    // Set project root early — all stores resolve paths from this
    setProjectRoot(args.cwd);
    // Migrate legacy ~/.pi/agent/ to ~/.bobbit/agent/ if needed
    migrateFromLegacyPiDir();
    // Scaffold .bobbit/ on first run (creates config, extensions, state dirs)
    scaffoldBobbitDir(args.cwd);
    // Stage bundled fd/rg binaries into <agentDir>/bin so pi-coding-agent
    // finds them via its existing getToolPath() lookup. Idempotent; failures
    // log a single warning but never crash startup. See src/server/binaries.ts.
    try {
        await stageBundledBinaries(globalAgentDir());
    }
    catch (e) {
        console.warn(`[binaries] Staging failed: ${e.message}`);
    }
    const authToken = loadOrCreateToken(args.newToken);
    // Resolve active system prompt: user override under .bobbit/config/ or shipped default.
    const systemPromptPath = resolveSystemPromptPath();
    if (systemPromptPath) {
        console.log(`  System prompt: ${systemPromptPath}`);
    }
    // Auto-disable TLS for loopback to avoid self-signed cert warnings on localhost
    const isLoopback = args.host === "127.0.0.1" || args.host === "::1" || args.host === "localhost";
    if (isLoopback && !args.tlsExplicit) {
        args.tls = false;
        console.log("  Binding to localhost — TLS disabled (use --tls to override).");
    }
    // Load deSEC config early — domain is needed for TLS cert SAN
    const desecConfig = loadDesecConfig();
    const extraDomains = desecConfig ? [desecConfig.domain] : [];
    // TLS setup — auto-generate cert (mkcert CA preferred, openssl fallback)
    const tls = args.tls ? await ensureTlsCert(args.host, extraDomains) : undefined;
    // Update deSEC dynDNS if configured (keeps domain pointing to current mesh IP)
    // Skip for loopback addresses (e.g. E2E tests with --host 127.0.0.1) to avoid
    // clobbering the DNS record with an unreachable IP.
    if (desecConfig && !isLoopback) {
        updateDesecIp(desecConfig, args.host); // fire and forget
    }
    const gateway = createGateway({
        host: args.host,
        port: args.port,
        portExplicit: args.portExplicit,
        authToken,
        defaultCwd: args.cwd,
        staticDir: args.staticDir,
        agentCliPath: args.agentCliPath,
        systemPromptPath,
        tls,
        forceAuth: args.forceAuth,
    });
    const actualPort = await gateway.start();
    // Collect reachable addresses for display
    const interfaces = os.networkInterfaces();
    const addresses = [];
    for (const [name, addrs] of Object.entries(interfaces)) {
        if (!addrs)
            continue;
        for (const addr of addrs) {
            if (addr.family === "IPv4" && !addr.internal) {
                addresses.push(`${addr.address} (${name})`);
            }
        }
    }
    const proto = args.tls ? "https" : "http";
    const baseUrl = `${proto}://${args.host}:${actualPort}`;
    // peerUrl is what same-host agents use to call back into the gateway. For a
    // wildcard bind (`0.0.0.0` / `::`), normalise to loopback so the connect side
    // has a real peer address (the wildcard is a listen-only address).
    const peerUrl = `${proto}://${loopbackForBind(args.host)}:${actualPort}`;
    const fullUrl = `${baseUrl}/?token=${encodeURIComponent(authToken)}`;
    // Write gateway URL to a discoverable file so Vite proxy and extensions can find it.
    const gatewayUrlPath = path.join(bobbitStateDir(), "gateway-url");
    fs.writeFileSync(gatewayUrlPath, peerUrl, "utf-8");
    const __cliDir = path.dirname(fileURLToPath(import.meta.url));
    const pkgVersion = JSON.parse(fs.readFileSync(path.resolve(__cliDir, "../../package.json"), "utf-8")).version;
    // Set terminal tab title
    process.stdout.write(`\x1b]0;Bobbit Server\x07`);
    console.log(`\nBobbit Gateway v${pkgVersion}`);
    console.log(`  Listening:  ${baseUrl}`);
    console.log(`  Auth token: ${authToken}`);
    console.log(`  Agent CWD:  ${args.cwd}`);
    if (args.staticDir) {
        console.log(`  UI:         ${fullUrl}`);
    }
    if (addresses.length > 0) {
        console.log(`  Accessible from: ${addresses.join(", ")}`);
    }
    console.log();
    console.log(`  \u26A0 This token grants full shell access to this machine.`);
    console.log(`  Keep it secret. Regenerate with --new-token.`);
    console.log();
    // Auto-open browser when serving the UI, passing token so the UI auto-connects.
    // Skipped when:
    //   - BOBBIT_NO_OPEN is set (explicit opt-out)
    //   - NODE_ENV === "test" (manual integration tests + any test harness; prevents
    //     browser tab spam from per-test gateway spawns)
    const suppressOpen = process.env.BOBBIT_NO_OPEN || process.env.NODE_ENV === "test";
    if (args.staticDir && !suppressOpen) {
        const cmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
        import("node:child_process").then(({ exec }) => exec(`${cmd} ${fullUrl}`));
    }
    // Graceful shutdown
    const shutdown = async () => {
        console.log("\nShutting down...");
        await gateway.shutdown();
        process.exit(0);
    };
    process.on("SIGINT", shutdown);
    process.on("SIGTERM", shutdown);
}
// Global error handlers — prevent silent zombification from stray rejections
process.on("unhandledRejection", (reason) => {
    console.error("[gateway] Unhandled rejection:", reason);
});
process.on("uncaughtException", (err) => {
    // EPIPE from writing to a dead child process stdin — not fatal, the session
    // will see a "process exited" error and handle it. Don't crash the gateway.
    if (err.code === "EPIPE") {
        console.warn("[gateway] EPIPE (ignored — child process stdin closed)");
        return;
    }
    // ENOTCONN on Windows when spawning a child process — the socket pair for
    // stdin/stdout/stderr fails transiently (e.g. under high fd pressure).
    // Same class of error as EPIPE — the calling code will see the spawn failure
    // and handle it. Don't crash the gateway.
    if (err.code === "ENOTCONN") {
        console.warn("[gateway] ENOTCONN (ignored — child process socket creation failed)");
        return;
    }
    console.error("[gateway] Uncaught exception:", err);
    process.exit(1);
});
main().catch((err) => {
    console.error("Fatal:", err);
    process.exit(1);
});
//# sourceMappingURL=cli.js.map