UNPKG

craftboxes

Version:

box: Claude Code on your laptop, the work on a cloud dev machine you reach as <name>.box, with your team databases through MCP.

1,387 lines • 71.1 kB
#!/usr/bin/env node

// src/index.ts
import { styleText as styleText5 } from "node:util";
import { Command } from "commander";
import { input as input2, select as select4 } from "@inquirer/prompts";

// src/api.ts
import { spawn } from "node:child_process";
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
import { join as join2 } from "node:path";
import { styleText as styleText2 } from "node:util";

// src/laptop.ts
import { execFile, execFileSync, spawnSync } from "node:child_process";
import { gzip } from "node:zlib";
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from "node:fs";
import { homedir as homedir2, userInfo } from "node:os";
import { join, sep } from "node:path";
import ssh2 from "ssh2";

// src/ui.ts
import { homedir } from "node:os";
import { styleText } from "node:util";
var dim = (s) => styleText("dim", s);
var ok = (s) => console.log(`${styleText("green", "\u2713")} ${s}`);
var miss = (s) => console.log(`${styleText("yellow", "!")} ${s}`);
var fail = (s) => {
  active?.();
  console.error(`${styleText("red", "\u2717")} ${s}`);
  process.exit(1);
};
var active;
var stopProgress = () => active?.();
function progress(steps, { pace = 5e3 } = {}) {
  const tty = process.stdout.isTTY;
  if (!tty) {
    console.log(dim(`${steps[0][1]}\u2026`));
    return Object.assign((done) => {
      if (done) ok(done);
    }, { say: (label2) => console.log(dim(`${label2}\u2026`)) });
  }
  const spin = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F", t0 = Date.now(), width = 16;
  let i = 0, said;
  process.stdout.write("\x1B[?25l");
  const restore = () => process.stdout.write("\x1B[?25h");
  process.once("exit", restore);
  const draw = () => {
    const ms = Date.now() - t0;
    const label2 = said ?? steps.filter(([at2]) => at2 <= ms).at(-1)[1];
    const at = Math.min(width - 1, Math.floor((1 - Math.exp(-ms / pace)) * width));
    const track = [...Array(width)].map((_, k) => k < at ? styleText("cyan", "\u2501") : k === at ? "\u{1F4E6}" : styleText("dim", "\u2500")).join("");
    const secs = ms / 1e3, clock = secs < 60 ? `${secs.toFixed(1)}s` : `${Math.floor(secs / 60)}m ${String(Math.floor(secs % 60)).padStart(2, "0")}s`;
    process.stdout.write(`\r\x1B[2K${styleText("cyan", spin[i++ % spin.length])} ${track} ${label2.slice(0, Math.max(20, (process.stdout.columns ?? 100) - width - 16))}\u2026 ${dim(clock)}`);
  };
  draw();
  const timer = setInterval(draw, 80);
  const stop = (done) => {
    clearInterval(timer);
    process.stdout.write("\r\x1B[2K");
    restore();
    active = void 0;
    if (done) ok(done);
  };
  active = stop;
  return Object.assign(stop, { say: (label2) => {
    said = label2;
  } });
}
var span = (ms) => {
  const m = Math.max(0, Math.round(ms / 6e4));
  return m < 60 ? `${m}m` : m < 1440 ? `${Math.floor(m / 60)}h ${m % 60}m` : `${Math.round(m / 1440)} days`;
};
var tilde = (p) => p.startsWith(homedir()) ? `~${p.slice(homedir().length)}` : p;
function label(b) {
  if (b.state === "paused") return styleText("blue", b.asleepSince ? `asleep ${span(Date.now() - new Date(b.asleepSince).getTime())}` : "asleep");
  if (b.state !== "running") return styleText("yellow", b.state);
  if (b.busy) return styleText("green", "\u25CF working");
  if (!b.idleMinutes) return styleText("green", "awake");
  const left = b.idleMinutes * 6e4 - (Date.now() - new Date(b.lastActiveAt ?? Date.now()).getTime());
  return styleText("yellow", `\u263E sleeps in ${span(Math.max(6e4, left))}`);
}

