@madarco/agentbox 0.1.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 (33) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-IDR4HVIC.js +106 -0
  3. package/dist/chunk-IDR4HVIC.js.map +1 -0
  4. package/dist/chunk-J35IH7W5.js +200 -0
  5. package/dist/chunk-J35IH7W5.js.map +1 -0
  6. package/dist/chunk-O5HS3QHW.js +2164 -0
  7. package/dist/chunk-O5HS3QHW.js.map +1 -0
  8. package/dist/chunk-OOOKFFR5.js +496 -0
  9. package/dist/chunk-OOOKFFR5.js.map +1 -0
  10. package/dist/chunk-RWJE6AER.js +515 -0
  11. package/dist/chunk-RWJE6AER.js.map +1 -0
  12. package/dist/chunk-SOMIKEN2.js +1651 -0
  13. package/dist/chunk-SOMIKEN2.js.map +1 -0
  14. package/dist/create-LSSO7H4I-GWNALUMF.js +15 -0
  15. package/dist/create-LSSO7H4I-GWNALUMF.js.map +1 -0
  16. package/dist/index.js +4067 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lifecycle-P4FSKGR2-3466P54Y.js +38 -0
  19. package/dist/lifecycle-P4FSKGR2-3466P54Y.js.map +1 -0
  20. package/dist/state-ZSP3ORXW-WI6KOIG3.js +26 -0
  21. package/dist/state-ZSP3ORXW-WI6KOIG3.js.map +1 -0
  22. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +19 -0
  23. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js.map +1 -0
  24. package/package.json +73 -0
  25. package/runtime/docker/Dockerfile.box +316 -0
  26. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +188 -0
  27. package/runtime/docker/packages/ctl/dist/bin.cjs +12770 -0
  28. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +52 -0
  29. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +77 -0
  30. package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +54 -0
  31. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +21 -0
  32. package/runtime/relay/bin.cjs +11467 -0
  33. package/share/agentbox-setup/SKILL.md +188 -0
