@madarco/agentbox 0.4.1 → 0.6.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.
- package/dist/{chunk-J35IH7W5.js → chunk-BBZMA2K6.js} +61 -23
- package/dist/chunk-BBZMA2K6.js.map +1 -0
- package/dist/{chunk-SOMIKEN2.js → chunk-HHMWQNLF.js} +272 -214
- package/dist/chunk-HHMWQNLF.js.map +1 -0
- package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
- package/dist/chunk-HPZMD5DE.js.map +1 -0
- package/dist/{chunk-NSIECUCS.js → chunk-HTTKML3C.js} +705 -289
- package/dist/chunk-HTTKML3C.js.map +1 -0
- package/dist/{chunk-WR5FFGE5.js → chunk-KJNZP6I3.js} +218 -128
- package/dist/chunk-KJNZP6I3.js.map +1 -0
- package/dist/{chunk-FQD6ZWYW.js → chunk-M7I247BK.js} +68 -65
- package/dist/chunk-M7I247BK.js.map +1 -0
- package/dist/create-6PWXI6HO-OWAMHBAK.js +15 -0
- package/dist/index.js +2394 -1283
- package/dist/index.js.map +1 -1
- package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-EMXR46DI-DUVBXNTV.js} +5 -5
- package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
- package/dist/stats-SZXOJE3D-N7OODCHW.js +19 -0
- package/package.json +3 -2
- package/runtime/docker/Dockerfile.box +65 -25
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +52 -55
- package/runtime/docker/packages/ctl/dist/bin.cjs +272 -160
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +52 -0
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +21 -15
- package/runtime/relay/bin.cjs +407 -12
- package/share/agentbox-setup/SKILL.md +52 -55
- package/dist/chunk-FQD6ZWYW.js.map +0 -1
- package/dist/chunk-IDR4HVIC.js.map +0 -1
- package/dist/chunk-J35IH7W5.js.map +0 -1
- package/dist/chunk-NSIECUCS.js.map +0 -1
- package/dist/chunk-SOMIKEN2.js.map +0 -1
- package/dist/chunk-WR5FFGE5.js.map +0 -1
- package/dist/create-4BQY2UYU-CGSW3RGE.js +0 -15
- package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
- /package/dist/{create-4BQY2UYU-CGSW3RGE.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
- /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
- /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
- /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-SZXOJE3D-N7OODCHW.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
createBox
|
|
4
|
-
|
|
3
|
+
createBox,
|
|
4
|
+
hostBackupHasCredentials,
|
|
5
|
+
syncClaudeCredentials
|
|
6
|
+
} from "./chunk-KJNZP6I3.js";
|
|
5
7
|
import {
|
|
6
8
|
AmbiguousBoxError,
|
|
7
9
|
BoxNotFoundError,
|
|
8
10
|
destroyBox,
|
|
9
|
-
getBoxEndpoints,
|
|
10
11
|
getBoxHostPaths,
|
|
11
12
|
inspectBox,
|
|
12
13
|
listBoxes,
|
|
@@ -16,19 +17,24 @@ import {
|
|
|
16
17
|
startBox,
|
|
17
18
|
stopBox,
|
|
18
19
|
unpauseBox
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-M7I247BK.js";
|
|
20
21
|
import {
|
|
21
22
|
ClaudeSessionError,
|
|
23
|
+
DEFAULT_RELAY_PORT,
|
|
22
24
|
SHARED_CLAUDE_VOLUME,
|
|
23
|
-
|
|
25
|
+
buildClaudeAttachArgv,
|
|
24
26
|
buildClaudeDashboardAttachArgv,
|
|
27
|
+
buildClaudeLoginRunArgv,
|
|
25
28
|
buildShellArgv,
|
|
26
29
|
buildVncUrls,
|
|
27
30
|
claudeSessionInfo,
|
|
31
|
+
clearRelayNotice,
|
|
28
32
|
containerHex,
|
|
29
33
|
ensureAgentboxTasksFile,
|
|
30
34
|
ensureClaudeVolume,
|
|
31
35
|
ensureRelay,
|
|
36
|
+
formatDetachNotice,
|
|
37
|
+
getRelayStatus,
|
|
32
38
|
ideProfile,
|
|
33
39
|
pullClaudeExtras,
|
|
34
40
|
rebuildPluginNativeDeps,
|
|
@@ -36,20 +42,24 @@ import {
|
|
|
36
42
|
renderStatusTable,
|
|
37
43
|
renderTaskTable,
|
|
38
44
|
resolveClaudeVolume,
|
|
45
|
+
runInteractiveClaudeLogin,
|
|
46
|
+
seedSetupSkillIntoVolume,
|
|
47
|
+
setRelayNotice,
|
|
39
48
|
startClaudeSession,
|
|
40
|
-
stopRelay
|
|
41
|
-
|
|
49
|
+
stopRelay,
|
|
50
|
+
warmUpClaudeCredentials
|
|
51
|
+
} from "./chunk-HTTKML3C.js";
|
|
42
52
|
import {
|
|
43
53
|
STATE_DIR,
|
|
44
54
|
readState,
|
|
45
55
|
resolveBoxRef
|
|
46
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-HPZMD5DE.js";
|
|
47
57
|
import {
|
|
48
58
|
agentboxHomeBytes,
|
|
49
|
-
|
|
59
|
+
allCheckpointImagesBytes,
|
|
50
60
|
boxResourceStats,
|
|
51
|
-
|
|
52
|
-
} from "./chunk-
|
|
61
|
+
projectCheckpointImageBytes
|
|
62
|
+
} from "./chunk-BBZMA2K6.js";
|
|
53
63
|
import {
|
|
54
64
|
DEFAULT_BOX_IMAGE,
|
|
55
65
|
DEFAULT_ENV_PATTERNS,
|
|
@@ -59,6 +69,7 @@ import {
|
|
|
59
69
|
configPathFor,
|
|
60
70
|
createCheckpoint,
|
|
61
71
|
detectEngine,
|
|
72
|
+
ensureImage,
|
|
62
73
|
execInBox,
|
|
63
74
|
findProjectRoot,
|
|
64
75
|
listCheckpoints,
|
|
@@ -71,19 +82,20 @@ import {
|
|
|
71
82
|
refreshExport,
|
|
72
83
|
removeCheckpoint,
|
|
73
84
|
removeImage,
|
|
85
|
+
scanHostEnvFiles,
|
|
74
86
|
setConfigValue,
|
|
75
87
|
setEngineOverride,
|
|
76
88
|
unsetConfigValue
|
|
77
|
-
} from "./chunk-
|
|
89
|
+
} from "./chunk-HHMWQNLF.js";
|
|
78
90
|
|
|
79
91
|
// src/index.ts
|
|
80
|
-
import { Command as
|
|
92
|
+
import { Command as Command29 } from "commander";
|
|
81
93
|
|
|
82
94
|
// ../../packages/sandbox-docker/dist/index.js
|
|
83
95
|
function browserSessionActive(stdout, exitCode) {
|
|
84
96
|
return exitCode === 0 && !/no active sessions/i.test(stdout);
|
|
85
97
|
}
|
|
86
|
-
async function ensureBoxBrowser(container, timeoutMs = 8e3) {
|
|
98
|
+
async function ensureBoxBrowser(container, timeoutMs = 8e3, targetUrl = "about:blank") {
|
|
87
99
|
const list = await execInBox(container, ["agent-browser", "session", "list"], {
|
|
88
100
|
user: "vscode",
|
|
89
101
|
timeoutMs
|
|
@@ -91,7 +103,7 @@ async function ensureBoxBrowser(container, timeoutMs = 8e3) {
|
|
|
91
103
|
if (browserSessionActive(list.stdout, list.exitCode)) {
|
|
92
104
|
return { up: true, alreadyRunning: true };
|
|
93
105
|
}
|
|
94
|
-
const open = await execInBox(container, ["agent-browser", "open", "--headed",
|
|
106
|
+
const open = await execInBox(container, ["agent-browser", "open", "--headed", targetUrl], {
|
|
95
107
|
user: "vscode",
|
|
96
108
|
timeoutMs
|
|
97
109
|
});
|
|
@@ -122,10 +134,10 @@ var HELP_GROUPS = [
|
|
|
122
134
|
},
|
|
123
135
|
{ title: "Inspect", commands: ["list", "status", "top"] },
|
|
124
136
|
{ title: "Lifecycle", commands: ["start", "stop", "destroy", "pause", "unpause"] },
|
|
125
|
-
{ title: "Sync & state", commands: ["
|
|
137
|
+
{ title: "Sync & state", commands: ["download", "cp", "checkpoint"] },
|
|
126
138
|
{
|
|
127
139
|
title: "Advanced",
|
|
128
|
-
commands: ["wait", "prune", "self-update", "config"]
|
|
140
|
+
commands: ["wait", "prune", "self-update", "config", "relay"]
|
|
129
141
|
}
|
|
130
142
|
];
|
|
131
143
|
function term(cmd) {
|
|
@@ -258,7 +270,7 @@ var browserCommand = new Command("browser").description(
|
|
|
258
270
|
log3.info("box is paused; unpausing");
|
|
259
271
|
await unpauseBox(box.id);
|
|
260
272
|
} else if (insp.state === "stopped") {
|
|
261
|
-
log3.info("box is stopped; starting
|
|
273
|
+
log3.info("box is stopped; starting");
|
|
262
274
|
await startBox(box.id);
|
|
263
275
|
} else if (insp.state === "missing") {
|
|
264
276
|
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
@@ -298,13 +310,12 @@ var browserCommand = new Command("browser").description(
|
|
|
298
310
|
});
|
|
299
311
|
|
|
300
312
|
// src/commands/claude.ts
|
|
301
|
-
import { confirm as confirm2, intro, isCancel as isCancel2, log as log5, outro,
|
|
313
|
+
import { confirm as confirm2, intro, isCancel as isCancel2, log as log5, outro, spinner } from "@clack/prompts";
|
|
302
314
|
import { Command as Command2 } from "commander";
|
|
303
315
|
|
|
304
316
|
// src/auth.ts
|
|
305
|
-
import {
|
|
306
|
-
import {
|
|
307
|
-
import { dirname, join } from "path";
|
|
317
|
+
import { readFile } from "fs/promises";
|
|
318
|
+
import { join } from "path";
|
|
308
319
|
var AUTH_FILE = join(STATE_DIR, "auth.json");
|
|
309
320
|
async function resolveClaudeAuth(processEnv, opts = {}) {
|
|
310
321
|
const env = {};
|
|
@@ -322,9 +333,9 @@ async function resolveClaudeAuth(processEnv, opts = {}) {
|
|
|
322
333
|
}
|
|
323
334
|
return { env: {}, source: "none" };
|
|
324
335
|
}
|
|
325
|
-
async function readAuthFile(
|
|
336
|
+
async function readAuthFile(path2 = AUTH_FILE) {
|
|
326
337
|
try {
|
|
327
|
-
const raw = await readFile(
|
|
338
|
+
const raw = await readFile(path2, "utf8");
|
|
328
339
|
const parsed = JSON.parse(raw);
|
|
329
340
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
330
341
|
const t = parsed.claudeCodeOauthToken;
|
|
@@ -336,22 +347,6 @@ async function readAuthFile(path = AUTH_FILE) {
|
|
|
336
347
|
return {};
|
|
337
348
|
}
|
|
338
349
|
}
|
|
339
|
-
async function writeAuthFile(next, path = AUTH_FILE) {
|
|
340
|
-
await mkdir(dirname(path), { recursive: true });
|
|
341
|
-
await writeFile(path, JSON.stringify(next, null, 2) + "\n", { mode: 384, flag: "w" });
|
|
342
|
-
}
|
|
343
|
-
function hostClaudeAvailable() {
|
|
344
|
-
const r = spawnSync2("which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
345
|
-
return r.status === 0 && (r.stdout?.toString().trim().length ?? 0) > 0;
|
|
346
|
-
}
|
|
347
|
-
function runHostSetupToken() {
|
|
348
|
-
const child = spawnSync2("claude", ["setup-token"], { stdio: "inherit" });
|
|
349
|
-
return { exitCode: child.status ?? -1 };
|
|
350
|
-
}
|
|
351
|
-
function isPlausibleOauthToken(s) {
|
|
352
|
-
const t = s.trim();
|
|
353
|
-
return t.startsWith("sk-ant-oat") && t.length >= 40;
|
|
354
|
-
}
|
|
355
350
|
|
|
356
351
|
// ../../packages/core/dist/index.js
|
|
357
352
|
var claudeCodeLauncher = {
|
|
@@ -429,51 +424,27 @@ function resolveLimits(box, flags) {
|
|
|
429
424
|
}
|
|
430
425
|
|
|
431
426
|
// src/wizard.ts
|
|
432
|
-
import { confirm, isCancel, log as log4 } from "@clack/prompts";
|
|
433
|
-
import {
|
|
434
|
-
import { homedir } from "os";
|
|
435
|
-
import { basename, join as join2 } from "path";
|
|
436
|
-
import { fileURLToPath } from "url";
|
|
427
|
+
import { confirm, isCancel, log as log4, multiselect } from "@clack/prompts";
|
|
428
|
+
import { basename } from "path";
|
|
437
429
|
var IN_BOX_SETUP_GUIDE_PATH = "/usr/local/share/agentbox/setup-guide.md";
|
|
438
|
-
var HOST_SKILLS_DIR = join2(homedir(), ".claude", "skills", "agentbox-setup");
|
|
439
|
-
var HOST_SKILL_FILE = join2(HOST_SKILLS_DIR, "SKILL.md");
|
|
440
|
-
function bundledSkillPath() {
|
|
441
|
-
return fileURLToPath(new URL("../share/agentbox-setup/SKILL.md", import.meta.url));
|
|
442
|
-
}
|
|
443
|
-
async function fileExists(p) {
|
|
444
|
-
try {
|
|
445
|
-
const st = await stat(p);
|
|
446
|
-
return st.isFile();
|
|
447
|
-
} catch {
|
|
448
|
-
return false;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
async function installAgentboxSetupSkill(opts = {}) {
|
|
452
|
-
const targetFile = opts.targetFile ?? HOST_SKILL_FILE;
|
|
453
|
-
const targetDir = join2(targetFile, "..");
|
|
454
|
-
if (await fileExists(targetFile)) return { installed: false, targetFile };
|
|
455
|
-
const src = opts.sourceFile ?? bundledSkillPath();
|
|
456
|
-
if (!await fileExists(src)) {
|
|
457
|
-
return { installed: false, targetFile };
|
|
458
|
-
}
|
|
459
|
-
await mkdir2(targetDir, { recursive: true, mode: 448 });
|
|
460
|
-
await copyFile(src, targetFile);
|
|
461
|
-
return { installed: true, targetFile };
|
|
462
|
-
}
|
|
463
430
|
function buildSetupInitialPrompt(workspace) {
|
|
464
431
|
const name = basename(workspace);
|
|
465
|
-
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
|
|
432
|
+
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 download env" on the host).`;
|
|
466
433
|
}
|
|
467
434
|
var WIZARD_AUTOLAUNCH_ENV = "AGENTBOX_WIZARD_AUTOLAUNCH";
|
|
435
|
+
var WIZARD_ENV_FILES_ENV = "AGENTBOX_WIZARD_ENV_FILES";
|
|
436
|
+
var WIZARD_ENV_SCAN_PATTERNS = DEFAULT_ENV_PATTERNS.filter((p) => p !== "agentbox.yaml");
|
|
468
437
|
async function maybeRunSetupWizard(args) {
|
|
469
438
|
if (process.env[WIZARD_AUTOLAUNCH_ENV] === "1") {
|
|
470
439
|
if (args.command !== "claude") return { action: "proceed" };
|
|
471
|
-
|
|
440
|
+
const envFiles = parseEnvFilesFromEnv(process.env[WIZARD_ENV_FILES_ENV]);
|
|
441
|
+
if (args.checkpointRef) return { action: "proceed", envFilesToImport: envFiles };
|
|
472
442
|
const proj2 = await findProjectRoot(args.workspace);
|
|
473
|
-
if (proj2.hasAgentboxYaml) return { action: "proceed" };
|
|
443
|
+
if (proj2.hasAgentboxYaml) return { action: "proceed", envFilesToImport: envFiles };
|
|
474
444
|
return {
|
|
475
445
|
action: "launch-with-prompt",
|
|
476
|
-
initialPrompt: buildSetupInitialPrompt(proj2.root)
|
|
446
|
+
initialPrompt: buildSetupInitialPrompt(proj2.root),
|
|
447
|
+
envFilesToImport: envFiles
|
|
477
448
|
};
|
|
478
449
|
}
|
|
479
450
|
if (args.yes) return { action: "proceed" };
|
|
@@ -484,25 +455,42 @@ async function maybeRunSetupWizard(args) {
|
|
|
484
455
|
log4.info(`starting from checkpoint "${args.checkpointRef}"; skipping agentbox.yaml setup`);
|
|
485
456
|
return { action: "proceed" };
|
|
486
457
|
}
|
|
458
|
+
let envFilesToImport;
|
|
459
|
+
if (!args.withEnv) {
|
|
460
|
+
const found = await scanHostEnvFiles(proj.root, WIZARD_ENV_SCAN_PATTERNS);
|
|
461
|
+
if (found.length > 0) {
|
|
462
|
+
const picked = await multiselect({
|
|
463
|
+
message: "Import host env/secret files into the box? (space to toggle, enter to confirm)",
|
|
464
|
+
options: found.map((p) => ({ value: p, label: p })),
|
|
465
|
+
initialValues: found,
|
|
466
|
+
required: false
|
|
467
|
+
});
|
|
468
|
+
if (!isCancel(picked) && Array.isArray(picked) && picked.length > 0) {
|
|
469
|
+
envFilesToImport = picked;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
487
473
|
const go = await confirm({
|
|
488
|
-
message: "
|
|
474
|
+
message: "New project detected, run setup wizard?",
|
|
489
475
|
initialValue: true
|
|
490
476
|
});
|
|
491
|
-
if (isCancel(go) || !go) return { action: "proceed" };
|
|
492
|
-
|
|
493
|
-
const r = await installAgentboxSetupSkill();
|
|
494
|
-
if (r.installed) {
|
|
495
|
-
log4.success(`installed /agentbox-setup skill at ${r.targetFile}`);
|
|
496
|
-
}
|
|
497
|
-
} catch (err) {
|
|
498
|
-
log4.warn(`could not install /agentbox-setup skill: ${err.message}`);
|
|
499
|
-
}
|
|
500
|
-
if (args.command === "create") return { action: "switch-to-claude" };
|
|
477
|
+
if (isCancel(go) || !go) return { action: "proceed", envFilesToImport };
|
|
478
|
+
if (args.command === "create") return { action: "switch-to-claude", envFilesToImport };
|
|
501
479
|
return {
|
|
502
480
|
action: "launch-with-prompt",
|
|
503
|
-
initialPrompt: buildSetupInitialPrompt(proj.root)
|
|
481
|
+
initialPrompt: buildSetupInitialPrompt(proj.root),
|
|
482
|
+
envFilesToImport
|
|
504
483
|
};
|
|
505
484
|
}
|
|
485
|
+
function serializeEnvFilesForEnv(files) {
|
|
486
|
+
if (!files || files.length === 0) return void 0;
|
|
487
|
+
return files.join("\0");
|
|
488
|
+
}
|
|
489
|
+
function parseEnvFilesFromEnv(raw) {
|
|
490
|
+
if (!raw) return void 0;
|
|
491
|
+
const out = raw.split("\0").filter((p) => p.length > 0);
|
|
492
|
+
return out.length > 0 ? out : void 0;
|
|
493
|
+
}
|
|
506
494
|
function passthroughFlags(opts) {
|
|
507
495
|
const out = [];
|
|
508
496
|
if (opts.workspace) out.push("--workspace", opts.workspace);
|
|
@@ -517,237 +505,1035 @@ function passthroughFlags(opts) {
|
|
|
517
505
|
return out;
|
|
518
506
|
}
|
|
519
507
|
|
|
520
|
-
// src/
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
function
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (opts.isolateClaudeConfig === true) box.isolateClaudeConfig = true;
|
|
532
|
-
if (opts.sharedDockerCache === true) box.dockerCacheShared = true;
|
|
533
|
-
const claude = {};
|
|
534
|
-
if (opts.sessionName !== void 0) claude.sessionName = opts.sessionName;
|
|
535
|
-
const out = {};
|
|
536
|
-
if (Object.keys(box).length > 0) out.box = box;
|
|
537
|
-
if (Object.keys(claude).length > 0) out.claude = claude;
|
|
538
|
-
return out;
|
|
539
|
-
}
|
|
540
|
-
async function offerSetupToken() {
|
|
541
|
-
log5.info("first time setup: setup token for Claude Code");
|
|
542
|
-
const canRun = hostClaudeAvailable();
|
|
543
|
-
if (canRun) {
|
|
544
|
-
const yes = await confirm2({
|
|
545
|
-
message: "Run `claude setup-token` now to save a token?",
|
|
546
|
-
initialValue: true
|
|
547
|
-
});
|
|
548
|
-
if (isCancel2(yes) || !yes) {
|
|
549
|
-
log5.info("ok, continuing without a saved token; /login inside the box once and it persists in the shared volume.");
|
|
508
|
+
// src/wrapped-pty/run.ts
|
|
509
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
510
|
+
|
|
511
|
+
// src/pty/pty-backend.ts
|
|
512
|
+
async function loadPtyBackend() {
|
|
513
|
+
try {
|
|
514
|
+
const ptyMod = await import("@homebridge/node-pty-prebuilt-multiarch");
|
|
515
|
+
const xtermMod = await import("@xterm/headless");
|
|
516
|
+
const spawn5 = ptyMod["spawn"] ?? ptyMod["default"]?.["spawn"];
|
|
517
|
+
const Terminal = xtermMod["Terminal"] ?? xtermMod["default"]?.["Terminal"];
|
|
518
|
+
if (typeof spawn5 !== "function" || typeof Terminal !== "function") {
|
|
550
519
|
return null;
|
|
551
520
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
log5.warn(
|
|
558
|
-
"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."
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
const pasted = await password({ message: "Paste OAuth token (or empty to skip):" });
|
|
562
|
-
if (isCancel2(pasted) || !pasted) {
|
|
563
|
-
log5.info("ok, continuing without a saved token; /login inside the box once and it persists in the shared volume.");
|
|
521
|
+
return {
|
|
522
|
+
ptySpawn: spawn5,
|
|
523
|
+
termCtor: Terminal
|
|
524
|
+
};
|
|
525
|
+
} catch {
|
|
564
526
|
return null;
|
|
565
527
|
}
|
|
566
|
-
const token = pasted.trim();
|
|
567
|
-
if (!isPlausibleOauthToken(token)) {
|
|
568
|
-
log5.warn("That doesn't look like an OAuth token (expected `sk-ant-oat\u2026`); saving anyway \u2014 verify inside the box.");
|
|
569
|
-
}
|
|
570
|
-
await writeAuthFile({ claudeCodeOauthToken: token });
|
|
571
|
-
log5.success(`saved to ${AUTH_FILE} (mode 0600)`);
|
|
572
|
-
return { env: { CLAUDE_CODE_OAUTH_TOKEN: token }, source: "auth-file" };
|
|
573
528
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
effectiveClaudeArgs = resolveAgentLauncher("claude-code").buildArgs(
|
|
605
|
-
wiz.initialPrompt,
|
|
606
|
-
claudeArgs
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
const useSnapshot = opts.hostSnapshot === false ? false : opts.hostSnapshot === true ? true : cfg.effective.box.hostSnapshot ?? true;
|
|
610
|
-
const sessionName = cfg.effective.claude.sessionName;
|
|
611
|
-
let resolved = await resolveClaudeAuth(process.env);
|
|
612
|
-
if (resolved.source === "none" && process.stdin.isTTY && !opts.yes) {
|
|
613
|
-
const next = await offerSetupToken();
|
|
614
|
-
if (next) resolved = next;
|
|
615
|
-
}
|
|
616
|
-
const s = spinner();
|
|
617
|
-
s.start("creating box");
|
|
618
|
-
let containerName = "";
|
|
619
|
-
try {
|
|
620
|
-
const withPlaywright = cfg.effective.box.withPlaywright || cfg.effective.browser.default !== "agent-browser";
|
|
621
|
-
const result = await createBox({
|
|
622
|
-
workspacePath: opts.workspace,
|
|
623
|
-
name: opts.name,
|
|
624
|
-
useSnapshot,
|
|
625
|
-
checkpointRef,
|
|
626
|
-
image: cfg.effective.box.image,
|
|
627
|
-
claudeConfig: { isolate: cfg.effective.box.isolateClaudeConfig },
|
|
628
|
-
claudeEnv: resolved.env,
|
|
629
|
-
withPlaywright,
|
|
630
|
-
withEnv: cfg.effective.box.withEnv,
|
|
631
|
-
vnc: { enabled: cfg.effective.box.vnc },
|
|
632
|
-
docker: { sharedCache: cfg.effective.box.dockerCacheShared },
|
|
633
|
-
limits: resolveLimits(cfg.effective.box, opts),
|
|
634
|
-
projectRoot,
|
|
635
|
-
onLog: (line) => s.message(clampSpinnerLine(line))
|
|
636
|
-
});
|
|
637
|
-
containerName = result.record.container;
|
|
638
|
-
s.message("checking plugin native deps");
|
|
639
|
-
const rebuild = await rebuildPluginNativeDeps(result.record.container, {
|
|
640
|
-
volume: result.record.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME,
|
|
641
|
-
onProgress: (line) => s.message(clampSpinnerLine(line))
|
|
642
|
-
});
|
|
643
|
-
s.message("starting claude session");
|
|
644
|
-
await startClaudeSession({
|
|
645
|
-
container: result.record.container,
|
|
646
|
-
claudeArgs: effectiveClaudeArgs,
|
|
647
|
-
sessionName,
|
|
648
|
-
boxName: result.record.name
|
|
649
|
-
});
|
|
650
|
-
const nSuffix = typeof result.record.projectIndex === "number" ? ` \xB7 n ${String(result.record.projectIndex)}` : "";
|
|
651
|
-
s.stop(`box ${result.record.container} ready${nSuffix}`);
|
|
652
|
-
for (const f of rebuild.failed) {
|
|
653
|
-
log5.warn(`plugin install failed for ${f.dir}; claude may still load it. stderr:
|
|
654
|
-
${f.stderr.trim()}`);
|
|
529
|
+
|
|
530
|
+
// src/wrapped-pty/input-router.ts
|
|
531
|
+
var KEY_ENTER = 13;
|
|
532
|
+
var KEY_LF = 10;
|
|
533
|
+
var KEY_ESC = 27;
|
|
534
|
+
var KEY_CTRL_C = 3;
|
|
535
|
+
var KEY_Y_LOW = 121;
|
|
536
|
+
var KEY_Y_UP = 89;
|
|
537
|
+
var KEY_N_LOW = 110;
|
|
538
|
+
var KEY_N_UP = 78;
|
|
539
|
+
function createInputRouter(opts) {
|
|
540
|
+
let active = null;
|
|
541
|
+
let disposed = false;
|
|
542
|
+
const settle = (answer, cancelled) => {
|
|
543
|
+
if (!active) return;
|
|
544
|
+
const body = {
|
|
545
|
+
id: active.ev.id,
|
|
546
|
+
answer,
|
|
547
|
+
...cancelled ? { cancelled: true } : {}
|
|
548
|
+
};
|
|
549
|
+
const p = active;
|
|
550
|
+
active = null;
|
|
551
|
+
p.resolve(body);
|
|
552
|
+
opts.onAnswer(body);
|
|
553
|
+
};
|
|
554
|
+
const handleCapturedByte = (b) => {
|
|
555
|
+
if (!active) return;
|
|
556
|
+
if (b === KEY_Y_LOW || b === KEY_Y_UP) {
|
|
557
|
+
settle("y");
|
|
558
|
+
return;
|
|
655
559
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
s.stop("failed");
|
|
660
|
-
if (err instanceof ClaudeSessionError) {
|
|
661
|
-
log5.error(err.message);
|
|
662
|
-
if (containerName) {
|
|
663
|
-
log5.info(`The box ${containerName} is still running. Destroy it with:`);
|
|
664
|
-
log5.info(` agentbox destroy ${containerName} -y`);
|
|
665
|
-
}
|
|
666
|
-
process.exit(1);
|
|
560
|
+
if (b === KEY_N_LOW || b === KEY_N_UP) {
|
|
561
|
+
settle("n");
|
|
562
|
+
return;
|
|
667
563
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
{
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
564
|
+
if (b === KEY_ESC || b === KEY_CTRL_C) {
|
|
565
|
+
settle("n", true);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (b === KEY_ENTER || b === KEY_LF) {
|
|
569
|
+
const def = active.ev.defaultAnswer ?? "n";
|
|
570
|
+
settle(def);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
return {
|
|
575
|
+
get capturing() {
|
|
576
|
+
return active !== null;
|
|
577
|
+
},
|
|
578
|
+
feed(buf) {
|
|
579
|
+
if (disposed) return;
|
|
580
|
+
if (active) {
|
|
581
|
+
if (buf.length > 1 && buf[0] === KEY_ESC) return;
|
|
582
|
+
for (let i = 0; i < buf.length; i++) {
|
|
583
|
+
const byte = buf[i];
|
|
584
|
+
if (byte === void 0) continue;
|
|
585
|
+
if (active) {
|
|
586
|
+
handleCapturedByte(byte);
|
|
587
|
+
} else {
|
|
588
|
+
opts.onForward(buf.subarray(i));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
opts.onForward(buf);
|
|
595
|
+
},
|
|
596
|
+
capture(ev) {
|
|
597
|
+
return new Promise((resolve2, reject) => {
|
|
598
|
+
if (active) {
|
|
599
|
+
settle("n", true);
|
|
600
|
+
}
|
|
601
|
+
active = { ev, resolve: resolve2, reject };
|
|
602
|
+
});
|
|
603
|
+
},
|
|
604
|
+
abort(reason) {
|
|
605
|
+
if (!active) return;
|
|
606
|
+
const p = active;
|
|
607
|
+
active = null;
|
|
608
|
+
const msg = reason === "pty-exit" ? "pty exited" : "resolved by sibling wrapper";
|
|
609
|
+
p.reject(new Error(msg));
|
|
610
|
+
},
|
|
611
|
+
dispose() {
|
|
612
|
+
if (disposed) return;
|
|
613
|
+
disposed = true;
|
|
614
|
+
if (active) {
|
|
615
|
+
const p = active;
|
|
616
|
+
active = null;
|
|
617
|
+
p.reject(new Error("input router disposed"));
|
|
705
618
|
}
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
s.message("checking plugin native deps");
|
|
709
|
-
const rebuild = await rebuildPluginNativeDeps(box.container, {
|
|
710
|
-
volume: box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME,
|
|
711
|
-
onProgress: (line) => s.message(clampSpinnerLine(line))
|
|
712
|
-
});
|
|
713
|
-
s.message("starting claude session");
|
|
714
|
-
await startClaudeSession({
|
|
715
|
-
container: box.container,
|
|
716
|
-
claudeArgs,
|
|
717
|
-
sessionName,
|
|
718
|
-
boxName: box.name
|
|
719
|
-
});
|
|
720
|
-
s.stop(`box ${box.container} ready`);
|
|
721
|
-
for (const f of rebuild.failed) {
|
|
722
|
-
log5.warn(`plugin install failed for ${f.dir}; claude may still load it. stderr:
|
|
723
|
-
${f.stderr.trim()}`);
|
|
724
|
-
}
|
|
725
|
-
outro("attaching \u2014 Control+a q to detach, leaves claude running");
|
|
726
|
-
attachClaudeSession(box.container, sessionName, reattachRef(box));
|
|
727
|
-
}
|
|
728
|
-
var claudeAttachCommand = new Command2("attach").description(
|
|
729
|
-
"Attach to a Claude Code tmux session in a box, starting one if none is running (auto-unpause/start)"
|
|
730
|
-
).argument(
|
|
731
|
-
"[box]",
|
|
732
|
-
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
733
|
-
).option("--session-name <name>", "tmux session name (default from config; built-in: claude)").option(
|
|
734
|
-
"--no-sync-config",
|
|
735
|
-
"when starting a fresh session, skip rsyncing the host's ~/.claude into the box's volume (faster)"
|
|
736
|
-
).action(async function(idOrName) {
|
|
737
|
-
const opts = this.optsWithGlobals();
|
|
738
|
-
intro("agentbox claude attach");
|
|
739
|
-
try {
|
|
740
|
-
const box = await resolveBoxOrExit(idOrName);
|
|
741
|
-
await startOrAttachClaude(box, [], opts);
|
|
742
|
-
} catch (err) {
|
|
743
|
-
if (err instanceof ClaudeSessionError) {
|
|
744
|
-
log5.error(err.message);
|
|
745
|
-
process.exit(1);
|
|
746
619
|
}
|
|
747
|
-
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/dashboard/sidebar.ts
|
|
624
|
+
function ellipsize(s, max) {
|
|
625
|
+
if (max <= 0) return "";
|
|
626
|
+
if (s.length <= max) return s;
|
|
627
|
+
if (max === 1) return "\u2026";
|
|
628
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
629
|
+
}
|
|
630
|
+
function ellipsizeHead(s, max) {
|
|
631
|
+
if (max <= 0) return "";
|
|
632
|
+
if (s.length <= max) return s;
|
|
633
|
+
if (max === 1) return "\u2026";
|
|
634
|
+
return "\u2026" + s.slice(s.length - (max - 1));
|
|
635
|
+
}
|
|
636
|
+
function activityCell(b) {
|
|
637
|
+
if (b.pendingPrompt) return "\u25B2 prompt";
|
|
638
|
+
if (b.checkpointing) return "\u25C6 checkpoint";
|
|
639
|
+
if (b.state !== "running") return `[${b.state}]`;
|
|
640
|
+
switch (b.claudeActivity) {
|
|
641
|
+
case "working":
|
|
642
|
+
return "\u25CF working";
|
|
643
|
+
case "idle":
|
|
644
|
+
return "\u25CB idle";
|
|
645
|
+
case "waiting":
|
|
646
|
+
return "\u25D0 waiting";
|
|
647
|
+
default:
|
|
648
|
+
return "? unknown";
|
|
748
649
|
}
|
|
749
|
-
}
|
|
750
|
-
var
|
|
650
|
+
}
|
|
651
|
+
var NEW_BOX_ID = "__agentbox_new__";
|
|
652
|
+
var NEW_BOX_LABEL = "+ New box";
|
|
653
|
+
var SIDEBAR_HEADER = "AgentBox";
|
|
654
|
+
function topBorder(label, w) {
|
|
655
|
+
const lead = `\u256D\u2500\u2500\u2500 ${label} `;
|
|
656
|
+
if (lead.length >= w) return lead.slice(0, w);
|
|
657
|
+
return lead + "\u2500".repeat(w - lead.length);
|
|
658
|
+
}
|
|
659
|
+
function fit(s, w) {
|
|
660
|
+
if (s.length === w) return s;
|
|
661
|
+
if (s.length > w) return s.slice(0, w);
|
|
662
|
+
return s + " ".repeat(w - s.length);
|
|
663
|
+
}
|
|
664
|
+
function center(s, w) {
|
|
665
|
+
if (s.length >= w) return s.slice(0, w);
|
|
666
|
+
const pad = w - s.length;
|
|
667
|
+
const leftPad = Math.floor(pad / 2);
|
|
668
|
+
return " ".repeat(leftPad) + s + " ".repeat(pad - leftPad);
|
|
669
|
+
}
|
|
670
|
+
function projectLabel(project) {
|
|
671
|
+
if (!project) return "(no project)";
|
|
672
|
+
const parts = project.split("/").filter(Boolean);
|
|
673
|
+
return parts[parts.length - 1] ?? project;
|
|
674
|
+
}
|
|
675
|
+
function stripTitleGlyph(s) {
|
|
676
|
+
const t = s.replace(/^[\s\p{S}*·]+/u, "");
|
|
677
|
+
return t.length > 0 ? t : s.trim();
|
|
678
|
+
}
|
|
679
|
+
function boxRow(b, marker, w) {
|
|
680
|
+
const numStr = b.index != null ? `${b.index} ` : "";
|
|
681
|
+
const status = activityCell(b);
|
|
682
|
+
const left = `${marker}${numStr}`;
|
|
683
|
+
const room = w - left.length - status.length - 1;
|
|
684
|
+
if (room <= 0) return fit(`${left}${status}`, w);
|
|
685
|
+
const middle = b.state === "running" && b.sessionTitle ? ellipsize(stripTitleGlyph(b.sessionTitle), room) : ellipsizeHead(b.name, room);
|
|
686
|
+
return fit(`${left}${middle}`, w - status.length) + status;
|
|
687
|
+
}
|
|
688
|
+
function sidebarLines(boxes, selectedId, w, h) {
|
|
689
|
+
const lines = [topBorder(SIDEBAR_HEADER, w), fit("", w)];
|
|
690
|
+
const rowOwner = [null, null];
|
|
691
|
+
const headerRows = [true, false];
|
|
692
|
+
const push = (line, owner, header) => {
|
|
693
|
+
lines.push(fit(line, w));
|
|
694
|
+
rowOwner.push(owner);
|
|
695
|
+
headerRows.push(header);
|
|
696
|
+
};
|
|
697
|
+
let prevProject;
|
|
698
|
+
let seenGroup = false;
|
|
699
|
+
for (const b of boxes) {
|
|
700
|
+
const marker = b.id === selectedId ? "\u25B8" : " ";
|
|
701
|
+
if (b.id === NEW_BOX_ID) {
|
|
702
|
+
push(`${marker}${NEW_BOX_LABEL}`, b.id, false);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (!seenGroup || b.project !== prevProject) {
|
|
706
|
+
push(center(` \u2500\u2500 ${projectLabel(b.project)} \u2500\u2500 `, w), null, true);
|
|
707
|
+
prevProject = b.project;
|
|
708
|
+
seenGroup = true;
|
|
709
|
+
}
|
|
710
|
+
push(boxRow(b, marker, w), b.id, false);
|
|
711
|
+
}
|
|
712
|
+
if (boxes.length === 0) push(" (no boxes)", null, false);
|
|
713
|
+
while (lines.length < h) push("", null, false);
|
|
714
|
+
return {
|
|
715
|
+
lines: lines.slice(0, h),
|
|
716
|
+
rowOwner: rowOwner.slice(0, h),
|
|
717
|
+
headerRows: headerRows.slice(0, h)
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function menuLines(boxName, w, h) {
|
|
721
|
+
const body = [
|
|
722
|
+
"",
|
|
723
|
+
` No Claude session in ${boxName}.`,
|
|
724
|
+
"",
|
|
725
|
+
" [c] Start Claude here",
|
|
726
|
+
" [s] Open a shell",
|
|
727
|
+
"",
|
|
728
|
+
" Ctrl+Option+\u2191/\u2193 switch \xB7 Ctrl-a then v/c/w/q (vnc/code/web/quit)"
|
|
729
|
+
];
|
|
730
|
+
const top = Math.max(0, Math.floor((h - body.length) / 2));
|
|
731
|
+
const out = [];
|
|
732
|
+
for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
function lifecycleMenuLines(boxName, state, confirmDestroy, w, h) {
|
|
736
|
+
const body = confirmDestroy ? [
|
|
737
|
+
"",
|
|
738
|
+
` Destroy ${boxName}?`,
|
|
739
|
+
" This removes the container and its volumes.",
|
|
740
|
+
"",
|
|
741
|
+
" [y] Yes, destroy",
|
|
742
|
+
" [any other key] Cancel"
|
|
743
|
+
] : [
|
|
744
|
+
"",
|
|
745
|
+
` Box ${boxName} is ${state}.`,
|
|
746
|
+
"",
|
|
747
|
+
state === "paused" ? " [u] Unpause" : " [s] Start",
|
|
748
|
+
" [d] Destroy",
|
|
749
|
+
"",
|
|
750
|
+
" Ctrl+Option+\u2191/\u2193 switch \xB7 Ctrl-a then q quit"
|
|
751
|
+
];
|
|
752
|
+
const top = Math.max(0, Math.floor((h - body.length) / 2));
|
|
753
|
+
const out = [];
|
|
754
|
+
for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
function createMenuLines(where, w, h) {
|
|
758
|
+
const body = [
|
|
759
|
+
"",
|
|
760
|
+
" Create a new box",
|
|
761
|
+
"",
|
|
762
|
+
" [c] Create + launch Claude",
|
|
763
|
+
" [n] Create only",
|
|
764
|
+
"",
|
|
765
|
+
` in ${where}`,
|
|
766
|
+
"",
|
|
767
|
+
" Ctrl+Option+\u2191/\u2193 switch \xB7 Ctrl-a then q quit"
|
|
768
|
+
];
|
|
769
|
+
const top = Math.max(0, Math.floor((h - body.length) / 2));
|
|
770
|
+
const out = [];
|
|
771
|
+
for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
var BAR_BG = "\x1B[48;2;48;48;48m";
|
|
775
|
+
var BAR_BASE = BAR_BG + "\x1B[38;5;250m";
|
|
776
|
+
var BAR_BRAND = "\x1B[48;5;39m\x1B[38;5;16m";
|
|
777
|
+
var BRAND_BOLD = "\x1B[1m";
|
|
778
|
+
var BRAND_NOBOLD = "\x1B[22m";
|
|
779
|
+
var HINT_KEY = "\x1B[38;5;255m";
|
|
780
|
+
var HINT_TXT = "\x1B[38;5;245m";
|
|
781
|
+
var BAR_RESET = "\x1B[0m";
|
|
782
|
+
var SWITCH_HINT = ["Control+Option+\u2191/\u2193", "switch"];
|
|
783
|
+
var HINT_GROUPS = [
|
|
784
|
+
SWITCH_HINT,
|
|
785
|
+
["Control+a c", "code"],
|
|
786
|
+
["Control+a v", "vnc"],
|
|
787
|
+
["Control+a w", "web"],
|
|
788
|
+
["Control+a q", "quit"]
|
|
789
|
+
];
|
|
790
|
+
var COLLAPSED_HINT_GROUPS = [
|
|
791
|
+
SWITCH_HINT,
|
|
792
|
+
["Control+a", "more"]
|
|
793
|
+
];
|
|
794
|
+
var ADVANCED_HINT_GROUPS = [
|
|
795
|
+
["c", "code"],
|
|
796
|
+
["v", "vnc"],
|
|
797
|
+
["w", "web"],
|
|
798
|
+
["s", "stop"],
|
|
799
|
+
["p", "pause"],
|
|
800
|
+
["d", "destroy"],
|
|
801
|
+
["q", "quit"]
|
|
802
|
+
];
|
|
803
|
+
function statusLine(box, w, stateLabel, groups = HINT_GROUPS) {
|
|
804
|
+
const state = stateLabel ?? (box ? box.state === "running" ? box.claudeActivity ?? "unknown" : box.state : "");
|
|
805
|
+
const brandPrefix = box ? " agentbox \u25B8 " : " agentbox ";
|
|
806
|
+
const base = box ? `${box.name} (${state})` : "";
|
|
807
|
+
const coreMain = box ? `${base} ` : "";
|
|
808
|
+
const corePlain = brandPrefix + coreMain;
|
|
809
|
+
const SEP = " \u2502 ";
|
|
810
|
+
const renderHints = (g) => ({
|
|
811
|
+
plain: g.map(([k, l]) => `${k}: ${l}`).join(SEP) + " ",
|
|
812
|
+
styled: g.map(([k, l]) => `${HINT_KEY}${k}${HINT_TXT}: ${l}`).join(`${HINT_TXT}${SEP}`) + " "
|
|
813
|
+
});
|
|
814
|
+
let hints = null;
|
|
815
|
+
for (const g of [groups, COLLAPSED_HINT_GROUPS]) {
|
|
816
|
+
const h = renderHints(g);
|
|
817
|
+
if (corePlain.length + h.plain.length + 1 <= w) {
|
|
818
|
+
hints = h;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (!hints) {
|
|
823
|
+
return BAR_BASE + BAR_BRAND + fit(corePlain, w) + BAR_RESET;
|
|
824
|
+
}
|
|
825
|
+
const room = w - corePlain.length - hints.plain.length - 1;
|
|
826
|
+
let titleSeg = "";
|
|
827
|
+
if (box?.sessionTitle && room >= 7) {
|
|
828
|
+
titleSeg = ` \u2014 ${ellipsize(box.sessionTitle, Math.min(40, room - 3))}`;
|
|
829
|
+
}
|
|
830
|
+
const leftPlain = brandPrefix + base + titleSeg + (box ? " " : "");
|
|
831
|
+
const leftStyled = BAR_BRAND + brandPrefix + BRAND_BOLD + base + titleSeg + (box ? " " : "") + BRAND_NOBOLD;
|
|
832
|
+
const gap = w - leftPlain.length - hints.plain.length;
|
|
833
|
+
return BAR_BASE + leftStyled + BAR_BASE + " ".repeat(gap) + hints.styled + BAR_RESET;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/wrapped-pty/footer.ts
|
|
837
|
+
var SPINNER_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
|
|
838
|
+
var URGENT = "\x1B[38;5;220m\x1B[1m";
|
|
839
|
+
var TXT = "\x1B[38;5;250m";
|
|
840
|
+
var SUBTLE = "\x1B[38;5;245m";
|
|
841
|
+
var RESET = "\x1B[0m";
|
|
842
|
+
var NOTICE_BG = "\x1B[48;5;220m";
|
|
843
|
+
var NOTICE_FG = "\x1B[38;5;16m\x1B[1m";
|
|
844
|
+
var CLAUDE_IDLE_HINTS = [
|
|
845
|
+
["Control+a q", "detach"]
|
|
846
|
+
];
|
|
847
|
+
var SHELL_IDLE_HINTS = [];
|
|
848
|
+
function padTo(visible, width) {
|
|
849
|
+
if (visible.length === width) return visible;
|
|
850
|
+
if (visible.length > width) {
|
|
851
|
+
if (width <= 1) return visible.slice(0, width);
|
|
852
|
+
return visible.slice(0, width - 1) + "\u2026";
|
|
853
|
+
}
|
|
854
|
+
return visible + " ".repeat(width - visible.length);
|
|
855
|
+
}
|
|
856
|
+
function renderFooter(state, cols) {
|
|
857
|
+
if (cols <= 0) return "";
|
|
858
|
+
if (state.kind === "idle") {
|
|
859
|
+
const sidebarBox = {
|
|
860
|
+
id: "",
|
|
861
|
+
// unused by statusLine
|
|
862
|
+
name: state.boxName,
|
|
863
|
+
state: "running",
|
|
864
|
+
// we're attached, so the container is up
|
|
865
|
+
claudeActivity: state.claudeActivity,
|
|
866
|
+
sessionTitle: state.sessionTitle
|
|
867
|
+
};
|
|
868
|
+
const hints = state.mode === "claude" ? CLAUDE_IDLE_HINTS : SHELL_IDLE_HINTS;
|
|
869
|
+
const stateLabel = state.mode === "shell" ? "shell" : void 0;
|
|
870
|
+
return statusLine(sidebarBox, cols, stateLabel, hints);
|
|
871
|
+
}
|
|
872
|
+
if (state.kind === "notice") {
|
|
873
|
+
const spinner5 = SPINNER_FRAMES[state.frame % SPINNER_FRAMES.length];
|
|
874
|
+
const prefix = ` ${spinner5} `;
|
|
875
|
+
const inner2 = Math.max(0, cols - prefix.length);
|
|
876
|
+
const message2 = padTo(state.message, inner2);
|
|
877
|
+
return `${NOTICE_BG}${NOTICE_FG}${prefix}${message2}${RESET}`;
|
|
878
|
+
}
|
|
879
|
+
const def = state.prompt.defaultAnswer ?? "n";
|
|
880
|
+
const yn = def === "y" ? "[Y/n]" : "[y/N]";
|
|
881
|
+
const tag2 = " [!] ";
|
|
882
|
+
const sep = " ";
|
|
883
|
+
const hintW = ` ${yn} `.length;
|
|
884
|
+
const inner = Math.max(0, cols - tag2.length - hintW);
|
|
885
|
+
const detailRaw = state.prompt.detail ?? "";
|
|
886
|
+
let message = state.prompt.message;
|
|
887
|
+
let detail = detailRaw;
|
|
888
|
+
const messageBudget = Math.max(8, inner - (detail.length > 0 ? sep.length + 8 : 0));
|
|
889
|
+
if (message.length > messageBudget) {
|
|
890
|
+
message = message.slice(0, Math.max(0, messageBudget - 1)) + "\u2026";
|
|
891
|
+
}
|
|
892
|
+
const usedByMessage = message.length;
|
|
893
|
+
const detailBudget = Math.max(0, inner - usedByMessage - sep.length);
|
|
894
|
+
if (detail.length > detailBudget) {
|
|
895
|
+
detail = detailBudget <= 1 ? "" : detail.slice(0, detailBudget - 1) + "\u2026";
|
|
896
|
+
}
|
|
897
|
+
const middlePlain = detail.length > 0 ? `${message}${sep}${detail}` : message;
|
|
898
|
+
const padded = padTo(middlePlain, inner);
|
|
899
|
+
return `${BAR_BG}${URGENT}${tag2}${TXT}${padded}${SUBTLE} ${yn} ${RESET}`;
|
|
900
|
+
}
|
|
901
|
+
function cursorMoveTo(row2, col) {
|
|
902
|
+
return `\x1B[${String(row2)};${String(col)}H`;
|
|
903
|
+
}
|
|
904
|
+
var CURSOR_SAVE = "\x1B7";
|
|
905
|
+
var CURSOR_RESTORE = "\x1B8";
|
|
906
|
+
var SYNC_BEGIN = "\x1B[?2026h";
|
|
907
|
+
var SYNC_END = "\x1B[?2026l";
|
|
908
|
+
|
|
909
|
+
// src/wrapped-pty/prompt-client.ts
|
|
910
|
+
import { request as httpRequest } from "http";
|
|
911
|
+
import { request as httpsRequest } from "https";
|
|
912
|
+
var INITIAL_BACKOFF_MS = 200;
|
|
913
|
+
var MAX_BACKOFF_MS = 5e3;
|
|
914
|
+
function subscribePrompts(opts) {
|
|
915
|
+
let closed = false;
|
|
916
|
+
let req = null;
|
|
917
|
+
let res = null;
|
|
918
|
+
let reconnectTimer = null;
|
|
919
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
920
|
+
let url;
|
|
921
|
+
try {
|
|
922
|
+
url = new URL(opts.relayBaseUrl);
|
|
923
|
+
} catch (err) {
|
|
924
|
+
if (opts.onError) opts.onError(err instanceof Error ? err : new Error(String(err)));
|
|
925
|
+
return { close: () => {
|
|
926
|
+
} };
|
|
927
|
+
}
|
|
928
|
+
const isHttps = url.protocol === "https:";
|
|
929
|
+
const transport = isHttps ? httpsRequest : httpRequest;
|
|
930
|
+
const port = url.port.length > 0 ? Number.parseInt(url.port, 10) : isHttps ? 443 : 80;
|
|
931
|
+
function scheduleReconnect() {
|
|
932
|
+
if (closed) return;
|
|
933
|
+
const delay = backoffMs;
|
|
934
|
+
backoffMs = Math.min(MAX_BACKOFF_MS, backoffMs * 2);
|
|
935
|
+
reconnectTimer = setTimeout(() => {
|
|
936
|
+
reconnectTimer = null;
|
|
937
|
+
connect();
|
|
938
|
+
}, delay);
|
|
939
|
+
if (typeof reconnectTimer.unref === "function") reconnectTimer.unref();
|
|
940
|
+
}
|
|
941
|
+
let buffer = "";
|
|
942
|
+
function consumeMessages() {
|
|
943
|
+
let idx = buffer.indexOf("\n\n");
|
|
944
|
+
while (idx !== -1) {
|
|
945
|
+
const raw = buffer.slice(0, idx);
|
|
946
|
+
buffer = buffer.slice(idx + 2);
|
|
947
|
+
idx = buffer.indexOf("\n\n");
|
|
948
|
+
if (raw.startsWith(":")) continue;
|
|
949
|
+
let event = "";
|
|
950
|
+
let dataLine = "";
|
|
951
|
+
for (const line of raw.split("\n")) {
|
|
952
|
+
if (line.startsWith("event:")) event = line.slice("event:".length).trim();
|
|
953
|
+
else if (line.startsWith("data:")) dataLine = line.slice("data:".length).trim();
|
|
954
|
+
}
|
|
955
|
+
if (event === "prompt-ask" && dataLine.length > 0) {
|
|
956
|
+
try {
|
|
957
|
+
const ev = JSON.parse(dataLine);
|
|
958
|
+
if (ev && typeof ev.id === "string") opts.onPrompt(ev);
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
} else if (event === "prompt-resolved" && dataLine.length > 0) {
|
|
962
|
+
try {
|
|
963
|
+
const payload = JSON.parse(dataLine);
|
|
964
|
+
if (payload && typeof payload.id === "string") opts.onResolved(payload.id);
|
|
965
|
+
} catch {
|
|
966
|
+
}
|
|
967
|
+
} else if (event === "notice-set" && dataLine.length > 0) {
|
|
968
|
+
try {
|
|
969
|
+
const ev = JSON.parse(dataLine);
|
|
970
|
+
if (ev && typeof ev.id === "string") opts.onNotice?.(ev);
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
} else if (event === "notice-clear" && dataLine.length > 0) {
|
|
974
|
+
try {
|
|
975
|
+
const payload = JSON.parse(dataLine);
|
|
976
|
+
if (payload && typeof payload.id === "string") opts.onNoticeCleared?.(payload.id);
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function connect() {
|
|
983
|
+
if (closed) return;
|
|
984
|
+
req = transport({
|
|
985
|
+
host: url.hostname,
|
|
986
|
+
port,
|
|
987
|
+
method: "GET",
|
|
988
|
+
path: `${url.pathname.replace(/\/$/, "")}/admin/prompts/stream?boxId=${encodeURIComponent(opts.boxId)}`,
|
|
989
|
+
headers: { Accept: "text/event-stream" }
|
|
990
|
+
});
|
|
991
|
+
req.on("response", (r) => {
|
|
992
|
+
res = r;
|
|
993
|
+
if (r.statusCode !== 200) {
|
|
994
|
+
if (opts.onError) opts.onError(new Error(`SSE stream returned ${String(r.statusCode)}`));
|
|
995
|
+
r.resume();
|
|
996
|
+
close();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
backoffMs = INITIAL_BACKOFF_MS;
|
|
1000
|
+
r.setEncoding("utf8");
|
|
1001
|
+
r.on("data", (chunk) => {
|
|
1002
|
+
buffer += chunk;
|
|
1003
|
+
consumeMessages();
|
|
1004
|
+
});
|
|
1005
|
+
r.on("end", () => {
|
|
1006
|
+
if (!closed) scheduleReconnect();
|
|
1007
|
+
});
|
|
1008
|
+
r.on("error", () => {
|
|
1009
|
+
if (!closed) scheduleReconnect();
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
req.on("error", () => {
|
|
1013
|
+
if (!closed) scheduleReconnect();
|
|
1014
|
+
});
|
|
1015
|
+
req.end();
|
|
1016
|
+
}
|
|
1017
|
+
function close() {
|
|
1018
|
+
if (closed) return;
|
|
1019
|
+
closed = true;
|
|
1020
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
1021
|
+
try {
|
|
1022
|
+
res?.destroy();
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
req?.destroy();
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
connect();
|
|
1031
|
+
return { close };
|
|
1032
|
+
}
|
|
1033
|
+
function postAnswer(opts) {
|
|
1034
|
+
return new Promise((resolve2) => {
|
|
1035
|
+
let url;
|
|
1036
|
+
try {
|
|
1037
|
+
url = new URL(opts.relayBaseUrl);
|
|
1038
|
+
} catch {
|
|
1039
|
+
resolve2({ ok: false, status: 0 });
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const isHttps = url.protocol === "https:";
|
|
1043
|
+
const transport = isHttps ? httpsRequest : httpRequest;
|
|
1044
|
+
const port = url.port.length > 0 ? Number.parseInt(url.port, 10) : isHttps ? 443 : 80;
|
|
1045
|
+
const json = JSON.stringify(opts.body);
|
|
1046
|
+
const req = transport(
|
|
1047
|
+
{
|
|
1048
|
+
host: url.hostname,
|
|
1049
|
+
port,
|
|
1050
|
+
method: "POST",
|
|
1051
|
+
path: `${url.pathname.replace(/\/$/, "")}/admin/prompts/answer`,
|
|
1052
|
+
headers: {
|
|
1053
|
+
"Content-Type": "application/json",
|
|
1054
|
+
"Content-Length": Buffer.byteLength(json).toString()
|
|
1055
|
+
},
|
|
1056
|
+
timeout: 3e3
|
|
1057
|
+
},
|
|
1058
|
+
(res) => {
|
|
1059
|
+
res.resume();
|
|
1060
|
+
const status = res.statusCode ?? 0;
|
|
1061
|
+
resolve2({ ok: status === 204 || status === 404, status });
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
req.on("error", () => resolve2({ ok: false, status: 0 }));
|
|
1065
|
+
req.on("timeout", () => {
|
|
1066
|
+
req.destroy();
|
|
1067
|
+
resolve2({ ok: false, status: 0 });
|
|
1068
|
+
});
|
|
1069
|
+
req.write(json);
|
|
1070
|
+
req.end();
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/wrapped-pty/run.ts
|
|
1075
|
+
var FOOTER_ROWS = 1;
|
|
1076
|
+
var STATUS_POLL_INTERVAL_MS = 3e3;
|
|
1077
|
+
var SPINNER_INTERVAL_MS = 120;
|
|
1078
|
+
async function runWrappedAttach(opts) {
|
|
1079
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
1080
|
+
return runFallback(opts.dockerArgv);
|
|
1081
|
+
}
|
|
1082
|
+
const backend = await loadPtyBackend();
|
|
1083
|
+
if (!backend) {
|
|
1084
|
+
process.stderr.write(
|
|
1085
|
+
"agentbox: permission prompts disabled (node-pty backend unavailable)\n"
|
|
1086
|
+
);
|
|
1087
|
+
return runFallback(opts.dockerArgv);
|
|
1088
|
+
}
|
|
1089
|
+
const cols = process.stdout.columns ?? 80;
|
|
1090
|
+
const rows = process.stdout.rows ?? 24;
|
|
1091
|
+
const innerRows = Math.max(1, rows - FOOTER_ROWS);
|
|
1092
|
+
const pty = backend.ptySpawn("docker", opts.dockerArgv, {
|
|
1093
|
+
name: "xterm-256color",
|
|
1094
|
+
cols,
|
|
1095
|
+
rows: innerRows,
|
|
1096
|
+
env: process.env
|
|
1097
|
+
});
|
|
1098
|
+
const buildIdle = (sessionTitle, claudeActivity) => ({
|
|
1099
|
+
kind: "idle",
|
|
1100
|
+
boxName: opts.boxName,
|
|
1101
|
+
sessionTitle,
|
|
1102
|
+
claudeActivity,
|
|
1103
|
+
mode: opts.mode
|
|
1104
|
+
});
|
|
1105
|
+
let footerState = buildIdle();
|
|
1106
|
+
let lastSessionTitle;
|
|
1107
|
+
let lastActivity;
|
|
1108
|
+
let capturingPrompt = null;
|
|
1109
|
+
let activeNotice = null;
|
|
1110
|
+
let noticeFrame = 0;
|
|
1111
|
+
let spinnerTimer = null;
|
|
1112
|
+
const redrawFooter = () => {
|
|
1113
|
+
const cs = process.stdout.columns ?? cols;
|
|
1114
|
+
const rs = process.stdout.rows ?? rows;
|
|
1115
|
+
const line = renderFooter(footerState, cs);
|
|
1116
|
+
const payload = SYNC_BEGIN + CURSOR_SAVE + cursorMoveTo(rs, 1) + line + CURSOR_RESTORE + SYNC_END;
|
|
1117
|
+
process.stdout.write(payload);
|
|
1118
|
+
};
|
|
1119
|
+
const recomputeFooter = () => {
|
|
1120
|
+
if (capturingPrompt) {
|
|
1121
|
+
footerState = { kind: "prompt", prompt: capturingPrompt };
|
|
1122
|
+
} else if (activeNotice) {
|
|
1123
|
+
footerState = { kind: "notice", message: activeNotice.message, frame: noticeFrame };
|
|
1124
|
+
} else {
|
|
1125
|
+
footerState = buildIdle(lastSessionTitle, lastActivity);
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
const startSpinner = () => {
|
|
1129
|
+
if (spinnerTimer) return;
|
|
1130
|
+
spinnerTimer = setInterval(() => {
|
|
1131
|
+
noticeFrame++;
|
|
1132
|
+
if (footerState.kind === "notice") {
|
|
1133
|
+
recomputeFooter();
|
|
1134
|
+
redrawFooter();
|
|
1135
|
+
}
|
|
1136
|
+
}, SPINNER_INTERVAL_MS);
|
|
1137
|
+
if (typeof spinnerTimer.unref === "function") spinnerTimer.unref();
|
|
1138
|
+
};
|
|
1139
|
+
const stopSpinner = () => {
|
|
1140
|
+
if (spinnerTimer) {
|
|
1141
|
+
clearInterval(spinnerTimer);
|
|
1142
|
+
spinnerTimer = null;
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
pty.onData((d) => {
|
|
1146
|
+
process.stdout.write(d);
|
|
1147
|
+
redrawFooter();
|
|
1148
|
+
});
|
|
1149
|
+
const router = createInputRouter({
|
|
1150
|
+
onForward: (b) => {
|
|
1151
|
+
pty.write(b.toString("utf8"));
|
|
1152
|
+
},
|
|
1153
|
+
onAnswer: (body) => {
|
|
1154
|
+
void postAnswer({ relayBaseUrl: opts.relayBaseUrl, body });
|
|
1155
|
+
capturingPrompt = null;
|
|
1156
|
+
recomputeFooter();
|
|
1157
|
+
redrawFooter();
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
1161
|
+
process.stdin.resume();
|
|
1162
|
+
const onStdinData = (chunk) => {
|
|
1163
|
+
router.feed(chunk);
|
|
1164
|
+
};
|
|
1165
|
+
process.stdin.on("data", onStdinData);
|
|
1166
|
+
const onResize = () => {
|
|
1167
|
+
const cs = process.stdout.columns ?? cols;
|
|
1168
|
+
const rs = process.stdout.rows ?? rows;
|
|
1169
|
+
const inner = Math.max(1, rs - FOOTER_ROWS);
|
|
1170
|
+
pty.resize(cs, inner);
|
|
1171
|
+
process.stdout.write(`\x1B[1;${String(inner)}r`);
|
|
1172
|
+
redrawFooter();
|
|
1173
|
+
};
|
|
1174
|
+
process.stdout.on("resize", onResize);
|
|
1175
|
+
const stream = subscribePrompts({
|
|
1176
|
+
relayBaseUrl: opts.relayBaseUrl,
|
|
1177
|
+
boxId: opts.boxId,
|
|
1178
|
+
onPrompt: (ev) => {
|
|
1179
|
+
capturingPrompt = ev;
|
|
1180
|
+
recomputeFooter();
|
|
1181
|
+
redrawFooter();
|
|
1182
|
+
router.capture(ev).catch(() => {
|
|
1183
|
+
});
|
|
1184
|
+
},
|
|
1185
|
+
onResolved: (id) => {
|
|
1186
|
+
if (capturingPrompt && capturingPrompt.id === id) {
|
|
1187
|
+
capturingPrompt = null;
|
|
1188
|
+
router.abort("resolved-elsewhere");
|
|
1189
|
+
recomputeFooter();
|
|
1190
|
+
redrawFooter();
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
onNotice: (ev) => {
|
|
1194
|
+
activeNotice = ev;
|
|
1195
|
+
startSpinner();
|
|
1196
|
+
recomputeFooter();
|
|
1197
|
+
redrawFooter();
|
|
1198
|
+
},
|
|
1199
|
+
onNoticeCleared: (id) => {
|
|
1200
|
+
if (activeNotice && activeNotice.id === id) {
|
|
1201
|
+
activeNotice = null;
|
|
1202
|
+
stopSpinner();
|
|
1203
|
+
recomputeFooter();
|
|
1204
|
+
redrawFooter();
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
const pollStatus = async () => {
|
|
1209
|
+
try {
|
|
1210
|
+
const status = await readBoxStatus({
|
|
1211
|
+
id: opts.boxId,
|
|
1212
|
+
name: opts.boxName,
|
|
1213
|
+
projectIndex: opts.projectIndex
|
|
1214
|
+
});
|
|
1215
|
+
const nextTitle = status?.claude?.sessionTitle?.trim() || void 0;
|
|
1216
|
+
const nextActivity = status?.claude?.state || void 0;
|
|
1217
|
+
if (nextTitle === lastSessionTitle && nextActivity === lastActivity) return;
|
|
1218
|
+
lastSessionTitle = nextTitle;
|
|
1219
|
+
lastActivity = nextActivity;
|
|
1220
|
+
if (footerState.kind === "idle") {
|
|
1221
|
+
recomputeFooter();
|
|
1222
|
+
redrawFooter();
|
|
1223
|
+
}
|
|
1224
|
+
} catch {
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
void pollStatus();
|
|
1228
|
+
const statusTimer = setInterval(() => {
|
|
1229
|
+
void pollStatus();
|
|
1230
|
+
}, STATUS_POLL_INTERVAL_MS);
|
|
1231
|
+
if (typeof statusTimer.unref === "function") statusTimer.unref();
|
|
1232
|
+
process.stdout.write(`\x1B[1;${String(innerRows)}r`);
|
|
1233
|
+
if (opts.mode === "shell") {
|
|
1234
|
+
process.stdout.write("\x1B[H\x1B[2J");
|
|
1235
|
+
}
|
|
1236
|
+
redrawFooter();
|
|
1237
|
+
const exitCode = await new Promise((resolve2) => {
|
|
1238
|
+
pty.onExit(({ exitCode: exitCode2 }) => resolve2(exitCode2));
|
|
1239
|
+
});
|
|
1240
|
+
process.stdin.off("data", onStdinData);
|
|
1241
|
+
process.stdout.off("resize", onResize);
|
|
1242
|
+
clearInterval(statusTimer);
|
|
1243
|
+
stopSpinner();
|
|
1244
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1245
|
+
process.stdin.pause();
|
|
1246
|
+
stream.close();
|
|
1247
|
+
router.dispose();
|
|
1248
|
+
const rsFinal = process.stdout.rows ?? rows;
|
|
1249
|
+
const csFinal = process.stdout.columns ?? cols;
|
|
1250
|
+
process.stdout.write(
|
|
1251
|
+
"\x1B[r" + cursorMoveTo(rsFinal, 1) + `\x1B[2K` + cursorMoveTo(rsFinal, csFinal)
|
|
1252
|
+
);
|
|
1253
|
+
if (exitCode === 0 && opts.detachNotice) {
|
|
1254
|
+
process.stdout.write("\x1B[1A\x1B[2K\r" + opts.detachNotice + "\n");
|
|
1255
|
+
}
|
|
1256
|
+
return exitCode;
|
|
1257
|
+
}
|
|
1258
|
+
function runFallback(argv) {
|
|
1259
|
+
const child = spawnSync2("docker", argv, { stdio: "inherit" });
|
|
1260
|
+
return child.status ?? 0;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/commands/claude.ts
|
|
1264
|
+
function reattachRef(r) {
|
|
1265
|
+
return typeof r.projectIndex === "number" ? String(r.projectIndex) : r.name;
|
|
1266
|
+
}
|
|
1267
|
+
function logPrune(rebuild) {
|
|
1268
|
+
if (rebuild.prunedBytes <= 0) return;
|
|
1269
|
+
const mb = Math.round(rebuild.prunedBytes / 1024 / 1024);
|
|
1270
|
+
const n = rebuild.pruned.length;
|
|
1271
|
+
log5.info(`pruned ${String(n)} stale plugin cache${n === 1 ? "" : "s"} (${String(mb)} MB freed)`);
|
|
1272
|
+
}
|
|
1273
|
+
var RELAY_HOST_URL = `http://127.0.0.1:${String(DEFAULT_RELAY_PORT)}`;
|
|
1274
|
+
async function attachClaudeWrapped(box, sessionName, reattach) {
|
|
1275
|
+
const code = await runWrappedAttach({
|
|
1276
|
+
container: box.container,
|
|
1277
|
+
dockerArgv: buildClaudeAttachArgv(box.container, sessionName),
|
|
1278
|
+
relayBaseUrl: RELAY_HOST_URL,
|
|
1279
|
+
boxId: box.id,
|
|
1280
|
+
boxName: box.name,
|
|
1281
|
+
projectIndex: box.projectIndex,
|
|
1282
|
+
mode: "claude",
|
|
1283
|
+
detachNotice: formatDetachNotice(reattach)
|
|
1284
|
+
});
|
|
1285
|
+
process.exit(code);
|
|
1286
|
+
}
|
|
1287
|
+
function buildClaudeCliOverrides(opts) {
|
|
1288
|
+
const box = {};
|
|
1289
|
+
if (opts.hostSnapshot !== void 0) box.hostSnapshot = opts.hostSnapshot;
|
|
1290
|
+
if (opts.image !== void 0) box.image = opts.image;
|
|
1291
|
+
if (opts.withPlaywright === true) box.withPlaywright = true;
|
|
1292
|
+
if (opts.withEnv === true) box.withEnv = true;
|
|
1293
|
+
if (opts.vnc === false) box.vnc = false;
|
|
1294
|
+
if (opts.isolateClaudeConfig === true) box.isolateClaudeConfig = true;
|
|
1295
|
+
if (opts.sharedDockerCache === true) box.dockerCacheShared = true;
|
|
1296
|
+
const claude = {};
|
|
1297
|
+
if (opts.sessionName !== void 0) claude.sessionName = opts.sessionName;
|
|
1298
|
+
const out = {};
|
|
1299
|
+
if (Object.keys(box).length > 0) out.box = box;
|
|
1300
|
+
if (Object.keys(claude).length > 0) out.claude = claude;
|
|
1301
|
+
return out;
|
|
1302
|
+
}
|
|
1303
|
+
async function runClaudeLoginContainer(image, extraArgs) {
|
|
1304
|
+
const { exitCode } = runInteractiveClaudeLogin(
|
|
1305
|
+
buildClaudeLoginRunArgv({ volume: SHARED_CLAUDE_VOLUME, image, extraArgs })
|
|
1306
|
+
);
|
|
1307
|
+
if (exitCode === 0) {
|
|
1308
|
+
const s = spinner();
|
|
1309
|
+
s.start("checking credentials");
|
|
1310
|
+
const warm = await warmUpClaudeCredentials(SHARED_CLAUDE_VOLUME, image, {
|
|
1311
|
+
onProgress: (line) => s.message(clampSpinnerLine(line))
|
|
1312
|
+
});
|
|
1313
|
+
s.stop(warm.warmed ? "credentials ready" : "credentials check incomplete \u2014 continuing");
|
|
1314
|
+
await syncClaudeCredentials({ volume: SHARED_CLAUDE_VOLUME }, { image, isolate: false });
|
|
1315
|
+
}
|
|
1316
|
+
return exitCode;
|
|
1317
|
+
}
|
|
1318
|
+
async function maybeRunClaudeLogin(args) {
|
|
1319
|
+
if (!process.stdin.isTTY || args.yes) return;
|
|
1320
|
+
if (args.authSource === "host-env") return;
|
|
1321
|
+
if (await hostBackupHasCredentials()) return;
|
|
1322
|
+
const message = args.authSource === "auth-file" ? "You're on a legacy API token (shows as 'Claude API'). Sign in with your Claude subscription instead?" : "Sign in with your Claude subscription? (saved and reused by every box)";
|
|
1323
|
+
const answer = await confirm2({ message, initialValue: true });
|
|
1324
|
+
if (isCancel2(answer) || !answer) {
|
|
1325
|
+
log5.info("Skipped sign-in \u2014 claude will prompt you to /login inside the box.");
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
const s = spinner();
|
|
1329
|
+
s.start("preparing sandbox image");
|
|
1330
|
+
await ensureImage(args.image, { onProgress: (line) => s.message(clampSpinnerLine(line)) });
|
|
1331
|
+
s.message("preparing claude config");
|
|
1332
|
+
await ensureClaudeVolume(
|
|
1333
|
+
{ volume: SHARED_CLAUDE_VOLUME },
|
|
1334
|
+
{ syncFromHost: true, image: args.image, hostWorkspace: args.hostWorkspace }
|
|
1335
|
+
);
|
|
1336
|
+
s.stop("image ready");
|
|
1337
|
+
const exitCode = await runClaudeLoginContainer(args.image, ["--claudeai"]);
|
|
1338
|
+
if (exitCode !== 0) {
|
|
1339
|
+
log5.warn("Claude login did not complete; continuing \u2014 run `agentbox claude login` to retry.");
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
log5.success("Signed in with your Claude subscription \u2014 saved for future boxes.");
|
|
1343
|
+
}
|
|
1344
|
+
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", "APFS-clone the host workspace into a per-box scratch dir before seeding /workspace (stabilizes the tar-pipe source)").option("--no-host-snapshot", "tar-pipe directly from the live host workspace at create time").option(
|
|
1345
|
+
"--snapshot <ref>",
|
|
1346
|
+
"start from a project checkpoint (see `agentbox checkpoint`); overrides box.defaultCheckpoint"
|
|
1347
|
+
).option("--image <ref>", "override the box image").option("-y, --yes", "skip prompts, accept defaults").option(
|
|
1348
|
+
"--isolate-claude-config",
|
|
1349
|
+
"use a per-box ~/.claude volume instead of the shared agentbox-claude-config"
|
|
1350
|
+
).option("--with-playwright", "also install @playwright/cli@latest globally inside the box").option(
|
|
1351
|
+
"--with-env",
|
|
1352
|
+
"copy host env/config files (.env*, secrets.toml, agentbox.yaml, ...) into /workspace at create time (gitignore-bypassing)"
|
|
1353
|
+
).option("--no-vnc", "disable the per-box Xvnc + noVNC web client (on by default)").option(
|
|
1354
|
+
"--shared-docker-cache",
|
|
1355
|
+
"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)"
|
|
1356
|
+
).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(
|
|
1357
|
+
"[claude-args...]",
|
|
1358
|
+
"extra args passed to claude inside the box; place after `--`, e.g. `agentbox claude -- --model sonnet`"
|
|
1359
|
+
).action(async (claudeArgs, opts) => {
|
|
1360
|
+
intro("Starting Claude in a box...");
|
|
1361
|
+
const cfg = await loadEffectiveConfig(opts.workspace, {
|
|
1362
|
+
cliOverrides: buildClaudeCliOverrides(opts)
|
|
1363
|
+
});
|
|
1364
|
+
const projectRoot = (await findProjectRoot(opts.workspace)).root;
|
|
1365
|
+
const checkpointRef = opts.snapshot && opts.snapshot.length > 0 ? opts.snapshot : cfg.effective.box.defaultCheckpoint.length > 0 ? cfg.effective.box.defaultCheckpoint : void 0;
|
|
1366
|
+
const resolved = await resolveClaudeAuth(process.env);
|
|
1367
|
+
await maybeRunClaudeLogin({
|
|
1368
|
+
image: cfg.effective.box.image,
|
|
1369
|
+
authSource: resolved.source,
|
|
1370
|
+
yes: !!opts.yes,
|
|
1371
|
+
hostWorkspace: opts.workspace
|
|
1372
|
+
});
|
|
1373
|
+
const wiz = await maybeRunSetupWizard({
|
|
1374
|
+
workspace: opts.workspace,
|
|
1375
|
+
yes: !!opts.yes,
|
|
1376
|
+
command: "claude",
|
|
1377
|
+
checkpointRef,
|
|
1378
|
+
withEnv: cfg.effective.box.withEnv
|
|
1379
|
+
});
|
|
1380
|
+
let effectiveClaudeArgs = claudeArgs;
|
|
1381
|
+
if (wiz.action === "launch-with-prompt" && wiz.initialPrompt) {
|
|
1382
|
+
effectiveClaudeArgs = resolveAgentLauncher("claude-code").buildArgs(
|
|
1383
|
+
wiz.initialPrompt,
|
|
1384
|
+
claudeArgs
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
const useSnapshot = opts.hostSnapshot === false ? false : opts.hostSnapshot === true ? true : cfg.effective.box.hostSnapshot ?? false;
|
|
1388
|
+
const sessionName = cfg.effective.claude.sessionName;
|
|
1389
|
+
const s = spinner();
|
|
1390
|
+
s.start("creating box");
|
|
1391
|
+
let containerName = "";
|
|
1392
|
+
try {
|
|
1393
|
+
const withPlaywright = cfg.effective.box.withPlaywright || cfg.effective.browser.default !== "agent-browser";
|
|
1394
|
+
const result = await createBox({
|
|
1395
|
+
workspacePath: opts.workspace,
|
|
1396
|
+
name: opts.name,
|
|
1397
|
+
useSnapshot,
|
|
1398
|
+
checkpointRef,
|
|
1399
|
+
image: cfg.effective.box.image,
|
|
1400
|
+
claudeConfig: { isolate: cfg.effective.box.isolateClaudeConfig },
|
|
1401
|
+
claudeEnv: resolved.env,
|
|
1402
|
+
withPlaywright,
|
|
1403
|
+
withEnv: cfg.effective.box.withEnv,
|
|
1404
|
+
envFilesToImport: wiz.envFilesToImport,
|
|
1405
|
+
vnc: { enabled: cfg.effective.box.vnc },
|
|
1406
|
+
docker: { sharedCache: cfg.effective.box.dockerCacheShared },
|
|
1407
|
+
limits: resolveLimits(cfg.effective.box, opts),
|
|
1408
|
+
projectRoot,
|
|
1409
|
+
onLog: (line) => s.message(clampSpinnerLine(line))
|
|
1410
|
+
});
|
|
1411
|
+
containerName = result.record.container;
|
|
1412
|
+
s.message("checking plugin native deps");
|
|
1413
|
+
const rebuild = await rebuildPluginNativeDeps(result.record.container, {
|
|
1414
|
+
volume: result.record.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME,
|
|
1415
|
+
onProgress: (line) => s.message(clampSpinnerLine(line))
|
|
1416
|
+
});
|
|
1417
|
+
s.message("starting claude session");
|
|
1418
|
+
await startClaudeSession({
|
|
1419
|
+
container: result.record.container,
|
|
1420
|
+
claudeArgs: effectiveClaudeArgs,
|
|
1421
|
+
sessionName,
|
|
1422
|
+
boxName: result.record.name
|
|
1423
|
+
});
|
|
1424
|
+
const nSuffix = typeof result.record.projectIndex === "number" ? ` \xB7 n ${String(result.record.projectIndex)}` : "";
|
|
1425
|
+
s.stop(`box ${result.record.container} ready${nSuffix}`);
|
|
1426
|
+
logPrune(rebuild);
|
|
1427
|
+
for (const f of rebuild.failed) {
|
|
1428
|
+
log5.warn(`plugin install failed for ${f.dir}; claude may still load it. stderr:
|
|
1429
|
+
${f.stderr.trim()}`);
|
|
1430
|
+
}
|
|
1431
|
+
outro("attaching \u2014 Control+a q to detach, leaves claude running");
|
|
1432
|
+
await attachClaudeWrapped(result.record, sessionName, reattachRef(result.record));
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
s.stop("failed");
|
|
1435
|
+
if (err instanceof ClaudeSessionError) {
|
|
1436
|
+
log5.error(err.message);
|
|
1437
|
+
if (containerName) {
|
|
1438
|
+
log5.info(`The box ${containerName} is still running. Destroy it with:`);
|
|
1439
|
+
log5.info(` agentbox destroy ${containerName} -y`);
|
|
1440
|
+
}
|
|
1441
|
+
process.exit(1);
|
|
1442
|
+
}
|
|
1443
|
+
handleLifecycleError(err);
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
async function startOrAttachClaude(box, claudeArgs, opts) {
|
|
1447
|
+
const cfg = await loadEffectiveConfig(box.workspacePath, {
|
|
1448
|
+
cliOverrides: opts.sessionName ? { claude: { sessionName: opts.sessionName } } : {}
|
|
1449
|
+
});
|
|
1450
|
+
const sessionName = cfg.effective.claude.sessionName;
|
|
1451
|
+
const resolved = await resolveClaudeAuth(process.env);
|
|
1452
|
+
const insp = await inspectBox(box.id);
|
|
1453
|
+
if (insp.state === "missing") {
|
|
1454
|
+
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
1455
|
+
}
|
|
1456
|
+
const existing = await claudeSessionInfo(box.container, sessionName);
|
|
1457
|
+
if (existing.running) {
|
|
1458
|
+
outro(`session "${sessionName}" already running \u2014 attaching (Control+a q to detach)`);
|
|
1459
|
+
await attachClaudeWrapped(box, sessionName, reattachRef(box));
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
await maybeRunClaudeLogin({
|
|
1463
|
+
image: box.image,
|
|
1464
|
+
authSource: resolved.source,
|
|
1465
|
+
yes: false,
|
|
1466
|
+
hostWorkspace: box.workspacePath
|
|
1467
|
+
});
|
|
1468
|
+
const s = spinner();
|
|
1469
|
+
s.start("preparing box");
|
|
1470
|
+
if (insp.state === "paused") {
|
|
1471
|
+
s.message("unpausing box");
|
|
1472
|
+
await unpauseBox(box.id);
|
|
1473
|
+
} else if (insp.state === "stopped") {
|
|
1474
|
+
s.message("starting box");
|
|
1475
|
+
await startBox(box.id);
|
|
1476
|
+
}
|
|
1477
|
+
const syncConfig = opts.syncConfig !== false;
|
|
1478
|
+
if (syncConfig) {
|
|
1479
|
+
s.message("syncing ~/.claude into box volume");
|
|
1480
|
+
const volume = box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME;
|
|
1481
|
+
await ensureClaudeVolume(
|
|
1482
|
+
{ volume },
|
|
1483
|
+
{
|
|
1484
|
+
syncFromHost: true,
|
|
1485
|
+
image: box.image,
|
|
1486
|
+
hostWorkspace: box.workspacePath
|
|
1487
|
+
}
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
const claudeVolume = box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME;
|
|
1491
|
+
await seedSetupSkillIntoVolume(claudeVolume, box.image);
|
|
1492
|
+
await syncClaudeCredentials(
|
|
1493
|
+
{ volume: claudeVolume },
|
|
1494
|
+
{ image: box.image, isolate: claudeVolume !== SHARED_CLAUDE_VOLUME }
|
|
1495
|
+
);
|
|
1496
|
+
s.message("checking plugin native deps");
|
|
1497
|
+
const rebuild = await rebuildPluginNativeDeps(box.container, {
|
|
1498
|
+
volume: box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME,
|
|
1499
|
+
onProgress: (line) => s.message(clampSpinnerLine(line))
|
|
1500
|
+
});
|
|
1501
|
+
s.message("starting claude session");
|
|
1502
|
+
await startClaudeSession({
|
|
1503
|
+
container: box.container,
|
|
1504
|
+
claudeArgs,
|
|
1505
|
+
sessionName,
|
|
1506
|
+
boxName: box.name
|
|
1507
|
+
});
|
|
1508
|
+
s.stop(`box ${box.container} ready`);
|
|
1509
|
+
logPrune(rebuild);
|
|
1510
|
+
for (const f of rebuild.failed) {
|
|
1511
|
+
log5.warn(`plugin install failed for ${f.dir}; claude may still load it. stderr:
|
|
1512
|
+
${f.stderr.trim()}`);
|
|
1513
|
+
}
|
|
1514
|
+
outro("attaching \u2014 Control+a q to detach, leaves claude running");
|
|
1515
|
+
await attachClaudeWrapped(box, sessionName, reattachRef(box));
|
|
1516
|
+
}
|
|
1517
|
+
var claudeAttachCommand = new Command2("attach").description(
|
|
1518
|
+
"Attach to a Claude Code tmux session in a box, starting one if none is running (auto-unpause/start; never re-syncs ~/.claude \u2014 use `claude start` for that)"
|
|
1519
|
+
).argument(
|
|
1520
|
+
"[box]",
|
|
1521
|
+
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
1522
|
+
).option("--session-name <name>", "tmux session name (default from config; built-in: claude)").action(async function(idOrName) {
|
|
1523
|
+
const opts = this.optsWithGlobals();
|
|
1524
|
+
intro("Attaching to Claude session...");
|
|
1525
|
+
try {
|
|
1526
|
+
const box = await resolveBoxOrExit(idOrName);
|
|
1527
|
+
await startOrAttachClaude(box, [], { ...opts, syncConfig: false });
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
if (err instanceof ClaudeSessionError) {
|
|
1530
|
+
log5.error(err.message);
|
|
1531
|
+
process.exit(1);
|
|
1532
|
+
}
|
|
1533
|
+
handleLifecycleError(err);
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
var claudeStartCommand = new Command2("start").description(
|
|
751
1537
|
"Start a Claude Code tmux session in an already-existing box (auto-unpause/start). If a session is already running, just attach."
|
|
752
1538
|
).argument(
|
|
753
1539
|
"[box]",
|
|
@@ -760,7 +1546,7 @@ var claudeStartCommand = new Command2("start").description(
|
|
|
760
1546
|
"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`"
|
|
761
1547
|
).action(async function(idOrName, claudeArgs) {
|
|
762
1548
|
const opts = this.optsWithGlobals();
|
|
763
|
-
intro("
|
|
1549
|
+
intro("Starting Claude in a box...");
|
|
764
1550
|
try {
|
|
765
1551
|
const { box, shifted } = await resolveBoxOrShift(idOrName);
|
|
766
1552
|
const effectiveClaudeArgs = shifted && idOrName ? [idOrName, ...claudeArgs] : claudeArgs;
|
|
@@ -773,19 +1559,53 @@ var claudeStartCommand = new Command2("start").description(
|
|
|
773
1559
|
handleLifecycleError(err);
|
|
774
1560
|
}
|
|
775
1561
|
});
|
|
1562
|
+
var claudeLoginCommand = new Command2("login").description(
|
|
1563
|
+
"Sign in to Claude for use in sandboxes (forwards args to `claude auth login`, e.g. --sso, --console). Runs in a throwaway container against the shared claude-config volume \u2014 usable before the first `agentbox claude`."
|
|
1564
|
+
).argument(
|
|
1565
|
+
"[args...]",
|
|
1566
|
+
"extra args forwarded to `claude auth login`; place after `--`, e.g. `agentbox claude login -- --sso`"
|
|
1567
|
+
).action(async (args) => {
|
|
1568
|
+
intro("Signing in to Claude...");
|
|
1569
|
+
if (!process.stdin.isTTY) {
|
|
1570
|
+
log5.error("`agentbox claude login` needs an interactive terminal.");
|
|
1571
|
+
process.exit(1);
|
|
1572
|
+
}
|
|
1573
|
+
try {
|
|
1574
|
+
const cfg = await loadEffectiveConfig(process.cwd());
|
|
1575
|
+
const image = cfg.effective.box.image;
|
|
1576
|
+
const s = spinner();
|
|
1577
|
+
s.start("preparing sandbox image");
|
|
1578
|
+
await ensureImage(image, { onProgress: (line) => s.message(clampSpinnerLine(line)) });
|
|
1579
|
+
s.stop("image ready");
|
|
1580
|
+
const exitCode = await runClaudeLoginContainer(image, args);
|
|
1581
|
+
if (exitCode !== 0) {
|
|
1582
|
+
log5.warn(`\`claude auth login\` exited with code ${String(exitCode)}`);
|
|
1583
|
+
process.exit(exitCode);
|
|
1584
|
+
}
|
|
1585
|
+
outro("signed in \u2014 credentials saved for future boxes");
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
handleLifecycleError(err);
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
776
1590
|
claudeCommand.addCommand(claudeAttachCommand);
|
|
777
1591
|
claudeCommand.addCommand(claudeStartCommand);
|
|
1592
|
+
claudeCommand.addCommand(claudeLoginCommand);
|
|
778
1593
|
|
|
779
1594
|
// src/commands/checkpoint.ts
|
|
780
1595
|
import { confirm as confirm3, isCancel as isCancel3, log as log6 } from "@clack/prompts";
|
|
781
1596
|
import { Command as Command3 } from "commander";
|
|
1597
|
+
var CHECKPOINT_NOTICE = "Checkpoint in progress \u2014 the box will be unresponsive for a moment";
|
|
1598
|
+
var CHECKPOINT_NOTICE_TTL_MS = 66e4;
|
|
782
1599
|
async function projectRootFor(cwd, recordRoot) {
|
|
783
1600
|
return recordRoot ?? (await findProjectRoot(cwd)).root;
|
|
784
1601
|
}
|
|
785
1602
|
var createSub = new Command3("create").description("Capture a box state as a project checkpoint (<box-name>-<n>)").argument(
|
|
786
1603
|
"[box]",
|
|
787
1604
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
788
|
-
).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").
|
|
1605
|
+
).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").option(
|
|
1606
|
+
"--replace",
|
|
1607
|
+
"if a checkpoint with the same name exists, rm it first (idempotent recapture; safe to retry when the previous run's stdout was lost)"
|
|
1608
|
+
).action(async (idOrName, opts) => {
|
|
789
1609
|
try {
|
|
790
1610
|
const box = await resolveBoxOrExit(idOrName);
|
|
791
1611
|
const insp = await inspectBox(box.id);
|
|
@@ -793,27 +1613,57 @@ var createSub = new Command3("create").description("Capture a box state as a pro
|
|
|
793
1613
|
log6.info("box is paused; unpausing");
|
|
794
1614
|
await unpauseBox(box.id);
|
|
795
1615
|
} else if (insp.state === "stopped") {
|
|
796
|
-
log6.info("box is stopped; starting
|
|
1616
|
+
log6.info("box is stopped; starting");
|
|
797
1617
|
await startBox(box.id);
|
|
798
1618
|
} else if (insp.state === "missing") {
|
|
799
1619
|
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
800
1620
|
}
|
|
801
1621
|
const projectRoot = await projectRootFor(box.workspacePath, box.projectRoot);
|
|
802
1622
|
const cfg = await loadEffectiveConfig(projectRoot);
|
|
803
|
-
const
|
|
804
|
-
box,
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
setDefault: opts.setDefault === true,
|
|
809
|
-
maxLayers: cfg.effective.checkpoint.maxLayers,
|
|
810
|
-
onLog: (line) => log6.info(line)
|
|
811
|
-
});
|
|
812
|
-
log6.success(
|
|
813
|
-
`checkpoint ${info.name} (${info.manifest.type}) -> ${info.dir}` + (opts.setDefault ? " [project default]" : "")
|
|
1623
|
+
const noticeId = await setRelayNotice(
|
|
1624
|
+
box.id,
|
|
1625
|
+
"checkpoint",
|
|
1626
|
+
CHECKPOINT_NOTICE,
|
|
1627
|
+
CHECKPOINT_NOTICE_TTL_MS
|
|
814
1628
|
);
|
|
815
|
-
|
|
816
|
-
|
|
1629
|
+
let signalled = false;
|
|
1630
|
+
const onSignal = () => {
|
|
1631
|
+
if (signalled) return;
|
|
1632
|
+
signalled = true;
|
|
1633
|
+
void (async () => {
|
|
1634
|
+
if (noticeId) await clearRelayNotice(box.id, noticeId);
|
|
1635
|
+
process.exit(130);
|
|
1636
|
+
})();
|
|
1637
|
+
};
|
|
1638
|
+
if (noticeId) {
|
|
1639
|
+
process.once("SIGINT", onSignal);
|
|
1640
|
+
process.once("SIGTERM", onSignal);
|
|
1641
|
+
}
|
|
1642
|
+
try {
|
|
1643
|
+
const info = await createCheckpoint({
|
|
1644
|
+
box,
|
|
1645
|
+
projectRoot,
|
|
1646
|
+
name: opts.name,
|
|
1647
|
+
merged: opts.merged === true,
|
|
1648
|
+
setDefault: opts.setDefault === true,
|
|
1649
|
+
replace: opts.replace === true,
|
|
1650
|
+
maxLayers: cfg.effective.checkpoint.maxLayers,
|
|
1651
|
+
onLog: (line) => log6.info(line)
|
|
1652
|
+
});
|
|
1653
|
+
log6.success(
|
|
1654
|
+
`checkpoint ${info.name} (${info.manifest.type}) -> ${info.dir}` + (opts.setDefault ? " [project default]" : "")
|
|
1655
|
+
);
|
|
1656
|
+
if (!opts.setDefault) {
|
|
1657
|
+
log6.info(
|
|
1658
|
+
`make it the default for new boxes: agentbox checkpoint set-default ${info.name}`
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
} finally {
|
|
1662
|
+
if (noticeId) {
|
|
1663
|
+
await clearRelayNotice(box.id, noticeId);
|
|
1664
|
+
process.removeListener("SIGINT", onSignal);
|
|
1665
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1666
|
+
}
|
|
817
1667
|
}
|
|
818
1668
|
} catch (err) {
|
|
819
1669
|
handleLifecycleError(err);
|
|
@@ -897,7 +1747,7 @@ var rmSub = new Command3("rm").description("Delete a checkpoint").argument("<ref
|
|
|
897
1747
|
handleLifecycleError(err);
|
|
898
1748
|
}
|
|
899
1749
|
});
|
|
900
|
-
var checkpointCommand = new Command3("checkpoint").description("
|
|
1750
|
+
var checkpointCommand = new Command3("checkpoint").alias("checkpoints").description("List and manage project checkpoints (warm box state new boxes can start from)").addCommand(createSub).addCommand(lsSub, { isDefault: true }).addCommand(setDefaultSub).addCommand(rmSub);
|
|
901
1751
|
|
|
902
1752
|
// src/commands/code.ts
|
|
903
1753
|
import { spawn } from "child_process";
|
|
@@ -944,7 +1794,7 @@ var codeCommand = new Command4("code").description("Open a box in VS Code or Cur
|
|
|
944
1794
|
log7.info(`box is paused; unpausing`);
|
|
945
1795
|
await unpauseBox(box.id);
|
|
946
1796
|
} else if (insp.state === "stopped") {
|
|
947
|
-
log7.info(`box is stopped; starting
|
|
1797
|
+
log7.info(`box is stopped; starting`);
|
|
948
1798
|
await startBox(box.id);
|
|
949
1799
|
} else if (insp.state === "missing") {
|
|
950
1800
|
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
@@ -1040,10 +1890,10 @@ async function launchOne(flavor, folderUri) {
|
|
|
1040
1890
|
return { code: fallback, flavor, via: "open" };
|
|
1041
1891
|
}
|
|
1042
1892
|
function spawnCommand(cmd, args) {
|
|
1043
|
-
return new Promise((
|
|
1893
|
+
return new Promise((resolve2) => {
|
|
1044
1894
|
const child = spawn(cmd, args, { stdio: "ignore" });
|
|
1045
|
-
child.once("error", () =>
|
|
1046
|
-
child.once("exit", (code) =>
|
|
1895
|
+
child.once("error", () => resolve2(127));
|
|
1896
|
+
child.once("exit", (code) => resolve2(code ?? -1));
|
|
1047
1897
|
});
|
|
1048
1898
|
}
|
|
1049
1899
|
async function fetchServiceNames(container) {
|
|
@@ -1121,9 +1971,9 @@ var getCommand = new Command5("get").description("Print the effective value of a
|
|
|
1121
1971
|
const value = leafValue(loaded, key);
|
|
1122
1972
|
const source = loaded.sources[key] ?? "default";
|
|
1123
1973
|
if (opts.json) {
|
|
1124
|
-
const layerView = (values,
|
|
1974
|
+
const layerView = (values, path2) => ({
|
|
1125
1975
|
value: rawLeafFromValues(values, key) ?? null,
|
|
1126
|
-
path
|
|
1976
|
+
path: path2
|
|
1127
1977
|
});
|
|
1128
1978
|
process.stdout.write(
|
|
1129
1979
|
JSON.stringify(
|
|
@@ -1272,9 +2122,9 @@ function pickFromScope(loaded, scope, key) {
|
|
|
1272
2122
|
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) => {
|
|
1273
2123
|
try {
|
|
1274
2124
|
const scope = resolveEditScope(opts);
|
|
1275
|
-
const
|
|
1276
|
-
if (opts.json) process.stdout.write(JSON.stringify({ scope, path }, null, 2) + "\n");
|
|
1277
|
-
else process.stdout.write(`${
|
|
2125
|
+
const path2 = await configPathFor(scope, process.cwd());
|
|
2126
|
+
if (opts.json) process.stdout.write(JSON.stringify({ scope, path: path2 }, null, 2) + "\n");
|
|
2127
|
+
else process.stdout.write(`${path2}
|
|
1278
2128
|
`);
|
|
1279
2129
|
} catch (err) {
|
|
1280
2130
|
handleError(err);
|
|
@@ -1283,9 +2133,9 @@ var pathCommand = new Command5("path").description("Print the file path for a co
|
|
|
1283
2133
|
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) => {
|
|
1284
2134
|
try {
|
|
1285
2135
|
const scope = resolveEditScope(opts);
|
|
1286
|
-
const
|
|
2136
|
+
const path2 = await configPathFor(scope, process.cwd());
|
|
1287
2137
|
const editor = process.env["EDITOR"] || process.env["VISUAL"] || "vi";
|
|
1288
|
-
const child = spawnSync3(editor, [
|
|
2138
|
+
const child = spawnSync3(editor, [path2], { stdio: "inherit" });
|
|
1289
2139
|
process.exit(child.status ?? 0);
|
|
1290
2140
|
} catch (err) {
|
|
1291
2141
|
handleError(err);
|
|
@@ -1309,25 +2159,236 @@ var listProjectsCommand = new Command5("list-projects").description("List direct
|
|
|
1309
2159
|
);
|
|
1310
2160
|
}
|
|
1311
2161
|
} catch (err) {
|
|
1312
|
-
handleError(err);
|
|
2162
|
+
handleError(err);
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
function handleError(err) {
|
|
2166
|
+
if (err instanceof UserConfigError) {
|
|
2167
|
+
process.stderr.write(`error: ${err.message}
|
|
2168
|
+
`);
|
|
2169
|
+
process.exit(2);
|
|
2170
|
+
}
|
|
2171
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2172
|
+
process.stderr.write(`error: ${msg}
|
|
2173
|
+
`);
|
|
2174
|
+
process.exit(1);
|
|
2175
|
+
}
|
|
2176
|
+
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);
|
|
2177
|
+
|
|
2178
|
+
// src/commands/cp.ts
|
|
2179
|
+
import { existsSync, mkdirSync, renameSync, statSync } from "fs";
|
|
2180
|
+
import * as path from "path";
|
|
2181
|
+
import { log as log8 } from "@clack/prompts";
|
|
2182
|
+
import { Command as Command6 } from "commander";
|
|
2183
|
+
import { execa } from "execa";
|
|
2184
|
+
function parseBoxArg(arg) {
|
|
2185
|
+
const idx = arg.indexOf(":");
|
|
2186
|
+
if (idx === -1) return null;
|
|
2187
|
+
const prefix = arg.slice(0, idx);
|
|
2188
|
+
if (prefix.includes("/")) return null;
|
|
2189
|
+
if (prefix.length === 0) return null;
|
|
2190
|
+
const p = arg.slice(idx + 1);
|
|
2191
|
+
if (p.length === 0) return null;
|
|
2192
|
+
return { boxRef: prefix, path: p };
|
|
2193
|
+
}
|
|
2194
|
+
function parseArgs(src, dst) {
|
|
2195
|
+
const srcBox = parseBoxArg(src);
|
|
2196
|
+
const dstBox = dst === void 0 ? null : parseBoxArg(dst);
|
|
2197
|
+
if (srcBox && dstBox) {
|
|
2198
|
+
throw new Error(
|
|
2199
|
+
"box-to-box copy is not supported; both arguments look like box paths (`name:/path`)."
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
if (!srcBox && !dstBox) {
|
|
2203
|
+
throw new Error(
|
|
2204
|
+
"one argument must be a box path of the form `<box>:/path` (e.g. `mybox:/workspace/foo`)."
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
if (srcBox) {
|
|
2208
|
+
return {
|
|
2209
|
+
direction: "download",
|
|
2210
|
+
boxRef: srcBox.boxRef,
|
|
2211
|
+
boxPath: srcBox.path,
|
|
2212
|
+
hostPath: dst
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
if (dst === void 0) {
|
|
2216
|
+
throw new Error("host -> box copy requires a destination, e.g. `agentbox cp ./foo box:/dst`.");
|
|
2217
|
+
}
|
|
2218
|
+
return {
|
|
2219
|
+
direction: "upload",
|
|
2220
|
+
boxRef: dstBox.boxRef,
|
|
2221
|
+
boxPath: dstBox.path,
|
|
2222
|
+
hostPath: src
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
function posixDirname(p) {
|
|
2226
|
+
return path.posix.dirname(p) || "/";
|
|
2227
|
+
}
|
|
2228
|
+
function asText(s) {
|
|
2229
|
+
if (s === void 0) return "";
|
|
2230
|
+
if (typeof s === "string") return s;
|
|
2231
|
+
return Buffer.from(s).toString("utf8");
|
|
2232
|
+
}
|
|
2233
|
+
async function uploadToBox(box, hostSrc, boxDst) {
|
|
2234
|
+
const srcAbs = path.resolve(hostSrc);
|
|
2235
|
+
if (!existsSync(srcAbs)) throw new Error(`source not found: ${hostSrc}`);
|
|
2236
|
+
const srcBasename = path.basename(srcAbs);
|
|
2237
|
+
const srcParent = path.dirname(srcAbs);
|
|
2238
|
+
let boxParent;
|
|
2239
|
+
let finalName;
|
|
2240
|
+
if (boxDst.endsWith("/")) {
|
|
2241
|
+
boxParent = boxDst.replace(/\/+$/, "") || "/";
|
|
2242
|
+
finalName = srcBasename;
|
|
2243
|
+
} else {
|
|
2244
|
+
const isDir = await execa(
|
|
2245
|
+
"docker",
|
|
2246
|
+
["exec", box.container, "test", "-d", boxDst],
|
|
2247
|
+
{ reject: false }
|
|
2248
|
+
);
|
|
2249
|
+
if (isDir.exitCode === 0) {
|
|
2250
|
+
boxParent = boxDst.replace(/\/+$/, "") || "/";
|
|
2251
|
+
finalName = srcBasename;
|
|
2252
|
+
} else {
|
|
2253
|
+
boxParent = posixDirname(boxDst);
|
|
2254
|
+
finalName = path.posix.basename(boxDst);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
const finalPath = boxParent === "/" ? `/${finalName}` : `${boxParent}/${finalName}`;
|
|
2258
|
+
const mk = await execa(
|
|
2259
|
+
"docker",
|
|
2260
|
+
["exec", "--user", "root", box.container, "mkdir", "-p", boxParent],
|
|
2261
|
+
{ reject: false }
|
|
2262
|
+
);
|
|
2263
|
+
if (mk.exitCode !== 0) {
|
|
2264
|
+
throw new Error(`mkdir -p ${boxParent} in box failed: ${asText(mk.stderr).slice(0, 300)}`);
|
|
2265
|
+
}
|
|
2266
|
+
const packed = await execa("tar", ["-C", srcParent, "-cf", "-", srcBasename], {
|
|
2267
|
+
encoding: "buffer",
|
|
2268
|
+
reject: false,
|
|
2269
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" }
|
|
2270
|
+
});
|
|
2271
|
+
if (packed.exitCode !== 0) {
|
|
2272
|
+
throw new Error(`tar pack failed: ${asText(packed.stderr).slice(0, 300)}`);
|
|
2273
|
+
}
|
|
2274
|
+
const extract = await execa(
|
|
2275
|
+
"docker",
|
|
2276
|
+
["exec", "-i", "--user", "root", box.container, "tar", "-xf", "-", "-C", boxParent],
|
|
2277
|
+
{ input: packed.stdout, reject: false }
|
|
2278
|
+
);
|
|
2279
|
+
if (extract.exitCode !== 0) {
|
|
2280
|
+
throw new Error(`tar extract in box failed: ${asText(extract.stderr).slice(0, 300)}`);
|
|
2281
|
+
}
|
|
2282
|
+
if (finalName !== srcBasename) {
|
|
2283
|
+
const initial = boxParent === "/" ? `/${srcBasename}` : `${boxParent}/${srcBasename}`;
|
|
2284
|
+
const mv = await execa(
|
|
2285
|
+
"docker",
|
|
2286
|
+
["exec", "--user", "root", box.container, "mv", initial, finalPath],
|
|
2287
|
+
{ reject: false }
|
|
2288
|
+
);
|
|
2289
|
+
if (mv.exitCode !== 0) {
|
|
2290
|
+
throw new Error(
|
|
2291
|
+
`rename ${initial} -> ${finalPath} in box failed: ${asText(mv.stderr).slice(0, 300)}`
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const chown = await execa(
|
|
2296
|
+
"docker",
|
|
2297
|
+
["exec", "--user", "root", box.container, "chown", "-R", "1000:1000", finalPath],
|
|
2298
|
+
{ reject: false }
|
|
2299
|
+
);
|
|
2300
|
+
if (chown.exitCode !== 0) {
|
|
2301
|
+
return {
|
|
2302
|
+
finalPath,
|
|
2303
|
+
warn: `chown ${finalPath} to vscode (uid 1000) failed; ownership inside the box may be root.`
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
return { finalPath, warn: null };
|
|
2307
|
+
}
|
|
2308
|
+
async function downloadFromBox(box, boxSrc, hostDst) {
|
|
2309
|
+
const srcBasename = path.posix.basename(boxSrc);
|
|
2310
|
+
const srcParent = posixDirname(boxSrc);
|
|
2311
|
+
const dstAbs = path.resolve(hostDst);
|
|
2312
|
+
let hostParent;
|
|
2313
|
+
let finalName;
|
|
2314
|
+
const dstExists = existsSync(dstAbs);
|
|
2315
|
+
if (hostDst.endsWith("/") || dstExists && statSync(dstAbs).isDirectory()) {
|
|
2316
|
+
hostParent = dstAbs;
|
|
2317
|
+
finalName = srcBasename;
|
|
2318
|
+
} else {
|
|
2319
|
+
hostParent = path.dirname(dstAbs);
|
|
2320
|
+
finalName = path.basename(dstAbs);
|
|
2321
|
+
}
|
|
2322
|
+
mkdirSync(hostParent, { recursive: true });
|
|
2323
|
+
const finalPath = path.join(hostParent, finalName);
|
|
2324
|
+
const packed = await execa(
|
|
2325
|
+
"docker",
|
|
2326
|
+
["exec", box.container, "tar", "-C", srcParent, "-cf", "-", srcBasename],
|
|
2327
|
+
{ encoding: "buffer", reject: false }
|
|
2328
|
+
);
|
|
2329
|
+
if (packed.exitCode !== 0) {
|
|
2330
|
+
throw new Error(`tar pack in box failed: ${asText(packed.stderr).slice(0, 300)}`);
|
|
2331
|
+
}
|
|
2332
|
+
const extract = await execa("tar", ["-xf", "-", "-C", hostParent], {
|
|
2333
|
+
input: packed.stdout,
|
|
2334
|
+
reject: false
|
|
2335
|
+
});
|
|
2336
|
+
if (extract.exitCode !== 0) {
|
|
2337
|
+
throw new Error(`tar extract on host failed: ${asText(extract.stderr).slice(0, 300)}`);
|
|
2338
|
+
}
|
|
2339
|
+
if (finalName !== srcBasename) {
|
|
2340
|
+
renameSync(path.join(hostParent, srcBasename), finalPath);
|
|
2341
|
+
}
|
|
2342
|
+
return { finalPath };
|
|
2343
|
+
}
|
|
2344
|
+
var cpCommand = new Command6("cp").description("Copy files between host and box (like `docker cp`; direction picked by `name:` prefix)").argument("<src>", "`box:/path` (download) or host path (upload)").argument(
|
|
2345
|
+
"[dst]",
|
|
2346
|
+
"`box:/path` (upload) or host path (download); defaults to cwd when downloading"
|
|
2347
|
+
).addHelpText(
|
|
2348
|
+
"after",
|
|
2349
|
+
[
|
|
2350
|
+
"",
|
|
2351
|
+
"Examples:",
|
|
2352
|
+
" agentbox cp mybox:/etc/foo ./foo # download (host path optional)",
|
|
2353
|
+
" agentbox cp mybox:/workspace/.env # download into cwd",
|
|
2354
|
+
" agentbox cp ./local.txt mybox:/workspace/ # upload (host path required)",
|
|
2355
|
+
" agentbox cp ./dir mybox:/workspace/ # upload directory (recursive)"
|
|
2356
|
+
].join("\n")
|
|
2357
|
+
).action(async (src, dst) => {
|
|
2358
|
+
try {
|
|
2359
|
+
const parsed = parseArgs(src, dst);
|
|
2360
|
+
const box = await resolveBoxOrExit(parsed.boxRef);
|
|
2361
|
+
const insp = await inspectBox(box.id);
|
|
2362
|
+
if (insp.state === "paused") {
|
|
2363
|
+
log8.info("box is paused; unpausing");
|
|
2364
|
+
await unpauseBox(box.id);
|
|
2365
|
+
} else if (insp.state === "stopped") {
|
|
2366
|
+
log8.info("box is stopped; starting");
|
|
2367
|
+
await startBox(box.id);
|
|
2368
|
+
} else if (insp.state === "missing") {
|
|
2369
|
+
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
2370
|
+
}
|
|
2371
|
+
if (parsed.direction === "upload") {
|
|
2372
|
+
const result = await uploadToBox(box, parsed.hostPath, parsed.boxPath);
|
|
2373
|
+
if (result.warn) {
|
|
2374
|
+
log8.warn(`copied to ${box.name}:${result.finalPath}, but ${result.warn}`);
|
|
2375
|
+
} else {
|
|
2376
|
+
process.stdout.write(`copied to ${box.name}:${result.finalPath}
|
|
2377
|
+
`);
|
|
2378
|
+
}
|
|
2379
|
+
} else {
|
|
2380
|
+
const result = await downloadFromBox(box, parsed.boxPath, parsed.hostPath ?? process.cwd());
|
|
2381
|
+
process.stdout.write(`copied to ${result.finalPath}
|
|
2382
|
+
`);
|
|
2383
|
+
}
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
handleLifecycleError(err);
|
|
1313
2386
|
}
|
|
1314
2387
|
});
|
|
1315
|
-
function handleError(err) {
|
|
1316
|
-
if (err instanceof UserConfigError) {
|
|
1317
|
-
process.stderr.write(`error: ${err.message}
|
|
1318
|
-
`);
|
|
1319
|
-
process.exit(2);
|
|
1320
|
-
}
|
|
1321
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1322
|
-
process.stderr.write(`error: ${msg}
|
|
1323
|
-
`);
|
|
1324
|
-
process.exit(1);
|
|
1325
|
-
}
|
|
1326
|
-
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);
|
|
1327
2388
|
|
|
1328
2389
|
// src/commands/create.ts
|
|
1329
|
-
import { intro as intro2, log as
|
|
1330
|
-
import { Command as
|
|
2390
|
+
import { intro as intro2, log as log9, outro as outro2, spinner as spinner2 } from "@clack/prompts";
|
|
2391
|
+
import { Command as Command7 } from "commander";
|
|
1331
2392
|
import { execSync, spawnSync as spawnSync4 } from "child_process";
|
|
1332
2393
|
function buildCliOverrides(opts) {
|
|
1333
2394
|
const box = {};
|
|
@@ -1342,19 +2403,31 @@ function buildCliOverrides(opts) {
|
|
|
1342
2403
|
function resolveUseSnapshot(opts, configDefault) {
|
|
1343
2404
|
if (opts.hostSnapshot === false) return false;
|
|
1344
2405
|
if (opts.hostSnapshot === true) return true;
|
|
1345
|
-
return configDefault ??
|
|
2406
|
+
return configDefault ?? false;
|
|
1346
2407
|
}
|
|
1347
2408
|
function resolveCheckpointRef(opts, configDefault) {
|
|
1348
2409
|
if (opts.snapshot && opts.snapshot.length > 0) return opts.snapshot;
|
|
1349
2410
|
return configDefault.length > 0 ? configDefault : void 0;
|
|
1350
2411
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
2412
|
+
var RELAY_HOST_URL2 = `http://127.0.0.1:${String(DEFAULT_RELAY_PORT)}`;
|
|
2413
|
+
async function attachShell(record) {
|
|
2414
|
+
const dockerArgv = ["exec", "-it", record.container, "bash"];
|
|
2415
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
2416
|
+
const child = spawnSync4("docker", dockerArgv, { stdio: "inherit" });
|
|
2417
|
+
process.exit(child.status ?? 0);
|
|
2418
|
+
}
|
|
2419
|
+
const code = await runWrappedAttach({
|
|
2420
|
+
container: record.container,
|
|
2421
|
+
dockerArgv,
|
|
2422
|
+
relayBaseUrl: RELAY_HOST_URL2,
|
|
2423
|
+
boxId: record.id,
|
|
2424
|
+
boxName: record.name,
|
|
2425
|
+
projectIndex: record.projectIndex,
|
|
2426
|
+
mode: "shell"
|
|
1354
2427
|
});
|
|
1355
|
-
process.exit(
|
|
2428
|
+
process.exit(code);
|
|
1356
2429
|
}
|
|
1357
|
-
var createCommand = new
|
|
2430
|
+
var createCommand = new Command7("create").description("Create and start a new agent box (Docker container with /workspace seeded via in-container git worktree)").option("-w, --workspace <path>", "host workspace to mount", process.cwd()).option("-n, --name <name>", "friendly box name (default: <workspace-basename>-<id>)").option("--host-snapshot", "APFS-clone the host workspace into a per-box scratch dir before seeding /workspace (stabilizes the tar-pipe source)").option("--no-host-snapshot", "bind the live workspace directly (host edits leak into reads)").option(
|
|
1358
2431
|
"--snapshot <ref>",
|
|
1359
2432
|
"start from a project checkpoint (see `agentbox checkpoint`); overrides box.defaultCheckpoint"
|
|
1360
2433
|
).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(
|
|
@@ -1363,8 +2436,8 @@ var createCommand = new Command6("create").description("Create and start a new a
|
|
|
1363
2436
|
).option("--no-vnc", "disable the per-box Xvnc + noVNC web client (on by default)").option(
|
|
1364
2437
|
"--shared-docker-cache",
|
|
1365
2438
|
"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)"
|
|
1366
|
-
).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
|
|
1367
|
-
intro2("
|
|
2439
|
+
).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 container writable-layer size (e.g. 10g); no-op on overlay2/macOS").option("-y, --yes", "skip prompts, accept defaults").action(async (opts) => {
|
|
2440
|
+
intro2("Setting up a new box...");
|
|
1368
2441
|
const cfg = await loadEffectiveConfig(opts.workspace, {
|
|
1369
2442
|
cliOverrides: buildCliOverrides(opts)
|
|
1370
2443
|
});
|
|
@@ -1374,14 +2447,18 @@ var createCommand = new Command6("create").description("Create and start a new a
|
|
|
1374
2447
|
workspace: opts.workspace,
|
|
1375
2448
|
yes: !!opts.yes,
|
|
1376
2449
|
command: "create",
|
|
1377
|
-
checkpointRef
|
|
2450
|
+
checkpointRef,
|
|
2451
|
+
withEnv: cfg.effective.box.withEnv
|
|
1378
2452
|
});
|
|
1379
2453
|
if (wiz.action === "switch-to-claude") {
|
|
1380
2454
|
process.env[WIZARD_AUTOLAUNCH_ENV] = "1";
|
|
2455
|
+
const serialized = serializeEnvFilesForEnv(wiz.envFilesToImport);
|
|
2456
|
+
if (serialized !== void 0) process.env[WIZARD_ENV_FILES_ENV] = serialized;
|
|
1381
2457
|
try {
|
|
1382
2458
|
await claudeCommand.parseAsync(passthroughFlags(opts), { from: "user" });
|
|
1383
2459
|
} finally {
|
|
1384
2460
|
delete process.env[WIZARD_AUTOLAUNCH_ENV];
|
|
2461
|
+
delete process.env[WIZARD_ENV_FILES_ENV];
|
|
1385
2462
|
}
|
|
1386
2463
|
return;
|
|
1387
2464
|
}
|
|
@@ -1398,6 +2475,7 @@ var createCommand = new Command6("create").description("Create and start a new a
|
|
|
1398
2475
|
image: cfg.effective.box.image,
|
|
1399
2476
|
withPlaywright,
|
|
1400
2477
|
withEnv: cfg.effective.box.withEnv,
|
|
2478
|
+
envFilesToImport: wiz.envFilesToImport,
|
|
1401
2479
|
vnc: { enabled: cfg.effective.box.vnc },
|
|
1402
2480
|
docker: { sharedCache: cfg.effective.box.dockerCacheShared },
|
|
1403
2481
|
limits: resolveLimits(cfg.effective.box, opts),
|
|
@@ -1405,26 +2483,21 @@ var createCommand = new Command6("create").description("Create and start a new a
|
|
|
1405
2483
|
onLog: (line) => s.message(clampSpinnerLine(line))
|
|
1406
2484
|
});
|
|
1407
2485
|
s.stop(`box ${result.record.container} ready`);
|
|
1408
|
-
|
|
2486
|
+
log9.info(`id: ${result.record.id}`);
|
|
1409
2487
|
if (typeof result.record.projectIndex === "number") {
|
|
1410
|
-
|
|
2488
|
+
log9.info(`n: ${String(result.record.projectIndex)} (in ${projectRoot})`);
|
|
1411
2489
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
log8.info(`lower: ${result.record.lowerPath}`);
|
|
1415
|
-
log8.info(`upper: ${result.record.upperVolume}`);
|
|
2490
|
+
log9.info(`container: ${result.record.container}`);
|
|
2491
|
+
log9.info(`image: ${result.record.image}${result.imageBuilt ? " (built just now)" : ""}`);
|
|
1416
2492
|
if (result.record.snapshotDir) {
|
|
1417
|
-
|
|
2493
|
+
log9.info(`snapshot: ${result.record.snapshotDir}`);
|
|
1418
2494
|
}
|
|
1419
2495
|
if (result.record.checkpointSource) {
|
|
1420
|
-
|
|
1421
|
-
`checkpoint: ${result.record.checkpointSource.ref} (${result.record.checkpointSource.type})`
|
|
2496
|
+
log9.info(
|
|
2497
|
+
`checkpoint: ${result.record.checkpointSource.ref} (${result.record.checkpointSource.type}) \u2192 ${result.record.checkpointImage ?? "(missing)"}`
|
|
1422
2498
|
);
|
|
1423
2499
|
}
|
|
1424
|
-
|
|
1425
|
-
log8.success(`${check.name} \u2014 ${check.detail}`);
|
|
1426
|
-
}
|
|
1427
|
-
log8.message(
|
|
2500
|
+
log9.message(
|
|
1428
2501
|
[
|
|
1429
2502
|
"",
|
|
1430
2503
|
"Try it:",
|
|
@@ -1432,8 +2505,7 @@ var createCommand = new Command6("create").description("Create and start a new a
|
|
|
1432
2505
|
` docker exec ${result.record.container} ls /workspace`,
|
|
1433
2506
|
"",
|
|
1434
2507
|
"Destroy:",
|
|
1435
|
-
`
|
|
1436
|
-
` docker volume rm ${result.record.upperVolume}`
|
|
2508
|
+
` agentbox destroy ${result.record.name}`
|
|
1437
2509
|
].join("\n")
|
|
1438
2510
|
);
|
|
1439
2511
|
const m = cfg.effective.maintenance;
|
|
@@ -1445,7 +2517,7 @@ var createCommand = new Command6("create").description("Create and start a new a
|
|
|
1445
2517
|
const protectedPaths = boxes.map((b) => b.projectRoot).filter((p) => typeof p === "string");
|
|
1446
2518
|
const res = await pruneOrphanProjectConfigs({ protectedPaths });
|
|
1447
2519
|
if (res.removed.length > 0) {
|
|
1448
|
-
|
|
2520
|
+
log9.info(
|
|
1449
2521
|
`cleaned ${String(res.removed.length)} orphan project config dir(s): ` + res.removed.map((r) => r.originalPath).join(", ")
|
|
1450
2522
|
);
|
|
1451
2523
|
}
|
|
@@ -1455,19 +2527,19 @@ var createCommand = new Command6("create").description("Create and start a new a
|
|
|
1455
2527
|
}
|
|
1456
2528
|
outro2("done");
|
|
1457
2529
|
if (opts.attach) {
|
|
1458
|
-
attachShell(result.record
|
|
2530
|
+
await attachShell(result.record);
|
|
1459
2531
|
}
|
|
1460
2532
|
} catch (err) {
|
|
1461
2533
|
s.stop("failed");
|
|
1462
2534
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1463
|
-
|
|
2535
|
+
log9.error(msg);
|
|
1464
2536
|
try {
|
|
1465
2537
|
const running = execSync('docker ps --format "{{.Names}}"', {
|
|
1466
2538
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1467
2539
|
}).toString().split("\n").filter((n) => n.startsWith("agentbox-"));
|
|
1468
2540
|
if (running.length > 0) {
|
|
1469
|
-
|
|
1470
|
-
|
|
2541
|
+
log9.warn(`leftover containers: ${running.join(", ")}`);
|
|
2542
|
+
log9.warn(`remove with: docker rm -f ${running.join(" ")}`);
|
|
1471
2543
|
}
|
|
1472
2544
|
} catch {
|
|
1473
2545
|
}
|
|
@@ -1477,8 +2549,8 @@ var createCommand = new Command6("create").description("Create and start a new a
|
|
|
1477
2549
|
|
|
1478
2550
|
// src/commands/dashboard.ts
|
|
1479
2551
|
import { spawn as spawn2 } from "child_process";
|
|
1480
|
-
import { log as
|
|
1481
|
-
import { Command as
|
|
2552
|
+
import { log as log10 } from "@clack/prompts";
|
|
2553
|
+
import { Command as Command8 } from "commander";
|
|
1482
2554
|
|
|
1483
2555
|
// src/dashboard/layout.ts
|
|
1484
2556
|
var SIDEBAR_WIDTH = 32;
|
|
@@ -1503,7 +2575,7 @@ function computeLayout(cols, rows) {
|
|
|
1503
2575
|
}
|
|
1504
2576
|
|
|
1505
2577
|
// src/dashboard/renderer.ts
|
|
1506
|
-
var
|
|
2578
|
+
var RESET2 = "\x1B[0m";
|
|
1507
2579
|
function fgParams(c) {
|
|
1508
2580
|
if (c.kind === "default") return "39";
|
|
1509
2581
|
if (c.kind === "palette") {
|
|
@@ -1548,7 +2620,7 @@ function composeRow(snap, y) {
|
|
|
1548
2620
|
}
|
|
1549
2621
|
out += cell.chars === "" ? " " : cell.chars;
|
|
1550
2622
|
}
|
|
1551
|
-
return out +
|
|
2623
|
+
return out + RESET2;
|
|
1552
2624
|
}
|
|
1553
2625
|
function cursorTo(row0, col0) {
|
|
1554
2626
|
return `\x1B[${String(row0 + 1)};${String(col0 + 1)}H`;
|
|
@@ -1561,7 +2633,7 @@ function diffFrame(prev, snap, rect) {
|
|
|
1561
2633
|
const payload = composeRow(snap, i);
|
|
1562
2634
|
rows[i] = payload;
|
|
1563
2635
|
if (prev && prev[i] === payload) continue;
|
|
1564
|
-
out += cursorTo(rect.y + i, rect.x) +
|
|
2636
|
+
out += cursorTo(rect.y + i, rect.x) + RESET2 + payload + RESET2;
|
|
1565
2637
|
}
|
|
1566
2638
|
if (snap.cursor.visible) {
|
|
1567
2639
|
const cy = Math.min(Math.max(snap.cursor.y, 0), h - 1);
|
|
@@ -1868,228 +2940,20 @@ var PtySession = class {
|
|
|
1868
2940
|
}
|
|
1869
2941
|
};
|
|
1870
2942
|
|
|
1871
|
-
// src/dashboard/sidebar.ts
|
|
1872
|
-
function ellipsize(s, max) {
|
|
1873
|
-
if (max <= 0) return "";
|
|
1874
|
-
if (s.length <= max) return s;
|
|
1875
|
-
if (max === 1) return "\u2026";
|
|
1876
|
-
return s.slice(0, max - 1) + "\u2026";
|
|
1877
|
-
}
|
|
1878
|
-
function ellipsizeHead(s, max) {
|
|
1879
|
-
if (max <= 0) return "";
|
|
1880
|
-
if (s.length <= max) return s;
|
|
1881
|
-
if (max === 1) return "\u2026";
|
|
1882
|
-
return "\u2026" + s.slice(s.length - (max - 1));
|
|
1883
|
-
}
|
|
1884
|
-
function activityCell(b) {
|
|
1885
|
-
if (b.state !== "running") return `[${b.state}]`;
|
|
1886
|
-
switch (b.claudeActivity) {
|
|
1887
|
-
case "working":
|
|
1888
|
-
return "\u25CF working";
|
|
1889
|
-
case "idle":
|
|
1890
|
-
return "\u25CB idle";
|
|
1891
|
-
case "waiting":
|
|
1892
|
-
return "\u25D0 waiting";
|
|
1893
|
-
default:
|
|
1894
|
-
return "? unknown";
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
var NEW_BOX_ID = "__agentbox_new__";
|
|
1898
|
-
var NEW_BOX_LABEL = "+ New box";
|
|
1899
|
-
var SIDEBAR_HEADER = "AgentBox";
|
|
1900
|
-
function topBorder(label, w) {
|
|
1901
|
-
const lead = `\u256D\u2500\u2500\u2500 ${label} `;
|
|
1902
|
-
if (lead.length >= w) return lead.slice(0, w);
|
|
1903
|
-
return lead + "\u2500".repeat(w - lead.length);
|
|
1904
|
-
}
|
|
1905
|
-
function fit(s, w) {
|
|
1906
|
-
if (s.length === w) return s;
|
|
1907
|
-
if (s.length > w) return s.slice(0, w);
|
|
1908
|
-
return s + " ".repeat(w - s.length);
|
|
1909
|
-
}
|
|
1910
|
-
function center(s, w) {
|
|
1911
|
-
if (s.length >= w) return s.slice(0, w);
|
|
1912
|
-
const pad = w - s.length;
|
|
1913
|
-
const leftPad = Math.floor(pad / 2);
|
|
1914
|
-
return " ".repeat(leftPad) + s + " ".repeat(pad - leftPad);
|
|
1915
|
-
}
|
|
1916
|
-
function projectLabel(project) {
|
|
1917
|
-
if (!project) return "(no project)";
|
|
1918
|
-
const parts = project.split("/").filter(Boolean);
|
|
1919
|
-
return parts[parts.length - 1] ?? project;
|
|
1920
|
-
}
|
|
1921
|
-
function stripTitleGlyph(s) {
|
|
1922
|
-
const t = s.replace(/^[\s\p{S}*·]+/u, "");
|
|
1923
|
-
return t.length > 0 ? t : s.trim();
|
|
1924
|
-
}
|
|
1925
|
-
function boxRow(b, marker, w) {
|
|
1926
|
-
const numStr = b.index != null ? `${b.index} ` : "";
|
|
1927
|
-
const status = activityCell(b);
|
|
1928
|
-
const left = `${marker}${numStr}`;
|
|
1929
|
-
const room = w - left.length - status.length - 1;
|
|
1930
|
-
if (room <= 0) return fit(`${left}${status}`, w);
|
|
1931
|
-
const middle = b.state === "running" && b.sessionTitle ? ellipsize(stripTitleGlyph(b.sessionTitle), room) : ellipsizeHead(b.name, room);
|
|
1932
|
-
return fit(`${left}${middle}`, w - status.length) + status;
|
|
1933
|
-
}
|
|
1934
|
-
function sidebarLines(boxes, selectedId, w, h) {
|
|
1935
|
-
const lines = [topBorder(SIDEBAR_HEADER, w), fit("", w)];
|
|
1936
|
-
const rowOwner = [null, null];
|
|
1937
|
-
const headerRows = [true, false];
|
|
1938
|
-
const push = (line, owner, header) => {
|
|
1939
|
-
lines.push(fit(line, w));
|
|
1940
|
-
rowOwner.push(owner);
|
|
1941
|
-
headerRows.push(header);
|
|
1942
|
-
};
|
|
1943
|
-
let prevProject;
|
|
1944
|
-
let seenGroup = false;
|
|
1945
|
-
for (const b of boxes) {
|
|
1946
|
-
const marker = b.id === selectedId ? "\u25B8" : " ";
|
|
1947
|
-
if (b.id === NEW_BOX_ID) {
|
|
1948
|
-
push(`${marker}${NEW_BOX_LABEL}`, b.id, false);
|
|
1949
|
-
continue;
|
|
1950
|
-
}
|
|
1951
|
-
if (!seenGroup || b.project !== prevProject) {
|
|
1952
|
-
push(center(` \u2500\u2500 ${projectLabel(b.project)} \u2500\u2500 `, w), null, true);
|
|
1953
|
-
prevProject = b.project;
|
|
1954
|
-
seenGroup = true;
|
|
1955
|
-
}
|
|
1956
|
-
push(boxRow(b, marker, w), b.id, false);
|
|
1957
|
-
}
|
|
1958
|
-
if (boxes.length === 0) push(" (no boxes)", null, false);
|
|
1959
|
-
while (lines.length < h) push("", null, false);
|
|
1960
|
-
return {
|
|
1961
|
-
lines: lines.slice(0, h),
|
|
1962
|
-
rowOwner: rowOwner.slice(0, h),
|
|
1963
|
-
headerRows: headerRows.slice(0, h)
|
|
1964
|
-
};
|
|
1965
|
-
}
|
|
1966
|
-
function menuLines(boxName, w, h) {
|
|
1967
|
-
const body = [
|
|
1968
|
-
"",
|
|
1969
|
-
` No Claude session in ${boxName}.`,
|
|
1970
|
-
"",
|
|
1971
|
-
" [c] Start Claude here",
|
|
1972
|
-
" [s] Open a shell",
|
|
1973
|
-
"",
|
|
1974
|
-
" Ctrl+Option+\u2191/\u2193 switch \xB7 Ctrl-a then v/c/w/q (vnc/code/web/quit)"
|
|
1975
|
-
];
|
|
1976
|
-
const top = Math.max(0, Math.floor((h - body.length) / 2));
|
|
1977
|
-
const out = [];
|
|
1978
|
-
for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
|
|
1979
|
-
return out;
|
|
1980
|
-
}
|
|
1981
|
-
function lifecycleMenuLines(boxName, state, confirmDestroy, w, h) {
|
|
1982
|
-
const body = confirmDestroy ? [
|
|
1983
|
-
"",
|
|
1984
|
-
` Destroy ${boxName}?`,
|
|
1985
|
-
" This removes the container and its volumes.",
|
|
1986
|
-
"",
|
|
1987
|
-
" [y] Yes, destroy",
|
|
1988
|
-
" [any other key] Cancel"
|
|
1989
|
-
] : [
|
|
1990
|
-
"",
|
|
1991
|
-
` Box ${boxName} is ${state}.`,
|
|
1992
|
-
"",
|
|
1993
|
-
state === "paused" ? " [u] Unpause" : " [s] Start",
|
|
1994
|
-
" [d] Destroy",
|
|
1995
|
-
"",
|
|
1996
|
-
" Ctrl+Option+\u2191/\u2193 switch \xB7 Ctrl-a then q quit"
|
|
1997
|
-
];
|
|
1998
|
-
const top = Math.max(0, Math.floor((h - body.length) / 2));
|
|
1999
|
-
const out = [];
|
|
2000
|
-
for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
|
|
2001
|
-
return out;
|
|
2002
|
-
}
|
|
2003
|
-
function createMenuLines(where, w, h) {
|
|
2004
|
-
const body = [
|
|
2005
|
-
"",
|
|
2006
|
-
" Create a new box",
|
|
2007
|
-
"",
|
|
2008
|
-
" [c] Create + launch Claude",
|
|
2009
|
-
" [n] Create only",
|
|
2010
|
-
"",
|
|
2011
|
-
` in ${where}`,
|
|
2012
|
-
"",
|
|
2013
|
-
" Ctrl+Option+\u2191/\u2193 switch \xB7 Ctrl-a then q quit"
|
|
2014
|
-
];
|
|
2015
|
-
const top = Math.max(0, Math.floor((h - body.length) / 2));
|
|
2016
|
-
const out = [];
|
|
2017
|
-
for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
|
|
2018
|
-
return out;
|
|
2019
|
-
}
|
|
2020
|
-
var BAR_BG = "\x1B[48;2;48;48;48m";
|
|
2021
|
-
var BAR_BASE = BAR_BG + "\x1B[38;5;250m";
|
|
2022
|
-
var BAR_BRAND = "\x1B[48;5;39m\x1B[38;5;16m";
|
|
2023
|
-
var BRAND_BOLD = "\x1B[1m";
|
|
2024
|
-
var BRAND_NOBOLD = "\x1B[22m";
|
|
2025
|
-
var HINT_KEY = "\x1B[38;5;255m";
|
|
2026
|
-
var HINT_TXT = "\x1B[38;5;245m";
|
|
2027
|
-
var BAR_RESET = "\x1B[0m";
|
|
2028
|
-
var SWITCH_HINT = ["Control+Option+\u2191/\u2193", "switch"];
|
|
2029
|
-
var HINT_GROUPS = [
|
|
2030
|
-
SWITCH_HINT,
|
|
2031
|
-
["Control+a c", "code"],
|
|
2032
|
-
["Control+a v", "vnc"],
|
|
2033
|
-
["Control+a w", "web"],
|
|
2034
|
-
["Control+a q", "quit"]
|
|
2035
|
-
];
|
|
2036
|
-
var COLLAPSED_HINT_GROUPS = [
|
|
2037
|
-
SWITCH_HINT,
|
|
2038
|
-
["Control+a", "more"]
|
|
2039
|
-
];
|
|
2040
|
-
var ADVANCED_HINT_GROUPS = [
|
|
2041
|
-
["c", "code"],
|
|
2042
|
-
["v", "vnc"],
|
|
2043
|
-
["w", "web"],
|
|
2044
|
-
["s", "stop"],
|
|
2045
|
-
["p", "pause"],
|
|
2046
|
-
["d", "destroy"],
|
|
2047
|
-
["q", "quit"]
|
|
2048
|
-
];
|
|
2049
|
-
function statusLine(box, w, stateLabel, groups = HINT_GROUPS) {
|
|
2050
|
-
const state = stateLabel ?? (box ? box.state === "running" ? box.claudeActivity ?? "unknown" : box.state : "");
|
|
2051
|
-
const brandPrefix = box ? " agentbox \u25B8 " : " agentbox ";
|
|
2052
|
-
const base = box ? `${box.name} (${state})` : "";
|
|
2053
|
-
const coreMain = box ? `${base} ` : "";
|
|
2054
|
-
const corePlain = brandPrefix + coreMain;
|
|
2055
|
-
const SEP = " \u2502 ";
|
|
2056
|
-
const renderHints = (g) => ({
|
|
2057
|
-
plain: g.map(([k, l]) => `${k}: ${l}`).join(SEP) + " ",
|
|
2058
|
-
styled: g.map(([k, l]) => `${HINT_KEY}${k}${HINT_TXT}: ${l}`).join(`${HINT_TXT}${SEP}`) + " "
|
|
2059
|
-
});
|
|
2060
|
-
let hints = null;
|
|
2061
|
-
for (const g of [groups, COLLAPSED_HINT_GROUPS]) {
|
|
2062
|
-
const h = renderHints(g);
|
|
2063
|
-
if (corePlain.length + h.plain.length + 1 <= w) {
|
|
2064
|
-
hints = h;
|
|
2065
|
-
break;
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
if (!hints) {
|
|
2069
|
-
return BAR_BASE + BAR_BRAND + fit(corePlain, w) + BAR_RESET;
|
|
2070
|
-
}
|
|
2071
|
-
const room = w - corePlain.length - hints.plain.length - 1;
|
|
2072
|
-
let titleSeg = "";
|
|
2073
|
-
if (box?.sessionTitle && room >= 7) {
|
|
2074
|
-
titleSeg = ` \u2014 ${ellipsize(box.sessionTitle, Math.min(40, room - 3))}`;
|
|
2075
|
-
}
|
|
2076
|
-
const leftPlain = brandPrefix + base + titleSeg + (box ? " " : "");
|
|
2077
|
-
const leftStyled = BAR_BRAND + brandPrefix + BRAND_BOLD + base + titleSeg + (box ? " " : "") + BRAND_NOBOLD;
|
|
2078
|
-
const gap = w - leftPlain.length - hints.plain.length;
|
|
2079
|
-
return BAR_BASE + leftStyled + BAR_BASE + " ".repeat(gap) + hints.styled + BAR_RESET;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
2943
|
// src/dashboard/compositor.ts
|
|
2083
2944
|
var SB_BODY = BAR_BG + "\x1B[38;5;250m";
|
|
2084
2945
|
var SB_HEADER = BAR_BG + "\x1B[38;5;39m\x1B[1m";
|
|
2085
2946
|
var SB_SELECTED = BAR_BG + "\x1B[38;5;255m\x1B[1m";
|
|
2947
|
+
var SB_PROMPT = BAR_BG + "\x1B[38;5;220m\x1B[1m";
|
|
2948
|
+
var SB_AWAITING = BAR_BG + "\x1B[38;5;51m\x1B[1m";
|
|
2086
2949
|
var SGR_RESET = "\x1B[0m";
|
|
2087
2950
|
var POLL_MS = 1e3;
|
|
2088
2951
|
var FRAME_MS = 16;
|
|
2089
2952
|
var RESIZE_DEBOUNCE_MS = 120;
|
|
2090
2953
|
var LEADER_LINGER_MS = 1500;
|
|
2091
|
-
var
|
|
2092
|
-
var
|
|
2954
|
+
var NOTICE_SPINNER_MS = 120;
|
|
2955
|
+
var SYNC_BEGIN2 = "\x1B[?2026h";
|
|
2956
|
+
var SYNC_END2 = "\x1B[?2026l";
|
|
2093
2957
|
function cursorTo2(x, y) {
|
|
2094
2958
|
return `\x1B[${String(y + 1)};${String(x + 1)}H`;
|
|
2095
2959
|
}
|
|
@@ -2125,6 +2989,9 @@ var Compositor = class {
|
|
|
2125
2989
|
this.pendingConfirm = null;
|
|
2126
2990
|
this.drawChrome();
|
|
2127
2991
|
}
|
|
2992
|
+
if (e.type === "forward" && this.activePrompts.has(this.selectedId)) {
|
|
2993
|
+
if (this.handlePromptKey(e.bytes)) return;
|
|
2994
|
+
}
|
|
2128
2995
|
if (e.type === "quit") this.onSig();
|
|
2129
2996
|
else if (e.type === "switch") this.switchBox(e.dir);
|
|
2130
2997
|
else if (e.type === "action") {
|
|
@@ -2168,6 +3035,26 @@ var Compositor = class {
|
|
|
2168
3035
|
leaderLingerTimer = null;
|
|
2169
3036
|
/** Set while a destroy confirm is pending in the status bar. */
|
|
2170
3037
|
pendingConfirm = null;
|
|
3038
|
+
/**
|
|
3039
|
+
* Per-box relay-prompt state. Populated by SSE `prompt-ask` events,
|
|
3040
|
+
* cleared by `prompt-resolved` events or by the local user answering.
|
|
3041
|
+
* The sidebar reads it to mark rows; drawChrome's status-line picker
|
|
3042
|
+
* reads it to swap to [!] mode when the SELECTED box is in this map.
|
|
3043
|
+
* Subscriptions are tracked separately in {@link promptStreams} so
|
|
3044
|
+
* we can dispose them when boxes disappear from the list.
|
|
3045
|
+
*/
|
|
3046
|
+
activePrompts = /* @__PURE__ */ new Map();
|
|
3047
|
+
/**
|
|
3048
|
+
* Per-box active relay notice (currently: a checkpoint freezing the box).
|
|
3049
|
+
* Drives the `◆ checkpoint` sidebar cell and the animated status-bar
|
|
3050
|
+
* warning. Shares the SSE subscriptions in {@link promptStreams}.
|
|
3051
|
+
*/
|
|
3052
|
+
activeNotices = /* @__PURE__ */ new Map();
|
|
3053
|
+
/** Monotonic spinner counter for the notice status bar. */
|
|
3054
|
+
noticeFrame = 0;
|
|
3055
|
+
/** Drives the spinner animation while {@link activeNotices} is non-empty. */
|
|
3056
|
+
noticeTimer = null;
|
|
3057
|
+
promptStreams = /* @__PURE__ */ new Map();
|
|
2171
3058
|
activeMode = "claude";
|
|
2172
3059
|
flashMsg = null;
|
|
2173
3060
|
flashTimer = null;
|
|
@@ -2209,18 +3096,98 @@ var Compositor = class {
|
|
|
2209
3096
|
if (!this.boxes.some((b) => b.id === this.selectedId) && this.boxes[0]) {
|
|
2210
3097
|
this.selectedId = this.boxes[0].id;
|
|
2211
3098
|
}
|
|
2212
|
-
await this.spawnActive();
|
|
2213
|
-
this.drawChrome();
|
|
2214
|
-
this.scheduleRender();
|
|
2215
|
-
this.pollTimer = setInterval(() => void this.poll(), POLL_MS);
|
|
2216
|
-
await new Promise((
|
|
2217
|
-
this.resolveDone =
|
|
2218
|
-
});
|
|
3099
|
+
await this.spawnActive();
|
|
3100
|
+
this.drawChrome();
|
|
3101
|
+
this.scheduleRender();
|
|
3102
|
+
this.pollTimer = setInterval(() => void this.poll(), POLL_MS);
|
|
3103
|
+
await new Promise((resolve2) => {
|
|
3104
|
+
this.resolveDone = resolve2;
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
async refreshBoxes() {
|
|
3108
|
+
try {
|
|
3109
|
+
this.boxes = await this.deps.listCandidates();
|
|
3110
|
+
} catch {
|
|
3111
|
+
}
|
|
3112
|
+
this.syncPromptSubscriptions();
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Diff the current box list against {@link promptStreams}: subscribe to
|
|
3116
|
+
* any newcomer (skipping the synthetic + New box entry and pre-relay
|
|
3117
|
+
* boxes), dispose any departed subscription. Idempotent — safe to call
|
|
3118
|
+
* after every poll. Disposed boxes also clear their {@link activePrompts}
|
|
3119
|
+
* entry so the sidebar marker doesn't linger.
|
|
3120
|
+
*/
|
|
3121
|
+
syncPromptSubscriptions() {
|
|
3122
|
+
if (this.tornDown) return;
|
|
3123
|
+
const url = this.deps.relayBaseUrl;
|
|
3124
|
+
if (!url) return;
|
|
3125
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
3126
|
+
for (const b of this.boxes) {
|
|
3127
|
+
if (b.id === NEW_BOX_ID) continue;
|
|
3128
|
+
wanted.add(b.id);
|
|
3129
|
+
}
|
|
3130
|
+
for (const [boxId, stream] of this.promptStreams) {
|
|
3131
|
+
if (!wanted.has(boxId)) {
|
|
3132
|
+
stream.close();
|
|
3133
|
+
this.promptStreams.delete(boxId);
|
|
3134
|
+
let changed = this.activePrompts.delete(boxId);
|
|
3135
|
+
if (this.activeNotices.delete(boxId)) changed = true;
|
|
3136
|
+
if (this.activeNotices.size === 0) this.stopNoticeSpinner();
|
|
3137
|
+
if (changed) this.drawChrome();
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
for (const boxId of wanted) {
|
|
3141
|
+
if (this.promptStreams.has(boxId)) continue;
|
|
3142
|
+
const stream = subscribePrompts({
|
|
3143
|
+
relayBaseUrl: url,
|
|
3144
|
+
boxId,
|
|
3145
|
+
onPrompt: (ev) => {
|
|
3146
|
+
if (this.tornDown) return;
|
|
3147
|
+
this.activePrompts.set(boxId, ev);
|
|
3148
|
+
this.drawChrome();
|
|
3149
|
+
},
|
|
3150
|
+
onResolved: (id) => {
|
|
3151
|
+
if (this.tornDown) return;
|
|
3152
|
+
const current = this.activePrompts.get(boxId);
|
|
3153
|
+
if (current && current.id === id) {
|
|
3154
|
+
this.activePrompts.delete(boxId);
|
|
3155
|
+
this.drawChrome();
|
|
3156
|
+
}
|
|
3157
|
+
},
|
|
3158
|
+
onNotice: (ev) => {
|
|
3159
|
+
if (this.tornDown) return;
|
|
3160
|
+
this.activeNotices.set(boxId, ev);
|
|
3161
|
+
this.startNoticeSpinner();
|
|
3162
|
+
this.drawChrome();
|
|
3163
|
+
},
|
|
3164
|
+
onNoticeCleared: (id) => {
|
|
3165
|
+
if (this.tornDown) return;
|
|
3166
|
+
const current = this.activeNotices.get(boxId);
|
|
3167
|
+
if (current && current.id === id) {
|
|
3168
|
+
this.activeNotices.delete(boxId);
|
|
3169
|
+
if (this.activeNotices.size === 0) this.stopNoticeSpinner();
|
|
3170
|
+
this.drawChrome();
|
|
3171
|
+
}
|
|
3172
|
+
},
|
|
3173
|
+
onError: () => {
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
this.promptStreams.set(boxId, stream);
|
|
3177
|
+
}
|
|
2219
3178
|
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
3179
|
+
startNoticeSpinner() {
|
|
3180
|
+
if (this.noticeTimer) return;
|
|
3181
|
+
this.noticeTimer = setInterval(() => {
|
|
3182
|
+
this.noticeFrame++;
|
|
3183
|
+
this.drawChrome();
|
|
3184
|
+
}, NOTICE_SPINNER_MS);
|
|
3185
|
+
if (typeof this.noticeTimer.unref === "function") this.noticeTimer.unref();
|
|
3186
|
+
}
|
|
3187
|
+
stopNoticeSpinner() {
|
|
3188
|
+
if (this.noticeTimer) {
|
|
3189
|
+
clearInterval(this.noticeTimer);
|
|
3190
|
+
this.noticeTimer = null;
|
|
2224
3191
|
}
|
|
2225
3192
|
}
|
|
2226
3193
|
selectedBox() {
|
|
@@ -2490,6 +3457,46 @@ var Compositor = class {
|
|
|
2490
3457
|
this.busy = false;
|
|
2491
3458
|
}
|
|
2492
3459
|
}
|
|
3460
|
+
/**
|
|
3461
|
+
* Try to consume `bytes` as an answer to the selected box's active relay
|
|
3462
|
+
* prompt. Returns true when the bytes were a recognized answer key (the
|
|
3463
|
+
* caller stops further dispatch); false when the bytes should flow on to
|
|
3464
|
+
* the pty / other handlers.
|
|
3465
|
+
*
|
|
3466
|
+
* Single-byte chunks only: y/Y/Enter accept, n/N deny, Esc/Ctrl-c deny
|
|
3467
|
+
* with `cancelled: true`. Multi-byte chunks starting with ESC (mouse,
|
|
3468
|
+
* arrows, focus events, etc.) are passed through — exact same rule the
|
|
3469
|
+
* wrapped-pty input-router uses.
|
|
3470
|
+
*/
|
|
3471
|
+
handlePromptKey(bytes) {
|
|
3472
|
+
if (bytes.length > 1 && bytes[0] === 27) return false;
|
|
3473
|
+
if (bytes.length === 0) return false;
|
|
3474
|
+
const b = bytes[0];
|
|
3475
|
+
let answer = null;
|
|
3476
|
+
let cancelled = false;
|
|
3477
|
+
if (b === 121 || b === 89) answer = "y";
|
|
3478
|
+
else if (b === 110 || b === 78) answer = "n";
|
|
3479
|
+
else if (b === 27 || b === 3) {
|
|
3480
|
+
answer = "n";
|
|
3481
|
+
cancelled = true;
|
|
3482
|
+
} else if (b === 13 || b === 10) {
|
|
3483
|
+
const ev2 = this.activePrompts.get(this.selectedId);
|
|
3484
|
+
answer = ev2?.defaultAnswer ?? "n";
|
|
3485
|
+
}
|
|
3486
|
+
if (answer === null) return false;
|
|
3487
|
+
const ev = this.activePrompts.get(this.selectedId);
|
|
3488
|
+
if (!ev) return false;
|
|
3489
|
+
this.activePrompts.delete(this.selectedId);
|
|
3490
|
+
this.drawChrome();
|
|
3491
|
+
const url = this.deps.relayBaseUrl;
|
|
3492
|
+
if (url) {
|
|
3493
|
+
void postAnswer({
|
|
3494
|
+
relayBaseUrl: url,
|
|
3495
|
+
body: { id: ev.id, answer, ...cancelled ? { cancelled: true } : {} }
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
return true;
|
|
3499
|
+
}
|
|
2493
3500
|
handleConfirmKey(bytes) {
|
|
2494
3501
|
const c = this.pendingConfirm;
|
|
2495
3502
|
if (!c) return;
|
|
@@ -2596,11 +3603,11 @@ var Compositor = class {
|
|
|
2596
3603
|
/** Blank the right pane and drop the diff cache (next paint is full). */
|
|
2597
3604
|
clearRightPane() {
|
|
2598
3605
|
const r = this.layout.right;
|
|
2599
|
-
let s =
|
|
3606
|
+
let s = SYNC_BEGIN2 + "\x1B[?25l";
|
|
2600
3607
|
for (let i = 0; i < r.h; i++) {
|
|
2601
3608
|
s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + " ".repeat(r.w);
|
|
2602
3609
|
}
|
|
2603
|
-
this.out.write(s +
|
|
3610
|
+
this.out.write(s + SYNC_END2);
|
|
2604
3611
|
this.prevRows = null;
|
|
2605
3612
|
}
|
|
2606
3613
|
scheduleRender() {
|
|
@@ -2620,12 +3627,12 @@ var Compositor = class {
|
|
|
2620
3627
|
if (this.session) {
|
|
2621
3628
|
const { out, rows } = diffFrame(this.prevRows, this.session.snapshot(), r);
|
|
2622
3629
|
this.prevRows = rows;
|
|
2623
|
-
if (out) this.out.write(
|
|
3630
|
+
if (out) this.out.write(SYNC_BEGIN2 + out + SYNC_END2);
|
|
2624
3631
|
} else if (this.menu) {
|
|
2625
3632
|
const lines = menuLines(this.menu.boxName, r.w, r.h);
|
|
2626
|
-
let s =
|
|
3633
|
+
let s = SYNC_BEGIN2 + "\x1B[?25l";
|
|
2627
3634
|
for (let i = 0; i < r.h; i++) s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + (lines[i] ?? "");
|
|
2628
|
-
this.out.write(s +
|
|
3635
|
+
this.out.write(s + SYNC_END2);
|
|
2629
3636
|
} else if (this.lifecycleMenu) {
|
|
2630
3637
|
const lines = lifecycleMenuLines(
|
|
2631
3638
|
this.lifecycleMenu.boxName,
|
|
@@ -2634,40 +3641,52 @@ var Compositor = class {
|
|
|
2634
3641
|
r.w,
|
|
2635
3642
|
r.h
|
|
2636
3643
|
);
|
|
2637
|
-
let s =
|
|
3644
|
+
let s = SYNC_BEGIN2 + "\x1B[?25l";
|
|
2638
3645
|
for (let i = 0; i < r.h; i++) s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + (lines[i] ?? "");
|
|
2639
|
-
this.out.write(s +
|
|
3646
|
+
this.out.write(s + SYNC_END2);
|
|
2640
3647
|
} else if (this.createMenu) {
|
|
2641
3648
|
const lines = createMenuLines(this.createMenu.where, r.w, r.h);
|
|
2642
|
-
let s =
|
|
3649
|
+
let s = SYNC_BEGIN2 + "\x1B[?25l";
|
|
2643
3650
|
for (let i = 0; i < r.h; i++) s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + (lines[i] ?? "");
|
|
2644
|
-
this.out.write(s +
|
|
3651
|
+
this.out.write(s + SYNC_END2);
|
|
2645
3652
|
} else if (this.placeholder) {
|
|
2646
|
-
let s =
|
|
3653
|
+
let s = SYNC_BEGIN2 + "\x1B[?25l";
|
|
2647
3654
|
for (let i = 0; i < r.h; i++) {
|
|
2648
3655
|
const line = (this.placeholder[i] ?? "").slice(0, r.w);
|
|
2649
3656
|
s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + line + " ".repeat(Math.max(0, r.w - line.length));
|
|
2650
3657
|
}
|
|
2651
|
-
this.out.write(s +
|
|
3658
|
+
this.out.write(s + SYNC_END2);
|
|
2652
3659
|
}
|
|
2653
3660
|
}
|
|
2654
3661
|
drawChrome() {
|
|
2655
3662
|
if (this.tornDown || this.layout.tooSmall) return;
|
|
2656
3663
|
const { sidebar, sepX, statusY } = this.layout;
|
|
3664
|
+
const decorate = this.activePrompts.size > 0 || this.activeNotices.size > 0;
|
|
3665
|
+
const boxesWithPrompt = decorate ? this.boxes.map((b) => {
|
|
3666
|
+
const pendingPrompt = this.activePrompts.has(b.id);
|
|
3667
|
+
const checkpointing = this.activeNotices.has(b.id);
|
|
3668
|
+
return pendingPrompt || checkpointing ? { ...b, pendingPrompt, checkpointing } : b;
|
|
3669
|
+
}) : this.boxes;
|
|
2657
3670
|
const { lines, rowOwner, headerRows } = sidebarLines(
|
|
2658
|
-
|
|
3671
|
+
boxesWithPrompt,
|
|
2659
3672
|
this.selectedId,
|
|
2660
3673
|
sidebar.w,
|
|
2661
3674
|
sidebar.h
|
|
2662
3675
|
);
|
|
2663
|
-
let s =
|
|
3676
|
+
let s = SYNC_BEGIN2 + "\x1B[0m";
|
|
2664
3677
|
for (let i = 0; i < lines.length; i++) {
|
|
2665
|
-
const
|
|
3678
|
+
const owner = rowOwner[i] ?? null;
|
|
3679
|
+
const isSelected = owner === this.selectedId;
|
|
3680
|
+
const hasPrompt = owner !== null && this.activePrompts.has(owner);
|
|
3681
|
+
const ownerBox = owner !== null ? boxesWithPrompt.find((b) => b.id === owner) : void 0;
|
|
3682
|
+
const isAwaiting = ownerBox?.claudeActivity === "waiting";
|
|
3683
|
+
const style = headerRows[i] ? SB_HEADER : isSelected ? SB_SELECTED : hasPrompt ? SB_PROMPT : isAwaiting ? SB_AWAITING : SB_BODY;
|
|
2666
3684
|
s += cursorTo2(0, i) + style + lines[i] + SGR_RESET;
|
|
2667
3685
|
}
|
|
2668
3686
|
for (let y = 0; y < sidebar.h; y++)
|
|
2669
3687
|
s += cursorTo2(sepX, y) + SB_HEADER + (y === 0 ? "\u256E" : "\u2502") + SGR_RESET;
|
|
2670
3688
|
let status;
|
|
3689
|
+
const activePromptForSelected = this.activePrompts.get(this.selectedId);
|
|
2671
3690
|
if (this.pendingConfirm) {
|
|
2672
3691
|
const w = this.layout.cols;
|
|
2673
3692
|
const txt = ` Destroy ${this.pendingConfirm.name}? y = confirm \xB7 any other key = cancel `.slice(0, w).padEnd(w);
|
|
@@ -2676,6 +3695,17 @@ var Compositor = class {
|
|
|
2676
3695
|
const w = this.layout.cols;
|
|
2677
3696
|
const txt = ` ${this.flashMsg} `.slice(0, w).padEnd(w);
|
|
2678
3697
|
status = `\x1B[7m${txt}\x1B[0m`;
|
|
3698
|
+
} else if (activePromptForSelected) {
|
|
3699
|
+
status = renderFooter(
|
|
3700
|
+
{ kind: "prompt", prompt: activePromptForSelected },
|
|
3701
|
+
this.layout.cols
|
|
3702
|
+
);
|
|
3703
|
+
} else if (this.activeNotices.has(this.selectedId)) {
|
|
3704
|
+
const notice = this.activeNotices.get(this.selectedId);
|
|
3705
|
+
status = renderFooter(
|
|
3706
|
+
{ kind: "notice", message: notice.message, frame: this.noticeFrame },
|
|
3707
|
+
this.layout.cols
|
|
3708
|
+
);
|
|
2679
3709
|
} else {
|
|
2680
3710
|
const stateLabel = this.selectedId === NEW_BOX_ID ? "create" : this.menu ? "menu" : this.session && this.activeMode === "shell" ? "shell" : void 0;
|
|
2681
3711
|
status = statusLine(
|
|
@@ -2686,7 +3716,7 @@ var Compositor = class {
|
|
|
2686
3716
|
);
|
|
2687
3717
|
}
|
|
2688
3718
|
s += cursorTo2(0, statusY) + status;
|
|
2689
|
-
this.out.write(s +
|
|
3719
|
+
this.out.write(s + SYNC_END2);
|
|
2690
3720
|
}
|
|
2691
3721
|
scheduleResize() {
|
|
2692
3722
|
if (this.resizeTimer) clearTimeout(this.resizeTimer);
|
|
@@ -2698,7 +3728,7 @@ var Compositor = class {
|
|
|
2698
3728
|
if (this.session && !this.layout.tooSmall) {
|
|
2699
3729
|
this.session.resize(Math.max(1, r.w), Math.max(1, r.h));
|
|
2700
3730
|
}
|
|
2701
|
-
this.out.write(
|
|
3731
|
+
this.out.write(SYNC_BEGIN2 + "\x1B[2J" + SYNC_END2);
|
|
2702
3732
|
this.drawChrome();
|
|
2703
3733
|
this.render();
|
|
2704
3734
|
}, RESIZE_DEBOUNCE_MS);
|
|
@@ -2711,6 +3741,11 @@ var Compositor = class {
|
|
|
2711
3741
|
if (this.resizeTimer) clearTimeout(this.resizeTimer);
|
|
2712
3742
|
if (this.flashTimer) clearTimeout(this.flashTimer);
|
|
2713
3743
|
if (this.leaderLingerTimer) clearTimeout(this.leaderLingerTimer);
|
|
3744
|
+
if (this.noticeTimer) clearInterval(this.noticeTimer);
|
|
3745
|
+
for (const stream of this.promptStreams.values()) stream.close();
|
|
3746
|
+
this.promptStreams.clear();
|
|
3747
|
+
this.activePrompts.clear();
|
|
3748
|
+
this.activeNotices.clear();
|
|
2714
3749
|
this.parser.dispose();
|
|
2715
3750
|
this.disposeSession();
|
|
2716
3751
|
this.inp.off("data", this.onData);
|
|
@@ -2748,29 +3783,23 @@ function toSidebar(b) {
|
|
|
2748
3783
|
project: b.projectRoot
|
|
2749
3784
|
};
|
|
2750
3785
|
}
|
|
2751
|
-
var dashboardCommand = new
|
|
3786
|
+
var dashboardCommand = new Command8("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) => {
|
|
2752
3787
|
try {
|
|
2753
3788
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
2754
|
-
|
|
3789
|
+
log10.error("agentbox dashboard needs an interactive terminal");
|
|
2755
3790
|
process.exit(2);
|
|
2756
3791
|
}
|
|
3792
|
+
const backend = await loadPtyBackend();
|
|
2757
3793
|
let ptySpawn;
|
|
2758
3794
|
let termCtor;
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
if (typeof spawn5 !== "function" || typeof Terminal !== "function") {
|
|
2765
|
-
throw new Error("terminal backend missing expected exports");
|
|
2766
|
-
}
|
|
2767
|
-
ptySpawn = spawn5;
|
|
2768
|
-
termCtor = Terminal;
|
|
2769
|
-
} catch {
|
|
2770
|
-
log9.error(
|
|
3795
|
+
if (backend) {
|
|
3796
|
+
ptySpawn = backend.ptySpawn;
|
|
3797
|
+
termCtor = backend.termCtor;
|
|
3798
|
+
} else {
|
|
3799
|
+
log10.error(
|
|
2771
3800
|
"agentbox dashboard is unavailable here (native terminal backend failed to load)"
|
|
2772
3801
|
);
|
|
2773
|
-
|
|
3802
|
+
log10.info("use `agentbox claude` / `agentbox claude attach` instead");
|
|
2774
3803
|
process.exit(2);
|
|
2775
3804
|
}
|
|
2776
3805
|
const project = await findProjectRoot(process.cwd());
|
|
@@ -2829,6 +3858,11 @@ var dashboardCommand = new Command7("dashboard").description("Box list + the sel
|
|
|
2829
3858
|
await rebuildPluginNativeDeps(box.container, {
|
|
2830
3859
|
volume: box.claudeConfigVolume
|
|
2831
3860
|
});
|
|
3861
|
+
const claudeVolume = box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME;
|
|
3862
|
+
await syncClaudeCredentials(
|
|
3863
|
+
{ volume: claudeVolume },
|
|
3864
|
+
{ image: box.image, isolate: claudeVolume !== SHARED_CLAUDE_VOLUME }
|
|
3865
|
+
);
|
|
2832
3866
|
await startClaudeSession({ container: box.container, claudeArgs: [], boxName: box.name });
|
|
2833
3867
|
const info = await claudeSessionInfo(box.container);
|
|
2834
3868
|
return {
|
|
@@ -2945,6 +3979,10 @@ var dashboardCommand = new Command7("dashboard").description("Box list + the sel
|
|
|
2945
3979
|
{
|
|
2946
3980
|
ptySpawn,
|
|
2947
3981
|
termCtor,
|
|
3982
|
+
// Host-side loopback URL the per-box SSE subscriptions connect to.
|
|
3983
|
+
// The relay binds 0.0.0.0; loopback is the admin/* path's required
|
|
3984
|
+
// source. Same constant the wrapped-pty wrappers use.
|
|
3985
|
+
relayBaseUrl: `http://127.0.0.1:${String(DEFAULT_RELAY_PORT)}`,
|
|
2948
3986
|
listCandidates,
|
|
2949
3987
|
resolveTarget,
|
|
2950
3988
|
startClaude,
|
|
@@ -2968,48 +4006,369 @@ var dashboardCommand = new Command7("dashboard").description("Box list + the sel
|
|
|
2968
4006
|
});
|
|
2969
4007
|
|
|
2970
4008
|
// src/commands/destroy.ts
|
|
2971
|
-
import { confirm as confirm4, isCancel as isCancel4, log as
|
|
2972
|
-
import { Command as
|
|
2973
|
-
var destroyCommand = new
|
|
4009
|
+
import { confirm as confirm4, isCancel as isCancel4, log as log11 } from "@clack/prompts";
|
|
4010
|
+
import { Command as Command9 } from "commander";
|
|
4011
|
+
var destroyCommand = new Command9("destroy").alias("rm").description("Destroy a box and discard its container writable layer (where /workspace lived)").argument(
|
|
2974
4012
|
"[box]",
|
|
2975
4013
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
2976
4014
|
).option("-y, --yes", "skip the confirmation prompt").option("--keep-snapshot", "don't delete the snapshot dir under ~/.agentbox/snapshots/").action(async (idOrName, opts) => {
|
|
2977
4015
|
try {
|
|
2978
4016
|
const box = await resolveBoxOrExit(idOrName);
|
|
2979
4017
|
if (!opts.yes) {
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
4018
|
+
log11.warn(
|
|
4019
|
+
"This will wipe the container writable layer \u2014 /workspace contents and agent work-in-progress are lost."
|
|
4020
|
+
);
|
|
4021
|
+
log11.info(`id: ${box.id}`);
|
|
4022
|
+
log11.info(`container: ${box.container}`);
|
|
4023
|
+
if (box.snapshotDir) {
|
|
4024
|
+
log11.info(`snapshot: ${box.snapshotDir}${opts.keepSnapshot ? " (will be kept)" : ""}`);
|
|
4025
|
+
}
|
|
4026
|
+
const ok = await confirm4({
|
|
4027
|
+
message: "Destroy this box?",
|
|
4028
|
+
initialValue: false
|
|
4029
|
+
});
|
|
4030
|
+
if (isCancel4(ok) || !ok) {
|
|
4031
|
+
log11.info("cancelled");
|
|
4032
|
+
return;
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
const result = await destroyBox(box.id, { keepSnapshot: opts.keepSnapshot });
|
|
4036
|
+
const out = [`destroyed ${result.record.container}`];
|
|
4037
|
+
if (result.removedContainer) out.push(" \u2713 container removed");
|
|
4038
|
+
out.push(` \u2713 volumes removed: ${result.removedVolumes.join(", ")}`);
|
|
4039
|
+
if (result.removedSnapshot) out.push(` \u2713 snapshot removed: ${result.removedSnapshot}`);
|
|
4040
|
+
else if (box.snapshotDir && opts.keepSnapshot) {
|
|
4041
|
+
out.push(` \xB7 snapshot kept: ${box.snapshotDir}`);
|
|
4042
|
+
}
|
|
4043
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
4044
|
+
} catch (err) {
|
|
4045
|
+
handleLifecycleError(err);
|
|
4046
|
+
}
|
|
4047
|
+
});
|
|
4048
|
+
|
|
4049
|
+
// src/commands/download.ts
|
|
4050
|
+
import { confirm as confirm8, isCancel as isCancel8, log as log15 } from "@clack/prompts";
|
|
4051
|
+
import { Command as Command13 } from "commander";
|
|
4052
|
+
|
|
4053
|
+
// src/commands/download-claude.ts
|
|
4054
|
+
import { confirm as confirm5, isCancel as isCancel5, log as log12 } from "@clack/prompts";
|
|
4055
|
+
import { Command as Command10 } from "commander";
|
|
4056
|
+
function tag(item) {
|
|
4057
|
+
const noun = item.category === "plugins" ? "plugin" : item.category.replace(/s$/, "");
|
|
4058
|
+
return ` ${item.category}/${item.name} (new ${noun})`;
|
|
4059
|
+
}
|
|
4060
|
+
var downloadClaudeCommand = new Command10("claude").description(
|
|
4061
|
+
"Download box-installed Claude skills/plugins/agents/commands back to host ~/.claude (additive)"
|
|
4062
|
+
).argument(
|
|
4063
|
+
"[box]",
|
|
4064
|
+
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
4065
|
+
).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "list new items and exit; don't write").action(async (idOrName, opts) => {
|
|
4066
|
+
try {
|
|
4067
|
+
const box = await resolveBoxOrExit(idOrName);
|
|
4068
|
+
const volume = box.claudeConfigVolume ?? resolveClaudeVolume({ isolate: false, boxId: box.id }).volume;
|
|
4069
|
+
if (volume === SHARED_CLAUDE_VOLUME) {
|
|
4070
|
+
log12.warn(
|
|
4071
|
+
`Reading the shared ${SHARED_CLAUDE_VOLUME} volume \u2014 it aggregates Claude extensions installed in ANY box, not just ${box.name}.`
|
|
4072
|
+
);
|
|
4073
|
+
}
|
|
4074
|
+
const image = box.image || DEFAULT_BOX_IMAGE;
|
|
4075
|
+
const preview = await pullClaudeExtras({ volume }, { image, dryRun: true });
|
|
4076
|
+
if (preview.newItems.length === 0 && preview.mergedRegistries.length === 0) {
|
|
4077
|
+
process.stdout.write("no new Claude extensions to download into ~/.claude\n");
|
|
4078
|
+
return;
|
|
4079
|
+
}
|
|
4080
|
+
for (const item of preview.newItems) process.stdout.write(`${tag(item)}
|
|
4081
|
+
`);
|
|
4082
|
+
for (const reg of preview.mergedRegistries) {
|
|
4083
|
+
process.stdout.write(` plugins/${reg} (merge new entries)
|
|
4084
|
+
`);
|
|
4085
|
+
}
|
|
4086
|
+
if (opts.dryRun) {
|
|
4087
|
+
process.stdout.write(
|
|
4088
|
+
`
|
|
4089
|
+
[dry-run] ${preview.newItems.length} item(s)${preview.mergedRegistries.length > 0 ? ` + ${preview.mergedRegistries.length} registry merge(s)` : ""} would be downloaded into ~/.claude
|
|
4090
|
+
`
|
|
4091
|
+
);
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
if (!opts.yes) {
|
|
4095
|
+
const ok = await confirm5({
|
|
4096
|
+
message: `Download ${preview.newItems.length} new Claude extension(s) into ~/.claude? (existing items are never overwritten)`,
|
|
4097
|
+
initialValue: false
|
|
4098
|
+
});
|
|
4099
|
+
if (isCancel5(ok) || !ok) {
|
|
4100
|
+
log12.info("cancelled");
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
const result = await pullClaudeExtras({ volume }, { image, dryRun: false });
|
|
4105
|
+
process.stdout.write(
|
|
4106
|
+
`downloaded ${result.newItems.length} extension(s)${result.mergedRegistries.length > 0 ? `, merged ${result.mergedRegistries.join(", ")}` : ""} into ~/.claude
|
|
4107
|
+
`
|
|
4108
|
+
);
|
|
4109
|
+
} catch (err) {
|
|
4110
|
+
handleLifecycleError(err);
|
|
4111
|
+
}
|
|
4112
|
+
});
|
|
4113
|
+
|
|
4114
|
+
// src/commands/download-config.ts
|
|
4115
|
+
import { confirm as confirm6, isCancel as isCancel6, log as log13 } from "@clack/prompts";
|
|
4116
|
+
import { Command as Command11 } from "commander";
|
|
4117
|
+
function tagChange(line) {
|
|
4118
|
+
const sp = line.indexOf(" ");
|
|
4119
|
+
const code = sp === -1 ? line : line.slice(0, sp);
|
|
4120
|
+
const path2 = sp === -1 ? "" : line.slice(sp + 1);
|
|
4121
|
+
const isNew = /^>f\++$/.test(code);
|
|
4122
|
+
return ` ${path2} ${isNew ? "(new)" : "(overwrites host)"}`;
|
|
4123
|
+
}
|
|
4124
|
+
var CONFIG_PATTERNS = ["agentbox.yaml"];
|
|
4125
|
+
var downloadConfigCommand = new Command11("config").description("Download agentbox.yaml box -> host").argument(
|
|
4126
|
+
"[box]",
|
|
4127
|
+
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
4128
|
+
).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) => {
|
|
4129
|
+
try {
|
|
4130
|
+
const box = await resolveBoxOrExit(idOrName);
|
|
4131
|
+
const insp = await inspectBox(box.id);
|
|
4132
|
+
if (insp.state === "paused") {
|
|
4133
|
+
log13.info("box is paused; unpausing");
|
|
4134
|
+
await unpauseBox(box.id);
|
|
4135
|
+
} else if (insp.state === "stopped") {
|
|
4136
|
+
log13.info("box is stopped; starting");
|
|
4137
|
+
await startBox(box.id);
|
|
4138
|
+
} else if (insp.state === "missing") {
|
|
4139
|
+
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
4140
|
+
}
|
|
4141
|
+
log13.info(`agentbox.yaml bypasses gitignore and copies directly into ${box.workspacePath}`);
|
|
4142
|
+
const preview = await pullToHost(box, {
|
|
4143
|
+
dryRun: true,
|
|
4144
|
+
respectGitignore: false,
|
|
4145
|
+
envPatterns: CONFIG_PATTERNS,
|
|
4146
|
+
noRefresh: !opts.refresh
|
|
4147
|
+
});
|
|
4148
|
+
if (preview.changes.length === 0) {
|
|
4149
|
+
process.stdout.write(`no config file to download into ${box.workspacePath}
|
|
4150
|
+
`);
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
for (const line of preview.changes) process.stdout.write(`${tagChange(line)}
|
|
4154
|
+
`);
|
|
4155
|
+
if (opts.dryRun) {
|
|
4156
|
+
process.stdout.write(
|
|
4157
|
+
`
|
|
4158
|
+
[dry-run] ${preview.changes.length} config file(s) would change in ${box.workspacePath}
|
|
4159
|
+
`
|
|
4160
|
+
);
|
|
4161
|
+
return;
|
|
4162
|
+
}
|
|
4163
|
+
if (!opts.yes) {
|
|
4164
|
+
const ok = await confirm6({
|
|
4165
|
+
message: `Download ${preview.changes.length} config file(s) into ${box.workspacePath}? (existing files will be overwritten)`,
|
|
4166
|
+
initialValue: false
|
|
4167
|
+
});
|
|
4168
|
+
if (isCancel6(ok) || !ok) {
|
|
4169
|
+
log13.info("cancelled");
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
const result = await pullToHost(box, {
|
|
4174
|
+
dryRun: false,
|
|
4175
|
+
respectGitignore: false,
|
|
4176
|
+
envPatterns: CONFIG_PATTERNS,
|
|
4177
|
+
// The dry-run pass above already refreshed (or intentionally skipped)
|
|
4178
|
+
// the scratch dir — don't rsync box->scratch a second time.
|
|
4179
|
+
noRefresh: true
|
|
4180
|
+
});
|
|
4181
|
+
process.stdout.write(
|
|
4182
|
+
`downloaded ${result.changes.length} config file(s) into ${result.hostPath}
|
|
4183
|
+
`
|
|
4184
|
+
);
|
|
4185
|
+
} catch (err) {
|
|
4186
|
+
handleLifecycleError(err);
|
|
4187
|
+
}
|
|
4188
|
+
});
|
|
4189
|
+
|
|
4190
|
+
// src/commands/download-env.ts
|
|
4191
|
+
import { confirm as confirm7, isCancel as isCancel7, log as log14 } from "@clack/prompts";
|
|
4192
|
+
import { Command as Command12 } from "commander";
|
|
4193
|
+
function tagChange2(line) {
|
|
4194
|
+
const sp = line.indexOf(" ");
|
|
4195
|
+
const code = sp === -1 ? line : line.slice(0, sp);
|
|
4196
|
+
const path2 = sp === -1 ? "" : line.slice(sp + 1);
|
|
4197
|
+
const isNew = /^>f\++$/.test(code);
|
|
4198
|
+
return ` ${path2} ${isNew ? "(new)" : "(overwrites host)"}`;
|
|
4199
|
+
}
|
|
4200
|
+
var downloadEnvCommand = new Command12("env").description(
|
|
4201
|
+
"Download gitignored env/config files (.env*, .envrc, secrets.toml, agentbox.yaml, ...) box -> host"
|
|
4202
|
+
).argument(
|
|
4203
|
+
"[box]",
|
|
4204
|
+
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
4205
|
+
).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "list matched files and exit; don't write").option(
|
|
4206
|
+
"--pattern <glob>",
|
|
4207
|
+
"extra basename glob to match (repeatable, adds to defaults)",
|
|
4208
|
+
(v, acc) => [...acc, v],
|
|
4209
|
+
[]
|
|
4210
|
+
).option("--no-refresh", "skip the box->scratch-dir rsync step").action(async (idOrName, opts) => {
|
|
4211
|
+
try {
|
|
4212
|
+
const box = await resolveBoxOrExit(idOrName);
|
|
4213
|
+
const insp = await inspectBox(box.id);
|
|
4214
|
+
if (insp.state === "paused") {
|
|
4215
|
+
log14.info("box is paused; unpausing");
|
|
4216
|
+
await unpauseBox(box.id);
|
|
4217
|
+
} else if (insp.state === "stopped") {
|
|
4218
|
+
log14.info("box is stopped; starting");
|
|
4219
|
+
await startBox(box.id);
|
|
4220
|
+
} else if (insp.state === "missing") {
|
|
4221
|
+
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
4222
|
+
}
|
|
4223
|
+
log14.info(
|
|
4224
|
+
`env/config files bypass gitignore and copy directly into ${box.workspacePath}`
|
|
4225
|
+
);
|
|
4226
|
+
const patterns = [...DEFAULT_ENV_PATTERNS, ...opts.pattern];
|
|
4227
|
+
const preview = await pullToHost(box, {
|
|
4228
|
+
dryRun: true,
|
|
4229
|
+
respectGitignore: false,
|
|
4230
|
+
envPatterns: patterns,
|
|
4231
|
+
noRefresh: !opts.refresh
|
|
4232
|
+
});
|
|
4233
|
+
if (preview.changes.length === 0) {
|
|
4234
|
+
process.stdout.write(`no env/config files to download into ${box.workspacePath}
|
|
4235
|
+
`);
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
for (const line of preview.changes) process.stdout.write(`${tagChange2(line)}
|
|
4239
|
+
`);
|
|
4240
|
+
if (opts.dryRun) {
|
|
4241
|
+
process.stdout.write(
|
|
4242
|
+
`
|
|
4243
|
+
[dry-run] ${preview.changes.length} env/config file(s) would change in ${box.workspacePath}
|
|
4244
|
+
`
|
|
4245
|
+
);
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
if (!opts.yes) {
|
|
4249
|
+
const ok = await confirm7({
|
|
4250
|
+
message: `Download ${preview.changes.length} env/config file(s) into ${box.workspacePath}? (existing files will be overwritten)`,
|
|
4251
|
+
initialValue: false
|
|
4252
|
+
});
|
|
4253
|
+
if (isCancel7(ok) || !ok) {
|
|
4254
|
+
log14.info("cancelled");
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
const result = await pullToHost(box, {
|
|
4259
|
+
dryRun: false,
|
|
4260
|
+
respectGitignore: false,
|
|
4261
|
+
envPatterns: patterns,
|
|
4262
|
+
// The dry-run pass above already refreshed (or intentionally skipped)
|
|
4263
|
+
// the scratch dir — don't rsync box->scratch a second time.
|
|
4264
|
+
noRefresh: true
|
|
4265
|
+
});
|
|
4266
|
+
process.stdout.write(
|
|
4267
|
+
`downloaded ${result.changes.length} env/config file(s) into ${result.hostPath}
|
|
4268
|
+
`
|
|
4269
|
+
);
|
|
4270
|
+
} catch (err) {
|
|
4271
|
+
handleLifecycleError(err);
|
|
4272
|
+
}
|
|
4273
|
+
});
|
|
4274
|
+
|
|
4275
|
+
// src/commands/download.ts
|
|
4276
|
+
var downloadCommand = new Command13("download").enablePositionalOptions().description("Download a box's /workspace back into your host workspace dir (gitignore-aware)").argument(
|
|
4277
|
+
"[box]",
|
|
4278
|
+
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
4279
|
+
).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "print the change list and exit; don't write").option(
|
|
4280
|
+
"--no-respect-gitignore",
|
|
4281
|
+
"disable git ls-files mode; use --exclude=node_modules,.git instead"
|
|
4282
|
+
).option(
|
|
4283
|
+
"--include-node-modules",
|
|
4284
|
+
"do not exclude node_modules in fallback mode (no effect in gitignore mode)"
|
|
4285
|
+
).option("--no-refresh", "skip the box->scratch-dir rsync step (use whatever's already there)").option(
|
|
4286
|
+
"--with-env",
|
|
4287
|
+
"also download env/config files (.env*, .envrc, secrets.toml, agentbox.yaml, ...) ignoring gitignore"
|
|
4288
|
+
).option(
|
|
4289
|
+
"--pattern <glob>",
|
|
4290
|
+
"extra env basename glob; only effective with --with-env (repeatable)",
|
|
4291
|
+
(v, acc) => [...acc, v],
|
|
4292
|
+
[]
|
|
4293
|
+
).action(async (idOrName, opts) => {
|
|
4294
|
+
try {
|
|
4295
|
+
const box = await resolveBoxOrExit(idOrName);
|
|
4296
|
+
const insp = await inspectBox(box.id);
|
|
4297
|
+
if (insp.state === "paused") {
|
|
4298
|
+
log15.info("box is paused; unpausing");
|
|
4299
|
+
await unpauseBox(box.id);
|
|
4300
|
+
} else if (insp.state === "stopped") {
|
|
4301
|
+
log15.info("box is stopped; starting");
|
|
4302
|
+
await startBox(box.id);
|
|
4303
|
+
} else if (insp.state === "missing") {
|
|
4304
|
+
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
4305
|
+
}
|
|
4306
|
+
const rootWorktree = box.gitWorktrees?.find((w) => w.kind === "root");
|
|
4307
|
+
if (rootWorktree) {
|
|
4308
|
+
log15.warn(
|
|
4309
|
+
`This box has been committing to branch \`${rootWorktree.branch}\` in a separate worktree.
|
|
4310
|
+
For a git-aware merge instead of a file copy, run from your checkout:
|
|
4311
|
+
git merge ${rootWorktree.branch}
|
|
4312
|
+
Continuing with rsync into ${box.workspacePath}`
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
const envPatterns = opts.withEnv ? [...DEFAULT_ENV_PATTERNS, ...opts.pattern] : void 0;
|
|
4316
|
+
const preview = await pullToHost(box, {
|
|
4317
|
+
dryRun: true,
|
|
4318
|
+
respectGitignore: opts.respectGitignore,
|
|
4319
|
+
includeNodeModules: opts.includeNodeModules,
|
|
4320
|
+
envPatterns,
|
|
4321
|
+
noRefresh: !opts.refresh
|
|
4322
|
+
});
|
|
4323
|
+
if (preview.changes.length === 0) {
|
|
4324
|
+
process.stdout.write(`no changes to download into ${box.workspacePath}
|
|
4325
|
+
`);
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
if (opts.dryRun) {
|
|
4329
|
+
for (const line of preview.changes) process.stdout.write(`${line}
|
|
4330
|
+
`);
|
|
4331
|
+
process.stdout.write(
|
|
4332
|
+
`
|
|
4333
|
+
[dry-run] ${preview.changes.length} file(s) would change in ${box.workspacePath}
|
|
4334
|
+
`
|
|
4335
|
+
);
|
|
4336
|
+
return;
|
|
4337
|
+
}
|
|
4338
|
+
if (!opts.yes) {
|
|
4339
|
+
const ok = await confirm8({
|
|
4340
|
+
message: `Download ${preview.changes.length} changed file(s)${opts.withEnv ? " (incl. env/config)" : ""} into ${box.workspacePath}?`,
|
|
2989
4341
|
initialValue: false
|
|
2990
4342
|
});
|
|
2991
|
-
if (
|
|
2992
|
-
|
|
4343
|
+
if (isCancel8(ok) || !ok) {
|
|
4344
|
+
log15.info("cancelled");
|
|
2993
4345
|
return;
|
|
2994
4346
|
}
|
|
2995
4347
|
}
|
|
2996
|
-
const result = await
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
4348
|
+
const result = await pullToHost(box, {
|
|
4349
|
+
dryRun: false,
|
|
4350
|
+
respectGitignore: opts.respectGitignore,
|
|
4351
|
+
includeNodeModules: opts.includeNodeModules,
|
|
4352
|
+
envPatterns,
|
|
4353
|
+
// The dry-run pass above already refreshed (or intentionally skipped)
|
|
4354
|
+
// the scratch dir — don't rsync box->scratch a second time.
|
|
4355
|
+
noRefresh: true
|
|
4356
|
+
});
|
|
4357
|
+
process.stdout.write(
|
|
4358
|
+
`updated ${result.changes.length} file(s) in ${result.hostPath}${result.usedGitignore ? "" : " (exclude-list mode)"}
|
|
4359
|
+
`
|
|
4360
|
+
);
|
|
3005
4361
|
} catch (err) {
|
|
3006
4362
|
handleLifecycleError(err);
|
|
3007
4363
|
}
|
|
3008
4364
|
});
|
|
4365
|
+
downloadCommand.addCommand(downloadEnvCommand);
|
|
4366
|
+
downloadCommand.addCommand(downloadClaudeCommand);
|
|
4367
|
+
downloadCommand.addCommand(downloadConfigCommand);
|
|
3009
4368
|
|
|
3010
4369
|
// src/commands/list.ts
|
|
3011
|
-
import { log as
|
|
3012
|
-
import { Command as
|
|
4370
|
+
import { log as log16 } from "@clack/prompts";
|
|
4371
|
+
import { Command as Command14 } from "commander";
|
|
3013
4372
|
import { pathToFileURL } from "url";
|
|
3014
4373
|
|
|
3015
4374
|
// src/hyperlink.ts
|
|
@@ -3094,11 +4453,11 @@ function urlCell(box, stream) {
|
|
|
3094
4453
|
width: parts.reduce((a, p) => a + p.width, 0) + sep.length * (parts.length - 1)
|
|
3095
4454
|
};
|
|
3096
4455
|
}
|
|
3097
|
-
function workspaceCell(
|
|
3098
|
-
const display = middleTruncate(
|
|
4456
|
+
function workspaceCell(path2, target, stream) {
|
|
4457
|
+
const display = middleTruncate(path2, target);
|
|
3099
4458
|
let url;
|
|
3100
4459
|
try {
|
|
3101
|
-
url = pathToFileURL(
|
|
4460
|
+
url = pathToFileURL(path2).href;
|
|
3102
4461
|
} catch {
|
|
3103
4462
|
return { text: display, width: display.length };
|
|
3104
4463
|
}
|
|
@@ -3136,579 +4495,332 @@ function renderTable(boxes, stream) {
|
|
|
3136
4495
|
};
|
|
3137
4496
|
return all.map(
|
|
3138
4497
|
(row2) => row2.map((cell, i) => padCell(cell ?? plain(""), i)).join(" ").trimEnd()
|
|
3139
|
-
).join("\n");
|
|
3140
|
-
}
|
|
3141
|
-
async function scopedBoxes(all) {
|
|
3142
|
-
const boxes = await listBoxes();
|
|
3143
|
-
if (all) return { boxes, projectRoot: "", scoped: false };
|
|
3144
|
-
const { root } = await findProjectRoot(process.cwd());
|
|
3145
|
-
return { boxes: boxes.filter((b) => b.projectRoot === root), projectRoot: root, scoped: true };
|
|
3146
|
-
}
|
|
3147
|
-
async function buildListText(all) {
|
|
3148
|
-
const { boxes, projectRoot, scoped: scoped2 } = await scopedBoxes(all);
|
|
3149
|
-
if (boxes.length === 0) {
|
|
3150
|
-
if (scoped2) {
|
|
3151
|
-
return `no boxes in this project (${projectRoot}) \u2014 run \`agentbox create\`, or \`agentbox list --all\` to see all`;
|
|
3152
|
-
}
|
|
3153
|
-
return "no boxes \u2014 run `agentbox create` to make one";
|
|
3154
|
-
}
|
|
3155
|
-
return renderTable(boxes, process.stdout);
|
|
3156
|
-
}
|
|
3157
|
-
var listCommand2 = withWatchOptions(
|
|
3158
|
-
new Command9("list").alias("ls").description("List agent boxes in the current project (-a for all)").option("-j, --json", "machine-readable JSON output").option("-a, --all", "include boxes from all projects")
|
|
3159
|
-
).action(async (opts) => {
|
|
3160
|
-
if (opts.json && opts.watch) {
|
|
3161
|
-
log11.error("cannot combine --json with --watch");
|
|
3162
|
-
process.exit(2);
|
|
3163
|
-
}
|
|
3164
|
-
const all = opts.all ?? false;
|
|
3165
|
-
if (opts.watch) {
|
|
3166
|
-
await watchRender(() => buildListText(all), opts.interval);
|
|
3167
|
-
return;
|
|
3168
|
-
}
|
|
3169
|
-
if (opts.json) {
|
|
3170
|
-
const { boxes } = await scopedBoxes(all);
|
|
3171
|
-
process.stdout.write(JSON.stringify(boxes, null, 2) + "\n");
|
|
3172
|
-
return;
|
|
3173
|
-
}
|
|
3174
|
-
process.stdout.write(await buildListText(all) + "\n");
|
|
3175
|
-
});
|
|
3176
|
-
|
|
3177
|
-
// src/commands/logs.ts
|
|
3178
|
-
import { log as log12 } from "@clack/prompts";
|
|
3179
|
-
import { Command as Command10 } from "commander";
|
|
3180
|
-
import { spawn as spawn3 } from "child_process";
|
|
3181
|
-
var logsCommand = new Command10("logs").description("Print recent log lines from a box service; -f to stream").argument(
|
|
3182
|
-
"[box]",
|
|
3183
|
-
"box ref (optional when cwd has exactly 1 box): project index, id, id prefix, name, or container"
|
|
3184
|
-
).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) => {
|
|
3185
|
-
try {
|
|
3186
|
-
let idOrName;
|
|
3187
|
-
let service;
|
|
3188
|
-
if (serviceArg !== void 0) {
|
|
3189
|
-
idOrName = boxArg;
|
|
3190
|
-
service = serviceArg;
|
|
3191
|
-
} else {
|
|
3192
|
-
idOrName = void 0;
|
|
3193
|
-
service = boxArg;
|
|
3194
|
-
}
|
|
3195
|
-
if (!service) {
|
|
3196
|
-
log12.error("missing <service> argument");
|
|
3197
|
-
log12.info("usage: agentbox logs [box] <service> [-n N] [-f]");
|
|
3198
|
-
process.exit(2);
|
|
3199
|
-
}
|
|
3200
|
-
const box = await resolveBoxOrExit(idOrName);
|
|
3201
|
-
const tail = String(Number.parseInt(opts.tail, 10) || 200);
|
|
3202
|
-
const args = ["agentbox-ctl", "logs", service, "--tail", tail];
|
|
3203
|
-
if (opts.follow) args.push("--follow");
|
|
3204
|
-
if (!opts.follow) {
|
|
3205
|
-
const proc = await execInBox(box.container, args, { user: "vscode" });
|
|
3206
|
-
if (proc.exitCode !== 0) {
|
|
3207
|
-
log12.error(`agentbox-ctl logs failed: ${proc.stderr || proc.stdout}`);
|
|
3208
|
-
process.exit(1);
|
|
3209
|
-
}
|
|
3210
|
-
process.stdout.write(proc.stdout);
|
|
3211
|
-
if (!proc.stdout.endsWith("\n")) process.stdout.write("\n");
|
|
3212
|
-
return;
|
|
3213
|
-
}
|
|
3214
|
-
const child = spawn3("docker", ["exec", "--user", "vscode", box.container, ...args], {
|
|
3215
|
-
stdio: ["ignore", "inherit", "inherit"]
|
|
3216
|
-
});
|
|
3217
|
-
child.on("exit", (code) => process.exit(code ?? 0));
|
|
3218
|
-
} catch (err) {
|
|
3219
|
-
handleLifecycleError(err);
|
|
3220
|
-
}
|
|
3221
|
-
});
|
|
3222
|
-
|
|
3223
|
-
// src/commands/open.ts
|
|
3224
|
-
import { log as log13 } from "@clack/prompts";
|
|
3225
|
-
import { Command as Command11 } from "commander";
|
|
3226
|
-
|
|
3227
|
-
// src/commands/path.ts
|
|
3228
|
-
async function runPath(box, opts) {
|
|
3229
|
-
try {
|
|
3230
|
-
const layer = opts.upper ? "upper" : "merged";
|
|
3231
|
-
const { record, paths } = await getBoxHostPaths(box.id);
|
|
3232
|
-
if (opts.refresh) {
|
|
3233
|
-
const refreshed = await refreshExport(record, {
|
|
3234
|
-
layer,
|
|
3235
|
-
includeNodeModules: opts.includeNodeModules
|
|
3236
|
-
});
|
|
3237
|
-
process.stdout.write(`${refreshed.hostPath}
|
|
3238
|
-
`);
|
|
3239
|
-
return;
|
|
3240
|
-
}
|
|
3241
|
-
const path = layer === "upper" ? paths.upperLiveOnHost ?? paths.upperExport : paths.mergedExport;
|
|
3242
|
-
process.stdout.write(`${path}
|
|
3243
|
-
`);
|
|
3244
|
-
} catch (err) {
|
|
3245
|
-
handleLifecycleError(err);
|
|
3246
|
-
}
|
|
3247
|
-
}
|
|
3248
|
-
|
|
3249
|
-
// src/commands/open.ts
|
|
3250
|
-
var openCommand = new Command11("open").description("Open a box's merged workspace in Finder (snapshot of the agent's view)").argument(
|
|
3251
|
-
"[box]",
|
|
3252
|
-
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
3253
|
-
).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(
|
|
3254
|
-
"--include-node-modules",
|
|
3255
|
-
"include /workspace/node_modules in the merged export (off by default)"
|
|
3256
|
-
).option("--path", "print the host workspace path instead of launching Finder").option("--print", "alias of --path").action(async (idOrName, opts) => {
|
|
3257
|
-
try {
|
|
3258
|
-
const box = await resolveBoxOrExit(idOrName);
|
|
3259
|
-
if (opts.path || opts.print) {
|
|
3260
|
-
await runPath(box, {
|
|
3261
|
-
upper: opts.upper,
|
|
3262
|
-
refresh: opts.refresh,
|
|
3263
|
-
// print refreshes by default; --no-refresh skips
|
|
3264
|
-
includeNodeModules: opts.includeNodeModules
|
|
3265
|
-
});
|
|
3266
|
-
return;
|
|
3267
|
-
}
|
|
3268
|
-
const layer = opts.upper ? "upper" : "merged";
|
|
3269
|
-
const result = await openBoxInFinder(box.id, {
|
|
3270
|
-
layer,
|
|
3271
|
-
includeNodeModules: opts.includeNodeModules,
|
|
3272
|
-
noRefresh: !opts.refresh,
|
|
3273
|
-
noOpen: false
|
|
3274
|
-
});
|
|
3275
|
-
const liveNote = !result.copied ? " (live)" : result.usedFallback ? " (tar fallback)" : "";
|
|
3276
|
-
process.stdout.write(`opened ${result.hostPath}${liveNote}
|
|
3277
|
-
`);
|
|
3278
|
-
if (opts.upper && result.engine !== "orbstack" && result.copied) {
|
|
3279
|
-
log13.info(
|
|
3280
|
-
"Tip: live upper-layer browsing requires OrbStack. Re-run `agentbox open --upper` to refresh."
|
|
3281
|
-
);
|
|
3282
|
-
}
|
|
3283
|
-
} catch (err) {
|
|
3284
|
-
handleLifecycleError(err);
|
|
3285
|
-
}
|
|
3286
|
-
});
|
|
3287
|
-
|
|
3288
|
-
// src/commands/pause.ts
|
|
3289
|
-
import { Command as Command12 } from "commander";
|
|
3290
|
-
var pauseCommand = new Command12("pause").description("Freeze a box (docker pause \u2014 0 CPU, RAM stays mapped)").argument(
|
|
3291
|
-
"[box]",
|
|
3292
|
-
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
3293
|
-
).action(async (idOrName) => {
|
|
3294
|
-
try {
|
|
3295
|
-
const box = await resolveBoxOrExit(idOrName);
|
|
3296
|
-
const record = await pauseBox(box.id);
|
|
3297
|
-
process.stdout.write(`paused ${record.container}
|
|
3298
|
-
`);
|
|
3299
|
-
} catch (err) {
|
|
3300
|
-
handleLifecycleError(err);
|
|
3301
|
-
}
|
|
3302
|
-
});
|
|
3303
|
-
|
|
3304
|
-
// src/commands/prune.ts
|
|
3305
|
-
import { confirm as confirm5, isCancel as isCancel5, log as log14 } from "@clack/prompts";
|
|
3306
|
-
import { Command as Command13 } from "commander";
|
|
3307
|
-
function totalRemovals(r, projectConfigs) {
|
|
3308
|
-
return r.removedRecords.length + r.removedContainers.length + r.removedVolumes.length + r.removedSnapshotDirs.length + r.removedBoxDirs.length + projectConfigs.length;
|
|
3309
|
-
}
|
|
3310
|
-
function summary(r, projectConfigs) {
|
|
3311
|
-
const lines = [];
|
|
3312
|
-
if (r.removedRecords.length > 0) {
|
|
3313
|
-
lines.push(
|
|
3314
|
-
` state records (${String(r.removedRecords.length)}): ${r.removedRecords.join(", ")}`
|
|
3315
|
-
);
|
|
3316
|
-
}
|
|
3317
|
-
if (r.removedContainers.length > 0) {
|
|
3318
|
-
lines.push(
|
|
3319
|
-
` containers (${String(r.removedContainers.length)}): ${r.removedContainers.join(", ")}`
|
|
3320
|
-
);
|
|
3321
|
-
}
|
|
3322
|
-
if (r.removedVolumes.length > 0) {
|
|
3323
|
-
lines.push(
|
|
3324
|
-
` volumes (${String(r.removedVolumes.length)}): ${r.removedVolumes.join(", ")}`
|
|
3325
|
-
);
|
|
3326
|
-
}
|
|
3327
|
-
if (r.removedSnapshotDirs.length > 0) {
|
|
3328
|
-
lines.push(
|
|
3329
|
-
` snapshot dirs (${String(r.removedSnapshotDirs.length)}): ${r.removedSnapshotDirs.join(", ")}`
|
|
3330
|
-
);
|
|
3331
|
-
}
|
|
3332
|
-
if (r.removedBoxDirs.length > 0) {
|
|
3333
|
-
lines.push(
|
|
3334
|
-
` box dirs (${String(r.removedBoxDirs.length)}): ${r.removedBoxDirs.join(", ")}`
|
|
3335
|
-
);
|
|
3336
|
-
}
|
|
3337
|
-
if (projectConfigs.length > 0) {
|
|
3338
|
-
lines.push(
|
|
3339
|
-
` project configs (${String(projectConfigs.length)}): ${projectConfigs.join(", ")}`
|
|
3340
|
-
);
|
|
3341
|
-
}
|
|
3342
|
-
return lines.length > 0 ? lines.join("\n") : " (nothing to remove)";
|
|
4498
|
+
).join("\n");
|
|
3343
4499
|
}
|
|
3344
|
-
async function
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
return [];
|
|
3350
|
-
}
|
|
4500
|
+
async function scopedBoxes(all) {
|
|
4501
|
+
const boxes = await listBoxes();
|
|
4502
|
+
if (all) return { boxes, projectRoot: "", scoped: false };
|
|
4503
|
+
const { root } = await findProjectRoot(process.cwd());
|
|
4504
|
+
return { boxes: boxes.filter((b) => b.projectRoot === root), projectRoot: root, scoped: true };
|
|
3351
4505
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
const dryRun = opts.dryRun ?? false;
|
|
3358
|
-
const protectedPaths = opts.all ? await liveProjectRoots() : [];
|
|
3359
|
-
const preview = await pruneBoxes({ dryRun: true, all: opts.all });
|
|
3360
|
-
const previewProjects = opts.all ? (await pruneOrphanProjectConfigs({ dryRun: true, protectedPaths })).removed.map(
|
|
3361
|
-
(r) => r.originalPath
|
|
3362
|
-
) : [];
|
|
3363
|
-
if (totalRemovals(preview, previewProjects) === 0) {
|
|
3364
|
-
process.stdout.write("nothing to prune\n");
|
|
3365
|
-
return;
|
|
3366
|
-
}
|
|
3367
|
-
log14.info(`would remove:
|
|
3368
|
-
${summary(preview, previewProjects)}`);
|
|
3369
|
-
if (dryRun) return;
|
|
3370
|
-
if (!opts.yes) {
|
|
3371
|
-
const ok = await confirm5({ message: "Proceed with prune?", initialValue: true });
|
|
3372
|
-
if (isCancel5(ok) || !ok) {
|
|
3373
|
-
log14.info("cancelled");
|
|
3374
|
-
return;
|
|
3375
|
-
}
|
|
4506
|
+
async function buildListText(all) {
|
|
4507
|
+
const { boxes, projectRoot, scoped: scoped2 } = await scopedBoxes(all);
|
|
4508
|
+
if (boxes.length === 0) {
|
|
4509
|
+
if (scoped2) {
|
|
4510
|
+
return `no boxes in this project (${projectRoot}) \u2014 run \`agentbox create\`, or \`agentbox list --all\` to see all`;
|
|
3376
4511
|
}
|
|
3377
|
-
|
|
3378
|
-
const removedProjects = opts.all ? (await pruneOrphanProjectConfigs({ protectedPaths })).removed.map((r) => r.originalPath) : [];
|
|
3379
|
-
process.stdout.write(`pruned:
|
|
3380
|
-
${summary(result, removedProjects)}
|
|
3381
|
-
`);
|
|
3382
|
-
} catch (err) {
|
|
3383
|
-
handleLifecycleError(err);
|
|
4512
|
+
return "no boxes \u2014 run `agentbox create` to make one";
|
|
3384
4513
|
}
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
// src/commands/pull-claude.ts
|
|
3392
|
-
import { confirm as confirm6, isCancel as isCancel6, log as log15 } from "@clack/prompts";
|
|
3393
|
-
import { Command as Command14 } from "commander";
|
|
3394
|
-
function tag(item) {
|
|
3395
|
-
const noun = item.category === "plugins" ? "plugin" : item.category.replace(/s$/, "");
|
|
3396
|
-
return ` ${item.category}/${item.name} (new ${noun})`;
|
|
4514
|
+
const table = renderTable(boxes, process.stdout);
|
|
4515
|
+
if (!scoped2) return table;
|
|
4516
|
+
const name = projectRoot.split("/").filter(Boolean).pop() ?? projectRoot;
|
|
4517
|
+
return `Project: ${name}
|
|
4518
|
+
${table}`;
|
|
3397
4519
|
}
|
|
3398
|
-
var
|
|
3399
|
-
"
|
|
3400
|
-
).
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
}
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
if (preview.newItems.length === 0 && preview.mergedRegistries.length === 0) {
|
|
3415
|
-
process.stdout.write("no new Claude extensions to pull into ~/.claude\n");
|
|
3416
|
-
return;
|
|
3417
|
-
}
|
|
3418
|
-
for (const item of preview.newItems) process.stdout.write(`${tag(item)}
|
|
3419
|
-
`);
|
|
3420
|
-
for (const reg of preview.mergedRegistries) {
|
|
3421
|
-
process.stdout.write(` plugins/${reg} (merge new entries)
|
|
3422
|
-
`);
|
|
3423
|
-
}
|
|
3424
|
-
if (opts.dryRun) {
|
|
3425
|
-
process.stdout.write(
|
|
3426
|
-
`
|
|
3427
|
-
[dry-run] ${preview.newItems.length} item(s)${preview.mergedRegistries.length > 0 ? ` + ${preview.mergedRegistries.length} registry merge(s)` : ""} would be pulled into ~/.claude
|
|
3428
|
-
`
|
|
3429
|
-
);
|
|
3430
|
-
return;
|
|
3431
|
-
}
|
|
3432
|
-
if (!opts.yes) {
|
|
3433
|
-
const ok = await confirm6({
|
|
3434
|
-
message: `Pull ${preview.newItems.length} new Claude extension(s) into ~/.claude? (existing items are never overwritten)`,
|
|
3435
|
-
initialValue: false
|
|
3436
|
-
});
|
|
3437
|
-
if (isCancel6(ok) || !ok) {
|
|
3438
|
-
log15.info("cancelled");
|
|
3439
|
-
return;
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
const result = await pullClaudeExtras({ volume }, { image, dryRun: false });
|
|
3443
|
-
process.stdout.write(
|
|
3444
|
-
`pulled ${result.newItems.length} extension(s)${result.mergedRegistries.length > 0 ? `, merged ${result.mergedRegistries.join(", ")}` : ""} into ~/.claude
|
|
3445
|
-
`
|
|
3446
|
-
);
|
|
3447
|
-
} catch (err) {
|
|
3448
|
-
handleLifecycleError(err);
|
|
4520
|
+
var listCommand2 = withWatchOptions(
|
|
4521
|
+
new Command14("list").alias("ls").description("List agent boxes in the current project (-a for all)").option("-j, --json", "machine-readable JSON output").option("-a, --all", "include boxes from all projects")
|
|
4522
|
+
).action(async (opts) => {
|
|
4523
|
+
if (opts.json && opts.watch) {
|
|
4524
|
+
log16.error("cannot combine --json with --watch");
|
|
4525
|
+
process.exit(2);
|
|
4526
|
+
}
|
|
4527
|
+
const all = opts.all ?? false;
|
|
4528
|
+
if (opts.watch) {
|
|
4529
|
+
await watchRender(() => buildListText(all), opts.interval);
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
4532
|
+
if (opts.json) {
|
|
4533
|
+
const { boxes } = await scopedBoxes(all);
|
|
4534
|
+
process.stdout.write(JSON.stringify(boxes, null, 2) + "\n");
|
|
4535
|
+
return;
|
|
3449
4536
|
}
|
|
4537
|
+
process.stdout.write(await buildListText(all) + "\n");
|
|
3450
4538
|
});
|
|
3451
4539
|
|
|
3452
|
-
// src/commands/
|
|
3453
|
-
import {
|
|
4540
|
+
// src/commands/logs.ts
|
|
4541
|
+
import { log as log17 } from "@clack/prompts";
|
|
3454
4542
|
import { Command as Command15 } from "commander";
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
const code = sp === -1 ? line : line.slice(0, sp);
|
|
3458
|
-
const path = sp === -1 ? "" : line.slice(sp + 1);
|
|
3459
|
-
const isNew = /^>f\++$/.test(code);
|
|
3460
|
-
return ` ${path} ${isNew ? "(new)" : "(overwrites host)"}`;
|
|
3461
|
-
}
|
|
3462
|
-
var CONFIG_PATTERNS = ["agentbox.yaml"];
|
|
3463
|
-
var pullConfigCommand = new Command15("config").description("Pull agentbox.yaml box -> host").argument(
|
|
4543
|
+
import { spawn as spawn3 } from "child_process";
|
|
4544
|
+
var logsCommand = new Command15("logs").description("Print recent log lines from a box service; -f to stream").argument(
|
|
3464
4545
|
"[box]",
|
|
3465
|
-
"box ref: project index, id, id prefix, name, or container
|
|
3466
|
-
).
|
|
4546
|
+
"box ref (optional when cwd has exactly 1 box): project index, id, id prefix, name, or container"
|
|
4547
|
+
).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) => {
|
|
3467
4548
|
try {
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
if (
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
} else
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
} else if (insp.state === "missing") {
|
|
3477
|
-
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
3478
|
-
}
|
|
3479
|
-
log16.info(`agentbox.yaml bypasses gitignore and copies directly into ${box.workspacePath}`);
|
|
3480
|
-
const preview = await pullToHost(box, {
|
|
3481
|
-
dryRun: true,
|
|
3482
|
-
respectGitignore: false,
|
|
3483
|
-
envPatterns: CONFIG_PATTERNS,
|
|
3484
|
-
noRefresh: !opts.refresh
|
|
3485
|
-
});
|
|
3486
|
-
if (preview.changes.length === 0) {
|
|
3487
|
-
process.stdout.write(`no config file to pull into ${box.workspacePath}
|
|
3488
|
-
`);
|
|
3489
|
-
return;
|
|
4549
|
+
let idOrName;
|
|
4550
|
+
let service;
|
|
4551
|
+
if (serviceArg !== void 0) {
|
|
4552
|
+
idOrName = boxArg;
|
|
4553
|
+
service = serviceArg;
|
|
4554
|
+
} else {
|
|
4555
|
+
idOrName = void 0;
|
|
4556
|
+
service = boxArg;
|
|
3490
4557
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
process.
|
|
3495
|
-
`
|
|
3496
|
-
[dry-run] ${preview.changes.length} config file(s) would change in ${box.workspacePath}
|
|
3497
|
-
`
|
|
3498
|
-
);
|
|
3499
|
-
return;
|
|
4558
|
+
if (!service) {
|
|
4559
|
+
log17.error("missing <service> argument");
|
|
4560
|
+
log17.info("usage: agentbox logs [box] <service> [-n N] [-f]");
|
|
4561
|
+
process.exit(2);
|
|
3500
4562
|
}
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
4563
|
+
const box = await resolveBoxOrExit(idOrName);
|
|
4564
|
+
const tail = String(Number.parseInt(opts.tail, 10) || 200);
|
|
4565
|
+
const args = ["agentbox-ctl", "logs", service, "--tail", tail];
|
|
4566
|
+
if (opts.follow) args.push("--follow");
|
|
4567
|
+
if (!opts.follow) {
|
|
4568
|
+
const proc = await execInBox(box.container, args, { user: "vscode" });
|
|
4569
|
+
if (proc.exitCode !== 0) {
|
|
4570
|
+
log17.error(`agentbox-ctl logs failed: ${proc.stderr || proc.stdout}`);
|
|
4571
|
+
process.exit(1);
|
|
3509
4572
|
}
|
|
4573
|
+
process.stdout.write(proc.stdout);
|
|
4574
|
+
if (!proc.stdout.endsWith("\n")) process.stdout.write("\n");
|
|
4575
|
+
return;
|
|
3510
4576
|
}
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3513
|
-
respectGitignore: false,
|
|
3514
|
-
envPatterns: CONFIG_PATTERNS,
|
|
3515
|
-
// The dry-run pass above already refreshed (or intentionally skipped)
|
|
3516
|
-
// the scratch dir — don't rsync box->scratch a second time.
|
|
3517
|
-
noRefresh: true
|
|
4577
|
+
const child = spawn3("docker", ["exec", "--user", "vscode", box.container, ...args], {
|
|
4578
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
3518
4579
|
});
|
|
3519
|
-
process.
|
|
3520
|
-
`pulled ${result.changes.length} config file(s) into ${result.hostPath}
|
|
3521
|
-
`
|
|
3522
|
-
);
|
|
4580
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
3523
4581
|
} catch (err) {
|
|
3524
4582
|
handleLifecycleError(err);
|
|
3525
4583
|
}
|
|
3526
4584
|
});
|
|
3527
4585
|
|
|
3528
|
-
// src/commands/
|
|
3529
|
-
import { confirm as confirm8, isCancel as isCancel8, log as log17 } from "@clack/prompts";
|
|
4586
|
+
// src/commands/open.ts
|
|
3530
4587
|
import { Command as Command16 } from "commander";
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
const path = sp === -1 ? "" : line.slice(sp + 1);
|
|
3535
|
-
const isNew = /^>f\++$/.test(code);
|
|
3536
|
-
return ` ${path} ${isNew ? "(new)" : "(overwrites host)"}`;
|
|
3537
|
-
}
|
|
3538
|
-
var pullEnvCommand = new Command16("env").description(
|
|
3539
|
-
"Pull gitignored env/config files (.env*, .envrc, secrets.toml, agentbox.yaml, ...) box -> host"
|
|
3540
|
-
).argument(
|
|
3541
|
-
"[box]",
|
|
3542
|
-
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
3543
|
-
).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "list matched files and exit; don't write").option(
|
|
3544
|
-
"--pattern <glob>",
|
|
3545
|
-
"extra basename glob to match (repeatable, adds to defaults)",
|
|
3546
|
-
(v, acc) => [...acc, v],
|
|
3547
|
-
[]
|
|
3548
|
-
).option("--no-refresh", "skip the box->scratch-dir rsync step").action(async (idOrName, opts) => {
|
|
4588
|
+
|
|
4589
|
+
// src/commands/path.ts
|
|
4590
|
+
async function runPath(box, opts) {
|
|
3549
4591
|
try {
|
|
3550
|
-
const
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
log17.info("box is stopped; starting (remounting overlay)");
|
|
3557
|
-
await startBox(box.id);
|
|
3558
|
-
} else if (insp.state === "missing") {
|
|
3559
|
-
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
3560
|
-
}
|
|
3561
|
-
log17.info(
|
|
3562
|
-
`env/config files bypass gitignore and copy directly into ${box.workspacePath}`
|
|
3563
|
-
);
|
|
3564
|
-
const patterns = [...DEFAULT_ENV_PATTERNS, ...opts.pattern];
|
|
3565
|
-
const preview = await pullToHost(box, {
|
|
3566
|
-
dryRun: true,
|
|
3567
|
-
respectGitignore: false,
|
|
3568
|
-
envPatterns: patterns,
|
|
3569
|
-
noRefresh: !opts.refresh
|
|
3570
|
-
});
|
|
3571
|
-
if (preview.changes.length === 0) {
|
|
3572
|
-
process.stdout.write(`no env/config files to pull into ${box.workspacePath}
|
|
3573
|
-
`);
|
|
3574
|
-
return;
|
|
3575
|
-
}
|
|
3576
|
-
for (const line of preview.changes) process.stdout.write(`${tagChange2(line)}
|
|
4592
|
+
const { record, paths } = await getBoxHostPaths(box.id);
|
|
4593
|
+
if (opts.refresh) {
|
|
4594
|
+
const refreshed = await refreshExport(record, {
|
|
4595
|
+
includeNodeModules: opts.includeNodeModules
|
|
4596
|
+
});
|
|
4597
|
+
process.stdout.write(`${refreshed.hostPath}
|
|
3577
4598
|
`);
|
|
3578
|
-
if (opts.dryRun) {
|
|
3579
|
-
process.stdout.write(
|
|
3580
|
-
`
|
|
3581
|
-
[dry-run] ${preview.changes.length} env/config file(s) would change in ${box.workspacePath}
|
|
3582
|
-
`
|
|
3583
|
-
);
|
|
3584
4599
|
return;
|
|
3585
4600
|
}
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
message: `Pull ${preview.changes.length} env/config file(s) into ${box.workspacePath}? (existing files will be overwritten)`,
|
|
3589
|
-
initialValue: false
|
|
3590
|
-
});
|
|
3591
|
-
if (isCancel8(ok) || !ok) {
|
|
3592
|
-
log17.info("cancelled");
|
|
3593
|
-
return;
|
|
3594
|
-
}
|
|
3595
|
-
}
|
|
3596
|
-
const result = await pullToHost(box, {
|
|
3597
|
-
dryRun: false,
|
|
3598
|
-
respectGitignore: false,
|
|
3599
|
-
envPatterns: patterns,
|
|
3600
|
-
// The dry-run pass above already refreshed (or intentionally skipped)
|
|
3601
|
-
// the scratch dir — don't rsync box->scratch a second time.
|
|
3602
|
-
noRefresh: true
|
|
3603
|
-
});
|
|
3604
|
-
process.stdout.write(
|
|
3605
|
-
`pulled ${result.changes.length} env/config file(s) into ${result.hostPath}
|
|
3606
|
-
`
|
|
3607
|
-
);
|
|
4601
|
+
process.stdout.write(`${paths.mergedExport}
|
|
4602
|
+
`);
|
|
3608
4603
|
} catch (err) {
|
|
3609
4604
|
handleLifecycleError(err);
|
|
3610
4605
|
}
|
|
3611
|
-
}
|
|
4606
|
+
}
|
|
3612
4607
|
|
|
3613
|
-
// src/commands/
|
|
3614
|
-
var
|
|
4608
|
+
// src/commands/open.ts
|
|
4609
|
+
var openCommand = new Command16("open").description("Open a box's /workspace in Finder (rsync'd snapshot of the agent's view)").argument(
|
|
3615
4610
|
"[box]",
|
|
3616
4611
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
3617
|
-
).option("-
|
|
3618
|
-
"--no-respect-gitignore",
|
|
3619
|
-
"disable git ls-files mode; use --exclude=node_modules,.git instead"
|
|
3620
|
-
).option(
|
|
4612
|
+
).option("--no-refresh", "skip the rsync; open whatever's already on disk").option(
|
|
3621
4613
|
"--include-node-modules",
|
|
3622
|
-
"
|
|
3623
|
-
).option("--
|
|
3624
|
-
"--with-env",
|
|
3625
|
-
"also pull env/config files (.env*, .envrc, secrets.toml, agentbox.yaml, ...) ignoring gitignore"
|
|
3626
|
-
).option(
|
|
3627
|
-
"--pattern <glob>",
|
|
3628
|
-
"extra env basename glob; only effective with --with-env (repeatable)",
|
|
3629
|
-
(v, acc) => [...acc, v],
|
|
3630
|
-
[]
|
|
3631
|
-
).action(async (idOrName, opts) => {
|
|
4614
|
+
"include /workspace/node_modules in the merged export (off by default)"
|
|
4615
|
+
).option("--path", "print the host workspace path instead of launching Finder").option("--print", "alias of --path").action(async (idOrName, opts) => {
|
|
3632
4616
|
try {
|
|
3633
4617
|
const box = await resolveBoxOrExit(idOrName);
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
} else if (insp.state === "missing") {
|
|
3642
|
-
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
3643
|
-
}
|
|
3644
|
-
const rootWorktree = box.gitWorktrees?.find((w) => w.kind === "root");
|
|
3645
|
-
if (rootWorktree) {
|
|
3646
|
-
log18.warn(
|
|
3647
|
-
`This box has been committing to branch \`${rootWorktree.branch}\` in a separate worktree.
|
|
3648
|
-
For a git-aware merge instead of a file copy, run from your checkout:
|
|
3649
|
-
git merge ${rootWorktree.branch}
|
|
3650
|
-
Continuing with rsync into ${box.workspacePath}`
|
|
3651
|
-
);
|
|
4618
|
+
if (opts.path || opts.print) {
|
|
4619
|
+
await runPath(box, {
|
|
4620
|
+
refresh: opts.refresh,
|
|
4621
|
+
// print refreshes by default; --no-refresh skips
|
|
4622
|
+
includeNodeModules: opts.includeNodeModules
|
|
4623
|
+
});
|
|
4624
|
+
return;
|
|
3652
4625
|
}
|
|
3653
|
-
const
|
|
3654
|
-
const preview = await pullToHost(box, {
|
|
3655
|
-
dryRun: true,
|
|
3656
|
-
respectGitignore: opts.respectGitignore,
|
|
4626
|
+
const result = await openBoxInFinder(box.id, {
|
|
3657
4627
|
includeNodeModules: opts.includeNodeModules,
|
|
3658
|
-
|
|
3659
|
-
|
|
4628
|
+
noRefresh: !opts.refresh,
|
|
4629
|
+
noOpen: false
|
|
3660
4630
|
});
|
|
3661
|
-
|
|
3662
|
-
|
|
4631
|
+
const liveNote = !result.copied ? " (live)" : result.usedFallback ? " (tar fallback)" : "";
|
|
4632
|
+
process.stdout.write(`opened ${result.hostPath}${liveNote}
|
|
3663
4633
|
`);
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
4634
|
+
} catch (err) {
|
|
4635
|
+
handleLifecycleError(err);
|
|
4636
|
+
}
|
|
4637
|
+
});
|
|
4638
|
+
|
|
4639
|
+
// src/commands/pause.ts
|
|
4640
|
+
import { Command as Command17 } from "commander";
|
|
4641
|
+
var pauseCommand = new Command17("pause").description("Freeze a box (docker pause \u2014 0 CPU, RAM stays mapped)").argument(
|
|
4642
|
+
"[box]",
|
|
4643
|
+
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
4644
|
+
).action(async (idOrName) => {
|
|
4645
|
+
try {
|
|
4646
|
+
const box = await resolveBoxOrExit(idOrName);
|
|
4647
|
+
const record = await pauseBox(box.id);
|
|
4648
|
+
process.stdout.write(`paused ${record.container}
|
|
3668
4649
|
`);
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
4650
|
+
} catch (err) {
|
|
4651
|
+
handleLifecycleError(err);
|
|
4652
|
+
}
|
|
4653
|
+
});
|
|
4654
|
+
|
|
4655
|
+
// src/commands/prune.ts
|
|
4656
|
+
import { confirm as confirm9, isCancel as isCancel9, log as log18 } from "@clack/prompts";
|
|
4657
|
+
import { Command as Command18 } from "commander";
|
|
4658
|
+
function totalRemovals(r, projectConfigs) {
|
|
4659
|
+
return r.removedRecords.length + r.removedContainers.length + r.removedVolumes.length + r.removedSnapshotDirs.length + r.removedBoxDirs.length + projectConfigs.length;
|
|
4660
|
+
}
|
|
4661
|
+
function summary(r, projectConfigs) {
|
|
4662
|
+
const lines = [];
|
|
4663
|
+
if (r.removedRecords.length > 0) {
|
|
4664
|
+
lines.push(
|
|
4665
|
+
` state records (${String(r.removedRecords.length)}): ${r.removedRecords.join(", ")}`
|
|
4666
|
+
);
|
|
4667
|
+
}
|
|
4668
|
+
if (r.removedContainers.length > 0) {
|
|
4669
|
+
lines.push(
|
|
4670
|
+
` containers (${String(r.removedContainers.length)}): ${r.removedContainers.join(", ")}`
|
|
4671
|
+
);
|
|
4672
|
+
}
|
|
4673
|
+
if (r.removedVolumes.length > 0) {
|
|
4674
|
+
lines.push(
|
|
4675
|
+
` volumes (${String(r.removedVolumes.length)}): ${r.removedVolumes.join(", ")}`
|
|
4676
|
+
);
|
|
4677
|
+
}
|
|
4678
|
+
if (r.removedSnapshotDirs.length > 0) {
|
|
4679
|
+
lines.push(
|
|
4680
|
+
` snapshot dirs (${String(r.removedSnapshotDirs.length)}): ${r.removedSnapshotDirs.join(", ")}`
|
|
4681
|
+
);
|
|
4682
|
+
}
|
|
4683
|
+
if (r.removedBoxDirs.length > 0) {
|
|
4684
|
+
lines.push(
|
|
4685
|
+
` box dirs (${String(r.removedBoxDirs.length)}): ${r.removedBoxDirs.join(", ")}`
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4688
|
+
if (projectConfigs.length > 0) {
|
|
4689
|
+
lines.push(
|
|
4690
|
+
` project configs (${String(projectConfigs.length)}): ${projectConfigs.join(", ")}`
|
|
4691
|
+
);
|
|
4692
|
+
}
|
|
4693
|
+
return lines.length > 0 ? lines.join("\n") : " (nothing to remove)";
|
|
4694
|
+
}
|
|
4695
|
+
async function liveProjectRoots() {
|
|
4696
|
+
try {
|
|
4697
|
+
const boxes = await listBoxes();
|
|
4698
|
+
return boxes.map((b) => b.projectRoot).filter((p) => typeof p === "string");
|
|
4699
|
+
} catch {
|
|
4700
|
+
return [];
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
var pruneCommand = new Command18("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(
|
|
4704
|
+
"--all",
|
|
4705
|
+
"also remove orphan agentbox-* containers, volumes, snapshot dirs, and orphan per-project config dirs"
|
|
4706
|
+
).option("-y, --yes", "skip the confirmation prompt").action(async (opts) => {
|
|
4707
|
+
try {
|
|
4708
|
+
const dryRun = opts.dryRun ?? false;
|
|
4709
|
+
const protectedPaths = opts.all ? await liveProjectRoots() : [];
|
|
4710
|
+
const preview = await pruneBoxes({ dryRun: true, all: opts.all });
|
|
4711
|
+
const previewProjects = opts.all ? (await pruneOrphanProjectConfigs({ dryRun: true, protectedPaths })).removed.map(
|
|
4712
|
+
(r) => r.originalPath
|
|
4713
|
+
) : [];
|
|
4714
|
+
if (totalRemovals(preview, previewProjects) === 0) {
|
|
4715
|
+
process.stdout.write("nothing to prune\n");
|
|
3674
4716
|
return;
|
|
3675
4717
|
}
|
|
4718
|
+
log18.info(`would remove:
|
|
4719
|
+
${summary(preview, previewProjects)}`);
|
|
4720
|
+
if (dryRun) return;
|
|
3676
4721
|
if (!opts.yes) {
|
|
3677
|
-
const ok = await confirm9({
|
|
3678
|
-
message: `Pull ${preview.changes.length} changed file(s)${opts.withEnv ? " (incl. env/config)" : ""} into ${box.workspacePath}?`,
|
|
3679
|
-
initialValue: false
|
|
3680
|
-
});
|
|
4722
|
+
const ok = await confirm9({ message: "Proceed with prune?", initialValue: true });
|
|
3681
4723
|
if (isCancel9(ok) || !ok) {
|
|
3682
4724
|
log18.info("cancelled");
|
|
3683
4725
|
return;
|
|
3684
4726
|
}
|
|
3685
4727
|
}
|
|
3686
|
-
const result = await
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
4728
|
+
const result = await pruneBoxes({ all: opts.all });
|
|
4729
|
+
const removedProjects = opts.all ? (await pruneOrphanProjectConfigs({ protectedPaths })).removed.map((r) => r.originalPath) : [];
|
|
4730
|
+
process.stdout.write(`pruned:
|
|
4731
|
+
${summary(result, removedProjects)}
|
|
4732
|
+
`);
|
|
4733
|
+
} catch (err) {
|
|
4734
|
+
handleLifecycleError(err);
|
|
4735
|
+
}
|
|
4736
|
+
});
|
|
4737
|
+
|
|
4738
|
+
// src/commands/relay.ts
|
|
4739
|
+
import { log as log19, spinner as spinner3 } from "@clack/prompts";
|
|
4740
|
+
import { Command as Command19 } from "commander";
|
|
4741
|
+
function renderStatus(s) {
|
|
4742
|
+
if (s.running && s.health) {
|
|
4743
|
+
return [
|
|
4744
|
+
"relay: running",
|
|
4745
|
+
` pid: ${s.pid === null ? "?" : String(s.pid)}`,
|
|
4746
|
+
` port: ${String(s.port)}`,
|
|
4747
|
+
` url: ${s.endpoint.hostUrl}`,
|
|
4748
|
+
` boxes: ${String(s.health.boxes)}`,
|
|
4749
|
+
` events: ${String(s.health.events)}`,
|
|
4750
|
+
` log: ${s.logFile}`
|
|
4751
|
+
].join("\n");
|
|
4752
|
+
}
|
|
4753
|
+
if (s.pidAlive) {
|
|
4754
|
+
return [
|
|
4755
|
+
`relay: not responding (pid ${String(s.pid)} alive but /healthz silent)`,
|
|
4756
|
+
` log: ${s.logFile}`
|
|
4757
|
+
].join("\n");
|
|
4758
|
+
}
|
|
4759
|
+
return ["relay: not running", ` log: ${s.logFile}`].join("\n");
|
|
4760
|
+
}
|
|
4761
|
+
var statusSub = new Command19("status").description("Show whether the host relay is running, with pid / port / box count").option("--json", "emit RelayStatus as JSON").action(async (opts) => {
|
|
4762
|
+
try {
|
|
4763
|
+
const s = await getRelayStatus();
|
|
4764
|
+
if (opts.json) {
|
|
4765
|
+
process.stdout.write(JSON.stringify(s, null, 2) + "\n");
|
|
4766
|
+
return;
|
|
4767
|
+
}
|
|
4768
|
+
process.stdout.write(renderStatus(s) + "\n");
|
|
4769
|
+
} catch (err) {
|
|
4770
|
+
handleLifecycleError(err);
|
|
4771
|
+
}
|
|
4772
|
+
});
|
|
4773
|
+
var stopSub = new Command19("stop").description("Stop the host relay process (idempotent)").action(async () => {
|
|
4774
|
+
try {
|
|
4775
|
+
const s = spinner3();
|
|
4776
|
+
s.start("stopping relay");
|
|
4777
|
+
const result = await stopRelay();
|
|
4778
|
+
s.stop(
|
|
4779
|
+
result.stopped ? `stopped relay (pid ${String(result.pid)})` : "relay was not running"
|
|
4780
|
+
);
|
|
4781
|
+
} catch (err) {
|
|
4782
|
+
handleLifecycleError(err);
|
|
4783
|
+
}
|
|
4784
|
+
});
|
|
4785
|
+
var startSub = new Command19("start").description("Start the host relay if not already running (idempotent)").action(async () => {
|
|
4786
|
+
try {
|
|
4787
|
+
const s = spinner3();
|
|
4788
|
+
s.start("starting relay");
|
|
4789
|
+
const ep = await ensureRelay();
|
|
4790
|
+
s.stop(`relay running on ${ep.hostUrl}`);
|
|
4791
|
+
} catch (err) {
|
|
4792
|
+
handleLifecycleError(err);
|
|
4793
|
+
}
|
|
4794
|
+
});
|
|
4795
|
+
var restartSub = new Command19("restart").description("Stop then start the host relay").action(async () => {
|
|
4796
|
+
try {
|
|
4797
|
+
const s = spinner3();
|
|
4798
|
+
s.start("stopping relay");
|
|
4799
|
+
const stopped = await stopRelay();
|
|
4800
|
+
s.stop(
|
|
4801
|
+
stopped.stopped ? `stopped relay (pid ${String(stopped.pid)})` : "relay was not running"
|
|
3698
4802
|
);
|
|
4803
|
+
const s2 = spinner3();
|
|
4804
|
+
s2.start("starting relay");
|
|
4805
|
+
try {
|
|
4806
|
+
const ep = await ensureRelay();
|
|
4807
|
+
s2.stop(`relay running on ${ep.hostUrl}`);
|
|
4808
|
+
} catch (err) {
|
|
4809
|
+
s2.stop("relay start failed");
|
|
4810
|
+
log19.warn(err instanceof Error ? err.message : String(err));
|
|
4811
|
+
throw err;
|
|
4812
|
+
}
|
|
3699
4813
|
} catch (err) {
|
|
3700
4814
|
handleLifecycleError(err);
|
|
3701
4815
|
}
|
|
3702
4816
|
});
|
|
3703
|
-
|
|
3704
|
-
pullCommand.addCommand(pullClaudeCommand);
|
|
3705
|
-
pullCommand.addCommand(pullConfigCommand);
|
|
4817
|
+
var relayCommand = new Command19("relay").description("Manage the host relay process (status / stop / start / restart)").addCommand(statusSub, { isDefault: true }).addCommand(stopSub).addCommand(startSub).addCommand(restartSub);
|
|
3706
4818
|
|
|
3707
4819
|
// src/commands/screen.ts
|
|
3708
4820
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
3709
|
-
import { log as
|
|
3710
|
-
import { Command as
|
|
3711
|
-
var screenCommand = new
|
|
4821
|
+
import { log as log20 } from "@clack/prompts";
|
|
4822
|
+
import { Command as Command20 } from "commander";
|
|
4823
|
+
var screenCommand = new Command20("screen").description("Open a box's VNC (noVNC) viewer in the browser (auto-unpause/start)").argument(
|
|
3712
4824
|
"[box]",
|
|
3713
4825
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
3714
4826
|
).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) => {
|
|
@@ -3719,17 +4831,27 @@ var screenCommand = new Command18("screen").description("Open a box's VNC (noVNC
|
|
|
3719
4831
|
}
|
|
3720
4832
|
const insp = await inspectBox(box.id);
|
|
3721
4833
|
if (insp.state === "paused") {
|
|
3722
|
-
|
|
4834
|
+
log20.info("box is paused; unpausing");
|
|
3723
4835
|
await unpauseBox(box.id);
|
|
3724
4836
|
} else if (insp.state === "stopped") {
|
|
3725
|
-
|
|
4837
|
+
log20.info("box is stopped; starting");
|
|
3726
4838
|
await startBox(box.id);
|
|
3727
4839
|
} else if (insp.state === "missing") {
|
|
3728
4840
|
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
3729
4841
|
}
|
|
3730
|
-
const
|
|
3731
|
-
|
|
3732
|
-
|
|
4842
|
+
const persisted = await readBoxStatus(box);
|
|
4843
|
+
const exposePort = persisted?.services.find((s) => s.expose)?.expose?.port;
|
|
4844
|
+
const inBoxUrl = exposePort !== void 0 ? `http://localhost:${String(exposePort)}` : "about:blank";
|
|
4845
|
+
const br = await ensureBoxBrowser(box.container, void 0, inBoxUrl);
|
|
4846
|
+
if (br.up && !br.alreadyRunning) {
|
|
4847
|
+
log20.info(
|
|
4848
|
+
exposePort !== void 0 ? `opened ${inBoxUrl} in the in-box browser (visible in the VNC view)` : "started in-box browser"
|
|
4849
|
+
);
|
|
4850
|
+
} else if (br.alreadyRunning) {
|
|
4851
|
+
log20.info("in-box browser already running; left it untouched");
|
|
4852
|
+
} else {
|
|
4853
|
+
log20.warn(`could not start in-box browser: ${br.reason ?? "unknown"}`);
|
|
4854
|
+
}
|
|
3733
4855
|
const engine = await detectEngine();
|
|
3734
4856
|
const urls = buildVncUrls(box, engine);
|
|
3735
4857
|
const url = opts.loopback ? urls.loopbackUrl : urls.orbUrl ?? urls.loopbackUrl;
|
|
@@ -3749,21 +4871,6 @@ var screenCommand = new Command18("screen").description("Open a box's VNC (noVNC
|
|
|
3749
4871
|
}
|
|
3750
4872
|
process.stdout.write(`opened ${url}
|
|
3751
4873
|
`);
|
|
3752
|
-
try {
|
|
3753
|
-
const { record } = await getBoxHostPaths(box.id);
|
|
3754
|
-
const persisted = await readBoxStatus(box.id);
|
|
3755
|
-
const eps = await getBoxEndpoints(record, engine, persisted);
|
|
3756
|
-
const webEp = eps.endpoints.find((e) => e.kind === "web");
|
|
3757
|
-
if (webEp?.reachable && webEp.url) {
|
|
3758
|
-
const webUrl = engine === "orbstack" && !opts.loopback ? `http://${record.container}.orb.local` : webEp.url;
|
|
3759
|
-
const w = spawnSync5("open", [webUrl], { stdio: "inherit" });
|
|
3760
|
-
if (w.status === 0) process.stdout.write(`also opened ${webUrl}
|
|
3761
|
-
`);
|
|
3762
|
-
else log19.warn(`could not open web app (${webUrl})`);
|
|
3763
|
-
}
|
|
3764
|
-
} catch (e) {
|
|
3765
|
-
log19.warn(`could not open web app: ${e instanceof Error ? e.message : String(e)}`);
|
|
3766
|
-
}
|
|
3767
4874
|
} catch (err) {
|
|
3768
4875
|
handleLifecycleError(err);
|
|
3769
4876
|
}
|
|
@@ -3771,15 +4878,16 @@ var screenCommand = new Command18("screen").description("Open a box's VNC (noVNC
|
|
|
3771
4878
|
|
|
3772
4879
|
// src/commands/shell.ts
|
|
3773
4880
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
3774
|
-
import { log as
|
|
3775
|
-
import { Command as
|
|
4881
|
+
import { log as log21 } from "@clack/prompts";
|
|
4882
|
+
import { Command as Command21 } from "commander";
|
|
4883
|
+
var RELAY_HOST_URL3 = `http://127.0.0.1:${String(DEFAULT_RELAY_PORT)}`;
|
|
3776
4884
|
function buildShellCliOverrides(opts) {
|
|
3777
4885
|
const shell = {};
|
|
3778
4886
|
if (opts.user !== void 0) shell.user = opts.user;
|
|
3779
4887
|
if (opts.login === false) shell.login = false;
|
|
3780
4888
|
return Object.keys(shell).length > 0 ? { shell } : {};
|
|
3781
4889
|
}
|
|
3782
|
-
var shellCommand = new
|
|
4890
|
+
var shellCommand = new Command21("shell").description("Open an interactive bash shell in a box (auto-unpause/start)").argument(
|
|
3783
4891
|
"[box]",
|
|
3784
4892
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
3785
4893
|
).argument(
|
|
@@ -3796,10 +4904,10 @@ var shellCommand = new Command19("shell").description("Open an interactive bash
|
|
|
3796
4904
|
const login = cfg.effective.shell.login;
|
|
3797
4905
|
const insp = await inspectBox(box.id);
|
|
3798
4906
|
if (insp.state === "paused") {
|
|
3799
|
-
|
|
4907
|
+
log21.info("box is paused; unpausing");
|
|
3800
4908
|
await unpauseBox(box.id);
|
|
3801
4909
|
} else if (insp.state === "stopped") {
|
|
3802
|
-
|
|
4910
|
+
log21.info("box is stopped; starting");
|
|
3803
4911
|
await startBox(box.id);
|
|
3804
4912
|
} else if (insp.state === "missing") {
|
|
3805
4913
|
throw new Error(`box ${box.name} has no container; was it destroyed?`);
|
|
@@ -3808,59 +4916,57 @@ var shellCommand = new Command19("shell").description("Open an interactive bash
|
|
|
3808
4916
|
const bashArgs = [];
|
|
3809
4917
|
if (login) bashArgs.push("-l");
|
|
3810
4918
|
if (effectiveCmd.length > 0) bashArgs.push("-c", effectiveCmd.join(" "));
|
|
3811
|
-
const
|
|
3812
|
-
const
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
{ stdio: "inherit" }
|
|
3826
|
-
|
|
3827
|
-
|
|
4919
|
+
const isInteractive = process.stdout.isTTY && process.stdin.isTTY;
|
|
4920
|
+
const ttyFlag = isInteractive ? "-it" : "-i";
|
|
4921
|
+
const dockerArgv = [
|
|
4922
|
+
"exec",
|
|
4923
|
+
ttyFlag,
|
|
4924
|
+
"-e",
|
|
4925
|
+
`TERM=${term2}`,
|
|
4926
|
+
"--user",
|
|
4927
|
+
user,
|
|
4928
|
+
box.container,
|
|
4929
|
+
"bash",
|
|
4930
|
+
...bashArgs
|
|
4931
|
+
];
|
|
4932
|
+
if (!isInteractive || effectiveCmd.length > 0) {
|
|
4933
|
+
const child = spawnSync6("docker", dockerArgv, { stdio: "inherit" });
|
|
4934
|
+
process.exit(child.status ?? 0);
|
|
4935
|
+
}
|
|
4936
|
+
const code = await runWrappedAttach({
|
|
4937
|
+
container: box.container,
|
|
4938
|
+
dockerArgv,
|
|
4939
|
+
relayBaseUrl: RELAY_HOST_URL3,
|
|
4940
|
+
boxId: box.id,
|
|
4941
|
+
boxName: box.name,
|
|
4942
|
+
projectIndex: box.projectIndex,
|
|
4943
|
+
mode: "shell"
|
|
4944
|
+
});
|
|
4945
|
+
process.exit(code);
|
|
3828
4946
|
} catch (err) {
|
|
3829
4947
|
handleLifecycleError(err);
|
|
3830
4948
|
}
|
|
3831
4949
|
});
|
|
3832
4950
|
|
|
3833
4951
|
// src/commands/start.ts
|
|
3834
|
-
import { Command as
|
|
3835
|
-
var startCommand = new
|
|
4952
|
+
import { Command as Command22 } from "commander";
|
|
4953
|
+
var startCommand = new Command22("start").description("Start a stopped box (docker start + relaunch ctl/dockerd/vnc daemons)").argument(
|
|
3836
4954
|
"[box]",
|
|
3837
4955
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
3838
4956
|
).action(async (idOrName) => {
|
|
3839
4957
|
try {
|
|
3840
4958
|
const box = await resolveBoxOrExit(idOrName);
|
|
3841
|
-
const { record
|
|
4959
|
+
const { record } = await startBox(box.id);
|
|
3842
4960
|
process.stdout.write(`started ${record.container}
|
|
3843
4961
|
`);
|
|
3844
|
-
const failed = overlayChecks.filter((c) => !c.ok);
|
|
3845
|
-
if (failed.length > 0) {
|
|
3846
|
-
for (const c of failed) {
|
|
3847
|
-
process.stderr.write(` \u2717 ${c.name}: ${c.detail}
|
|
3848
|
-
`);
|
|
3849
|
-
}
|
|
3850
|
-
process.exit(1);
|
|
3851
|
-
}
|
|
3852
|
-
for (const c of overlayChecks) {
|
|
3853
|
-
process.stdout.write(` \u2713 ${c.name}
|
|
3854
|
-
`);
|
|
3855
|
-
}
|
|
3856
4962
|
} catch (err) {
|
|
3857
4963
|
handleLifecycleError(err);
|
|
3858
4964
|
}
|
|
3859
4965
|
});
|
|
3860
4966
|
|
|
3861
4967
|
// src/commands/status.ts
|
|
3862
|
-
import { log as
|
|
3863
|
-
import { Command as
|
|
4968
|
+
import { log as log23 } from "@clack/prompts";
|
|
4969
|
+
import { Command as Command23 } from "commander";
|
|
3864
4970
|
|
|
3865
4971
|
// src/endpoints-render.ts
|
|
3866
4972
|
function renderEndpointLines(endpoints, stream) {
|
|
@@ -3923,27 +5029,24 @@ function fmtAgo(iso) {
|
|
|
3923
5029
|
}
|
|
3924
5030
|
|
|
3925
5031
|
// src/commands/inspect.ts
|
|
3926
|
-
import { log as
|
|
5032
|
+
import { log as log22 } from "@clack/prompts";
|
|
3927
5033
|
function fmtLimit(n, unit) {
|
|
3928
5034
|
return n && n > 0 ? `${String(n)}${unit}` : "unlimited";
|
|
3929
5035
|
}
|
|
3930
5036
|
async function renderText(i) {
|
|
3931
5037
|
const lim = i.record.resourceLimits;
|
|
5038
|
+
const ckptName = i.record.checkpointSource?.ref;
|
|
3932
5039
|
const projectRoot = i.record.projectRoot ?? i.record.workspacePath;
|
|
3933
|
-
const ckptBytes = await
|
|
3934
|
-
const upperHost = i.hostPaths.upperLiveOnHost ? `${i.hostPaths.upperLiveOnHost} (live)` : `${i.hostPaths.upperExport} (run \`agentbox open --upper\` to refresh)`;
|
|
5040
|
+
const ckptBytes = ckptName ? await projectCheckpointImageBytes(projectRoot, ckptName) : null;
|
|
3935
5041
|
const lines = [
|
|
3936
5042
|
`id ${i.record.id}`,
|
|
3937
5043
|
`name ${i.record.name}`,
|
|
3938
5044
|
`container ${i.record.container}`,
|
|
3939
5045
|
`image ${i.record.image}`,
|
|
3940
5046
|
`state ${i.state}`,
|
|
3941
|
-
`
|
|
3942
|
-
`workspace ${i.record.workspacePath}`,
|
|
5047
|
+
`workspace ${i.record.workspacePath} (container fs at /workspace)`,
|
|
3943
5048
|
`project ${i.record.projectRoot ?? "(unset \u2014 pre-feature box)"}`,
|
|
3944
5049
|
`n ${typeof i.record.projectIndex === "number" ? String(i.record.projectIndex) : "(none)"}`,
|
|
3945
|
-
`lower ${i.record.lowerPath}`,
|
|
3946
|
-
`upper volume ${i.upperVolume.name}${i.upperVolume.mountpoint ? ` (${i.upperVolume.mountpoint})` : ""}`,
|
|
3947
5050
|
`claude config ${i.record.claudeConfigVolume ?? "(none)"}`,
|
|
3948
5051
|
`claude session ${renderClaudeSession(i)}`,
|
|
3949
5052
|
`claude activity ${renderClaudeActivity(i)}`,
|
|
@@ -3956,15 +5059,20 @@ async function renderText(i) {
|
|
|
3956
5059
|
`cpu limit ${fmtLimit(lim?.cpus, "")}`,
|
|
3957
5060
|
`pids limit ${fmtLimit(lim?.pidsLimit, "")}`,
|
|
3958
5061
|
`disk limit ${lim?.disk ? `${lim.disk} (best-effort; no-op on overlay2/macOS)` : "unlimited"}`,
|
|
3959
|
-
`snapshot dir ${i.record.snapshotDir ?? "(none
|
|
5062
|
+
`snapshot dir ${i.record.snapshotDir ?? "(none)"}`,
|
|
3960
5063
|
`snapshot size ${fmtBytes(i.snapshotSizeBytes)}`,
|
|
3961
|
-
`checkpoint
|
|
5064
|
+
`checkpoint ${renderCheckpoint(i, ckptBytes)}`,
|
|
3962
5065
|
`host export ${i.hostPaths.mergedExport} (run \`agentbox open\` to refresh)`,
|
|
3963
|
-
`upper host ${upperHost}`,
|
|
3964
5066
|
`created ${i.record.createdAt}`
|
|
3965
5067
|
];
|
|
3966
5068
|
return lines.join("\n");
|
|
3967
5069
|
}
|
|
5070
|
+
function renderCheckpoint(i, sizeBytes) {
|
|
5071
|
+
const src = i.record.checkpointSource;
|
|
5072
|
+
if (!src || !i.record.checkpointImage) return "(none)";
|
|
5073
|
+
const sizePart = sizeBytes !== null ? ` ${fmtBytes(sizeBytes)}` : "";
|
|
5074
|
+
return `${src.ref} (${src.type}, chain ${src.chain.length}) \u2192 ${i.record.checkpointImage}${sizePart}`;
|
|
5075
|
+
}
|
|
3968
5076
|
function renderClaudeSession(i) {
|
|
3969
5077
|
if (i.claudeSession === null) return "(n/a \u2014 box not running)";
|
|
3970
5078
|
if (!i.claudeSession.running) return `not running ("${i.claudeSession.sessionName}")`;
|
|
@@ -3988,7 +5096,7 @@ function renderEndpoints(i) {
|
|
|
3988
5096
|
async function runInspect(box, opts) {
|
|
3989
5097
|
try {
|
|
3990
5098
|
if (opts.json && opts.watch) {
|
|
3991
|
-
|
|
5099
|
+
log22.error("cannot combine --json with --watch");
|
|
3992
5100
|
process.exit(2);
|
|
3993
5101
|
}
|
|
3994
5102
|
if (opts.watch) {
|
|
@@ -4008,14 +5116,14 @@ async function runInspect(box, opts) {
|
|
|
4008
5116
|
|
|
4009
5117
|
// src/commands/status.ts
|
|
4010
5118
|
var statusCommand = withWatchOptions(
|
|
4011
|
-
new
|
|
5119
|
+
new Command23("status").description("Show service + task status from a box's agentbox-ctl daemon").argument(
|
|
4012
5120
|
"[box]",
|
|
4013
5121
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
4014
5122
|
).option("-j, --json", "machine-readable JSON output").option("--inspect", "show detailed box info (volumes, limits, paths) instead of service/task status")
|
|
4015
5123
|
).action(async (idOrName, opts) => {
|
|
4016
5124
|
try {
|
|
4017
5125
|
if (opts.json && opts.watch) {
|
|
4018
|
-
|
|
5126
|
+
log23.error("cannot combine --json with --watch");
|
|
4019
5127
|
process.exit(2);
|
|
4020
5128
|
}
|
|
4021
5129
|
const box = await resolveBoxOrExit(idOrName);
|
|
@@ -4170,8 +5278,8 @@ function renderPersisted2(s, state) {
|
|
|
4170
5278
|
}
|
|
4171
5279
|
|
|
4172
5280
|
// src/commands/stop.ts
|
|
4173
|
-
import { Command as
|
|
4174
|
-
var stopCommand = new
|
|
5281
|
+
import { Command as Command24 } from "commander";
|
|
5282
|
+
var stopCommand = new Command24("stop").description("Stop a box (docker stop; preserves upper + node_modules volumes)").argument(
|
|
4175
5283
|
"[box]",
|
|
4176
5284
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
4177
5285
|
).action(async (idOrName) => {
|
|
@@ -4189,7 +5297,7 @@ restart with: agentbox start ${record.name}
|
|
|
4189
5297
|
});
|
|
4190
5298
|
|
|
4191
5299
|
// src/commands/top.ts
|
|
4192
|
-
import { Command as
|
|
5300
|
+
import { Command as Command25 } from "commander";
|
|
4193
5301
|
var COLS = ["BOX", "STATE", "CPU%", "MEM USAGE / LIMIT", "MEM%", "PIDS", "DISK", "NET I/O"];
|
|
4194
5302
|
function row(name, state, s) {
|
|
4195
5303
|
const mem = `${fmtBytes(s.memUsedBytes)} / ${fmtBytes(s.memLimitBytes)}`;
|
|
@@ -4228,7 +5336,7 @@ async function snapshot(idOrName, opts) {
|
|
|
4228
5336
|
async function renderProjectFooters() {
|
|
4229
5337
|
const parts = [];
|
|
4230
5338
|
const [ckpt, home] = await Promise.all([
|
|
4231
|
-
|
|
5339
|
+
allCheckpointImagesBytes(),
|
|
4232
5340
|
agentboxHomeBytes()
|
|
4233
5341
|
]);
|
|
4234
5342
|
if (home !== null) parts.push(`~/.agentbox: ${fmtBytes(home)}`);
|
|
@@ -4237,7 +5345,7 @@ async function renderProjectFooters() {
|
|
|
4237
5345
|
|
|
4238
5346
|
SYSTEM: ${parts.join(" - ")}` : "";
|
|
4239
5347
|
}
|
|
4240
|
-
var topCommand = new
|
|
5348
|
+
var topCommand = new Command25("top").description("Live resource monitor (cpu/mem/pids/disk) for a box, the project, or every box").argument(
|
|
4241
5349
|
"[box]",
|
|
4242
5350
|
"box ref (default: every box on the host; --project narrows to the cwd's project)"
|
|
4243
5351
|
).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) => {
|
|
@@ -4270,8 +5378,8 @@ var topCommand = new Command23("top").description("Live resource monitor (cpu/me
|
|
|
4270
5378
|
});
|
|
4271
5379
|
|
|
4272
5380
|
// src/commands/unpause.ts
|
|
4273
|
-
import { Command as
|
|
4274
|
-
var unpauseCommand = new
|
|
5381
|
+
import { Command as Command26 } from "commander";
|
|
5382
|
+
var unpauseCommand = new Command26("unpause").description("Resume a paused box (docker unpause \u2014 sub-second)").argument(
|
|
4275
5383
|
"[box]",
|
|
4276
5384
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
4277
5385
|
).action(async (idOrName) => {
|
|
@@ -4287,8 +5395,8 @@ var unpauseCommand = new Command24("unpause").description("Resume a paused box (
|
|
|
4287
5395
|
|
|
4288
5396
|
// src/commands/update.ts
|
|
4289
5397
|
import { spawn as spawn4 } from "child_process";
|
|
4290
|
-
import { confirm as confirm10, intro as intro3, isCancel as isCancel10, log as
|
|
4291
|
-
import { Command as
|
|
5398
|
+
import { confirm as confirm10, intro as intro3, isCancel as isCancel10, log as log24, outro as outro3, spinner as spinner4 } from "@clack/prompts";
|
|
5399
|
+
import { Command as Command27 } from "commander";
|
|
4292
5400
|
|
|
4293
5401
|
// src/exec-method.ts
|
|
4294
5402
|
function detectExecutionMethod(input) {
|
|
@@ -4332,7 +5440,7 @@ function runInherit(cmd, args) {
|
|
|
4332
5440
|
child.on("close", (code) => resolveP(code ?? 0));
|
|
4333
5441
|
});
|
|
4334
5442
|
}
|
|
4335
|
-
var updateCommand = new
|
|
5443
|
+
var updateCommand = new Command27("self-update").description(
|
|
4336
5444
|
"Update agentbox: self-update via npm/pnpm (unless run via npx), wipe the box image so it rebuilds, and reload the relay"
|
|
4337
5445
|
).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) => {
|
|
4338
5446
|
try {
|
|
@@ -4342,7 +5450,7 @@ var updateCommand = new Command25("self-update").description(
|
|
|
4342
5450
|
});
|
|
4343
5451
|
intro3("agentbox self-update");
|
|
4344
5452
|
const selfStep = opts.skipSelf ? "self-update: skipped (--skip-self)" : describeSelfUpdate(method);
|
|
4345
|
-
|
|
5453
|
+
log24.info(
|
|
4346
5454
|
[
|
|
4347
5455
|
"plan:",
|
|
4348
5456
|
` ${selfStep}`,
|
|
@@ -4357,52 +5465,52 @@ var updateCommand = new Command25("self-update").description(
|
|
|
4357
5465
|
if (!opts.yes) {
|
|
4358
5466
|
const ok = await confirm10({ message: "Proceed with update?", initialValue: true });
|
|
4359
5467
|
if (isCancel10(ok) || !ok) {
|
|
4360
|
-
|
|
5468
|
+
log24.info("cancelled");
|
|
4361
5469
|
return;
|
|
4362
5470
|
}
|
|
4363
5471
|
}
|
|
4364
5472
|
let selfUpdated = false;
|
|
4365
5473
|
if (opts.skipSelf) {
|
|
4366
|
-
|
|
5474
|
+
log24.info("skipping self-update (--skip-self)");
|
|
4367
5475
|
} else {
|
|
4368
5476
|
const cmd = selfUpdateCommand(method);
|
|
4369
5477
|
if (cmd === null) {
|
|
4370
|
-
|
|
5478
|
+
log24.info(describeSelfUpdate(method));
|
|
4371
5479
|
} else {
|
|
4372
|
-
|
|
5480
|
+
log24.info(`running: ${cmd.cmd} ${cmd.args.join(" ")}`);
|
|
4373
5481
|
const code = await runInherit(cmd.cmd, cmd.args);
|
|
4374
5482
|
if (code !== 0) {
|
|
4375
5483
|
throw new Error(`${cmd.cmd} exited with code ${String(code)}`);
|
|
4376
5484
|
}
|
|
4377
5485
|
selfUpdated = true;
|
|
4378
|
-
|
|
5486
|
+
log24.success(`updated ${PKG} via ${cmd.cmd}`);
|
|
4379
5487
|
}
|
|
4380
5488
|
}
|
|
4381
|
-
const s =
|
|
5489
|
+
const s = spinner4();
|
|
4382
5490
|
s.start(`removing image ${DEFAULT_BOX_IMAGE}`);
|
|
4383
5491
|
const removed = await removeImage(DEFAULT_BOX_IMAGE);
|
|
4384
5492
|
s.stop(
|
|
4385
5493
|
removed ? `removed image ${DEFAULT_BOX_IMAGE} (rebuilds on next create/claude)` : `image ${DEFAULT_BOX_IMAGE} not present (nothing to remove)`
|
|
4386
5494
|
);
|
|
4387
|
-
const sr =
|
|
5495
|
+
const sr = spinner4();
|
|
4388
5496
|
sr.start("stopping relay");
|
|
4389
5497
|
const stop = await stopRelay();
|
|
4390
5498
|
sr.stop(
|
|
4391
5499
|
stop.stopped ? `stopped relay (pid ${String(stop.pid)})` : "relay was not running"
|
|
4392
5500
|
);
|
|
4393
5501
|
if (selfUpdated) {
|
|
4394
|
-
|
|
5502
|
+
log24.info(
|
|
4395
5503
|
"relay will restart automatically (with the updated build) on your next `agentbox create` / `agentbox claude`"
|
|
4396
5504
|
);
|
|
4397
5505
|
} else {
|
|
4398
|
-
const sr2 =
|
|
5506
|
+
const sr2 = spinner4();
|
|
4399
5507
|
sr2.start("restarting relay");
|
|
4400
5508
|
try {
|
|
4401
5509
|
const ep = await ensureRelay();
|
|
4402
5510
|
sr2.stop(`relay back up on ${ep.hostUrl}`);
|
|
4403
5511
|
} catch (err) {
|
|
4404
5512
|
sr2.stop("relay restart failed");
|
|
4405
|
-
|
|
5513
|
+
log24.warn(
|
|
4406
5514
|
`${err instanceof Error ? err.message : String(err)} \u2014 it will retry on the next box command`
|
|
4407
5515
|
);
|
|
4408
5516
|
}
|
|
@@ -4414,9 +5522,9 @@ var updateCommand = new Command25("self-update").description(
|
|
|
4414
5522
|
});
|
|
4415
5523
|
|
|
4416
5524
|
// src/commands/wait.ts
|
|
4417
|
-
import { log as
|
|
4418
|
-
import { Command as
|
|
4419
|
-
var waitCommand = new
|
|
5525
|
+
import { log as log25 } from "@clack/prompts";
|
|
5526
|
+
import { Command as Command28 } from "commander";
|
|
5527
|
+
var waitCommand = new Command28("wait").description("Block until the box reports all autostart units ready").argument(
|
|
4420
5528
|
"[box]",
|
|
4421
5529
|
"box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
|
|
4422
5530
|
).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) => {
|
|
@@ -4431,7 +5539,7 @@ var waitCommand = new Command26("wait").description("Block until the box reports
|
|
|
4431
5539
|
try {
|
|
4432
5540
|
parsed = JSON.parse(proc.stdout);
|
|
4433
5541
|
} catch {
|
|
4434
|
-
|
|
5542
|
+
log25.error(`agentbox-ctl wait-ready failed: ${proc.stderr || proc.stdout}`);
|
|
4435
5543
|
process.exit(1);
|
|
4436
5544
|
}
|
|
4437
5545
|
if (opts.json) {
|
|
@@ -4451,7 +5559,8 @@ var waitCommand = new Command26("wait").description("Block until the box reports
|
|
|
4451
5559
|
});
|
|
4452
5560
|
|
|
4453
5561
|
// src/index.ts
|
|
4454
|
-
|
|
5562
|
+
process.env.DOCKER_CLI_HINTS ??= "false";
|
|
5563
|
+
var program = new Command29();
|
|
4455
5564
|
program.name("agentbox").description("Launch coding agents in isolated sandboxes").version("0.0.0");
|
|
4456
5565
|
program.enablePositionalOptions();
|
|
4457
5566
|
program.addCommand(createCommand);
|
|
@@ -4462,7 +5571,8 @@ program.addCommand(listCommand2);
|
|
|
4462
5571
|
program.addCommand(openCommand);
|
|
4463
5572
|
program.addCommand(browserCommand);
|
|
4464
5573
|
program.addCommand(screenCommand);
|
|
4465
|
-
program.addCommand(
|
|
5574
|
+
program.addCommand(downloadCommand);
|
|
5575
|
+
program.addCommand(cpCommand);
|
|
4466
5576
|
program.addCommand(statusCommand);
|
|
4467
5577
|
program.addCommand(topCommand);
|
|
4468
5578
|
program.addCommand(dashboardCommand);
|
|
@@ -4476,6 +5586,7 @@ program.addCommand(destroyCommand);
|
|
|
4476
5586
|
program.addCommand(pruneCommand);
|
|
4477
5587
|
program.addCommand(checkpointCommand);
|
|
4478
5588
|
program.addCommand(configCommand);
|
|
5589
|
+
program.addCommand(relayCommand);
|
|
4479
5590
|
program.addCommand(updateCommand);
|
|
4480
5591
|
program.configureHelp({ visibleCommands: () => [] });
|
|
4481
5592
|
program.addHelpText("after", () => "\n" + buildGroupedHelp(program));
|