// src/laptop.ts
var OLD_CONFIG = join(homedir2(), ".config", "craftboxes");
var OLD_BOXES = join(homedir2(), ".craftboxes");
var NEW_CONFIG = join(homedir2(), ".config", "box");
var NEW_BOXES = join(homedir2(), ".box");
var CONFIG_DIR = existsSync(OLD_CONFIG) ? OLD_CONFIG : NEW_CONFIG;
var BOXES = existsSync(OLD_BOXES) ? OLD_BOXES : NEW_BOXES;
var SERVICES = { 5432: "Postgres", 6379: "Redis", 3306: "MySQL", 27017: "MongoDB", 8123: "ClickHouse" };
function moveHome() {
  const moves = [[OLD_CONFIG, NEW_CONFIG], [OLD_BOXES, NEW_BOXES]].filter(([from]) => existsSync(from));
  if (!moves.length) return;
  if (otherBoxRunning()) return miss(`Close your other box windows so box can move ~/.craftboxes to ~/.box`);
  const clash = moves.flatMap(([from, to]) => existsSync(to) ? readdirSync(from).filter((f) => existsSync(join(to, f))).map((f) => join(to, f)) : []);
  if (clash.length) return miss(`box can't move ~/.craftboxes to ~/.box while ${clash.join(", ")} exists`);
  try {
    for (const [from, to] of moves) {
      if (!existsSync(to)) {
        renameSync(from, to);
        continue;
      }
      for (const f of readdirSync(from)) renameSync(join(from, f), join(to, f));
      rmdirSync(from);
    }
  } catch (e) {
    return miss(`Couldn't move ~/.craftboxes to ~/.box yet: ${e.message}`);
  }
  if (moves.some(([from]) => from === OLD_BOXES)) {
    repairWorktrees();
    moveClaudeProjects();
  }
  ok(`Moved ~/.craftboxes to ~/.box ${dim("\xB7 reopen VS Code windows with box code")}`);
  const again = spawnSync(process.execPath, process.argv.slice(1), { stdio: "inherit", env: { ...process.env, CBOX_UPDATED: "1" } });
  process.exit(again.status ?? 0);
}
function otherBoxRunning() {
  try {
    return execFileSync("ps", ["-A", "-o", "pid=,args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 }).split("\n").some((line) => {
      const [pid, ...args] = line.trim().split(/\s+/);
      const command = args.join(" ");
      return ![process.pid, process.ppid].includes(Number(pid)) && /(^|[\s/])c?box(\.mjs)?(\s|$)/.test(command) && !/\s(proxy|sync)(\s|$)/.test(command);
    });
  } catch {
    return false;
  }
}
function repairWorktrees() {
  for (const env of readdirSync(NEW_BOXES)) {
    const clone = join(NEW_BOXES, env, ".clone");
    if (env.startsWith(".") || !existsSync(join(clone, ".git"))) continue;
    const worktrees = readdirSync(join(NEW_BOXES, env)).map((d) => join(NEW_BOXES, env, d, "repo")).filter((r) => existsSync(join(r, ".git")) && statSync(join(r, ".git")).isFile());
    try {
      if (worktrees.length) execFileSync("git", ["-C", clone, "worktree", "repair", ...worktrees], { stdio: "ignore", timeout: 3e4 });
    } catch {
      miss(`Couldn't repair the ${env} worktrees. Run: git -C ${clone} worktree repair`);
    }
  }
}
function moveClaudeProjects() {
  const projects = join(homedir2(), ".claude", "projects");
  const [from, to] = [OLD_BOXES, NEW_BOXES].map((p) => p.replace(/[^a-zA-Z0-9]/g, "-"));
  for (const dir of existsSync(projects) ? readdirSync(projects) : []) {
    const target = join(projects, to + dir.slice(from.length));
    if ((dir === from || dir.startsWith(`${from}-`)) && !existsSync(target)) renameSync(join(projects, dir), target);
  }
  const file2 = join(homedir2(), ".claude.json");
  try {
    const config = JSON.parse(readFileSync(file2, "utf8"));
    const keys = Object.keys(config.projects ?? {});
    if (!keys.some((k) => k.startsWith(OLD_BOXES + sep))) return;
    config.projects = Object.fromEntries(Object.entries(config.projects).map(([k, v]) => [k.startsWith(OLD_BOXES + sep) ? NEW_BOXES + k.slice(OLD_BOXES.length) : k, v]));
    writeFileSync(file2, JSON.stringify(config, null, 2));
  } catch {
  }
}
function localMcp() {
  const claudeJson = join(homedir2(), ".claude.json");
  const servers = existsSync(claudeJson) ? JSON.parse(readFileSync(claudeJson, "utf8")).mcpServers ?? {} : {};
  const skipped = [];
  const mcpServers = {};
  for (const [name, s] of Object.entries(servers)) {
    if (s.command && /^(\/|\.|~)/.test(s.command)) skipped.push(name);
    else mcpServers[name] = s;
  }
  const mcpOAuth = Object.fromEntries(Object.entries(localCreds().mcpOAuth ?? {}).map(([k, v]) => {
    const { refreshToken, ...rest } = v;
    void refreshToken;
    return [k, rest];
  }));
  return { mcpServers, mcpOAuth, skipped };
}
function localClaudeHome() {
  const root = join(homedir2(), ".claude");
  const json = (p) => {
    try {
      return JSON.parse(readFileSync(join(root, p), "utf8"));
    } catch {
      return {};
    }
  };
  const settings = json("settings.json");
  const enabled = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
  const installed = json("plugins/installed_plugins.json").plugins ?? {};
  const known = json("plugins/known_marketplaces.json");
  const plugins = {}, marketplaces = {};
  for (const id of enabled) {
    const mine = (installed[id] ?? []).filter((e) => e.scope === "user" && e.installPath?.startsWith(root));
    const m = known[id.split("@")[1]];
    if (!mine.length || !m?.installLocation?.startsWith(root)) continue;
    plugins[id] = mine;
    marketplaces[id.split("@")[1]] = m;
  }
  const dirs = ["skills", "agents", "commands", "output-styles"].filter((d) => existsSync(join(root, d)));
  const paths = [...dirs, ...Object.values(plugins).flat().map((e) => e.installPath), ...Object.values(marketplaces).map((m) => m.installLocation)].map((p) => p.startsWith(root) ? p.slice(root.length + 1) : p);
  const tar = !paths.length ? Promise.resolve(void 0) : new Promise((done) => execFile("tar", ["-chf", "-", "--no-xattrs", "--exclude=.git", "--exclude=node_modules", "--exclude=.DS_Store", "-C", root, ...new Set(paths)], { encoding: "buffer", maxBuffer: 256 << 20, env: { ...process.env, COPYFILE_DISABLE: "1" } }, (err, out) => err ? done(void 0) : gzip(out, (e, gz) => done(e ? void 0 : gz))));
  const memory = existsSync(join(root, "CLAUDE.md")) ? readFileSync(join(root, "CLAUDE.md"), "utf8") : "";
  return { from: root, pending: paths.length > 0, tar, plugins, marketplaces, enabled: Object.keys(plugins), memory, skills: existsSync(join(root, "skills")) ? readdirSync(join(root, "skills")).filter((f) => !f.startsWith(".")).length : 0 };
}
function laptopKey() {
  const priv = join(CONFIG_DIR, "id_ed25519");
  if (!existsSync(priv)) {
    const k = ssh2.utils.generateKeyPairSync("ed25519", { comment: "box" });
    mkdirSync(CONFIG_DIR, { recursive: true });
    writeFileSync(priv, k.private, { mode: 384 });
    writeFileSync(priv + ".pub", k.public.trim() + "\n");
  }
  return { privateKey: readFileSync(priv), publicKey: readFileSync(priv + ".pub", "utf8").trim() };
}
function sshKeys(boxKey) {
  const home2 = join(homedir2(), ".ssh");
  const mine = existsSync(home2) ? readdirSync(home2).filter((f) => f.endsWith(".pub")).map((f) => readFileSync(join(home2, f), "utf8").trim().split(/\s+/).slice(0, 2).join(" ")) : [];
  return [boxKey, ...mine.filter((k) => /^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(256|384|521)|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com) [A-Za-z0-9+/=]+$/.test(k))].join("\n");
}
function localCreds() {
  const file2 = join(homedir2(), ".claude", ".credentials.json");
  if (process.platform !== "darwin") return existsSync(file2) ? JSON.parse(readFileSync(file2, "utf8")) : {};
  for (const account of [["-a", userInfo().username], []]) {
    try {
      return JSON.parse(execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials", ...account, "-w"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
    } catch {
    }
  }
  return {};
}
function localGitHubToken() {
  const out = (cmd, args, input3) => {
    try {
      return execFileSync(cmd, args, { encoding: "utf8", input: input3, stdio: [input3 ? "pipe" : "ignore", "pipe", "ignore"], env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }).trim() || void 0;
    } catch {
      return void 0;
    }
  };
  return out("gh", ["auth", "token", "--hostname", "github.com"]) ?? out("git", ["credential", "fill"], "protocol=https\nhost=github.com\n\n")?.match(/^password=(.+)$/m)?.[1];
}
function gitRepo() {
  const git2 = (...args) => {
    try {
      return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 }).trim();
    } catch {
      return "";
    }
  };
  const dir = git2("rev-parse", "--show-toplevel");
  if (!dir) return void 0;
  const url = git2("remote", "get-url", "origin") || git2("remote", "get-url", "upstream");
  const m = /github\.com[:/]+([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(url);
  if (!m) return void 0;
  const branch = git2("branch", "--show-current");
  const pushed = branch && git2("rev-parse", "--abbrev-ref", `${branch}@{upstream}`);
  const defaultBranch = git2("symbolic-ref", "--short", "refs/remotes/origin/HEAD").replace(/^origin\//, "");
  return { dir, repo: `${m[1]}/${m[2]}`, branch: pushed && branch !== defaultBranch ? branch : void 0 };
}
function localCopy() {
  const git2 = (...args) => {
    try {
      return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, maxBuffer: 16 << 20 }).trim();
    } catch {
      return "";
    }
  };
  const top = git2("rev-parse", "--show-toplevel");
  if (!top) return void 0;
  const repo = /github\.com[:/]+([\w.-]+\/[\w.-]+?)(?:\.git)?\/?$/.exec(git2("-C", top, "remote", "get-url", "origin"))?.[1];
  const files = git2("-C", top, "ls-files", "--others", "--ignored", "--exclude-standard", "--directory").split("\n").filter((f) => /(^|\/)(\.env[^/]*|[^/]+\.local|docker-compose\.override\.ya?ml)$/.test(f) && !/(^|\/)(node_modules|\.claude)\//.test(f)).slice(0, 8);
  let listening = "";
  try {
    listening = execFileSync("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 });
  } catch {
  }
  const ports = new Set([...listening.matchAll(/:(\d+) \(LISTEN\)/g)].map((m) => Number(m[1])));
  const services = Object.entries(SERVICES).filter(([port]) => ports.has(Number(port))).map(([port, name]) => `${name} :${port}`);
  return { dir: top, repo, branch: git2("-C", top, "branch", "--show-current") || void 0, files, services };
}

// src/api.ts
var file = join2(CONFIG_DIR, "config.json");
var load = () => {
  if (!existsSync2(file)) return { server: process.env.CB_SERVER ?? "https://box.abuaboud.me" };
  const c = JSON.parse(readFileSync2(file, "utf8"));
  return { ...c, server: c.server.replace("://craftboxes.abuaboud.me", "://box.abuaboud.me") };
};
var save = (c) => {
  mkdirSync2(CONFIG_DIR, { recursive: true });
  writeFileSync2(file, JSON.stringify(c, null, 2));
  chmodSync(file, 384);
};
async function api(path, init = {}, retried = false) {
  if (!load().token) await login();
  const c = load();
  const res = await fetch(new URL(path, c.server), { ...init, headers: { authorization: `Bearer ${c.token}`, ...init.body ? { "content-type": "application/json" } : {} } });
  const body = await res.json().catch(() => ({}));
  if (res.status === 401 && !retried) {
    save({ ...c, token: void 0 });
    return api(path, init, true);
  }
  if (!res.ok) fail(body.error ?? `Request failed (${res.status})`);
  return body;
}
async function login(server) {
  stopProgress();
  const c = { ...load(), ...server && { server } };
  const post = (path, body) => fetch(new URL(path, c.server), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }).then((r) => r.json());
  const code2 = await post("/api/auth/device/code", { client_id: "cb" });
  if (!code2.device_code) fail(`The server refused: ${code2.error_description ?? code2.message ?? "no device code"}`);
  console.log(`Open ${styleText2("underline", code2.verification_uri_complete)} and approve ${styleText2("bold", code2.user_code)}`);
  if (process.stdout.isTTY) spawn(process.platform === "darwin" ? "open" : "xdg-open", [code2.verification_uri_complete], { stdio: "ignore", detached: true }).on("error", () => {
  }).unref();
  for (let wait = code2.interval ?? 5; ; ) {
    await new Promise((r) => setTimeout(r, wait * 1e3));
    const t = await post("/api/auth/device/token", { grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: code2.device_code, client_id: "cb" });
    if (t.access_token) {
      c.token = t.access_token;
      break;
    }
    if (t.error === "slow_down") wait += 5;
    else if (t.error !== "authorization_pending") fail(t.error_description ?? t.error);
  }
  save(c);
  const me = await fetch(new URL("/api/me", c.server), { headers: { authorization: `Bearer ${c.token}` } }).then((r) => r.json());
  save({ ...load(), email: me.user.email });
  ok(`Signed in as ${me.user.email} ${dim(`\xB7 ${me.org.name}`)}`);
  console.log();
}
async function findBox(name) {
  const box = (await api("/api/boxes")).find((b) => b.name === name || b.id === name);
  return box ?? fail(`No box named ${name}. box ls lists them.`);
}

// src/environments.ts
import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
import { join as join3 } from "node:path";
import { styleText as styleText3 } from "node:util";
import { input, select } from "@inquirer/prompts";
async function chooseEnvironment(envName, here) {
  const envs = await api("/api/environments");
  if (envName) return waitReady(envs.find((e) => e.name === envName) ?? fail(`No environment named ${envName}.`));
  const mine = here ? envs.filter((e) => e.repo.toLowerCase() === here.repo.toLowerCase()) : [];
  const setUp = async () => setUpEnvironment(here?.repo ?? await askRepo(), here?.dir, envs);
  if (!envs.length) return waitReady(await setUp());
  if (!process.stdin.isTTY && mine.length === 1) return waitReady(mine[0]);
  const label2 = (e) => `${e.name.padEnd(16)}${dim(`${mine.includes(e) ? "this repo" : e.repo} \xB7 ${e.size ?? "small"}${e.status === "ready" ? "" : ` \xB7 ${e.status}`}`)}`;
  const create = { name: styleText3("cyan", here ? `+ Set up a new environment for ${here.repo}` : "+ Set up a new environment"), value: "" };
  const pick = await select({
    message: here ? `Environment for ${here.repo}${here.branch ? dim(` \xB7 branch ${here.branch}`) : ""}` : `Environment ${dim("(you're not in a repo)")}`,
    choices: [...mine.map((e) => ({ name: label2(e), value: e.name })), ...here && !mine.length ? [create] : [], ...envs.filter((e) => !mine.includes(e)).map((e) => ({ name: label2(e), value: e.name })), ...here && !mine.length ? [] : [create]]
  });
  return waitReady(pick ? envs.find((e) => e.name === pick) : await setUp());
}
var askRepo = () => input({ message: "GitHub repo (owner/name)", validate: (v) => /^[\w.-]+\/[\w.-]+$/.test(v.trim()) || "Write it as owner/name" }).then((v) => v.trim());
async function waitReady(env) {
  if (env.status === "ready") return env;
  if (env.status === "failed") fail(`The ${env.name} environment failed to build: ${(env.error ?? "").split("\n")[0]}. Rebuild it from the dashboard.`);
  const t0 = Date.now();
  const bar = progress([[0, env.progress ?? "Building the snapshot"]], { pace: 9e4 });
  for (; ; ) {
    await new Promise((r) => setTimeout(r, 2e3));
    const now = await api(`/api/environments/${env.id}`);
    if (now.progress) bar.say(now.progress);
    if (now.status === "ready") {
      const secs = Math.round((Date.now() - t0) / 1e3);
      bar(`Snapshot ready ${dim(`${secs >= 60 ? `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s` : `${secs}s`} \xB7 next boxes start in about 2s`)}`);
      return now;
    }
    if (now.status === "failed") {
      bar();
      fail(`The build failed: ${(now.error ?? "").trim().split("\n").slice(-3).join(" ").slice(0, 300)}`);
    }
  }
}
function detectSetup(dir) {
  if (!dir) return void 0;
  const has = (f) => existsSync3(join3(dir, f));
  let scripts = {};
  try {
    scripts = JSON.parse(readFileSync3(join3(dir, "package.json"), "utf8")).scripts ?? {};
  } catch {
  }
  const pm = has("bun.lock") || has("bun.lockb") ? "bun" : has("pnpm-lock.yaml") ? "pnpm" : has("yarn.lock") ? "yarn" : has("package-lock.json") ? "npm" : void 0;
  const own = ["setup", "bootstrap"].find((k) => scripts[k]);
  if (pm && own) return { setup: `${pm} run ${own}`, from: `the ${own} script in package.json` };
  const byLock = [
    ["bun.lock", "bun install"],
    ["bun.lockb", "bun install"],
    ["pnpm-lock.yaml", "pnpm install --frozen-lockfile"],
    ["yarn.lock", "yarn install --immutable"],
    ["package-lock.json", "npm ci"],
    ["uv.lock", "uv sync"],
    ["requirements.txt", "pip install -r requirements.txt"],
    ["go.mod", "go mod download"],
    ["Cargo.lock", "cargo fetch"],
    ["Gemfile.lock", "bundle install"]
  ];
  const hit = byLock.find(([f]) => has(f));
  return hit && { setup: hit[1], from: hit[0] };
}
function detectSize(dir) {
  if (!dir) return { size: "small", why: "the default" };
  let files = 0, kb = 0;
  try {
    files = Number(execFileSync2("sh", ["-c", "git ls-files | wc -l"], { cwd: dir, encoding: "utf8", timeout: 5e3 }).trim());
  } catch {
  }
  try {
    kb = Number(execFileSync2("du", ["-sk", ".git"], { cwd: dir, encoding: "utf8", timeout: 5e3 }).split("	")[0]);
  } catch {
  }
  const why = `${files.toLocaleString()} files, ${(kb / 1024 / 1024).toFixed(1)} GB of history`;
  return files > 2e4 || kb > 1024 * 1024 ? { size: "medium", why } : { size: "small", why };
}
function secretNames(dir) {
  for (const f of [".env.example", ".env.sample", ".env.template"]) {
    try {
      return readFileSync3(join3(dir ?? "", f), "utf8").split("\n").map((l) => /^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/.exec(l)?.[1]).filter((k) => !!k);
    } catch {
    }
  }
  return [];
}
async function setUpEnvironment(repo, dir, envs) {
  miss(`${repo} ${dim("has no environment yet. Setting one up:")}`);
  const detected = detectSetup(dir), sized = detectSize(dir), names = secretNames(dir);
  let name = repo.split("/")[1].toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").slice(0, 28) || "env";
  for (let i = 2, base = name; envs.some((e) => e.name === name); i++) name = `${base}-${i}`;
  let setup = detected?.setup ?? "", size = sized.size, secrets = "";
  const sizes = { small: "2 vCPU, 4 GB", medium: "4 vCPU, 8 GB", large: "8 vCPU, 16 GB" };
  for (; ; ) {
    console.log();
    console.log(`  ${dim("Name     ")} ${name}`);
    console.log(`  ${dim("Repo     ")} ${repo}`);
    console.log(`  ${dim("Machine  ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${sizes[size]}${size === sized.size ? ` (${sized.why})` : ""}`)}`);
    console.log(`  ${dim("Setup    ")} ${setup ? styleText3("cyan", setup) : dim("none")} ${dim(detected && setup === detected.setup ? `\xB7 from ${detected.from}` : setup ? "" : "\xB7 boxes start from a plain clone")}`);
    if (names.length) console.log(`  ${dim("Secrets  ")} ${names.join(", ")} ${dim(secrets ? "\xB7 values copied from .env" : "\xB7 names from .env.example, values empty")}`);
    console.log();
    const choice = await select({ message: "Create it?", choices: [
      { name: "Create and start a box", value: "go" },
      ...names.length && !secrets && dir && existsSync3(join3(dir, ".env")) ? [{ name: "Copy the secret values from my .env first", value: "secrets" }] : [],
      { name: "Change something", value: "edit" },
      { name: "Cancel", value: "cancel" }
    ] });
    if (choice === "cancel") process.exit(0);
    if (choice === "secrets") {
      const values = Object.fromEntries(readFileSync3(join3(dir, ".env"), "utf8").split("\n").map((l) => /^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(l)).filter((m) => !!m).map((m) => [m[1], m[2].replace(/^(['"])(.*)\1$/, "$2")]));
      secrets = names.filter((n) => values[n] !== void 0).map((n) => `${n}=${values[n]}`).join("\n");
      ok(`${secrets.split("\n").filter(Boolean).length} of ${names.length} secret values copied`);
      continue;
    }
    if (choice === "edit") {
      name = await input({ message: "Name", default: name, validate: (v) => /^[a-z0-9][a-z0-9-]{0,30}$/.test(v) || "Lowercase letters, digits and dashes" });
      size = await select({ message: "Machine", default: size, choices: Object.entries(sizes).map(([value, spec]) => ({ name: `${value.padEnd(8)}${dim(spec)}`, value })) });
      setup = (await input({ message: "Setup script (runs once in the snapshot; empty for none)", default: setup })).trim();
      continue;
    }
    break;
  }
  await ensureGitHub(repo);
  const { id } = await api("/api/environments", { method: "POST", body: JSON.stringify({ name, repo, size, setup, secrets }) });
  ok(`Environment ${styleText3("cyan", name)} created`);
  return { id, name, repo, status: "building", size, progress: "Starting a build machine" };
}
async function ensureGitHub(repo) {
  const open = (url) => {
    if (process.stdout.isTTY) spawn2(process.platform === "darwin" ? "open" : "xdg-open", [url], { stdio: "ignore", detached: true }).on("error", () => {
    }).unref();
  };
  const status = () => api("/api/github/repos");
  let r = await status();
  if (!r.linked) {
    const url = new URL("/connect/github", load().server).toString();
    console.log(`
${styleText3("yellow", "!")} Connect GitHub so Box can clone ${repo} and your boxes can push as you.`);
    console.log(dim(`  Opening ${url}`));
    open(url);
    const bar = progress([[0, "Waiting for you to connect GitHub in the browser"]], { pace: 6e4 });
    for (const end = Date.now() + 10 * 6e4; !r.linked; r = await status()) {
      if (Date.now() > end) {
        bar();
        fail("Gave up waiting after 10 minutes. Run box again when it's done.");
      }
      await new Promise((resolve) => setTimeout(resolve, 2500));
    }
    bar();
    ok("GitHub connected");
  }
  const isPublic = await fetch(`https://api.github.com/repos/${repo}`, { signal: AbortSignal.timeout(5e3) }).then((res) => res.ok ? res.json() : null).then((j) => j && !j.private).catch(() => false);
  if (!isPublic && !r.repos.some((p) => p.fullName.toLowerCase() === repo.toLowerCase())) fail(`Your GitHub login can't reach ${repo}.`);
}

// src/local.ts
import { execFileSync as execFileSync3, spawn as spawn3 } from "node:child_process";
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync2, watch, writeFileSync as writeFileSync4 } from "node:fs";
import { homedir as homedir4 } from "node:os";
import { basename, dirname, join as join5 } from "node:path";
import { select as select2 } from "@inquirer/prompts";

