// packages/web/server/terminal-relay-node.ts
import { createServer } from "node:http";
import { mkdir, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { join as join2 } from "node:path";
import { createRequire as createRequire2 } from "node:module";

// packages/web/server/terminal-relay-session.ts
import { createRequire } from "module";
import { execFileSync, execSync } from "child_process";
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { join, dirname as pathDirname } from "path";
var require2 = createRequire(import.meta.url);
var pty = require2("@lydell/node-pty");
var DEFAULT_ORPHAN_TTL_MS = 30 * 60 * 1000;
var MAX_BUFFER_SIZE = 512 * 1024;
var sessions = new Map;
function generateId() {
  return Math.random().toString(36).slice(2, 10);
}
function send(ws, data) {
  if (ws.readyState === 1) {
    ws.send(JSON.stringify(data));
  }
}
function ptyFdClosed(err) {
  const message = err instanceof Error ? err.message : String(err);
  return message.includes("EBADF") || message.toLowerCase().includes("bad file descriptor");
}
function markSessionPtyClosed(session, err, op) {
  if (session.exited)
    return;
  session.exited = true;
  console.warn(`[relay] Session ${session.id}: PTY ${op} failed after fd closed (${err instanceof Error ? err.message : String(err)})`);
  scheduleReap(session, 1e4);
}
function sessionOwnsSocket(session, ws) {
  return session.ws === ws;
}
function writeSession(session, data) {
  if (session.exited)
    return false;
  try {
    session.pty.write(data);
    return true;
  } catch (err) {
    if (ptyFdClosed(err)) {
      markSessionPtyClosed(session, err, "write");
      return false;
    }
    throw err;
  }
}
function resizeSession(session, cols, rows) {
  if (session.exited)
    return false;
  try {
    session.pty.resize(cols, rows);
    if (session.backend === "tmux" && session.tmuxSession) {
      resizeTmuxWindow(session.tmuxSession, cols, rows);
    }
    session.cols = cols;
    session.rows = rows;
    return true;
  } catch (err) {
    if (ptyFdClosed(err)) {
      markSessionPtyClosed(session, err, "resize");
      return false;
    }
    throw err;
  }
}
function resolveCwd(raw) {
  const home = process.env.HOME || "/tmp";
  const expanded = (raw || home).replace(/^~/, home);
  if (existsSync(expanded))
    return expanded;
  try {
    mkdirSync(expanded, { recursive: true });
    console.log(`[relay] Created missing cwd: ${expanded}`);
    return expanded;
  } catch {
    console.warn(`[relay] Could not create cwd ${expanded}, falling back to ${home}`);
    return home;
  }
}
function findBin(name, envOverride) {
  if (envOverride && process.env[envOverride])
    return process.env[envOverride];
  try {
    return execSync(`which ${name}`, { encoding: "utf8" }).trim() || null;
  } catch {
    return null;
  }
}
function findClaudeBin() {
  return findBin("claude", "CLAUDE_BIN");
}
function findPiBin() {
  return findBin("pi", "PI_BIN");
}
function findShellBin() {
  const configured = process.env.SHELL;
  if (configured && existsSync(configured))
    return configured;
  return findBin("zsh") ?? findBin("bash") ?? findBin("sh") ?? (existsSync("/bin/sh") ? "/bin/sh" : null);
}
function normalizePiProviderForCli(provider) {
  if (!provider)
    return;
  if (provider === "copilot" || provider === "github")
    return "github-copilot";
  return provider;
}
function tmuxSessionExists(name) {
  try {
    execSync(`tmux has-session -t ${name} 2>/dev/null`, { encoding: "utf8" });
    return true;
  } catch {
    return false;
  }
}
function resizeTmuxWindow(name, cols, rows) {
  try {
    execFileSync("tmux", [
      "resize-window",
      "-t",
      name,
      "-x",
      String(cols),
      "-y",
      String(rows)
    ], { stdio: "ignore" });
    return true;
  } catch {
    return false;
  }
}
function bootstrapFiles(cwd, files, sessionId) {
  for (const [relPath, content] of Object.entries(files)) {
    const absPath = join(cwd, relPath);
    if (!existsSync(absPath)) {
      try {
        const dir = pathDirname(absPath);
        if (!existsSync(dir))
          mkdirSync(dir, { recursive: true });
        writeFileSync(absPath, content, "utf-8");
        console.log(`[relay] Session ${sessionId}: bootstrapped ${relPath}`);
      } catch (err) {
        console.warn(`[relay] Session ${sessionId}: failed to bootstrap ${relPath}:`, err);
      }
    }
  }
}
function spawnTmuxSession(tmuxName, cols, rows, cwd, commandBin, commandArgs, env) {
  const exists = tmuxSessionExists(tmuxName);
  if (!exists) {
    const shellCmd = [commandBin, ...commandArgs].map((a) => a.includes(" ") ? `'${a}'` : a).join(" ");
    execSync(`tmux new-session -d -s ${tmuxName} -x ${cols} -y ${rows} -c '${cwd}' '${shellCmd}'`, { env });
    console.log(`[relay] Created tmux session: ${tmuxName}`);
  } else {
    resizeTmuxWindow(tmuxName, cols, rows);
    console.log(`[relay] Attaching to existing tmux session: ${tmuxName}`);
  }
  return pty.spawn("tmux", ["attach", "-t", tmuxName], {
    name: "xterm-256color",
    cols,
    rows,
    cwd,
    env
  });
}
function createSession(ws, msg) {
  const id = generateId();
  const cols = Math.max(msg.cols || 80, 20);
  const rows = Math.max(msg.rows || 24, 4);
  const backend = msg.backend || "pty";
  const tmuxName = msg.tmuxSession || `hudson-${id}`;
  const agent = msg.agent || "claude";
  let agentBin;
  if (agent === "shell") {
    agentBin = findShellBin();
    if (!agentBin) {
      const reason = "No login shell found. Set SHELL or install zsh, bash, or sh.";
      console.error(`[relay] Session ${id} failed: ${reason}`);
      send(ws, { type: "session:error", error: reason });
      return null;
    }
  } else if (agent === "pi") {
    agentBin = findPiBin();
    if (!agentBin) {
      const reason = "pi CLI not found. Install it with: npm install -g @mariozechner/pi-coding-agent";
      console.error(`[relay] Session ${id} failed: ${reason}`);
      send(ws, { type: "session:error", error: reason });
      return null;
    }
  } else {
    agentBin = findClaudeBin();
    if (!agentBin) {
      const reason = "Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code";
      console.error(`[relay] Session ${id} failed: ${reason}`);
      send(ws, { type: "session:error", error: reason });
      return null;
    }
  }
  if (!existsSync(agentBin)) {
    const reason = `${agent} binary not found at ${agentBin}`;
    console.error(`[relay] Session ${id} failed: ${reason}`);
    send(ws, { type: "session:error", error: reason });
    return null;
  }
  if (backend === "tmux" && !findBin("tmux")) {
    const reason = "tmux not found. Install it with: brew install tmux";
    console.error(`[relay] Session ${id} failed: ${reason}`);
    send(ws, { type: "session:error", error: reason });
    return null;
  }
  const cwd = resolveCwd(msg.cwd);
  if (msg.workspaceFiles) {
    bootstrapFiles(cwd, msg.workspaceFiles, id);
  }
  let agentArgs;
  if (agent === "shell") {
    const shellName = agentBin.split("/").pop() ?? "";
    agentArgs = shellName === "sh" ? [] : ["-l"];
  } else if (agent === "pi") {
    agentArgs = ["--verbose"];
    const provider = normalizePiProviderForCli(msg.provider);
    if (provider)
      agentArgs.push("--provider", provider);
    if (msg.model)
      agentArgs.push("--model", msg.model);
    if (msg.systemPrompt)
      agentArgs.push("--system-prompt", msg.systemPrompt);
  } else {
    agentArgs = ["--verbose"];
    if (msg.systemPrompt)
      agentArgs.push("--system-prompt", msg.systemPrompt);
  }
  const env = { ...process.env, TERM: "xterm-256color", FORCE_COLOR: "1" };
  delete env.CLAUDECODE;
  let ptyProcess;
  try {
    if (backend === "tmux") {
      console.log(`[relay] Session ${id}: tmux backend (session: ${tmuxName}) in ${cwd} [agent: ${agent}]`);
      ptyProcess = spawnTmuxSession(tmuxName, cols, rows, cwd, agentBin, agentArgs, env);
    } else {
      console.log(`[relay] Session ${id}: pty backend, spawning ${agentBin} in ${cwd} [agent: ${agent}]`);
      ptyProcess = pty.spawn(agentBin, agentArgs, {
        name: "xterm-256color",
        cols,
        rows,
        cwd,
        env
      });
    }
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    console.error(`[relay] Session ${id}: failed to spawn PTY — ${message}`);
    send(ws, { type: "session:error", error: `Failed to spawn terminal: ${message}` });
    return null;
  }
  const orphanTTL = msg.orphanTTL && msg.orphanTTL > 0 ? msg.orphanTTL : DEFAULT_ORPHAN_TTL_MS;
  const session = {
    id,
    pty: ptyProcess,
    ws,
    outputBuffer: "",
    cols,
    rows,
    reapTimer: null,
    orphanTTL,
    backend,
    ...backend === "tmux" ? { tmuxSession: tmuxName } : {},
    exited: false,
    exitCode: null
  };
  const startTime = Date.now();
  ptyProcess.onData((data) => {
    session.outputBuffer += data;
    if (session.outputBuffer.length > MAX_BUFFER_SIZE) {
      session.outputBuffer = session.outputBuffer.slice(-MAX_BUFFER_SIZE);
    }
    if (session.ws && session.ws.readyState === 1) {
      send(session.ws, { type: "terminal:data", data });
    }
  });
  ptyProcess.onExit(({ exitCode }) => {
    session.exited = true;
    session.exitCode = exitCode;
    const uptime = Date.now() - startTime;
    const crashed = exitCode !== 0 && uptime < 5000;
    let reason;
    if (crashed) {
      const clean = session.outputBuffer.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").trim();
      const lines = clean.split(`
`).filter((l) => l.trim()).slice(-5);
      reason = lines.join(`
`) || `Process exited with code ${exitCode}`;
      console.error(`[relay] Session ${id} crashed after ${uptime}ms (code ${exitCode}): ${reason}`);
    }
    if (session.ws) {
      send(session.ws, { type: "session:exit", exitCode, ...reason ? { reason } : {} });
    }
    scheduleReap(session, 1e4);
  });
  sessions.set(id, session);
  console.log(`[relay] Session ${id} created (${cols}x${rows})`);
  return session;
}
function attachSession(session, ws, cols, rows) {
  if (session.reapTimer) {
    clearTimeout(session.reapTimer);
    session.reapTimer = null;
  }
  session.ws = ws;
  if (cols && rows) {
    const c = Math.max(cols, 20);
    const r = Math.max(rows, 4);
    if (c !== session.cols || r !== session.rows) {
      resizeSession(session, c, r);
    }
  }
  if (session.exited) {
    send(ws, { type: "session:exit", exitCode: session.exitCode });
  } else if (session.outputBuffer.length > 0) {
    send(ws, { type: "terminal:data", data: session.outputBuffer });
  }
  console.log(`[relay] Session ${session.id} reconnected`);
}
function detachSession(session) {
  session.ws = null;
  if (session.exited) {
    scheduleReap(session, 5000);
  } else {
    scheduleReap(session, session.orphanTTL);
    console.log(`[relay] Session ${session.id} detached (orphaned for ${session.orphanTTL / 1000}s)`);
  }
}
function scheduleReap(session, delay) {
  if (session.reapTimer)
    clearTimeout(session.reapTimer);
  session.reapTimer = setTimeout(() => {
    if (!session.ws) {
      destroy(session.id);
    }
  }, delay);
}
function destroy(sessionId) {
  const session = sessions.get(sessionId);
  if (!session)
    return;
  if (session.reapTimer)
    clearTimeout(session.reapTimer);
  try {
    session.pty.kill();
  } catch {}
  sessions.delete(sessionId);
  if (session.backend === "tmux") {
    console.log(`[relay] Session ${sessionId} bridge destroyed (tmux session '${session.tmuxSession}' still alive)`);
  } else {
    console.log(`[relay] Session ${sessionId} destroyed`);
  }
}

// packages/web/server/terminal-relay-node.ts
process.title = "scout-relay";
var require3 = createRequire2(import.meta.url);
var { WebSocketServer } = require3("ws");
var hostname = process.env.OPENSCOUT_WEB_TERMINAL_RELAY_HOST?.trim() || process.env.OPENSCOUT_WEB_HOST?.trim() || process.env.SCOUT_WEB_HOST?.trim() || "127.0.0.1";
var port = Number.parseInt(process.env.OPENSCOUT_WEB_TERMINAL_RELAY_PORT?.trim() || "3201", 10);
var UPLOAD_DIR = "/tmp/scout-uploads";
var pendingCommand = null;
function queueTerminalCommand(input) {
  if (!input.cwd) {
    for (const [, session] of sessions) {
      if (writeSession(session, input.command + `
`)) {
        return;
      }
    }
  }
  pendingCommand = input;
}
function drainPendingCommand() {
  const command = pendingCommand;
  pendingCommand = null;
  return command;
}
function parseMessage(raw) {
  try {
    const message = JSON.parse(raw);
    if (typeof message.type === "string") {
      return message;
    }
  } catch {}
  return null;
}
function setCors(res) {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
}
async function readJson(req) {
  const chunks = [];
  for await (const chunk of req) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
  }
  if (chunks.length === 0) {
    return null;
  }
  try {
    return JSON.parse(Buffer.concat(chunks).toString("utf8"));
  } catch {
    return null;
  }
}
function writeJson(res, status, body) {
  res.writeHead(status, { "Content-Type": "application/json" });
  res.end(JSON.stringify(body));
}
function relayUploadPath(name) {
  return join2(UPLOAD_DIR, `${randomUUID()}-${name}`);
}
var server = createServer(async (req, res) => {
  setCors(res);
  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }
  const url = new URL(req.url || "/", `http://${hostname}:${port}`);
  if (req.method === "GET" && url.pathname === "/health") {
    let attachedSessions = 0;
    for (const session of sessions.values()) {
      if (session.ws) {
        attachedSessions += 1;
      }
    }
    writeJson(res, 200, {
      ok: true,
      surface: "openscout-terminal-relay",
      pid: process.pid,
      sessions: sessions.size,
      attachedSessions
    });
    return;
  }
  if (req.method === "POST" && (url.pathname === "/api/upload" || url.pathname === "/api/relay/upload")) {
    const body = await readJson(req);
    if (!body?.name || !body.data) {
      writeJson(res, 400, { error: "Missing name or data" });
      return;
    }
    await mkdir(UPLOAD_DIR, { recursive: true });
    const filepath = relayUploadPath(body.name);
    await writeFile(filepath, Buffer.from(body.data, "base64"));
    writeJson(res, 200, { path: filepath });
    return;
  }
  if (req.method === "POST" && url.pathname === "/api/terminal/run") {
    const body = await readJson(req);
    const command = body?.command?.trim();
    if (!command) {
      writeJson(res, 400, { error: "missing command" });
      return;
    }
    queueTerminalCommand({
      command,
      cwd: body?.cwd?.trim() || null,
      agentId: body?.agentId?.trim() || null
    });
    writeJson(res, 200, { ok: true });
    return;
  }
  writeJson(res, 404, { error: "Not found" });
});
var wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
  let sessionId = null;
  ws.on("message", (raw) => {
    const msg = parseMessage(raw.toString());
    if (!msg) {
      return;
    }
    switch (msg.type) {
      case "session:init": {
        const pending = drainPendingCommand();
        if (sessionId) {
          const previous = sessions.get(sessionId);
          if (previous && sessionOwnsSocket(previous, ws)) {
            detachSession(previous);
          }
        }
        const session = createSession(ws, pending?.cwd ? { ...msg, cwd: pending.cwd, agent: "shell" } : msg);
        if (!session) {
          break;
        }
        sessionId = session.id;
        send(ws, { type: "session:ready", sessionId: session.id });
        if (pending) {
          setTimeout(() => writeSession(session, pending.command + `
`), 400);
        }
        break;
      }
      case "session:reconnect": {
        const pending = drainPendingCommand();
        if (pending?.cwd) {
          if (sessionId) {
            const previous = sessions.get(sessionId);
            if (previous && sessionOwnsSocket(previous, ws)) {
              detachSession(previous);
            }
          }
          const session = createSession(ws, {
            type: "session:init",
            cols: msg.cols ?? 80,
            rows: msg.rows ?? 24,
            cwd: pending.cwd,
            agent: "shell"
          });
          if (!session) {
            break;
          }
          sessionId = session.id;
          send(ws, { type: "session:ready", sessionId: session.id });
          setTimeout(() => writeSession(session, pending.command + `
`), 400);
          break;
        }
        const existing = sessions.get(msg.sessionId);
        if (existing && !existing.exited) {
          if (sessionId && sessionId !== msg.sessionId) {
            const previous = sessions.get(sessionId);
            if (previous && sessionOwnsSocket(previous, ws)) {
              detachSession(previous);
            }
          }
          if (existing.ws && existing.ws !== ws) {
            send(existing.ws, { type: "session:detached" });
          }
          sessionId = existing.id;
          attachSession(existing, ws, msg.cols, msg.rows);
          send(ws, {
            type: "session:ready",
            sessionId: existing.id,
            reconnected: true
          });
          if (pending) {
            setTimeout(() => writeSession(existing, pending.command + `
`), 400);
          }
        } else {
          send(ws, { type: "session:expired", sessionId: msg.sessionId });
        }
        break;
      }
      case "terminal:input": {
        if (!sessionId) {
          return;
        }
        const session = sessions.get(sessionId);
        if (session && sessionOwnsSocket(session, ws)) {
          writeSession(session, msg.data);
        }
        break;
      }
      case "terminal:resize": {
        if (!sessionId) {
          return;
        }
        const session = sessions.get(sessionId);
        if (session && sessionOwnsSocket(session, ws)) {
          const cols = Math.max(msg.cols || 80, 20);
          const rows = Math.max(msg.rows || 24, 4);
          resizeSession(session, cols, rows);
        }
        break;
      }
    }
  });
  ws.on("close", () => {
    if (!sessionId) {
      return;
    }
    const session = sessions.get(sessionId);
    if (session && sessionOwnsSocket(session, ws)) {
      detachSession(session);
    }
    sessionId = null;
  });
  ws.on("error", (err) => {
    console.error("[relay] WebSocket error:", err.message);
    if (!sessionId) {
      return;
    }
    const session = sessions.get(sessionId);
    if (session && sessionOwnsSocket(session, ws)) {
      detachSession(session);
    }
    sessionId = null;
  });
});
server.listen(port, hostname, () => {
  console.log(`[relay] Server listening on http://${hostname}:${port} (HTTP + WebSocket)`);
});
var shutdown = () => {
  console.log(`
[relay] Shutting down...`);
  for (const [id] of sessions) {
    destroy(id);
  }
  wss.close();
  server.close(() => process.exit(0));
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