package/dist/index.js ADDED
@@ -0,0 +1,4067 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createBox
4
+ } from "./chunk-OOOKFFR5.js";
5
+ import {
6
+ AmbiguousBoxError,
7
+ BoxNotFoundError,
8
+ destroyBox,
9
+ getBoxHostPaths,
10
+ inspectBox,
11
+ listBoxes,
12
+ openBoxInFinder,
13
+ pauseBox,
14
+ pruneBoxes,
15
+ startBox,
16
+ stopBox,
17
+ unpauseBox
18
+ } from "./chunk-RWJE6AER.js";
19
+ import {
20
+ ClaudeSessionError,
21
+ SHARED_CLAUDE_VOLUME,
22
+ attachClaudeSession,
23
+ buildClaudeDashboardAttachArgv,
24
+ buildShellArgv,
25
+ buildVncUrls,
26
+ claudeSessionInfo,
27
+ containerHex,
28
+ ensureAgentboxTasksFile,
29
+ ensureClaudeVolume,
30
+ ensureRelay,
31
+ ideProfile,
32
+ pullClaudeExtras,
33
+ rebuildPluginNativeDeps,
34
+ renderPortsTable,
35
+ renderStatusTable,
36
+ renderTaskTable,
37
+ resolveClaudeVolume,
38
+ startClaudeSession,
39
+ stopRelay
40
+ } from "./chunk-O5HS3QHW.js";
41
+ import {
42
+ STATE_DIR,
43
+ readState,
44
+ resolveBoxRef
45
+ } from "./chunk-IDR4HVIC.js";
46
+ import {
47
+ agentboxHomeBytes,
48
+ allCheckpointVolumesBytes,
49
+ boxResourceStats,
50
+ projectCheckpointVolumeBytes
51
+ } from "./chunk-J35IH7W5.js";
52
+ import {
53
+ DEFAULT_BOX_IMAGE,
54
+ DEFAULT_ENV_PATTERNS,
55
+ KEY_REGISTRY,
56
+ UserConfigError,
57
+ bumpProjectGcCounter,
58
+ configPathFor,
59
+ createCheckpoint,
60
+ detectEngine,
61
+ execInBox,
62
+ findProjectRoot,
63
+ listCheckpoints,
64
+ listProjectsConfigured,
65
+ loadEffectiveConfig,
66
+ lookupKey,
67
+ pruneOrphanProjectConfigs,
68
+ pullToHost,
69
+ refreshExport,
70
+ removeCheckpoint,
71
+ removeImage,
72
+ setConfigValue,
73
+ setEngineOverride,
74
+ unsetConfigValue
75
+ } from "./chunk-SOMIKEN2.js";
76
+
77
+ // src/index.ts
78
+ import { Command as Command27 } from "commander";
79
+
80
+ // ../../packages/sandbox-docker/dist/index.js
81
+ function browserSessionActive(stdout, exitCode) {
82
+ return exitCode === 0 && !/no active sessions/i.test(stdout);
83
+ }
84
+ async function ensureBoxBrowser(container, timeoutMs = 8e3) {
85
+ const list = await execInBox(container, ["agent-browser", "session", "list"], {
86
+ user: "vscode",
87
+ timeoutMs
88
+ });
89
+ if (browserSessionActive(list.stdout, list.exitCode)) {
90
+ return { up: true, alreadyRunning: true };
91
+ }
92
+ const open = await execInBox(container, ["agent-browser", "open", "--headed", "about:blank"], {
93
+ user: "vscode",
94
+ timeoutMs
95
+ });
96
+ if (open.exitCode === 0) return { up: true };
97
+ return {
98
+ up: false,
99
+ reason: `agent-browser open failed: ${open.stderr || open.stdout || `exit ${String(open.exitCode)}`}`
100
+ };
101
+ }
102
+
103
+ // src/engine-override.ts
104
+ async function applyEngineOverrideAtStartup() {
105
+ try {
106
+ const loaded = await loadEffectiveConfig(process.cwd());
107
+ const kind = loaded.effective.engine.kind;
108
+ if (kind === "auto") return;
109
+ setEngineOverride(kind);
110
+ } catch {
111
+ }
112
+ }
113
+
114
+ // src/help.ts
115
+ var HELP_GROUPS = [
116
+ { title: "Create & run", commands: ["create", "claude"] },
117
+ {
118
+ title: "Access",
119
+ commands: ["dashboard", "browser", "screen", "code", "shell", "open", "logs"]
120
+ },
121
+ { title: "Inspect", commands: ["list", "status", "top"] },
122
+ { title: "Lifecycle", commands: ["start", "stop", "destroy", "pause", "unpause"] },
123
+ { title: "Sync & state", commands: ["pull", "checkpoint"] },
124
+ {
125
+ title: "Advanced",
126
+ commands: ["wait", "prune", "self-update", "config"]
127
+ }
128
+ ];
129
+ function term(cmd) {
130
+ const aliases = cmd.aliases();
131
+ return aliases.length ? `${cmd.name()}|${aliases.join("|")}` : cmd.name();
132
+ }
133
+ function buildGroupedHelp(program2) {
134
+ const byName = new Map(program2.commands.map((c) => [c.name(), c]));
135
+ const grouped = new Set(HELP_GROUPS.flatMap((g) => g.commands));
136
+ const orphans = program2.commands.map((c) => c.name()).filter((n) => !grouped.has(n));
137
+ const groups = [...HELP_GROUPS];
138
+ if (orphans.length) groups.push({ title: "Other", commands: orphans });
139
+ const terms = [];
140
+ for (const g of groups) {
141
+ for (const name of g.commands) {
142
+ const cmd = byName.get(name);
143
+ if (cmd) terms.push(term(cmd));
144
+ }
145
+ }
146
+ const pad = Math.max(0, ...terms.map((t) => t.length)) + 2;
147
+ const lines = ["Commands:"];
148
+ for (const g of groups) {
149
+ const title = g.hint ? `${g.title} (${g.hint})` : g.title;
150
+ lines.push("", ` ${title}`);
151
+ for (const name of g.commands) {
152
+ const cmd = byName.get(name);
153
+ if (!cmd) continue;
154
+ lines.push(` ${term(cmd).padEnd(pad)}${cmd.description()}`);
155
+ }
156
+ }
157
+ lines.push("", "Run `agentbox <command> --help` for command-specific options.");
158
+ return lines.join("\n");
159
+ }
160
+
161
+ // src/commands/browser.ts
162
+ import { spawnSync } from "child_process";
163
+ import { log as log3 } from "@clack/prompts";
164
+ import { Command } from "commander";
165
+
166
+ // src/box-ref.ts
167
+ import { log } from "@clack/prompts";
168
+ async function resolveBoxOrShift(ref, opts = {}) {
169
+ const cwd = opts.cwd ?? process.cwd();
170
+ const project = await findProjectRoot(cwd);
171
+ const state = await readState();
172
+ const firstTry = resolveBoxRef(ref, state, project.root);
173
+ if (firstTry.kind === "ok") return { box: firstTry.box, shifted: false };
174
+ if (ref !== void 0) {
175
+ const pick = resolveBoxRef(void 0, state, project.root);
176
+ if (pick.kind === "ok") return { box: pick.box, shifted: true };
177
+ if (pick.kind === "ambiguous") {
178
+ log.error(`multiple boxes in this project \u2014 pick one:`);
179
+ for (const b of pick.matches) {
180
+ const idx = typeof b.projectIndex === "number" ? `${String(b.projectIndex)})` : " -)";
181
+ process.stderr.write(` ${idx} ${b.name} (id ${b.id})
182
+ `);
183
+ }
184
+ log.info("try: agentbox <cmd> <n> -- <args> (or use the box name / id prefix)");
185
+ process.exit(2);
186
+ }
187
+ }
188
+ const box = await resolveBoxOrExit(ref, opts);
189
+ return { box, shifted: false };
190
+ }
191
+ async function resolveBoxOrExit(ref, opts = {}) {
192
+ const cwd = opts.cwd ?? process.cwd();
193
+ const project = await findProjectRoot(cwd);
194
+ const state = await readState();
195
+ const result = resolveBoxRef(ref, state, project.root);
196
+ if (result.kind === "ok") return result.box;
197
+ if (result.kind === "ambiguous") {
198
+ if (ref === void 0) {
199
+ log.error(`multiple boxes in this project \u2014 pick one:`);
200
+ for (const b of result.matches) {
201
+ const idx = typeof b.projectIndex === "number" ? `${String(b.projectIndex)})` : " -)";
202
+ process.stderr.write(` ${idx} ${b.name} (id ${b.id})
203
+ `);
204
+ }
205
+ log.info("try: agentbox <cmd> <n> (or use the box name / id prefix)");
206
+ process.exit(2);
207
+ }
208
+ throw new AmbiguousBoxError(ref, result.matches);
209
+ }
210
+ if (ref === void 0) {
211
+ log.error(`no boxes in this project (${project.root})`);
212
+ log.info("run `agentbox create` to make one, or pass a box ref explicitly");
213
+ process.exit(2);
214
+ }
215
+ if (/^[1-9][0-9]*$/.test(ref.trim())) {
216
+ log.error(`no box with index ${ref.trim()} in this project (${project.root})`);
217
+ log.info("run `agentbox list` to see available indices");
218
+ process.exit(2);
219
+ }
220
+ throw new BoxNotFoundError(ref);
221
+ }
222
+
223
+ // src/commands/_errors.ts
224
+ import { log as log2 } from "@clack/prompts";
225
+ function handleLifecycleError(err) {
226
+ if (err instanceof BoxNotFoundError) {
227
+ log2.error(err.message);
228
+ log2.info("Run `agentbox list` to see available boxes.");
229
+ process.exit(2);
230
+ }
231
+ if (err instanceof AmbiguousBoxError) {
232
+ log2.error(err.message);
233
+ log2.info("Specify more characters of the id, or use the full name.");
234
+ process.exit(2);
235
+ }
236
+ if (err instanceof ClaudeSessionError) {
237
+ log2.error(err.message);
238
+ process.exit(2);
239
+ }
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ log2.error(msg);
242
+ process.exit(1);
243
+ }
244
+
245
+ // src/commands/browser.ts
246
+ var browserCommand = new Command("browser").description(
247
+ "Open a box's web app URL in the browser, even when no service declares `expose:` (auto-unpause/start)"
248
+ ).argument(
249
+ "[box]",
250
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
251
+ ).option("--print", "print the URL to stdout instead of launching the browser").option("--loopback", "use the 127.0.0.1 URL instead of the OrbStack .orb.local URL").action(async (idOrName, opts) => {
252
+ try {
253
+ const box = await resolveBoxOrExit(idOrName);
254
+ const insp = await inspectBox(box.id);
255
+ if (insp.state === "paused") {
256
+ log3.info("box is paused; unpausing");
257
+ await unpauseBox(box.id);
258
+ } else if (insp.state === "stopped") {
259
+ log3.info("box is stopped; starting (remounting overlay)");
260
+ await startBox(box.id);
261
+ } else if (insp.state === "missing") {
262
+ throw new Error(`box ${box.name} has no container; was it destroyed?`);
263
+ }
264
+ const { record } = await getBoxHostPaths(box.id);
265
+ if (record.webContainerPort === void 0) {
266
+ throw new Error(
267
+ `box ${box.name} predates the reserved web port; recreate it to use \`agentbox browser\``
268
+ );
269
+ }
270
+ const engine = await detectEngine();
271
+ let url;
272
+ if (engine === "orbstack" && !opts.loopback) {
273
+ url = `http://${record.container}.orb.local`;
274
+ } else {
275
+ if (record.webHostPort === void 0) {
276
+ throw new Error(
277
+ `web port not resolved for box ${box.name}; is the container running? try \`agentbox inspect ${box.name}\``
278
+ );
279
+ }
280
+ url = `http://127.0.0.1:${String(record.webHostPort)}`;
281
+ }
282
+ if (opts.print) {
283
+ process.stdout.write(`${url}
284
+ `);
285
+ return;
286
+ }
287
+ const opened = spawnSync("open", [url], { stdio: "inherit" });
288
+ if (opened.status !== 0) {
289
+ throw new Error(`open ${url} failed (exit ${String(opened.status ?? "n/a")})`);
290
+ }
291
+ process.stdout.write(`opened ${url}
292
+ `);
293
+ } catch (err) {
294
+ handleLifecycleError(err);
295
+ }
296
+ });
297
+
298
+ // src/commands/claude.ts
299
+ import { confirm as confirm2, intro, isCancel as isCancel2, log as log5, outro, password, spinner } from "@clack/prompts";
300
+ import { Command as Command2 } from "commander";
301
+
302
+ // src/auth.ts
303
+ import { spawnSync as spawnSync2 } from "child_process";
304
+ import { mkdir, readFile, writeFile } from "fs/promises";
305
+ import { dirname, join } from "path";
306
+ var AUTH_FILE = join(STATE_DIR, "auth.json");
307
+ async function resolveClaudeAuth(processEnv, opts = {}) {
308
+ const env = {};
309
+ const envApiKey = processEnv["ANTHROPIC_API_KEY"];
310
+ const envOauth = processEnv["CLAUDE_CODE_OAUTH_TOKEN"];
311
+ if (typeof envApiKey === "string" && envApiKey.length > 0) env["ANTHROPIC_API_KEY"] = envApiKey;
312
+ if (typeof envOauth === "string" && envOauth.length > 0) env["CLAUDE_CODE_OAUTH_TOKEN"] = envOauth;
313
+ if (Object.keys(env).length > 0) return { env, source: "host-env" };
314
+ const file = await readAuthFile(opts.authFilePath);
315
+ if (file.claudeCodeOauthToken && file.claudeCodeOauthToken.length > 0) {
316
+ return {
317
+ env: { CLAUDE_CODE_OAUTH_TOKEN: file.claudeCodeOauthToken },
318
+ source: "auth-file"
319
+ };
320
+ }
321
+ return { env: {}, source: "none" };
322
+ }
323
+ async function readAuthFile(path = AUTH_FILE) {
324
+ try {
325
+ const raw = await readFile(path, "utf8");
326
+ const parsed = JSON.parse(raw);
327
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
328
+ const t = parsed.claudeCodeOauthToken;
329
+ return typeof t === "string" && t.length > 0 ? { claudeCodeOauthToken: t } : {};
330
+ }
331
+ return {};
332
+ } catch (err) {
333
+ if (err.code === "ENOENT") return {};
334
+ return {};
335
+ }
336
+ }
337
+ async function writeAuthFile(next, path = AUTH_FILE) {
338
+ await mkdir(dirname(path), { recursive: true });
339
+ await writeFile(path, JSON.stringify(next, null, 2) + "\n", { mode: 384, flag: "w" });
340
+ }
341
+ function hostClaudeAvailable() {
342
+ const r = spawnSync2("which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] });
343
+ return r.status === 0 && (r.stdout?.toString().trim().length ?? 0) > 0;
344
+ }
345
+ function runHostSetupToken() {
346
+ const child = spawnSync2("claude", ["setup-token"], { stdio: "inherit" });
347
+ return { exitCode: child.status ?? -1 };
348
+ }
349
+ function isPlausibleOauthToken(s) {
350
+ const t = s.trim();
351
+ return t.startsWith("sk-ant-oat") && t.length >= 40;
352
+ }
353
+
354
+ // ../../packages/core/dist/index.js
355
+ var claudeCodeLauncher = {
356
+ kind: "claude-code",
357
+ // claude treats its first positional argument as the seed user turn in
358
+ // interactive mode (`claude "<message>"`), so we slot the initial message
359
+ // ahead of any user-passed flags.
360
+ buildArgs(initialMessage, userArgs) {
361
+ if (!initialMessage) return [...userArgs];
362
+ return [initialMessage, ...userArgs];
363
+ }
364
+ };
365
+ var codexLauncher = {
366
+ kind: "codex",
367
+ buildArgs() {
368
+ throw new Error(
369
+ "codex agent is not yet supported by agentbox; install + wire the codex launcher first"
370
+ );
371
+ }
372
+ };
373
+ function resolveAgentLauncher(kind) {
374
+ if (kind === "claude-code") return claudeCodeLauncher;
375
+ if (kind === "codex") return codexLauncher;
376
+ throw new Error(`unknown agent kind: ${String(kind)}`);
377
+ }
378
+
379
+ // src/spinner-line.ts
380
+ var SPINNER_CHROME = 6;
381
+ function clampSpinnerLine(line) {
382
+ const cols = process.stdout.columns;
383
+ if (!process.stdout.isTTY || !cols) return line;
384
+ const trimmed = line.replace(/\s+$/, "");
385
+ const max = cols - SPINNER_CHROME;
386
+ if (max <= 1 || trimmed.length <= max) return trimmed;
387
+ return `${trimmed.slice(0, max - 1)}\u2026`;
388
+ }
389
+
390
+ // src/limits.ts
391
+ function parseMemoryToBytes(raw) {
392
+ const m = /^\s*([\d.]+)\s*([bkmg]?)b?\s*$/i.exec(raw);
393
+ if (!m) throw new Error(`invalid --memory value "${raw}" (try e.g. 512m, 2g)`);
394
+ const n = Number(m[1]);
395
+ if (!Number.isFinite(n) || n < 0) {
396
+ throw new Error(`invalid --memory value "${raw}"`);
397
+ }
398
+ const unit = (m[2] ?? "").toLowerCase();
399
+ const factor = unit === "k" ? 1024 : unit === "m" ? 1024 ** 2 : unit === "g" ? 1024 ** 3 : 1;
400
+ return Math.floor(n * factor);
401
+ }
402
+ var MIB = 1024 * 1024;
403
+ function resolveLimits(box, flags) {
404
+ let memoryBytes = box.memory > 0 ? box.memory * MIB : null;
405
+ if (flags.memory !== void 0 && flags.memory !== "") {
406
+ memoryBytes = parseMemoryToBytes(flags.memory);
407
+ }
408
+ let cpus = box.cpus > 0 ? box.cpus : null;
409
+ if (flags.cpus !== void 0 && flags.cpus !== "") {
410
+ const n = Number(flags.cpus);
411
+ if (!Number.isFinite(n) || n < 0) {
412
+ throw new Error(`invalid --cpus value "${flags.cpus}"`);
413
+ }
414
+ cpus = n > 0 ? n : null;
415
+ }
416
+ let pidsLimit = box.pidsLimit > 0 ? box.pidsLimit : null;
417
+ if (flags.pidsLimit !== void 0 && flags.pidsLimit !== "") {
418
+ const n = Number(flags.pidsLimit);
419
+ if (!Number.isInteger(n) || n < 0) {
420
+ throw new Error(`invalid --pids-limit value "${flags.pidsLimit}"`);
421
+ }
422
+ pidsLimit = n > 0 ? n : null;
423
+ }
424
+ let disk = box.disk ? box.disk : null;
425
+ if (flags.disk !== void 0 && flags.disk !== "") disk = flags.disk;
426
+ return { memoryBytes, cpus, pidsLimit, disk };
427
+ }
428
+
429
+ // src/wizard.ts
430
+ import { confirm, isCancel, log as log4 } from "@clack/prompts";
431
+ import { copyFile, mkdir as mkdir2, stat } from "fs/promises";
432
+ import { homedir } from "os";
433
+ import { basename, join as join2 } from "path";
434
+ import { fileURLToPath } from "url";
435
+ var IN_BOX_SETUP_GUIDE_PATH = "/usr/local/share/agentbox/setup-guide.md";
436
+ var HOST_SKILLS_DIR = join2(homedir(), ".claude", "skills", "agentbox-setup");
437
+ var HOST_SKILL_FILE = join2(HOST_SKILLS_DIR, "SKILL.md");
438
+ function bundledSkillPath() {
439
+ return fileURLToPath(new URL("../share/agentbox-setup/SKILL.md", import.meta.url));
440
+ }
441
+ async function fileExists(p) {
442
+ try {
443
+ const st = await stat(p);
444
+ return st.isFile();
445
+ } catch {
446
+ return false;
447
+ }
448
+ }
449
+ async function installAgentboxSetupSkill(opts = {}) {
450
+ const targetFile = opts.targetFile ?? HOST_SKILL_FILE;
451
+ const targetDir = join2(targetFile, "..");
452
+ if (await fileExists(targetFile)) return { installed: false, targetFile };
453
+ const src = opts.sourceFile ?? bundledSkillPath();
454
+ if (!await fileExists(src)) {
455
+ return { installed: false, targetFile };
456
+ }
457
+ await mkdir2(targetDir, { recursive: true, mode: 448 });
458
+ await copyFile(src, targetFile);
459
+ return { installed: true, targetFile };
460
+ }
461
+ function buildSetupInitialPrompt(workspace) {
462
+ const name = basename(workspace);
463
+ return `The user just opened a new agentbox sandbox for "${name}" but the workspace has no agentbox.yaml yet. Please run the /agentbox-setup skill (or read ${IN_BOX_SETUP_GUIDE_PATH} if the skill is not loaded), then explore /workspace and propose an agentbox.yaml. Save the file to /workspace/agentbox.yaml. Then run \`agentbox-ctl reload\` from inside the box so the already-running supervisor applies the new config and immediately runs the declared tasks and autostarts the services (no box restart needed). When done, summarise what services and tasks you declared, and remind the user how to land the file on the host (commit through the bind-mounted .git, or "agentbox pull env" on the host).`;
464
+ }
465
+ var WIZARD_AUTOLAUNCH_ENV = "AGENTBOX_WIZARD_AUTOLAUNCH";
466
+ async function maybeRunSetupWizard(args) {
467
+ if (process.env[WIZARD_AUTOLAUNCH_ENV] === "1") {
468
+ if (args.command !== "claude") return { action: "proceed" };
469
+ if (args.checkpointRef) return { action: "proceed" };
470
+ const proj2 = await findProjectRoot(args.workspace);
471
+ if (proj2.hasAgentboxYaml) return { action: "proceed" };
472
+ return {
473
+ action: "launch-with-prompt",
474
+ initialPrompt: buildSetupInitialPrompt(proj2.root)
475
+ };
476
+ }
477
+ if (args.yes) return { action: "proceed" };
478
+ if (!process.stdin.isTTY) return { action: "proceed" };
479
+ const proj = await findProjectRoot(args.workspace);
480
+ if (proj.hasAgentboxYaml) return { action: "proceed" };
481
+ if (args.checkpointRef) {
482
+ log4.info(`starting from checkpoint "${args.checkpointRef}"; skipping agentbox.yaml setup`);
483
+ return { action: "proceed" };
484
+ }
485
+ const go = await confirm({
486
+ message: "Set up a new Agentbox environment?",
487
+ initialValue: true
488
+ });
489
+ if (isCancel(go) || !go) return { action: "proceed" };
490
+ try {
491
+ const r = await installAgentboxSetupSkill();
492
+ if (r.installed) {
493
+ log4.success(`installed /agentbox-setup skill at ${r.targetFile}`);
494
+ }
495
+ } catch (err) {
496
+ log4.warn(`could not install /agentbox-setup skill: ${err.message}`);
497
+ }
498
+ if (args.command === "create") return { action: "switch-to-claude" };
499
+ return {
500
+ action: "launch-with-prompt",
501
+ initialPrompt: buildSetupInitialPrompt(proj.root)
502
+ };
503
+ }
504
+ function passthroughFlags(opts) {
505
+ const out = [];
506
+ if (opts.workspace) out.push("--workspace", opts.workspace);
507
+ if (opts.name) out.push("--name", opts.name);
508
+ if (opts.hostSnapshot === true) out.push("--host-snapshot");
509
+ if (opts.hostSnapshot === false) out.push("--no-host-snapshot");
510
+ if (opts.snapshot) out.push("--snapshot", opts.snapshot);
511
+ if (opts.image) out.push("--image", opts.image);
512
+ if (opts.withPlaywright === true) out.push("--with-playwright");
513
+ if (opts.vnc === false) out.push("--no-vnc");
514
+ if (opts.sharedDockerCache === true) out.push("--shared-docker-cache");
515
+ return out;
516
+ }
517
+
518
+ // src/commands/claude.ts
519
+ function reattachRef(r) {
520
+ return typeof r.projectIndex === "number" ? String(r.projectIndex) : r.name;
521
+ }
522
+ function buildClaudeCliOverrides(opts) {
523
+ const box = {};
524
+ if (opts.hostSnapshot !== void 0) box.hostSnapshot = opts.hostSnapshot;
525
+ if (opts.image !== void 0) box.image = opts.image;
526
+ if (opts.withPlaywright === true) box.withPlaywright = true;
527
+ if (opts.withEnv === true) box.withEnv = true;
528
+ if (opts.vnc === false) box.vnc = false;
529
+ if (opts.isolateClaudeConfig === true) box.isolateClaudeConfig = true;
530
+ if (opts.sharedDockerCache === true) box.dockerCacheShared = true;
531
+ const claude = {};
532
+ if (opts.sessionName !== void 0) claude.sessionName = opts.sessionName;
533
+ const out = {};
534
+ if (Object.keys(box).length > 0) out.box = box;
535
+ if (Object.keys(claude).length > 0) out.claude = claude;
536
+ return out;
537
+ }
538
+ async function offerSetupToken() {
539
+ log5.info("first time setup: setup token for Claude Code");
540
+ const canRun = hostClaudeAvailable();
541
+ if (canRun) {
542
+ const yes = await confirm2({
543
+ message: "Run `claude setup-token` now to save a token?",
544
+ initialValue: true
545
+ });
546
+ if (isCancel2(yes) || !yes) {
547
+ log5.info("ok, continuing without a saved token; /login inside the box once and it persists in the shared volume.");
548
+ return null;
549
+ }
550
+ const { exitCode } = runHostSetupToken();
551
+ if (exitCode !== 0) {
552
+ log5.warn(`\`claude setup-token\` exited with code ${String(exitCode)}; you can still paste a token below if you have one.`);
553
+ }
554
+ } else {
555
+ log5.warn(
556
+ "Claude Code is not installed on the host, so I cannot run `claude setup-token` for you. Run it on a machine that has Claude Code installed, then paste the token below \u2014 or skip and /login inside the box."
557
+ );
558
+ }
559
+ const pasted = await password({ message: "Paste OAuth token (or empty to skip):" });
560
+ if (isCancel2(pasted) || !pasted) {
561
+ log5.info("ok, continuing without a saved token; /login inside the box once and it persists in the shared volume.");
562
+ return null;
563
+ }
564
+ const token = pasted.trim();
565
+ if (!isPlausibleOauthToken(token)) {
566
+ log5.warn("That doesn't look like an OAuth token (expected `sk-ant-oat\u2026`); saving anyway \u2014 verify inside the box.");
567
+ }
568
+ await writeAuthFile({ claudeCodeOauthToken: token });
569
+ log5.success(`saved to ${AUTH_FILE} (mode 0600)`);
570
+ return { env: { CLAUDE_CODE_OAUTH_TOKEN: token }, source: "auth-file" };
571
+ }
572
+ var claudeCommand = new Command2("claude").description("Create a sandboxed box and launch Claude Code in a detachable tmux session").option("-w, --workspace <path>", "host workspace to mount", process.cwd()).option("-n, --name <name>", "friendly box name (default: <workspace-basename>-<id>)").option("--host-snapshot", "use a frozen APFS clone of the host workspace as the overlay lower").option("--no-host-snapshot", "bind the live workspace directly (host edits leak into reads)").option(
573
+ "--snapshot <ref>",
574
+ "start from a project checkpoint (see `agentbox checkpoint`); overrides box.defaultCheckpoint"
575
+ ).option("--image <ref>", "override the box image").option("-y, --yes", "skip prompts, accept defaults (host-snapshot=on)").option(
576
+ "--isolate-claude-config",
577
+ "use a per-box ~/.claude volume instead of the shared agentbox-claude-config"
578
+ ).option("--with-playwright", "also install @playwright/cli@latest globally inside the box").option(
579
+ "--with-env",
580
+ "copy host env/config files (.env*, secrets.toml, agentbox.yaml, ...) into /workspace at create time (gitignore-bypassing)"
581
+ ).option("--no-vnc", "disable the per-box Xvnc + noVNC web client (on by default)").option(
582
+ "--shared-docker-cache",
583
+ "use the shared 'agentbox-docker-cache' volume for in-box docker images (preserved on destroy; only one box can run at a time when set)"
584
+ ).option("--session-name <name>", "tmux session name (default from config; built-in: claude)").option("--memory <size>", "memory ceiling (e.g. 512m, 2g); unset = unlimited").option("--cpus <n>", "CPU count cap (fractional ok, e.g. 1.5); unset = unlimited").option("--pids-limit <n>", "max process count (PIDs cgroup); unset = unlimited").option("--disk <size>", "best-effort writable-layer size (e.g. 10g); no-op on overlay2/macOS").argument(
585
+ "[claude-args...]",
586
+ "extra args passed to claude inside the box; place after `--`, e.g. `agentbox claude -- --model sonnet`"
587
+ ).action(async (claudeArgs, opts) => {
588
+ intro("agentbox claude");
589
+ const cfg = await loadEffectiveConfig(opts.workspace, {
590
+ cliOverrides: buildClaudeCliOverrides(opts)
591
+ });
592
+ const projectRoot = (await findProjectRoot(opts.workspace)).root;
593
+ const checkpointRef = opts.snapshot && opts.snapshot.length > 0 ? opts.snapshot : cfg.effective.box.defaultCheckpoint.length > 0 ? cfg.effective.box.defaultCheckpoint : void 0;
594
+ const wiz = await maybeRunSetupWizard({
595
+ workspace: opts.workspace,
596
+ yes: !!opts.yes,
597
+ command: "claude",
598
+ checkpointRef
599
+ });
600
+ let effectiveClaudeArgs = claudeArgs;
601
+ if (wiz.action === "launch-with-prompt" && wiz.initialPrompt) {
602
+ effectiveClaudeArgs = resolveAgentLauncher("claude-code").buildArgs(
603
+ wiz.initialPrompt,
604
+ claudeArgs
605
+ );
606
+ }
607
+ const useSnapshot = opts.hostSnapshot === false ? false : opts.hostSnapshot === true ? true : cfg.effective.box.hostSnapshot ?? true;
608
+ const sessionName = cfg.effective.claude.sessionName;
609
+ let resolved = await resolveClaudeAuth(process.env);
610
+ if (resolved.source === "none" && process.stdin.isTTY && !opts.yes) {
611
+ const next = await offerSetupToken();
612
+ if (next) resolved = next;
613
+ }
614
+ const s = spinner();
615
+ s.start("creating box");
616
+ let containerName = "";
617
+ try {
618
+ const withPlaywright = cfg.effective.box.withPlaywright || cfg.effective.browser.default !== "agent-browser";
619
+ const result = await createBox({
620
+ workspacePath: opts.workspace,
621
+ name: opts.name,
622
+ useSnapshot,
623
+ checkpointRef,
624
+ image: cfg.effective.box.image,
625
+ claudeConfig: { isolate: cfg.effective.box.isolateClaudeConfig },
626
+ claudeEnv: resolved.env,
627
+ withPlaywright,
628
+ withEnv: cfg.effective.box.withEnv,
629
+ vnc: { enabled: cfg.effective.box.vnc },
630
+ docker: { sharedCache: cfg.effective.box.dockerCacheShared },
631
+ limits: resolveLimits(cfg.effective.box, opts),
632
+ projectRoot,
633
+ onLog: (line) => s.message(clampSpinnerLine(line))
634
+ });
635
+ containerName = result.record.container;
636
+ s.message("checking plugin native deps");
637
+ const rebuild = await rebuildPluginNativeDeps(result.record.container, {
638
+ volume: result.record.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME,
639
+ onProgress: (line) => s.message(clampSpinnerLine(line))
640
+ });
641
+ s.message("starting claude session");
642
+ await startClaudeSession({
643
+ container: result.record.container,
644
+ claudeArgs: effectiveClaudeArgs,
645
+ sessionName,
646
+ boxName: result.record.name
647
+ });
648
+ const nSuffix = typeof result.record.projectIndex === "number" ? ` \xB7 n ${String(result.record.projectIndex)}` : "";
649
+ s.stop(`box ${result.record.container} ready${nSuffix}`);
650
+ for (const f of rebuild.failed) {
651
+ log5.warn(`plugin install failed for ${f.dir}; claude may still load it. stderr:
652
+ ${f.stderr.trim()}`);
653
+ }
654
+ outro("attaching \u2014 Ctrl-b d to detach, leaves claude running");
655
+ attachClaudeSession(result.record.container, sessionName, reattachRef(result.record));
656
+ } catch (err) {
657
+ s.stop("failed");
658
+ if (err instanceof ClaudeSessionError) {
659
+ log5.error(err.message);
660
+ if (containerName) {
661
+ log5.info(`The box ${containerName} is still running. Destroy it with:`);
662
+ log5.info(` agentbox destroy ${containerName} -y`);
663
+ }
664
+ process.exit(1);
665
+ }
666
+ handleLifecycleError(err);
667
+ }
668
+ });
669
+ async function startOrAttachClaude(box, claudeArgs, opts) {
670
+ const cfg = await loadEffectiveConfig(box.workspacePath, {
671
+ cliOverrides: opts.sessionName ? { claude: { sessionName: opts.sessionName } } : {}
672
+ });
673
+ const sessionName = cfg.effective.claude.sessionName;
674
+ const insp = await inspectBox(box.id);
675
+ if (insp.state === "missing") {
676
+ throw new Error(`box ${box.name} has no container; was it destroyed?`);
677
+ }
678
+ const existing = await claudeSessionInfo(box.container, sessionName);
679
+ if (existing.running) {
680
+ outro(`session "${sessionName}" already running \u2014 attaching (Ctrl-b d to detach)`);
681
+ attachClaudeSession(box.container, sessionName, reattachRef(box));
682
+ return;
683
+ }
684
+ const s = spinner();
685
+ s.start("preparing box");
686
+ if (insp.state === "paused") {
687
+ s.message("unpausing box");
688
+ await unpauseBox(box.id);
689
+ } else if (insp.state === "stopped") {
690
+ s.message("starting box (remounting overlay)");
691
+ await startBox(box.id);
692
+ }
693
+ const syncConfig = opts.syncConfig !== false;
694
+ if (syncConfig) {
695
+ s.message("syncing ~/.claude into box volume");
696
+ const volume = box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME;
697
+ await ensureClaudeVolume(
698
+ { volume },
699
+ {
700
+ syncFromHost: true,
701
+ image: box.image,
702
+ hostWorkspace: box.workspacePath
703
+ }
704
+ );
705
+ }
706
+ s.message("checking plugin native deps");
707
+ const rebuild = await rebuildPluginNativeDeps(box.container, {
708
+ volume: box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME,
709
+ onProgress: (line) => s.message(clampSpinnerLine(line))
710
+ });
711
+ s.message("starting claude session");
712
+ await startClaudeSession({
713
+ container: box.container,
714
+ claudeArgs,
715
+ sessionName,
716
+ boxName: box.name
717
+ });
718
+ s.stop(`box ${box.container} ready`);
719
+ for (const f of rebuild.failed) {
720
+ log5.warn(`plugin install failed for ${f.dir}; claude may still load it. stderr:
721
+ ${f.stderr.trim()}`);
722
+ }
723
+ outro("attaching \u2014 Ctrl-b d to detach, leaves claude running");
724
+ attachClaudeSession(box.container, sessionName, reattachRef(box));
725
+ }
726
+ var claudeAttachCommand = new Command2("attach").description(
727
+ "Attach to a Claude Code tmux session in a box, starting one if none is running (auto-unpause/start)"
728
+ ).argument(
729
+ "[box]",
730
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
731
+ ).option("--session-name <name>", "tmux session name (default from config; built-in: claude)").option(
732
+ "--no-sync-config",
733
+ "when starting a fresh session, skip rsyncing the host's ~/.claude into the box's volume (faster)"
734
+ ).action(async function(idOrName) {
735
+ const opts = this.optsWithGlobals();
736
+ intro("agentbox claude attach");
737
+ try {
738
+ const box = await resolveBoxOrExit(idOrName);
739
+ await startOrAttachClaude(box, [], opts);
740
+ } catch (err) {
741
+ if (err instanceof ClaudeSessionError) {
742
+ log5.error(err.message);
743
+ process.exit(1);
744
+ }
745
+ handleLifecycleError(err);
746
+ }
747
+ });
748
+ var claudeStartCommand = new Command2("start").description(
749
+ "Start a Claude Code tmux session in an already-existing box (auto-unpause/start). If a session is already running, just attach."
750
+ ).argument(
751
+ "[box]",
752
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
753
+ ).option("--session-name <name>", "tmux session name (default from config; built-in: claude)").option(
754
+ "--no-sync-config",
755
+ "skip rsyncing the host's ~/.claude into the box's volume before starting (faster; use existing in-box state)"
756
+ ).argument(
757
+ "[claude-args...]",
758
+ "extra args passed to claude when starting a new session; ignored if a session is already running. Place after `--`, e.g. `agentbox claude start 1 -- --model sonnet`"
759
+ ).action(async function(idOrName, claudeArgs) {
760
+ const opts = this.optsWithGlobals();
761
+ intro("agentbox claude start");
762
+ try {
763
+ const { box, shifted } = await resolveBoxOrShift(idOrName);
764
+ const effectiveClaudeArgs = shifted && idOrName ? [idOrName, ...claudeArgs] : claudeArgs;
765
+ await startOrAttachClaude(box, effectiveClaudeArgs, opts);
766
+ } catch (err) {
767
+ if (err instanceof ClaudeSessionError) {
768
+ log5.error(err.message);
769
+ process.exit(1);
770
+ }
771
+ handleLifecycleError(err);
772
+ }
773
+ });
774
+ claudeCommand.addCommand(claudeAttachCommand);
775
+ claudeCommand.addCommand(claudeStartCommand);
776
+
777
+ // src/commands/checkpoint.ts
778
+ import { confirm as confirm3, isCancel as isCancel3, log as log6 } from "@clack/prompts";
779
+ import { Command as Command3 } from "commander";
780
+ async function projectRootFor(cwd, recordRoot) {
781
+ return recordRoot ?? (await findProjectRoot(cwd)).root;
782
+ }
783
+ var createSub = new Command3("create").description("Capture a box state as a project checkpoint (<box-name>-<n>)").argument(
784
+ "[box]",
785
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
786
+ ).option("--name <name>", "checkpoint name (default: <box-name>-<next>)").option("--merged", "flatten lower+upper into one tree instead of a layered delta").option("--set-default", "mark this checkpoint as the project default for new boxes").action(async (idOrName, opts) => {
787
+ try {
788
+ const box = await resolveBoxOrExit(idOrName);
789
+ const insp = await inspectBox(box.id);
790
+ if (insp.state === "paused") {
791
+ log6.info("box is paused; unpausing");
792
+ await unpauseBox(box.id);
793
+ } else if (insp.state === "stopped") {
794
+ log6.info("box is stopped; starting (remounting overlay)");
795
+ await startBox(box.id);
796
+ } else if (insp.state === "missing") {
797
+ throw new Error(`box ${box.name} has no container; was it destroyed?`);
798
+ }
799
+ const projectRoot = await projectRootFor(box.workspacePath, box.projectRoot);
800
+ const cfg = await loadEffectiveConfig(projectRoot);
801
+ const info = await createCheckpoint({
802
+ box,
803
+ projectRoot,
804
+ name: opts.name,
805
+ merged: opts.merged === true,
806
+ setDefault: opts.setDefault === true,
807
+ maxLayers: cfg.effective.checkpoint.maxLayers,
808
+ onLog: (line) => log6.info(line)
809
+ });
810
+ log6.success(
811
+ `checkpoint ${info.name} (${info.manifest.type}) -> ${info.dir}` + (opts.setDefault ? " [project default]" : "")
812
+ );
813
+ if (!opts.setDefault) {
814
+ log6.info(`make it the default for new boxes: agentbox checkpoint set-default ${info.name}`);
815
+ }
816
+ } catch (err) {
817
+ handleLifecycleError(err);
818
+ }
819
+ });
820
+ var lsSub = new Command3("ls").description("List this project's checkpoints").action(async () => {
821
+ try {
822
+ const projectRoot = (await findProjectRoot(process.cwd())).root;
823
+ const cfg = await loadEffectiveConfig(projectRoot);
824
+ const def = cfg.effective.box.defaultCheckpoint;
825
+ const list = await listCheckpoints(projectRoot);
826
+ if (list.length === 0) {
827
+ process.stdout.write(`no checkpoints for ${projectRoot}
828
+ `);
829
+ return;
830
+ }
831
+ for (const c of list) {
832
+ const flag = c.name === def ? " *default" : "";
833
+ process.stdout.write(
834
+ `${c.name} ${c.manifest.type} from ${c.manifest.sourceBoxName} ${c.manifest.createdAt}${flag}
835
+ `
836
+ );
837
+ }
838
+ } catch (err) {
839
+ handleLifecycleError(err);
840
+ }
841
+ });
842
+ var setDefaultSub = new Command3("set-default").description("Pin a checkpoint as the project default (box.defaultCheckpoint)").argument("[ref]", "checkpoint name (omit with --clear)").option("--clear", "unset the project default instead of setting one").action(async (ref, opts) => {
843
+ try {
844
+ const projectRoot = (await findProjectRoot(process.cwd())).root;
845
+ if (opts.clear) {
846
+ if (ref !== void 0) {
847
+ throw new Error("pass either a <ref> or --clear, not both");
848
+ }
849
+ const r2 = await unsetConfigValue("project", "box.defaultCheckpoint", projectRoot);
850
+ process.stdout.write(
851
+ r2.existed ? `cleared project default checkpoint (wrote ${r2.path})
852
+ ` : `no project default checkpoint was set (${r2.path})
853
+ `
854
+ );
855
+ return;
856
+ }
857
+ if (ref === void 0) {
858
+ throw new Error("missing <ref> (or pass --clear to unset the default)");
859
+ }
860
+ const list = await listCheckpoints(projectRoot);
861
+ if (!list.some((c) => c.name === ref)) {
862
+ throw new Error(`checkpoint not found: ${ref} (see \`agentbox checkpoint ls\`)`);
863
+ }
864
+ const r = await setConfigValue("project", "box.defaultCheckpoint", ref, projectRoot);
865
+ process.stdout.write(`project default checkpoint = ${ref} (wrote ${r.path})
866
+ `);
867
+ } catch (err) {
868
+ handleLifecycleError(err);
869
+ }
870
+ });
871
+ var rmSub = new Command3("rm").description("Delete a checkpoint").argument("<ref>", "checkpoint name").option("-y, --yes", "skip the confirmation prompt").action(async (ref, opts) => {
872
+ try {
873
+ const projectRoot = (await findProjectRoot(process.cwd())).root;
874
+ if (!opts.yes) {
875
+ const ok = await confirm3({ message: `Delete checkpoint ${ref}?`, initialValue: false });
876
+ if (isCancel3(ok) || !ok) {
877
+ log6.info("cancelled");
878
+ return;
879
+ }
880
+ }
881
+ const removed = await removeCheckpoint(projectRoot, ref);
882
+ if (!removed) throw new Error(`checkpoint not found: ${ref}`);
883
+ process.stdout.write(`removed checkpoint ${ref}
884
+ `);
885
+ const cfg = await loadEffectiveConfig(projectRoot);
886
+ if (cfg.layers.project.values.box?.defaultCheckpoint === ref) {
887
+ await unsetConfigValue("project", "box.defaultCheckpoint", projectRoot);
888
+ log6.info(`cleared project default checkpoint (was ${ref})`);
889
+ } else if (cfg.effective.box.defaultCheckpoint === ref) {
890
+ log6.warn(
891
+ `default checkpoint ${ref} is set outside the per-project config (global or agentbox.yaml defaults) \u2014 clear it manually`
892
+ );
893
+ }
894
+ } catch (err) {
895
+ handleLifecycleError(err);
896
+ }
897
+ });
898
+ var checkpointCommand = new Command3("checkpoint").description("Capture and manage project checkpoints (warm box state new boxes can start from)").addCommand(createSub, { isDefault: true }).addCommand(lsSub).addCommand(setDefaultSub).addCommand(rmSub);
899
+
900
+ // src/commands/code.ts
901
+ import { spawn } from "child_process";
902
+ import { log as log7 } from "@clack/prompts";
903
+ import { Command as Command4, InvalidArgumentError } from "commander";
904
+ function buildCodeCliOverrides(opts) {
905
+ const code = {};
906
+ if (opts.ide !== void 0) code.ide = opts.ide;
907
+ if (opts.noWait === true) code.wait = false;
908
+ if (opts.noAutoTerminals === true) code.autoTerminals = false;
909
+ if (opts.timeout !== void 0) {
910
+ const n = Number(opts.timeout);
911
+ if (Number.isFinite(n) && Number.isInteger(n)) code.timeoutMs = n;
912
+ }
913
+ return Object.keys(code).length > 0 ? { code } : {};
914
+ }
915
+ function parseIdeFlavor(value) {
916
+ if (value === "vscode" || value === "cursor") return value;
917
+ throw new InvalidArgumentError(`expected one of: vscode, cursor (got "${value}")`);
918
+ }
919
+ var codeCommand = new Command4("code").description("Open a box in VS Code or Cursor via the Dev Containers extension").argument(
920
+ "[box]",
921
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
922
+ ).option("--no-wait", "don't block on agentbox-ctl wait-ready before opening").option("--timeout <ms>", "wait-ready timeout in milliseconds (default from config; built-in: 120000)").option("--no-auto-terminals", "don't generate /workspace/.vscode/tasks.json").option("--regen-tasks", "overwrite a user-owned tasks.json (skips sentinel check)", false).option(
923
+ "--ide <flavor>",
924
+ "force a specific IDE: vscode | cursor (default from config; built-in: auto)",
925
+ parseIdeFlavor
926
+ ).option(
927
+ "--print",
928
+ "print the folder URI instead of launching the IDE (still refreshes/waits)"
929
+ ).action(async (idOrName, opts) => {
930
+ try {
931
+ const box = await resolveBoxOrExit(idOrName);
932
+ const cfg = await loadEffectiveConfig(box.workspacePath, {
933
+ cliOverrides: buildCodeCliOverrides(opts)
934
+ });
935
+ const wait = cfg.effective.code.wait;
936
+ const autoTerminals = cfg.effective.code.autoTerminals;
937
+ const timeoutMs = String(cfg.effective.code.timeoutMs);
938
+ const ide = cfg.effective.code.ide;
939
+ const forcedIde = ide === "auto" ? void 0 : ide;
940
+ const insp = await inspectBox(box.id);
941
+ if (insp.state === "paused") {
942
+ log7.info(`box is paused; unpausing`);
943
+ await unpauseBox(box.id);
944
+ } else if (insp.state === "stopped") {
945
+ log7.info(`box is stopped; starting (remounting overlay)`);
946
+ await startBox(box.id);
947
+ } else if (insp.state === "missing") {
948
+ throw new Error(`box ${box.name} has no container; was it destroyed?`);
949
+ }
950
+ if (wait) {
951
+ const reply = await runWaitReady(box.container, timeoutMs);
952
+ if (!reply.ready) {
953
+ const lines = [];
954
+ if (reply.timedOut.length > 0) lines.push(`timed out: ${reply.timedOut.join(", ")}`);
955
+ if (reply.failed.length > 0) lines.push(`failed: ${reply.failed.join(", ")}`);
956
+ log7.warn(`box not fully ready (${lines.join("; ")}). Opening anyway.`);
957
+ } else {
958
+ log7.success("all units ready");
959
+ }
960
+ }
961
+ if (autoTerminals) {
962
+ try {
963
+ const services = await fetchServiceNames(box.container);
964
+ const r = await ensureAgentboxTasksFile(box.container, services, {
965
+ regen: opts.regenTasks
966
+ });
967
+ if (r.status === "wrote") {
968
+ log7.info(`wrote /workspace/.vscode/tasks.json (${String(services.length)} service(s))`);
969
+ } else if (r.status === "skipped-user-owned") {
970
+ log7.warn(
971
+ "user-owned .vscode/tasks.json detected; skipping auto-terminals (pass --regen-tasks to overwrite)"
972
+ );
973
+ }
974
+ } catch (err) {
975
+ log7.warn(
976
+ `auto-terminals failed: ${err instanceof Error ? err.message : String(err)}`
977
+ );
978
+ }
979
+ }
980
+ const folderUri = `vscode-remote://attached-container+${containerHex(box.container)}/workspace`;
981
+ if (opts.print) {
982
+ process.stdout.write(folderUri + "\n");
983
+ return;
984
+ }
985
+ const exit = await launchIde(folderUri, forcedIde);
986
+ if (exit.code !== 0) {
987
+ log7.error(`failed to launch ${exit.flavor ? ideProfile(exit.flavor).displayName : "IDE"} via ${exit.via} (exit ${String(exit.code)})`);
988
+ process.stdout.write(folderUri + "\n");
989
+ process.exit(1);
990
+ }
991
+ log7.success(
992
+ `opening ${box.container} in ${ideProfile(exit.flavor).displayName} (${exit.via})`
993
+ );
994
+ } catch (err) {
995
+ handleLifecycleError(err);
996
+ }
997
+ });
998
+ async function runWaitReady(container, timeoutMs) {
999
+ const proc = await execInBox(
1000
+ container,
1001
+ ["agentbox-ctl", "wait-ready", "--json", "--timeout", timeoutMs],
1002
+ { user: "vscode" }
1003
+ );
1004
+ try {
1005
+ return JSON.parse(proc.stdout);
1006
+ } catch {
1007
+ throw new Error(
1008
+ `agentbox-ctl wait-ready returned unparseable output: ${proc.stderr || proc.stdout}`
1009
+ );
1010
+ }
1011
+ }
1012
+ async function launchIde(folderUri, forced) {
1013
+ if (forced) {
1014
+ return launchOne(forced, folderUri);
1015
+ }
1016
+ const code = await tryCli("vscode", folderUri);
1017
+ if (code !== null) return code;
1018
+ const cursor = await tryCli("cursor", folderUri);
1019
+ if (cursor !== null) return cursor;
1020
+ log7.warn("neither `code` nor `cursor` found in PATH; falling back to `open vscode://...`");
1021
+ return launchOne("vscode", folderUri);
1022
+ }
1023
+ async function tryCli(flavor, folderUri) {
1024
+ const profile = ideProfile(flavor);
1025
+ const code = await spawnCommand(profile.cli, ["--folder-uri", folderUri]);
1026
+ if (code === 127) return null;
1027
+ return { code, flavor, via: "cli" };
1028
+ }
1029
+ async function launchOne(flavor, folderUri) {
1030
+ const profile = ideProfile(flavor);
1031
+ const cliCode = await spawnCommand(profile.cli, ["--folder-uri", folderUri]);
1032
+ if (cliCode !== 127) return { code: cliCode, flavor, via: "cli" };
1033
+ log7.warn(
1034
+ `\`${profile.cli}\` not found in PATH; falling back to \`open ${profile.protocolScheme}://...\` (the %2B URL-encoding bug may break attach)`
1035
+ );
1036
+ const url = `${profile.protocolScheme}://${folderUri.replace(/^vscode-remote:\/\//, "vscode-remote/")}`;
1037
+ const fallback = await spawnCommand("open", [url]);
1038
+ return { code: fallback, flavor, via: "open" };
1039
+ }
1040
+ function spawnCommand(cmd, args) {
1041
+ return new Promise((resolve) => {
1042
+ const child = spawn(cmd, args, { stdio: "ignore" });
1043
+ child.once("error", () => resolve(127));
1044
+ child.once("exit", (code) => resolve(code ?? -1));
1045
+ });
1046
+ }
1047
+ async function fetchServiceNames(container) {
1048
+ const proc = await execInBox(container, ["agentbox-ctl", "status", "--json"], {
1049
+ user: "vscode"
1050
+ });
1051
+ if (proc.exitCode !== 0) return [];
1052
+ try {
1053
+ const reply = JSON.parse(proc.stdout);
1054
+ return reply.services.map((s) => ({ name: s.name }));
1055
+ } catch {
1056
+ return [];
1057
+ }
1058
+ }
1059
+
1060
+ // src/commands/config.ts
1061
+ import { spawnSync as spawnSync3 } from "child_process";
1062
+ import { Command as Command5, InvalidArgumentError as InvalidArgumentError2 } from "commander";
1063
+ function resolveWriteScope(opts) {
1064
+ if (opts.global && opts.project) {
1065
+ fail("pass at most one of --global / --project");
1066
+ }
1067
+ if (opts.global) return "global";
1068
+ return "project";
1069
+ }
1070
+ function resolveEditScope(opts) {
1071
+ const flags = [opts.global, opts.project, opts.workspace].filter(Boolean).length;
1072
+ if (flags > 1) fail("pass at most one of --global / --project / --workspace");
1073
+ if (opts.workspace) return "workspace";
1074
+ if (opts.global) return "global";
1075
+ return "project";
1076
+ }
1077
+ function fail(message) {
1078
+ process.stderr.write(`error: ${message}
1079
+ `);
1080
+ process.exit(1);
1081
+ }
1082
+ function leafValue(loaded, key) {
1083
+ const idx = key.indexOf(".");
1084
+ const branch = key.slice(0, idx);
1085
+ const leaf = key.slice(idx + 1);
1086
+ return loaded.effective[branch]?.[leaf];
1087
+ }
1088
+ function rawLeafFromValues(values, key) {
1089
+ if (!values) return void 0;
1090
+ const idx = key.indexOf(".");
1091
+ const b = values[key.slice(0, idx)];
1092
+ if (!b || typeof b !== "object") return void 0;
1093
+ return b[key.slice(idx + 1)];
1094
+ }
1095
+ function describeSource(source, loaded) {
1096
+ switch (source) {
1097
+ case "cli":
1098
+ return "cli flag";
1099
+ case "workspace":
1100
+ return loaded.layers.workspace.path ? `workspace ${loaded.layers.workspace.path}` : "workspace";
1101
+ case "project":
1102
+ return `project ${loaded.layers.project.path}`;
1103
+ case "global":
1104
+ return `global ${loaded.layers.global.path}`;
1105
+ case "default":
1106
+ return "built-in default";
1107
+ }
1108
+ }
1109
+ function fmtValue(v) {
1110
+ if (v === void 0) return "<unset>";
1111
+ if (typeof v === "string") return v;
1112
+ return String(v);
1113
+ }
1114
+ var getCommand = new Command5("get").description("Print the effective value of a config key (with --all, show every layer)").argument("<key>", "dot-path key (e.g. box.hostSnapshot)").option("--all", "print every layer with its source").option("--json", "machine-readable output").action(async (key, opts) => {
1115
+ const desc = lookupKey(key);
1116
+ if (!desc) fail(`unknown key "${key}"`);
1117
+ try {
1118
+ const loaded = await loadEffectiveConfig(process.cwd());
1119
+ const value = leafValue(loaded, key);
1120
+ const source = loaded.sources[key] ?? "default";
1121
+ if (opts.json) {
1122
+ const layerView = (values, path) => ({
1123
+ value: rawLeafFromValues(values, key) ?? null,
1124
+ path
1125
+ });
1126
+ process.stdout.write(
1127
+ JSON.stringify(
1128
+ {
1129
+ key,
1130
+ value: value ?? null,
1131
+ source,
1132
+ layers: opts.all ? {
1133
+ cli: layerView(loaded.layers.cli.values, null),
1134
+ workspace: layerView(
1135
+ loaded.layers.workspace.values,
1136
+ loaded.layers.workspace.path
1137
+ ),
1138
+ project: layerView(
1139
+ loaded.layers.project.values,
1140
+ loaded.layers.project.path
1141
+ ),
1142
+ global: layerView(
1143
+ loaded.layers.global.values,
1144
+ loaded.layers.global.path
1145
+ ),
1146
+ default: { value: leafValue({ ...loaded, effective: loaded.layers.defaults }, key) ?? null, path: null }
1147
+ } : void 0
1148
+ },
1149
+ null,
1150
+ 2
1151
+ ) + "\n"
1152
+ );
1153
+ return;
1154
+ }
1155
+ if (opts.all) {
1156
+ const lines = [
1157
+ `${key}:`,
1158
+ ` effective: ${fmtValue(value)} (${describeSource(source, loaded)})`,
1159
+ ` cli: ${fmtValue(rawLeafFromValues(loaded.layers.cli.values, key))}`,
1160
+ ` workspace: ${fmtValue(rawLeafFromValues(loaded.layers.workspace.values, key))}` + (loaded.layers.workspace.path ? ` ${loaded.layers.workspace.path}` : ""),
1161
+ ` project: ${fmtValue(rawLeafFromValues(loaded.layers.project.values, key))} ${loaded.layers.project.path}`,
1162
+ ` global: ${fmtValue(rawLeafFromValues(loaded.layers.global.values, key))} ${loaded.layers.global.path}`,
1163
+ ` default: ${fmtValue(leafValue({ ...loaded, effective: loaded.layers.defaults }, key))}`
1164
+ ];
1165
+ process.stdout.write(lines.join("\n") + "\n");
1166
+ return;
1167
+ }
1168
+ process.stdout.write(`${key} = ${fmtValue(value)} (from: ${describeSource(source, loaded)})
1169
+ `);
1170
+ } catch (err) {
1171
+ handleError(err);
1172
+ }
1173
+ });
1174
+ var setCommand = new Command5("set").description("Set a config key in the global or per-project file (default: --project)").argument("<key>", "dot-path key (e.g. box.hostSnapshot)").argument("<value>", "value to set; coerced to the key's declared type").option("--global", "write to ~/.agentbox/config.yaml").option("--project", "write to ~/.agentbox/projects/<hash>/config.yaml (default)").action(async (key, value, opts) => {
1175
+ const scope = resolveWriteScope(opts);
1176
+ try {
1177
+ const r = await setConfigValue(scope, key, value, process.cwd(), { raw: true });
1178
+ if (opts.json) {
1179
+ process.stdout.write(
1180
+ JSON.stringify({ key, scope, value: r.coerced, path: r.path }, null, 2) + "\n"
1181
+ );
1182
+ } else {
1183
+ process.stdout.write(`${key} = ${fmtValue(r.coerced)} (wrote ${r.path})
1184
+ `);
1185
+ }
1186
+ } catch (err) {
1187
+ handleError(err);
1188
+ }
1189
+ });
1190
+ var unsetCommand = new Command5("unset").description("Remove a config key from the global or per-project file (default: --project)").argument("<key>", "dot-path key (e.g. box.hostSnapshot)").option("--global", "edit ~/.agentbox/config.yaml").option("--project", "edit ~/.agentbox/projects/<hash>/config.yaml (default)").action(async (key, opts) => {
1191
+ const scope = resolveWriteScope(opts);
1192
+ try {
1193
+ const r = await unsetConfigValue(scope, key, process.cwd());
1194
+ if (r.existed) {
1195
+ process.stdout.write(`removed ${key} from ${r.path}
1196
+ `);
1197
+ } else {
1198
+ process.stdout.write(`${key} was not set in ${r.path}
1199
+ `);
1200
+ }
1201
+ } catch (err) {
1202
+ handleError(err);
1203
+ }
1204
+ });
1205
+ function parseListScope(value) {
1206
+ if (value === "global" || value === "project" || value === "workspace" || value === "effective") {
1207
+ return value;
1208
+ }
1209
+ throw new InvalidArgumentError2(`expected one of: global, project, workspace, effective`);
1210
+ }
1211
+ var listCommand = new Command5("list").description("List config values, either for a single layer or the merged effective view").option(
1212
+ "--scope <s>",
1213
+ "one of: global, project, workspace, effective (default: effective)",
1214
+ parseListScope,
1215
+ "effective"
1216
+ ).option("--include-advanced", "include advanced keys (image, ports)").option("--json", "machine-readable output").action(async (opts) => {
1217
+ try {
1218
+ const loaded = await loadEffectiveConfig(process.cwd());
1219
+ const scope = opts.scope ?? "effective";
1220
+ const showAdvanced = !!opts.includeAdvanced;
1221
+ const visibleKeys = KEY_REGISTRY.filter((d) => showAdvanced || !d.advanced);
1222
+ if (opts.json) {
1223
+ const obj = {};
1224
+ for (const desc of visibleKeys) {
1225
+ const value = pickFromScope(loaded, scope, desc.key);
1226
+ obj[desc.key] = scope === "effective" ? { value: value ?? null, source: loaded.sources[desc.key] ?? "default" } : { value: value ?? null };
1227
+ }
1228
+ process.stdout.write(JSON.stringify({ scope, keys: obj }, null, 2) + "\n");
1229
+ return;
1230
+ }
1231
+ if (scope === "effective") {
1232
+ const lines = [];
1233
+ for (const desc of visibleKeys) {
1234
+ const value = leafValue(loaded, desc.key);
1235
+ const source = loaded.sources[desc.key] ?? "default";
1236
+ lines.push(`${desc.key.padEnd(28)} ${fmtValue(value).padEnd(20)} (${source})`);
1237
+ }
1238
+ process.stdout.write(lines.join("\n") + "\n");
1239
+ return;
1240
+ }
1241
+ const layerPath = scope === "global" ? loaded.layers.global.path : scope === "project" ? loaded.layers.project.path : loaded.layers.workspace.path;
1242
+ process.stdout.write(`# ${scope} ${layerPath ?? "(no agentbox.yaml in ancestors)"}
1243
+ `);
1244
+ let any = false;
1245
+ for (const desc of visibleKeys) {
1246
+ const v = pickFromScope(loaded, scope, desc.key);
1247
+ if (v === void 0) continue;
1248
+ any = true;
1249
+ process.stdout.write(`${desc.key.padEnd(28)} ${fmtValue(v)}
1250
+ `);
1251
+ }
1252
+ if (!any) process.stdout.write("(no values set in this scope)\n");
1253
+ } catch (err) {
1254
+ handleError(err);
1255
+ }
1256
+ });
1257
+ function pickFromScope(loaded, scope, key) {
1258
+ switch (scope) {
1259
+ case "global":
1260
+ return rawLeafFromValues(loaded.layers.global.values, key);
1261
+ case "project":
1262
+ return rawLeafFromValues(loaded.layers.project.values, key);
1263
+ case "workspace":
1264
+ return rawLeafFromValues(loaded.layers.workspace.values, key);
1265
+ case "effective":
1266
+ default:
1267
+ return leafValue(loaded, key);
1268
+ }
1269
+ }
1270
+ var pathCommand = new Command5("path").description("Print the file path for a config scope (default: --project)").option("--global", "~/.agentbox/config.yaml").option("--project", "~/.agentbox/projects/<hash>/config.yaml (default)").option("--workspace", "./agentbox.yaml (resolved by walking up to the nearest one)").option("--json", "machine-readable output").action(async (opts) => {
1271
+ try {
1272
+ const scope = resolveEditScope(opts);
1273
+ const path = await configPathFor(scope, process.cwd());
1274
+ if (opts.json) process.stdout.write(JSON.stringify({ scope, path }, null, 2) + "\n");
1275
+ else process.stdout.write(`${path}
1276
+ `);
1277
+ } catch (err) {
1278
+ handleError(err);
1279
+ }
1280
+ });
1281
+ var editCommand = new Command5("edit").description("Open a config file in $EDITOR (default: --project)").option("--global", "edit ~/.agentbox/config.yaml").option("--project", "edit ~/.agentbox/projects/<hash>/config.yaml (default)").option("--workspace", "edit ./agentbox.yaml (the resolved one \u2014 and remember to fill in the `defaults:` block)").action(async (opts) => {
1282
+ try {
1283
+ const scope = resolveEditScope(opts);
1284
+ const path = await configPathFor(scope, process.cwd());
1285
+ const editor = process.env["EDITOR"] || process.env["VISUAL"] || "vi";
1286
+ const child = spawnSync3(editor, [path], { stdio: "inherit" });
1287
+ process.exit(child.status ?? 0);
1288
+ } catch (err) {
1289
+ handleError(err);
1290
+ }
1291
+ });
1292
+ var listProjectsCommand = new Command5("list-projects").description("List directories that have per-user-per-project config recorded under ~/.agentbox/projects/").option("--json", "machine-readable output").action(async (opts) => {
1293
+ try {
1294
+ const projects = await listProjectsConfigured();
1295
+ if (opts.json) {
1296
+ process.stdout.write(JSON.stringify(projects, null, 2) + "\n");
1297
+ return;
1298
+ }
1299
+ if (projects.length === 0) {
1300
+ process.stdout.write("(no per-project config recorded)\n");
1301
+ return;
1302
+ }
1303
+ for (const p of projects) {
1304
+ process.stdout.write(
1305
+ `${p.hash} ${p.originalPath}${p.hasConfigFile ? "" : " (meta only \u2014 no config.yaml)"}
1306
+ `
1307
+ );
1308
+ }
1309
+ } catch (err) {
1310
+ handleError(err);
1311
+ }
1312
+ });
1313
+ function handleError(err) {
1314
+ if (err instanceof UserConfigError) {
1315
+ process.stderr.write(`error: ${err.message}
1316
+ `);
1317
+ process.exit(2);
1318
+ }
1319
+ const msg = err instanceof Error ? err.message : String(err);
1320
+ process.stderr.write(`error: ${msg}
1321
+ `);
1322
+ process.exit(1);
1323
+ }
1324
+ var configCommand = new Command5("config").description("Read / write layered config (global, per-project, workspace `defaults:` block)").addCommand(getCommand).addCommand(setCommand).addCommand(unsetCommand).addCommand(listCommand).addCommand(pathCommand).addCommand(editCommand).addCommand(listProjectsCommand);
1325
+
1326
+ // src/commands/create.ts
1327
+ import { intro as intro2, log as log8, outro as outro2, spinner as spinner2 } from "@clack/prompts";
1328
+ import { Command as Command6 } from "commander";
1329
+ import { execSync, spawnSync as spawnSync4 } from "child_process";
1330
+ function buildCliOverrides(opts) {
1331
+ const box = {};
1332
+ if (opts.hostSnapshot !== void 0) box.hostSnapshot = opts.hostSnapshot;
1333
+ if (opts.image !== void 0) box.image = opts.image;
1334
+ if (opts.withPlaywright === true) box.withPlaywright = true;
1335
+ if (opts.withEnv === true) box.withEnv = true;
1336
+ if (opts.vnc === false) box.vnc = false;
1337
+ if (opts.sharedDockerCache === true) box.dockerCacheShared = true;
1338
+ return Object.keys(box).length > 0 ? { box } : {};
1339
+ }
1340
+ function resolveUseSnapshot(opts, configDefault) {
1341
+ if (opts.hostSnapshot === false) return false;
1342
+ if (opts.hostSnapshot === true) return true;
1343
+ return configDefault ?? true;
1344
+ }
1345
+ function resolveCheckpointRef(opts, configDefault) {
1346
+ if (opts.snapshot && opts.snapshot.length > 0) return opts.snapshot;
1347
+ return configDefault.length > 0 ? configDefault : void 0;
1348
+ }
1349
+ function attachShell(container) {
1350
+ const child = spawnSync4("docker", ["exec", "-it", container, "bash"], {
1351
+ stdio: "inherit"
1352
+ });
1353
+ process.exit(child.status ?? 0);
1354
+ }
1355
+ var createCommand = new Command6("create").description("Create and start a new agent box (Docker container with FUSE overlay)").option("-w, --workspace <path>", "host workspace to mount", process.cwd()).option("-n, --name <name>", "friendly box name (default: <workspace-basename>-<id>)").option("--host-snapshot", "use a frozen APFS clone of the host workspace as the overlay lower").option("--no-host-snapshot", "bind the live workspace directly (host edits leak into reads)").option(
1356
+ "--snapshot <ref>",
1357
+ "start from a project checkpoint (see `agentbox checkpoint`); overrides box.defaultCheckpoint"
1358
+ ).option("--image <ref>", "override the box image", void 0).option("--attach", "drop into a shell inside the box after it is ready").option("--with-playwright", "also install @playwright/cli@latest globally inside the box").option(
1359
+ "--with-env",
1360
+ "copy host env/config files (.env*, secrets.toml, agentbox.yaml, ...) into /workspace at create time (gitignore-bypassing)"
1361
+ ).option("--no-vnc", "disable the per-box Xvnc + noVNC web client (on by default)").option(
1362
+ "--shared-docker-cache",
1363
+ "use the shared 'agentbox-docker-cache' volume for in-box docker images (preserved on destroy; only one box can run at a time when set)"
1364
+ ).option("--memory <size>", "memory ceiling (e.g. 512m, 2g); unset = unlimited").option("--cpus <n>", "CPU count cap (fractional ok, e.g. 1.5); unset = unlimited").option("--pids-limit <n>", "max process count (PIDs cgroup); unset = unlimited").option("--disk <size>", "best-effort writable-layer size (e.g. 10g); no-op on overlay2/macOS").option("-y, --yes", "skip prompts, accept defaults (host-snapshot=on)").action(async (opts) => {
1365
+ intro2("agentbox create");
1366
+ const cfg = await loadEffectiveConfig(opts.workspace, {
1367
+ cliOverrides: buildCliOverrides(opts)
1368
+ });
1369
+ const projectRoot = (await findProjectRoot(opts.workspace)).root;
1370
+ const checkpointRef = resolveCheckpointRef(opts, cfg.effective.box.defaultCheckpoint);
1371
+ const wiz = await maybeRunSetupWizard({
1372
+ workspace: opts.workspace,
1373
+ yes: !!opts.yes,
1374
+ command: "create",
1375
+ checkpointRef
1376
+ });
1377
+ if (wiz.action === "switch-to-claude") {
1378
+ process.env[WIZARD_AUTOLAUNCH_ENV] = "1";
1379
+ try {
1380
+ await claudeCommand.parseAsync(passthroughFlags(opts), { from: "user" });
1381
+ } finally {
1382
+ delete process.env[WIZARD_AUTOLAUNCH_ENV];
1383
+ }
1384
+ return;
1385
+ }
1386
+ const useSnapshot = resolveUseSnapshot(opts, cfg.effective.box.hostSnapshot);
1387
+ const s = spinner2();
1388
+ s.start("creating box");
1389
+ try {
1390
+ const withPlaywright = cfg.effective.box.withPlaywright || cfg.effective.browser.default !== "agent-browser";
1391
+ const result = await createBox({
1392
+ workspacePath: opts.workspace,
1393
+ name: opts.name,
1394
+ useSnapshot,
1395
+ checkpointRef,
1396
+ image: cfg.effective.box.image,
1397
+ withPlaywright,
1398
+ withEnv: cfg.effective.box.withEnv,
1399
+ vnc: { enabled: cfg.effective.box.vnc },
1400
+ docker: { sharedCache: cfg.effective.box.dockerCacheShared },
1401
+ limits: resolveLimits(cfg.effective.box, opts),
1402
+ projectRoot,
1403
+ onLog: (line) => s.message(clampSpinnerLine(line))
1404
+ });
1405
+ s.stop(`box ${result.record.container} ready`);
1406
+ log8.info(`id: ${result.record.id}`);
1407
+ if (typeof result.record.projectIndex === "number") {
1408
+ log8.info(`n: ${String(result.record.projectIndex)} (in ${projectRoot})`);
1409
+ }
1410
+ log8.info(`container: ${result.record.container}`);
1411
+ log8.info(`image: ${result.record.image}${result.imageBuilt ? " (built just now)" : ""}`);
1412
+ log8.info(`lower: ${result.record.lowerPath}`);
1413
+ log8.info(`upper: ${result.record.upperVolume}`);
1414
+ if (result.record.snapshotDir) {
1415
+ log8.info(`snapshot: ${result.record.snapshotDir}`);
1416
+ }
1417
+ if (result.record.checkpointSource) {
1418
+ log8.info(
1419
+ `checkpoint: ${result.record.checkpointSource.ref} (${result.record.checkpointSource.type})`
1420
+ );
1421
+ }
1422
+ for (const check of result.overlayChecks) {
1423
+ log8.success(`${check.name} \u2014 ${check.detail}`);
1424
+ }
1425
+ log8.message(
1426
+ [
1427
+ "",
1428
+ "Try it:",
1429
+ ` docker exec -it ${result.record.container} bash`,
1430
+ ` docker exec ${result.record.container} ls /workspace`,
1431
+ "",
1432
+ "Destroy:",
1433
+ ` docker rm -f ${result.record.container}`,
1434
+ ` docker volume rm ${result.record.upperVolume}`
1435
+ ].join("\n")
1436
+ );
1437
+ const m = cfg.effective.maintenance;
1438
+ if (m.pruneProjectConfigs) {
1439
+ try {
1440
+ const n = await bumpProjectGcCounter();
1441
+ if (n % m.pruneProjectConfigsEvery === 0) {
1442
+ const boxes = await listBoxes();
1443
+ const protectedPaths = boxes.map((b) => b.projectRoot).filter((p) => typeof p === "string");
1444
+ const res = await pruneOrphanProjectConfigs({ protectedPaths });
1445
+ if (res.removed.length > 0) {
1446
+ log8.info(
1447
+ `cleaned ${String(res.removed.length)} orphan project config dir(s): ` + res.removed.map((r) => r.originalPath).join(", ")
1448
+ );
1449
+ }
1450
+ }
1451
+ } catch {
1452
+ }
1453
+ }
1454
+ outro2("done");
1455
+ if (opts.attach) {
1456
+ attachShell(result.record.container);
1457
+ }
1458
+ } catch (err) {
1459
+ s.stop("failed");
1460
+ const msg = err instanceof Error ? err.message : String(err);
1461
+ log8.error(msg);
1462
+ try {
1463
+ const running = execSync('docker ps --format "{{.Names}}"', {
1464
+ stdio: ["ignore", "pipe", "ignore"]
1465
+ }).toString().split("\n").filter((n) => n.startsWith("agentbox-"));
1466
+ if (running.length > 0) {
1467
+ log8.warn(`leftover containers: ${running.join(", ")}`);
1468
+ log8.warn(`remove with: docker rm -f ${running.join(" ")}`);
1469
+ }
1470
+ } catch {
1471
+ }
1472
+ process.exit(1);
1473
+ }
1474
+ });
1475
+
1476
+ // src/commands/dashboard.ts
1477
+ import { spawn as spawn2 } from "child_process";
1478
+ import { log as log9 } from "@clack/prompts";
1479
+ import { Command as Command7 } from "commander";
1480
+
1481
+ // src/dashboard/layout.ts
1482
+ var SIDEBAR_WIDTH = 32;
1483
+ var MIN_RIGHT_W = 20;
1484
+ var MIN_RIGHT_H = 4;
1485
+ function computeLayout(cols, rows) {
1486
+ const sidebarW = Math.min(SIDEBAR_WIDTH, Math.max(0, cols - MIN_RIGHT_W - 1));
1487
+ const sepX = sidebarW;
1488
+ const rightX = sidebarW + 1;
1489
+ const rightW = Math.max(0, cols - rightX);
1490
+ const statusY = rows - 1;
1491
+ const paneH = Math.max(0, statusY);
1492
+ return {
1493
+ cols,
1494
+ rows,
1495
+ sidebar: { x: 0, y: 0, w: sidebarW, h: paneH },
1496
+ sepX,
1497
+ right: { x: rightX, y: 0, w: rightW, h: paneH },
1498
+ statusY,
1499
+ tooSmall: rightW < MIN_RIGHT_W || paneH < MIN_RIGHT_H
1500
+ };
1501
+ }
1502
+
1503
+ // src/dashboard/renderer.ts
1504
+ var RESET = "\x1B[0m";
1505
+ function fgParams(c) {
1506
+ if (c.kind === "default") return "39";
1507
+ if (c.kind === "palette") {
1508
+ const n = c.n;
1509
+ if (n < 8) return String(30 + n);
1510
+ if (n < 16) return String(90 + (n - 8));
1511
+ return `38;5;${String(n)}`;
1512
+ }
1513
+ return `38;2;${String(c.rgb >> 16 & 255)};${String(c.rgb >> 8 & 255)};${String(c.rgb & 255)}`;
1514
+ }
1515
+ function bgParams(c) {
1516
+ if (c.kind === "default") return "49";
1517
+ if (c.kind === "palette") {
1518
+ const n = c.n;
1519
+ if (n < 8) return String(40 + n);
1520
+ if (n < 16) return String(100 + (n - 8));
1521
+ return `48;5;${String(n)}`;
1522
+ }
1523
+ return `48;2;${String(c.rgb >> 16 & 255)};${String(c.rgb >> 8 & 255)};${String(c.rgb & 255)}`;
1524
+ }
1525
+ function sgrFor(cell) {
1526
+ const parts = ["0", fgParams(cell.fg), bgParams(cell.bg)];
1527
+ if (cell.bold) parts.push("1");
1528
+ if (cell.dim) parts.push("2");
1529
+ if (cell.italic) parts.push("3");
1530
+ if (cell.underline) parts.push("4");
1531
+ if (cell.inverse) parts.push("7");
1532
+ if (cell.invisible) parts.push("8");
1533
+ if (cell.strike) parts.push("9");
1534
+ return `\x1B[${parts.join(";")}m`;
1535
+ }
1536
+ function composeRow(snap, y) {
1537
+ let out = "";
1538
+ let lastSgr = "";
1539
+ for (let x = 0; x < snap.cols; x++) {
1540
+ const cell = snap.cell(x, y);
1541
+ if (cell.width === 0) continue;
1542
+ const sgr = sgrFor(cell);
1543
+ if (sgr !== lastSgr) {
1544
+ out += sgr;
1545
+ lastSgr = sgr;
1546
+ }
1547
+ out += cell.chars === "" ? " " : cell.chars;
1548
+ }
1549
+ return out + RESET;
1550
+ }
1551
+ function cursorTo(row0, col0) {
1552
+ return `\x1B[${String(row0 + 1)};${String(col0 + 1)}H`;
1553
+ }
1554
+ function diffFrame(prev, snap, rect) {
1555
+ const h = Math.min(rect.h, snap.rows);
1556
+ const rows = new Array(h);
1557
+ let out = "\x1B[?25l";
1558
+ for (let i = 0; i < h; i++) {
1559
+ const payload = composeRow(snap, i);
1560
+ rows[i] = payload;
1561
+ if (prev && prev[i] === payload) continue;
1562
+ out += cursorTo(rect.y + i, rect.x) + RESET + payload + RESET;
1563
+ }
1564
+ if (snap.cursor.visible) {
1565
+ const cy = Math.min(Math.max(snap.cursor.y, 0), h - 1);
1566
+ const cx = Math.min(Math.max(snap.cursor.x, 0), rect.w - 1);
1567
+ out += cursorTo(rect.y + cy, rect.x + cx) + "\x1B[?25h";
1568
+ }
1569
+ return { out, rows };
1570
+ }
1571
+
1572
+ // src/dashboard/input.ts
1573
+ var LEADER = 1;
1574
+ var ESC = 27;
1575
+ var InputParser = class {
1576
+ state = "normal";
1577
+ esc = [];
1578
+ fwd = [];
1579
+ timer = null;
1580
+ timerId = 0;
1581
+ leaderMs;
1582
+ escMs;
1583
+ setTimer;
1584
+ clearTimer;
1585
+ onEvent;
1586
+ mouseTransform;
1587
+ constructor(opts) {
1588
+ this.onEvent = opts.onEvent;
1589
+ this.mouseTransform = opts.mouseTransform;
1590
+ this.leaderMs = opts.leaderMs ?? 700;
1591
+ this.escMs = opts.escMs ?? 50;
1592
+ this.setTimer = opts.setTimer ?? ((ms, fn) => setTimeout(fn, ms));
1593
+ this.clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
1594
+ }
1595
+ feed(buf) {
1596
+ let i = 0;
1597
+ while (i < buf.length) {
1598
+ const b = buf[i];
1599
+ if (this.state === "normal") {
1600
+ if (b === LEADER) {
1601
+ this.flush();
1602
+ this.state = "leader";
1603
+ this.arm(this.leaderMs, "leader");
1604
+ } else if (b === ESC) {
1605
+ this.flush();
1606
+ this.state = "esc";
1607
+ this.esc = [ESC];
1608
+ this.arm(this.escMs, "esc");
1609
+ } else {
1610
+ this.fwd.push(b);
1611
+ }
1612
+ i++;
1613
+ continue;
1614
+ }
1615
+ if (this.state === "leader") {
1616
+ this.disarm();
1617
+ if (b === LEADER) {
1618
+ this.fwd.push(LEADER);
1619
+ this.flush();
1620
+ } else {
1621
+ const c = String.fromCharCode(b);
1622
+ if (c === "v") this.onEvent({ type: "action", name: "vnc" });
1623
+ else if (c === "w") this.onEvent({ type: "action", name: "web" });
1624
+ else if (c === "c") this.onEvent({ type: "action", name: "code" });
1625
+ else if (c === "q" || c === "d") this.onEvent({ type: "quit" });
1626
+ else if (c === "k" || c === "p" || c === "P") this.onEvent({ type: "switch", dir: "prev" });
1627
+ else if (c === "j" || c === "n" || c === "N") this.onEvent({ type: "switch", dir: "next" });
1628
+ else {
1629
+ this.fwd.push(b);
1630
+ this.flush();
1631
+ }
1632
+ }
1633
+ this.state = "normal";
1634
+ i++;
1635
+ continue;
1636
+ }
1637
+ if (this.state === "mouseX10") {
1638
+ this.esc.push(b);
1639
+ if (this.esc.length === 6) {
1640
+ this.disarm();
1641
+ this.emitMouseX10();
1642
+ this.reset();
1643
+ } else {
1644
+ this.arm(this.escMs, "esc");
1645
+ }
1646
+ i++;
1647
+ continue;
1648
+ }
1649
+ if (this.esc.length === 1) {
1650
+ if (b === 91 || b === 79) {
1651
+ this.esc.push(b);
1652
+ this.arm(this.escMs, "esc");
1653
+ i++;
1654
+ continue;
1655
+ }
1656
+ this.disarm();
1657
+ this.forwardVerbatim([ESC]);
1658
+ this.reset();
1659
+ continue;
1660
+ }
1661
+ if (this.esc[1] === 91 && this.esc.length === 2 && b === 77) {
1662
+ this.esc.push(b);
1663
+ this.state = "mouseX10";
1664
+ this.arm(this.escMs, "esc");
1665
+ i++;
1666
+ continue;
1667
+ }
1668
+ this.esc.push(b);
1669
+ const isFinal = this.esc[1] === 79 ? this.esc.length === 3 : b >= 64 && b <= 126;
1670
+ const isParam = b >= 32 && b <= 63;
1671
+ if (isFinal) {
1672
+ this.disarm();
1673
+ this.classifyCsi();
1674
+ this.reset();
1675
+ } else if (isParam || this.esc[1] === 79) {
1676
+ this.arm(this.escMs, "esc");
1677
+ } else {
1678
+ this.disarm();
1679
+ this.forwardVerbatim(this.esc);
1680
+ this.reset();
1681
+ }
1682
+ i++;
1683
+ }
1684
+ if (this.state === "normal") this.flush();
1685
+ }
1686
+ dispose() {
1687
+ this.disarm();
1688
+ }
1689
+ classifyCsi() {
1690
+ const s = String.fromCharCode(...this.esc);
1691
+ if (s === "\x1B[1;7A") return void this.onEvent({ type: "switch", dir: "prev" });
1692
+ if (s === "\x1B[1;7B") return void this.onEvent({ type: "switch", dir: "next" });
1693
+ const m = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(s);
1694
+ if (m) return this.emitMouseSgr(m);
1695
+ this.forwardVerbatim(this.esc);
1696
+ }
1697
+ emitMouseSgr(m) {
1698
+ if (!this.mouseTransform) {
1699
+ this.forwardVerbatim(this.esc);
1700
+ return;
1701
+ }
1702
+ const t = this.mouseTransform(Number(m[2]), Number(m[3]));
1703
+ if (!t) return;
1704
+ this.forwardVerbatim([
1705
+ ...Buffer.from(`\x1B[<${m[1]};${String(t.x)};${String(t.y)}${m[4]}`, "latin1")
1706
+ ]);
1707
+ }
1708
+ emitMouseX10() {
1709
+ const e = this.esc;
1710
+ if (e.length !== 6 || !this.mouseTransform) {
1711
+ this.forwardVerbatim(e);
1712
+ return;
1713
+ }
1714
+ const t = this.mouseTransform(e[4] - 32, e[5] - 32);
1715
+ if (!t) return;
1716
+ this.forwardVerbatim([27, 91, 77, e[3], t.x + 32, t.y + 32]);
1717
+ }
1718
+ reset() {
1719
+ this.state = "normal";
1720
+ this.esc = [];
1721
+ }
1722
+ flush() {
1723
+ if (this.fwd.length === 0) return;
1724
+ this.onEvent({ type: "forward", bytes: Buffer.from(this.fwd) });
1725
+ this.fwd = [];
1726
+ }
1727
+ forwardVerbatim(bytes) {
1728
+ for (const x of bytes) this.fwd.push(x);
1729
+ this.flush();
1730
+ }
1731
+ arm(ms, kind) {
1732
+ this.disarm();
1733
+ const id = ++this.timerId;
1734
+ this.timer = this.setTimer(ms, () => {
1735
+ if (id !== this.timerId) return;
1736
+ this.timer = null;
1737
+ if (kind === "leader" && this.state === "leader") {
1738
+ this.fwd.push(LEADER);
1739
+ this.flush();
1740
+ this.state = "normal";
1741
+ } else if (kind === "esc" && (this.state === "esc" || this.state === "mouseX10")) {
1742
+ this.forwardVerbatim(this.esc);
1743
+ this.reset();
1744
+ }
1745
+ });
1746
+ }
1747
+ disarm() {
1748
+ this.timerId++;
1749
+ if (this.timer != null) {
1750
+ this.clearTimer(this.timer);
1751
+ this.timer = null;
1752
+ }
1753
+ }
1754
+ };
1755
+
1756
+ // src/dashboard/pty-session.ts
1757
+ function fgSpec(c) {
1758
+ if (c.isFgDefault()) return { kind: "default" };
1759
+ if (c.isFgPalette()) return { kind: "palette", n: c.getFgColor() };
1760
+ if (c.isFgRGB()) return { kind: "rgb", rgb: c.getFgColor() };
1761
+ return { kind: "default" };
1762
+ }
1763
+ function bgSpec(c) {
1764
+ if (c.isBgDefault()) return { kind: "default" };
1765
+ if (c.isBgPalette()) return { kind: "palette", n: c.getBgColor() };
1766
+ if (c.isBgRGB()) return { kind: "rgb", rgb: c.getBgColor() };
1767
+ return { kind: "default" };
1768
+ }
1769
+ var MOUSE_ENABLE_SEQ = "\x1B[?1000h\x1B[?1002h\x1B[?1006h";
1770
+ var MOUSE_MODES = [1e3, 1002, 1003, 1005, 1006, 1015];
1771
+ var MOUSE_DISABLE_SEQ = MOUSE_MODES.map((n) => `\x1B[?${String(n)}l`).join("");
1772
+ var BLANK = {
1773
+ width: 1,
1774
+ chars: " ",
1775
+ fg: { kind: "default" },
1776
+ bg: { kind: "default" },
1777
+ bold: false,
1778
+ dim: false,
1779
+ italic: false,
1780
+ underline: false,
1781
+ inverse: false,
1782
+ invisible: false,
1783
+ strike: false
1784
+ };
1785
+ var PtySession = class {
1786
+ term;
1787
+ pty;
1788
+ disposed = false;
1789
+ // Reused per cell read — valid only until the next cell() call (the renderer
1790
+ // consumes it synchronously within composeRow).
1791
+ out = { ...BLANK };
1792
+ constructor(spawn5, TerminalClass, dockerArgv, cols, rows, onRenderable, onExit) {
1793
+ this.term = new TerminalClass({
1794
+ cols,
1795
+ rows,
1796
+ allowProposedApi: true,
1797
+ scrollback: 0,
1798
+ convertEol: false
1799
+ });
1800
+ this.pty = spawn5("docker", dockerArgv, {
1801
+ name: "xterm-256color",
1802
+ cols,
1803
+ rows,
1804
+ env: process.env
1805
+ });
1806
+ this.pty.onData((d) => {
1807
+ this.term.write(d, () => onRenderable());
1808
+ });
1809
+ this.term.onData((d) => {
1810
+ if (!this.disposed) this.pty.write(d);
1811
+ });
1812
+ this.pty.onExit(() => {
1813
+ if (!this.disposed) onExit();
1814
+ });
1815
+ }
1816
+ write(bytes) {
1817
+ if (!this.disposed) this.pty.write(bytes.toString("utf8"));
1818
+ }
1819
+ resize(cols, rows) {
1820
+ if (this.disposed) return;
1821
+ this.term.resize(cols, rows);
1822
+ this.pty.resize(cols, rows);
1823
+ }
1824
+ snapshot() {
1825
+ const buf = this.term.buffer.active;
1826
+ const base = buf.baseY;
1827
+ const cell = buf.getNullCell();
1828
+ const o = this.out;
1829
+ return {
1830
+ cols: this.term.cols,
1831
+ rows: this.term.rows,
1832
+ cursor: { x: buf.cursorX, y: buf.cursorY, visible: true },
1833
+ cell: (x, y) => {
1834
+ const line = buf.getLine(base + y);
1835
+ if (!line) return BLANK;
1836
+ line.getCell(x, cell);
1837
+ o.width = cell.getWidth();
1838
+ o.chars = cell.getChars();
1839
+ o.fg = fgSpec(cell);
1840
+ o.bg = bgSpec(cell);
1841
+ o.bold = Boolean(cell.isBold());
1842
+ o.dim = Boolean(cell.isDim());
1843
+ o.italic = Boolean(cell.isItalic());
1844
+ o.underline = Boolean(cell.isUnderline());
1845
+ o.inverse = Boolean(cell.isInverse());
1846
+ o.invisible = Boolean(cell.isInvisible());
1847
+ o.strike = Boolean(cell.isStrikethrough());
1848
+ return o;
1849
+ }
1850
+ };
1851
+ }
1852
+ dispose() {
1853
+ if (this.disposed) return;
1854
+ this.disposed = true;
1855
+ try {
1856
+ this.pty.kill();
1857
+ } catch {
1858
+ }
1859
+ this.term.dispose();
1860
+ }
1861
+ };
1862
+
1863
+ // src/dashboard/sidebar.ts
1864
+ function activityCell(b) {
1865
+ if (b.state !== "running") return `[${b.state}]`;
1866
+ switch (b.claudeActivity) {
1867
+ case "working":
1868
+ return "\u25CF working";
1869
+ case "idle":
1870
+ return "\u25CB idle";
1871
+ case "waiting":
1872
+ return "\u25D0 waiting";
1873
+ default:
1874
+ return "? unknown";
1875
+ }
1876
+ }
1877
+ var NEW_BOX_ID = "__agentbox_new__";
1878
+ var NEW_BOX_LABEL = "+ New box";
1879
+ var SIDEBAR_HEADER = "\u2550 AgentBox \u2550";
1880
+ var SIDEBAR_HEADER_LINES = 2;
1881
+ function fit(s, w) {
1882
+ if (s.length === w) return s;
1883
+ if (s.length > w) return s.slice(0, w);
1884
+ return s + " ".repeat(w - s.length);
1885
+ }
1886
+ function center(s, w) {
1887
+ if (s.length >= w) return s.slice(0, w);
1888
+ const pad = w - s.length;
1889
+ const leftPad = Math.floor(pad / 2);
1890
+ return " ".repeat(leftPad) + s + " ".repeat(pad - leftPad);
1891
+ }
1892
+ function sidebarLines(boxes, selectedId, w, h) {
1893
+ const lines = [center(SIDEBAR_HEADER, w), fit("", w)];
1894
+ const nameW = Math.min(16, Math.max(6, ...boxes.map((b) => b.name.length), 6));
1895
+ for (const b of boxes) {
1896
+ const marker = b.id === selectedId ? "\u25B8 " : " ";
1897
+ const row2 = b.id === NEW_BOX_ID ? `${marker}${NEW_BOX_LABEL}` : `${marker}${fit(b.name, nameW)} ${activityCell(b)}`;
1898
+ lines.push(fit(row2, w));
1899
+ }
1900
+ if (boxes.length === 0) lines.push(fit(" (no boxes)", w));
1901
+ while (lines.length < h) lines.push(fit("", w));
1902
+ return lines.slice(0, h);
1903
+ }
1904
+ function menuLines(boxName, w, h) {
1905
+ const body = [
1906
+ "",
1907
+ ` No Claude session in ${boxName}.`,
1908
+ "",
1909
+ " [c] Start Claude here",
1910
+ " [s] Open a shell",
1911
+ "",
1912
+ " Ctrl+Option+\u2191/\u2193 switch \xB7 Ctrl-a then v/c/w/q (vnc/code/web/quit)"
1913
+ ];
1914
+ const top = Math.max(0, Math.floor((h - body.length) / 2));
1915
+ const out = [];
1916
+ for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
1917
+ return out;
1918
+ }
1919
+ function createMenuLines(where, w, h) {
1920
+ const body = [
1921
+ "",
1922
+ " Create a new box",
1923
+ "",
1924
+ " [c] Create + launch Claude",
1925
+ " [n] Create only",
1926
+ "",
1927
+ ` in ${where}`,
1928
+ "",
1929
+ " Ctrl+Option+\u2191/\u2193 switch \xB7 Ctrl-a then q quit"
1930
+ ];
1931
+ const top = Math.max(0, Math.floor((h - body.length) / 2));
1932
+ const out = [];
1933
+ for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
1934
+ return out;
1935
+ }
1936
+ var BAR_BG = "\x1B[48;2;48;48;48m";
1937
+ var BAR_BASE = BAR_BG + "\x1B[38;5;250m";
1938
+ var BAR_BRAND = "\x1B[48;5;39m\x1B[38;5;16m";
1939
+ var BRAND_BOLD = "\x1B[1m";
1940
+ var BRAND_NOBOLD = "\x1B[22m";
1941
+ var HINT_KEY = "\x1B[38;5;255m";
1942
+ var HINT_TXT = "\x1B[38;5;245m";
1943
+ var BAR_RESET = "\x1B[0m";
1944
+ var HINT_GROUPS = [
1945
+ ["Control+Option+Up/Down", "switch"],
1946
+ ["Control+a c", "code"],
1947
+ ["Control+a v", "vnc"],
1948
+ ["Control+a w", "web"],
1949
+ ["Control+a q", "quit"]
1950
+ ];
1951
+ function statusLine(box, w, stateLabel) {
1952
+ const state = stateLabel ?? (box ? box.state === "running" ? box.claudeActivity ?? "unknown" : box.state : "");
1953
+ const brandPrefix = box ? " agentbox \u25B8 " : " agentbox ";
1954
+ const brandMain = box ? `${box.name} (${state}) ` : "";
1955
+ const left = brandPrefix + brandMain;
1956
+ const leftStyled = BAR_BRAND + brandPrefix + BRAND_BOLD + brandMain + BRAND_NOBOLD;
1957
+ const SEP = " \u2502 ";
1958
+ const rightPlain = HINT_GROUPS.map(([k, l]) => `${k}: ${l}`).join(SEP) + " ";
1959
+ const rightStyled = HINT_GROUPS.map(([k, l]) => `${HINT_KEY}${k}${HINT_TXT}: ${l}`).join(
1960
+ `${HINT_TXT}${SEP}`
1961
+ ) + " ";
1962
+ if (left.length + rightPlain.length + 1 > w) {
1963
+ return BAR_BASE + BAR_BRAND + fit(left, w) + BAR_RESET;
1964
+ }
1965
+ const gap = w - left.length - rightPlain.length;
1966
+ return BAR_BASE + leftStyled + BAR_BASE + " ".repeat(gap) + rightStyled + BAR_RESET;
1967
+ }
1968
+
1969
+ // src/dashboard/compositor.ts
1970
+ var SB_BODY = BAR_BG + "\x1B[38;5;250m";
1971
+ var SB_HEADER = BAR_BG + "\x1B[38;5;39m\x1B[1m";
1972
+ var SB_SELECTED = BAR_BG + "\x1B[38;5;255m\x1B[1m";
1973
+ var SGR_RESET = "\x1B[0m";
1974
+ var POLL_MS = 1e3;
1975
+ var FRAME_MS = 16;
1976
+ var RESIZE_DEBOUNCE_MS = 120;
1977
+ var SYNC_BEGIN = "\x1B[?2026h";
1978
+ var SYNC_END = "\x1B[?2026l";
1979
+ function cursorTo2(x, y) {
1980
+ return `\x1B[${String(y + 1)};${String(x + 1)}H`;
1981
+ }
1982
+ var Compositor = class {
1983
+ constructor(deps, initialId) {
1984
+ this.deps = deps;
1985
+ this.selectedId = initialId;
1986
+ this.layout = computeLayout(this.out.columns ?? 100, this.out.rows ?? 30);
1987
+ this.parser = new InputParser({
1988
+ onEvent: (e) => {
1989
+ if (e.type === "quit") this.onSig();
1990
+ else if (e.type === "switch") this.switchBox(e.dir);
1991
+ else if (e.type === "action") void this.doAction(e.name);
1992
+ else if (this.createMenu) this.handleCreateMenuKey(e.bytes);
1993
+ else if (this.menu) this.handleMenuKey(e.bytes);
1994
+ else this.session?.write(e.bytes);
1995
+ },
1996
+ // Absolute 1-based host coords → right-pane-local 1-based; null = the
1997
+ // pointer is over the sidebar/status, so Claude shouldn't see it.
1998
+ mouseTransform: (x, y) => {
1999
+ const r = this.layout.right;
2000
+ if (!this.session || this.layout.tooSmall) return null;
2001
+ const lx = x - r.x;
2002
+ const ly = y - r.y;
2003
+ if (lx < 1 || ly < 1 || lx > r.w || ly > r.h) return null;
2004
+ return { x: lx, y: ly };
2005
+ }
2006
+ });
2007
+ }
2008
+ deps;
2009
+ out = process.stdout;
2010
+ inp = process.stdin;
2011
+ boxes = [];
2012
+ selectedId;
2013
+ session = null;
2014
+ placeholder = null;
2015
+ menu = null;
2016
+ createMenu = null;
2017
+ activeMode = "claude";
2018
+ flashMsg = null;
2019
+ flashTimer = null;
2020
+ /** True while a start-Claude / open-shell action is in flight (suppresses
2021
+ * the poll respawn so it can't interrupt the transition). */
2022
+ busy = false;
2023
+ layout;
2024
+ prevRows = null;
2025
+ renderTimer = null;
2026
+ pollTimer = null;
2027
+ resizeTimer = null;
2028
+ parser;
2029
+ tornDown = false;
2030
+ resolveDone = null;
2031
+ onData = (d) => this.parser.feed(d);
2032
+ onResize = () => this.scheduleResize();
2033
+ onSig = () => {
2034
+ this.teardown();
2035
+ process.exit(0);
2036
+ };
2037
+ onFatal = (err) => {
2038
+ this.teardown();
2039
+ process.stderr.write(`dashboard: ${err instanceof Error ? err.stack ?? err.message : String(err)}
2040
+ `);
2041
+ process.exit(1);
2042
+ };
2043
+ async run() {
2044
+ this.out.write("\x1B[?1049h\x1B[?25l\x1B[2J" + MOUSE_ENABLE_SEQ);
2045
+ if (this.inp.isTTY) this.inp.setRawMode(true);
2046
+ this.inp.resume();
2047
+ this.inp.on("data", this.onData);
2048
+ this.out.on("resize", this.onResize);
2049
+ process.once("SIGINT", this.onSig);
2050
+ process.once("SIGTERM", this.onSig);
2051
+ process.once("uncaughtException", this.onFatal);
2052
+ process.once("unhandledRejection", this.onFatal);
2053
+ process.once("exit", () => this.teardown());
2054
+ await this.refreshBoxes();
2055
+ if (!this.boxes.some((b) => b.id === this.selectedId) && this.boxes[0]) {
2056
+ this.selectedId = this.boxes[0].id;
2057
+ }
2058
+ await this.spawnActive();
2059
+ this.drawChrome();
2060
+ this.scheduleRender();
2061
+ this.pollTimer = setInterval(() => void this.poll(), POLL_MS);
2062
+ await new Promise((resolve) => {
2063
+ this.resolveDone = resolve;
2064
+ });
2065
+ }
2066
+ async refreshBoxes() {
2067
+ try {
2068
+ this.boxes = await this.deps.listCandidates();
2069
+ } catch {
2070
+ }
2071
+ }
2072
+ selectedBox() {
2073
+ return this.boxes.find((b) => b.id === this.selectedId);
2074
+ }
2075
+ async poll() {
2076
+ const before = JSON.stringify(this.boxes.map((b) => [b.id, b.state, b.claudeActivity]));
2077
+ await this.refreshBoxes();
2078
+ if (this.busy) {
2079
+ } else if (!this.boxes.some((b) => b.id === this.selectedId) && this.boxes[0]) {
2080
+ this.selectedId = this.boxes[0].id;
2081
+ await this.spawnActive();
2082
+ } else {
2083
+ const box = this.selectedBox();
2084
+ const running = box?.state === "running";
2085
+ const reresolve = this.session && !running || this.placeholder && running || this.menu && !running;
2086
+ if (reresolve) await this.spawnActive();
2087
+ }
2088
+ if (JSON.stringify(this.boxes.map((b) => [b.id, b.state, b.claudeActivity])) !== before) {
2089
+ this.drawChrome();
2090
+ }
2091
+ }
2092
+ disposeSession() {
2093
+ if (!this.session) return;
2094
+ this.session.dispose();
2095
+ this.session = null;
2096
+ }
2097
+ async spawnActive() {
2098
+ this.disposeSession();
2099
+ this.placeholder = null;
2100
+ this.menu = null;
2101
+ this.createMenu = null;
2102
+ this.clearRightPane();
2103
+ const id = this.selectedId;
2104
+ const target = await this.deps.resolveTarget(id);
2105
+ if (this.selectedId !== id || this.tornDown) return;
2106
+ this.applyTarget(target);
2107
+ }
2108
+ /** Turn a resolved/started target into the right-pane state. */
2109
+ applyTarget(target) {
2110
+ this.disposeSession();
2111
+ this.placeholder = null;
2112
+ this.menu = null;
2113
+ this.createMenu = null;
2114
+ if (target.kind === "attach") {
2115
+ this.activeMode = target.mode ?? "claude";
2116
+ this.session = new PtySession(
2117
+ this.deps.ptySpawn,
2118
+ this.deps.termCtor,
2119
+ target.argv,
2120
+ Math.max(1, this.layout.right.w),
2121
+ Math.max(1, this.layout.right.h),
2122
+ () => this.scheduleRender(),
2123
+ () => this.onSessionExit()
2124
+ );
2125
+ } else if (target.kind === "menu") {
2126
+ this.menu = { boxName: this.selectedBox()?.name ?? this.selectedId };
2127
+ } else if (target.kind === "create-menu") {
2128
+ this.createMenu = { where: target.where };
2129
+ } else {
2130
+ this.placeholder = target.lines;
2131
+ }
2132
+ this.prevRows = null;
2133
+ this.drawChrome();
2134
+ this.scheduleRender();
2135
+ }
2136
+ handleMenuKey(bytes) {
2137
+ for (const b of bytes) {
2138
+ if (b === 99 || b === 13 || b === 10) {
2139
+ void this.chooseAction("claude");
2140
+ return;
2141
+ }
2142
+ if (b === 115) {
2143
+ void this.chooseAction("shell");
2144
+ return;
2145
+ }
2146
+ }
2147
+ }
2148
+ async chooseAction(which) {
2149
+ if (this.busy) return;
2150
+ const id = this.selectedId;
2151
+ const name = this.selectedBox()?.name ?? id;
2152
+ this.busy = true;
2153
+ this.menu = null;
2154
+ this.createMenu = null;
2155
+ this.placeholder = ["", which === "claude" ? " Starting Claude\u2026" : " Opening shell\u2026"];
2156
+ this.prevRows = null;
2157
+ this.drawChrome();
2158
+ this.scheduleRender();
2159
+ try {
2160
+ const target = which === "claude" ? await this.deps.startClaude(id) : await this.deps.openShell(id);
2161
+ if (this.selectedId !== id || this.tornDown) return;
2162
+ this.applyTarget(target);
2163
+ } catch (err) {
2164
+ if (this.selectedId !== id || this.tornDown) return;
2165
+ const msg = err instanceof Error ? err.message : String(err);
2166
+ this.placeholder = [
2167
+ "",
2168
+ ` Failed to ${which === "claude" ? "start Claude" : "open a shell"} in ${name}:`,
2169
+ ` ${msg}`,
2170
+ "",
2171
+ which === "claude" ? ` Try from a shell: agentbox claude start ${name}` : ` Try from a shell: agentbox shell ${name}`
2172
+ ];
2173
+ this.prevRows = null;
2174
+ this.scheduleRender();
2175
+ } finally {
2176
+ this.busy = false;
2177
+ }
2178
+ }
2179
+ handleCreateMenuKey(bytes) {
2180
+ for (const b of bytes) {
2181
+ if (b === 99 || b === 13 || b === 10) {
2182
+ void this.chooseCreate(true);
2183
+ return;
2184
+ }
2185
+ if (b === 110) {
2186
+ void this.chooseCreate(false);
2187
+ return;
2188
+ }
2189
+ }
2190
+ }
2191
+ async chooseCreate(withClaude) {
2192
+ if (this.busy) return;
2193
+ this.busy = true;
2194
+ this.menu = null;
2195
+ this.createMenu = null;
2196
+ this.placeholder = ["", " Creating box\u2026", ""];
2197
+ this.prevRows = null;
2198
+ this.drawChrome();
2199
+ this.scheduleRender();
2200
+ try {
2201
+ const { boxId, attach } = await this.deps.createNewBox(withClaude, (line) => {
2202
+ if (this.tornDown) return;
2203
+ this.placeholder = ["", " Creating box\u2026", " " + line];
2204
+ this.prevRows = null;
2205
+ this.scheduleRender();
2206
+ });
2207
+ if (this.tornDown) return;
2208
+ this.selectedId = boxId;
2209
+ await this.refreshBoxes();
2210
+ if (attach) {
2211
+ this.applyTarget(attach);
2212
+ } else {
2213
+ await this.spawnActive();
2214
+ this.flash("box created");
2215
+ }
2216
+ } catch (err) {
2217
+ if (this.tornDown) return;
2218
+ const msg = err instanceof Error ? err.message : String(err);
2219
+ this.placeholder = ["", " Failed to create box:", ` ${msg}`, "", " Try from a shell: agentbox create"];
2220
+ this.prevRows = null;
2221
+ this.drawChrome();
2222
+ this.scheduleRender();
2223
+ } finally {
2224
+ this.busy = false;
2225
+ }
2226
+ }
2227
+ async doAction(name) {
2228
+ if (this.selectedId === NEW_BOX_ID) {
2229
+ this.flash("select a box first");
2230
+ return;
2231
+ }
2232
+ const id = this.selectedId;
2233
+ let msg;
2234
+ try {
2235
+ msg = name === "vnc" ? await this.deps.openVnc(id) : name === "code" ? await this.deps.openCode(id) : await this.deps.openWeb(id);
2236
+ } catch (err) {
2237
+ msg = err instanceof Error ? err.message : String(err);
2238
+ }
2239
+ this.flash(msg);
2240
+ }
2241
+ /** Briefly show `msg` in the status row, then revert. */
2242
+ flash(msg) {
2243
+ this.flashMsg = msg;
2244
+ if (this.flashTimer) clearTimeout(this.flashTimer);
2245
+ this.flashTimer = setTimeout(() => {
2246
+ this.flashTimer = null;
2247
+ this.flashMsg = null;
2248
+ this.drawChrome();
2249
+ }, 2500);
2250
+ this.drawChrome();
2251
+ }
2252
+ onSessionExit() {
2253
+ this.disposeSession();
2254
+ this.placeholder = ["", " session ended \u2014 Ctrl-a \u2191/\u2193 to switch boxes"];
2255
+ this.prevRows = null;
2256
+ this.scheduleRender();
2257
+ }
2258
+ switchBox(dir) {
2259
+ if (this.boxes.length === 0) return;
2260
+ const i = Math.max(
2261
+ 0,
2262
+ this.boxes.findIndex((b) => b.id === this.selectedId)
2263
+ );
2264
+ const n = this.boxes.length;
2265
+ const next = dir === "prev" ? (i - 1 + n) % n : (i + 1) % n;
2266
+ this.selectedId = this.boxes[next].id;
2267
+ this.drawChrome();
2268
+ void this.spawnActive();
2269
+ }
2270
+ /** Blank the right pane and drop the diff cache (next paint is full). */
2271
+ clearRightPane() {
2272
+ const r = this.layout.right;
2273
+ let s = SYNC_BEGIN + "\x1B[?25l";
2274
+ for (let i = 0; i < r.h; i++) {
2275
+ s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + " ".repeat(r.w);
2276
+ }
2277
+ this.out.write(s + SYNC_END);
2278
+ this.prevRows = null;
2279
+ }
2280
+ scheduleRender() {
2281
+ if (this.renderTimer || this.tornDown) return;
2282
+ this.renderTimer = setTimeout(() => {
2283
+ this.renderTimer = null;
2284
+ this.render();
2285
+ }, FRAME_MS);
2286
+ }
2287
+ render() {
2288
+ if (this.tornDown) return;
2289
+ const r = this.layout.right;
2290
+ if (this.layout.tooSmall) {
2291
+ this.out.write(cursorTo2(0, 0) + "\x1B[2J" + cursorTo2(0, 0) + "terminal too small");
2292
+ return;
2293
+ }
2294
+ if (this.session) {
2295
+ const { out, rows } = diffFrame(this.prevRows, this.session.snapshot(), r);
2296
+ this.prevRows = rows;
2297
+ if (out) this.out.write(SYNC_BEGIN + out + SYNC_END);
2298
+ } else if (this.menu) {
2299
+ const lines = menuLines(this.menu.boxName, r.w, r.h);
2300
+ let s = SYNC_BEGIN + "\x1B[?25l";
2301
+ for (let i = 0; i < r.h; i++) s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + (lines[i] ?? "");
2302
+ this.out.write(s + SYNC_END);
2303
+ } else if (this.createMenu) {
2304
+ const lines = createMenuLines(this.createMenu.where, r.w, r.h);
2305
+ let s = SYNC_BEGIN + "\x1B[?25l";
2306
+ for (let i = 0; i < r.h; i++) s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + (lines[i] ?? "");
2307
+ this.out.write(s + SYNC_END);
2308
+ } else if (this.placeholder) {
2309
+ let s = SYNC_BEGIN + "\x1B[?25l";
2310
+ for (let i = 0; i < r.h; i++) {
2311
+ const line = (this.placeholder[i] ?? "").slice(0, r.w);
2312
+ s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + line + " ".repeat(Math.max(0, r.w - line.length));
2313
+ }
2314
+ this.out.write(s + SYNC_END);
2315
+ }
2316
+ }
2317
+ drawChrome() {
2318
+ if (this.tornDown || this.layout.tooSmall) return;
2319
+ const { sidebar, sepX, statusY } = this.layout;
2320
+ const lines = sidebarLines(this.boxes, this.selectedId, sidebar.w, sidebar.h);
2321
+ const selIdx = this.boxes.findIndex((b) => b.id === this.selectedId);
2322
+ const selRow = selIdx >= 0 ? SIDEBAR_HEADER_LINES + selIdx : -1;
2323
+ let s = SYNC_BEGIN + "\x1B[0m";
2324
+ for (let i = 0; i < lines.length; i++) {
2325
+ const style = i === 0 ? SB_HEADER : i === selRow ? SB_SELECTED : SB_BODY;
2326
+ s += cursorTo2(0, i) + style + lines[i] + SGR_RESET;
2327
+ }
2328
+ for (let y = 0; y < sidebar.h; y++) s += cursorTo2(sepX, y) + "\u2502";
2329
+ let status;
2330
+ if (this.flashMsg) {
2331
+ const w = this.layout.cols;
2332
+ const txt = ` ${this.flashMsg} `.slice(0, w).padEnd(w);
2333
+ status = `\x1B[7m${txt}\x1B[0m`;
2334
+ } else {
2335
+ const stateLabel = this.selectedId === NEW_BOX_ID ? "create" : this.menu ? "menu" : this.session && this.activeMode === "shell" ? "shell" : void 0;
2336
+ status = statusLine(this.selectedBox(), this.layout.cols, stateLabel);
2337
+ }
2338
+ s += cursorTo2(0, statusY) + status;
2339
+ this.out.write(s + SYNC_END);
2340
+ }
2341
+ scheduleResize() {
2342
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
2343
+ this.resizeTimer = setTimeout(() => {
2344
+ this.resizeTimer = null;
2345
+ this.layout = computeLayout(this.out.columns ?? 100, this.out.rows ?? 30);
2346
+ this.prevRows = null;
2347
+ const r = this.layout.right;
2348
+ if (this.session && !this.layout.tooSmall) {
2349
+ this.session.resize(Math.max(1, r.w), Math.max(1, r.h));
2350
+ }
2351
+ this.out.write(SYNC_BEGIN + "\x1B[2J" + SYNC_END);
2352
+ this.drawChrome();
2353
+ this.render();
2354
+ }, RESIZE_DEBOUNCE_MS);
2355
+ }
2356
+ teardown() {
2357
+ if (this.tornDown) return;
2358
+ this.tornDown = true;
2359
+ if (this.renderTimer) clearTimeout(this.renderTimer);
2360
+ if (this.pollTimer) clearInterval(this.pollTimer);
2361
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
2362
+ if (this.flashTimer) clearTimeout(this.flashTimer);
2363
+ this.parser.dispose();
2364
+ this.disposeSession();
2365
+ this.inp.off("data", this.onData);
2366
+ this.out.off("resize", this.onResize);
2367
+ if (this.inp.isTTY) this.inp.setRawMode(false);
2368
+ this.inp.pause();
2369
+ this.out.write(MOUSE_DISABLE_SEQ + "\x1B[?25h\x1B[0m\x1B[?1049l");
2370
+ this.resolveDone?.();
2371
+ }
2372
+ };
2373
+
2374
+ // src/commands/dashboard.ts
2375
+ function sortBoxes(boxes) {
2376
+ return [...boxes].sort((a, b) => {
2377
+ const ap = a.projectRoot ?? "";
2378
+ const bp = b.projectRoot ?? "";
2379
+ if (ap !== bp) return ap.localeCompare(bp);
2380
+ const ai = a.projectIndex ?? Number.POSITIVE_INFINITY;
2381
+ const bi = b.projectIndex ?? Number.POSITIVE_INFINITY;
2382
+ if (ai !== bi) return ai - bi;
2383
+ return a.name.localeCompare(b.name);
2384
+ });
2385
+ }
2386
+ function scoped(all, projectRoot, boxes) {
2387
+ return sortBoxes(all ? boxes : boxes.filter((b) => b.projectRoot === projectRoot));
2388
+ }
2389
+ function toSidebar(b) {
2390
+ return { id: b.id, name: b.name, state: b.state, claudeActivity: b.claudeActivity };
2391
+ }
2392
+ var dashboardCommand = new Command7("dashboard").description("Box list + the selected box live Agent session").argument("[box]", "initial box (default: first running box; -p restricts to the cwd project)").option("-p, --project", "only this project's boxes (default: all boxes globally)").action(async (idOrName, opts) => {
2393
+ try {
2394
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
2395
+ log9.error("agentbox dashboard needs an interactive terminal");
2396
+ process.exit(2);
2397
+ }
2398
+ let ptySpawn;
2399
+ let termCtor;
2400
+ try {
2401
+ const ptyMod = await import("@homebridge/node-pty-prebuilt-multiarch");
2402
+ const xtermMod = await import("@xterm/headless");
2403
+ const spawn5 = ptyMod["spawn"] ?? ptyMod["default"]?.["spawn"];
2404
+ const Terminal = xtermMod["Terminal"] ?? xtermMod["default"]?.["Terminal"];
2405
+ if (typeof spawn5 !== "function" || typeof Terminal !== "function") {
2406
+ throw new Error("terminal backend missing expected exports");
2407
+ }
2408
+ ptySpawn = spawn5;
2409
+ termCtor = Terminal;
2410
+ } catch {
2411
+ log9.error(
2412
+ "agentbox dashboard is unavailable here (native terminal backend failed to load)"
2413
+ );
2414
+ log9.info("use `agentbox claude` / `agentbox claude attach` instead");
2415
+ process.exit(2);
2416
+ }
2417
+ const project = await findProjectRoot(process.cwd());
2418
+ let showAll = !opts.project;
2419
+ const full = await listBoxes();
2420
+ const scoped0 = scoped(showAll, project.root, full);
2421
+ let initialId;
2422
+ if (idOrName !== void 0) {
2423
+ const picked = await resolveBoxOrExit(idOrName);
2424
+ initialId = picked.id;
2425
+ if (!scoped0.some((b) => b.id === picked.id)) showAll = true;
2426
+ } else if (scoped0.length === 0) {
2427
+ initialId = NEW_BOX_ID;
2428
+ } else {
2429
+ initialId = (scoped0.find((b) => b.state === "running") ?? scoped0[0]).id;
2430
+ }
2431
+ const newBoxEntry = { id: NEW_BOX_ID, name: NEW_BOX_LABEL, state: "new" };
2432
+ const listCandidates = async () => [
2433
+ newBoxEntry,
2434
+ ...scoped(showAll, project.root, await listBoxes()).map(toSidebar)
2435
+ ];
2436
+ const resolveTarget = async (boxId) => {
2437
+ if (boxId === NEW_BOX_ID) return { kind: "create-menu", where: project.root };
2438
+ const box = (await listBoxes()).find((b) => b.id === boxId);
2439
+ if (!box) return { kind: "placeholder", lines: ["", " box not found"] };
2440
+ if (box.state !== "running") {
2441
+ return {
2442
+ kind: "placeholder",
2443
+ lines: [
2444
+ "",
2445
+ ` box ${box.name} is ${box.state}.`,
2446
+ ` Start it: agentbox start ${box.name}`
2447
+ ]
2448
+ };
2449
+ }
2450
+ const info = await claudeSessionInfo(box.container);
2451
+ if (info.running) {
2452
+ return {
2453
+ kind: "attach",
2454
+ argv: buildClaudeDashboardAttachArgv(box.container, info.sessionName)
2455
+ };
2456
+ }
2457
+ return { kind: "menu" };
2458
+ };
2459
+ const findBox2 = async (boxId) => {
2460
+ const box = (await listBoxes()).find((b) => b.id === boxId);
2461
+ if (!box) throw new Error("box not found");
2462
+ if (box.state !== "running") throw new Error(`box is ${box.state}`);
2463
+ return box;
2464
+ };
2465
+ const startClaude = async (boxId) => {
2466
+ const box = await findBox2(boxId);
2467
+ await rebuildPluginNativeDeps(box.container, {
2468
+ volume: box.claudeConfigVolume
2469
+ });
2470
+ await startClaudeSession({ container: box.container, claudeArgs: [], boxName: box.name });
2471
+ const info = await claudeSessionInfo(box.container);
2472
+ return {
2473
+ kind: "attach",
2474
+ argv: buildClaudeDashboardAttachArgv(box.container, info.sessionName),
2475
+ mode: "claude"
2476
+ };
2477
+ };
2478
+ const openShell = async (boxId) => {
2479
+ const box = await findBox2(boxId);
2480
+ return { kind: "attach", argv: buildShellArgv(box.container), mode: "shell" };
2481
+ };
2482
+ const createNewBox = async (withClaude, onProgress) => {
2483
+ const cfg = await loadEffectiveConfig(project.root);
2484
+ const auth = await resolveClaudeAuth(process.env);
2485
+ const checkpointRef = cfg.effective.box.defaultCheckpoint.length > 0 ? cfg.effective.box.defaultCheckpoint : void 0;
2486
+ const result = await createBox({
2487
+ workspacePath: project.root,
2488
+ useSnapshot: cfg.effective.box.hostSnapshot ?? true,
2489
+ checkpointRef,
2490
+ image: cfg.effective.box.image,
2491
+ claudeConfig: { isolate: cfg.effective.box.isolateClaudeConfig },
2492
+ claudeEnv: auth.env,
2493
+ withPlaywright: cfg.effective.box.withPlaywright || cfg.effective.browser.default !== "agent-browser",
2494
+ withEnv: cfg.effective.box.withEnv,
2495
+ vnc: { enabled: cfg.effective.box.vnc },
2496
+ docker: { sharedCache: cfg.effective.box.dockerCacheShared },
2497
+ limits: resolveLimits(cfg.effective.box, {}),
2498
+ projectRoot: project.root,
2499
+ onLog: onProgress
2500
+ });
2501
+ if (!withClaude) return { boxId: result.record.id };
2502
+ await rebuildPluginNativeDeps(result.record.container, {
2503
+ volume: result.record.claudeConfigVolume
2504
+ });
2505
+ await startClaudeSession({
2506
+ container: result.record.container,
2507
+ claudeArgs: [],
2508
+ boxName: result.record.name
2509
+ });
2510
+ const info = await claudeSessionInfo(result.record.container);
2511
+ return {
2512
+ boxId: result.record.id,
2513
+ attach: {
2514
+ kind: "attach",
2515
+ argv: buildClaudeDashboardAttachArgv(result.record.container, info.sessionName),
2516
+ mode: "claude"
2517
+ }
2518
+ };
2519
+ };
2520
+ const detach = (cmd, args) => {
2521
+ spawn2(cmd, args, { detached: true, stdio: "ignore" }).unref();
2522
+ };
2523
+ const findEndpointUrl = async (boxId, kind) => {
2524
+ const box = (await listBoxes()).find((b) => b.id === boxId);
2525
+ if (!box) return { name: boxId, url: null };
2526
+ const ep = box.endpoints.endpoints.find((e) => e.kind === kind);
2527
+ return { name: box.name, url: ep && ep.reachable && ep.url ? ep.url : null };
2528
+ };
2529
+ const openVnc = async (boxId) => {
2530
+ const { url } = await findEndpointUrl(boxId, "vnc");
2531
+ if (!url) return "VNC not available for this box";
2532
+ try {
2533
+ const box = await findBox2(boxId);
2534
+ const br = await ensureBoxBrowser(box.container);
2535
+ if (!br.up) return `VNC: in-box browser unavailable (${br.reason ?? "box not running?"})`;
2536
+ } catch {
2537
+ }
2538
+ detach("open", [url]);
2539
+ return "Opening VNC in browser\u2026";
2540
+ };
2541
+ const openWeb = async (boxId) => {
2542
+ const box = (await listBoxes()).find((b) => b.id === boxId);
2543
+ if (!box) return "box not found";
2544
+ const ep = box.endpoints.endpoints.find((e) => e.kind === "web");
2545
+ const url = ep && ep.reachable && ep.url ? ep.url : `http://${box.endpoints.domain}`;
2546
+ detach("open", [url]);
2547
+ return `Opening ${url.replace(/^https?:\/\//, "")}\u2026`;
2548
+ };
2549
+ const openCode = async (boxId) => {
2550
+ const box = (await listBoxes()).find((b) => b.id === boxId);
2551
+ if (!box) return "box not found";
2552
+ detach(process.execPath, [process.argv[1], "code", box.name, "--no-wait"]);
2553
+ return "Launching VS Code / Cursor\u2026";
2554
+ };
2555
+ const compositor = new Compositor(
2556
+ {
2557
+ ptySpawn,
2558
+ termCtor,
2559
+ listCandidates,
2560
+ resolveTarget,
2561
+ startClaude,
2562
+ openShell,
2563
+ createNewBox,
2564
+ openVnc,
2565
+ openCode,
2566
+ openWeb
2567
+ },
2568
+ initialId
2569
+ );
2570
+ await compositor.run();
2571
+ process.exit(0);
2572
+ } catch (err) {
2573
+ handleLifecycleError(err);
2574
+ }
2575
+ });
2576
+
2577
+ // src/commands/destroy.ts
2578
+ import { confirm as confirm4, isCancel as isCancel4, log as log10 } from "@clack/prompts";
2579
+ import { Command as Command8 } from "commander";
2580
+ var destroyCommand = new Command8("destroy").alias("rm").description("Destroy a box and discard its upper volume").argument(
2581
+ "[box]",
2582
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
2583
+ ).option("-y, --yes", "skip the confirmation prompt").option("--keep-snapshot", "don't delete the snapshot dir under ~/.agentbox/snapshots/").action(async (idOrName, opts) => {
2584
+ try {
2585
+ const box = await resolveBoxOrExit(idOrName);
2586
+ if (!opts.yes) {
2587
+ log10.warn(`This will discard the upper volume \u2014 agent work-in-progress is lost.`);
2588
+ log10.info(`id: ${box.id}`);
2589
+ log10.info(`container: ${box.container}`);
2590
+ log10.info(`upper: ${box.upperVolume}`);
2591
+ if (box.snapshotDir) {
2592
+ log10.info(`snapshot: ${box.snapshotDir}${opts.keepSnapshot ? " (will be kept)" : ""}`);
2593
+ }
2594
+ const ok = await confirm4({
2595
+ message: "Destroy this box?",
2596
+ initialValue: false
2597
+ });
2598
+ if (isCancel4(ok) || !ok) {
2599
+ log10.info("cancelled");
2600
+ return;
2601
+ }
2602
+ }
2603
+ const result = await destroyBox(box.id, { keepSnapshot: opts.keepSnapshot });
2604
+ const out = [`destroyed ${result.record.container}`];
2605
+ if (result.removedContainer) out.push(" \u2713 container removed");
2606
+ out.push(` \u2713 volumes removed: ${result.removedVolumes.join(", ")}`);
2607
+ if (result.removedSnapshot) out.push(` \u2713 snapshot removed: ${result.removedSnapshot}`);
2608
+ else if (box.snapshotDir && opts.keepSnapshot) {
2609
+ out.push(` \xB7 snapshot kept: ${box.snapshotDir}`);
2610
+ }
2611
+ process.stdout.write(out.join("\n") + "\n");
2612
+ } catch (err) {
2613
+ handleLifecycleError(err);
2614
+ }
2615
+ });
2616
+
2617
+ // src/commands/list.ts
2618
+ import { log as log11 } from "@clack/prompts";
2619
+ import { Command as Command9 } from "commander";
2620
+ import { pathToFileURL } from "url";
2621
+
2622
+ // src/hyperlink.ts
2623
+ import { supportsHyperlink } from "supports-hyperlinks";
2624
+ var ESC2 = "\x1B";
2625
+ var ST = `${ESC2}\\`;
2626
+ function hyperlink(label, url, stream) {
2627
+ const out = stream ?? process.stdout;
2628
+ if (!supportsHyperlink(out)) return label;
2629
+ return `${ESC2}]8;;${url}${ST}${label}${ESC2}]8;;${ST}`;
2630
+ }
2631
+
2632
+ // src/watch.ts
2633
+ function withWatchOptions(cmd) {
2634
+ return cmd.option("-w, --watch", "redraw continuously until interrupted (Ctrl-C)").option("--interval <seconds>", "refresh interval for --watch", "2");
2635
+ }
2636
+ function parseIntervalMs(raw) {
2637
+ const n = Number(raw);
2638
+ if (!Number.isFinite(n) || n <= 0) return 2e3;
2639
+ return Math.max(250, Math.round(n * 1e3));
2640
+ }
2641
+ async function watchRender(produce, rawInterval) {
2642
+ const ms = parseIntervalMs(rawInterval);
2643
+ const intervalLabel = `${String(ms / 1e3)}s`;
2644
+ process.stdout.write("\x1B[?25l");
2645
+ process.once("exit", () => process.stdout.write("\x1B[?25h"));
2646
+ process.once("SIGINT", () => process.exit(0));
2647
+ const sleep = (d) => new Promise((r) => setTimeout(r, d));
2648
+ for (; ; ) {
2649
+ let body;
2650
+ try {
2651
+ body = await produce();
2652
+ } catch (err) {
2653
+ body = `error: ${err instanceof Error ? err.message : String(err)}`;
2654
+ }
2655
+ const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
2656
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
2657
+ process.stdout.write(
2658
+ `watching every ${intervalLabel} \u2014 ${ts} \u2014 Ctrl-C to exit
2659
+
2660
+ ${body.replace(/\n+$/, "")}
2661
+ `
2662
+ );
2663
+ await sleep(ms);
2664
+ }
2665
+ }
2666
+
2667
+ // src/commands/list.ts
2668
+ var plain = (s) => ({ text: s, width: s.length });
2669
+ function middleTruncate(s, n) {
2670
+ if (s.length <= n) return s;
2671
+ if (n <= 1) return s.length > 0 ? "\u2026" : "";
2672
+ const slash = s.lastIndexOf("/");
2673
+ const tail = slash >= 0 ? s.slice(slash) : "";
2674
+ if (tail.length > 0 && tail.length + 2 <= n) {
2675
+ const head = s.slice(0, n - 1 - tail.length);
2676
+ return `${head}\u2026${tail}`;
2677
+ }
2678
+ return s.slice(0, n - 1) + "\u2026";
2679
+ }
2680
+ function urlCell(box, stream) {
2681
+ const eps = box.endpoints.endpoints;
2682
+ const vnc = eps.find((e) => e.kind === "vnc" && e.url);
2683
+ const primary = eps.find((e) => e.kind === "web" && e.url) ?? eps.find((e) => e.kind === "service" && e.url) ?? vnc;
2684
+ if (!primary?.url) return plain("");
2685
+ let display;
2686
+ try {
2687
+ display = new URL(primary.url).host;
2688
+ } catch {
2689
+ display = primary.url;
2690
+ }
2691
+ const parts = [
2692
+ { text: hyperlink(display, primary.url, stream), width: display.length }
2693
+ ];
2694
+ if (vnc?.url && vnc !== primary) {
2695
+ const label = "(VNC)";
2696
+ parts.push({ text: hyperlink(label, vnc.url, stream), width: label.length });
2697
+ }
2698
+ const sep = " ";
2699
+ return {
2700
+ text: parts.map((p) => p.text).join(sep),
2701
+ width: parts.reduce((a, p) => a + p.width, 0) + sep.length * (parts.length - 1)
2702
+ };
2703
+ }
2704
+ function workspaceCell(path, target, stream) {
2705
+ const display = middleTruncate(path, target);
2706
+ let url;
2707
+ try {
2708
+ url = pathToFileURL(path).href;
2709
+ } catch {
2710
+ return { text: display, width: display.length };
2711
+ }
2712
+ return { text: hyperlink(display, url, stream), width: display.length };
2713
+ }
2714
+ function renderTable(boxes, stream) {
2715
+ const header = ["N", "NAME", "STATE", "CLAUDE", "URL", "WORKSPACE"];
2716
+ const lead = boxes.map((b) => [
2717
+ plain(typeof b.projectIndex === "number" ? String(b.projectIndex) : ""),
2718
+ plain(b.name),
2719
+ plain(b.state),
2720
+ plain(b.claudeActivity ?? ""),
2721
+ urlCell(b, stream)
2722
+ ]);
2723
+ const leadHeader = header.slice(0, 5).map(plain);
2724
+ const fixedWidths = [0, 1, 2, 3, 4].map(
2725
+ (col) => Math.max(leadHeader[col]?.width ?? 0, ...lead.map((r) => r[col]?.width ?? 0))
2726
+ );
2727
+ const term2 = stream.columns && stream.columns > 0 ? stream.columns : 120;
2728
+ const fixedTotal = fixedWidths.reduce((a, b) => a + b, 0) + header.length * 2;
2729
+ const naturalWs = Math.max(
2730
+ header[5]?.length ?? 0,
2731
+ ...boxes.map((b) => b.workspacePath.length)
2732
+ );
2733
+ const wsWidth = Math.min(naturalWs, Math.max(16, term2 - fixedTotal));
2734
+ const widths = [...fixedWidths, wsWidth];
2735
+ const rows = boxes.map((b, idx) => [
2736
+ ...lead[idx],
2737
+ workspaceCell(b.workspacePath, wsWidth, stream)
2738
+ ]);
2739
+ const all = [[...leadHeader, plain(header[5])], ...rows];
2740
+ const padCell = (cell, col) => {
2741
+ const target = widths[col] ?? 0;
2742
+ return cell.text + " ".repeat(Math.max(0, target - cell.width));
2743
+ };
2744
+ return all.map(
2745
+ (row2) => row2.map((cell, i) => padCell(cell ?? plain(""), i)).join(" ").trimEnd()
2746
+ ).join("\n");
2747
+ }
2748
+ async function buildListText() {
2749
+ const boxes = await listBoxes();
2750
+ if (boxes.length === 0) return "no boxes \u2014 run `agentbox create` to make one";
2751
+ return renderTable(boxes, process.stdout);
2752
+ }
2753
+ var listCommand2 = withWatchOptions(
2754
+ new Command9("list").alias("ls").description("List all known agent boxes").option("-j, --json", "machine-readable JSON output")
2755
+ ).action(async (opts) => {
2756
+ if (opts.json && opts.watch) {
2757
+ log11.error("cannot combine --json with --watch");
2758
+ process.exit(2);
2759
+ }
2760
+ if (opts.watch) {
2761
+ await watchRender(buildListText, opts.interval);
2762
+ return;
2763
+ }
2764
+ if (opts.json) {
2765
+ const boxes = await listBoxes();
2766
+ process.stdout.write(JSON.stringify(boxes, null, 2) + "\n");
2767
+ return;
2768
+ }
2769
+ process.stdout.write(await buildListText() + "\n");
2770
+ });
2771
+
2772
+ // src/commands/logs.ts
2773
+ import { log as log12 } from "@clack/prompts";
2774
+ import { Command as Command10 } from "commander";
2775
+ import { spawn as spawn3 } from "child_process";
2776
+ var logsCommand = new Command10("logs").description("Print recent log lines from a box service; -f to stream").argument(
2777
+ "[box]",
2778
+ "box ref (optional when cwd has exactly 1 box): project index, id, id prefix, name, or container"
2779
+ ).argument("[service]", "service name from agentbox.yaml").option("-n, --tail <n>", "how many recent lines to print first", "200").option("-f, --follow", "keep the connection open and stream new lines").action(async (boxArg, serviceArg, opts) => {
2780
+ try {
2781
+ let idOrName;
2782
+ let service;
2783
+ if (serviceArg !== void 0) {
2784
+ idOrName = boxArg;
2785
+ service = serviceArg;
2786
+ } else {
2787
+ idOrName = void 0;
2788
+ service = boxArg;
2789
+ }
2790
+ if (!service) {
2791
+ log12.error("missing <service> argument");
2792
+ log12.info("usage: agentbox logs [box] <service> [-n N] [-f]");
2793
+ process.exit(2);
2794
+ }
2795
+ const box = await resolveBoxOrExit(idOrName);
2796
+ const tail = String(Number.parseInt(opts.tail, 10) || 200);
2797
+ const args = ["agentbox-ctl", "logs", service, "--tail", tail];
2798
+ if (opts.follow) args.push("--follow");
2799
+ if (!opts.follow) {
2800
+ const proc = await execInBox(box.container, args, { user: "vscode" });
2801
+ if (proc.exitCode !== 0) {
2802
+ log12.error(`agentbox-ctl logs failed: ${proc.stderr || proc.stdout}`);
2803
+ process.exit(1);
2804
+ }
2805
+ process.stdout.write(proc.stdout);
2806
+ if (!proc.stdout.endsWith("\n")) process.stdout.write("\n");
2807
+ return;
2808
+ }
2809
+ const child = spawn3("docker", ["exec", "--user", "vscode", box.container, ...args], {
2810
+ stdio: ["ignore", "inherit", "inherit"]
2811
+ });
2812
+ child.on("exit", (code) => process.exit(code ?? 0));
2813
+ } catch (err) {
2814
+ handleLifecycleError(err);
2815
+ }
2816
+ });
2817
+
2818
+ // src/commands/open.ts
2819
+ import { log as log13 } from "@clack/prompts";
2820
+ import { Command as Command11 } from "commander";
2821
+
2822
+ // src/commands/path.ts
2823
+ async function runPath(box, opts) {
2824
+ try {
2825
+ const layer = opts.upper ? "upper" : "merged";
2826
+ const { record, paths } = await getBoxHostPaths(box.id);
2827
+ if (opts.refresh) {
2828
+ const refreshed = await refreshExport(record, {
2829
+ layer,
2830
+ includeNodeModules: opts.includeNodeModules
2831
+ });
2832
+ process.stdout.write(`${refreshed.hostPath}
2833
+ `);
2834
+ return;
2835
+ }
2836
+ const path = layer === "upper" ? paths.upperLiveOnHost ?? paths.upperExport : paths.mergedExport;
2837
+ process.stdout.write(`${path}
2838
+ `);
2839
+ } catch (err) {
2840
+ handleLifecycleError(err);
2841
+ }
2842
+ }
2843
+
2844
+ // src/commands/open.ts
2845
+ var openCommand = new Command11("open").description("Open a box's merged workspace in Finder (snapshot of the agent's view)").argument(
2846
+ "[box]",
2847
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
2848
+ ).option("--upper", "open just the writes layer (live on OrbStack, snapshot on Docker Desktop)").option("--no-refresh", "skip the rsync; open whatever's already on disk").option(
2849
+ "--include-node-modules",
2850
+ "include /workspace/node_modules in the merged export (off by default)"
2851
+ ).option("--path", "print the host workspace path instead of launching Finder").option("--print", "alias of --path").action(async (idOrName, opts) => {
2852
+ try {
2853
+ const box = await resolveBoxOrExit(idOrName);
2854
+ if (opts.path || opts.print) {
2855
+ await runPath(box, {
2856
+ upper: opts.upper,
2857
+ refresh: opts.refresh,
2858
+ // print refreshes by default; --no-refresh skips
2859
+ includeNodeModules: opts.includeNodeModules
2860
+ });
2861
+ return;
2862
+ }
2863
+ const layer = opts.upper ? "upper" : "merged";
2864
+ const result = await openBoxInFinder(box.id, {
2865
+ layer,
2866
+ includeNodeModules: opts.includeNodeModules,
2867
+ noRefresh: !opts.refresh,
2868
+ noOpen: false
2869
+ });
2870
+ const liveNote = !result.copied ? " (live)" : result.usedFallback ? " (tar fallback)" : "";
2871
+ process.stdout.write(`opened ${result.hostPath}${liveNote}
2872
+ `);
2873
+ if (opts.upper && result.engine !== "orbstack" && result.copied) {
2874
+ log13.info(
2875
+ "Tip: live upper-layer browsing requires OrbStack. Re-run `agentbox open --upper` to refresh."
2876
+ );
2877
+ }
2878
+ } catch (err) {
2879
+ handleLifecycleError(err);
2880
+ }
2881
+ });
2882
+
2883
+ // src/commands/pause.ts
2884
+ import { Command as Command12 } from "commander";
2885
+ var pauseCommand = new Command12("pause").description("Freeze a box (docker pause \u2014 0 CPU, RAM stays mapped)").argument(
2886
+ "[box]",
2887
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
2888
+ ).action(async (idOrName) => {
2889
+ try {
2890
+ const box = await resolveBoxOrExit(idOrName);
2891
+ const record = await pauseBox(box.id);
2892
+ process.stdout.write(`paused ${record.container}
2893
+ `);
2894
+ } catch (err) {
2895
+ handleLifecycleError(err);
2896
+ }
2897
+ });
2898
+
2899
+ // src/commands/prune.ts
2900
+ import { confirm as confirm5, isCancel as isCancel5, log as log14 } from "@clack/prompts";
2901
+ import { Command as Command13 } from "commander";
2902
+ function totalRemovals(r, projectConfigs) {
2903
+ return r.removedRecords.length + r.removedContainers.length + r.removedVolumes.length + r.removedSnapshotDirs.length + r.removedBoxDirs.length + projectConfigs.length;
2904
+ }
2905
+ function summary(r, projectConfigs) {
2906
+ const lines = [];
2907
+ if (r.removedRecords.length > 0) {
2908
+ lines.push(
2909
+ ` state records (${String(r.removedRecords.length)}): ${r.removedRecords.join(", ")}`
2910
+ );
2911
+ }
2912
+ if (r.removedContainers.length > 0) {
2913
+ lines.push(
2914
+ ` containers (${String(r.removedContainers.length)}): ${r.removedContainers.join(", ")}`
2915
+ );
2916
+ }
2917
+ if (r.removedVolumes.length > 0) {
2918
+ lines.push(
2919
+ ` volumes (${String(r.removedVolumes.length)}): ${r.removedVolumes.join(", ")}`
2920
+ );
2921
+ }
2922
+ if (r.removedSnapshotDirs.length > 0) {
2923
+ lines.push(
2924
+ ` snapshot dirs (${String(r.removedSnapshotDirs.length)}): ${r.removedSnapshotDirs.join(", ")}`
2925
+ );
2926
+ }
2927
+ if (r.removedBoxDirs.length > 0) {
2928
+ lines.push(
2929
+ ` box dirs (${String(r.removedBoxDirs.length)}): ${r.removedBoxDirs.join(", ")}`
2930
+ );
2931
+ }
2932
+ if (projectConfigs.length > 0) {
2933
+ lines.push(
2934
+ ` project configs (${String(projectConfigs.length)}): ${projectConfigs.join(", ")}`
2935
+ );
2936
+ }
2937
+ return lines.length > 0 ? lines.join("\n") : " (nothing to remove)";
2938
+ }
2939
+ async function liveProjectRoots() {
2940
+ try {
2941
+ const boxes = await listBoxes();
2942
+ return boxes.map((b) => b.projectRoot).filter((p) => typeof p === "string");
2943
+ } catch {
2944
+ return [];
2945
+ }
2946
+ }
2947
+ var pruneCommand = new Command13("prune").description("Clean up orphan state.json records (and with --all, orphan docker resources)").option("--dry-run", "show what would be removed, don't change anything").option(
2948
+ "--all",
2949
+ "also remove orphan agentbox-* containers, volumes, snapshot dirs, and orphan per-project config dirs"
2950
+ ).option("-y, --yes", "skip the confirmation prompt").action(async (opts) => {
2951
+ try {
2952
+ const dryRun = opts.dryRun ?? false;
2953
+ const protectedPaths = opts.all ? await liveProjectRoots() : [];
2954
+ const preview = await pruneBoxes({ dryRun: true, all: opts.all });
2955
+ const previewProjects = opts.all ? (await pruneOrphanProjectConfigs({ dryRun: true, protectedPaths })).removed.map(
2956
+ (r) => r.originalPath
2957
+ ) : [];
2958
+ if (totalRemovals(preview, previewProjects) === 0) {
2959
+ process.stdout.write("nothing to prune\n");
2960
+ return;
2961
+ }
2962
+ log14.info(`would remove:
2963
+ ${summary(preview, previewProjects)}`);
2964
+ if (dryRun) return;
2965
+ if (!opts.yes) {
2966
+ const ok = await confirm5({ message: "Proceed with prune?", initialValue: true });
2967
+ if (isCancel5(ok) || !ok) {
2968
+ log14.info("cancelled");
2969
+ return;
2970
+ }
2971
+ }
2972
+ const result = await pruneBoxes({ all: opts.all });
2973
+ const removedProjects = opts.all ? (await pruneOrphanProjectConfigs({ protectedPaths })).removed.map((r) => r.originalPath) : [];
2974
+ process.stdout.write(`pruned:
2975
+ ${summary(result, removedProjects)}
2976
+ `);
2977
+ } catch (err) {
2978
+ handleLifecycleError(err);
2979
+ }
2980
+ });
2981
+
2982
+ // src/commands/pull.ts
2983
+ import { confirm as confirm9, isCancel as isCancel9, log as log18 } from "@clack/prompts";
2984
+ import { Command as Command17 } from "commander";
2985
+
2986
+ // src/commands/pull-claude.ts
2987
+ import { confirm as confirm6, isCancel as isCancel6, log as log15 } from "@clack/prompts";
2988
+ import { Command as Command14 } from "commander";
2989
+ function tag(item) {
2990
+ const noun = item.category === "plugins" ? "plugin" : item.category.replace(/s$/, "");
2991
+ return ` ${item.category}/${item.name} (new ${noun})`;
2992
+ }
2993
+ var pullClaudeCommand = new Command14("claude").description(
2994
+ "Pull box-installed Claude skills/plugins/agents/commands back to host ~/.claude (additive)"
2995
+ ).argument(
2996
+ "[box]",
2997
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
2998
+ ).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "list new items and exit; don't write").action(async (idOrName, opts) => {
2999
+ try {
3000
+ const box = await resolveBoxOrExit(idOrName);
3001
+ const volume = box.claudeConfigVolume ?? resolveClaudeVolume({ isolate: false, boxId: box.id }).volume;
3002
+ if (volume === SHARED_CLAUDE_VOLUME) {
3003
+ log15.warn(
3004
+ `Reading the shared ${SHARED_CLAUDE_VOLUME} volume \u2014 it aggregates Claude extensions installed in ANY box, not just ${box.name}.`
3005
+ );
3006
+ }
3007
+ const image = box.image || DEFAULT_BOX_IMAGE;
3008
+ const preview = await pullClaudeExtras({ volume }, { image, dryRun: true });
3009
+ if (preview.newItems.length === 0 && preview.mergedRegistries.length === 0) {
3010
+ process.stdout.write("no new Claude extensions to pull into ~/.claude\n");
3011
+ return;
3012
+ }
3013
+ for (const item of preview.newItems) process.stdout.write(`${tag(item)}
3014
+ `);
3015
+ for (const reg of preview.mergedRegistries) {
3016
+ process.stdout.write(` plugins/${reg} (merge new entries)
3017
+ `);
3018
+ }
3019
+ if (opts.dryRun) {
3020
+ process.stdout.write(
3021
+ `
3022
+ [dry-run] ${preview.newItems.length} item(s)${preview.mergedRegistries.length > 0 ? ` + ${preview.mergedRegistries.length} registry merge(s)` : ""} would be pulled into ~/.claude
3023
+ `
3024
+ );
3025
+ return;
3026
+ }
3027
+ if (!opts.yes) {
3028
+ const ok = await confirm6({
3029
+ message: `Pull ${preview.newItems.length} new Claude extension(s) into ~/.claude? (existing items are never overwritten)`,
3030
+ initialValue: false
3031
+ });
3032
+ if (isCancel6(ok) || !ok) {
3033
+ log15.info("cancelled");
3034
+ return;
3035
+ }
3036
+ }
3037
+ const result = await pullClaudeExtras({ volume }, { image, dryRun: false });
3038
+ process.stdout.write(
3039
+ `pulled ${result.newItems.length} extension(s)${result.mergedRegistries.length > 0 ? `, merged ${result.mergedRegistries.join(", ")}` : ""} into ~/.claude
3040
+ `
3041
+ );
3042
+ } catch (err) {
3043
+ handleLifecycleError(err);
3044
+ }
3045
+ });
3046
+
3047
+ // src/commands/pull-config.ts
3048
+ import { confirm as confirm7, isCancel as isCancel7, log as log16 } from "@clack/prompts";
3049
+ import { Command as Command15 } from "commander";
3050
+ function tagChange(line) {
3051
+ const sp = line.indexOf(" ");
3052
+ const code = sp === -1 ? line : line.slice(0, sp);
3053
+ const path = sp === -1 ? "" : line.slice(sp + 1);
3054
+ const isNew = /^>f\++$/.test(code);
3055
+ return ` ${path} ${isNew ? "(new)" : "(overwrites host)"}`;
3056
+ }
3057
+ var CONFIG_PATTERNS = ["agentbox.yaml"];
3058
+ var pullConfigCommand = new Command15("config").description("Pull agentbox.yaml box -> host").argument(
3059
+ "[box]",
3060
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
3061
+ ).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "list matched files and exit; don't write").option("--no-refresh", "skip the box->scratch-dir rsync step").action(async (idOrName, opts) => {
3062
+ try {
3063
+ const box = await resolveBoxOrExit(idOrName);
3064
+ const insp = await inspectBox(box.id);
3065
+ if (insp.state === "paused") {
3066
+ log16.info("box is paused; unpausing");
3067
+ await unpauseBox(box.id);
3068
+ } else if (insp.state === "stopped") {
3069
+ log16.info("box is stopped; starting (remounting overlay)");
3070
+ await startBox(box.id);
3071
+ } else if (insp.state === "missing") {
3072
+ throw new Error(`box ${box.name} has no container; was it destroyed?`);
3073
+ }
3074
+ log16.info(`agentbox.yaml bypasses gitignore and copies directly into ${box.workspacePath}`);
3075
+ const preview = await pullToHost(box, {
3076
+ dryRun: true,
3077
+ respectGitignore: false,
3078
+ envPatterns: CONFIG_PATTERNS,
3079
+ noRefresh: !opts.refresh
3080
+ });
3081
+ if (preview.changes.length === 0) {
3082
+ process.stdout.write(`no config file to pull into ${box.workspacePath}
3083
+ `);
3084
+ return;
3085
+ }
3086
+ for (const line of preview.changes) process.stdout.write(`${tagChange(line)}
3087
+ `);
3088
+ if (opts.dryRun) {
3089
+ process.stdout.write(
3090
+ `
3091
+ [dry-run] ${preview.changes.length} config file(s) would change in ${box.workspacePath}
3092
+ `
3093
+ );
3094
+ return;
3095
+ }
3096
+ if (!opts.yes) {
3097
+ const ok = await confirm7({
3098
+ message: `Pull ${preview.changes.length} config file(s) into ${box.workspacePath}? (existing files will be overwritten)`,
3099
+ initialValue: false
3100
+ });
3101
+ if (isCancel7(ok) || !ok) {
3102
+ log16.info("cancelled");
3103
+ return;
3104
+ }
3105
+ }
3106
+ const result = await pullToHost(box, {
3107
+ dryRun: false,
3108
+ respectGitignore: false,
3109
+ envPatterns: CONFIG_PATTERNS,
3110
+ // The dry-run pass above already refreshed (or intentionally skipped)
3111
+ // the scratch dir — don't rsync box->scratch a second time.
3112
+ noRefresh: true
3113
+ });
3114
+ process.stdout.write(
3115
+ `pulled ${result.changes.length} config file(s) into ${result.hostPath}
3116
+ `
3117
+ );
3118
+ } catch (err) {
3119
+ handleLifecycleError(err);
3120
+ }
3121
+ });
3122
+
3123
+ // src/commands/pull-env.ts
3124
+ import { confirm as confirm8, isCancel as isCancel8, log as log17 } from "@clack/prompts";
3125
+ import { Command as Command16 } from "commander";
3126
+ function tagChange2(line) {
3127
+ const sp = line.indexOf(" ");
3128
+ const code = sp === -1 ? line : line.slice(0, sp);
3129
+ const path = sp === -1 ? "" : line.slice(sp + 1);
3130
+ const isNew = /^>f\++$/.test(code);
3131
+ return ` ${path} ${isNew ? "(new)" : "(overwrites host)"}`;
3132
+ }
3133
+ var pullEnvCommand = new Command16("env").description(
3134
+ "Pull gitignored env/config files (.env*, .envrc, secrets.toml, agentbox.yaml, ...) box -> host"
3135
+ ).argument(
3136
+ "[box]",
3137
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
3138
+ ).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "list matched files and exit; don't write").option(
3139
+ "--pattern <glob>",
3140
+ "extra basename glob to match (repeatable, adds to defaults)",
3141
+ (v, acc) => [...acc, v],
3142
+ []
3143
+ ).option("--no-refresh", "skip the box->scratch-dir rsync step").action(async (idOrName, opts) => {
3144
+ try {
3145
+ const box = await resolveBoxOrExit(idOrName);
3146
+ const insp = await inspectBox(box.id);
3147
+ if (insp.state === "paused") {
3148
+ log17.info("box is paused; unpausing");
3149
+ await unpauseBox(box.id);
3150
+ } else if (insp.state === "stopped") {
3151
+ log17.info("box is stopped; starting (remounting overlay)");
3152
+ await startBox(box.id);
3153
+ } else if (insp.state === "missing") {
3154
+ throw new Error(`box ${box.name} has no container; was it destroyed?`);
3155
+ }
3156
+ log17.info(
3157
+ `env/config files bypass gitignore and copy directly into ${box.workspacePath}`
3158
+ );
3159
+ const patterns = [...DEFAULT_ENV_PATTERNS, ...opts.pattern];
3160
+ const preview = await pullToHost(box, {
3161
+ dryRun: true,
3162
+ respectGitignore: false,
3163
+ envPatterns: patterns,
3164
+ noRefresh: !opts.refresh
3165
+ });
3166
+ if (preview.changes.length === 0) {
3167
+ process.stdout.write(`no env/config files to pull into ${box.workspacePath}
3168
+ `);
3169
+ return;
3170
+ }
3171
+ for (const line of preview.changes) process.stdout.write(`${tagChange2(line)}
3172
+ `);
3173
+ if (opts.dryRun) {
3174
+ process.stdout.write(
3175
+ `
3176
+ [dry-run] ${preview.changes.length} env/config file(s) would change in ${box.workspacePath}
3177
+ `
3178
+ );
3179
+ return;
3180
+ }
3181
+ if (!opts.yes) {
3182
+ const ok = await confirm8({
3183
+ message: `Pull ${preview.changes.length} env/config file(s) into ${box.workspacePath}? (existing files will be overwritten)`,
3184
+ initialValue: false
3185
+ });
3186
+ if (isCancel8(ok) || !ok) {
3187
+ log17.info("cancelled");
3188
+ return;
3189
+ }
3190
+ }
3191
+ const result = await pullToHost(box, {
3192
+ dryRun: false,
3193
+ respectGitignore: false,
3194
+ envPatterns: patterns,
3195
+ // The dry-run pass above already refreshed (or intentionally skipped)
3196
+ // the scratch dir — don't rsync box->scratch a second time.
3197
+ noRefresh: true
3198
+ });
3199
+ process.stdout.write(
3200
+ `pulled ${result.changes.length} env/config file(s) into ${result.hostPath}
3201
+ `
3202
+ );
3203
+ } catch (err) {
3204
+ handleLifecycleError(err);
3205
+ }
3206
+ });
3207
+
3208
+ // src/commands/pull.ts
3209
+ var pullCommand = new Command17("pull").enablePositionalOptions().description("Pull a box's /workspace back into your host workspace dir (gitignore-aware)").argument(
3210
+ "[box]",
3211
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
3212
+ ).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "print the change list and exit; don't write").option(
3213
+ "--no-respect-gitignore",
3214
+ "disable git ls-files mode; use --exclude=node_modules,.git instead"
3215
+ ).option(
3216
+ "--include-node-modules",
3217
+ "do not exclude node_modules in fallback mode (no effect in gitignore mode)"
3218
+ ).option("--no-refresh", "skip the box->scratch-dir rsync step (use whatever's already there)").option(
3219
+ "--with-env",
3220
+ "also pull env/config files (.env*, .envrc, secrets.toml, agentbox.yaml, ...) ignoring gitignore"
3221
+ ).option(
3222
+ "--pattern <glob>",
3223
+ "extra env basename glob; only effective with --with-env (repeatable)",
3224
+ (v, acc) => [...acc, v],
3225
+ []
3226
+ ).action(async (idOrName, opts) => {
3227
+ try {
3228
+ const box = await resolveBoxOrExit(idOrName);
3229
+ const insp = await inspectBox(box.id);
3230
+ if (insp.state === "paused") {
3231
+ log18.info("box is paused; unpausing");
3232
+ await unpauseBox(box.id);
3233
+ } else if (insp.state === "stopped") {
3234
+ log18.info("box is stopped; starting (remounting overlay)");
3235
+ await startBox(box.id);
3236
+ } else if (insp.state === "missing") {
3237
+ throw new Error(`box ${box.name} has no container; was it destroyed?`);
3238
+ }
3239
+ const rootWorktree = box.gitWorktrees?.find((w) => w.kind === "root");
3240
+ if (rootWorktree) {
3241
+ log18.warn(
3242
+ `This box has been committing to branch \`${rootWorktree.branch}\` in a separate worktree.
3243
+ For a git-aware merge instead of a file copy, run from your checkout:
3244
+ git merge ${rootWorktree.branch}
3245
+ Continuing with rsync into ${box.workspacePath}`
3246
+ );
3247
+ }
3248
+ const envPatterns = opts.withEnv ? [...DEFAULT_ENV_PATTERNS, ...opts.pattern] : void 0;
3249
+ const preview = await pullToHost(box, {
3250
+ dryRun: true,
3251
+ respectGitignore: opts.respectGitignore,
3252
+ includeNodeModules: opts.includeNodeModules,
3253
+ envPatterns,
3254
+ noRefresh: !opts.refresh
3255
+ });
3256
+ if (preview.changes.length === 0) {
3257
+ process.stdout.write(`no changes to pull into ${box.workspacePath}
3258
+ `);
3259
+ return;
3260
+ }
3261
+ if (opts.dryRun) {
3262
+ for (const line of preview.changes) process.stdout.write(`${line}
3263
+ `);
3264
+ process.stdout.write(
3265
+ `
3266
+ [dry-run] ${preview.changes.length} file(s) would change in ${box.workspacePath}
3267
+ `
3268
+ );
3269
+ return;
3270
+ }
3271
+ if (!opts.yes) {
3272
+ const ok = await confirm9({
3273
+ message: `Pull ${preview.changes.length} changed file(s)${opts.withEnv ? " (incl. env/config)" : ""} into ${box.workspacePath}?`,
3274
+ initialValue: false
3275
+ });
3276
+ if (isCancel9(ok) || !ok) {
3277
+ log18.info("cancelled");
3278
+ return;
3279
+ }
3280
+ }
3281
+ const result = await pullToHost(box, {
3282
+ dryRun: false,
3283
+ respectGitignore: opts.respectGitignore,
3284
+ includeNodeModules: opts.includeNodeModules,
3285
+ envPatterns,
3286
+ // The dry-run pass above already refreshed (or intentionally skipped)
3287
+ // the scratch dir — don't rsync box->scratch a second time.
3288
+ noRefresh: true
3289
+ });
3290
+ process.stdout.write(
3291
+ `updated ${result.changes.length} file(s) in ${result.hostPath}${result.usedGitignore ? "" : " (exclude-list mode)"}
3292
+ `
3293
+ );
3294
+ } catch (err) {
3295
+ handleLifecycleError(err);
3296
+ }
3297
+ });
3298
+ pullCommand.addCommand(pullEnvCommand);
3299
+ pullCommand.addCommand(pullClaudeCommand);
3300
+ pullCommand.addCommand(pullConfigCommand);
3301
+
3302
+ // src/commands/screen.ts
3303
+ import { spawnSync as spawnSync5 } from "child_process";
3304
+ import { log as log19 } from "@clack/prompts";
3305
+ import { Command as Command18 } from "commander";
3306
+ var screenCommand = new Command18("screen").description("Open a box's VNC (noVNC) viewer in the browser (auto-unpause/start)").argument(
3307
+ "[box]",
3308
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
3309
+ ).option("--print", "print the URL to stdout instead of launching the browser").option("--loopback", "use the 127.0.0.1 URL instead of the OrbStack .orb.local URL").action(async (idOrName, opts) => {
3310
+ try {
3311
+ const box = await resolveBoxOrExit(idOrName);
3312
+ if (!box.vncEnabled) {
3313
+ throw new Error(`VNC is disabled for box ${box.name} \u2014 recreate without \`--no-vnc\``);
3314
+ }
3315
+ const insp = await inspectBox(box.id);
3316
+ if (insp.state === "paused") {
3317
+ log19.info("box is paused; unpausing");
3318
+ await unpauseBox(box.id);
3319
+ } else if (insp.state === "stopped") {
3320
+ log19.info("box is stopped; starting (remounting overlay)");
3321
+ await startBox(box.id);
3322
+ } else if (insp.state === "missing") {
3323
+ throw new Error(`box ${box.name} has no container; was it destroyed?`);
3324
+ }
3325
+ const br = await ensureBoxBrowser(box.container);
3326
+ if (br.up && !br.alreadyRunning) log19.info("started in-box browser");
3327
+ else if (!br.up) log19.warn(`could not start in-box browser: ${br.reason ?? "unknown"}`);
3328
+ const engine = await detectEngine();
3329
+ const urls = buildVncUrls(box, engine);
3330
+ const url = opts.loopback ? urls.loopbackUrl : urls.orbUrl ?? urls.loopbackUrl;
3331
+ if (!url) {
3332
+ throw new Error(
3333
+ `VNC URL unavailable (daemon may not be up); try \`agentbox inspect ${box.name}\``
3334
+ );
3335
+ }
3336
+ if (opts.print) {
3337
+ process.stdout.write(`${url}
3338
+ `);
3339
+ return;
3340
+ }
3341
+ const opened = spawnSync5("open", [url], { stdio: "inherit" });
3342
+ if (opened.status !== 0) {
3343
+ throw new Error(`open ${url} failed (exit ${String(opened.status ?? "n/a")})`);
3344
+ }
3345
+ process.stdout.write(`opened ${url}
3346
+ `);
3347
+ } catch (err) {
3348
+ handleLifecycleError(err);
3349
+ }
3350
+ });
3351
+
3352
+ // src/commands/shell.ts
3353
+ import { spawnSync as spawnSync6 } from "child_process";
3354
+ import { log as log20 } from "@clack/prompts";
3355
+ import { Command as Command19 } from "commander";
3356
+ function buildShellCliOverrides(opts) {
3357
+ const shell = {};
3358
+ if (opts.user !== void 0) shell.user = opts.user;
3359
+ if (opts.login === false) shell.login = false;
3360
+ return Object.keys(shell).length > 0 ? { shell } : {};
3361
+ }
3362
+ var shellCommand = new Command19("shell").description("Open an interactive bash shell in a box (auto-unpause/start)").argument(
3363
+ "[box]",
3364
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
3365
+ ).argument(
3366
+ "[cmd...]",
3367
+ "optional one-shot command to run instead of an interactive shell; place after `--`, e.g. `agentbox shell smoke -- ls /workspace`"
3368
+ ).option("--user <name>", "user inside the container (default from config; built-in: vscode)").option("--no-login", "invoke `bash` instead of `bash -l` (skip login profile)").action(async (idOrName, cmd, opts) => {
3369
+ try {
3370
+ const { box, shifted } = await resolveBoxOrShift(idOrName);
3371
+ const effectiveCmd = shifted && idOrName ? [idOrName, ...cmd] : cmd;
3372
+ const cfg = await loadEffectiveConfig(box.workspacePath, {
3373
+ cliOverrides: buildShellCliOverrides(opts)
3374
+ });
3375
+ const user = cfg.effective.shell.user;
3376
+ const login = cfg.effective.shell.login;
3377
+ const insp = await inspectBox(box.id);
3378
+ if (insp.state === "paused") {
3379
+ log20.info("box is paused; unpausing");
3380
+ await unpauseBox(box.id);
3381
+ } else if (insp.state === "stopped") {
3382
+ log20.info("box is stopped; starting (remounting overlay)");
3383
+ await startBox(box.id);
3384
+ } else if (insp.state === "missing") {
3385
+ throw new Error(`box ${box.name} has no container; was it destroyed?`);
3386
+ }
3387
+ const term2 = process.env["TERM"] ?? "xterm-256color";
3388
+ const bashArgs = [];
3389
+ if (login) bashArgs.push("-l");
3390
+ if (effectiveCmd.length > 0) bashArgs.push("-c", effectiveCmd.join(" "));
3391
+ const ttyFlag = process.stdout.isTTY && process.stdin.isTTY ? "-it" : "-i";
3392
+ const child = spawnSync6(
3393
+ "docker",
3394
+ [
3395
+ "exec",
3396
+ ttyFlag,
3397
+ "-e",
3398
+ `TERM=${term2}`,
3399
+ "--user",
3400
+ user,
3401
+ box.container,
3402
+ "bash",
3403
+ ...bashArgs
3404
+ ],
3405
+ { stdio: "inherit" }
3406
+ );
3407
+ process.exit(child.status ?? 0);
3408
+ } catch (err) {
3409
+ handleLifecycleError(err);
3410
+ }
3411
+ });
3412
+
3413
+ // src/commands/start.ts
3414
+ import { Command as Command20 } from "commander";
3415
+ var startCommand = new Command20("start").description("Start a stopped box (docker start + re-mount the FUSE overlay)").argument(
3416
+ "[box]",
3417
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
3418
+ ).action(async (idOrName) => {
3419
+ try {
3420
+ const box = await resolveBoxOrExit(idOrName);
3421
+ const { record, overlayChecks } = await startBox(box.id);
3422
+ process.stdout.write(`started ${record.container}
3423
+ `);
3424
+ const failed = overlayChecks.filter((c) => !c.ok);
3425
+ if (failed.length > 0) {
3426
+ for (const c of failed) {
3427
+ process.stderr.write(` \u2717 ${c.name}: ${c.detail}
3428
+ `);
3429
+ }
3430
+ process.exit(1);
3431
+ }
3432
+ for (const c of overlayChecks) {
3433
+ process.stdout.write(` \u2713 ${c.name}
3434
+ `);
3435
+ }
3436
+ } catch (err) {
3437
+ handleLifecycleError(err);
3438
+ }
3439
+ });
3440
+
3441
+ // src/commands/status.ts
3442
+ import { log as log22 } from "@clack/prompts";
3443
+ import { Command as Command21 } from "commander";
3444
+
3445
+ // src/endpoints-render.ts
3446
+ function renderEndpointLines(endpoints, stream) {
3447
+ if (endpoints.endpoints.length === 0) return [];
3448
+ const entries = [
3449
+ { name: "domain", value: endpoints.domain }
3450
+ ];
3451
+ for (const ep of endpoints.endpoints) {
3452
+ if (ep.url) {
3453
+ entries.push({ name: ep.name, value: hyperlink(ep.url, ep.url, stream) });
3454
+ } else if (ep.kind === "vnc") {
3455
+ entries.push({ name: ep.name, value: "enabled (URL unavailable \u2014 daemon may not be up)" });
3456
+ } else if (ep.kind === "web") {
3457
+ entries.push({
3458
+ name: "web",
3459
+ value: "reserved (set a service `expose:` in agentbox.yaml)"
3460
+ });
3461
+ } else {
3462
+ entries.push({
3463
+ name: ep.name,
3464
+ value: `port ${String(ep.containerPort)} (box-only \u2014 not reachable from host)`
3465
+ });
3466
+ }
3467
+ }
3468
+ const nameWidth = Math.max(...entries.map((e) => e.name.length));
3469
+ return entries.map((e) => ` ${e.name.padEnd(nameWidth)} ${e.value}`);
3470
+ }
3471
+
3472
+ // src/fmt.ts
3473
+ function fmtBytes(n) {
3474
+ if (n === null || n === void 0) return "n/a";
3475
+ if (n < 1024) return `${String(n)} B`;
3476
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
3477
+ if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
3478
+ return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
3479
+ }
3480
+ function fmtPercent(n) {
3481
+ return n === null || n === void 0 ? "\u2014" : `${n.toFixed(1)}%`;
3482
+ }
3483
+ function fmtAgo(iso) {
3484
+ if (!iso) return null;
3485
+ const then = Date.parse(iso);
3486
+ if (Number.isNaN(then)) return null;
3487
+ const secs = Math.round((Date.now() - then) / 1e3);
3488
+ if (secs < 0) return "just now";
3489
+ if (secs < 45) return "just now";
3490
+ const units = [
3491
+ ["day", 86400],
3492
+ ["hour", 3600],
3493
+ ["minute", 60],
3494
+ ["second", 1]
3495
+ ];
3496
+ for (const [name, size] of units) {
3497
+ if (secs >= size) {
3498
+ const n = Math.round(secs / size);
3499
+ return `${String(n)} ${name}${n === 1 ? "" : "s"} ago`;
3500
+ }
3501
+ }
3502
+ return "just now";
3503
+ }
3504
+
3505
+ // src/commands/inspect.ts
3506
+ import { log as log21 } from "@clack/prompts";
3507
+ function fmtLimit(n, unit) {
3508
+ return n && n > 0 ? `${String(n)}${unit}` : "unlimited";
3509
+ }
3510
+ async function renderText(i) {
3511
+ const lim = i.record.resourceLimits;
3512
+ const projectRoot = i.record.projectRoot ?? i.record.workspacePath;
3513
+ const ckptBytes = await projectCheckpointVolumeBytes(projectRoot);
3514
+ const upperHost = i.hostPaths.upperLiveOnHost ? `${i.hostPaths.upperLiveOnHost} (live)` : `${i.hostPaths.upperExport} (run \`agentbox open --upper\` to refresh)`;
3515
+ const lines = [
3516
+ `id ${i.record.id}`,
3517
+ `name ${i.record.name}`,
3518
+ `container ${i.record.container}`,
3519
+ `image ${i.record.image}`,
3520
+ `state ${i.state}`,
3521
+ `overlay ${i.overlayMounted ? "mounted at /workspace" : "not mounted"}`,
3522
+ `workspace ${i.record.workspacePath}`,
3523
+ `project ${i.record.projectRoot ?? "(unset \u2014 pre-feature box)"}`,
3524
+ `n ${typeof i.record.projectIndex === "number" ? String(i.record.projectIndex) : "(none)"}`,
3525
+ `lower ${i.record.lowerPath}`,
3526
+ `upper volume ${i.upperVolume.name}${i.upperVolume.mountpoint ? ` (${i.upperVolume.mountpoint})` : ""}`,
3527
+ `claude config ${i.record.claudeConfigVolume ?? "(none)"}`,
3528
+ `claude session ${renderClaudeSession(i)}`,
3529
+ `claude activity ${renderClaudeActivity(i)}`,
3530
+ `persisted ${renderPersisted(i)}`,
3531
+ `playwright ${i.record.withPlaywright ? "yes" : "no"}`,
3532
+ `env files ${i.record.withEnv ? "yes" : "no"}`,
3533
+ "endpoints",
3534
+ ...renderEndpoints(i),
3535
+ `mem limit ${lim?.memoryBytes ? fmtBytes(lim.memoryBytes) : "unlimited"}`,
3536
+ `cpu limit ${fmtLimit(lim?.cpus, "")}`,
3537
+ `pids limit ${fmtLimit(lim?.pidsLimit, "")}`,
3538
+ `disk limit ${lim?.disk ? `${lim.disk} (best-effort; no-op on overlay2/macOS)` : "unlimited"}`,
3539
+ `snapshot dir ${i.record.snapshotDir ?? "(none \u2014 live workspace mount)"}`,
3540
+ `snapshot size ${fmtBytes(i.snapshotSizeBytes)}`,
3541
+ `checkpoint vol ${ckptBytes === null ? "(none)" : fmtBytes(ckptBytes)}`,
3542
+ `host export ${i.hostPaths.mergedExport} (run \`agentbox open\` to refresh)`,
3543
+ `upper host ${upperHost}`,
3544
+ `created ${i.record.createdAt}`
3545
+ ];
3546
+ return lines.join("\n");
3547
+ }
3548
+ function renderClaudeSession(i) {
3549
+ if (i.claudeSession === null) return "(n/a \u2014 box not running)";
3550
+ if (!i.claudeSession.running) return `not running ("${i.claudeSession.sessionName}")`;
3551
+ const since = i.claudeSession.startedAt ? ` since ${i.claudeSession.startedAt}` : "";
3552
+ return `running ("${i.claudeSession.sessionName}")${since}`;
3553
+ }
3554
+ function renderClaudeActivity(i) {
3555
+ const c = i.persistedStatus?.claude;
3556
+ if (!c) return "(none)";
3557
+ return `${c.state}${c.updatedAt ? ` (updated ${c.updatedAt})` : ""}`;
3558
+ }
3559
+ function renderPersisted(i) {
3560
+ const s = i.persistedStatus;
3561
+ if (!s) return "(none)";
3562
+ return `${s.timestamp} (${String(s.services.length)} svc, ${String(s.tasks.length)} tasks, ${String(s.ports.length)} ports)`;
3563
+ }
3564
+ function renderEndpoints(i) {
3565
+ const lines = renderEndpointLines(i.endpoints, process.stdout);
3566
+ return lines.length > 0 ? lines : [" (none)"];
3567
+ }
3568
+ async function runInspect(box, opts) {
3569
+ try {
3570
+ if (opts.json && opts.watch) {
3571
+ log21.error("cannot combine --json with --watch");
3572
+ process.exit(2);
3573
+ }
3574
+ if (opts.watch) {
3575
+ await watchRender(async () => renderText(await inspectBox(box.id)), opts.interval);
3576
+ return;
3577
+ }
3578
+ const result = await inspectBox(box.id);
3579
+ if (opts.json) {
3580
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
3581
+ } else {
3582
+ process.stdout.write(await renderText(result) + "\n");
3583
+ }
3584
+ } catch (err) {
3585
+ handleLifecycleError(err);
3586
+ }
3587
+ }
3588
+
3589
+ // src/commands/status.ts
3590
+ var statusCommand = withWatchOptions(
3591
+ new Command21("status").description("Show service + task status from a box's agentbox-ctl daemon").argument(
3592
+ "[box]",
3593
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
3594
+ ).option("-j, --json", "machine-readable JSON output").option("--inspect", "show detailed box info (volumes, limits, paths) instead of service/task status")
3595
+ ).action(async (idOrName, opts) => {
3596
+ try {
3597
+ if (opts.json && opts.watch) {
3598
+ log22.error("cannot combine --json with --watch");
3599
+ process.exit(2);
3600
+ }
3601
+ const box = await resolveBoxOrExit(idOrName);
3602
+ if (opts.inspect) {
3603
+ await runInspect(box, { json: opts.json, watch: opts.watch, interval: opts.interval });
3604
+ return;
3605
+ }
3606
+ if (opts.watch) {
3607
+ await watchRender(() => buildStatusText(box.id, box.container), opts.interval);
3608
+ return;
3609
+ }
3610
+ if (opts.json) {
3611
+ const inspected = await inspectBox(box.id);
3612
+ const live = await fetchLive(inspected.state, box.container);
3613
+ const resources = await boxResourceStats(inspected.record);
3614
+ process.stdout.write(
3615
+ JSON.stringify(
3616
+ {
3617
+ state: inspected.state,
3618
+ source: live ? "live" : "persisted",
3619
+ ...live ?? {},
3620
+ resources,
3621
+ claudeSession: inspected.claudeSession,
3622
+ persisted: inspected.persistedStatus,
3623
+ endpoints: inspected.endpoints
3624
+ },
3625
+ null,
3626
+ 2
3627
+ ) + "\n"
3628
+ );
3629
+ return;
3630
+ }
3631
+ process.stdout.write(await buildStatusText(box.id, box.container) + "\n");
3632
+ } catch (err) {
3633
+ handleLifecycleError(err);
3634
+ }
3635
+ });
3636
+ async function fetchLive(state, container) {
3637
+ if (state !== "running") return null;
3638
+ const proc = await execInBox(container, ["agentbox-ctl", "status", "--json"], {
3639
+ user: "vscode"
3640
+ });
3641
+ if (proc.exitCode !== 0) return null;
3642
+ try {
3643
+ return JSON.parse(proc.stdout);
3644
+ } catch {
3645
+ return null;
3646
+ }
3647
+ }
3648
+ async function buildStatusText(id, container) {
3649
+ const inspected = await inspectBox(id);
3650
+ const { state, endpoints, persistedStatus } = inspected;
3651
+ const live = await fetchLive(state, container);
3652
+ const out = [];
3653
+ const epLines = renderEndpointLines(endpoints, process.stdout);
3654
+ if (epLines.length > 0) {
3655
+ out.push("ENDPOINTS", epLines.join("\n"), "");
3656
+ }
3657
+ out.push("RESOURCES", renderResources(await boxResourceStats(inspected.record)), "");
3658
+ out.push("CLAUDE", renderClaude(inspected, persistedStatus));
3659
+ if (live) {
3660
+ if (live.tasks.length > 0) {
3661
+ out.push("", "TASKS", renderTaskTable(live.tasks));
3662
+ }
3663
+ out.push("", "SERVICES", renderStatusTable(live.services));
3664
+ out.push("", "PORTS", renderPortsTable(live.ports));
3665
+ return out.join("\n");
3666
+ }
3667
+ if (!persistedStatus) {
3668
+ out.push(
3669
+ "",
3670
+ `box is ${state}; no persisted status (box predates this feature, or the relay never received a snapshot)`
3671
+ );
3672
+ return out.join("\n");
3673
+ }
3674
+ out.push("", renderPersisted2(persistedStatus, state));
3675
+ return out.join("\n");
3676
+ }
3677
+ function renderResources(s) {
3678
+ const lim = (v) => v ? ` (limit ${typeof v === "number" ? String(v) : v})` : "";
3679
+ const seg = [];
3680
+ if (s.live) {
3681
+ seg.push(`cpu ${fmtPercent(s.cpuPercent)}${lim(s.limits.cpus)}`);
3682
+ seg.push(
3683
+ `mem ${fmtBytes(s.memUsedBytes)} / ${fmtBytes(s.memLimitBytes)} (${fmtPercent(s.memPercent)})${lim(s.limits.memoryBytes ? fmtBytes(s.limits.memoryBytes) : null)}`
3684
+ );
3685
+ seg.push(`pids ${s.pids === null ? "\u2014" : String(s.pids)}${lim(s.limits.pidsLimit)}`);
3686
+ } else {
3687
+ seg.push("not running");
3688
+ if (s.limits.memoryBytes) seg.push(`mem limit ${fmtBytes(s.limits.memoryBytes)}`);
3689
+ if (s.limits.cpus) seg.push(`cpu limit ${String(s.limits.cpus)}`);
3690
+ if (s.limits.pidsLimit) seg.push(`pids limit ${String(s.limits.pidsLimit)}`);
3691
+ }
3692
+ seg.push(
3693
+ `disk ${fmtBytes(s.diskUsedBytes)}${s.limits.disk ? ` (limit ${s.limits.disk}, no-op on overlay2/macOS)` : ""}`
3694
+ );
3695
+ if (s.snapshotDiskBytes !== null) seg.push(`snapshot ${fmtBytes(s.snapshotDiskBytes)}`);
3696
+ if (s.checkpointVolumeBytes !== null) seg.push(`ckpt ${fmtBytes(s.checkpointVolumeBytes)}`);
3697
+ let line = ` ${seg.join(" ")}`;
3698
+ for (const w of s.warnings) line += `
3699
+ note: ${w}`;
3700
+ return line;
3701
+ }
3702
+ function renderClaude(i, persisted) {
3703
+ const s = i.claudeSession;
3704
+ let session;
3705
+ if (s === null) {
3706
+ session = "no session (box not running)";
3707
+ } else if (!s.running) {
3708
+ session = `no session ("${s.sessionName}")`;
3709
+ } else {
3710
+ const ago = fmtAgo(s.startedAt);
3711
+ session = `running ("${s.sessionName}")${ago ? `, started ${ago}` : ""}`;
3712
+ }
3713
+ const lines = [` session ${session}`];
3714
+ if (persisted) {
3715
+ const c = persisted.claude;
3716
+ const ago = fmtAgo(c.updatedAt);
3717
+ lines.push(` activity ${c.state}${ago ? ` (${ago})` : ""}`);
3718
+ }
3719
+ return lines.join("\n");
3720
+ }
3721
+ function renderPersisted2(s, state) {
3722
+ const out = [`(persisted snapshot from ${s.timestamp}; box is ${state})`, ""];
3723
+ if (s.tasks.length > 0) {
3724
+ out.push("TASKS");
3725
+ out.push(...s.tasks.map((t) => ` ${t.name} ${t.state}`));
3726
+ out.push("");
3727
+ }
3728
+ out.push("SERVICES");
3729
+ if (s.services.length === 0) {
3730
+ out.push(" (none)");
3731
+ } else {
3732
+ out.push(
3733
+ ...s.services.map(
3734
+ (svc) => ` ${svc.name} ${svc.state}${svc.port !== null ? ` :${String(svc.port)}` : ""}`
3735
+ )
3736
+ );
3737
+ }
3738
+ out.push("");
3739
+ out.push("PORTS");
3740
+ if (s.ports.length === 0) {
3741
+ out.push(" (none listening)");
3742
+ } else {
3743
+ const other = s.ports.filter((p) => !p.service).map((p) => p.port).sort((a, b) => a - b);
3744
+ out.push(
3745
+ ...s.ports.filter((p) => p.service).map((p) => ` :${String(p.port)} (${p.service})`)
3746
+ );
3747
+ if (other.length > 0) out.push(` other (${other.length}): ${other.join(", ")}`);
3748
+ }
3749
+ return out.join("\n");
3750
+ }
3751
+
3752
+ // src/commands/stop.ts
3753
+ import { Command as Command22 } from "commander";
3754
+ var stopCommand = new Command22("stop").description("Stop a box (docker stop; preserves upper + node_modules volumes)").argument(
3755
+ "[box]",
3756
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
3757
+ ).action(async (idOrName) => {
3758
+ try {
3759
+ const box = await resolveBoxOrExit(idOrName);
3760
+ const record = await stopBox(box.id);
3761
+ process.stdout.write(
3762
+ `stopped ${record.container}
3763
+ restart with: agentbox start ${record.name}
3764
+ `
3765
+ );
3766
+ } catch (err) {
3767
+ handleLifecycleError(err);
3768
+ }
3769
+ });
3770
+
3771
+ // src/commands/top.ts
3772
+ import { Command as Command23 } from "commander";
3773
+ var COLS = ["BOX", "STATE", "CPU%", "MEM USAGE / LIMIT", "MEM%", "PIDS", "DISK", "NET I/O"];
3774
+ function row(name, state, s) {
3775
+ const mem = `${fmtBytes(s.memUsedBytes)} / ${fmtBytes(s.memLimitBytes)}`;
3776
+ const net = s.netRxBytes === null && s.netTxBytes === null ? "\u2014" : `${fmtBytes(s.netRxBytes)} / ${fmtBytes(s.netTxBytes)}`;
3777
+ return [
3778
+ name,
3779
+ state,
3780
+ fmtPercent(s.cpuPercent),
3781
+ s.live ? mem : "\u2014",
3782
+ fmtPercent(s.memPercent),
3783
+ s.pids === null ? "\u2014" : String(s.pids),
3784
+ fmtBytes(s.diskUsedBytes),
3785
+ s.live ? net : "\u2014"
3786
+ ];
3787
+ }
3788
+ function renderTable2(rows) {
3789
+ const all = [COLS, ...rows];
3790
+ const widths = COLS.map((_, c) => Math.max(...all.map((r) => r[c].length)));
3791
+ return all.map((r) => r.map((cell, c) => cell.padEnd(widths[c])).join(" ").trimEnd()).join("\n");
3792
+ }
3793
+ async function selectBoxes(idOrName, opts) {
3794
+ const boxes = await listBoxes();
3795
+ if (idOrName === void 0) {
3796
+ if (!opts.project) return boxes;
3797
+ const project = await findProjectRoot(process.cwd());
3798
+ return boxes.filter((b) => b.projectRoot === project.root);
3799
+ }
3800
+ const picked = await resolveBoxOrExit(idOrName);
3801
+ return boxes.filter((b) => b.id === picked.id);
3802
+ }
3803
+ async function snapshot(idOrName, opts) {
3804
+ const boxes = await selectBoxes(idOrName, opts);
3805
+ const stats = await Promise.all(boxes.map((b) => boxResourceStats(b)));
3806
+ return { boxes, stats };
3807
+ }
3808
+ async function renderProjectFooters() {
3809
+ const parts = [];
3810
+ const [ckpt, home] = await Promise.all([
3811
+ allCheckpointVolumesBytes(),
3812
+ agentboxHomeBytes()
3813
+ ]);
3814
+ if (home !== null) parts.push(`~/.agentbox: ${fmtBytes(home)}`);
3815
+ if (ckpt !== null) parts.push(`checkpoints: ${fmtBytes(ckpt)}`);
3816
+ return parts.length > 0 ? `
3817
+
3818
+ SYSTEM: ${parts.join(" - ")}` : "";
3819
+ }
3820
+ var topCommand = new Command23("top").description("Live resource monitor (cpu/mem/pids/disk) for a box, the project, or every box").argument(
3821
+ "[box]",
3822
+ "box ref (default: every box on the host; --project narrows to the cwd's project)"
3823
+ ).option("-p, --project", "show only boxes in the cwd's project").option("--once", "print a single snapshot instead of watching").option("-j, --json", "machine-readable JSON (implies --once)").option("--interval <seconds>", "refresh interval", "2").action(async (idOrName, opts) => {
3824
+ try {
3825
+ if (opts.json) {
3826
+ const { boxes, stats } = await snapshot(idOrName, opts);
3827
+ process.stdout.write(
3828
+ JSON.stringify(
3829
+ boxes.map((b, i) => ({ box: b.name, state: b.state, ...stats[i] })),
3830
+ null,
3831
+ 2
3832
+ ) + "\n"
3833
+ );
3834
+ return;
3835
+ }
3836
+ const produce = async (watching) => {
3837
+ const { boxes, stats } = await snapshot(idOrName, opts);
3838
+ const scope = opts.project ? "no boxes for this project" : "no boxes";
3839
+ const header = boxes.length === 0 ? watching ? `${scope} (waiting...)` : scope : renderTable2(boxes.map((b, i) => row(b.name, b.state, stats[i])));
3840
+ return header + await renderProjectFooters();
3841
+ };
3842
+ if (opts.once) {
3843
+ process.stdout.write(await produce(false) + "\n");
3844
+ return;
3845
+ }
3846
+ await watchRender(() => produce(true), opts.interval);
3847
+ } catch (err) {
3848
+ handleLifecycleError(err);
3849
+ }
3850
+ });
3851
+
3852
+ // src/commands/unpause.ts
3853
+ import { Command as Command24 } from "commander";
3854
+ var unpauseCommand = new Command24("unpause").description("Resume a paused box (docker unpause \u2014 sub-second)").argument(
3855
+ "[box]",
3856
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
3857
+ ).action(async (idOrName) => {
3858
+ try {
3859
+ const box = await resolveBoxOrExit(idOrName);
3860
+ const record = await unpauseBox(box.id);
3861
+ process.stdout.write(`unpaused ${record.container}
3862
+ `);
3863
+ } catch (err) {
3864
+ handleLifecycleError(err);
3865
+ }
3866
+ });
3867
+
3868
+ // src/commands/update.ts
3869
+ import { spawn as spawn4 } from "child_process";
3870
+ import { confirm as confirm10, intro as intro3, isCancel as isCancel10, log as log23, outro as outro3, spinner as spinner3 } from "@clack/prompts";
3871
+ import { Command as Command25 } from "commander";
3872
+
3873
+ // src/exec-method.ts
3874
+ function detectExecutionMethod(input) {
3875
+ const ua = input.userAgent ?? "";
3876
+ const argv1 = input.argv1 ?? "";
3877
+ if (argv1.includes("/_npx/") || argv1.includes("/.npm/_npx") || /\bnpx\//.test(ua)) {
3878
+ return "npx";
3879
+ }
3880
+ if (/\bpnpm\//.test(ua)) {
3881
+ return "pnpm";
3882
+ }
3883
+ if (/\bnpm\//.test(ua)) {
3884
+ return "npm";
3885
+ }
3886
+ return "direct";
3887
+ }
3888
+
3889
+ // src/commands/update.ts
3890
+ var PKG = "@madarco/agentbox";
3891
+ function selfUpdateCommand(method) {
3892
+ if (method === "npm") return { cmd: "npm", args: ["install", "-g", `${PKG}@latest`] };
3893
+ if (method === "pnpm") return { cmd: "pnpm", args: ["add", "-g", `${PKG}@latest`] };
3894
+ return null;
3895
+ }
3896
+ function describeSelfUpdate(method) {
3897
+ switch (method) {
3898
+ case "npm":
3899
+ return "self-update: npm install -g @madarco/agentbox@latest";
3900
+ case "pnpm":
3901
+ return "self-update: pnpm add -g @madarco/agentbox@latest";
3902
+ case "npx":
3903
+ return "self-update: skipped (running via npx \u2014 always the latest version)";
3904
+ case "direct":
3905
+ return "self-update: skipped (running from source \u2014 no global install to update)";
3906
+ }
3907
+ }
3908
+ function runInherit(cmd, args) {
3909
+ return new Promise((resolveP, rejectP) => {
3910
+ const child = spawn4(cmd, args, { stdio: "inherit" });
3911
+ child.on("error", rejectP);
3912
+ child.on("close", (code) => resolveP(code ?? 0));
3913
+ });
3914
+ }
3915
+ var updateCommand = new Command25("self-update").description(
3916
+ "Update agentbox: self-update via npm/pnpm (unless run via npx), wipe the box image so it rebuilds, and reload the relay"
3917
+ ).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "show what would happen, don't change anything").option("--skip-self", "skip the package self-update; only refresh the image + relay").action(async (opts) => {
3918
+ try {
3919
+ const method = detectExecutionMethod({
3920
+ userAgent: process.env.npm_config_user_agent,
3921
+ argv1: process.argv[1]
3922
+ });
3923
+ intro3("agentbox self-update");
3924
+ const selfStep = opts.skipSelf ? "self-update: skipped (--skip-self)" : describeSelfUpdate(method);
3925
+ log23.info(
3926
+ [
3927
+ "plan:",
3928
+ ` ${selfStep}`,
3929
+ ` image: docker image rm -f ${DEFAULT_BOX_IMAGE} (rebuilds on next create/claude)`,
3930
+ " relay: stop, then respawn unless a self-update ran"
3931
+ ].join("\n")
3932
+ );
3933
+ if (opts.dryRun) {
3934
+ outro3("dry run \u2014 nothing changed");
3935
+ return;
3936
+ }
3937
+ if (!opts.yes) {
3938
+ const ok = await confirm10({ message: "Proceed with update?", initialValue: true });
3939
+ if (isCancel10(ok) || !ok) {
3940
+ log23.info("cancelled");
3941
+ return;
3942
+ }
3943
+ }
3944
+ let selfUpdated = false;
3945
+ if (opts.skipSelf) {
3946
+ log23.info("skipping self-update (--skip-self)");
3947
+ } else {
3948
+ const cmd = selfUpdateCommand(method);
3949
+ if (cmd === null) {
3950
+ log23.info(describeSelfUpdate(method));
3951
+ } else {
3952
+ log23.info(`running: ${cmd.cmd} ${cmd.args.join(" ")}`);
3953
+ const code = await runInherit(cmd.cmd, cmd.args);
3954
+ if (code !== 0) {
3955
+ throw new Error(`${cmd.cmd} exited with code ${String(code)}`);
3956
+ }
3957
+ selfUpdated = true;
3958
+ log23.success(`updated ${PKG} via ${cmd.cmd}`);
3959
+ }
3960
+ }
3961
+ const s = spinner3();
3962
+ s.start(`removing image ${DEFAULT_BOX_IMAGE}`);
3963
+ const removed = await removeImage(DEFAULT_BOX_IMAGE);
3964
+ s.stop(
3965
+ removed ? `removed image ${DEFAULT_BOX_IMAGE} (rebuilds on next create/claude)` : `image ${DEFAULT_BOX_IMAGE} not present (nothing to remove)`
3966
+ );
3967
+ const sr = spinner3();
3968
+ sr.start("stopping relay");
3969
+ const stop = await stopRelay();
3970
+ sr.stop(
3971
+ stop.stopped ? `stopped relay (pid ${String(stop.pid)})` : "relay was not running"
3972
+ );
3973
+ if (selfUpdated) {
3974
+ log23.info(
3975
+ "relay will restart automatically (with the updated build) on your next `agentbox create` / `agentbox claude`"
3976
+ );
3977
+ } else {
3978
+ const sr2 = spinner3();
3979
+ sr2.start("restarting relay");
3980
+ try {
3981
+ const ep = await ensureRelay();
3982
+ sr2.stop(`relay back up on ${ep.hostUrl}`);
3983
+ } catch (err) {
3984
+ sr2.stop("relay restart failed");
3985
+ log23.warn(
3986
+ `${err instanceof Error ? err.message : String(err)} \u2014 it will retry on the next box command`
3987
+ );
3988
+ }
3989
+ }
3990
+ outro3("update complete");
3991
+ } catch (err) {
3992
+ handleLifecycleError(err);
3993
+ }
3994
+ });
3995
+
3996
+ // src/commands/wait.ts
3997
+ import { log as log24 } from "@clack/prompts";
3998
+ import { Command as Command26 } from "commander";
3999
+ var waitCommand = new Command26("wait").description("Block until the box reports all autostart units ready").argument(
4000
+ "[box]",
4001
+ "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
4002
+ ).option("--timeout <ms>", "overall timeout in milliseconds", "120000").option("--units <names...>", "restrict to the named units").option("-j, --json", "machine-readable JSON output").action(async (idOrName, opts) => {
4003
+ try {
4004
+ const box = await resolveBoxOrExit(idOrName);
4005
+ const ctlArgs = ["agentbox-ctl", "wait-ready", "--json", "--timeout", opts.timeout];
4006
+ if (opts.units && opts.units.length > 0) {
4007
+ ctlArgs.push("--units", ...opts.units);
4008
+ }
4009
+ const proc = await execInBox(box.container, ctlArgs, { user: "vscode" });
4010
+ let parsed;
4011
+ try {
4012
+ parsed = JSON.parse(proc.stdout);
4013
+ } catch {
4014
+ log24.error(`agentbox-ctl wait-ready failed: ${proc.stderr || proc.stdout}`);
4015
+ process.exit(1);
4016
+ }
4017
+ if (opts.json) {
4018
+ process.stdout.write(JSON.stringify(parsed, null, 2) + "\n");
4019
+ } else if (parsed.ready) {
4020
+ process.stdout.write("ready\n");
4021
+ } else {
4022
+ const lines = ["not ready"];
4023
+ if (parsed.timedOut.length > 0) lines.push(` timed out: ${parsed.timedOut.join(", ")}`);
4024
+ if (parsed.failed.length > 0) lines.push(` failed: ${parsed.failed.join(", ")}`);
4025
+ process.stdout.write(lines.join("\n") + "\n");
4026
+ }
4027
+ process.exit(parsed.ready ? 0 : 1);
4028
+ } catch (err) {
4029
+ handleLifecycleError(err);
4030
+ }
4031
+ });
4032
+
4033
+ // src/index.ts
4034
+ var program = new Command27();
4035
+ program.name("agentbox").description("Launch coding agents in isolated sandboxes").version("0.0.0");
4036
+ program.enablePositionalOptions();
4037
+ program.addCommand(createCommand);
4038
+ program.addCommand(claudeCommand);
4039
+ program.addCommand(codeCommand);
4040
+ program.addCommand(shellCommand);
4041
+ program.addCommand(listCommand2);
4042
+ program.addCommand(openCommand);
4043
+ program.addCommand(browserCommand);
4044
+ program.addCommand(screenCommand);
4045
+ program.addCommand(pullCommand);
4046
+ program.addCommand(statusCommand);
4047
+ program.addCommand(topCommand);
4048
+ program.addCommand(dashboardCommand);
4049
+ program.addCommand(waitCommand);
4050
+ program.addCommand(logsCommand);
4051
+ program.addCommand(pauseCommand);
4052
+ program.addCommand(unpauseCommand);
4053
+ program.addCommand(stopCommand);
4054
+ program.addCommand(startCommand);
4055
+ program.addCommand(destroyCommand);
4056
+ program.addCommand(pruneCommand);
4057
+ program.addCommand(checkpointCommand);
4058
+ program.addCommand(configCommand);
4059
+ program.addCommand(updateCommand);
4060
+ program.configureHelp({ visibleCommands: () => [] });
4061
+ program.addHelpText("after", () => "\n" + buildGroupedHelp(program));
4062
+ await applyEngineOverrideAtStartup();
4063
+ program.parseAsync(process.argv).catch((err) => {
4064
+ console.error(err);
4065
+ process.exit(1);
4066
+ });
4067
+ //# sourceMappingURL=index.js.map