// src/ssh.ts
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
import { connect } from "node:net";
import { homedir as homedir3 } from "node:os";
import { join as join4 } from "node:path";
var SELF = [process.execPath, process.argv[1]];
var selfCommand = (...args) => [...SELF, ...args].map((a) => `"${a}"`).join(" ");
function ensureSshConfig() {
  const sshDir = join4(homedir3(), ".ssh");
  mkdirSync3(sshDir, { recursive: true, mode: 448 });
  laptopKey();
  writeFileSync3(join4(sshDir, "box"), [
    "Host *.box",
    "  User user",
    `  IdentityFile "${join4(CONFIG_DIR, "id_ed25519")}"`,
    `  ProxyCommand ${selfCommand("proxy", "%h")}`,
    "  StrictHostKeyChecking no",
    "  UserKnownHostsFile /dev/null",
    "  LogLevel ERROR",
    "  ServerAliveInterval 30",
    ""
  ].join("\n"));
  const config = join4(sshDir, "config");
  const now = existsSync4(config) ? readFileSync4(config, "utf8") : "";
  const renamed = now.replace(/^Include ~\/\.ssh\/craftboxes$/m, "Include ~/.ssh/box");
  const next = /^Include ~\/\.ssh\/box$/m.test(renamed) ? renamed : `Include ~/.ssh/box

${renamed}`;
  if (next !== now) writeFileSync3(config, next, { mode: 384 });
  rmSync(join4(sshDir, "craftboxes"), { force: true });
}
async function proxy(host) {
  const name = host.replace(/\.box$/, "");
  const got = await api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) });
  const targets = [...got.ssh.hosts].reverse();
  for (let attempt = 0; attempt < 20; attempt++) {
    for (const [h, port] of targets) {
      const sock = await new Promise((done) => {
        const s = connect({ host: h, port, timeout: 5e3 });
        s.once("connect", () => {
          s.setTimeout(0);
          done(s);
        }).once("error", () => done(void 0)).once("timeout", () => {
          s.destroy();
          done(void 0);
        });
      });
      if (!sock) continue;
      process.stdin.pipe(sock);
      sock.pipe(process.stdout);
      sock.on("close", () => process.exit(0));
      return;
    }
    await new Promise((r) => setTimeout(r, 1e3));
  }
  fail(`Couldn't reach ${name} over SSH.`);
}

