#!/usr/bin/env node
import {
  hostOpenCommand
} from "./chunk-SNTHHWKY.js";

// ../../packages/sandbox-hetzner/dist/chunk-MDODRIBH.js
import { existsSync, readFileSync } from "fs";
import { homedir } from "os";
import { resolve } from "path";
import { spawnSync } from "child_process";
import {
  chmodSync,
  existsSync as existsSync2,
  mkdirSync,
  readFileSync as readFileSync2,
  renameSync,
  writeFileSync
} from "fs";
import { homedir as homedir2 } from "os";
import { dirname, resolve as resolve2 } from "path";
import { confirm, isCancel, intro, log, note, outro, password, spinner } from "@clack/prompts";
var HETZNER_KEYS = ["HCLOUD_TOKEN", "HCLOUD_ENDPOINT"];
var loaded = false;
function ensureHetznerEnvLoaded() {
  if (loaded) return;
  loaded = true;
  importHetznerFromFile(resolve(homedir(), ".agentbox", "secrets.env"));
}
function importHetznerFromFile(path) {
  if (!existsSync(path)) return;
  let body;
  try {
    body = readFileSync(path, "utf8");
  } catch {
    return;
  }
  const parsed = parseEnvFile(body);
  for (const key of HETZNER_KEYS) {
    if (process.env[key] !== void 0) continue;
    const value = parsed[key];
    if (typeof value === "string") {
      process.env[key] = value;
    }
  }
}
function parseEnvFile(body) {
  const out = {};
  for (const rawLine of body.split(/\r?\n/)) {
    const line = rawLine.trim();
    if (line.length === 0 || line.startsWith("#")) continue;
    const stripped = line.startsWith("export ") ? line.slice("export ".length) : line;
    const eq = stripped.indexOf("=");
    if (eq <= 0) continue;
    const key = stripped.slice(0, eq).trim();
    let value = stripped.slice(eq + 1).trim();
    if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
      value = value.slice(1, -1);
    }
    out[key] = value;
  }
  return out;
}
var DEFAULT_HCLOUD_ENDPOINT = "https://api.hetzner.cloud/v1";
var HetznerApiError = class extends Error {
  statusCode;
  code;
  details;
  constructor(statusCode, code, message, details) {
    super(`hetzner ${String(statusCode)} ${code}: ${message}`);
    this.name = "HetznerApiError";
    this.statusCode = statusCode;
    this.code = code;
    this.details = details;
  }
};
function makeHetznerClient(opts = {}) {
  ensureHetznerEnvLoaded();
  const rawToken = opts.token ?? process.env.HCLOUD_TOKEN;
  if (!rawToken || rawToken.trim().length === 0) {
    throw new Error(
      "Hetzner credentials not configured: HCLOUD_TOKEN is empty.\nRun `agentbox hetzner login` interactively, or set HCLOUD_TOKEN in the environment."
    );
  }
  const token = rawToken.trim();
  const endpoint = (opts.endpoint ?? process.env.HCLOUD_ENDPOINT ?? DEFAULT_HCLOUD_ENDPOINT).replace(/\/$/, "");
  const fetchImpl = opts.fetchImpl ?? fetch;
  async function req(method, path, body) {
    const url = `${endpoint}${path}`;
    const init = {
      method,
      headers: {
        Authorization: `Bearer ${token}`,
        ...body !== void 0 ? { "Content-Type": "application/json" } : {}
      },
      ...body !== void 0 ? { body: JSON.stringify(body) } : {}
    };
    const res = await fetchImpl(url, init);
    if (res.status === 204) return null;
    if (res.status === 404) return null;
    if (!res.ok) {
      let parsed = {};
      try {
        parsed = await res.json();
      } catch {
      }
      const code = parsed.error?.code ?? `http_${String(res.status)}`;
      const msg = parsed.error?.message ?? res.statusText ?? "unknown error";
      throw new HetznerApiError(res.status, code, msg, parsed.error?.details);
    }
    const text = await res.text();
    if (text.length === 0) return null;
    return JSON.parse(text);
  }
  async function reqExpect(method, path, body) {
    const out = await req(method, path, body);
    if (out === null) {
      throw new HetznerApiError(0, "empty_response", `expected a body from ${method} ${path}`);
    }
    return out;
  }
  return {
    async getServer(id) {
      const r = await req("GET", `/servers/${String(id)}`);
      return r?.server ?? null;
    },
    async createServer(reqBody) {
      const r = await reqExpect(
        "POST",
        "/servers",
        reqBody
      );
      return { server: r.server, action: r.action };
    },
    async listServers(opts2) {
      const params = new URLSearchParams();
      if (opts2?.label_selector) params.set("label_selector", opts2.label_selector);
      params.set("per_page", "50");
      const all = [];
      let pageNum = 1;
      while (true) {
        params.set("page", String(pageNum));
        const r = await reqExpect("GET", `/servers?${params.toString()}`);
        all.push(...r.servers);
        const next = r.meta?.pagination?.next_page;
        if (typeof next !== "number") break;
        pageNum = next;
      }
      return all;
    },
    async deleteServer(id) {
      const r = await req("DELETE", `/servers/${String(id)}`);
      return r?.action ?? null;
    },
    async powerOn(id) {
      const r = await reqExpect(
        "POST",
        `/servers/${String(id)}/actions/poweron`
      );
      return r.action;
    },
    async powerOff(id) {
      const r = await reqExpect(
        "POST",
        `/servers/${String(id)}/actions/poweroff`
      );
      return r.action;
    },
    async shutdown(id) {
      const r = await reqExpect(
        "POST",
        `/servers/${String(id)}/actions/shutdown`
      );
      return r.action;
    },
    async createImage(id, body) {
      const r = await reqExpect(
        "POST",
        `/servers/${String(id)}/actions/create_image`,
        body
      );
      return { image: r.image, action: r.action };
    },
    async getImage(id) {
      const r = await req("GET", `/images/${String(id)}`);
      return r?.image ?? null;
    },
    async listImages(opts2) {
      const params = new URLSearchParams();
      if (opts2?.type) params.set("type", opts2.type);
      if (opts2?.label_selector) params.set("label_selector", opts2.label_selector);
      if (opts2?.name) params.set("name", opts2.name);
      params.set("per_page", "50");
      const all = [];
      let pageNum = 1;
      while (true) {
        params.set("page", String(pageNum));
        const r = await reqExpect("GET", `/images?${params.toString()}`);
        all.push(...r.images);
        const next = r.meta?.pagination?.next_page;
        if (typeof next !== "number") break;
        pageNum = next;
      }
      return all;
    },
    async deleteImage(id) {
      await req("DELETE", `/images/${String(id)}`);
    },
    async createFirewall(reqBody) {
      const r = await reqExpect("POST", "/firewalls", reqBody);
      return r.firewall;
    },
    async setFirewallRules(id, rules) {
      const r = await reqExpect(
        "POST",
        `/firewalls/${String(id)}/actions/set_rules`,
        { rules }
      );
      return r.actions;
    },
    async getFirewall(id) {
      const r = await req("GET", `/firewalls/${String(id)}`);
      return r?.firewall ?? null;
    },
    async deleteFirewall(id) {
      await req("DELETE", `/firewalls/${String(id)}`);
    },
    async listLocations() {
      const r = await reqExpect("GET", "/locations");
      return r.locations;
    }
  };
}
var DASHBOARD_KEYS_URL = "https://console.hetzner.cloud/projects";
var MANAGED_KEYS = ["HCLOUD_TOKEN", "HCLOUD_ENDPOINT"];
async function ensureHetznerCredentials(opts = {}) {
  ensureHetznerEnvLoaded();
  if (!opts.force && hasUsableCredentials()) return;
  if (!process.stdin.isTTY) return;
  intro("Hetzner Cloud setup");
  note(
    `AgentBox needs a Hetzner Cloud API token (project-scoped) to provision VPSes.

1. Open ${DASHBOARD_KEYS_URL}
2. Pick a project (or create one).
3. Security \u2192 API Tokens \u2192 Generate API Token (Read + Write).`,
    "API token required"
  );
  const open = await confirm({
    message: `Open ${DASHBOARD_KEYS_URL} in your browser?`,
    initialValue: true
  });
  if (isCancel(open)) {
    log.warn("Hetzner setup cancelled \u2014 re-run `agentbox hetzner login` when ready.");
    return;
  }
  if (open) openDashboard();
  for (let attempt = 0; attempt < 2; attempt++) {
    const creds = await promptForCredentials();
    if (creds === null) return;
    const result = await validateCredentials(creds);
    if (result.ok) {
      persistCredentials(creds);
      log.success(`Hetzner credentials saved to ${secretsPath()}`);
      outro("Setup complete.");
      return;
    }
    if (result.kind === "auth" && attempt === 0) {
      log.error(`That token was rejected by Hetzner: ${result.message}`);
      log.info("Try again, or press Ctrl-C to cancel.");
      continue;
    }
    if (result.kind === "network") {
      log.warn(`Could not reach Hetzner to validate (${result.message}) \u2014 saving anyway.`);
      persistCredentials(creds);
      log.success(`Hetzner credentials saved to ${secretsPath()}`);
      outro("Setup complete (unvalidated).");
      return;
    }
    throw new Error(`Hetzner credentials rejected: ${result.message}`);
  }
}
function hasUsableCredentials() {
  return typeof process.env.HCLOUD_TOKEN === "string" && process.env.HCLOUD_TOKEN.length > 0;
}
async function promptForCredentials() {
  const token = await password({
    message: "Paste your Hetzner Cloud API token",
    validate(v) {
      if (!v || v.trim().length === 0) return "Cannot be empty";
      return void 0;
    }
  });
  if (isCancel(token)) {
    log.warn("Hetzner setup cancelled.");
    return null;
  }
  return { token: token.trim() };
}
async function validateCredentials(creds) {
  const s = spinner();
  s.start("Validating credentials with Hetzner");
  try {
    const client = makeHetznerClient({ token: creds.token, endpoint: creds.endpoint });
    await client.listLocations();
    s.stop("Hetzner credentials accepted");
    return { ok: true };
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    s.stop("Hetzner credentials check failed");
    if (/401|403|unauthor|forbidden|invalid|token/i.test(message)) {
      return { ok: false, kind: "auth", message };
    }
    return { ok: false, kind: "network", message };
  }
}
function persistCredentials(creds) {
  process.env.HCLOUD_TOKEN = creds.token;
  if (creds.endpoint) process.env.HCLOUD_ENDPOINT = creds.endpoint;
  const path = secretsPath();
  mkdirSync(dirname(path), { recursive: true });
  let existing = "";
  if (existsSync2(path)) {
    try {
      existing = readFileSync2(path, "utf8");
    } catch {
      existing = "";
    }
  }
  const kept = existing.split(/\r?\n/).filter((line) => {
    const stripped = line.startsWith("export ") ? line.slice("export ".length) : line;
    const eq = stripped.indexOf("=");
    if (eq <= 0) return true;
    const key = stripped.slice(0, eq).trim();
    return !MANAGED_KEYS.includes(key);
  }).join("\n").replace(/\s+$/u, "");
  const lines = [`HCLOUD_TOKEN=${creds.token}`];
  if (creds.endpoint) lines.push(`HCLOUD_ENDPOINT=${creds.endpoint}`);
  const body = (kept ? `${kept}
` : "") + lines.join("\n") + "\n";
  const tmp = `${path}.tmp`;
  writeFileSync(tmp, body, { mode: 384 });
  try {
    chmodSync(tmp, 384);
  } catch {
  }
  renameSync(tmp, path);
  try {
    chmodSync(path, 384);
  } catch {
  }
}
function openDashboard() {
  try {
    const r = spawnSync(hostOpenCommand(), [DASHBOARD_KEYS_URL], { stdio: "ignore" });
    if (r.status !== 0) {
      log.warn(`Could not auto-open the browser \u2014 visit ${DASHBOARD_KEYS_URL} manually.`);
    }
  } catch {
    log.warn(`Could not auto-open the browser \u2014 visit ${DASHBOARD_KEYS_URL} manually.`);
  }
}
function secretsPath() {
  return resolve2(homedir2(), ".agentbox", "secrets.env");
}
function readHetznerCredStatus() {
  const shellHadToken = !!process.env.HCLOUD_TOKEN;
  ensureHetznerEnvLoaded();
  const token = process.env.HCLOUD_TOKEN;
  const endpoint = process.env.HCLOUD_ENDPOINT;
  if (!token) return { source: "none" };
  return {
    token,
    endpoint,
    source: shellHadToken ? "env" : "secrets.env"
  };
}
function maskKey(value) {
  if (value.length <= 8) return "*".repeat(value.length);
  return `${value.slice(0, 4)}\u2026${"*".repeat(8)}${value.slice(-4)}`;
}
var PROBES = [
  "https://api.ipify.org",
  "https://ifconfig.io/ip",
  "https://icanhazip.com"
];
var TIMEOUT_MS = 3e3;
var IPV4_RE = /^(?:\d{1,3}\.){3}\d{1,3}$/;
var IPV6_RE = /^[0-9a-fA-F:]+$/;
async function detectEgressIp(opts = {}) {
  const probes = opts.probes ?? PROBES;
  const timeout = opts.timeoutMs ?? TIMEOUT_MS;
  const fetchImpl = opts.fetchImpl ?? fetch;
  const errors = [];
  for (const url of probes) {
    try {
      const ip = await raceTimeout(probe(url, fetchImpl), timeout);
      if (ip) {
        opts.onLog?.(`egress-ip: detected ${ip} via ${url}`);
        return ip;
      }
      errors.push(`${url}: empty/invalid response`);
    } catch (err) {
      errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
    }
  }
  throw new Error(
    `could not auto-detect the host's egress IP \u2014 all ${String(probes.length)} probes failed:
` + errors.map((e) => `  - ${e}`).join("\n") + `
Override with --firewall-source <cidr> (e.g. --firewall-source 0.0.0.0/0 for the explicit-open opt-in).`
  );
}
async function probe(url, fetchImpl) {
  const res = await fetchImpl(url, { method: "GET" });
  if (!res.ok) return null;
  const body = (await res.text()).trim();
  if (IPV4_RE.test(body)) {
    const parts = body.split(".").map((p) => Number.parseInt(p, 10));
    if (parts.every((p) => p >= 0 && p <= 255)) return body;
    return null;
  }
  if (IPV6_RE.test(body) && body.includes(":")) return body;
  return null;
}
async function raceTimeout(p, ms) {
  let timer;
  try {
    return await Promise.race([
      p,
      new Promise((_resolve, reject) => {
        timer = setTimeout(() => reject(new Error(`probe timed out after ${String(ms)}ms`)), ms);
      })
    ]);
  } finally {
    if (timer !== void 0) clearTimeout(timer);
  }
}
var DEFAULT_BACKOFF = [1e3, 2e3, 4e3];
var DEFAULT_ATTEMPT_TIMEOUT_MS = 3e4;
var AttemptTimeoutError = class extends Error {
  constructor(method, ms) {
    super(`hetzner ${method}: per-attempt timeout after ${String(ms)}ms`);
    this.name = "AttemptTimeoutError";
  }
};
function isAttemptTimeout(err) {
  return err instanceof AttemptTimeoutError;
}
function isRetriable(err, allowAmbiguous) {
  if (err instanceof HetznerApiError) {
    if (err.statusCode === 429 || err.code === "rate_limit_exceeded") return true;
    if (err.statusCode >= 500 && err.statusCode <= 599) return allowAmbiguous;
    if (err.code === "locked" || err.code === "conflict") return true;
    return false;
  }
  if (err instanceof AttemptTimeoutError) return allowAmbiguous;
  if (err && typeof err === "object") {
    const candidates = [err, err.cause];
    for (const c of candidates) {
      if (!c || typeof c !== "object") continue;
      const code = c.code;
      if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ECONNABORTED" || code === "EAI_AGAIN" || code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "UND_ERR_SOCKET" || code === "UND_ERR_CONNECT_TIMEOUT") {
        return allowAmbiguous;
      }
    }
  }
  return false;
}
async function withHetznerRetry(opts, fn) {
  const backoff = opts.backoffMs ?? DEFAULT_BACKOFF;
  const maxAttempts = backoff.length + 1;
  const timeoutMs = opts.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS;
  const log2 = opts.onRetry ?? defaultRetryLog;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await raceTimeout2(fn(), timeoutMs, opts.method);
    } catch (err) {
      const last = attempt === maxAttempts;
      if (last || !isRetriable(err, opts.retryOnAmbiguous)) throw err;
      const delay = backoff[attempt - 1] ?? backoff[backoff.length - 1] ?? 4e3;
      log2(
        `hetzner ${opts.method}: attempt ${String(attempt)} failed (${errorSummary(err)}); retrying in ${String(delay)}ms`
      );
      await sleep(delay);
    }
  }
  throw new Error(`withHetznerRetry: exhausted attempts for ${opts.method}`);
}
function defaultRetryLog(line) {
  process.stderr.write(`
[hetzner-retry] ${line}
`);
}
function sleep(ms) {
  return new Promise((resolve3) => setTimeout(resolve3, ms));
}
async function raceTimeout2(p, ms, method) {
  let timer;
  try {
    return await Promise.race([
      p,
      new Promise((_resolve, reject) => {
        timer = setTimeout(() => reject(new AttemptTimeoutError(method, ms)), ms);
      })
    ]);
  } finally {
    if (timer !== void 0) clearTimeout(timer);
  }
}
function errorSummary(err) {
  if (err instanceof HetznerApiError) {
    return `HetznerApiError ${String(err.statusCode)} ${err.code}: ${truncate(err.message)}`;
  }
  if (err instanceof Error) {
    const code = err.code;
    return code !== void 0 ? `${err.name}(${String(code)}): ${truncate(err.message)}` : `${err.name}: ${truncate(err.message)}`;
  }
  return truncate(String(err));
}
function truncate(s, max = 160) {
  return s.length > max ? `${s.slice(0, max)}\u2026` : s;
}
function sshOnlyInboundRule(sourceCidr) {
  return [
    {
      direction: "in",
      protocol: "tcp",
      port: "22",
      source_ips: [sourceCidr],
      description: "agentbox: SSH from host egress IP only"
    }
  ];
}
async function createPerBoxFirewall(client, opts) {
  return withHetznerRetry(
    { method: "createFirewall", retryOnAmbiguous: false, attemptTimeoutMs: 6e4 },
    () => client.createFirewall({
      name: opts.name,
      rules: sshOnlyInboundRule(opts.sourceCidr),
      labels: {
        "agentbox.managed": "true",
        "agentbox.role": "box",
        ...opts.labels
      }
    })
  );
}
async function syncFirewallSource(client, firewallId, sourceCidr) {
  await withHetznerRetry(
    { method: "setFirewallRules", retryOnAmbiguous: true, attemptTimeoutMs: 6e4 },
    () => client.setFirewallRules(firewallId, sshOnlyInboundRule(sourceCidr))
  );
}
async function deletePerBoxFirewall(client, firewallId, opts = {}) {
  const deadline = Date.now() + (opts.detachWaitMs ?? 6e4);
  let interval = 1e3;
  while (true) {
    try {
      await withHetznerRetry(
        { method: "deleteFirewall", retryOnAmbiguous: true, attemptTimeoutMs: 3e4 },
        () => client.deleteFirewall(firewallId)
      );
      return;
    } catch (err) {
      if (err instanceof HetznerApiError && (err.statusCode === 404 || err.code === "not_found")) {
        return;
      }
      const stillAttached = err instanceof HetznerApiError && (err.statusCode === 409 || err.code === "conflict" || err.code === "resource_in_use");
      if (stillAttached && Date.now() < deadline) {
        await new Promise((r) => setTimeout(r, interval));
        interval = Math.min(interval * 2, 8e3);
        continue;
      }
      throw err;
    }
  }
}
function normalizeSourceCidr(raw) {
  const trimmed = raw.trim();
  if (trimmed.includes("/")) return trimmed;
  if (trimmed.includes(":")) return `${trimmed}/128`;
  return `${trimmed}/32`;
}

export {
  ensureHetznerEnvLoaded,
  DEFAULT_HCLOUD_ENDPOINT,
  HetznerApiError,
  makeHetznerClient,
  ensureHetznerCredentials,
  secretsPath,
  readHetznerCredStatus,
  maskKey,
  detectEgressIp,
  isAttemptTimeout,
  isRetriable,
  withHetznerRetry,
  sshOnlyInboundRule,
  createPerBoxFirewall,
  syncFirewallSource,
  deletePerBoxFirewall,
  normalizeSourceCidr
};
//# sourceMappingURL=chunk-R5XIDQFR.js.map