#!/usr/bin/env node
import {
  DEFAULT_HCLOUD_ENDPOINT,
  HetznerApiError,
  createPerBoxFirewall,
  deletePerBoxFirewall,
  detectEgressIp,
  ensureHetznerCredentials,
  ensureHetznerEnvLoaded,
  isAttemptTimeout,
  isRetriable,
  makeHetznerClient,
  maskKey,
  normalizeSourceCidr,
  readHetznerCredStatus,
  secretsPath,
  sshOnlyInboundRule,
  syncFirewallSource,
  withHetznerRetry
} from "./chunk-R5XIDQFR.js";
import {
  createCloudProvider
} from "./chunk-MXXXKJYS.js";
import {
  stageClaudeCredentialsForUpload,
  stageClaudeStaticForUpload,
  stageCodexCredentialsForUpload,
  stageCodexStaticForUpload,
  stageOpencodeCredentialsForUpload,
  stageOpencodeStaticForUpload
} from "./chunk-ZJXTIH6C.js";
import {
  computeContextSha256,
  preparedStatePathFor,
  readCliStamp,
  readPreparedStateRaw,
  writePreparedStateRaw
} from "./chunk-SNTHHWKY.js";
import "./chunk-G3H2L3O2.js";

// ../../packages/sandbox-hetzner/dist/index.js
import { existsSync as existsSync3 } from "fs";
import { rm as rm2, rename, mkdir as mkdir3 } from "fs/promises";
import { join as join4 } from "path";
import { execa as execa4 } from "execa";
import { resolve as resolvePath } from "path";
import { join as join2 } from "path";
import { existsSync } from "fs";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import { mkdir, readFile } from "fs/promises";
import { dirname as dirname2, join, resolve as resolve2 } from "path";
import { execa } from "execa";
import { execa as execa2 } from "execa";
import { existsSync as existsSync2 } from "fs";
import { mkdir as mkdir2, rm } from "fs/promises";
import { createServer } from "net";
import { homedir } from "os";
import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
import { execa as execa3 } from "execa";
function generatePrepareCloudInit(opts) {
  const pubkey = opts.sshPubkey.trim();
  return [
    "#cloud-config",
    "# AgentBox temporary prepare VPS \u2014 used by `agentbox prepare --provider hetzner`",
    "# to bake the base snapshot. SSH key is single-use and discarded on VPS destroy.",
    "disable_root: false",
    "ssh_pwauth: false",
    // Hetzner's Ubuntu 24.04 stock image enforces a first-login password
    // change for root. With key-based auth that path can't run (no TTY),
    // so sshd refuses with "Password change required but no TTY available."
    // Telling cloud-init to NOT expire passwords + clearing root's expiry
    // via `passwd -d` removes the gate. Belt-and-braces: the chpasswd block
    // covers cloud-init's own users-and-groups run; the runcmd covers the
    // case where the image's pre-baked expiry survives cloud-init.
    "chpasswd:",
    "  expire: false",
    "users:",
    "  - name: root",
    "    lock_passwd: false",
    "    ssh_authorized_keys:",
    `      - ${yamlScalar(pubkey)}`,
    "runcmd:",
    "  - [ passwd, -d, root ]",
    '  - [ chage, -E, "-1", -I, "-1", -M, "99999", root ]',
    '  - [ bash, -lc, "echo agentbox-prepare-ready" ]',
    ""
  ].join("\n");
}
function generateBoxCloudInit(opts) {
  const pubkey = opts.sshPubkey.trim();
  const lines = [
    "#cloud-config",
    `# AgentBox per-box VPS \u2014 box '${opts.boxName}'`,
    "disable_root: true",
    "ssh_pwauth: false",
    // Same first-login expiry guard as the prepare cloud-init — keeps
    // Hetzner's Ubuntu hardening from blocking our key-based vscode login.
    "chpasswd:",
    "  expire: false",
    "users:",
    "  - name: vscode",
    "    lock_passwd: false",
    "    sudo: ALL=(ALL) NOPASSWD:ALL",
    "    ssh_authorized_keys:",
    `      - ${yamlScalar(pubkey)}`
  ];
  const writeFiles = [
    "    path: /etc/hosts",
    "    append: true",
    `    content: "127.0.0.1 ${opts.boxName}.localhost\\n"`
  ];
  lines.push("write_files:");
  lines.push("  - " + writeFiles[0]);
  for (let i = 1; i < writeFiles.length; i++) {
    lines.push("    " + writeFiles[i]);
  }
  if (opts.boxEnv && Object.keys(opts.boxEnv).length > 0) {
    const envContent = Object.entries(opts.boxEnv).map(([k, v]) => `${k}=${v}`).join("\\n") + "\\n";
    lines.push("  - path: /etc/agentbox/box.env");
    lines.push('    permissions: "0644"');
    lines.push(`    content: "${envContent}"`);
  }
  lines.push("");
  return lines.join("\n");
}
function yamlScalar(value) {
  if (/["\\]/.test(value)) {
    return JSON.stringify(value);
  }
  return `"${value}"`;
}
async function pollUntil(label, check, opts = {}) {
  const deadline = Date.now() + (opts.deadlineMs ?? 5 * 6e4);
  const max = opts.maxIntervalMs ?? 1e4;
  let interval = opts.intervalMs ?? 1e3;
  let attempt = 0;
  while (true) {
    attempt += 1;
    const out = await check();
    if (out !== null && out !== void 0) return out;
    if (Date.now() >= deadline) {
      throw new Error(`hetzner: timed out waiting for ${label} after ${String(attempt)} attempts`);
    }
    opts.onPoll?.(`${label}: not ready yet (attempt ${String(attempt)}); polling again in ${String(interval)}ms`);
    await new Promise((r) => setTimeout(r, interval));
    interval = Math.min(interval * 2, max);
  }
}
var SCHEMA = 2;
function preparedStatePath() {
  return preparedStatePathFor("hetzner");
}
function readPreparedState() {
  const raw = readPreparedStateRaw("hetzner");
  if (raw === null || typeof raw !== "object") return { schema: SCHEMA };
  const parsed = raw;
  if (parsed.schema === 1) {
    const v1 = parsed;
    return migrateFromV1(v1);
  }
  if (parsed.schema !== SCHEMA) {
    return { schema: SCHEMA };
  }
  return {
    schema: SCHEMA,
    base: parsed.base
  };
}
function migrateFromV1(v1) {
  const base = v1.base ? {
    imageId: v1.base.imageId,
    description: v1.base.description,
    createdAt: v1.base.createdAt,
    contextSha256: v1.base.installScriptSha256
  } : void 0;
  return {
    schema: SCHEMA,
    base
  };
}
function writePreparedState(state) {
  writePreparedStateRaw("hetzner", state);
}
function updatePreparedState(mutate) {
  const s = readPreparedState();
  mutate(s);
  writePreparedState(s);
}
var SELF = dirname(fileURLToPath(import.meta.url));
function findStagedCliRuntimeRoot() {
  const candidates = [
    resolve(SELF, "..", "runtime"),
    // <cliRoot>/dist/.. → <cliRoot> then /runtime
    resolve(SELF, "..", "..", "runtime")
    // chunk-NNNN.js at <cliRoot>/dist/<sub>/.. → <cliRoot>/runtime
  ];
  for (const c of candidates) {
    if (existsSync(resolve(c, "hetzner", "scripts", "install-box.sh"))) return c;
  }
  return void 0;
}
var RUNTIME_ASSETS = [
  { name: "install-box.sh", remoteBasename: "agentbox-install.sh", remoteMode: 493 },
  { name: "agentbox-ctl", remoteBasename: "agentbox-ctl", remoteMode: 493 },
  { name: "agentbox-vnc-start", remoteBasename: "agentbox-vnc-start", remoteMode: 493 },
  { name: "agentbox-dockerd-start", remoteBasename: "agentbox-dockerd-start", remoteMode: 493 },
  { name: "agentbox-checkpoint-cleanup", remoteBasename: "agentbox-checkpoint-cleanup", remoteMode: 493 },
  { name: "agentbox-open", remoteBasename: "agentbox-open", remoteMode: 493 },
  { name: "gh-shim", remoteBasename: "agentbox-gh-shim", remoteMode: 493 },
  { name: "git-shim", remoteBasename: "agentbox-git-shim", remoteMode: 493 },
  { name: "custom-system-CLAUDE.md", remoteBasename: "agentbox-custom-CLAUDE.md", remoteMode: 420 },
  { name: "claude-managed-settings.json", remoteBasename: "agentbox-managed-settings.json", remoteMode: 420 },
  { name: "agentbox-codex-hooks.json", remoteBasename: "agentbox-codex-hooks.json", remoteMode: 420 },
  { name: "agentbox-setup-skill.md", remoteBasename: "agentbox-setup-skill.md", remoteMode: 420 }
];
function candidatesFor(name, opts = {}) {
  const cliRoot = opts.cliRuntimeRoot;
  const monorepo = opts.repoRoot ?? guessRepoRoot();
  const monorepoRelative = {
    "install-box.sh": ["packages/sandbox-hetzner/scripts/install-box.sh"],
    "agentbox-ctl": ["packages/ctl/dist/bin.cjs"],
    "agentbox-vnc-start": ["packages/sandbox-docker/scripts/agentbox-vnc-start"],
    "agentbox-dockerd-start": ["packages/sandbox-docker/scripts/agentbox-dockerd-start"],
    "agentbox-checkpoint-cleanup": ["packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup"],
    "agentbox-open": ["packages/sandbox-docker/scripts/agentbox-open"],
    "gh-shim": ["packages/sandbox-docker/scripts/gh-shim"],
    "git-shim": ["packages/sandbox-docker/scripts/git-shim"],
    "custom-system-CLAUDE.md": ["packages/sandbox-hetzner/scripts/custom-system-CLAUDE.md"],
    "claude-managed-settings.json": ["packages/sandbox-docker/scripts/claude-managed-settings.json"],
    "agentbox-codex-hooks.json": ["packages/sandbox-docker/scripts/agentbox-codex-hooks.json"],
    "agentbox-setup-skill.md": ["apps/cli/share/agentbox-setup/SKILL.md"]
  };
  const cliRelative = {
    "install-box.sh": ["hetzner/scripts/install-box.sh"],
    "agentbox-ctl": ["hetzner/ctl.cjs"],
    "agentbox-vnc-start": ["hetzner/agentbox-vnc-start", "docker/packages/sandbox-docker/scripts/agentbox-vnc-start"],
    "agentbox-dockerd-start": ["hetzner/agentbox-dockerd-start", "docker/packages/sandbox-docker/scripts/agentbox-dockerd-start"],
    "agentbox-checkpoint-cleanup": ["hetzner/agentbox-checkpoint-cleanup", "docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup"],
    "agentbox-open": ["hetzner/agentbox-open", "docker/packages/sandbox-docker/scripts/agentbox-open"],
    "gh-shim": ["hetzner/gh-shim", "docker/packages/sandbox-docker/scripts/gh-shim"],
    "git-shim": ["hetzner/git-shim", "docker/packages/sandbox-docker/scripts/git-shim"],
    "custom-system-CLAUDE.md": ["hetzner/custom-system-CLAUDE.md"],
    "claude-managed-settings.json": ["hetzner/claude-managed-settings.json", "docker/packages/sandbox-docker/scripts/claude-managed-settings.json"],
    "agentbox-codex-hooks.json": ["hetzner/agentbox-codex-hooks.json", "docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json"],
    "agentbox-setup-skill.md": ["hetzner/agentbox-setup-skill.md", "docker/apps/cli/share/agentbox-setup/SKILL.md"]
  };
  const out = [];
  if (cliRoot) {
    for (const rel of cliRelative[name] ?? []) out.push(resolve(cliRoot, rel));
  }
  for (const rel of monorepoRelative[name] ?? []) out.push(resolve(monorepo, rel));
  return out;
}
function resolveRuntimeAssets(opts = {}) {
  const out = [];
  const missing = [];
  for (const asset of RUNTIME_ASSETS) {
    const cands = candidatesFor(asset.name, opts);
    const hit = cands.find((p) => existsSync(p));
    if (!hit) {
      missing.push({ name: asset.name, tried: cands });
      continue;
    }
    out.push({ ...asset, localPath: hit });
  }
  if (missing.length > 0) {
    const lines = missing.flatMap((m) => [`  - ${m.name}: tried`, ...m.tried.map((p) => `      ${p}`)]);
    throw new Error(
      `hetzner: could not resolve runtime assets \u2014 these files are needed to install on the prepare VPS:
` + lines.join("\n") + `

If you are running from the monorepo, ensure \`pnpm -w build\` has run so packages/ctl/dist/bin.cjs exists. If you are running from a published CLI bundle, the runtime/hetzner tree should be staged automatically.`
    );
  }
  return out;
}
function guessRepoRoot() {
  let cur = SELF;
  for (let i = 0; i < 8; i++) {
    if (existsSync(resolve(cur, "pnpm-workspace.yaml"))) return cur;
    const parent = dirname(cur);
    if (parent === cur) break;
    cur = parent;
  }
  return SELF;
}
async function mintSshKey(targetDir, comment) {
  const dir = resolve2(targetDir);
  const priv = join(dir, "id_ed25519");
  const pub = `${priv}.pub`;
  await mkdir(dir, { recursive: true, mode: 448 });
  await execa(
    "ssh-keygen",
    ["-t", "ed25519", "-N", "", "-C", comment, "-f", priv, "-q"],
    { stdio: "pipe" }
  );
  const publicKey = (await readFile(pub, "utf8")).trim();
  return { dir, privatePath: priv, publicPath: pub, publicKey };
}
async function mintPrepareKey() {
  const root = resolve2(homedirOrCwd(), ".agentbox", "hetzner", `prepare-${Date.now().toString(36)}`);
  const key = await mintSshKey(root, `agentbox-prepare-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
  return {
    ...key,
    cleanup: async () => {
      try {
        const { rm: rm3 } = await import("fs/promises");
        await rm3(dirname2(key.privatePath), { recursive: true, force: true });
      } catch {
      }
    }
  };
}
function homedirOrCwd() {
  try {
    return process.env.HOME ?? process.cwd();
  } catch {
    return process.cwd();
  }
}
function sshOptArgs(target) {
  const out = [
    "-i",
    target.identity,
    "-o",
    "StrictHostKeyChecking=accept-new",
    "-o",
    `UserKnownHostsFile=${target.knownHosts}`,
    "-o",
    "GlobalKnownHostsFile=/dev/null",
    "-o",
    "BatchMode=yes",
    "-o",
    "LogLevel=ERROR"
  ];
  if (target.controlPath) {
    out.push("-o", `ControlPath=${target.controlPath}`);
  }
  for (const [k, v] of Object.entries(target.options ?? {})) {
    out.push("-o", `${k}=${v}`);
  }
  return out;
}
async function sshExec(target, remoteCmd, opts = {}) {
  const argv = [
    ...sshOptArgs(target),
    `${target.user}@${target.host}`,
    remoteCmd
  ];
  const child = execa2("ssh", argv, {
    reject: false,
    timeout: opts.timeoutMs,
    env: { ...process.env, ...opts.env },
    stdio: opts.onLine ? ["ignore", "pipe", "pipe"] : ["ignore", "pipe", "pipe"]
  });
  if (opts.onLine) {
    const handle = (chunk) => {
      const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
      for (const line of text.split(/\r?\n/)) {
        if (line.length > 0) opts.onLine?.(line);
      }
    };
    child.stdout?.on("data", handle);
    child.stderr?.on("data", handle);
  }
  const res = await child;
  return {
    exitCode: typeof res.exitCode === "number" ? res.exitCode : 1,
    stdout: typeof res.stdout === "string" ? res.stdout : "",
    stderr: typeof res.stderr === "string" ? res.stderr : ""
  };
}
async function scpUpload(target, localPath, remotePath, opts = {}) {
  const argv = [
    ...sshOptArgs(target),
    localPath,
    `${target.user}@${target.host}:${remotePath}`
  ];
  const res = await execa2("scp", argv, {
    reject: false,
    timeout: opts.timeoutMs
  });
  if (res.exitCode !== 0) {
    throw new Error(
      `scp upload failed (exit ${String(res.exitCode)}): ${localPath} \u2192 ${remotePath}
${res.stderr ?? ""}`
    );
  }
}
async function scpDownload(target, remotePath, localPath, opts = {}) {
  const argv = [
    ...sshOptArgs(target),
    `${target.user}@${target.host}:${remotePath}`,
    localPath
  ];
  const res = await execa2("scp", argv, {
    reject: false,
    timeout: opts.timeoutMs
  });
  if (res.exitCode !== 0) {
    throw new Error(
      `scp download failed (exit ${String(res.exitCode)}): ${remotePath} \u2192 ${localPath}
${res.stderr ?? ""}`
    );
  }
}
async function waitForSsh(target, deadlineMs, intervalMs = 5e3) {
  const stop = Date.now() + deadlineMs;
  while (Date.now() < stop) {
    const res = await sshExec(
      { ...target, options: { ...target.options, ConnectTimeout: "5" } },
      "true",
      { timeoutMs: 1e4 }
    );
    if (res.exitCode === 0) return true;
    await new Promise((r) => setTimeout(r, intervalMs));
  }
  return false;
}
var TEMP_SERVER_TYPE_DEFAULT = "cx23";
var TEMP_SERVER_LOCATION_DEFAULT = "nbg1";
var PREPARE_SSH_DEADLINE_MS = 5 * 6e4;
var INSTALL_SCRIPT_TIMEOUT_MS = 30 * 6e4;
var SNAPSHOT_DEADLINE_MS = 20 * 6e4;
async function prepareHetzner(opts = {}) {
  await ensureHetznerCredentials();
  const client2 = makeHetznerClient();
  const log = opts.onLog ?? (() => {
  });
  const progress = (step) => log(`prepare-hetzner: ${step}`);
  const existingState = readPreparedState();
  const assets = resolveRuntimeAssets({
    cliRuntimeRoot: opts.cliRuntimeRoot ?? findStagedCliRuntimeRoot(),
    repoRoot: opts.repoRoot
  });
  const contextSha = await computeContextSha256(
    assets.map((a) => ({ rel: a.name, abs: a.localPath }))
  );
  if (!opts.force && existingState.base) {
    const remote = await client2.getImage(existingState.base.imageId).catch(() => null);
    if (remote && existingState.base.contextSha256 === contextSha) {
      progress(
        `base snapshot ${String(existingState.base.imageId)} already exists (fingerprint ${contextSha.slice(0, 12)} matches); skipping rebuild (pass --force to override)`
      );
      return {
        snapshotName: existingState.base.description,
        imageId: existingState.base.imageId
      };
    }
    if (!remote) {
      progress(`recorded base snapshot ${String(existingState.base.imageId)} is gone on Hetzner; rebuilding`);
    } else {
      progress(
        `build context changed (was ${existingState.base.contextSha256?.slice(0, 12) ?? "<none>"}, now ${contextSha.slice(0, 12)}); rebuilding base snapshot`
      );
    }
  }
  progress("minting ephemeral ssh key");
  const key = await mintPrepareKey();
  let firewallId = null;
  let serverId = null;
  try {
    progress("detecting host egress IP");
    const source = opts.firewallSource ? normalizeSourceCidr(opts.firewallSource) : `${await detectEgressIp({ onLog: log })}/32`;
    const stamp = Date.now().toString(36);
    const firewallName = `agentbox-prepare-${stamp}`;
    progress(`creating firewall ${firewallName} (source ${source})`);
    const firewall = await createPerBoxFirewall(client2, {
      name: firewallName,
      sourceCidr: source,
      labels: { "agentbox.role": "prepare" }
    });
    firewallId = firewall.id;
    const serverName = `agentbox-prepare-${stamp}`;
    const cloudInit = generatePrepareCloudInit({ sshPubkey: key.publicKey });
    progress(`creating temp VPS ${serverName} (${opts.serverType ?? TEMP_SERVER_TYPE_DEFAULT} / ${opts.location ?? TEMP_SERVER_LOCATION_DEFAULT})`);
    const created = await client2.createServer({
      name: serverName,
      server_type: opts.serverType ?? TEMP_SERVER_TYPE_DEFAULT,
      image: "ubuntu-24.04",
      location: opts.location ?? TEMP_SERVER_LOCATION_DEFAULT,
      user_data: cloudInit,
      firewalls: [{ firewall: firewall.id }],
      labels: { "agentbox.managed": "true", "agentbox.role": "prepare" },
      start_after_create: true
    });
    serverId = created.server.id;
    const ip = created.server.public_net.ipv4?.ip;
    if (!ip) {
      throw new Error("hetzner: temp VPS came up without an IPv4 address");
    }
    progress(`waiting for ssh on ${ip} (deadline ${String(PREPARE_SSH_DEADLINE_MS / 1e3)}s)`);
    const sshTarget = {
      host: ip,
      user: "root",
      identity: key.privatePath,
      knownHosts: join2(key.dir, "known_hosts")
    };
    const up = await waitForSsh(sshTarget, PREPARE_SSH_DEADLINE_MS);
    if (!up) {
      throw new Error(`hetzner: ssh on ${ip} did not come up within ${String(PREPARE_SSH_DEADLINE_MS / 1e3)}s`);
    }
    progress("ssh up \u2014 scp'ing runtime assets");
    for (const asset of assets) {
      const remote = `/tmp/${asset.remoteBasename}`;
      log(`prepare-hetzner: scp ${asset.name} -> ${remote}`);
      await scpUpload(sshTarget, asset.localPath, remote);
      if (asset.remoteMode !== void 0) {
        const modeOctal = asset.remoteMode.toString(8);
        await sshExec(sshTarget, `chmod ${modeOctal} ${remote}`);
      }
    }
    progress("running install-box.sh on temp VPS (this takes ~5-15 min)");
    const installRes = await sshExec(
      sshTarget,
      `sudo mkdir -p /var/log/agentbox && set -o pipefail && bash -x /tmp/agentbox-install.sh 2>&1 | sudo tee /var/log/agentbox/install.log`,
      {
        timeoutMs: INSTALL_SCRIPT_TIMEOUT_MS,
        onLine: (line) => log(`[install] ${line}`)
      }
    );
    if (installRes.exitCode !== 0) {
      throw new Error(
        `install-box.sh failed on temp VPS (exit ${String(installRes.exitCode)})
Last stderr: ${installRes.stderr.slice(-500) || "(empty)"}
The full trace was preserved at /var/log/agentbox/install.log inside any box made from the resulting snapshot.`
      );
    }
    progress("install script complete");
    progress("staging host agent static config");
    const stagings = [];
    try {
      const claudeTar = await stageClaudeStaticForUpload({ hostWorkspace: opts.hostWorkspace });
      for (const w of claudeTar.warnings) log(`prepare-hetzner: ${w}`);
      if (claudeTar.tarballPath) stagings.push({ kind: "claude", tar: claudeTar, dest: "/home/vscode/.claude" });
      else await claudeTar.cleanup();
      const codexTar = await stageCodexStaticForUpload();
      for (const w of codexTar.warnings) log(`prepare-hetzner: ${w}`);
      if (codexTar.tarballPath) stagings.push({ kind: "codex", tar: codexTar, dest: "/home/vscode/.codex" });
      else await codexTar.cleanup();
      const opencodeTar = await stageOpencodeStaticForUpload();
      for (const w of opencodeTar.warnings) log(`prepare-hetzner: ${w}`);
      if (opencodeTar.tarballPath) stagings.push({ kind: "opencode", tar: opencodeTar, dest: "/home/vscode/.local/share/opencode" });
      else await opencodeTar.cleanup();
      for (const s of stagings) {
        const remote = `/tmp/agentbox-${s.kind}-static.tar.gz`;
        log(`prepare-hetzner: scp ${s.kind} static (${s.tar.tarballPath}) -> ${remote}`);
        await scpUpload(sshTarget, s.tar.tarballPath, remote);
        const extractCmd = `sudo -u vscode mkdir -p ${s.dest} && sudo -u vscode tar -xzf ${remote} -C ${s.dest} --no-same-permissions --no-same-owner -m && rm -f ${remote}`;
        const r = await sshExec(sshTarget, extractCmd, { onLine: (line) => log(`[stage:${s.kind}] ${line}`) });
        if (r.exitCode !== 0) {
          throw new Error(
            `prepare-hetzner: ${s.kind} static extract failed (exit ${String(r.exitCode)}): ${r.stderr.slice(-300)}`
          );
        }
        progress(`baked ${s.kind} static config into snapshot`);
      }
    } finally {
      for (const s of stagings) await s.tar.cleanup();
    }
    const description = opts.name ?? `agentbox-base-${stamp}`;
    progress(`creating snapshot '${description}' from VPS ${String(serverId)}`);
    const snap = await client2.createImage(serverId, {
      type: "snapshot",
      description,
      labels: { "agentbox.role": "base", "agentbox.schema": "1" }
    });
    progress(`snapshot create requested (image id ${String(snap.image.id)}); polling until available`);
    const ready = await pollUntil(
      `image ${String(snap.image.id)} availability`,
      async () => {
        const img = await client2.getImage(snap.image.id);
        if (!img) return null;
        if (img.status === "available") return img;
        return null;
      },
      { deadlineMs: SNAPSHOT_DEADLINE_MS, intervalMs: 3e3, maxIntervalMs: 1e4, onPoll: (l) => log(`prepare-hetzner: ${l}`) }
    );
    progress("persisting hetzner-prepared.json");
    const state = readPreparedState();
    const cliStamp = readCliStamp();
    state.base = {
      imageId: ready.id,
      description: ready.description,
      createdAt: (/* @__PURE__ */ new Date()).toISOString(),
      contextSha256: contextSha,
      cliVersion: cliStamp.cliVersion,
      cliCommit: cliStamp.cliCommit
    };
    writePreparedState(state);
    log(`prepare-hetzner: wrote ${preparedStatePath()}`);
    progress(`deleting temp VPS ${String(serverId)}`);
    await client2.deleteServer(serverId);
    serverId = null;
    progress(`deleting per-prepare firewall ${String(firewallId)}`);
    await deletePerBoxFirewall(client2, firewallId);
    firewallId = null;
    progress(`prepare complete \u2014 base snapshot ${String(ready.id)} (${ready.description})`);
    return { snapshotName: ready.description, imageId: ready.id };
  } catch (err) {
    if (serverId !== null) {
      log(`prepare-hetzner: cleanup \u2014 deleting temp VPS ${String(serverId)} after failure`);
      try {
        await client2.deleteServer(serverId);
      } catch (cleanupErr) {
        log(
          `prepare-hetzner: WARN \u2014 failed to delete temp VPS ${String(serverId)}; check the Hetzner dashboard manually. ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`
        );
      }
    }
    if (firewallId !== null) {
      log(`prepare-hetzner: cleanup \u2014 deleting per-prepare firewall ${String(firewallId)} after failure`);
      try {
        await deletePerBoxFirewall(client2, firewallId);
      } catch (cleanupErr) {
        log(
          `prepare-hetzner: WARN \u2014 failed to delete firewall ${String(firewallId)}; check the Hetzner dashboard manually. ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`
        );
      }
    }
    throw err;
  } finally {
    await key.cleanup();
  }
}
var prepareHetznerProvider = (req) => prepareHetzner({
  name: req.name,
  hostWorkspace: req.hostWorkspace ?? process.cwd(),
  force: req.force,
  onLog: req.onLog
});
async function ensureHetznerBaseSnapshot() {
  const state = readPreparedState();
  if (state.base !== void 0) return;
  throw new Error(
    "no Hetzner base snapshot found.\nRun `agentbox prepare --provider hetzner` first (Hetzner cannot build images from a Dockerfile,\nso the base snapshot is a one-time prerequisite for cloud boxes on this backend)."
  );
}
var HOST = "127.0.0.1";
var EPHEMERAL_MIN = 49152;
var EPHEMERAL_MAX = 65535;
var SshTunnelManager = class {
  boxes = /* @__PURE__ */ new Map();
  /**
   * Open the ControlMaster for `boxId`. Idempotent: if a master is already
   * up for this box (socket exists + responsive), no-op. Otherwise spawn a
   * fresh `ssh -fNT -M` and wait for the socket to appear.
   */
  async open(opts) {
    const boxSshDir = opts.boxSshDir ?? defaultBoxSshDir(opts.boxId);
    await mkdir2(boxSshDir, { recursive: true, mode: 448 });
    const controlPath = join3(boxSshDir, "control.sock");
    const knownHosts = join3(boxSshDir, "known_hosts");
    const user = opts.vpsUser ?? "vscode";
    const tunnel = {
      controlPath,
      vpsHost: opts.vpsHost,
      vpsUser: user,
      identity: opts.identity,
      boxSshDir,
      forwards: /* @__PURE__ */ new Map()
    };
    if (existsSync2(controlPath) && await this.isAlive(controlPath)) {
      this.boxes.set(opts.boxId, tunnel);
      return;
    }
    if (existsSync2(controlPath)) {
      await rm(controlPath, { force: true });
    }
    const connectTimeout = opts.connectTimeoutSeconds ?? 10;
    const argv = [
      "-fNT",
      "-M",
      "-S",
      controlPath,
      "-i",
      opts.identity,
      "-o",
      "StrictHostKeyChecking=accept-new",
      "-o",
      `UserKnownHostsFile=${knownHosts}`,
      "-o",
      "GlobalKnownHostsFile=/dev/null",
      "-o",
      "BatchMode=yes",
      "-o",
      "LogLevel=ERROR",
      "-o",
      "ExitOnForwardFailure=yes",
      "-o",
      "ServerAliveInterval=30",
      "-o",
      "ServerAliveCountMax=3",
      "-o",
      `ConnectTimeout=${String(connectTimeout)}`,
      `${user}@${opts.vpsHost}`
    ];
    const res = await execa3("ssh", argv, { reject: false });
    if (res.exitCode !== 0 || !existsSync2(controlPath)) {
      throw new Error(
        `ssh ControlMaster failed for ${opts.boxId} (exit ${String(res.exitCode)}): ${res.stderr || res.stdout || "(no output)"}`
      );
    }
    this.boxes.set(opts.boxId, tunnel);
  }
  /**
   * Mint (or fetch the cached) `127.0.0.1:<localPort> → vps:127.0.0.1:<remotePort>`
   * forward. Returns the local port. Idempotent per (boxId, remotePort).
   *
   * The cached entry is only returned when the underlying ControlMaster is
   * still alive — without that check we'd happily hand back a localPort that
   * stopped listening when the master died (e.g. transient network blip,
   * host sleep/wake). When the master is dead we drop ALL cached forwards
   * for this box (they all share one tunnel) and re-mint from scratch.
   */
  async forward(boxId, remotePort) {
    const tunnel = this.getTunnelOrThrow(boxId);
    const cached = tunnel.forwards.get(remotePort);
    if (cached !== void 0 && await this.isAlive(tunnel.controlPath)) {
      return cached;
    }
    if (cached !== void 0) {
      tunnel.forwards.clear();
    }
    const localPort = await pickFreePort();
    const argv = [
      "-O",
      "forward",
      "-L",
      `${HOST}:${String(localPort)}:${HOST}:${String(remotePort)}`,
      "-S",
      tunnel.controlPath,
      "dummy"
      // the target host is ignored when -O is used, but argv needs one
    ];
    const res = await execa3("ssh", argv, { reject: false });
    if (res.exitCode !== 0) {
      throw new Error(
        `ssh -O forward failed for ${boxId} (exit ${String(res.exitCode)}): ${res.stderr || res.stdout || "(no output)"}`
      );
    }
    tunnel.forwards.set(remotePort, localPort);
    return localPort;
  }
  /**
   * Tear down a dead ControlMaster + every cached forward for this box,
   * then re-open from scratch. Idempotent — if the master is already alive
   * the master open() is a no-op, but the cached forwards still get
   * cleared so the next forward() call re-mints them. Returns when the
   * master is open and the box's forwards map is empty (ready for fresh
   * forward() calls).
   *
   * Use case: the cloud-poller observes ECONNREFUSED on the local port and
   * asks the backend to refresh the preview URL — that path calls into
   * here so the master + forward both come back fresh.
   */
  async refresh(opts) {
    const existing = this.boxes.get(opts.boxId);
    if (existing) {
      const alive = await this.isAlive(existing.controlPath);
      if (!alive && existsSync2(existing.controlPath)) {
        try {
          await execa3(
            "ssh",
            ["-O", "exit", "-S", existing.controlPath, "dummy"],
            { reject: false }
          );
        } catch {
        }
        await rm(existing.controlPath, { force: true });
      }
      existing.forwards.clear();
    }
    await this.open(opts);
  }
  /** Tear down a single forward. Idempotent — unknown ports are no-ops. */
  async unforward(boxId, remotePort) {
    const tunnel = this.getTunnelOrThrow(boxId);
    const localPort = tunnel.forwards.get(remotePort);
    if (localPort === void 0) return;
    const argv = [
      "-O",
      "cancel",
      "-L",
      `${HOST}:${String(localPort)}:${HOST}:${String(remotePort)}`,
      "-S",
      tunnel.controlPath,
      "dummy"
    ];
    await execa3("ssh", argv, { reject: false });
    tunnel.forwards.delete(remotePort);
  }
  /**
   * Close the ControlMaster (and all its forwards). Idempotent — if no
   * master is recorded, no-op. Removes the socket file.
   */
  async close(boxId) {
    const tunnel = this.boxes.get(boxId);
    if (!tunnel) return;
    if (existsSync2(tunnel.controlPath)) {
      await execa3("ssh", ["-O", "exit", "-S", tunnel.controlPath, "dummy"], { reject: false });
      await rm(tunnel.controlPath, { force: true });
    }
    this.boxes.delete(boxId);
  }
  /** Tear down every open box. */
  async closeAll() {
    const ids = Array.from(this.boxes.keys());
    await Promise.all(ids.map((id) => this.close(id)));
  }
  /** Path to the ControlMaster socket for `boxId`, if open. */
  controlPath(boxId) {
    return this.boxes.get(boxId)?.controlPath;
  }
  /** True if a ControlMaster is registered for `boxId` (regardless of liveness). */
  has(boxId) {
    return this.boxes.has(boxId);
  }
  /** Per-box ssh dir (tests use this to verify the layout). */
  boxSshDir(boxId) {
    return this.boxes.get(boxId)?.boxSshDir;
  }
  /** Re-open the manager record for an existing-on-disk control socket. */
  registerExisting(boxId, opts) {
    const boxSshDir = opts.boxSshDir ?? defaultBoxSshDir(opts.boxId);
    this.boxes.set(boxId, {
      controlPath: join3(boxSshDir, "control.sock"),
      vpsHost: opts.vpsHost,
      vpsUser: opts.vpsUser ?? "vscode",
      identity: opts.identity,
      boxSshDir,
      forwards: /* @__PURE__ */ new Map()
    });
  }
  async isAlive(controlPath) {
    const res = await execa3("ssh", ["-O", "check", "-S", controlPath, "dummy"], { reject: false });
    return res.exitCode === 0;
  }
  getTunnelOrThrow(boxId) {
    const t = this.boxes.get(boxId);
    if (!t) throw new Error(`no SSH ControlMaster registered for box ${boxId}; call open() first`);
    return t;
  }
};
function defaultBoxSshDir(boxId) {
  return resolve3(homedir(), ".agentbox", "boxes", boxId, "ssh");
}
async function pickFreePort() {
  return new Promise((resolveOk, reject) => {
    const srv = createServer();
    srv.unref();
    srv.on("error", reject);
    srv.listen(0, HOST, () => {
      const addr = srv.address();
      if (!addr || typeof addr === "string") {
        srv.close();
        reject(new Error("could not get a free local port"));
        return;
      }
      const port = addr.port;
      srv.close(() => {
        if (port < EPHEMERAL_MIN || port > EPHEMERAL_MAX) {
          resolveOk(port);
        } else {
          resolveOk(port);
        }
      });
    });
  });
}
var HETZNER_DEFAULT_BOX_IMAGE_REF = "agentbox-base";
var SCAFFOLDING_FALLBACK_IMAGE = "agentbox/box:dev";
var VPS_USER = "vscode";
var PROVISION_SSH_DEADLINE_MS = 5 * 6e4;
var ACTION_DEADLINE_MS = 5 * 6e4;
var SNAPSHOT_DEADLINE_MS2 = 20 * 6e4;
var HETZNER_DEFAULT_SERVER_TYPE = "cx23";
var HETZNER_DEFAULT_LOCATION = "nbg1";
var tunnels = new SshTunnelManager();
function mapState(s) {
  switch (s) {
    case "running":
      return "running";
    case "starting":
    case "initializing":
    case "stopping":
    case "migrating":
    case "rebuilding":
      return "running";
    case "off":
      return "paused";
    case "deleting":
    case "unknown":
    default:
      return "missing";
  }
}
function client() {
  return makeHetznerClient();
}
async function getServerStrict(id) {
  const s = await client().getServer(id);
  if (!s) {
    throw new Error(`hetzner: server ${String(id)} not found (already destroyed?)`);
  }
  return s;
}
async function findImageByDescription(c, description) {
  const all = await c.listImages({ type: "snapshot" });
  return all.find((i) => i.description === description) ?? null;
}
async function resolveImageId(c, req) {
  const ref = req.snapshot ?? req.image;
  if (!ref || ref === HETZNER_DEFAULT_BOX_IMAGE_REF || ref === SCAFFOLDING_FALLBACK_IMAGE) {
    await ensureHetznerBaseSnapshot();
    const state = readPreparedState();
    if (!state.base) {
      throw new Error(
        "no Hetzner base snapshot found \u2014 run `agentbox prepare --provider hetzner` to bake one."
      );
    }
    return state.base.imageId;
  }
  if (/^\d+$/.test(ref)) {
    return Number.parseInt(ref, 10);
  }
  const snap = await findImageByDescription(c, ref);
  if (snap) return snap.id;
  return ref;
}
function perBoxDir(sandboxId) {
  return resolvePath(defaultBoxSshDir(sandboxId), "..");
}
async function ensurePerBoxState(sandboxId) {
  const dir = perBoxDir(sandboxId);
  const sshDir = join4(dir, "ssh");
  await mkdir3(sshDir, { recursive: true, mode: 448 });
  return {
    dir,
    identity: join4(sshDir, "id_ed25519"),
    knownHosts: join4(sshDir, "known_hosts")
  };
}
function bashScript(s) {
  return `bash -lc ${shellQuote(s)}`;
}
function shellQuote(s) {
  return `'${s.replace(/'/g, `'\\''`)}'`;
}
function buildSshTarget(state, vpsIp, controlPath) {
  return {
    host: vpsIp,
    user: VPS_USER,
    identity: state.identity,
    knownHosts: state.knownHosts,
    controlPath
  };
}
async function ensureTunnel(sandboxId, state, vpsIp) {
  if (tunnels.has(sandboxId)) return;
  await tunnels.open({
    boxId: sandboxId,
    vpsHost: vpsIp,
    identity: state.identity
  });
}
async function ensureLiveTarget(sandboxId) {
  const id = Number.parseInt(sandboxId, 10);
  if (!Number.isFinite(id)) {
    throw new Error(`hetzner: invalid sandboxId ${sandboxId}`);
  }
  const server = await getServerStrict(id);
  const vpsIp = server.public_net.ipv4?.ip;
  if (!vpsIp) {
    throw new Error(`hetzner: server ${String(id)} has no IPv4 address`);
  }
  const state = await ensurePerBoxState(sandboxId);
  if (!existsSync3(state.identity)) {
    throw new Error(
      `hetzner: per-box SSH key missing for sandbox ${sandboxId} (expected at ${state.identity}). If this box was created by a different host, you'll need to re-create it on this host.`
    );
  }
  await ensureTunnel(sandboxId, state, vpsIp);
  const controlPath = tunnels.controlPath(sandboxId);
  return { target: buildSshTarget(state, vpsIp, controlPath), state, vpsIp };
}
var hetznerBackend = {
  name: "hetzner",
  async provision(req) {
    const c = client();
    const onLog = req.onLog ?? (() => {
    });
    const progress = (s) => onLog(`hetzner: ${s}`);
    await ensureHetznerBaseSnapshot();
    const imageRef = await resolveImageId(c, req);
    const egressOverride = req.env?.AGENTBOX_HETZNER_FIREWALL_SOURCE ?? process.env.AGENTBOX_HETZNER_FIREWALL_SOURCE;
    const source = egressOverride ? normalizeSourceCidr(egressOverride) : `${await detectEgressIp({ onLog })}/32`;
    progress(`firewall source: ${source}`);
    const stamp = Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
    const tempDir = resolvePath(
      process.env.HOME ?? process.cwd(),
      ".agentbox",
      "hetzner",
      `pending-${stamp}`,
      "ssh"
    );
    const key = await mintSshKey(tempDir, `agentbox-box-${req.name}-${stamp}`);
    let firewallId = null;
    let serverId = null;
    try {
      const firewall = await createPerBoxFirewall(c, {
        name: `agentbox-${req.name}-${stamp}`,
        sourceCidr: source,
        labels: {
          "agentbox.box": req.name,
          "agentbox.role": "box"
        }
      });
      firewallId = firewall.id;
      const boxEnv = {};
      for (const [k, v] of Object.entries(req.env ?? {})) {
        if (k.startsWith("AGENTBOX_")) boxEnv[k] = v;
      }
      const cloudInit = generateBoxCloudInit({
        sshPubkey: key.publicKey,
        boxName: req.name,
        boxEnv: Object.keys(boxEnv).length > 0 ? boxEnv : void 0
      });
      progress(`creating VPS '${req.name}' from image ${String(imageRef)} (cx22 / nbg1 unless overridden)`);
      const created = await withHetznerRetry(
        { method: "createServer", retryOnAmbiguous: false, attemptTimeoutMs: 12e4 },
        () => c.createServer({
          name: `agentbox-${req.name}-${stamp}`,
          server_type: HETZNER_DEFAULT_SERVER_TYPE,
          image: imageRef,
          location: HETZNER_DEFAULT_LOCATION,
          user_data: cloudInit,
          firewalls: [{ firewall: firewall.id }],
          labels: {
            "agentbox.managed": "true",
            "agentbox.role": "box",
            "agentbox.box": req.name,
            "agentbox.firewall": String(firewall.id)
          },
          start_after_create: true
        })
      );
      serverId = created.server.id;
      const vpsIp = created.server.public_net.ipv4?.ip;
      if (!vpsIp) {
        throw new Error(`hetzner: server ${String(serverId)} came up without an IPv4 address`);
      }
      progress(`server ${String(serverId)} provisioned at ${vpsIp}; waiting for ssh`);
      const sandboxId = String(serverId);
      const state = await ensurePerBoxState(sandboxId);
      await rename(key.privatePath, state.identity);
      await rename(key.publicPath, `${state.identity}.pub`);
      await rm2(key.dir, { recursive: true, force: true });
      const pendingParent = resolvePath(key.dir, "..");
      await rm2(pendingParent, { recursive: true, force: true });
      const up = await waitForSsh(buildSshTarget(state, vpsIp), PROVISION_SSH_DEADLINE_MS);
      if (!up) {
        throw new Error(`hetzner: ssh on ${vpsIp} did not come up within ${String(PROVISION_SSH_DEADLINE_MS / 1e3)}s`);
      }
      await ensureTunnel(sandboxId, state, vpsIp);
      progress("ssh up; ControlMaster open");
      const liveTarget = buildSshTarget(state, vpsIp, tunnels.controlPath(sandboxId));
      try {
        await pushHetznerAgentCredentials(liveTarget, onLog);
      } catch (credErr) {
        onLog(
          `hetzner: WARN \u2014 agent credential push failed (${credErr instanceof Error ? credErr.message : String(credErr)}); in-box claude/codex/opencode will prompt for interactive login`
        );
      }
      return { sandboxId };
    } catch (err) {
      if (serverId !== null) {
        progress(`cleanup \u2014 deleting server ${String(serverId)} after provision failure`);
        try {
          await c.deleteServer(serverId);
        } catch (cleanupErr) {
          onLog(
            `hetzner: WARN \u2014 failed to delete server ${String(serverId)}; check the Hetzner dashboard manually. ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`
          );
        }
      }
      if (firewallId !== null) {
        try {
          await deletePerBoxFirewall(c, firewallId);
        } catch {
        }
      }
      try {
        if (existsSync3(key.dir)) await rm2(key.dir, { recursive: true, force: true });
        if (serverId !== null) {
          const finalDir = perBoxDir(String(serverId));
          if (existsSync3(finalDir)) await rm2(finalDir, { recursive: true, force: true });
        }
      } catch {
      }
      throw err;
    }
  },
  async get(sandboxId) {
    const id = Number.parseInt(sandboxId, 10);
    if (!Number.isFinite(id)) return null;
    const server = await client().getServer(id);
    return server ? { sandboxId } : null;
  },
  async list() {
    const servers = await client().listServers({ label_selector: "agentbox.managed=true" });
    return servers.map((s) => ({
      sandboxId: String(s.id),
      name: s.labels["agentbox.box"] ?? s.name,
      createdAt: s.created,
      state: mapState(s.status)
    }));
  },
  async start(h) {
    const id = Number.parseInt(h.sandboxId, 10);
    await client().powerOn(id);
    await pollUntil(
      `server ${h.sandboxId} running`,
      async () => {
        const s = await client().getServer(id);
        return s?.status === "running" ? s : null;
      },
      { deadlineMs: ACTION_DEADLINE_MS, intervalMs: 2e3, maxIntervalMs: 8e3 }
    );
    const server = await getServerStrict(id);
    const vpsIp = server.public_net.ipv4?.ip;
    if (!vpsIp) {
      throw new Error(`hetzner: server ${h.sandboxId} has no IPv4 address after start`);
    }
    const state = await ensurePerBoxState(h.sandboxId);
    const up = await waitForSsh(buildSshTarget(state, vpsIp), PROVISION_SSH_DEADLINE_MS);
    if (!up) {
      throw new Error(
        `hetzner: ssh on ${vpsIp} did not come up within ${String(PROVISION_SSH_DEADLINE_MS / 1e3)}s after start`
      );
    }
  },
  async stop(h) {
    const id = Number.parseInt(h.sandboxId, 10);
    try {
      await client().shutdown(id);
      await pollUntil(
        `server ${h.sandboxId} off`,
        async () => {
          const s = await client().getServer(id);
          return s?.status === "off" ? s : null;
        },
        { deadlineMs: 6e4, intervalMs: 2e3, maxIntervalMs: 8e3 }
      );
    } catch {
      await client().powerOff(id);
    }
    await tunnels.close(h.sandboxId);
  },
  async pause(h) {
    await this.stop(h);
  },
  async resume(h) {
    await this.start(h);
  },
  async destroy(h) {
    const id = Number.parseInt(h.sandboxId, 10);
    await tunnels.close(h.sandboxId);
    const c = client();
    let firewallId;
    try {
      const server = await c.getServer(id);
      firewallId = server ? Number.parseInt(server.labels["agentbox.firewall"] ?? "", 10) : void 0;
    } catch {
    }
    try {
      await c.deleteServer(id);
    } catch (err) {
      if (!(err instanceof HetznerApiError && (err.statusCode === 404 || err.code === "not_found"))) {
        throw err;
      }
    }
    if (firewallId && Number.isFinite(firewallId)) {
      await deletePerBoxFirewall(c, firewallId);
    }
    const dir = perBoxDir(h.sandboxId);
    try {
      await rm2(dir, { recursive: true, force: true });
    } catch {
    }
  },
  async state(h) {
    const id = Number.parseInt(h.sandboxId, 10);
    const s = await client().getServer(id);
    return s ? mapState(s.status) : "missing";
  },
  async exec(h, cmd, opts) {
    const { target } = await ensureLiveTarget(h.sandboxId);
    const argv = [
      ...sshOptArgs(target),
      `${target.user}@${target.host}`,
      bashScript(opts?.cwd ? `cd ${shellQuote(opts.cwd)} && ${cmd}` : cmd)
    ];
    const res = await execa4("ssh", argv, {
      reject: false,
      timeout: opts?.attemptTimeoutMs ?? 12e4,
      env: { ...process.env, ...opts?.env ?? {} }
    });
    return {
      exitCode: typeof res.exitCode === "number" ? res.exitCode : 1,
      stdout: typeof res.stdout === "string" ? res.stdout : "",
      stderr: typeof res.stderr === "string" ? res.stderr : ""
    };
  },
  async uploadFile(h, localPath, remotePath) {
    const { target } = await ensureLiveTarget(h.sandboxId);
    const argv = [
      ...sshOptArgs(target),
      localPath,
      `${target.user}@${target.host}:${remotePath}`
    ];
    const res = await execa4("scp", argv, { reject: false, timeout: 3e5 });
    if (res.exitCode !== 0) {
      throw new Error(`hetzner: scp upload failed (exit ${String(res.exitCode)}): ${res.stderr || ""}`);
    }
  },
  async downloadFile(h, remotePath, localPath) {
    const { target } = await ensureLiveTarget(h.sandboxId);
    const argv = [
      ...sshOptArgs(target),
      `${target.user}@${target.host}:${remotePath}`,
      localPath
    ];
    const res = await execa4("scp", argv, { reject: false, timeout: 3e5 });
    if (res.exitCode !== 0) {
      throw new Error(`hetzner: scp download failed (exit ${String(res.exitCode)}): ${res.stderr || ""}`);
    }
  },
  async listFiles(h, remoteDir) {
    const res = await this.exec(
      h,
      // -L for nicer dir-detection on symlinks; `--printf` is non-portable so
      // use a small awk wrap that prints `<name>\t<d|f>` per entry.
      `find ${shellQuote(remoteDir)} -mindepth 1 -maxdepth 1 -printf '%f\\t%y\\n'`
    );
    if (res.exitCode !== 0) return [];
    return res.stdout.split(/\r?\n/).filter((line) => line.length > 0).map((line) => {
      const [name, kind] = line.split("	");
      return { name: name ?? line, isDir: kind === "d" };
    });
  },
  async previewUrl(h, port) {
    const { state, vpsIp } = await ensureLiveTarget(h.sandboxId);
    void state;
    void vpsIp;
    const localPort = await tunnels.forward(h.sandboxId, port);
    return { url: `http://127.0.0.1:${String(localPort)}` };
  },
  async signedPreviewUrl(h, port, _ttl) {
    void _ttl;
    return this.previewUrl(h, port);
  },
  async refreshPreviewUrl(h, port) {
    const { state, vpsIp } = await ensureLiveTarget(h.sandboxId);
    void state;
    void vpsIp;
    await tunnels.refresh({
      boxId: h.sandboxId,
      vpsHost: vpsIp,
      identity: state.identity
    });
    const localPort = await tunnels.forward(h.sandboxId, port);
    return { url: `http://127.0.0.1:${String(localPort)}` };
  },
  async startInBoxPortless(h, opts) {
    const tlsFlag = opts.tls ? "" : "--no-tls";
    const startCmd = `sudo portless proxy start ${tlsFlag} -p ${String(opts.proxyPort)}`.replace(/\s+/g, " ");
    const aliasCmd = `sudo portless alias ${shellQuote(opts.boxName)} ${String(opts.webPort)}`;
    await this.exec(h, `${startCmd}; ${aliasCmd}`);
  },
  async attachArgv(h) {
    const { target } = await ensureLiveTarget(h.sandboxId);
    return [
      "ssh",
      ...sshOptArgs(target),
      `${target.user}@${target.host}`
    ];
  },
  async createSnapshot(h, name) {
    const id = Number.parseInt(h.sandboxId, 10);
    const c = client();
    const { image } = await withHetznerRetry(
      { method: "createImage", retryOnAmbiguous: false, attemptTimeoutMs: 12e4 },
      () => c.createImage(id, {
        type: "snapshot",
        description: name,
        labels: { "agentbox.role": "ckpt", "agentbox.box": h.sandboxId }
      })
    );
    await pollUntil(
      `image ${String(image.id)} availability`,
      async () => {
        const img = await c.getImage(image.id);
        return img?.status === "available" ? img : null;
      },
      { deadlineMs: SNAPSHOT_DEADLINE_MS2, intervalMs: 3e3, maxIntervalMs: 1e4 }
    );
  },
  async deleteSnapshot(name) {
    const c = client();
    const img = await findImageByDescription(c, name);
    if (!img) return;
    try {
      await c.deleteImage(img.id);
    } catch (err) {
      if (err instanceof HetznerApiError && (err.statusCode === 404 || err.code === "not_found")) return;
      throw err;
    }
  }
};
async function pushHetznerAgentCredentials(target, log) {
  const specs = [
    { kind: "claude", stage: stageClaudeCredentialsForUpload, dest: "/home/vscode/.agentbox-creds/claude" },
    { kind: "codex", stage: stageCodexCredentialsForUpload, dest: "/home/vscode/.agentbox-creds/codex" },
    { kind: "opencode", stage: stageOpencodeCredentialsForUpload, dest: "/home/vscode/.agentbox-creds/opencode" }
  ];
  for (const spec of specs) {
    const staged = await spec.stage();
    for (const w of staged.warnings) log(`hetzner: [${spec.kind}-creds] ${w}`);
    try {
      if (!staged.tarballPath) {
        log(`hetzner: ${spec.kind}: no host credentials to push (skipping)`);
        continue;
      }
      const remote = `/tmp/agentbox-${spec.kind}-creds.tar.gz`;
      const argv = [
        ...sshOptArgs(target),
        staged.tarballPath,
        `${target.user}@${target.host}:${remote}`
      ];
      const scpRes = await execa4("scp", argv, { reject: false, timeout: 12e4 });
      if (scpRes.exitCode !== 0) {
        throw new Error(
          `scp ${spec.kind} credentials failed (exit ${String(scpRes.exitCode)}): ${scpRes.stderr ?? ""}`
        );
      }
      const extract = await execa4(
        "ssh",
        [
          ...sshOptArgs(target),
          `${target.user}@${target.host}`,
          `sudo -u vscode mkdir -p ${spec.dest} && sudo -u vscode tar -xzf ${remote} -C ${spec.dest} --no-same-permissions --no-same-owner -m && rm -f ${remote}`
        ],
        { reject: false, timeout: 3e4 }
      );
      if (extract.exitCode !== 0) {
        throw new Error(
          `${spec.kind} credential extract failed (exit ${String(extract.exitCode)}): ${extract.stderr ?? ""}`
        );
      }
      log(`hetzner: ${spec.kind}: credentials pushed`);
    } finally {
      await staged.cleanup();
    }
  }
}
var cloudProvider = createCloudProvider(hetznerBackend, {
  defaultResources: { cpu: 2, memory: 4, disk: 40 }
});
var hetznerProvider = {
  ...cloudProvider,
  prepare: prepareHetznerProvider
};
export {
  DEFAULT_HCLOUD_ENDPOINT,
  HETZNER_DEFAULT_BOX_IMAGE_REF,
  HetznerApiError,
  RUNTIME_ASSETS,
  candidatesFor,
  createPerBoxFirewall,
  deletePerBoxFirewall,
  detectEgressIp,
  ensureHetznerBaseSnapshot,
  ensureHetznerCredentials,
  ensureHetznerEnvLoaded,
  generateBoxCloudInit,
  generatePrepareCloudInit,
  hetznerBackend,
  hetznerProvider,
  isAttemptTimeout,
  isRetriable,
  makeHetznerClient,
  maskKey,
  mintPrepareKey,
  mintSshKey,
  normalizeSourceCidr,
  pollUntil,
  prepareHetzner,
  prepareHetznerProvider,
  preparedStatePath,
  readHetznerCredStatus,
  readPreparedState,
  resolveRuntimeAssets,
  scpDownload,
  scpUpload,
  secretsPath,
  sshExec,
  sshOnlyInboundRule,
  sshOptArgs,
  syncFirewallSource,
  updatePreparedState,
  waitForSsh,
  withHetznerRetry,
  writePreparedState
};
//# sourceMappingURL=dist-WMQDMTWS.js.map