// src/local.ts
async function newLocalBox(env, branch, name, o) {
  const stop = progress([[0, "Finding a machine"], [900, `Unpacking the ${env} snapshot`], [2500, "Connecting GitHub"], [9e3, "Bigger machines take a few extra seconds"]]);
  const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, agent: "local", size: o.size, idleMinutes: o.alwaysOn ? 0 : void 0, sshKey: sshKeys(laptopKey().publicKey) }) }).finally(() => stop());
  ok(`${box.name} is ready ${dim(`\xB7 ssh ${box.name}.box`)}`);
  await openLocal({ name: box.name, env, here: localCopy(), branch, fresh: true });
}
var CLONE = ".clone";
var MIRROR_SKIP = [".git", "node_modules", "dist", "build", ".turbo", ".nx", ".next", ".cache", "coverage", ".DS_Store"];
function claudeMd(name, here) {
  const lines = [
    `# Machine: ${name}.box`,
    `Run every command there with \`ssh ${name}.box '<command>'\`, never on this laptop. ./repo is a git worktree on this laptop and its files are mirrored to ~/repo on the machine: read and edit files in ./repo.`,
    `Run git here, never on the machine: \`git -C repo status\`, commit and push from ./repo.`
  ];
  if (here) {
    const where = tilde(here.dir);
    lines.push("", `# The user's local copy: ${where}${here.repo ? ` (${here.repo}${here.branch ? `, ${here.branch}` : ""})` : ""}`);
    const mine = here.files.map((f) => `${where}/${f}`);
    if (mine.length) lines.push(`"My .env", "my data" or "my setup" mean their laptop: ${mine.join(", ")}.`);
    if (here.services.length) lines.push(`Running on their laptop: ${here.services.join(", ")}.`);
  }
  lines.push("", "How to do anything else: @../../llms.txt");
  return lines.join("\n") + "\n";
}
function boxFolder(env, name) {
  return join5(BOXES, env, name.startsWith(`${env}-`) ? name.slice(env.length + 1) : name);
}
function localBoxes() {
  if (!existsSync5(BOXES)) return [];
  const found = [];
  for (const top of readdirSync2(BOXES, { withFileTypes: true })) {
    if (!top.isDirectory() || top.name.startsWith(".")) continue;
    const env = top.name, envDir = join5(BOXES, env);
    for (const entry of readdirSync2(envDir, { withFileTypes: true })) {
      const file2 = join5(envDir, entry.name, ".box.json");
      if (!entry.isDirectory() || !existsSync5(file2)) continue;
      const state = JSON.parse(readFileSync5(file2, "utf8"));
      if (state.name) found.push({ name: state.name, env, folder: join5(envDir, entry.name), owner: state.owner });
    }
  }
  return found;
}
function prepareFolder(name, env, here, mcp) {
  const folder = boxFolder(env, name);
  const legacy = join5(BOXES, name);
  if (!existsSync5(folder) && existsSync5(join5(legacy, ".box.json"))) {
    mkdirSync4(dirname(folder), { recursive: true });
    renameSync2(legacy, folder);
  }
  mkdirSync4(join5(folder, ".claude"), { recursive: true });
  const stateFile = join5(folder, ".box.json");
  const state = existsSync5(stateFile) ? JSON.parse(readFileSync5(stateFile, "utf8")) : {};
  const local = here ?? state.local;
  const next = { ...state, name, env, owner: load().email, local };
  writeFileSync4(join5(folder, "CLAUDE.md"), claudeMd(name, local));
  const hook = (direction) => [{ matcher: "Bash", hooks: [{ type: "command", command: selfCommand("sync", name, direction, folder), timeout: 600 }] }];
  writeFileSync4(join5(folder, ".claude", "settings.json"), JSON.stringify({ enableAllProjectMcpServers: true, permissions: { allow: [`Bash(ssh ${name}.box:*)`, `Bash(scp:*)`, `Bash(rsync:*)`, "mcp__box"] }, hooks: { PreToolUse: hook("push"), PostToolUse: hook("pull") } }, null, 2));
  if (mcp) writeFileSync4(join5(folder, ".mcp.json"), JSON.stringify({ mcpServers: { box: { type: "http", url: mcp.url, headers: { authorization: `Box ${mcp.token}` } } } }, null, 2), { mode: 384 });
  writeFileSync4(stateFile, JSON.stringify(next, null, 2));
  return folder;
}
async function ensureClone(env) {
  const clone = join5(BOXES, env.name, CLONE);
  const token = localGitHubToken();
  const auth = token ? ["-c", `http.https://github.com/.extraheader=AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`] : [];
  if (existsSync5(join5(clone, ".git"))) {
    await git([...auth, "-C", clone, "fetch", "-q", "--prune", "origin"]);
    return clone;
  }
  const partial = `${clone}.partial`;
  rmSync2(partial, { recursive: true, force: true });
  mkdirSync4(dirname(clone), { recursive: true });
  const bar = progress([[0, `Cloning ${env.repo} once for every ${env.name} box`]], { pace: 6e4 });
  const cloned = await git([...auth, "clone", "-q", "--no-checkout", "--filter=blob:none", token ? `https://github.com/${env.repo}.git` : `git@github.com:${env.repo}.git`, partial]);
  if (!cloned.ok) {
    bar();
    fail(`Couldn't clone ${env.repo}: ${cloned.err.split("\n").at(-1)}. Run gh auth login and try again.`);
  }
  await git(["-C", partial, "update-ref", "--no-deref", "HEAD", "HEAD"]);
  renameSync2(partial, clone);
  bar(`Cloned ${env.repo} into ${tilde(clone)}`);
  return clone;
}
async function ensureWorktree({ clone, folder, name, branch }) {
  const repo = join5(folder, "repo");
  if (existsSync5(join5(repo, ".git")) && statSync2(join5(repo, ".git")).isFile()) return false;
  if (existsSync5(repo)) renameSync2(repo, `${repo}.mirror`);
  await git(["-C", clone, "worktree", "prune"]);
  const onOrigin = branch && (await git(["-C", clone, "rev-parse", "-q", "--verify", `refs/remotes/origin/${branch}`])).ok;
  const tries = onOrigin ? [["worktree", "add", "-q", repo, branch], ["worktree", "add", "-q", "--detach", repo, `origin/${branch}`]] : [["worktree", "add", "-q", "--no-track", "-b", `cb/${name}`, repo, "origin/HEAD"], ["worktree", "add", "-q", repo, `cb/${name}`]];
  for (const args of tries) {
    const r = await git(["-C", clone, ...args]);
    if (r.ok) return true;
    rmSync2(repo, { recursive: true, force: true });
  }
  if (existsSync5(`${repo}.mirror`)) renameSync2(`${repo}.mirror`, repo);
  return fail(`Couldn't make a worktree for ${name} in ${tilde(clone)}.`);
}
async function setUpWorktree({ clone, folder, name, branch, fresh }) {
  if (!await ensureWorktree({ clone, folder, name, branch })) return false;
  const repo = join5(folder, "repo");
  const bar = progress([[0, fresh ? `Sending ${tilde(repo)} to ${name}` : `Copying ${name}'s work into ${tilde(repo)}`]], { pace: 6e4 });
  let copied = false;
  for (let attempt = 0; attempt < 4 && !copied; attempt++) copied = await (fresh ? push(name, folder, false, ["--delete"]) : pull(name, folder)) || (await new Promise((r) => setTimeout(r, 3e3)), false);
  if (copied && fresh) await alignBox(name, repo);
  bar(copied ? `${tilde(repo)} is a worktree of ${tilde(clone)}` : void 0);
  if (!copied) fail(`Couldn't sync ${tilde(repo)} with ${name}. Check that ssh ${name}.box works.`);
  rmSync2(`${repo}.mirror`, { recursive: true, force: true });
  return true;
}
async function alignBox(name, repo) {
  const sha = (await git(["-C", repo, "rev-parse", "HEAD"])).out;
  const branch = (await git(["-C", repo, "branch", "--show-current"])).out;
  if (!/^[0-9a-f]{40}$/.test(sha)) return;
  const point = branch && /^[\w./-]+$/.test(branch) ? `git symbolic-ref HEAD refs/heads/${branch}` : "git checkout -q --detach";
  await new Promise((done) => spawn3("ssh", [`${name}.box`, `cd repo && { git cat-file -e ${sha}^{commit} 2>/dev/null || git fetch -q origin ${sha} 2>/dev/null; } && git cat-file -e ${sha}^{commit} && ${point} && git reset -q ${sha}`], { stdio: "ignore" }).on("close", done).on("error", done));
}
function newestTranscript(folder) {
  const sessions = join5(homedir4(), ".claude", "projects", folder.replace(/[^a-zA-Z0-9]/g, "-"));
  return existsSync5(sessions) ? readdirSync2(sessions).filter((f) => f.endsWith(".jsonl")).map((f) => join5(sessions, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs)[0] : void 0;
}
function backgroundSession(folder) {
  try {
    const sessions = JSON.parse(execFileSync3("claude", ["agents", "--json", "--cwd", folder], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e4 }));
    return sessions.filter((s) => s.kind === "background").sort((a, b) => b.startedAt - a.startedAt)[0]?.id;
  } catch {
    return void 0;
  }
}
function laptopState(folder) {
  const branch = (() => {
    try {
      return execFileSync3("git", ["-C", join5(folder, "repo"), "branch", "--show-current"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 }).trim();
    } catch {
      return "";
    }
  })();
  const newest = newestTranscript(folder);
  const titles = newest ? [...readFileSync5(newest, "utf8").matchAll(/"aiTitle":"((?:[^"\\]|\\.)*)"/g)] : [];
  const task = titles.length ? (() => {
    try {
      return JSON.parse(`"${titles.at(-1)[1]}"`);
    } catch {
      return void 0;
    }
  })() : void 0;
  return { branch: branch || void 0, task, working: !!newest && Date.now() - statSync2(newest).mtimeMs < 9e4 };
}
var git = (args) => new Promise((done) => {
  const p = spawn3("git", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
  let out = "", err = "";
  p.stdout.on("data", (d) => {
    out += d;
  });
  p.stderr.on("data", (d) => {
    err += d;
  });
  p.on("close", (code2) => done({ ok: code2 === 0, out: out.trim(), err: err.trim() }));
  p.on("error", (e) => done({ ok: false, out: "", err: e.message }));
});
function watchRepo(name, folder) {
  const repo = join5(folder, "repo");
  let timer, poll, running = false, again = false;
  const run = async () => {
    if (running) {
      again = true;
      return;
    }
    running = true;
    do {
      again = false;
      await push(name, folder, true);
    } while (again);
    running = false;
  };
  const schedule = () => {
    clearTimeout(timer);
    timer = setTimeout(() => void run(), 800);
  };
  const pollInstead = () => {
    poll ??= setInterval(() => void run(), 5e3);
  };
  const close = process.platform === "linux" ? watchEachDir(repo, schedule, pollInstead) : watchTree(repo, schedule, pollInstead);
  return async () => {
    clearTimeout(timer);
    clearInterval(poll);
    close();
    while (running) await new Promise((r) => setTimeout(r, 200));
  };
}
function watchTree(repo, changed, failed) {
  try {
    const watcher = watch(repo, { recursive: true }, (_event, file2) => {
      if (file2 && !String(file2).split(/[\\/]/).some((part) => MIRROR_SKIP.includes(part))) changed();
    }).on("error", () => {
      watcher.close();
      failed();
    });
    return () => watcher.close();
  } catch {
    failed();
    return () => void 0;
  }
}
function watchEachDir(repo, changed, failed) {
  const watchers = /* @__PURE__ */ new Map();
  let broken = false;
  const closeAll = () => {
    for (const w of watchers.values()) w.close();
    watchers.clear();
  };
  const fail2 = () => {
    if (broken) return;
    broken = true;
    closeAll();
    failed();
  };
  const add = (dir) => {
    if (broken || watchers.has(dir)) return;
    try {
      watchers.set(dir, watch(dir, (_event, file2) => {
        if (file2) track(join5(dir, String(file2)));
        changed();
      }).on("error", fail2));
    } catch {
      fail2();
    }
  };
  const track = (path) => {
    const known = watchers.get(path);
    if (known && !existsSync5(path)) {
      known.close();
      watchers.delete(path);
    } else if (!known && basename(path) !== ".git" && isDirectory(path) && !ignored(repo, path)) add(path);
  };
  const dirs = trackedDirs(repo);
  if (dirs) for (const d of dirs) add(d);
  else fail2();
  return closeAll;
}
function trackedDirs(repo) {
  try {
    const files = execFileSync3("git", ["-C", repo, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 << 20, stdio: ["ignore", "pipe", "ignore"] }).split("\0");
    const dirs = /* @__PURE__ */ new Set([repo]);
    for (const f of files) for (let d = dirname(f); d !== "." && !dirs.has(join5(repo, d)); d = dirname(d)) dirs.add(join5(repo, d));
    return [...dirs].filter(isDirectory);
  } catch {
    return void 0;
  }
}
function ignored(repo, path) {
  try {
    execFileSync3("git", ["-C", repo, "check-ignore", "-q", path], { stdio: "ignore" });
    return true;
  } catch {
    return false;
  }
}
function isDirectory(path) {
  try {
    return statSync2(path).isDirectory();
  } catch {
    return false;
  }
}
async function prune(boxes) {
  const email = load().email;
  const mine = localBoxes().filter((b) => b.owner && b.owner === email);
  if (!mine.length) return;
  const alive = new Set((boxes ?? await api("/api/boxes")).filter((b) => b.state !== "gone").map((b) => b.name));
  for (const b of mine) {
    if (alive.has(b.name)) continue;
    await dropFolder(b);
    console.log(dim(`Removed ${tilde(b.folder)}: ${b.name} no longer exists`));
  }
}
async function dropFolder(b) {
  rmSync2(b.folder, { recursive: true, force: true });
  const clone = join5(BOXES, b.env, CLONE);
  if (existsSync5(clone)) await git(["-C", clone, "worktree", "prune"]);
}
var rsync = (from, to, extra = [], quiet2 = false) => new Promise((done) => {
  const args = ["-az", ...extra, ...MIRROR_SKIP.flatMap((x) => ["--exclude", x]), from, to];
  const p = spawn3("rsync", args, { stdio: ["ignore", "ignore", "pipe"] });
  let err = "";
  p.stderr.on("data", (d) => {
    err += d;
  });
  p.on("close", (code2) => {
    if (code2 && !quiet2) console.error(dim(`rsync: ${err.trim().split("\n").slice(-2).join(" ")}`));
    done(code2 === 0);
  });
});
var pull = (name, folder, quiet2 = false) => rsync(`${name}.box:repo/`, join5(folder, "repo") + "/", ["--delete"], quiet2);
var push = (name, folder, quiet2 = false, extra = []) => rsync(join5(folder, "repo") + "/", `${name}.box:repo/`, extra, quiet2);
async function openLocal({ name, env, here, branch, fresh = false }) {
  ensureSshConfig();
  const stop = progress([[0, `Opening ${name}`], [1500, "Waking the box up"]]);
  const [got, envs] = await Promise.all([
    api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) }),
    api("/api/environments")
  ]).finally(() => stop());
  const environment = envs.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
  const folder = prepareFolder(name, env, here, got.mcp);
  try {
    writeFileSync4(join5(BOXES, "llms.txt"), await fetch(new URL("/llms.txt", load().server)).then((r) => r.ok ? r.text() : Promise.reject()));
  } catch {
  }
  trustFolder(folder);
  if (!await setUpWorktree({ clone: await ensureClone(environment), folder, name, branch, fresh })) await pull(name, folder);
  if (here) ok(`Your local copy: ${tilde(here.dir)}${here.files.length || here.services.length ? dim(` \xB7 ${[...here.files, ...here.services].join(", ")}`) : ""}`);
  console.log(dim(`\u25CF Keeping ${name} awake and ${tilde(folder)}/repo in sync while Claude is open`));
  const stopWatching = watchRepo(name, folder);
  const heartbeat = () => void api(`/api/boxes/${name}/heartbeat`, { method: "POST", body: JSON.stringify({ typed: true, ...laptopState(folder) }) }).catch(() => void 0);
  const beat = setInterval(heartbeat, 6e4);
  heartbeat();
  const background = backgroundSession(folder);
  const code2 = background && await claude(folder, ["attach", background]) === 0 ? 0 : await claude(folder, ["--dangerously-skip-permissions", ...newestTranscript(folder) ? ["--continue"] : []]);
  clearInterval(beat);
  await stopWatching();
  await push(name, folder);
  ok(`Claude closed \xB7 repo synced \xB7 ${name} sleeps once it's quiet ${dim(`\xB7 come back with: box ${name}`)}`);
  process.exit(code2);
}
var claude = (folder, args) => new Promise((done) => spawn3("claude", args, { cwd: folder, stdio: "inherit", env: { ...process.env, ...process.getuid?.() === 0 && { IS_SANDBOX: "1" } } }).on("error", () => fail("Claude Code is not installed on this laptop. npm i -g @anthropic-ai/claude-code, or use box --remote.")).on("close", (c) => done(c ?? 0)));
function trustFolder(folder) {
  const file2 = join5(homedir4(), ".claude.json");
  try {
    const config = JSON.parse(readFileSync5(file2, "utf8"));
    if (config.projects?.[folder]?.hasTrustDialogAccepted) return;
    config.projects = { ...config.projects, [folder]: { ...config.projects?.[folder], hasTrustDialogAccepted: true } };
    writeFileSync4(file2, JSON.stringify(config, null, 2));
  } catch {
  }
}
async function sync(name, direction, folder) {
  const chunks = [];
  for await (const c of process.stdin) chunks.push(c);
  let command = "";
  try {
    command = JSON.parse(Buffer.concat(chunks).toString("utf8")).tool_input?.command ?? "";
  } catch {
  }
  if (!command.includes(`${name}.box`) || !existsSync5(join5(folder, "repo"))) return;
  await (direction === "push" ? push(name, folder) : pull(name, folder));
}
async function code(envArg) {
  const boxes = await api("/api/boxes");
  await prune(boxes);
  const here = localBoxes().filter((b) => existsSync5(join5(b.folder, "repo")));
  const envs = [...new Set(here.map((b) => b.env))].sort();
  if (!envs.length) return console.log(dim("No boxes on this laptop yet. Run box first."));
  const env = envArg ?? (envs.length === 1 ? envs[0] : await select2({ message: "Open the boxes of which environment?", choices: envs.map((e) => ({ name: `${e.padEnd(18)}${dim(`${here.filter((b) => b.env === e).length} boxes`)}`, value: e })) }));
  const group = here.filter((b) => b.env === env);
  if (!group.length) fail(`No boxes from ${env} on this laptop.`);
  const awake = new Set(boxes.filter((b) => b.state === "running").map((b) => b.name));
  const environment = (await api("/api/environments")).find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
  const clone = await ensureClone(environment);
  const migrated = [];
  for (const b of group.filter((g) => awake.has(g.name))) {
    if (await setUpWorktree({ clone, folder: b.folder, name: b.name, branch: boxes.find((x) => x.name === b.name)?.branch ?? void 0, fresh: false })) migrated.push(b.name);
  }
  const workspace = join5(BOXES, env, `${env}.code-workspace`);
  writeFileSync4(workspace, JSON.stringify({ folders: group.map((b) => ({ name: basename(b.folder), path: `${basename(b.folder)}/repo` })), settings: { "files.exclude": { "**/node_modules": true } } }, null, 2));
  spawn3("code", [workspace], { stdio: "ignore", detached: true }).on("error", () => miss(`VS Code's code command isn't installed. In VS Code run: Shell Command: Install 'code' command in PATH, or open ${tilde(workspace)}.`)).unref();
  ok(`${group.length} ${env} box${group.length === 1 ? "" : "es"} in one window ${dim(`\xB7 ${tilde(workspace)}`)}`);
  await Promise.all(group.filter((b) => awake.has(b.name) && !migrated.includes(b.name)).map((b) => pull(b.name, b.folder, true)));
  const stops = group.map((b) => watchRepo(b.name, b.folder));
  console.log(dim("Syncing your saves to the boxes. Leave this open; ctrl+c stops syncing."));
  await new Promise((resolve) => process.once("SIGINT", resolve));
  await Promise.all(stops.map((s) => s()));
  ok("Stopped syncing");
  process.exit(0);
}
async function rm(name, opts = {}) {
  const local = localBoxes().find((b) => b.name === name);
  if (local && existsSync5(join5(local.folder, "repo"))) await push(name, local.folder, true);
  const stop = progress([[0, `Saving unpushed work to cb/${name}`], [2500, `Deleting ${name}`], [6e3, "Cleaning up its preview URLs"]]);
  await api(`/api/boxes/${name}${opts.force ? "?force=1" : ""}`, { method: "DELETE" }).finally(() => stop());
  if (local) await dropFolder(local);
  ok(`Deleted ${name}${local ? dim(` \xB7 removed ${tilde(local.folder)}`) : ""}`);
}

