diff --git a/packages/daemon/src/index.ts b/packages/daemon/src/index.ts index 67da54c..e40ce2b 100644 --- a/packages/daemon/src/index.ts +++ b/packages/daemon/src/index.ts @@ -29,12 +29,6 @@ type Decision = { decision: "allow" | "deny"; reason?: string }; const HOOK_PORT = Number(process.env.AGENT_HELM_HOOK_PORT ?? 8788); const AGENT_CMD = process.env.AGENT_CMD ?? "gemini"; -const cmd = process.argv[2]; -if (cmd === "hook") void runHookMode(); -else if (cmd === "logout") logout(); -else if (cmd === "whoami") whoami(); -else void runConnect(); - /* ------------------------------ creds-hantering ---------------------------- */ interface Creds { @@ -184,21 +178,17 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s const AGENT_CWD = process.env.AGENT_CWD ?? process.cwd(); const HOOK_DEFAULT: "allow" | "deny" = process.env.AGENT_HELM_HOOK_DEFAULT === "deny" ? "deny" : "allow"; const HOOK_TIMEOUT = Number(process.env.AGENT_HELM_HOOK_TIMEOUT ?? 120000); + // Explicit hook-port om satt; annars en ledig (ephemeral) port så flera + // instanser på samma host inte krockar. Faktiska porten skickas till agenten. + const explicitHookPort = process.env.AGENT_HELM_HOOK_PORT ? Number(process.env.AGENT_HELM_HOOK_PORT) : 0; const COLS = 100; const ROWS = 30; - const pty: IPty = spawn(AGENT_CMD, AGENT_ARGS, { - name: "xterm-256color", - cols: COLS, - rows: ROWS, - cwd: AGENT_CWD, - env: { ...process.env, AGENT_HELM_HOOK_PORT: String(HOOK_PORT) } as Record, - }); - console.log(`[daemon] startade '${AGENT_CMD}' (pid ${pty.pid})`); - let ws: WebSocket | null = null; let reconnectTimer: ReturnType | null = null; let authFailed = false; + let pty: IPty | null = null; + let started = false; const pending = new Map void>(); function send(msg: DaemonMessage): void { @@ -215,7 +205,7 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s const msg = parseMessage<{ type: string; [k: string]: any }>(raw.toString()); if (!msg) return; if (msg.type === "server:input") { - pty.write(msg.data); + pty?.write(msg.data); } else if (msg.type === "server:approval-decision") { const resolve = pending.get(msg.approvalId); if (resolve) { @@ -232,7 +222,7 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s }); ws.on("close", () => { if (authFailed) { - pty.kill(); + pty?.kill(); process.exit(1); } console.log("[daemon] anslutning stängd — återansluter om 2s"); @@ -249,14 +239,34 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s }, 2000); } - pty.onData((data) => send({ type: "daemon:output", data })); - pty.onExit(({ exitCode }) => { - console.log(`[daemon] agenten avslutades (kod ${exitCode})`); - send({ type: "daemon:exit", code: exitCode }); - setTimeout(() => process.exit(exitCode), 200); - }); + function startPty(hookPort: number): void { + if (started) return; + started = true; + pty = spawn(AGENT_CMD, AGENT_ARGS, { + name: "xterm-256color", + cols: COLS, + rows: ROWS, + cwd: AGENT_CWD, + env: { ...process.env, AGENT_HELM_HOOK_PORT: String(hookPort) } as Record, + }); + console.log(`[daemon] startade '${AGENT_CMD}' (pid ${pty.pid})`); + pty.onData((data) => send({ type: "daemon:output", data })); + pty.onExit(({ exitCode }) => { + console.log(`[daemon] agenten avslutades (kod ${exitCode})`); + send({ type: "daemon:exit", code: exitCode }); + setTimeout(() => process.exit(exitCode), 200); + }); + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + connect(); + } - // Lokal hook-endpoint (endast 127.0.0.1). + function shutdown(): void { + pty?.kill(); + process.exit(0); + } + + // Lokal hook-endpoint (endast 127.0.0.1). Binds FÖRST så agenten får porten. const hookServer = http.createServer((req, res) => { if (req.method !== "POST" || req.url !== "/approve") { res.writeHead(404); @@ -293,18 +303,16 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s send({ type: "daemon:approval", approvalId, toolName, toolInput: parsed.toolInput }); }); }); - hookServer.listen(HOOK_PORT, "127.0.0.1", () => { - console.log(`[daemon] hook-endpoint på 127.0.0.1:${HOOK_PORT} (default vid ingen UI: ${HOOK_DEFAULT})`); + hookServer.on("error", (e: NodeJS.ErrnoException) => { + console.error(`[daemon] kunde inte binda hook-porten (${e.code ?? e.message}) — kör utan hook (default: ${HOOK_DEFAULT}).`); + startPty(explicitHookPort || HOOK_PORT); + }); + hookServer.listen(explicitHookPort, "127.0.0.1", () => { + const addr = hookServer.address(); + const port = typeof addr === "object" && addr ? addr.port : explicitHookPort; + console.log(`[daemon] hook-endpoint på 127.0.0.1:${port} (default vid ingen UI: ${HOOK_DEFAULT})`); + startPty(port); }); - - function shutdown(): void { - pty.kill(); - process.exit(0); - } - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); - - connect(); } /* --------------------------------- hook-läge ------------------------------- */ @@ -372,3 +380,14 @@ function askDaemon(toolName: string, toolInput: unknown): Promise { req.end(); }); } + +/* --------------------------------- dispatch -------------------------------- */ +// VIKTIGT: körs sist, efter att alla module-level const (CONFIG_DIR/CRED_FILE …) +// och funktioner initierats. Annars skulle runConnect:s synkrona del (fram till +// första await) köras under modul-init och läsa dessa const i TDZ → loadCreds() +// får en undefined path → returnerar null → onödigt nytt device-flöde. +const cmd = process.argv[2]; +if (cmd === "hook") void runHookMode(); +else if (cmd === "logout") logout(); +else if (cmd === "whoami") whoami(); +else void runConnect();