@madarco/agentbox 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{chunk-3NCUES35.js → chunk-6VTAPD4H.js} +123 -112
  2. package/dist/chunk-6VTAPD4H.js.map +1 -0
  3. package/dist/{chunk-J35IH7W5.js → chunk-7J5AJLWG.js} +61 -23
  4. package/dist/chunk-7J5AJLWG.js.map +1 -0
  5. package/dist/{chunk-3JKQNOXP.js → chunk-FJNIFTWK.js} +66 -65
  6. package/dist/chunk-FJNIFTWK.js.map +1 -0
  7. package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
  8. package/dist/chunk-HPZMD5DE.js.map +1 -0
  9. package/dist/{chunk-MOC54XL6.js → chunk-PXUBE5KS.js} +376 -245
  10. package/dist/chunk-PXUBE5KS.js.map +1 -0
  11. package/dist/{chunk-SOMIKEN2.js → chunk-RFC5F5HR.js} +272 -214
  12. package/dist/chunk-RFC5F5HR.js.map +1 -0
  13. package/dist/create-AHZ3GVEZ-TGEDL7UX.js +15 -0
  14. package/dist/index.js +2760 -1857
  15. package/dist/index.js.map +1 -1
  16. package/dist/{lifecycle-YTMZYKOE-TD5S5FTS.js → lifecycle-LFOL6YFM-TCHDX3J5.js} +5 -5
  17. package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
  18. package/dist/stats-Z4BVJODD-HEC4TMUZ.js +19 -0
  19. package/package.json +3 -2
  20. package/runtime/docker/Dockerfile.box +53 -20
  21. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +39 -50
  22. package/runtime/docker/packages/ctl/dist/bin.cjs +219 -148
  23. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +42 -0
  24. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +26 -15
  25. package/runtime/relay/bin.cjs +288 -12
  26. package/share/agentbox-setup/SKILL.md +39 -50
  27. package/dist/chunk-3JKQNOXP.js.map +0 -1
  28. package/dist/chunk-3NCUES35.js.map +0 -1
  29. package/dist/chunk-IDR4HVIC.js.map +0 -1
  30. package/dist/chunk-J35IH7W5.js.map +0 -1
  31. package/dist/chunk-MOC54XL6.js.map +0 -1
  32. package/dist/chunk-SOMIKEN2.js.map +0 -1
  33. package/dist/create-SE6H4B5U-IWAZHJHV.js +0 -15
  34. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
  35. /package/dist/{create-SE6H4B5U-IWAZHJHV.js.map → create-AHZ3GVEZ-TGEDL7UX.js.map} +0 -0
  36. /package/dist/{lifecycle-YTMZYKOE-TD5S5FTS.js.map → lifecycle-LFOL6YFM-TCHDX3J5.js.map} +0 -0
  37. /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
  38. /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-Z4BVJODD-HEC4TMUZ.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../packages/sandbox-docker/src/stats.ts"],"sourcesContent":["import { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { execa } from 'execa';\nimport type { BoxResourceLimits, BoxResourceStats } from '@agentbox/core';\nimport { CHECKPOINT_IMAGE_PREFIX, checkpointImageTag } from './checkpoint.js';\nimport {\n inspectContainer,\n inspectContainerStatus,\n inspectVolumeMountpoint,\n} from './docker.js';\nimport { detectEngine } from './host-export.js';\nimport type { BoxRecord } from './state.js';\n\n/**\n * Parse one of Docker's human-formatted size tokens (`512MiB`, `1.2kB`,\n * `3.4GB`, `0B`, `--`). Returns null when unparseable. Docker mixes binary\n * (`KiB/MiB/GiB`) and decimal (`kB/MB/GB`) suffixes depending on the column, so\n * we handle both.\n */\nexport function parseDockerSize(raw: string): number | null {\n const s = raw.trim();\n if (!s || s === '--' || s === 'N/A') return null;\n const m = /^([\\d.]+)\\s*([A-Za-z]*)$/.exec(s);\n if (!m) return null;\n const n = Number(m[1]);\n if (!Number.isFinite(n)) return null;\n const unit = (m[2] ?? '').toLowerCase();\n const mult: Record<string, number> = {\n '': 1,\n b: 1,\n kb: 1e3,\n mb: 1e6,\n gb: 1e9,\n tb: 1e12,\n kib: 1024,\n mib: 1024 ** 2,\n gib: 1024 ** 3,\n tib: 1024 ** 4,\n };\n const factor = mult[unit];\n return factor === undefined ? null : n * factor;\n}\n\nfunction parsePercent(raw: string | undefined): number | null {\n if (!raw) return null;\n const n = Number(raw.replace('%', '').trim());\n return Number.isFinite(n) ? n : null;\n}\n\n/** Split Docker's \"<a> / <b>\" pair columns (MemUsage, NetIO, BlockIO). */\nfunction splitPair(raw: string | undefined): [string, string] | null {\n if (!raw) return null;\n const parts = raw.split('/');\n if (parts.length !== 2) return null;\n return [parts[0]!.trim(), parts[1]!.trim()];\n}\n\nasync function duBytes(path: string): Promise<number | null> {\n const result = await execa('du', ['-sk', path], { reject: false });\n if (result.exitCode !== 0) return null;\n const kb = Number.parseInt((result.stdout ?? '').split(/\\s+/)[0] ?? '', 10);\n return Number.isNaN(kb) ? null : kb * 1024;\n}\n\n/**\n * Best-effort on-host byte size of a Docker named volume. Fastest path first:\n * 1. OrbStack exposes volumes live at ~/OrbStack/docker/volumes/<name>/.\n * 2. `docker system df -v` (cheap-walked once; may report \"N/A\").\n * 3. The reported mountpoint, only when host-readable (Linux native Docker).\n * Returns null when no path is reachable from the host (the macOS VM case\n * where `system df` also has no number).\n */\nexport async function volumeSizeBytes(name: string): Promise<number | null> {\n if (!name) return null;\n const engine = await detectEngine();\n if (engine === 'orbstack') {\n const live = join(homedir(), 'OrbStack', 'docker', 'volumes', name);\n const sz = await duBytes(live);\n if (sz !== null) return sz;\n }\n const df = await execa(\n 'docker',\n ['system', 'df', '-v', '--format', '{{json .Volumes}}'],\n { reject: false },\n );\n if (df.exitCode === 0) {\n try {\n const vols = JSON.parse(df.stdout || '[]') as Array<{ Name?: string; Size?: string }>;\n const hit = vols.find((v) => v.Name === name);\n const parsed = hit?.Size ? parseDockerSize(hit.Size) : null;\n if (parsed !== null) return parsed;\n } catch {\n // fall through to mountpoint\n }\n }\n const mp = await inspectVolumeMountpoint(name);\n if (mp && !mp.startsWith('/var/lib/docker')) {\n return duBytes(mp);\n }\n return null;\n}\n\n/**\n * On-host byte size of a Docker image (sum of its own layer sizes — what\n * `docker images` reports). Null on docker errors.\n */\nasync function imageBytes(tag: string): Promise<number | null> {\n const r = await execa('docker', ['image', 'inspect', tag, '--format', '{{.Size}}'], {\n reject: false,\n });\n if (r.exitCode !== 0) return null;\n const n = Number.parseInt((r.stdout ?? '').trim(), 10);\n return Number.isFinite(n) ? n : null;\n}\n\n/**\n * Size of a project's most-recent checkpoint image (the head of the lineage,\n * resolved via the `checkpointImageTag` helper from a checkpoint *name*). The\n * caller passes the name because we don't enumerate manifests from this\n * module — that lives in checkpoint.ts. Null when the image isn't present.\n */\nexport async function projectCheckpointImageBytes(\n projectRoot: string,\n name: string,\n): Promise<number | null> {\n return imageBytes(checkpointImageTag(projectRoot, name));\n}\n\n/**\n * Total on-host bytes of every checkpoint image (the durable, cross-box\n * warm-state assets). Walks every image tag under `CHECKPOINT_IMAGE_PREFIX`.\n * Null when none exist.\n */\nexport async function allCheckpointImagesBytes(): Promise<number | null> {\n const r = await execa(\n 'docker',\n [\n 'image',\n 'ls',\n '--format',\n '{{.Repository}}:{{.Tag}}\\t{{.Size}}',\n `${CHECKPOINT_IMAGE_PREFIX}*`,\n ],\n { reject: false },\n );\n if (r.exitCode !== 0) return null;\n const lines = (r.stdout ?? '')\n .split('\\n')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n if (lines.length === 0) return null;\n let total = 0;\n let any = false;\n for (const line of lines) {\n const [, size] = line.split('\\t');\n const n = size ? parseDockerSize(size) : null;\n if (n !== null) {\n total += n;\n any = true;\n }\n }\n return any ? total : null;\n}\n\n/** On-host byte size of the whole ~/.agentbox state/runtime directory. */\nexport async function agentboxHomeBytes(): Promise<number | null> {\n return duBytes(join(homedir(), '.agentbox'));\n}\n\nfunction limitsFromRecord(record: BoxRecord): BoxResourceLimits {\n const r = record.resourceLimits;\n return {\n memoryBytes: r?.memoryBytes && r.memoryBytes > 0 ? r.memoryBytes : null,\n cpus: r?.cpus && r.cpus > 0 ? r.cpus : null,\n pidsLimit: r?.pidsLimit && r.pidsLimit > 0 ? r.pidsLimit : null,\n disk: r?.disk || null,\n };\n}\n\n/**\n * Cross-check persisted limits against the live container's HostConfig so an\n * externally `docker update`d box still reports the truth. The persisted\n * record stays the fallback when the container is gone.\n */\nfunction reconcileLimits(persisted: BoxResourceLimits, dockerJson: unknown): BoxResourceLimits {\n const hc = (dockerJson as { HostConfig?: Record<string, unknown> } | null)?.HostConfig;\n if (!hc) return persisted;\n const mem = typeof hc.Memory === 'number' && hc.Memory > 0 ? hc.Memory : null;\n const nano = typeof hc.NanoCpus === 'number' && hc.NanoCpus > 0 ? hc.NanoCpus : null;\n const pids = typeof hc.PidsLimit === 'number' && hc.PidsLimit > 0 ? hc.PidsLimit : null;\n return {\n memoryBytes: mem ?? persisted.memoryBytes,\n cpus: nano ? nano / 1e9 : persisted.cpus,\n pidsLimit: pids ?? persisted.pidsLimit,\n disk: persisted.disk,\n };\n}\n\ninterface DockerStatsLine {\n CPUPerc?: string;\n MemUsage?: string;\n MemPerc?: string;\n PIDs?: string;\n NetIO?: string;\n BlockIO?: string;\n}\n\n/**\n * Container writable-layer size from `docker ps --size`. With the overlay\n * gone, `/workspace` lives here (not in a named volume), so this is the\n * box's primary writable-surface number.\n */\nasync function containerWritableBytes(container: string): Promise<number | null> {\n const r = await execa(\n 'docker',\n ['ps', '-a', '--filter', `name=^${container}$`, '--format', '{{.Size}}', '--size'],\n { reject: false },\n );\n if (r.exitCode !== 0) return null;\n // `--size` produces `<rw> (virtual <total>)`; we want the first number.\n const first = (r.stdout ?? '').split('\\n')[0]?.trim();\n if (!first) return null;\n const m = /^([^()]+?)(?:\\s*\\(.*\\))?$/.exec(first);\n const sz = m ? m[1]!.trim() : first;\n return parseDockerSize(sz);\n}\n\n/**\n * Provider-agnostic resource snapshot for a box. CPU/mem/pids/IO come from\n * `docker stats --no-stream` (point-in-time sample; only when the container is\n * running). Disk is the container's writable layer (where `/workspace` lives\n * now that the overlay is gone) plus the in-box dockerd's data-root volume;\n * the per-box host snapshot dir and the checkpoint image lineage are\n * reported on their own fields.\n */\nexport async function boxResourceStats(record: BoxRecord): Promise<BoxResourceStats> {\n const warnings: string[] = [];\n const dockerJson = await inspectContainer(record.container);\n const limits = reconcileLimits(limitsFromRecord(record), dockerJson);\n\n const [diskContainer, diskDocker, snapshotDiskBytes, checkpointImageBytesValue] =\n await Promise.all([\n containerWritableBytes(record.container),\n record.dockerVolume ? volumeSizeBytes(record.dockerVolume) : Promise.resolve(null),\n record.snapshotDir ? duBytes(record.snapshotDir) : Promise.resolve(null),\n record.checkpointImage ? imageBytes(record.checkpointImage) : Promise.resolve(null),\n ]);\n const diskUsedBytes =\n diskContainer === null && diskDocker === null\n ? null\n : (diskContainer ?? 0) + (diskDocker ?? 0);\n if (diskUsedBytes === null) {\n warnings.push('disk usage unavailable on this engine');\n }\n\n const base: BoxResourceStats = {\n source: 'docker',\n live: false,\n cpuPercent: null,\n memUsedBytes: null,\n memLimitBytes: limits.memoryBytes,\n memPercent: null,\n pids: null,\n diskUsedBytes,\n snapshotDiskBytes,\n checkpointVolumeBytes: checkpointImageBytesValue,\n netRxBytes: null,\n netTxBytes: null,\n blockReadBytes: null,\n blockWriteBytes: null,\n limits,\n warnings,\n };\n\n if ((await inspectContainerStatus(record.container)) !== 'running') {\n return base;\n }\n\n const proc = await execa(\n 'docker',\n ['stats', '--no-stream', '--format', '{{json .}}', record.container],\n { reject: false },\n );\n if (proc.exitCode !== 0 || !proc.stdout.trim()) {\n return base;\n }\n let line: DockerStatsLine;\n try {\n line = JSON.parse(proc.stdout.trim().split('\\n')[0]!) as DockerStatsLine;\n } catch {\n return base;\n }\n\n const memPair = splitPair(line.MemUsage);\n const memUsedBytes = memPair ? parseDockerSize(memPair[0]) : null;\n const memEngineTotal = memPair ? parseDockerSize(memPair[1]) : null;\n const netPair = splitPair(line.NetIO);\n const blkPair = splitPair(line.BlockIO);\n\n return {\n ...base,\n live: true,\n cpuPercent: parsePercent(line.CPUPerc),\n memUsedBytes,\n // The applied limit when set; otherwise docker stats' own denominator\n // (the engine/host total).\n memLimitBytes: limits.memoryBytes ?? memEngineTotal,\n memPercent: parsePercent(line.MemPerc),\n pids: line.PIDs ? Number.parseInt(line.PIDs, 10) || null : null,\n netRxBytes: netPair ? parseDockerSize(netPair[0]) : null,\n netTxBytes: netPair ? parseDockerSize(netPair[1]) : null,\n blockReadBytes: blkPair ? parseDockerSize(blkPair[0]) : null,\n blockWriteBytes: blkPair ? parseDockerSize(blkPair[1]) : null,\n };\n}\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,aAAa;AAiBf,SAAS,gBAAgB,KAA4B;AAC1D,QAAM,IAAI,IAAI,KAAK;AACnB,MAAI,CAAC,KAAK,MAAM,QAAQ,MAAM,MAAO,QAAO;AAC5C,QAAM,IAAI,2BAA2B,KAAK,CAAC;AAC3C,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,IAAI,OAAO,EAAE,CAAC,CAAC;AACrB,MAAI,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AAChC,QAAM,QAAQ,EAAE,CAAC,KAAK,IAAI,YAAY;AACtC,QAAM,OAA+B;IACnC,IAAI;IACJ,GAAG;IACH,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,KAAK;IACL,KAAK,QAAQ;IACb,KAAK,QAAQ;IACb,KAAK,QAAQ;EACf;AACA,QAAM,SAAS,KAAK,IAAI;AACxB,SAAO,WAAW,SAAY,OAAO,IAAI;AAC3C;AAEA,SAAS,aAAa,KAAwC;AAC5D,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,IAAI,OAAO,IAAI,QAAQ,KAAK,EAAE,EAAE,KAAK,CAAC;AAC5C,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAGA,SAAS,UAAU,KAAkD;AACnE,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO,CAAC,MAAM,CAAC,EAAG,KAAK,GAAG,MAAM,CAAC,EAAG,KAAK,CAAC;AAC5C;AAEA,eAAe,QAAQ,MAAsC;AAC3D,QAAM,SAAS,MAAM,MAAM,MAAM,CAAC,OAAO,IAAI,GAAG,EAAE,QAAQ,MAAM,CAAC;AACjE,MAAI,OAAO,aAAa,EAAG,QAAO;AAClC,QAAM,KAAK,OAAO,UAAU,OAAO,UAAU,IAAI,MAAM,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE;AAC1E,SAAO,OAAO,MAAM,EAAE,IAAI,OAAO,KAAK;AACxC;AAUA,eAAsB,gBAAgB,MAAsC;AAC1E,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,SAAS,MAAM,aAAa;AAClC,MAAI,WAAW,YAAY;AACzB,UAAM,OAAO,KAAK,QAAQ,GAAG,YAAY,UAAU,WAAW,IAAI;AAClE,UAAM,KAAK,MAAM,QAAQ,IAAI;AAC7B,QAAI,OAAO,KAAM,QAAO;EAC1B;AACA,QAAM,KAAK,MAAM;IACf;IACA,CAAC,UAAU,MAAM,MAAM,YAAY,mBAAmB;IACtD,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,GAAG,aAAa,GAAG;AACrB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,GAAG,UAAU,IAAI;AACzC,YAAM,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AAC5C,YAAM,SAAS,KAAK,OAAO,gBAAgB,IAAI,IAAI,IAAI;AACvD,UAAI,WAAW,KAAM,QAAO;IAC9B,QAAQ;IAER;EACF;AACA,QAAM,KAAK,MAAM,wBAAwB,IAAI;AAC7C,MAAI,MAAM,CAAC,GAAG,WAAW,iBAAiB,GAAG;AAC3C,WAAO,QAAQ,EAAE;EACnB;AACA,SAAO;AACT;AAMA,eAAe,WAAW,KAAqC;AAC7D,QAAM,IAAI,MAAM,MAAM,UAAU,CAAC,SAAS,WAAW,KAAK,YAAY,WAAW,GAAG;IAClF,QAAQ;EACV,CAAC;AACD,MAAI,EAAE,aAAa,EAAG,QAAO;AAC7B,QAAM,IAAI,OAAO,UAAU,EAAE,UAAU,IAAI,KAAK,GAAG,EAAE;AACrD,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAQA,eAAsB,4BACpB,aACA,MACwB;AACxB,SAAO,WAAW,mBAAmB,aAAa,IAAI,CAAC;AACzD;AAOA,eAAsB,2BAAmD;AACvE,QAAM,IAAI,MAAM;IACd;IACA;MACE;MACA;MACA;MACA;MACA,GAAG,uBAAuB;IAC5B;IACA,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,EAAE,aAAa,EAAG,QAAO;AAC7B,QAAM,SAAS,EAAE,UAAU,IACxB,MAAM,IAAI,EACV,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC7B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,MAAI,QAAQ;AACZ,MAAI,MAAM;AACV,aAAW,QAAQ,OAAO;AACxB,UAAM,CAAC,EAAE,IAAI,IAAI,KAAK,MAAM,GAAI;AAChC,UAAM,IAAI,OAAO,gBAAgB,IAAI,IAAI;AACzC,QAAI,MAAM,MAAM;AACd,eAAS;AACT,YAAM;IACR;EACF;AACA,SAAO,MAAM,QAAQ;AACvB;AAGA,eAAsB,oBAA4C;AAChE,SAAO,QAAQ,KAAK,QAAQ,GAAG,WAAW,CAAC;AAC7C;AAEA,SAAS,iBAAiB,QAAsC;AAC9D,QAAM,IAAI,OAAO;AACjB,SAAO;IACL,aAAa,GAAG,eAAe,EAAE,cAAc,IAAI,EAAE,cAAc;IACnE,MAAM,GAAG,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO;IACvC,WAAW,GAAG,aAAa,EAAE,YAAY,IAAI,EAAE,YAAY;IAC3D,MAAM,GAAG,QAAQ;EACnB;AACF;AAOA,SAAS,gBAAgB,WAA8B,YAAwC;AAC7F,QAAM,KAAM,YAAgE;AAC5E,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,MAAM,OAAO,GAAG,WAAW,YAAY,GAAG,SAAS,IAAI,GAAG,SAAS;AACzE,QAAM,OAAO,OAAO,GAAG,aAAa,YAAY,GAAG,WAAW,IAAI,GAAG,WAAW;AAChF,QAAM,OAAO,OAAO,GAAG,cAAc,YAAY,GAAG,YAAY,IAAI,GAAG,YAAY;AACnF,SAAO;IACL,aAAa,OAAO,UAAU;IAC9B,MAAM,OAAO,OAAO,MAAM,UAAU;IACpC,WAAW,QAAQ,UAAU;IAC7B,MAAM,UAAU;EAClB;AACF;AAgBA,eAAe,uBAAuB,WAA2C;AAC/E,QAAM,IAAI,MAAM;IACd;IACA,CAAC,MAAM,MAAM,YAAY,SAAS,SAAS,KAAK,YAAY,aAAa,QAAQ;IACjF,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,EAAE,aAAa,EAAG,QAAO;AAE7B,QAAM,SAAS,EAAE,UAAU,IAAI,MAAM,IAAI,EAAE,CAAC,GAAG,KAAK;AACpD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,IAAI,4BAA4B,KAAK,KAAK;AAChD,QAAM,KAAK,IAAI,EAAE,CAAC,EAAG,KAAK,IAAI;AAC9B,SAAO,gBAAgB,EAAE;AAC3B;AAUA,eAAsB,iBAAiB,QAA8C;AACnF,QAAM,WAAqB,CAAC;AAC5B,QAAM,aAAa,MAAM,iBAAiB,OAAO,SAAS;AAC1D,QAAM,SAAS,gBAAgB,iBAAiB,MAAM,GAAG,UAAU;AAEnE,QAAM,CAAC,eAAe,YAAY,mBAAmB,yBAAyB,IAC5E,MAAM,QAAQ,IAAI;IAChB,uBAAuB,OAAO,SAAS;IACvC,OAAO,eAAe,gBAAgB,OAAO,YAAY,IAAI,QAAQ,QAAQ,IAAI;IACjF,OAAO,cAAc,QAAQ,OAAO,WAAW,IAAI,QAAQ,QAAQ,IAAI;IACvE,OAAO,kBAAkB,WAAW,OAAO,eAAe,IAAI,QAAQ,QAAQ,IAAI;EACpF,CAAC;AACH,QAAM,gBACJ,kBAAkB,QAAQ,eAAe,OACrC,QACC,iBAAiB,MAAM,cAAc;AAC5C,MAAI,kBAAkB,MAAM;AAC1B,aAAS,KAAK,uCAAuC;EACvD;AAEA,QAAM,OAAyB;IAC7B,QAAQ;IACR,MAAM;IACN,YAAY;IACZ,cAAc;IACd,eAAe,OAAO;IACtB,YAAY;IACZ,MAAM;IACN;IACA;IACA,uBAAuB;IACvB,YAAY;IACZ,YAAY;IACZ,gBAAgB;IAChB,iBAAiB;IACjB;IACA;EACF;AAEA,MAAK,MAAM,uBAAuB,OAAO,SAAS,MAAO,WAAW;AAClE,WAAO;EACT;AAEA,QAAM,OAAO,MAAM;IACjB;IACA,CAAC,SAAS,eAAe,YAAY,cAAc,OAAO,SAAS;IACnE,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,KAAK,aAAa,KAAK,CAAC,KAAK,OAAO,KAAK,GAAG;AAC9C,WAAO;EACT;AACA,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,MAAM,KAAK,OAAO,KAAK,EAAE,MAAM,IAAI,EAAE,CAAC,CAAE;EACtD,QAAQ;AACN,WAAO;EACT;AAEA,QAAM,UAAU,UAAU,KAAK,QAAQ;AACvC,QAAM,eAAe,UAAU,gBAAgB,QAAQ,CAAC,CAAC,IAAI;AAC7D,QAAM,iBAAiB,UAAU,gBAAgB,QAAQ,CAAC,CAAC,IAAI;AAC/D,QAAM,UAAU,UAAU,KAAK,KAAK;AACpC,QAAM,UAAU,UAAU,KAAK,OAAO;AAEtC,SAAO;IACL,GAAG;IACH,MAAM;IACN,YAAY,aAAa,KAAK,OAAO;IACrC;;;IAGA,eAAe,OAAO,eAAe;IACrC,YAAY,aAAa,KAAK,OAAO;IACrC,MAAM,KAAK,OAAO,OAAO,SAAS,KAAK,MAAM,EAAE,KAAK,OAAO;IAC3D,YAAY,UAAU,gBAAgB,QAAQ,CAAC,CAAC,IAAI;IACpD,YAAY,UAAU,gBAAgB,QAAQ,CAAC,CAAC,IAAI;IACpD,gBAAgB,UAAU,gBAAgB,QAAQ,CAAC,CAAC,IAAI;IACxD,iBAAiB,UAAU,gBAAgB,QAAQ,CAAC,CAAC,IAAI;EAC3D;AACF;","names":[]}
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- DEFAULT_LOWER_DIRS,
4
3
  RELAY_CONTAINER_NAME,
5
4
  RELAY_IMAGE_REF,
6
5
  RELAY_NETWORK_NAME,
@@ -11,6 +10,7 @@ import {
11
10
  SNAPSHOTS_ROOT,
12
11
  VNC_CONTAINER_PORT,
13
12
  WEB_CONTAINER_PORT,
13
+ bindWorktrees,
14
14
  buildVncUrls,
15
15
  claudeSessionInfo,
16
16
  cursorServerVolumeName,
@@ -20,43 +20,41 @@ import {
20
20
  launchDockerdDaemon,
21
21
  launchVncDaemon,
22
22
  loadConfig,
23
- mountOverlay,
24
23
  registerBoxWithRelay,
25
- removeBoxWorktree,
26
- verifyOverlay,
24
+ removeInBoxWorktree,
27
25
  vscodeServerVolumeName
28
- } from "./chunk-MOC54XL6.js";
26
+ } from "./chunk-PXUBE5KS.js";
29
27
  import {
30
28
  findBox,
31
29
  readState,
32
30
  recordBox,
33
31
  removeBoxRecord
34
- } from "./chunk-IDR4HVIC.js";
32
+ } from "./chunk-HPZMD5DE.js";
35
33
  import {
36
34
  BOXES_ROOT,
37
- CHECKPOINT_VOLUME_PREFIX,
35
+ CHECKPOINT_IMAGE_PREFIX,
38
36
  boxRunDirFor,
39
37
  detectEngine,
40
38
  getHostPaths,
41
39
  inspectContainer,
42
40
  inspectContainerStatus,
43
- inspectVolumeMountpoint,
44
41
  listAgentboxContainers,
45
42
  listAgentboxVolumes,
43
+ listAllCheckpointImages,
46
44
  openInFinder,
47
45
  pauseContainer,
48
46
  publishedHostPort,
49
47
  readBoxStatus,
50
48
  removeContainer,
49
+ removeImage,
51
50
  removeNetwork,
52
51
  removeVolume,
53
52
  startContainer,
54
53
  stopContainer,
55
- unpauseContainer,
56
- volumeExists
57
- } from "./chunk-SOMIKEN2.js";
54
+ unpauseContainer
55
+ } from "./chunk-RFC5F5HR.js";
58
56
 
59
- // ../../packages/sandbox-docker/dist/chunk-6GQGGQBQ.js
57
+ // ../../packages/sandbox-docker/dist/chunk-B2YVULC6.js
60
58
  import { execa } from "execa";
61
59
  import { readdir, rm, stat } from "fs/promises";
62
60
  import { join as join2 } from "path";
@@ -127,7 +125,7 @@ async function listBoxes() {
127
125
  return Promise.all(
128
126
  boxes.map(async (b) => {
129
127
  const state = await inspectContainerStatus(b.container);
130
- const persisted = await readBoxStatus(b.id);
128
+ const persisted = await readBoxStatus(b);
131
129
  const endpoints = await getBoxEndpoints(b, engine, persisted);
132
130
  return {
133
131
  ...b,
@@ -196,28 +194,23 @@ async function stopBox(idOrName) {
196
194
  async function startBox(idOrName) {
197
195
  const box = await resolveBox(idOrName);
198
196
  for (const w of box.gitWorktrees ?? []) {
199
- if (!await pathExists(w.hostWorktreeDir)) {
200
- throw new Error(`box worktree missing on host: ${w.hostWorktreeDir} (recreate the box)`);
201
- }
202
197
  if (!await pathExists(join2(w.hostMainRepo, ".git"))) {
203
198
  throw new Error(
204
199
  `main repo for box worktree missing: ${join2(w.hostMainRepo, ".git")} (recreate the box)`
205
200
  );
206
201
  }
207
202
  }
208
- if (box.checkpointVolume && !await volumeExists(box.checkpointVolume)) {
209
- throw new Error(
210
- `box checkpoint volume missing: ${box.checkpointVolume} (recreate the box)`
203
+ await startContainer(box.container);
204
+ if ((box.gitWorktrees ?? []).length > 0) {
205
+ await bindWorktrees(
206
+ box.container,
207
+ (box.gitWorktrees ?? []).map((w) => ({
208
+ kind: w.kind,
209
+ containerPath: w.containerPath,
210
+ gitWorktreePath: w.gitWorktreePath
211
+ }))
211
212
  );
212
213
  }
213
- await startContainer(box.container);
214
- const nestedWorktrees = (box.gitWorktrees ?? []).filter((w) => w.kind === "nested").map((w) => ({
215
- containerPath: w.containerPath,
216
- mountFromPath: `/agentbox-worktrees/${w.relPathFromWorkspace}`
217
- }));
218
- const lowerDirs = box.lowerDirs && box.lowerDirs.length > 0 ? box.lowerDirs : void 0;
219
- await mountOverlay(box.container, { lowerDirs, nestedWorktrees });
220
- const overlayChecks = await verifyOverlay(box.container, lowerDirs ?? DEFAULT_LOWER_DIRS);
221
214
  if (box.socketPath) {
222
215
  await launchCtlDaemon(box.container, box.socketPath);
223
216
  }
@@ -251,12 +244,13 @@ async function startBox(idOrName) {
251
244
  name: box.name,
252
245
  containerName: box.container,
253
246
  createdAt: box.createdAt,
247
+ projectIndex: box.projectIndex,
254
248
  worktrees: box.gitWorktrees
255
249
  });
256
250
  } catch {
257
251
  }
258
252
  }
259
- return { record: box, overlayChecks };
253
+ return { record: box };
260
254
  }
261
255
  async function openBoxInFinder(idOrName, opts) {
262
256
  const box = await resolveBox(idOrName);
@@ -282,18 +276,8 @@ async function dirSizeBytes(path) {
282
276
  async function inspectBox(idOrName) {
283
277
  const record = await resolveBox(idOrName);
284
278
  const state = await inspectContainerStatus(record.container);
285
- const upperMountpoint = await inspectVolumeMountpoint(record.upperVolume);
286
279
  const snapshotSizeBytes = record.snapshotDir ? await dirSizeBytes(record.snapshotDir) : null;
287
280
  const dockerJson = await inspectContainer(record.container);
288
- let overlayMounted = false;
289
- if (state === "running" || state === "paused") {
290
- const probe = await execa(
291
- "docker",
292
- ["exec", "--user", "root", record.container, "mountpoint", "-q", "/workspace"],
293
- { reject: false }
294
- );
295
- overlayMounted = probe.exitCode === 0;
296
- }
297
281
  let claudeSession = null;
298
282
  if (state === "running") {
299
283
  try {
@@ -304,14 +288,12 @@ async function inspectBox(idOrName) {
304
288
  }
305
289
  const hostPaths = await getHostPaths(record);
306
290
  const engine = await detectEngine();
307
- const persistedStatus = await readBoxStatus(record.id);
291
+ const persistedStatus = await readBoxStatus(record);
308
292
  const endpoints = await getBoxEndpoints(record, engine, persistedStatus);
309
293
  return {
310
294
  record,
311
295
  state,
312
- upperVolume: { name: record.upperVolume, mountpoint: upperMountpoint },
313
296
  snapshotSizeBytes,
314
- overlayMounted,
315
297
  dockerInspect: dockerJson,
316
298
  claudeSession,
317
299
  persistedStatus,
@@ -327,10 +309,16 @@ async function destroyBox(idOrName, opts = {}) {
327
309
  } catch {
328
310
  }
329
311
  }
330
- for (const w of box.gitWorktrees ?? []) {
331
- try {
332
- await removeBoxWorktree({ hostMainRepo: w.hostMainRepo, worktreeDir: w.hostWorktreeDir });
333
- } catch {
312
+ const ownsWorktrees = !box.checkpointImage;
313
+ if (ownsWorktrees) {
314
+ for (const w of box.gitWorktrees ?? []) {
315
+ try {
316
+ await removeInBoxWorktree({
317
+ hostMainRepo: w.hostMainRepo,
318
+ gitWorktreePath: w.gitWorktreePath
319
+ });
320
+ } catch {
321
+ }
334
322
  }
335
323
  }
336
324
  const beforeContainer = await inspectContainerStatus(box.container);
@@ -338,11 +326,6 @@ async function destroyBox(idOrName, opts = {}) {
338
326
  const afterContainer = await inspectContainerStatus(box.container);
339
327
  const removedContainer = beforeContainer !== "missing" && afterContainer === "missing";
340
328
  const removedVolumes = [];
341
- const legacyNodeModulesVolume = `agentbox-nm-${box.id}`;
342
- for (const v of [box.upperVolume, legacyNodeModulesVolume]) {
343
- await removeVolume(v);
344
- removedVolumes.push(v);
345
- }
346
329
  if (box.claudeConfigVolume && box.claudeConfigVolume !== SHARED_CLAUDE_VOLUME) {
347
330
  await removeVolume(box.claudeConfigVolume);
348
331
  removedVolumes.push(box.claudeConfigVolume);
@@ -369,7 +352,7 @@ async function destroyBox(idOrName, opts = {}) {
369
352
  }
370
353
  }
371
354
  try {
372
- await rm(boxRunDirFor(box.id), { recursive: true, force: true });
355
+ await rm(boxRunDirFor(box), { recursive: true, force: true });
373
356
  } catch {
374
357
  }
375
358
  await removeBoxRecord(box.id);
@@ -391,6 +374,15 @@ async function listBoxDirs() {
391
374
  return [];
392
375
  }
393
376
  }
377
+ async function listCheckpointImageTags() {
378
+ const r = await execa(
379
+ "docker",
380
+ ["image", "ls", "--format", "{{.Repository}}:{{.Tag}}", `${CHECKPOINT_IMAGE_PREFIX}*`],
381
+ { reject: false }
382
+ );
383
+ if (r.exitCode !== 0) return [];
384
+ return (r.stdout ?? "").split("\n").map((s) => s.trim()).filter((s) => s.startsWith(CHECKPOINT_IMAGE_PREFIX));
385
+ }
394
386
  async function pruneBoxes(opts = {}) {
395
387
  const dryRun = opts.dryRun ?? false;
396
388
  const all = opts.all ?? false;
@@ -403,23 +395,21 @@ async function pruneBoxes(opts = {}) {
403
395
  let orphanVolumes = [];
404
396
  let orphanSnapshots = [];
405
397
  let orphanBoxDirs = [];
398
+ let orphanCheckpointImages = [];
406
399
  if (all) {
407
400
  const liveContainers = await listAgentboxContainers();
408
401
  const liveVolumes = await listAgentboxVolumes();
409
402
  const liveSnapshotDirs = await listSnapshotDirs();
410
403
  const liveBoxDirs = await listBoxDirs();
404
+ const liveCheckpointImages = await listCheckpointImageTags();
405
+ const manifestPinnedImages = await listAllCheckpointImages();
411
406
  const survivingBoxes = boxes.filter((b) => !missingRecords.some((m) => m.id === b.id));
412
407
  const expectedContainers = /* @__PURE__ */ new Set([
413
408
  ...survivingBoxes.map((b) => b.container)
414
- // The relay no longer runs as a container (it's a host node process
415
- // now). Any agentbox-relay container is a leftover from a previous
416
- // version of agentbox; it will be collected as an orphan below.
409
+ // The relay no longer runs as a container; leftovers are collected
410
+ // below.
417
411
  ]);
418
412
  const expectedVolumes = /* @__PURE__ */ new Set([
419
- // agentbox-nm-<id> reconstructed for back-compat: a surviving box
420
- // created before the nm volume was removed still mounts it, so it must
421
- // stay allowlisted. Inert for newer boxes (no such volume exists).
422
- ...survivingBoxes.flatMap((b) => [b.upperVolume, `agentbox-nm-${b.id}`]),
423
413
  ...survivingBoxes.map((b) => b.claudeConfigVolume).filter((v) => typeof v === "string"),
424
414
  ...survivingBoxes.map((b) => b.vscodeServerVolume).filter((v) => typeof v === "string"),
425
415
  ...survivingBoxes.map((b) => b.cursorServerVolume).filter((v) => typeof v === "string"),
@@ -435,15 +425,22 @@ async function pruneBoxes(opts = {}) {
435
425
  SHARED_DOCKER_CACHE_VOLUME
436
426
  ]);
437
427
  const expectedSnapshots = new Set(
438
- survivingBoxes.filter((b) => b.snapshotDir !== null).map((b) => b.snapshotDir)
428
+ survivingBoxes.filter(
429
+ (b) => typeof b.snapshotDir === "string"
430
+ ).map((b) => b.snapshotDir)
439
431
  );
440
- const expectedBoxDirs = new Set(survivingBoxes.map((b) => boxRunDirFor(b.id)));
432
+ const expectedBoxDirs = new Set(survivingBoxes.map((b) => boxRunDirFor(b)));
433
+ const expectedCheckpointImages = /* @__PURE__ */ new Set([
434
+ ...survivingBoxes.map((b) => b.checkpointImage).filter((v) => typeof v === "string"),
435
+ ...manifestPinnedImages
436
+ ]);
441
437
  orphanContainers = liveContainers.filter((c) => !expectedContainers.has(c));
442
- orphanVolumes = liveVolumes.filter(
443
- (v) => !expectedVolumes.has(v) && !v.startsWith(CHECKPOINT_VOLUME_PREFIX)
444
- );
438
+ orphanVolumes = liveVolumes.filter((v) => !expectedVolumes.has(v));
445
439
  orphanSnapshots = liveSnapshotDirs.filter((d) => !expectedSnapshots.has(d));
446
440
  orphanBoxDirs = liveBoxDirs.filter((d) => !expectedBoxDirs.has(d));
441
+ orphanCheckpointImages = liveCheckpointImages.filter(
442
+ (t) => !expectedCheckpointImages.has(t)
443
+ );
447
444
  }
448
445
  if (dryRun) {
449
446
  return {
@@ -452,6 +449,7 @@ async function pruneBoxes(opts = {}) {
452
449
  removedVolumes: orphanVolumes,
453
450
  removedSnapshotDirs: orphanSnapshots,
454
451
  removedBoxDirs: orphanBoxDirs,
452
+ removedCheckpointImages: orphanCheckpointImages,
455
453
  dryRun: true
456
454
  };
457
455
  }
@@ -470,6 +468,9 @@ async function pruneBoxes(opts = {}) {
470
468
  } catch {
471
469
  }
472
470
  }
471
+ for (const img of orphanCheckpointImages) {
472
+ await removeImage(img, { force: true });
473
+ }
473
474
  if (all) {
474
475
  try {
475
476
  await removeContainer(RELAY_CONTAINER_NAME);
@@ -490,6 +491,7 @@ async function pruneBoxes(opts = {}) {
490
491
  removedVolumes: orphanVolumes,
491
492
  removedSnapshotDirs: orphanSnapshots,
492
493
  removedBoxDirs: orphanBoxDirs,
494
+ removedCheckpointImages: orphanCheckpointImages,
493
495
  dryRun: false
494
496
  };
495
497
  }
@@ -504,7 +506,6 @@ async function snapshotPresent(path) {
504
506
  }
505
507
 
506
508
  export {
507
- getBoxEndpoints,
508
509
  listBoxes,
509
510
  BoxNotFoundError,
510
511
  AmbiguousBoxError,
@@ -519,4 +520,4 @@ export {
519
520
  pruneBoxes,
520
521
  snapshotPresent
521
522
  };
522
- //# sourceMappingURL=chunk-3JKQNOXP.js.map
523
+ //# sourceMappingURL=chunk-FJNIFTWK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../packages/sandbox-docker/src/lifecycle.ts","../../../packages/sandbox-docker/src/endpoints.ts"],"sourcesContent":["import { execa } from 'execa';\nimport { readdir, rm, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { BoxState } from '@agentbox/core';\nimport type { BoxStatus, ClaudeActivityState } from '@agentbox/ctl';\nimport { claudeSessionInfo, SHARED_CLAUDE_VOLUME, type ClaudeSessionInfo } from './claude.js';\nimport { bindWorktrees, removeInBoxWorktree } from './in-box-git.js';\nimport {\n cursorServerVolumeName,\n SHARED_CURSOR_EXTENSIONS_VOLUME,\n SHARED_VSCODE_EXTENSIONS_VOLUME,\n vscodeServerVolumeName,\n} from './vscode.js';\nimport {\n BOXES_ROOT,\n boxRunDirFor,\n detectEngine,\n getHostPaths,\n openInFinder,\n readBoxStatus,\n type HostPaths,\n type OpenOptions,\n type OpenResult,\n} from './host-export.js';\nimport {\n inspectContainer,\n inspectContainerStatus,\n listAgentboxContainers,\n listAgentboxVolumes,\n pauseContainer,\n publishedHostPort,\n removeContainer,\n removeImage,\n removeNetwork,\n removeVolume,\n startContainer,\n stopContainer,\n unpauseContainer,\n} from './docker.js';\nimport { CHECKPOINT_IMAGE_PREFIX, listAllCheckpointImages } from './checkpoint.js';\nimport { launchCtlDaemon } from './ctl.js';\nimport { launchDockerdDaemon, SHARED_DOCKER_CACHE_VOLUME } from './dockerd.js';\nimport { launchVncDaemon, VNC_CONTAINER_PORT } from './vnc.js';\nimport { WEB_CONTAINER_PORT } from './web.js';\nimport { getBoxEndpoints, type BoxEndpoints } from './endpoints.js';\nimport {\n ensureRelay,\n forgetBoxFromRelay,\n registerBoxWithRelay,\n RELAY_CONTAINER_NAME,\n RELAY_IMAGE_REF,\n RELAY_NETWORK_NAME,\n} from './relay.js';\nimport { SNAPSHOTS_ROOT } from './snapshot.js';\nimport {\n findBox,\n readState,\n recordBox,\n removeBoxRecord,\n type BoxRecord,\n type FindBoxResult,\n} from './state.js';\n\nexport interface ListedBox extends BoxRecord {\n state: BoxState;\n endpoints: BoxEndpoints;\n /** From the persisted status file; undefined for pre-feature/never-pushed boxes. */\n claudeActivity?: ClaudeActivityState;\n /** Sanitized in-box terminal title Claude set; undefined when none. */\n claudeSessionTitle?: string;\n}\n\nexport async function listBoxes(): Promise<ListedBox[]> {\n const { boxes } = await readState();\n const engine = await detectEngine();\n return Promise.all(\n boxes.map(async (b): Promise<ListedBox> => {\n const state = await inspectContainerStatus(b.container);\n const persisted = await readBoxStatus(b);\n const endpoints = await getBoxEndpoints(b, engine, persisted);\n return {\n ...b,\n state,\n endpoints,\n claudeActivity: persisted?.claude.state,\n claudeSessionTitle: persisted?.claude.sessionTitle,\n };\n }),\n );\n}\n\nexport class BoxNotFoundError extends Error {\n constructor(public readonly query: string) {\n super(`no agentbox matches \"${query}\"`);\n this.name = 'BoxNotFoundError';\n }\n}\n\nexport class AmbiguousBoxError extends Error {\n constructor(\n public readonly query: string,\n public readonly matches: BoxRecord[],\n ) {\n const ids = matches.map((m) => m.id).join(', ');\n super(`\"${query}\" matches multiple boxes: ${ids}`);\n this.name = 'AmbiguousBoxError';\n }\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await stat(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function resolveBox(idOrName: string): Promise<BoxRecord> {\n const state = await readState();\n const result: FindBoxResult = findBox(idOrName, state);\n switch (result.kind) {\n case 'ok':\n return result.box;\n case 'none':\n throw new BoxNotFoundError(idOrName);\n case 'ambiguous':\n throw new AmbiguousBoxError(idOrName, result.matches);\n }\n}\n\nexport async function pauseBox(idOrName: string): Promise<BoxRecord> {\n const box = await resolveBox(idOrName);\n await pauseContainer(box.container);\n return box;\n}\n\nexport async function unpauseBox(idOrName: string): Promise<BoxRecord> {\n const box = await resolveBox(idOrName);\n await unpauseContainer(box.container);\n return box;\n}\n\nexport async function stopBox(idOrName: string): Promise<BoxRecord> {\n const box = await resolveBox(idOrName);\n await stopContainer(box.container);\n return box;\n}\n\nexport interface StartedBox {\n record: BoxRecord;\n}\n\n/**\n * Re-start a stopped box.\n *\n * /workspace is just the container's writable filesystem now, so there's no\n * overlay to remount — `docker start` brings everything back. The in-box\n * supervisor, dockerd, and Xvnc all die with the container, so we relaunch\n * them via the same exec-d helpers `create` used. Ephemeral host ports for\n * VNC + web get re-allocated by Docker on `start`, so we re-resolve and\n * persist those too.\n */\nexport async function startBox(idOrName: string): Promise<StartedBox> {\n const box = await resolveBox(idOrName);\n // .git bind-mounts are baked into the container at create time; if a host\n // main repo's .git/ has been deleted out from under us, restart fails with\n // an opaque mount error. Surface it loudly.\n for (const w of box.gitWorktrees ?? []) {\n if (!(await pathExists(join(w.hostMainRepo, '.git')))) {\n throw new Error(\n `main repo for box worktree missing: ${join(w.hostMainRepo, '.git')} (recreate the box)`,\n );\n }\n }\n await startContainer(box.container);\n\n // /workspace bind mounts don't survive `docker stop` (the mount namespace\n // is recreated on start). Re-bind each registered worktree before any\n // daemon comes up — the supervisor and dockerd may resolve paths under\n // /workspace and would see the image's empty dir without this.\n if ((box.gitWorktrees ?? []).length > 0) {\n await bindWorktrees(\n box.container,\n (box.gitWorktrees ?? []).map((w) => ({\n kind: w.kind,\n containerPath: w.containerPath,\n gitWorktreePath: w.gitWorktreePath,\n })),\n );\n }\n\n if (box.socketPath) {\n // The daemon died with the container; relaunch it. Best-effort, same as\n // create.ts — a missing config or other startup issue shouldn't block\n // resumption of the box itself.\n await launchCtlDaemon(box.container, box.socketPath);\n }\n if (box.dockerVolume) {\n await launchDockerdDaemon(box.container);\n }\n if (box.vncEnabled) {\n // Xvnc + websockify both die with the container. The password is already\n // in the container env (set at `docker run` time and preserved across\n // start/stop), so we don't need to forward it here.\n await launchVncDaemon(box.container);\n // Docker re-allocates an ephemeral host port for `-p 0:6080` on every\n // `start`. Re-resolve and persist; the orb.local URL is name-based and\n // unaffected. Best-effort — a failed resolve just leaves the record as-is.\n const freshHostPort = await publishedHostPort(box.container, VNC_CONTAINER_PORT);\n if (freshHostPort && freshHostPort !== box.vncHostPort) {\n box.vncHostPort = freshHostPort;\n await recordBox(box);\n }\n }\n // Same ephemeral-reallocation story for the reserved web port. Gated on\n // webContainerPort so pre-feature boxes (no `-p 0:80` mapping) are skipped.\n if (box.webContainerPort !== undefined) {\n const freshWebPort = await publishedHostPort(\n box.container,\n box.webContainerPort ?? WEB_CONTAINER_PORT,\n );\n if (freshWebPort && freshWebPort !== box.webHostPort) {\n box.webHostPort = freshWebPort;\n await recordBox(box);\n }\n }\n // Relay's in-memory registry may have been lost if the relay restarted\n // between create and now (or this is the first start after a host reboot).\n // Re-ensure + re-register so outbound push from the box keeps working.\n if (box.relayToken) {\n try {\n await ensureRelay();\n await registerBoxWithRelay({\n boxId: box.id,\n token: box.relayToken,\n name: box.name,\n containerName: box.container,\n createdAt: box.createdAt,\n projectIndex: box.projectIndex,\n worktrees: box.gitWorktrees,\n });\n } catch {\n // best-effort\n }\n }\n return { record: box };\n}\n\nexport interface OpenedBox extends OpenResult {\n record: BoxRecord;\n}\n\nexport async function openBoxInFinder(idOrName: string, opts: OpenOptions): Promise<OpenedBox> {\n const box = await resolveBox(idOrName);\n const result = await openInFinder(box, opts);\n return { ...result, record: box };\n}\n\nexport async function getBoxHostPaths(\n idOrName: string,\n): Promise<{ record: BoxRecord; paths: HostPaths }> {\n const box = await resolveBox(idOrName);\n const paths = await getHostPaths(box);\n return { record: box, paths };\n}\n\nexport interface InspectedBox {\n record: BoxRecord;\n state: BoxState;\n snapshotSizeBytes: number | null;\n dockerInspect: unknown;\n /** Null when the container isn't running; otherwise best-effort probe of the tmux 'claude' session. */\n claudeSession: ClaudeSessionInfo | null;\n /** Persisted status snapshot (services/tasks/ports/claude); null when none. */\n persistedStatus: BoxStatus | null;\n /** Host paths for `agentbox open`. */\n hostPaths: HostPaths;\n /** Box network surface: domain + VNC + service ports. */\n endpoints: BoxEndpoints;\n}\n\nasync function dirSizeBytes(path: string): Promise<number | null> {\n try {\n const result = await execa('du', ['-sk', path], { reject: false });\n if (result.exitCode !== 0) return null;\n const sizeKb = Number.parseInt((result.stdout ?? '').split(/\\s+/)[0] ?? '', 10);\n if (Number.isNaN(sizeKb)) return null;\n return sizeKb * 1024;\n } catch {\n return null;\n }\n}\n\nexport async function inspectBox(idOrName: string): Promise<InspectedBox> {\n const record = await resolveBox(idOrName);\n const state = await inspectContainerStatus(record.container);\n const snapshotSizeBytes = record.snapshotDir ? await dirSizeBytes(record.snapshotDir) : null;\n const dockerJson = await inspectContainer(record.container);\n\n let claudeSession: ClaudeSessionInfo | null = null;\n if (state === 'running') {\n try {\n claudeSession = await claudeSessionInfo(record.container);\n } catch {\n claudeSession = null;\n }\n }\n\n const hostPaths = await getHostPaths(record);\n const engine = await detectEngine();\n const persistedStatus = await readBoxStatus(record);\n const endpoints = await getBoxEndpoints(record, engine, persistedStatus);\n\n return {\n record,\n state,\n snapshotSizeBytes,\n dockerInspect: dockerJson,\n claudeSession,\n persistedStatus,\n hostPaths,\n endpoints,\n };\n}\n\nexport interface DestroyOptions {\n keepSnapshot?: boolean;\n}\n\nexport interface DestroyResult {\n record: BoxRecord;\n removedContainer: boolean;\n removedVolumes: string[];\n removedSnapshot: string | null;\n}\n\nexport async function destroyBox(\n idOrName: string,\n opts: DestroyOptions = {},\n): Promise<DestroyResult> {\n const box = await resolveBox(idOrName);\n\n // Each step is best-effort. We collect what actually went away so the CLI\n // can show a truthful summary even if e.g. the container was gone already.\n if (box.relayToken) {\n try {\n await forgetBoxFromRelay(box.id);\n } catch {\n // best-effort — relay may be down or already wiped the entry\n }\n }\n // Deregister each in-container worktree from the host main repo. Skip\n // when this box was checkpoint-restored: its `gitWorktrees` were inherited\n // from the source box via the checkpoint manifest, and the same\n // `gitWorktreePath` may still be in use by the source (or by sibling\n // restores). Removing the registration here would break those. The\n // registration is cosmetically `prunable` on the host anyway (the path is\n // container-only) and can be reaped with `git worktree prune` when the\n // user is sure no box references it.\n const ownsWorktrees = !box.checkpointImage;\n if (ownsWorktrees) {\n for (const w of box.gitWorktrees ?? []) {\n try {\n await removeInBoxWorktree({\n hostMainRepo: w.hostMainRepo,\n gitWorktreePath: w.gitWorktreePath,\n });\n } catch {\n // best-effort\n }\n }\n }\n const beforeContainer = await inspectContainerStatus(box.container);\n await removeContainer(box.container);\n const afterContainer = await inspectContainerStatus(box.container);\n const removedContainer = beforeContainer !== 'missing' && afterContainer === 'missing';\n\n const removedVolumes: string[] = [];\n // Per-box claude config volumes are box-private — safe to remove. The shared\n // SHARED_CLAUDE_VOLUME holds user identity (auth, skills, plugins) across\n // every box, so never auto-remove it; users delete it manually if they want.\n if (box.claudeConfigVolume && box.claudeConfigVolume !== SHARED_CLAUDE_VOLUME) {\n await removeVolume(box.claudeConfigVolume);\n removedVolumes.push(box.claudeConfigVolume);\n }\n // Per-box `.vscode-server` and `.cursor-server` volumes. The shared\n // SHARED_*_EXTENSIONS_VOLUMEs are never auto-removed (parallel reasoning to\n // the shared claude volume).\n const perBoxIdeVolumes = [\n box.vscodeServerVolume ?? vscodeServerVolumeName(box.id),\n box.cursorServerVolume ?? cursorServerVolumeName(box.id),\n ];\n for (const v of perBoxIdeVolumes) {\n await removeVolume(v);\n removedVolumes.push(v);\n }\n // Per-box dockerd data root. Skip when this box used the shared cache —\n // wiping it would also remove image layers other boxes (or future ones)\n // depend on. The shared volume is allowlisted in `pruneBoxes --all` too.\n if (box.dockerVolume && !box.dockerCacheShared) {\n await removeVolume(box.dockerVolume);\n removedVolumes.push(box.dockerVolume);\n }\n\n let removedSnapshot: string | null = null;\n if (box.snapshotDir && !opts.keepSnapshot) {\n try {\n await rm(box.snapshotDir, { recursive: true, force: true });\n removedSnapshot = box.snapshotDir;\n } catch {\n removedSnapshot = null;\n }\n }\n\n // The per-box runtime dir holds the ctl socket plus the workspace export\n // dir used by `agentbox open`. Wipe the whole thing so destroy leaves no\n // residue under ~/.agentbox/boxes/.\n try {\n await rm(boxRunDirFor(box), { recursive: true, force: true });\n } catch {\n // best-effort\n }\n\n await removeBoxRecord(box.id);\n\n return { record: box, removedContainer, removedVolumes, removedSnapshot };\n}\n\nexport interface PruneOptions {\n dryRun?: boolean;\n all?: boolean;\n}\n\nexport interface PruneResult {\n removedRecords: string[];\n removedContainers: string[];\n removedVolumes: string[];\n removedSnapshotDirs: string[];\n removedBoxDirs: string[];\n removedCheckpointImages: string[];\n dryRun: boolean;\n}\n\nasync function listSnapshotDirs(): Promise<string[]> {\n try {\n const entries = await readdir(SNAPSHOTS_ROOT, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory()).map((e) => join(SNAPSHOTS_ROOT, e.name));\n } catch {\n return [];\n }\n}\n\nasync function listBoxDirs(): Promise<string[]> {\n try {\n const entries = await readdir(BOXES_ROOT, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory()).map((e) => join(BOXES_ROOT, e.name));\n } catch {\n return [];\n }\n}\n\n/**\n * Local Docker image *tags* that look like checkpoint images\n * (`agentbox-ckpt-<projectHash>:<name>`). Used by `prune --all` to find\n * candidates for reaping. An image is reapable only when **both** of these\n * are true: no surviving box's `checkpointImage` points at it, **and** no\n * on-disk manifest under `~/.agentbox/checkpoints/<projectHash>/<name>/`\n * names it as its `image` (see `listAllCheckpointImages`) — otherwise a\n * `destroy` + `prune --all` would silently break checkpoints the user still\n * intends to start new boxes from. Best-effort: returns empty on docker\n * errors.\n */\nasync function listCheckpointImageTags(): Promise<string[]> {\n const r = await execa(\n 'docker',\n ['image', 'ls', '--format', '{{.Repository}}:{{.Tag}}', `${CHECKPOINT_IMAGE_PREFIX}*`],\n { reject: false },\n );\n if (r.exitCode !== 0) return [];\n return (r.stdout ?? '')\n .split('\\n')\n .map((s) => s.trim())\n .filter((s) => s.startsWith(CHECKPOINT_IMAGE_PREFIX));\n}\n\nexport async function pruneBoxes(opts: PruneOptions = {}): Promise<PruneResult> {\n const dryRun = opts.dryRun ?? false;\n const all = opts.all ?? false;\n\n const { boxes } = await readState();\n\n // Step 1: missing-state records.\n const stateChecks = await Promise.all(\n boxes.map(async (b) => ({ box: b, status: await inspectContainerStatus(b.container) })),\n );\n const missingRecords = stateChecks.filter((c) => c.status === 'missing').map((c) => c.box);\n\n // Step 2 (only with --all): orphan docker containers / volumes / snapshot\n // dirs / per-box dirs / unreferenced checkpoint images.\n let orphanContainers: string[] = [];\n let orphanVolumes: string[] = [];\n let orphanSnapshots: string[] = [];\n let orphanBoxDirs: string[] = [];\n let orphanCheckpointImages: string[] = [];\n\n if (all) {\n const liveContainers = await listAgentboxContainers();\n const liveVolumes = await listAgentboxVolumes();\n const liveSnapshotDirs = await listSnapshotDirs();\n const liveBoxDirs = await listBoxDirs();\n const liveCheckpointImages = await listCheckpointImageTags();\n // Manifests on disk are the durable source of truth for \"this checkpoint\n // exists\" — `destroyBox` leaves them alone on purpose, so an image whose\n // source box was destroyed is still pinned as long as its manifest is\n // there.\n const manifestPinnedImages = await listAllCheckpointImages();\n // The state we'd have AFTER step 1 runs: missing-state records gone.\n const survivingBoxes = boxes.filter((b) => !missingRecords.some((m) => m.id === b.id));\n const expectedContainers = new Set<string>([\n ...survivingBoxes.map((b) => b.container),\n // The relay no longer runs as a container; leftovers are collected\n // below.\n ]);\n const expectedVolumes = new Set<string>([\n ...survivingBoxes\n .map((b) => b.claudeConfigVolume)\n .filter((v): v is string => typeof v === 'string'),\n ...survivingBoxes\n .map((b) => b.vscodeServerVolume)\n .filter((v): v is string => typeof v === 'string'),\n ...survivingBoxes\n .map((b) => b.cursorServerVolume)\n .filter((v): v is string => typeof v === 'string'),\n ...survivingBoxes\n .map((b) => b.dockerVolume)\n .filter((v): v is string => typeof v === 'string'),\n // The shared claude-config volume holds user identity across every box;\n // never reap it via prune even if no surviving box currently references it.\n SHARED_CLAUDE_VOLUME,\n // Shared across boxes: downloaded IDE extensions. Same reasoning.\n SHARED_VSCODE_EXTENSIONS_VOLUME,\n SHARED_CURSOR_EXTENSIONS_VOLUME,\n // Shared in-box docker image cache — opt-in via `box.dockerCacheShared`,\n // never auto-removed (image layers may be reused by future boxes).\n SHARED_DOCKER_CACHE_VOLUME,\n ]);\n const expectedSnapshots = new Set(\n survivingBoxes\n .filter((b): b is BoxRecord & { snapshotDir: string } =>\n typeof b.snapshotDir === 'string',\n )\n .map((b) => b.snapshotDir),\n );\n const expectedBoxDirs = new Set(survivingBoxes.map((b) => boxRunDirFor(b)));\n // Checkpoint images: keep any tag that either a surviving box's\n // `checkpointImage` points at, or that any on-disk manifest still claims\n // as its `image`. The manifest case is the one that matters most after\n // destroy: the source box is gone but the user still wants to seed new\n // boxes from the checkpoint. The surviving-box case stays as a fallback\n // for the edge where someone `rm -rf`'d a manifest dir while a box\n // restored from it is still running.\n const expectedCheckpointImages = new Set<string>([\n ...survivingBoxes\n .map((b) => b.checkpointImage)\n .filter((v): v is string => typeof v === 'string'),\n ...manifestPinnedImages,\n ]);\n orphanContainers = liveContainers.filter((c) => !expectedContainers.has(c));\n orphanVolumes = liveVolumes.filter((v) => !expectedVolumes.has(v));\n orphanSnapshots = liveSnapshotDirs.filter((d) => !expectedSnapshots.has(d));\n orphanBoxDirs = liveBoxDirs.filter((d) => !expectedBoxDirs.has(d));\n orphanCheckpointImages = liveCheckpointImages.filter(\n (t) => !expectedCheckpointImages.has(t),\n );\n }\n\n if (dryRun) {\n return {\n removedRecords: missingRecords.map((b) => b.id),\n removedContainers: orphanContainers,\n removedVolumes: orphanVolumes,\n removedSnapshotDirs: orphanSnapshots,\n removedBoxDirs: orphanBoxDirs,\n removedCheckpointImages: orphanCheckpointImages,\n dryRun: true,\n };\n }\n\n for (const b of missingRecords) await removeBoxRecord(b.id);\n for (const c of orphanContainers) await removeContainer(c);\n for (const v of orphanVolumes) await removeVolume(v);\n for (const d of orphanSnapshots) {\n try {\n await rm(d, { recursive: true, force: true });\n } catch {\n // best-effort\n }\n }\n for (const d of orphanBoxDirs) {\n try {\n await rm(d, { recursive: true, force: true });\n } catch {\n // best-effort\n }\n }\n for (const img of orphanCheckpointImages) {\n await removeImage(img, { force: true });\n }\n\n // Migration sweep: the relay used to be a docker container on a dedicated\n // network with its own image. None of those exist after this version of\n // agentbox; drop any leftovers from previous installs. Idempotent and\n // best-effort — these calls succeed silently if the objects are already\n // gone.\n if (all) {\n try {\n await removeContainer(RELAY_CONTAINER_NAME);\n } catch {\n // best-effort\n }\n try {\n await execa('docker', ['image', 'rm', RELAY_IMAGE_REF], { reject: false });\n } catch {\n // best-effort\n }\n try {\n await removeNetwork(RELAY_NETWORK_NAME);\n } catch {\n // best-effort\n }\n }\n\n return {\n removedRecords: missingRecords.map((b) => b.id),\n removedContainers: orphanContainers,\n removedVolumes: orphanVolumes,\n removedSnapshotDirs: orphanSnapshots,\n removedBoxDirs: orphanBoxDirs,\n removedCheckpointImages: orphanCheckpointImages,\n dryRun: false,\n };\n}\n\n// Help vitest / unit tests get to the snapshot-root constant without pulling\n// the whole snapshot module surface.\nexport { SNAPSHOTS_ROOT };\n\n// Re-export the file existence helper for inspect output; useful guard for\n// callers that want to know if a snapshot dir was ever created.\nexport async function snapshotPresent(path: string | null): Promise<boolean> {\n if (!path) return false;\n try {\n const s = await stat(path);\n return s.isDirectory();\n } catch {\n return false;\n }\n}\n","import { join } from 'node:path';\nimport { loadConfig } from '@agentbox/ctl';\nimport type { BoxStatus } from '@agentbox/ctl';\nimport type { BoxRecord } from './state.js';\nimport type { DockerEngine } from './host-export.js';\nimport { buildVncUrls, VNC_CONTAINER_PORT } from './vnc.js';\nimport { WEB_CONTAINER_PORT } from './web.js';\n\nexport interface BoxEndpoint {\n kind: 'vnc' | 'service' | 'web';\n /** Service name (kind === 'service'/'web') or 'vnc' (kind === 'vnc'). */\n name: string;\n /** In-container port (6080 for VNC, the `ready_when.port` value for services). */\n containerPort: number;\n /**\n * Host-side URL the user can open. Undefined when the port isn't reachable\n * from the host (service ports on Docker Desktop, since we don't auto-publish\n * them today).\n */\n url?: string;\n /** Whether the URL is reachable from the host on the current engine. */\n reachable: boolean;\n}\n\nexport interface BoxEndpoints {\n /** Bare hostname/IP for the box — `<container>.orb.local` on OrbStack, `127.0.0.1` otherwise. */\n domain: string;\n /** True when domain is the OrbStack auto-DNS (any in-container port works). */\n domainIsOrb: boolean;\n /** Ordered list of endpoints: VNC first (if enabled), then services in agentbox.yaml order. */\n endpoints: BoxEndpoint[];\n}\n\n/**\n * Build the box's user-facing network surface. Pure host-side: no docker exec,\n * no network — safe to call from `agentbox list` in a tight loop.\n *\n * Service ports come from the persisted status snapshot when available\n * (`~/.agentbox/boxes/<id>/status.json`, pushed by the in-box supervisor via\n * the relay). That snapshot resolves `ready_when.port` *inside the box*, so it\n * works even when `agentbox.yaml` lives only in the box and was never pulled to\n * the host. Falls back to parsing the host's `agentbox.yaml` for pre-relay\n * boxes (or ones that never pushed a snapshot).\n *\n * Missing config + no snapshot is non-fatal: the VNC entry (if any) is still\n * returned. Engine drives reachability — OrbStack auto-routes\n * `<container>.orb.local:<port>` for any in-box port; other engines see only\n * what we explicitly publish via `docker run -p`, which today is just VNC.\n */\nexport async function getBoxEndpoints(\n record: BoxRecord,\n engine: DockerEngine,\n persisted?: BoxStatus | null,\n): Promise<BoxEndpoints> {\n const domainIsOrb = engine === 'orbstack';\n const domain = domainIsOrb ? `${record.container}.orb.local` : '127.0.0.1';\n\n const endpoints: BoxEndpoint[] = [];\n\n if (record.vncEnabled && record.vncPassword) {\n const vncUrls = buildVncUrls(record, engine);\n const url = vncUrls.orbUrl ?? vncUrls.loopbackUrl;\n endpoints.push({\n kind: 'vnc',\n name: 'vnc',\n containerPort: VNC_CONTAINER_PORT,\n url,\n reachable: Boolean(url),\n });\n }\n\n // The single `expose:`-flagged service, from the snapshot first (works when\n // agentbox.yaml lives only in the box), else the host yaml.\n let webServiceName: string | null = null;\n const persistedWeb = persisted?.services.find((s) => s.expose);\n if (persistedWeb) {\n webServiceName = persistedWeb.name;\n }\n\n const pushService = (name: string, port: number): void => {\n // The web service is surfaced as the dedicated `web` endpoint below;\n // don't also list it as a generic service.\n if (name === webServiceName) return;\n endpoints.push({\n kind: 'service',\n name,\n containerPort: port,\n // Only OrbStack auto-routes arbitrary in-box ports; on other engines we\n // don't publish service ports, so the URL isn't host-reachable.\n ...(domainIsOrb\n ? { url: `http://${domain}:${String(port)}`, reachable: true }\n : { reachable: false }),\n });\n };\n\n const persistedServices = persisted?.services.filter(\n (s): s is typeof s & { port: number } => typeof s.port === 'number',\n );\n if (persistedServices && persistedServices.length > 0) {\n for (const svc of persistedServices) pushService(svc.name, svc.port);\n } else {\n try {\n const cfg = await loadConfig(join(record.workspacePath, 'agentbox.yaml'));\n if (!webServiceName) {\n webServiceName = cfg.services.find((s) => s.expose)?.name ?? null;\n }\n for (const svc of cfg.services) {\n if (svc.readyWhen?.kind !== 'port') continue;\n pushService(svc.name, svc.readyWhen.port);\n }\n } catch {\n // No persisted snapshot and no host agentbox.yaml — skip service\n // endpoints. The VNC entry, if any, is unaffected.\n }\n }\n\n // Web endpoint: only for boxes that reserved container :80 at create. The\n // URL is the published loopback host port — uniform across engines, NOT\n // gated on OrbStack (requirement: don't rely on orb auto-DNS). No url until\n // both a service declares `expose:` and the host port is resolved; until\n // then it renders as \"reserved\".\n if (record.webContainerPort !== undefined) {\n const hasTarget = webServiceName !== null && record.webHostPort !== undefined;\n endpoints.push({\n kind: 'web',\n name: webServiceName ?? 'web',\n containerPort: record.webContainerPort ?? WEB_CONTAINER_PORT,\n ...(hasTarget\n ? { url: `http://127.0.0.1:${String(record.webHostPort)}`, reachable: true }\n : { reachable: false }),\n });\n }\n\n return { domain, domainIsOrb, endpoints };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,aAAa;AACtB,SAAS,SAAS,IAAI,YAAY;AAClC,SAAS,QAAAA,aAAY;ACFrB,SAAS,YAAY;AAiDrB,eAAsB,gBACpB,QACA,QACA,WACuB;AACvB,QAAM,cAAc,WAAW;AAC/B,QAAM,SAAS,cAAc,GAAG,OAAO,SAAS,eAAe;AAE/D,QAAM,YAA2B,CAAC;AAElC,MAAI,OAAO,cAAc,OAAO,aAAa;AAC3C,UAAM,UAAU,aAAa,QAAQ,MAAM;AAC3C,UAAM,MAAM,QAAQ,UAAU,QAAQ;AACtC,cAAU,KAAK;MACb,MAAM;MACN,MAAM;MACN,eAAe;MACf;MACA,WAAW,QAAQ,GAAG;IACxB,CAAC;EACH;AAIA,MAAI,iBAAgC;AACpC,QAAM,eAAe,WAAW,SAAS,KAAK,CAAC,MAAM,EAAE,MAAM;AAC7D,MAAI,cAAc;AAChB,qBAAiB,aAAa;EAChC;AAEA,QAAM,cAAc,CAAC,MAAc,SAAuB;AAGxD,QAAI,SAAS,eAAgB;AAC7B,cAAU,KAAK;MACb,MAAM;MACN;MACA,eAAe;;;MAGf,GAAI,cACA,EAAE,KAAK,UAAU,MAAM,IAAI,OAAO,IAAI,CAAC,IAAI,WAAW,KAAK,IAC3D,EAAE,WAAW,MAAM;IACzB,CAAC;EACH;AAEA,QAAM,oBAAoB,WAAW,SAAS;IAC5C,CAAC,MAAwC,OAAO,EAAE,SAAS;EAC7D;AACA,MAAI,qBAAqB,kBAAkB,SAAS,GAAG;AACrD,eAAW,OAAO,kBAAmB,aAAY,IAAI,MAAM,IAAI,IAAI;EACrE,OAAO;AACL,QAAI;AACF,YAAM,MAAM,MAAM,WAAW,KAAK,OAAO,eAAe,eAAe,CAAC;AACxE,UAAI,CAAC,gBAAgB;AACnB,yBAAiB,IAAI,SAAS,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;MAC/D;AACA,iBAAW,OAAO,IAAI,UAAU;AAC9B,YAAI,IAAI,WAAW,SAAS,OAAQ;AACpC,oBAAY,IAAI,MAAM,IAAI,UAAU,IAAI;MAC1C;IACF,QAAQ;IAGR;EACF;AAOA,MAAI,OAAO,qBAAqB,QAAW;AACzC,UAAM,YAAY,mBAAmB,QAAQ,OAAO,gBAAgB;AACpE,cAAU,KAAK;MACb,MAAM;MACN,MAAM,kBAAkB;MACxB,eAAe,OAAO,oBAAoB;MAC1C,GAAI,YACA,EAAE,KAAK,oBAAoB,OAAO,OAAO,WAAW,CAAC,IAAI,WAAW,KAAK,IACzE,EAAE,WAAW,MAAM;IACzB,CAAC;EACH;AAEA,SAAO,EAAE,QAAQ,aAAa,UAAU;AAC1C;AD9DA,eAAsB,YAAkC;AACtD,QAAM,EAAE,MAAM,IAAI,MAAM,UAAU;AAClC,QAAM,SAAS,MAAM,aAAa;AAClC,SAAO,QAAQ;IACb,MAAM,IAAI,OAAO,MAA0B;AACzC,YAAM,QAAQ,MAAM,uBAAuB,EAAE,SAAS;AACtD,YAAM,YAAY,MAAM,cAAc,CAAC;AACvC,YAAM,YAAY,MAAM,gBAAgB,GAAG,QAAQ,SAAS;AAC5D,aAAO;QACL,GAAG;QACH;QACA;QACA,gBAAgB,WAAW,OAAO;QAClC,oBAAoB,WAAW,OAAO;MACxC;IACF,CAAC;EACH;AACF;AAEO,IAAM,mBAAN,cAA+B,MAAM;EAC1C,YAA4B,OAAe;AACzC,UAAM,wBAAwB,KAAK,GAAG;AADZ,SAAA,QAAA;AAE1B,SAAK,OAAO;EACd;EAH4B;AAI9B;AAEO,IAAM,oBAAN,cAAgC,MAAM;EAC3C,YACkB,OACA,SAChB;AACA,UAAM,MAAM,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,IAAI;AAC9C,UAAM,IAAI,KAAK,6BAA6B,GAAG,EAAE;AAJjC,SAAA,QAAA;AACA,SAAA,UAAA;AAIhB,SAAK,OAAO;EACd;EANkB;EACA;AAMpB;AAEA,eAAe,WAAW,GAA6B;AACrD,MAAI;AACF,UAAM,KAAK,CAAC;AACZ,WAAO;EACT,QAAQ;AACN,WAAO;EACT;AACF;AAEA,eAAe,WAAW,UAAsC;AAC9D,QAAM,QAAQ,MAAM,UAAU;AAC9B,QAAM,SAAwB,QAAQ,UAAU,KAAK;AACrD,UAAQ,OAAO,MAAM;IACnB,KAAK;AACH,aAAO,OAAO;IAChB,KAAK;AACH,YAAM,IAAI,iBAAiB,QAAQ;IACrC,KAAK;AACH,YAAM,IAAI,kBAAkB,UAAU,OAAO,OAAO;EACxD;AACF;AAEA,eAAsB,SAAS,UAAsC;AACnE,QAAM,MAAM,MAAM,WAAW,QAAQ;AACrC,QAAM,eAAe,IAAI,SAAS;AAClC,SAAO;AACT;AAEA,eAAsB,WAAW,UAAsC;AACrE,QAAM,MAAM,MAAM,WAAW,QAAQ;AACrC,QAAM,iBAAiB,IAAI,SAAS;AACpC,SAAO;AACT;AAEA,eAAsB,QAAQ,UAAsC;AAClE,QAAM,MAAM,MAAM,WAAW,QAAQ;AACrC,QAAM,cAAc,IAAI,SAAS;AACjC,SAAO;AACT;AAgBA,eAAsB,SAAS,UAAuC;AACpE,QAAM,MAAM,MAAM,WAAW,QAAQ;AAIrC,aAAW,KAAK,IAAI,gBAAgB,CAAC,GAAG;AACtC,QAAI,CAAE,MAAM,WAAWC,MAAK,EAAE,cAAc,MAAM,CAAC,GAAI;AACrD,YAAM,IAAI;QACR,uCAAuCA,MAAK,EAAE,cAAc,MAAM,CAAC;MACrE;IACF;EACF;AACA,QAAM,eAAe,IAAI,SAAS;AAMlC,OAAK,IAAI,gBAAgB,CAAC,GAAG,SAAS,GAAG;AACvC,UAAM;MACJ,IAAI;OACH,IAAI,gBAAgB,CAAC,GAAG,IAAI,CAAC,OAAO;QACnC,MAAM,EAAE;QACR,eAAe,EAAE;QACjB,iBAAiB,EAAE;MACrB,EAAE;IACJ;EACF;AAEA,MAAI,IAAI,YAAY;AAIlB,UAAM,gBAAgB,IAAI,WAAW,IAAI,UAAU;EACrD;AACA,MAAI,IAAI,cAAc;AACpB,UAAM,oBAAoB,IAAI,SAAS;EACzC;AACA,MAAI,IAAI,YAAY;AAIlB,UAAM,gBAAgB,IAAI,SAAS;AAInC,UAAM,gBAAgB,MAAM,kBAAkB,IAAI,WAAW,kBAAkB;AAC/E,QAAI,iBAAiB,kBAAkB,IAAI,aAAa;AACtD,UAAI,cAAc;AAClB,YAAM,UAAU,GAAG;IACrB;EACF;AAGA,MAAI,IAAI,qBAAqB,QAAW;AACtC,UAAM,eAAe,MAAM;MACzB,IAAI;MACJ,IAAI,oBAAoB;IAC1B;AACA,QAAI,gBAAgB,iBAAiB,IAAI,aAAa;AACpD,UAAI,cAAc;AAClB,YAAM,UAAU,GAAG;IACrB;EACF;AAIA,MAAI,IAAI,YAAY;AAClB,QAAI;AACF,YAAM,YAAY;AAClB,YAAM,qBAAqB;QACzB,OAAO,IAAI;QACX,OAAO,IAAI;QACX,MAAM,IAAI;QACV,eAAe,IAAI;QACnB,WAAW,IAAI;QACf,cAAc,IAAI;QAClB,WAAW,IAAI;MACjB,CAAC;IACH,QAAQ;IAER;EACF;AACA,SAAO,EAAE,QAAQ,IAAI;AACvB;AAMA,eAAsB,gBAAgB,UAAkB,MAAuC;AAC7F,QAAM,MAAM,MAAM,WAAW,QAAQ;AACrC,QAAM,SAAS,MAAM,aAAa,KAAK,IAAI;AAC3C,SAAO,EAAE,GAAG,QAAQ,QAAQ,IAAI;AAClC;AAEA,eAAsB,gBACpB,UACkD;AAClD,QAAM,MAAM,MAAM,WAAW,QAAQ;AACrC,QAAM,QAAQ,MAAM,aAAa,GAAG;AACpC,SAAO,EAAE,QAAQ,KAAK,MAAM;AAC9B;AAiBA,eAAe,aAAa,MAAsC;AAChE,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,MAAM,CAAC,OAAO,IAAI,GAAG,EAAE,QAAQ,MAAM,CAAC;AACjE,QAAI,OAAO,aAAa,EAAG,QAAO;AAClC,UAAM,SAAS,OAAO,UAAU,OAAO,UAAU,IAAI,MAAM,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE;AAC9E,QAAI,OAAO,MAAM,MAAM,EAAG,QAAO;AACjC,WAAO,SAAS;EAClB,QAAQ;AACN,WAAO;EACT;AACF;AAEA,eAAsB,WAAW,UAAyC;AACxE,QAAM,SAAS,MAAM,WAAW,QAAQ;AACxC,QAAM,QAAQ,MAAM,uBAAuB,OAAO,SAAS;AAC3D,QAAM,oBAAoB,OAAO,cAAc,MAAM,aAAa,OAAO,WAAW,IAAI;AACxF,QAAM,aAAa,MAAM,iBAAiB,OAAO,SAAS;AAE1D,MAAI,gBAA0C;AAC9C,MAAI,UAAU,WAAW;AACvB,QAAI;AACF,sBAAgB,MAAM,kBAAkB,OAAO,SAAS;IAC1D,QAAQ;AACN,sBAAgB;IAClB;EACF;AAEA,QAAM,YAAY,MAAM,aAAa,MAAM;AAC3C,QAAM,SAAS,MAAM,aAAa;AAClC,QAAM,kBAAkB,MAAM,cAAc,MAAM;AAClD,QAAM,YAAY,MAAM,gBAAgB,QAAQ,QAAQ,eAAe;AAEvE,SAAO;IACL;IACA;IACA;IACA,eAAe;IACf;IACA;IACA;IACA;EACF;AACF;AAaA,eAAsB,WACpB,UACA,OAAuB,CAAC,GACA;AACxB,QAAM,MAAM,MAAM,WAAW,QAAQ;AAIrC,MAAI,IAAI,YAAY;AAClB,QAAI;AACF,YAAM,mBAAmB,IAAI,EAAE;IACjC,QAAQ;IAER;EACF;AASA,QAAM,gBAAgB,CAAC,IAAI;AAC3B,MAAI,eAAe;AACjB,eAAW,KAAK,IAAI,gBAAgB,CAAC,GAAG;AACtC,UAAI;AACF,cAAM,oBAAoB;UACxB,cAAc,EAAE;UAChB,iBAAiB,EAAE;QACrB,CAAC;MACH,QAAQ;MAER;IACF;EACF;AACA,QAAM,kBAAkB,MAAM,uBAAuB,IAAI,SAAS;AAClE,QAAM,gBAAgB,IAAI,SAAS;AACnC,QAAM,iBAAiB,MAAM,uBAAuB,IAAI,SAAS;AACjE,QAAM,mBAAmB,oBAAoB,aAAa,mBAAmB;AAE7E,QAAM,iBAA2B,CAAC;AAIlC,MAAI,IAAI,sBAAsB,IAAI,uBAAuB,sBAAsB;AAC7E,UAAM,aAAa,IAAI,kBAAkB;AACzC,mBAAe,KAAK,IAAI,kBAAkB;EAC5C;AAIA,QAAM,mBAAmB;IACvB,IAAI,sBAAsB,uBAAuB,IAAI,EAAE;IACvD,IAAI,sBAAsB,uBAAuB,IAAI,EAAE;EACzD;AACA,aAAW,KAAK,kBAAkB;AAChC,UAAM,aAAa,CAAC;AACpB,mBAAe,KAAK,CAAC;EACvB;AAIA,MAAI,IAAI,gBAAgB,CAAC,IAAI,mBAAmB;AAC9C,UAAM,aAAa,IAAI,YAAY;AACnC,mBAAe,KAAK,IAAI,YAAY;EACtC;AAEA,MAAI,kBAAiC;AACrC,MAAI,IAAI,eAAe,CAAC,KAAK,cAAc;AACzC,QAAI;AACF,YAAM,GAAG,IAAI,aAAa,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC1D,wBAAkB,IAAI;IACxB,QAAQ;AACN,wBAAkB;IACpB;EACF;AAKA,MAAI;AACF,UAAM,GAAG,aAAa,GAAG,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;EAC9D,QAAQ;EAER;AAEA,QAAM,gBAAgB,IAAI,EAAE;AAE5B,SAAO,EAAE,QAAQ,KAAK,kBAAkB,gBAAgB,gBAAgB;AAC1E;AAiBA,eAAe,mBAAsC;AACnD,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,gBAAgB,EAAE,eAAe,KAAK,CAAC;AACrE,WAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,IAAI,CAAC,MAAMA,MAAK,gBAAgB,EAAE,IAAI,CAAC;EACvF,QAAQ;AACN,WAAO,CAAC;EACV;AACF;AAEA,eAAe,cAAiC;AAC9C,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,YAAY,EAAE,eAAe,KAAK,CAAC;AACjE,WAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,IAAI,CAAC,MAAMA,MAAK,YAAY,EAAE,IAAI,CAAC;EACnF,QAAQ;AACN,WAAO,CAAC;EACV;AACF;AAaA,eAAe,0BAA6C;AAC1D,QAAM,IAAI,MAAM;IACd;IACA,CAAC,SAAS,MAAM,YAAY,4BAA4B,GAAG,uBAAuB,GAAG;IACrF,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,EAAE,aAAa,EAAG,QAAO,CAAC;AAC9B,UAAQ,EAAE,UAAU,IACjB,MAAM,IAAI,EACV,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,CAAC,MAAM,EAAE,WAAW,uBAAuB,CAAC;AACxD;AAEA,eAAsB,WAAW,OAAqB,CAAC,GAAyB;AAC9E,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,MAAM,KAAK,OAAO;AAExB,QAAM,EAAE,MAAM,IAAI,MAAM,UAAU;AAGlC,QAAM,cAAc,MAAM,QAAQ;IAChC,MAAM,IAAI,OAAO,OAAO,EAAE,KAAK,GAAG,QAAQ,MAAM,uBAAuB,EAAE,SAAS,EAAE,EAAE;EACxF;AACA,QAAM,iBAAiB,YAAY,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AAIzF,MAAI,mBAA6B,CAAC;AAClC,MAAI,gBAA0B,CAAC;AAC/B,MAAI,kBAA4B,CAAC;AACjC,MAAI,gBAA0B,CAAC;AAC/B,MAAI,yBAAmC,CAAC;AAExC,MAAI,KAAK;AACP,UAAM,iBAAiB,MAAM,uBAAuB;AACpD,UAAM,cAAc,MAAM,oBAAoB;AAC9C,UAAM,mBAAmB,MAAM,iBAAiB;AAChD,UAAM,cAAc,MAAM,YAAY;AACtC,UAAM,uBAAuB,MAAM,wBAAwB;AAK3D,UAAM,uBAAuB,MAAM,wBAAwB;AAE3D,UAAM,iBAAiB,MAAM,OAAO,CAAC,MAAM,CAAC,eAAe,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC;AACrF,UAAM,qBAAqB,oBAAI,IAAY;MACzC,GAAG,eAAe,IAAI,CAAC,MAAM,EAAE,SAAS;;;IAG1C,CAAC;AACD,UAAM,kBAAkB,oBAAI,IAAY;MACtC,GAAG,eACA,IAAI,CAAC,MAAM,EAAE,kBAAkB,EAC/B,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;MACnD,GAAG,eACA,IAAI,CAAC,MAAM,EAAE,kBAAkB,EAC/B,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;MACnD,GAAG,eACA,IAAI,CAAC,MAAM,EAAE,kBAAkB,EAC/B,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;MACnD,GAAG,eACA,IAAI,CAAC,MAAM,EAAE,YAAY,EACzB,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;;;MAGnD;;MAEA;MACA;;;MAGA;IACF,CAAC;AACD,UAAM,oBAAoB,IAAI;MAC5B,eACG;QAAO,CAAC,MACP,OAAO,EAAE,gBAAgB;MAC3B,EACC,IAAI,CAAC,MAAM,EAAE,WAAW;IAC7B;AACA,UAAM,kBAAkB,IAAI,IAAI,eAAe,IAAI,CAAC,MAAM,aAAa,CAAC,CAAC,CAAC;AAQ1E,UAAM,2BAA2B,oBAAI,IAAY;MAC/C,GAAG,eACA,IAAI,CAAC,MAAM,EAAE,eAAe,EAC5B,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;MACnD,GAAG;IACL,CAAC;AACD,uBAAmB,eAAe,OAAO,CAAC,MAAM,CAAC,mBAAmB,IAAI,CAAC,CAAC;AAC1E,oBAAgB,YAAY,OAAO,CAAC,MAAM,CAAC,gBAAgB,IAAI,CAAC,CAAC;AACjE,sBAAkB,iBAAiB,OAAO,CAAC,MAAM,CAAC,kBAAkB,IAAI,CAAC,CAAC;AAC1E,oBAAgB,YAAY,OAAO,CAAC,MAAM,CAAC,gBAAgB,IAAI,CAAC,CAAC;AACjE,6BAAyB,qBAAqB;MAC5C,CAAC,MAAM,CAAC,yBAAyB,IAAI,CAAC;IACxC;EACF;AAEA,MAAI,QAAQ;AACV,WAAO;MACL,gBAAgB,eAAe,IAAI,CAAC,MAAM,EAAE,EAAE;MAC9C,mBAAmB;MACnB,gBAAgB;MAChB,qBAAqB;MACrB,gBAAgB;MAChB,yBAAyB;MACzB,QAAQ;IACV;EACF;AAEA,aAAW,KAAK,eAAgB,OAAM,gBAAgB,EAAE,EAAE;AAC1D,aAAW,KAAK,iBAAkB,OAAM,gBAAgB,CAAC;AACzD,aAAW,KAAK,cAAe,OAAM,aAAa,CAAC;AACnD,aAAW,KAAK,iBAAiB;AAC/B,QAAI;AACF,YAAM,GAAG,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;IAC9C,QAAQ;IAER;EACF;AACA,aAAW,KAAK,eAAe;AAC7B,QAAI;AACF,YAAM,GAAG,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;IAC9C,QAAQ;IAER;EACF;AACA,aAAW,OAAO,wBAAwB;AACxC,UAAM,YAAY,KAAK,EAAE,OAAO,KAAK,CAAC;EACxC;AAOA,MAAI,KAAK;AACP,QAAI;AACF,YAAM,gBAAgB,oBAAoB;IAC5C,QAAQ;IAER;AACA,QAAI;AACF,YAAM,MAAM,UAAU,CAAC,SAAS,MAAM,eAAe,GAAG,EAAE,QAAQ,MAAM,CAAC;IAC3E,QAAQ;IAER;AACA,QAAI;AACF,YAAM,cAAc,kBAAkB;IACxC,QAAQ;IAER;EACF;AAEA,SAAO;IACL,gBAAgB,eAAe,IAAI,CAAC,MAAM,EAAE,EAAE;IAC9C,mBAAmB;IACnB,gBAAgB;IAChB,qBAAqB;IACrB,gBAAgB;IAChB,yBAAyB;IACzB,QAAQ;EACV;AACF;AAQA,eAAsB,gBAAgB,MAAuC;AAC3E,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,IAAI,MAAM,KAAK,IAAI;AACzB,WAAO,EAAE,YAAY;EACvB,QAAQ;AACN,WAAO;EACT;AACF;","names":["join","join"]}
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../packages/sandbox-docker/dist/chunk-A72AUJNS.js
3
+ // ../../packages/sandbox-docker/dist/chunk-JHUIWLIF.js
4
4
  import { mkdir, readFile, writeFile } from "fs/promises";
5
5
  import { homedir } from "os";
6
6
  import { dirname, join } from "path";
@@ -103,4 +103,4 @@ export {
103
103
  autoPickProjectBox,
104
104
  resolveBoxRef
105
105
  };
106
- //# sourceMappingURL=chunk-IDR4HVIC.js.map
106
+ //# sourceMappingURL=chunk-HPZMD5DE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../packages/sandbox-docker/src/state.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\nexport const STATE_DIR = join(homedir(), '.agentbox');\nexport const STATE_FILE = join(STATE_DIR, 'state.json');\n\nexport interface BoxRecord {\n id: string;\n name: string;\n container: string;\n /**\n * The image the box was started from. For plain boxes this is\n * `agentbox/box:dev` (the base image); for boxes started from a checkpoint\n * it's the checkpoint image tag (and `checkpointImage` mirrors it). Used by\n * `inspect`, by `prune --all` to know which checkpoint images are still in\n * use, and by `agentbox self-update` to know which image to wipe.\n */\n image: string;\n workspacePath: string;\n /**\n * Optional per-box scratch dir holding a `cp -c` APFS clone of the host\n * workspace, made at create time when `--host-snapshot` is on. The clone is\n * the source of the tar pipe into the container's `/workspace`; absent when\n * the option wasn't used. Removed on `destroyBox` (the named-volume model is\n * gone, so this is the only host-side scratch left).\n */\n snapshotDir?: string | null;\n /**\n * Host-side path to the agentbox-ctl unix socket bind-mounted into the\n * container at /run/agentbox/ctl.sock. Absent for boxes created before this\n * field existed (treated as \"ctl not available\").\n */\n socketPath?: string;\n /**\n * Docker volume mounted at /home/vscode/.claude inside the box. The default\n * shared volume (`agentbox-claude-config`) is reused across boxes; isolated\n * boxes get a per-box volume suffixed with the box id. Absent for boxes\n * created before this field existed.\n */\n claudeConfigVolume?: string;\n /**\n * Per-box volume holding `.vscode-server` (server binary + TS cache).\n * The shared `agentbox-vscode-extensions` volume layers over the `extensions`\n * subdir at run time and isn't recorded here (never auto-removed). Absent\n * for boxes created before this field existed.\n */\n vscodeServerVolume?: string;\n /**\n * Per-box volume holding `.cursor-server` (Cursor server binary + state).\n * Parallel to `vscodeServerVolume`. Absent for boxes created before this\n * field existed — lifecycle code falls back to deriving from `id`.\n */\n cursorServerVolume?: string;\n /**\n * Bearer token the in-box supervisor uses to authenticate with the host\n * relay. Generated at create time and forwarded as AGENTBOX_RELAY_TOKEN.\n * Absent for boxes created before the relay existed — those boxes simply\n * skip outbound push.\n */\n relayToken?: string;\n /**\n * Per-box git worktrees created inside the container against the bind-mounted\n * main `.git/` (`git worktree add -b agentbox/<name> /workspace HEAD` from\n * `seedWorkspace`). Empty/absent when the host workspace is not a git\n * checkout. The root entry (kind: 'root') is the worktree at /workspace;\n * nested entries (kind: 'nested', from monorepo 1st-level `.git` dirs) live\n * at /workspace/<relPathFromWorkspace>. The host has no per-box worktree\n * dir under ~/.agentbox/boxes/<id>/worktrees/ — `git push` runs in the\n * shared host main repo against `branch` (the relay's git RPC).\n */\n gitWorktrees?: GitWorktreeRecord[];\n /**\n * True when the box was created with --with-playwright. The install happens\n * once at create time (npm install -g @playwright/cli@latest inside the\n * container); we record the choice for `agentbox inspect` visibility. Absent\n * on boxes created before this field existed (treated as false).\n */\n withPlaywright?: boolean;\n /**\n * True when the box was created with --with-env. The host's env/config files\n * (DEFAULT_ENV_PATTERNS) were copied into /workspace once at create time,\n * bypassing gitignore; recorded for `agentbox inspect` visibility. Absent on\n * boxes created before this field existed (treated as false).\n */\n withEnv?: boolean;\n /**\n * VNC stack (Xvnc + websockify + noVNC) is enabled for this box. Absent on\n * boxes created before VNC support landed → treated as disabled.\n */\n vncEnabled?: boolean;\n /** Container-side noVNC web port. Fixed to 6080 today; here for future-proofing. */\n vncContainerPort?: number;\n /** Random host port Docker assigned to the noVNC web server (resolved via `docker port`). */\n vncHostPort?: number;\n /** Per-box password baked into Xvnc's PasswordFile and embedded in the auto-connect URL. */\n vncPassword?: string;\n /**\n * Container port reserved for the web service `expose:` forward. Fixed to 80\n * today; the `-p` mapping is created unconditionally at `create`. Absent on\n * boxes created before web-port reservation landed → no web endpoint until\n * the box is recreated.\n */\n webContainerPort?: number;\n /** Random host port Docker assigned to container :80 (resolved via `docker port`). */\n webHostPort?: number;\n /**\n * Volume mounted at /var/lib/docker for the in-box dockerd. Per-box\n * (`agentbox-docker-<id>`) by default; the shared `agentbox-docker-cache`\n * volume when `dockerCacheShared` is true. Absent on boxes created before\n * DinD landed — those boxes have no in-box dockerd at all.\n */\n dockerVolume?: string;\n /**\n * True when this box's `dockerVolume` is the shared cache. Tells `destroyBox`\n * to skip removal (the shared volume holds image layers other boxes may\n * reuse) and `pruneBoxes --all` to allowlist it.\n */\n dockerCacheShared?: boolean;\n /**\n * Absolute host path of the project this box belongs to. Set by `createBox`\n * from the CLI-supplied `findProjectRoot(workspacePath)` (nearest ancestor\n * dir holding `agentbox.yaml`, else workspacePath itself). Used by\n * `resolveBoxRef` + `autoPickProjectBox` to scope numeric refs and auto-pick\n * to the cwd's project. Absent on boxes created before this field existed —\n * those boxes are never auto-picked or matched by numeric index.\n */\n projectRoot?: string;\n /**\n * Monotonic 1-based index within `projectRoot`. Allocated once at create via\n * `allocateProjectIndex` and never recycled — destroying box #2 leaves a gap\n * (next new box is #3, not #2). Lets `agentbox open 3` mean the same box for\n * that box's whole lifetime.\n */\n projectIndex?: number;\n /**\n * The checkpoint image tag this box was started from (when `--snapshot <ref>`\n * resolved to a checkpoint). Mirrors `image`. Absent on plain boxes. Used by\n * `prune --all` to know which `agentbox-ckpt-*` image tags are still in use\n * by a live box, and by `inspect`/`status` for lineage display.\n */\n checkpointImage?: string;\n /**\n * Lineage of the checkpoint this box was started from. Drives chain-depth\n * (auto-flatten threshold) and `agentbox inspect`. Absent when the box was\n * not created from a checkpoint. `chain` is base-most last; for a flattened\n * checkpoint it's a single entry.\n */\n checkpointSource?: {\n ref: string;\n type: 'layered' | 'flattened';\n /** Checkpoint refs composing the chain, base-most last. */\n chain: string[];\n };\n /**\n * Resource ceilings actually applied at `docker run` (bytes/fractional/count;\n * the `disk` string only present when the engine's storage driver enforces\n * it — dropped + warned on overlay2/macOS). Absent on legacy boxes → treated\n * as unlimited. Surfaced by `agentbox inspect` and cross-checked by\n * `boxResourceStats`.\n */\n resourceLimits?: {\n memoryBytes?: number;\n cpus?: number;\n pidsLimit?: number;\n disk?: string;\n };\n createdAt: string; // ISO-8601\n}\n\nexport interface GitWorktreeRecord {\n kind: 'root' | 'nested';\n /** Host path to the main repo whose `.git/` is bind-mounted RW at the identical path inside the container. */\n hostMainRepo: string;\n /**\n * Agent-visible container path of the worktree (`/workspace` for root,\n * `/workspace/<sub>` for nested). After `seedWorkspace` runs this is a\n * symlink to `gitWorktreePath`.\n */\n containerPath: string;\n /**\n * Per-box unique path where git registered the worktree\n * (`/home/vscode/.agentbox-worktrees/<fsSafeBranch>`). Load-bearing: the\n * host main repo's worktree registry is keyed by absolute path, so multiple\n * concurrent boxes in the same project must register at *different* paths\n * even though they all expose `/workspace` to the agent. `destroyBox` uses\n * this path to deregister the worktree on the host.\n */\n gitWorktreePath: string;\n /** Branch the worktree was created on, e.g. `agentbox/<box-name>`. The relay's `git.push`/`git.fetch` runs against this branch in the shared host `.git`. */\n branch: string;\n /** Workspace-relative path the repo was found at (empty string for root). */\n relPathFromWorkspace: string;\n}\n\nexport interface StateFile {\n version: 1;\n boxes: BoxRecord[];\n}\n\nconst EMPTY: StateFile = { version: 1, boxes: [] };\n\nexport async function readState(path: string = STATE_FILE): Promise<StateFile> {\n try {\n const raw = await readFile(path, 'utf8');\n const parsed = JSON.parse(raw) as StateFile;\n if (parsed.version !== 1 || !Array.isArray(parsed.boxes)) {\n throw new Error(`unrecognized state file shape at ${path}`);\n }\n return parsed;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return { ...EMPTY };\n }\n throw err;\n }\n}\n\nexport async function writeState(state: StateFile, path: string = STATE_FILE): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, JSON.stringify(state, null, 2) + '\\n', 'utf8');\n}\n\nexport async function recordBox(box: BoxRecord, path: string = STATE_FILE): Promise<void> {\n const state = await readState(path);\n const next: StateFile = {\n version: 1,\n boxes: [...state.boxes.filter((b) => b.id !== box.id), box],\n };\n await writeState(next, path);\n}\n\nexport async function removeBoxRecord(id: string, path: string = STATE_FILE): Promise<boolean> {\n const state = await readState(path);\n const before = state.boxes.length;\n const next: StateFile = {\n version: 1,\n boxes: state.boxes.filter((b) => b.id !== id),\n };\n if (next.boxes.length === before) return false;\n await writeState(next, path);\n return true;\n}\n\nexport type FindBoxResult =\n | { kind: 'ok'; box: BoxRecord }\n | { kind: 'none' }\n | { kind: 'ambiguous'; matches: BoxRecord[] };\n\n/**\n * Resolve a user-supplied identifier against the state file. Matching\n * precedence mirrors `docker`'s container reference resolution:\n *\n * 1. exact id\n * 2. unique id prefix\n * 3. exact name\n * 4. exact container name\n *\n * Returns `'ambiguous'` if step 2 finds more than one match (steps 1, 3, 4\n * are exact-match so they cannot be ambiguous on their own).\n */\nexport function findBox(idOrName: string, state: StateFile): FindBoxResult {\n const q = idOrName.trim();\n if (q.length === 0) return { kind: 'none' };\n\n const exactId = state.boxes.find((b) => b.id === q);\n if (exactId) return { kind: 'ok', box: exactId };\n\n const prefixMatches = state.boxes.filter((b) => b.id.startsWith(q));\n if (prefixMatches.length === 1) return { kind: 'ok', box: prefixMatches[0]! };\n if (prefixMatches.length > 1) return { kind: 'ambiguous', matches: prefixMatches };\n\n const byName = state.boxes.find((b) => b.name === q);\n if (byName) return { kind: 'ok', box: byName };\n\n const byContainer = state.boxes.find((b) => b.container === q);\n if (byContainer) return { kind: 'ok', box: byContainer };\n\n return { kind: 'none' };\n}\n\n/**\n * Next monotonic 1-based index for the given project. Reads only `state.boxes`\n * — caller is responsible for persisting the assignment. Boxes without\n * `projectRoot` are ignored (legacy records); boxes in *other* projects are\n * also ignored. Indices are never recycled, so a destroyed #2 leaves a gap.\n */\nexport function allocateProjectIndex(state: StateFile, projectRoot: string): number {\n let max = 0;\n for (const b of state.boxes) {\n if (b.projectRoot !== projectRoot) continue;\n if (typeof b.projectIndex === 'number' && b.projectIndex > max) {\n max = b.projectIndex;\n }\n }\n return max + 1;\n}\n\n/**\n * Auto-pick when a command's `[box]` argument is omitted. Returns the unique\n * box for `projectRoot`, an `ambiguous` carrying all candidates so the CLI can\n * print a chooser, or `none`.\n */\nexport function autoPickProjectBox(state: StateFile, projectRoot: string): FindBoxResult {\n const matches = state.boxes.filter((b) => b.projectRoot === projectRoot);\n if (matches.length === 0) return { kind: 'none' };\n if (matches.length === 1) return { kind: 'ok', box: matches[0]! };\n return { kind: 'ambiguous', matches };\n}\n\n/**\n * Top-level resolver every CLI command goes through. Combines numeric-index\n * lookup with the legacy `findBox` matcher:\n *\n * - `ref === undefined` and `projectRoot` known → autoPickProjectBox.\n * - `ref` is a pure positive integer and `projectRoot` known → resolve as\n * project index. **Never** falls through to `findBox` on miss, so\n * `agentbox open 3` is reserved for the index and won't accidentally\n * match a hex id like `3abc…`.\n * - Otherwise → `findBox` (id → prefix → name → container).\n */\nexport function resolveBoxRef(\n ref: string | undefined,\n state: StateFile,\n projectRoot: string | undefined,\n): FindBoxResult {\n if (ref === undefined) {\n if (projectRoot === undefined) return { kind: 'none' };\n return autoPickProjectBox(state, projectRoot);\n }\n const trimmed = ref.trim();\n if (projectRoot !== undefined && /^[1-9][0-9]*$/.test(trimmed)) {\n const idx = Number.parseInt(trimmed, 10);\n const hit = state.boxes.find(\n (b) => b.projectRoot === projectRoot && b.projectIndex === idx,\n );\n return hit ? { kind: 'ok', box: hit } : { kind: 'none' };\n }\n return findBox(trimmed, state);\n}\n"],"mappings":"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","names":[]}