// src/remote.ts
import { createHash } from "node:crypto";
import { styleText as styleText4 } from "node:util";
import { select as select3 } from "@inquirer/prompts";
import ssh22 from "ssh2";

// src/predict.ts
import xterm from "@xterm/headless";
function predictor(out, cols, rows) {
  const screen = new xterm.Terminal({ cols, rows, scrollback: 0, allowProposedApi: true });
  let pending = [];
  let visible = false;
  let parsing = 0;
  let boxCursor;
  let quietUntil = 0;
  const inputStart = /* @__PURE__ */ new Map();
  const buf = () => screen.buffer.active;
  const cursor = () => ({ x: buf().cursorX, y: buf().cursorY });
  const chars = (x, y) => buf().getLine(buf().viewportY + y)?.getCell(x)?.getChars() || " ";
  const paint = (x, y) => {
    const c = buf().getLine(buf().viewportY + y)?.getCell(x);
    if (!c) return "";
    const color = (rgb, pal, v, base) => rgb ? `;${base + 8};2;${v >> 16 & 255};${v >> 8 & 255};${v & 255}` : pal ? `;${base + 8};5;${v}` : "";
    const sgr = `0${c.isBold() ? ";1" : ""}${c.isDim() ? ";2" : ""}${c.isItalic() ? ";3" : ""}${c.isUnderline() ? ";4" : ""}${c.isInverse() ? ";7" : ""}${c.isStrikethrough() ? ";9" : ""}` + color(c.isFgRGB(), c.isFgPalette(), c.getFgColor(), 30) + color(c.isBgRGB(), c.isBgPalette(), c.getBgColor(), 40);
    return `\x1B[${y + 1};${x + 1}H\x1B[${sgr}m${c.getChars() || " "}`;
  };
  const at = (x, y) => `\x1B[${y + 1};${x + 1}H`;
  const caret = () => pending.length ? { x: pending.at(-1).x + (pending.at(-1).ch === "\b" ? 0 : 1), y: pending.at(-1).y } : boxCursor ?? cursor();
  function giveUp(quiet2 = 0) {
    if (!pending.length) return;
    const { x, y } = cursor();
    out(`\x1B7${pending.map((g) => paint(g.x, g.y)).join("")}\x1B8${at(x, y)}`);
    pending = [];
    quietUntil = Date.now() + quiet2;
  }
  function settle() {
    if (parsing) return;
    const now = Date.now(), { x, y } = cursor();
    if (pending.some((g) => g.y !== y)) return giveUp(500);
    const passed = (g) => g.ch === "\b" ? x <= g.x : x > g.x;
    if (pending.some((g) => passed(g) && chars(g.x, g.y) !== (g.ch === "\b" ? " " : g.ch))) return giveUp(300);
    pending = pending.filter((g) => {
      if (!passed(g)) return true;
      if (g.ch !== "\b") inputStart.set(g.y, Math.min(inputStart.get(g.y) ?? g.x, g.x));
      return false;
    });
    if (pending.some((g) => now - g.at > 1500)) return giveUp(1e3);
    if (!pending.length) return;
    const c = caret();
    out(`\x1B7\x1B[0m${pending.map((g) => at(g.x, g.y) + (g.ch === "\b" ? " " : g.ch)).join("")}\x1B8${at(c.x, c.y)}`);
  }
  const timer = setInterval(settle, 200);
  timer.unref();
  return {
    fromBox(d) {
      out(d);
      const text = Buffer.from(d).toString("latin1");
      const show = text.lastIndexOf("\x1B[?25h"), hide = text.lastIndexOf("\x1B[?25l");
      if (show !== hide) visible = show > hide;
      const end = /\x1b\[(\d+);(\d+)H(?:\x1b\[\?25[hl])*$/.exec(text);
      boxCursor = end ? { x: Number(end[2]) - 1, y: Number(end[1]) - 1 } : void 0;
      parsing++;
      screen.write(d, () => {
        parsing--;
        settle();
      });
    },
    fromKeys(d) {
      if (!visible || parsing && !boxCursor && !pending.length || Date.now() < quietUntil || pending.length > 40) return;
      for (const b of d) {
        const { x, y } = caret();
        if (b >= 32 && b < 127 && x < cols - 2 && d.length === 1) {
          const ch = String.fromCharCode(b);
          pending.push({ x, y, ch, at: Date.now() });
          out(`\x1B7\x1B[0m${at(x, y)}${ch}\x1B8${at(x + 1, y)}`);
        } else if ((b === 127 || b === 8) && d.length === 1) {
          const last = pending.at(-1);
          if (last && last.ch !== "\b" && last.x === x - 1) {
            pending.pop();
            out(`\x1B7${paint(x - 1, y)}\x1B8${at(x - 1, y)}`);
          } else if (x - 1 >= (inputStart.get(y) ?? Infinity) && (chars(x, y) === " " || pending.some((g) => g.ch === "\b" && g.x === x && g.y === y))) {
            pending.push({ x: x - 1, y, ch: "\b", at: Date.now() });
            out(`\x1B7\x1B[0m${at(x - 1, y)} \x1B8${at(x - 1, y)}`);
          }
        }
      }
    },
    resize(c, r) {
      cols = c;
      giveUp();
      screen.resize(c, r);
    }
  };
}

// src/remote.ts
var mcpCount = (s) => {
  const n = Object.keys(s).length;
  return `${n} MCP server${n === 1 ? "" : "s"}`;
};
async function newRemoteBox(env, branch, name, o) {
  const agent = o.agent;
  const idle = o.alwaysOn ? "0" : void 0;
  const { mcpServers, mcpOAuth, skipped } = localMcp();
  ok(`${mcpCount(mcpServers)} will be copied into the box ${dim(skipped.length ? `(skipping local-only: ${skipped.join(", ")})` : "")}`);
  const claudeHome = localClaudeHome();
  const stop = progress([
    [0, "Finding a machine"],
    [900, `Unpacking the ${env} snapshot`],
    [2e3, "Signing Claude Code in"],
    [2900, `Copying ${mcpCount(mcpServers)}`],
    [3300, `Copying ${claudeHome.skills} skill${claudeHome.skills === 1 ? "" : "s"} and ${claudeHome.enabled.length} plugin${claudeHome.enabled.length === 1 ? "" : "s"}`],
    [3700, "Connecting GitHub"],
    [4600, "Waking Claude up"],
    [9e3, "Bigger machines take a few extra seconds"]
  ]);
  const { tar, ...home2 } = claudeHome;
  const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, size: o.size, idleMinutes: idle === void 0 ? void 0 : Number(idle), mcpServers, mcpOAuth, claudeHome: home2, sshKey: sshKeys(laptopKey().publicKey), agent }) }).finally(() => stop());
  if (home2.pending) void sendHome(box.name, tar);
  await attach(box.name, { name: box.name, ssh: box.ssh });
}
async function sendHome(name, tar) {
  const buf = await tar;
  if (!buf || buf.length > 30 << 20) return api(`/api/boxes/${name}/home`, { method: "POST", body: "{}" }).catch(() => void 0);
  const sha = createHash("sha256").update(buf).digest("hex");
  const post = (body) => api(`/api/boxes/${name}/home`, { method: "POST", body: JSON.stringify(body) });
  if ((await post({ sha }).catch(() => ({ need: false }))).need) await post({ sha, tgz: buf.toString("base64") }).catch(() => void 0);
}
async function dial({ user, hosts }, privateKey, attempts) {
  const once = () => new Promise((resolve, reject) => {
    let left = hosts.length, won = false;
    if (!left) return reject(new Error("The box has no address yet."));
    for (const [host, port] of hosts) {
      const c = new ssh22.Client();
      c.on("ready", () => {
        if (won) return c.end();
        won = true;
        resolve(c);
      }).on("error", (e) => {
        if (!won && --left === 0) reject(e);
      }).connect({ host, port, username: user, privateKey, readyTimeout: 6e3, keepaliveInterval: 15e3, hostVerifier: () => true });
    }
  });
  for (let attempt = 1; ; attempt++) {
    try {
      return await once();
    } catch (err) {
      if (attempt >= attempts) throw err;
      await new Promise((r) => setTimeout(r, 1500));
    }
  }
}
async function attach(name, ready) {
  if (!name) {
    const boxes = await api("/api/boxes");
    if (!boxes.length) return console.log(dim("No boxes yet. Run box."));
    name = await select3({ message: "Attach to", choices: boxes.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}`, value: b.name })) });
  }
  const boxName = name;
  const key = laptopKey();
  const keys = [];
  let session, conn, echo, typed = false, pressed = () => {
  };
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
  process.stdin.resume();
  process.stdin.on("data", (d) => {
    if (d.includes(29) || !session && d.includes(3)) return leave();
    typed = true;
    if (session) {
      echo?.fromKeys(d);
      session.write(d);
    } else {
      keys.push(d);
      pressed();
    }
  });
  setInterval(() => {
    if (session) void api(`/api/boxes/${boxName}/heartbeat`, { method: "POST", body: JSON.stringify({ typed }) }).catch(() => void 0);
    typed = false;
  }, 3e4).unref();
  function leave() {
    conn?.end();
    process.stdout.write(TERMINAL_RESET);
    if (process.stdin.isTTY) process.stdin.setRawMode(false);
    console.log(dim(`
Detached. ${boxName} sleeps once Claude is done. box ${boxName} to return.`));
    process.exit(0);
  }
  let ssh = ready?.ssh, attempts = 4, announced = false;
  for (; ; ) {
    if (!ssh) {
      attempts = 4;
      const box = await findBox(boxName);
      if (box.state === "gone") fail(`${boxName} is gone. Make a new one with box.`);
      if (box.state === "paused" || box.state === "pausing") {
        sleepScreen(box);
        if (!keys.length) await new Promise((r) => {
          pressed = r;
        });
        wakingScreen(box, keys);
        pressed = () => wakingScreen(box, keys);
      }
      const stop = keys.length ? () => void 0 : progress([[0, `Opening ${boxName}`], [1200, "Waking the box up"], [4e3, "Starting Claude"]]);
      ssh = (await api(`/api/boxes/${boxName}/attach`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(key.publicKey) }) }).finally(() => stop())).ssh;
    }
    conn = await dial(ssh, key.privateKey, attempts).catch(() => void 0);
    attempts = 1;
    if (conn && !announced && !keys.length) console.log(dim(`Attaching to ${boxName}. Detach with ctrl-], come back with box ${boxName}.`));
    announced = true;
    if (!conn) {
      ssh = void 0;
      for (let i = 0; i < 20 && (await findBox(boxName)).state === "running"; i++) await new Promise((r) => setTimeout(r, 1500));
      continue;
    }
    const { columns: cols = 100, rows = 30 } = process.stdout;
    echo = process.env.CBOX_PREDICT === "0" ? void 0 : predictor((s) => process.stdout.write(s), cols, rows);
    const stream = await new Promise((resolve, reject) => conn.exec(
      "dtach -A ~/.cb/claude.sock -E -r winch bash -l ~/.cb/start.sh",
      { pty: { term: process.env.TERM || "xterm-256color", cols, rows } },
      (err, s) => err ? reject(err) : resolve(s)
    ));
    let exited = false;
    stream.on("exit", (code2, signal) => {
      exited = code2 === 0 && !signal;
    });
    const closed = new Promise((r) => stream.on("close", r));
    let settle;
    stream.on("data", (d) => {
      echo ? echo.fromBox(d) : process.stdout.write(d);
      if (!keys.length) return;
      clearTimeout(settle);
      settle = setTimeout(() => {
        session?.write(Buffer.concat(keys.splice(0)));
      }, 800);
    });
    stream.setWindow(rows, cols - 1, 0, 0);
    setTimeout(() => stream.setWindow(rows, cols, 0, 0), 100);
    const resize = () => {
      echo?.resize(process.stdout.columns, process.stdout.rows);
      stream.setWindow(process.stdout.rows, process.stdout.columns, 0, 0);
    };
    process.stdout.on("resize", resize);
    session = stream;
    typed = false;
    void api(`/api/boxes/${boxName}/heartbeat`, { method: "POST", body: JSON.stringify({ typed: false }) }).catch(() => void 0);
    await closed;
    session = void 0;
    process.stdout.off("resize", resize);
    if (exited) leave();
    conn.end();
    process.stdout.write(TERMINAL_RESET);
  }
}
var RATES = { small: 0.018, medium: 0.036, large: 0.072 };
function centered(lines) {
  const { columns = 100, rows = 30 } = process.stdout;
  const visible = (t) => t.replace(/\x1b\[[0-9;]*m/g, "").length;
  const top = Math.max(0, Math.floor((rows - lines.length) / 2));
  process.stdout.write("\x1B[?1049h\x1B[2J\x1B[?25l" + lines.map((l, i) => `\x1B[${top + i + 1};${Math.max(1, Math.floor((columns - visible(l)) / 2) + 1)}H${l}`).join(""));
}
function sleepScreen(box) {
  const since = box.asleepSince ? Date.now() - new Date(box.asleepSince).getTime() : 0;
  const saved = since / 36e5 * (RATES[box.size ?? "small"] ?? RATES.small);
  centered([
    styleText4("blue", "\u263E"),
    "",
    styleText4("bold", `${box.name} is asleep`),
    dim(`Last used ${span(Date.now() - new Date(box.lastActiveAt ?? Date.now()).getTime())} ago. Your files and the conversation are saved.`),
    "",
    `Press any key to wake it  ${dim("\xB7")}  ctrl+] to leave`,
    "",
    dim(since ? `asleep ${span(since)} \xB7 $${saved.toFixed(2)} not spent` : "asleep")
  ]);
}
function wakingScreen(box, keys) {
  const text = Buffer.concat(keys).toString("utf8").replace(/[\x00-\x1f\x7f]/g, "");
  centered([
    styleText4("yellow", "\u2600"),
    "",
    styleText4("bold", `Waking ${box.name}`),
    dim("a few seconds"),
    "",
    text ? `${dim("you typed:")} ${text}` : "",
    dim("what you type now goes to Claude once it is up")
  ]);
}
var TERMINAL_RESET = "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?1004l\x1B[?2004l\x1B[<u\x1B[?1049l\x1B[?25h\x1B[0m";

// src/update.ts
import { execFileSync as execFileSync4, spawn as spawn4, spawnSync as spawnSync2 } from "node:child_process";
import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
import { basename as basename2, dirname as dirname2, join as join6 } from "node:path";
import { fileURLToPath } from "node:url";
var PACKAGE = "@abuaboud/box";
var LATEST = `https://registry.npmjs.org/${PACKAGE.replace("/", "%2f")}/latest`;
var INSTALLED = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
var VERSION = INSTALLED.version;
function npmPrefix() {
  const pkgDir = dirname2(dirname2(fileURLToPath(import.meta.url)));
  const modules = basename2(dirname2(pkgDir)).startsWith("@") ? dirname2(dirname2(pkgDir)) : dirname2(pkgDir);
  if (basename2(modules) !== "node_modules") return void 0;
  return basename2(dirname2(modules)) === "lib" ? dirname2(dirname2(modules)) : dirname2(modules);
}
var newer = (a, b) => {
  const x = a.split(".").map(Number), y = b.split(".").map(Number);
  for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
  return false;
};
var due = (latest) => INSTALLED.name !== PACKAGE || newer(latest, VERSION);
var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
function installSteps(version, prefix) {
  const at = prefix ? ["--prefix", prefix] : [];
  const box = `${PACKAGE}@${version}`;
  return INSTALLED.name === PACKAGE ? [["i", "-g", "--prefer-online", ...at, box]] : [["i", "-g", "--prefer-online", "--force", ...at, box], ["rm", "-g", ...at, INSTALLED.name], ["i", "-g", "--prefer-offline", "--force", ...at, box]];
}
async function updateFirst() {
  const prefix = npmPrefix();
  if (!prefix || process.env.CBOX_NO_UPDATE || process.env.CBOX_UPDATED) return;
  const latest = await fetch(LATEST, { signal: AbortSignal.timeout(2500) }).then((r) => r.json()).then((j) => j.version, () => void 0);
  if (!latest || !due(latest)) return;
  const stop = progress([[0, `Updating box ${VERSION} \u2192 ${latest}`]]);
  const done = installSteps(latest, prefix).every((args) => spawnSync2(NPM, args, { stdio: "ignore" }).status === 0);
  stop();
  if (!done) return miss(`Couldn't update box to ${latest}, so carrying on with ${VERSION}`);
  ok(`Updated box ${dim(VERSION)} \u2192 ${latest}`);
  try {
    writeFileSync5(join6(CONFIG_DIR, "update.json"), JSON.stringify({ version: latest, checkedAt: Date.now() }));
  } catch {
  }
  const again = spawnSync2(process.execPath, process.argv.slice(1), { stdio: "inherit", env: { ...process.env, CBOX_UPDATED: "1" } });
  process.exit(again.status ?? 0);
}
function autoUpdate() {
  const prefix = npmPrefix();
  if (process.env.CBOX_NO_UPDATE || process.env.CBOX_UPDATED || !prefix) return;
  const file2 = join6(CONFIG_DIR, "update.json");
  let state = {};
  try {
    state = JSON.parse(readFileSync6(file2, "utf8"));
  } catch {
  }
  if (state.version && state.version !== VERSION) ok(`Updated box ${dim(state.version)} \u2192 ${VERSION}`);
  const check = INSTALLED.name !== PACKAGE || !state.checkedAt || Date.now() - state.checkedAt > 12 * 36e5;
  try {
    mkdirSync5(CONFIG_DIR, { recursive: true });
    writeFileSync5(file2, JSON.stringify({ version: VERSION, checkedAt: check ? Date.now() : state.checkedAt }));
  } catch {
    return;
  }
  if (!check) return;
  const job = `fetch(${JSON.stringify(LATEST)}).then(r => r.json()).then(({ version }) => {
    const newer = (a, b) => { const x = a.split('.').map(Number), y = b.split('.').map(Number); for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0); return false }
    if (${JSON.stringify(INSTALLED.name === PACKAGE)} && !newer(version, ${JSON.stringify(VERSION)})) return
    for (const args of ${JSON.stringify(installSteps("{v}", prefix))}) require('node:child_process').execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args.map((a) => a.replace('{v}', version)), { stdio: 'ignore' })
  }).catch(() => {})`;
  spawn4(process.execPath, ["-e", job], { detached: true, stdio: "ignore" }).on("error", () => {
  }).unref();
}
async function upgrade() {
  const latest = await fetch(LATEST).then((r) => r.json()).then((j) => j.version).catch(() => fail("Could not reach the npm registry."));
  if (!due(latest)) return ok(`box ${VERSION} is the latest`);
  console.log(dim(`Updating box ${VERSION} \u2192 ${latest}\u2026`));
  try {
    for (const args of installSteps(latest, npmPrefix())) execFileSync4(NPM, args, { stdio: "inherit" });
  } catch {
    fail(`npm could not update box. Try: npm i -g ${PACKAGE}@latest`);
  }
  ok(`Updated box to ${latest}`);
}

// src/index.ts
var ANIMALS = ["otter", "heron", "lynx", "moth", "yak", "orca", "wren", "newt", "ibis", "mole", "kiwi", "crab", "swan", "toad", "hare", "seal"];
var NAME = /^[a-z0-9][a-z0-9-]{0,30}$/;
async function newBox(o = {}) {
  await updateFirst();
  if (o.size && !["small", "medium", "large"].includes(o.size)) fail("Pick --size small, medium or large.");
  if (o.name !== void 0 && !NAME.test(o.name)) fail("Name the box with lowercase letters, digits and dashes, like webhook-retry.");
  const here = gitRepo();
  const picked = await chooseEnvironment(o.env, here);
  const branch = here && picked.repo.toLowerCase() === here.repo.toLowerCase() ? here.branch : void 0;
  const name = o.name ?? await askName(branch);
  return o.remote || o.agent === "codex" ? newRemoteBox(picked.name, branch, name, o) : newLocalBox(picked.name, branch, name, o);
}
async function askName(branch) {
  const fromBranch = branch?.split("/").at(-1)?.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30);
  const suggestion = fromBranch || `${ANIMALS[Math.floor(Math.random() * ANIMALS.length)]}-${Math.random().toString(16).slice(2, 5)}`;
  if (!process.stdin.isTTY) return void 0;
  const picked = await input2({
    message: `Name this box ${dim("(optional \xB7 you will see it in its folder, ssh name and the dashboard)")}`,
    default: suggestion,
    validate: (v) => !v.trim() || NAME.test(v.trim()) || "Lowercase letters, digits and dashes"
  });
  return picked.trim() || suggestion;
}
async function home(name, o) {
  const making = o.env || o.remote || o.agent || o.size || o.alwaysOn;
  if (name && making) fail(`--env, --remote, --agent, --size and --always-on only shape a new box. Open ${name} with box ${name}, or make a new one with box new --name <name> and the options.`);
  if (name) return openBox(name);
  const email = load().email;
  const all = await api("/api/boxes");
  await prune(all);
  const mine = all.filter((b) => b.state !== "gone" && (!email || b.owner === email));
  if (!mine.length || !process.stdin.isTTY || making) return newBox(o);
  const pick = await select4({ message: "Open a box", choices: [
    ...mine.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}${dim(b.agent === "local" ? "" : " \xB7 remote")}`, value: b.name })),
    { name: styleText5("cyan", "+ new box"), value: "" }
  ] });
  return pick ? openBox(pick) : newBox(o);
}
async function openBox(name) {
  const box = await findBox(name);
  if (box.state === "gone") fail(`${name} is gone. Make a new one with box new.`);
  return box.agent === "local" ? openLocal({ name: box.name, env: box.env ?? "boxes", here: localCopy(), branch: box.branch ?? void 0 }) : attach(box.name);
}
async function ls() {
  const email = load().email;
  const all = await api("/api/boxes");
  await prune(all);
  const mine = all.filter((b) => !email || b.owner === email);
  if (!mine.length) return console.log(dim("No boxes of yours. Run box."));
  const width = Math.max(...mine.map((b) => b.name.length)) + 2;
  console.log(dim(`${"BOX".padEnd(width)}${"STATE".padEnd(20)}WHERE`));
  for (const b of mine) {
    const l = label(b);
    console.log(`${b.name.padEnd(width)}${l}${" ".repeat(Math.max(1, 20 - l.replace(/\x1b\[[0-9;]*m/g, "").length))}${dim(b.agent === "local" ? "laptop" : "remote")}`);
  }
}
var quiet = ["proxy", "sync"].includes(process.argv[2] ?? "");
if (quiet) console.log = console.error;
if (!quiet && !process.env.CBOX_UPDATED && !process.argv.slice(2).some((a) => ["-V", "--version", "-h", "--help", "help"].includes(a))) console.log(dim(`box ${VERSION}`));
if (!quiet) {
  moveHome();
  autoUpdate();
  ensureSshConfig();
}
var boxOptions = (c) => c.option("-e, --env <name>", "environment").option("-n, --name <name>", "name the box, like webhook-retry (default: asked, or generated)").option("--remote", "run Claude on the machine instead of this laptop").option("--agent <name>", "claude (default) or codex; codex implies --remote").option("--size <size>", "small, medium or large (default: the environment's)").option("--always-on", "never sleep, for example for a demo");
var cli = boxOptions(new Command("box").enablePositionalOptions().description("Dev machines for Claude: Claude on your laptop, the work on a box").version(VERSION, "-v, --version").argument("[box]", "open this box")).action((name, o) => home(name, o));
boxOptions(cli.command("new").description("always make a fresh box")).action((o) => newBox(o));
cli.command("ls").description("list your boxes").action(ls);
cli.command("code [env]").description("open an environment's boxes in one VS Code window, each a git worktree, and sync your saves").action((env) => code(env));
cli.command("rm <box>").option("-f, --force", "delete even while someone is attached or Claude is working").description("delete a box, pushing unpushed work to cb/<box>").action(rm);
cli.command("login").option("--server <url>", "box server").description("sign in from the browser").action((o) => login(o.server));
cli.command("upgrade").description("update box to the latest version now").action(upgrade);
cli.command("attach [box]", { hidden: true }).action((name) => name ? openBox(name) : home(void 0, {}));
cli.command("proxy <host>", { hidden: true }).action(proxy);
cli.command("sync <box> <direction> <folder>", { hidden: true }).action(sync);
await cli.parseAsync();