@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
|
@@ -6,24 +6,28 @@ import {
|
|
|
6
6
|
execInBox,
|
|
7
7
|
orbstackVolumePath,
|
|
8
8
|
removeContainer,
|
|
9
|
+
sanitizeMnemonic,
|
|
9
10
|
volumeExists
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-HHMWQNLF.js";
|
|
11
12
|
|
|
12
|
-
// ../../packages/sandbox-docker/dist/chunk-
|
|
13
|
+
// ../../packages/sandbox-docker/dist/chunk-SRQIM7LG.js
|
|
13
14
|
import { spawnSync } from "child_process";
|
|
14
15
|
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
15
16
|
import { homedir, tmpdir } from "os";
|
|
16
17
|
import { join, relative } from "path";
|
|
18
|
+
import { setTimeout as delay } from "timers/promises";
|
|
17
19
|
import { execa } from "execa";
|
|
18
20
|
import { randomBytes } from "crypto";
|
|
19
21
|
import { execa as execa2 } from "execa";
|
|
20
22
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
21
23
|
import { join as join2 } from "path";
|
|
22
24
|
import { execa as execa3 } from "execa";
|
|
25
|
+
import { execa as execa4 } from "execa";
|
|
23
26
|
import { mkdir as mkdir2, readdir as readdir3, rm as rm2, stat as stat3 } from "fs/promises";
|
|
24
27
|
import { homedir as homedir2, platform } from "os";
|
|
25
28
|
import { join as join3, resolve } from "path";
|
|
26
29
|
import { stat as stat4 } from "fs/promises";
|
|
30
|
+
import { execa as execa5 } from "execa";
|
|
27
31
|
import { spawn } from "child_process";
|
|
28
32
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
29
33
|
import { existsSync, openSync } from "fs";
|
|
@@ -31,7 +35,7 @@ import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2
|
|
|
31
35
|
import { request as httpRequest } from "http";
|
|
32
36
|
import { homedir as homedir3 } from "os";
|
|
33
37
|
import { dirname, join as join4, resolve as resolve2 } from "path";
|
|
34
|
-
import { setTimeout as
|
|
38
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
35
39
|
import { fileURLToPath } from "url";
|
|
36
40
|
|
|
37
41
|
// ../../packages/relay/dist/index.js
|
|
@@ -41,7 +45,7 @@ var RELAY_NETWORK_NAME = "agentbox-net";
|
|
|
41
45
|
var RELAY_IMAGE_REF = "agentbox/relay:dev";
|
|
42
46
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
43
47
|
|
|
44
|
-
// ../../packages/sandbox-docker/dist/chunk-
|
|
48
|
+
// ../../packages/sandbox-docker/dist/chunk-SRQIM7LG.js
|
|
45
49
|
function isHostPathHookCommand(command, hostHome) {
|
|
46
50
|
if (typeof command !== "string" || command.length === 0) return false;
|
|
47
51
|
if (hostHome.length === 0) return false;
|
|
@@ -78,6 +82,27 @@ function filterHostHooks(data, hostHome) {
|
|
|
78
82
|
}
|
|
79
83
|
return { data: clone, removedCommands };
|
|
80
84
|
}
|
|
85
|
+
function trustWorkspace(data, workspacePath) {
|
|
86
|
+
const clone = structuredClone(data);
|
|
87
|
+
if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
|
|
88
|
+
return { data: clone, trusted: false };
|
|
89
|
+
}
|
|
90
|
+
if (workspacePath.length === 0) return { data: clone, trusted: false };
|
|
91
|
+
const obj = clone;
|
|
92
|
+
if (obj.projects === null || typeof obj.projects !== "object" || Array.isArray(obj.projects)) {
|
|
93
|
+
obj.projects = {};
|
|
94
|
+
}
|
|
95
|
+
const projects = obj.projects;
|
|
96
|
+
const existing = projects[workspacePath];
|
|
97
|
+
const entry = existing !== null && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
|
|
98
|
+
if (entry.hasTrustDialogAccepted === true) {
|
|
99
|
+
projects[workspacePath] = entry;
|
|
100
|
+
return { data: clone, trusted: false };
|
|
101
|
+
}
|
|
102
|
+
entry.hasTrustDialogAccepted = true;
|
|
103
|
+
projects[workspacePath] = entry;
|
|
104
|
+
return { data: clone, trusted: true };
|
|
105
|
+
}
|
|
81
106
|
function addProjectAlias(data, fromPath, toPath) {
|
|
82
107
|
const clone = structuredClone(data);
|
|
83
108
|
if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
|
|
@@ -104,17 +129,17 @@ function addProjectAlias(data, fromPath, toPath) {
|
|
|
104
129
|
}
|
|
105
130
|
return { data: clone, aliased: true };
|
|
106
131
|
}
|
|
107
|
-
function
|
|
132
|
+
function setInstallMethodNative(data) {
|
|
108
133
|
const clone = structuredClone(data);
|
|
109
134
|
if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
|
|
110
|
-
return { data: clone,
|
|
135
|
+
return { data: clone, applied: false };
|
|
111
136
|
}
|
|
112
137
|
const obj = clone;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return { data: clone,
|
|
138
|
+
const changed = obj.installMethod !== "native" || obj.autoUpdates !== false || obj.autoUpdatesProtectedForNative !== true;
|
|
139
|
+
obj.installMethod = "native";
|
|
140
|
+
obj.autoUpdates = false;
|
|
141
|
+
obj.autoUpdatesProtectedForNative = true;
|
|
142
|
+
return { data: clone, applied: changed };
|
|
118
143
|
}
|
|
119
144
|
var SKILL_EXCLUDE_PREFIXES = ["agentbox-"];
|
|
120
145
|
var CONTAINER_PLUGINS_PREFIX = "/home/vscode/.claude/plugins/";
|
|
@@ -188,11 +213,31 @@ function mergeInstalledPlugins(hostJson, boxJson, opts) {
|
|
|
188
213
|
(host, merged) => ({ ...host, plugins: merged })
|
|
189
214
|
);
|
|
190
215
|
}
|
|
216
|
+
function referencedPluginVersionKeys(installedPluginsJson) {
|
|
217
|
+
const keys = /* @__PURE__ */ new Set();
|
|
218
|
+
if (!isPlainObject(installedPluginsJson)) return keys;
|
|
219
|
+
const plugins = installedPluginsJson["plugins"];
|
|
220
|
+
if (!isPlainObject(plugins)) return keys;
|
|
221
|
+
for (const entries of Object.values(plugins)) {
|
|
222
|
+
if (!Array.isArray(entries)) continue;
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (!isPlainObject(entry)) continue;
|
|
225
|
+
const installPath = entry["installPath"];
|
|
226
|
+
if (typeof installPath !== "string") continue;
|
|
227
|
+
const segments = installPath.split("/").filter((s) => s.length > 0);
|
|
228
|
+
if (segments.length < 3) continue;
|
|
229
|
+
keys.add(segments.slice(-3).join("/"));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return keys;
|
|
233
|
+
}
|
|
191
234
|
var SHARED_CLAUDE_VOLUME = "agentbox-claude-config";
|
|
192
235
|
var DEFAULT_CLAUDE_SESSION = "claude";
|
|
193
236
|
var CONTAINER_CLAUDE_DIR = "/home/vscode/.claude";
|
|
194
237
|
var CONTAINER_USER = "vscode";
|
|
195
238
|
var CONTAINER_WORKSPACE = "/workspace";
|
|
239
|
+
var IN_BOX_SETUP_GUIDE_PATH = "/usr/local/share/agentbox/setup-guide.md";
|
|
240
|
+
var SETUP_SKILL_DST = "/dst/skills/agentbox-setup/SKILL.md";
|
|
196
241
|
function resolveClaudeVolume(opts) {
|
|
197
242
|
if (opts.isolate) {
|
|
198
243
|
return { volume: `${SHARED_CLAUDE_VOLUME}-${opts.boxId}` };
|
|
@@ -207,6 +252,14 @@ async function pathExists(p) {
|
|
|
207
252
|
return false;
|
|
208
253
|
}
|
|
209
254
|
}
|
|
255
|
+
async function volumeHasClaudeJson(volume, image) {
|
|
256
|
+
const res = await execa(
|
|
257
|
+
"docker",
|
|
258
|
+
["run", "--rm", "-v", `${volume}:/dst`, image, "sh", "-c", "test -e /dst/_claude.json"],
|
|
259
|
+
{ reject: false }
|
|
260
|
+
);
|
|
261
|
+
return res.exitCode === 0;
|
|
262
|
+
}
|
|
210
263
|
async function findBrokenSymlinks(root) {
|
|
211
264
|
const broken = [];
|
|
212
265
|
async function walk(dir) {
|
|
@@ -241,6 +294,7 @@ async function ensureClaudeVolume(spec, opts) {
|
|
|
241
294
|
if (!await pathExists(hostClaude)) return { created, synced: false };
|
|
242
295
|
const hostClaudeJson = join(homedir(), ".claude.json");
|
|
243
296
|
const hasJson = await pathExists(hostClaudeJson);
|
|
297
|
+
const seedClaudeJson = !await volumeHasClaudeJson(spec.volume, opts.image);
|
|
244
298
|
const hostHome = homedir();
|
|
245
299
|
const hostAgents = join(homedir(), ".agents");
|
|
246
300
|
const hasAgents = await pathExists(hostAgents);
|
|
@@ -258,12 +312,13 @@ async function ensureClaudeVolume(spec, opts) {
|
|
|
258
312
|
"-v",
|
|
259
313
|
`${hostClaude}:/src-claude:ro`
|
|
260
314
|
];
|
|
261
|
-
if (hasJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
|
|
315
|
+
if (hasJson && seedClaudeJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
|
|
262
316
|
if (hasAgents) args.push("-v", `${hostAgents}:/.agents:ro`);
|
|
263
317
|
const filterDir = await mkdtemp(join(tmpdir(), "agentbox-claude-filter-"));
|
|
264
318
|
let filteredHookCount = 0;
|
|
265
|
-
let
|
|
319
|
+
let installMethodFixed = false;
|
|
266
320
|
let aliasedProjectKey = false;
|
|
321
|
+
let workspaceTrusted = false;
|
|
267
322
|
try {
|
|
268
323
|
const settingsResult = await maybeFilterTo(
|
|
269
324
|
join(hostClaude, "settings.json"),
|
|
@@ -271,21 +326,40 @@ async function ensureClaudeVolume(spec, opts) {
|
|
|
271
326
|
hostHome
|
|
272
327
|
);
|
|
273
328
|
filteredHookCount += settingsResult.removedHooks;
|
|
274
|
-
if (
|
|
329
|
+
if (!seedClaudeJson) {
|
|
330
|
+
} else if (hasJson) {
|
|
275
331
|
const jsonResult = await maybeFilterTo(
|
|
276
332
|
hostClaudeJson,
|
|
277
333
|
join(filterDir, "_claude.json"),
|
|
278
334
|
hostHome,
|
|
279
335
|
{
|
|
280
|
-
|
|
281
|
-
aliasProject: opts.hostWorkspace ? { from: opts.hostWorkspace, to: CONTAINER_WORKSPACE } : void 0
|
|
336
|
+
setInstallMethodNative: true,
|
|
337
|
+
aliasProject: opts.hostWorkspace ? { from: opts.hostWorkspace, to: CONTAINER_WORKSPACE } : void 0,
|
|
338
|
+
trustWorkspacePath: CONTAINER_WORKSPACE
|
|
282
339
|
}
|
|
283
340
|
);
|
|
284
341
|
filteredHookCount += jsonResult.removedHooks;
|
|
285
|
-
|
|
342
|
+
installMethodFixed = jsonResult.installMethodFixed;
|
|
286
343
|
aliasedProjectKey = jsonResult.aliasedProjectKey;
|
|
344
|
+
workspaceTrusted = jsonResult.workspaceTrusted;
|
|
345
|
+
} else {
|
|
346
|
+
await writeFile(
|
|
347
|
+
join(filterDir, "_claude.json"),
|
|
348
|
+
JSON.stringify(
|
|
349
|
+
{
|
|
350
|
+
installMethod: "native",
|
|
351
|
+
autoUpdates: false,
|
|
352
|
+
autoUpdatesProtectedForNative: true,
|
|
353
|
+
projects: { [CONTAINER_WORKSPACE]: { hasTrustDialogAccepted: true } }
|
|
354
|
+
},
|
|
355
|
+
null,
|
|
356
|
+
2
|
|
357
|
+
)
|
|
358
|
+
);
|
|
359
|
+
installMethodFixed = true;
|
|
360
|
+
workspaceTrusted = true;
|
|
287
361
|
}
|
|
288
|
-
if (filteredHookCount > 0 ||
|
|
362
|
+
if (filteredHookCount > 0 || installMethodFixed || aliasedProjectKey || workspaceTrusted) {
|
|
289
363
|
args.push("-v", `${filterDir}:/src-filter:ro`);
|
|
290
364
|
}
|
|
291
365
|
const brokenSymlinks = await findBrokenSymlinks(hostClaude);
|
|
@@ -332,22 +406,57 @@ async function ensureClaudeVolume(spec, opts) {
|
|
|
332
406
|
} finally {
|
|
333
407
|
await rm(filterDir, { recursive: true, force: true });
|
|
334
408
|
}
|
|
335
|
-
return {
|
|
409
|
+
return {
|
|
410
|
+
created,
|
|
411
|
+
synced: true,
|
|
412
|
+
filteredHookCount,
|
|
413
|
+
installMethodFixed,
|
|
414
|
+
aliasedProjectKey,
|
|
415
|
+
workspaceTrusted
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function seedSetupSkillIntoVolume(volume, image) {
|
|
419
|
+
try {
|
|
420
|
+
const { stdout } = await execa("docker", [
|
|
421
|
+
"run",
|
|
422
|
+
"--rm",
|
|
423
|
+
"--user",
|
|
424
|
+
"0",
|
|
425
|
+
"-v",
|
|
426
|
+
`${volume}:/dst`,
|
|
427
|
+
image,
|
|
428
|
+
"sh",
|
|
429
|
+
"-c",
|
|
430
|
+
// Always overwrite from the image so an image upgrade propagates. Prints
|
|
431
|
+
// SEEDED on success; the whole thing is `|| true` so a missing image
|
|
432
|
+
// asset is a clean no-op, never a non-zero exit.
|
|
433
|
+
`{ [ -f ${IN_BOX_SETUP_GUIDE_PATH} ] && rm -rf /dst/skills/agentbox-setup && mkdir -p /dst/skills/agentbox-setup && cp -a ${IN_BOX_SETUP_GUIDE_PATH} ${SETUP_SKILL_DST} && chown -R 1000:1000 /dst/skills/agentbox-setup && echo SEEDED; } || true`
|
|
434
|
+
]);
|
|
435
|
+
return { seeded: stdout.includes("SEEDED") };
|
|
436
|
+
} catch {
|
|
437
|
+
return { seeded: false };
|
|
438
|
+
}
|
|
336
439
|
}
|
|
337
440
|
async function maybeFilterTo(src, dest, hostHome, opts = {}) {
|
|
441
|
+
const zero = {
|
|
442
|
+
removedHooks: 0,
|
|
443
|
+
installMethodFixed: false,
|
|
444
|
+
aliasedProjectKey: false,
|
|
445
|
+
workspaceTrusted: false
|
|
446
|
+
};
|
|
338
447
|
let parsed;
|
|
339
448
|
try {
|
|
340
449
|
parsed = JSON.parse(await readFile(src, "utf8"));
|
|
341
450
|
} catch {
|
|
342
|
-
return
|
|
451
|
+
return zero;
|
|
343
452
|
}
|
|
344
453
|
const filtered = filterHostHooks(parsed, hostHome);
|
|
345
454
|
let working = filtered.data;
|
|
346
|
-
let
|
|
347
|
-
if (opts.
|
|
348
|
-
const r =
|
|
455
|
+
let installFixed = false;
|
|
456
|
+
if (opts.setInstallMethodNative) {
|
|
457
|
+
const r = setInstallMethodNative(working);
|
|
349
458
|
working = r.data;
|
|
350
|
-
|
|
459
|
+
installFixed = r.applied;
|
|
351
460
|
}
|
|
352
461
|
let aliased = false;
|
|
353
462
|
if (opts.aliasProject) {
|
|
@@ -355,14 +464,21 @@ async function maybeFilterTo(src, dest, hostHome, opts = {}) {
|
|
|
355
464
|
working = r.data;
|
|
356
465
|
aliased = r.aliased;
|
|
357
466
|
}
|
|
358
|
-
|
|
359
|
-
|
|
467
|
+
let trusted = false;
|
|
468
|
+
if (opts.trustWorkspacePath) {
|
|
469
|
+
const r = trustWorkspace(working, opts.trustWorkspacePath);
|
|
470
|
+
working = r.data;
|
|
471
|
+
trusted = r.trusted;
|
|
472
|
+
}
|
|
473
|
+
if (filtered.removedCommands.length === 0 && !installFixed && !aliased && !trusted) {
|
|
474
|
+
return zero;
|
|
360
475
|
}
|
|
361
476
|
await writeFile(dest, JSON.stringify(working, null, 2));
|
|
362
477
|
return {
|
|
363
478
|
removedHooks: filtered.removedCommands.length,
|
|
364
|
-
|
|
365
|
-
aliasedProjectKey: aliased
|
|
479
|
+
installMethodFixed: installFixed,
|
|
480
|
+
aliasedProjectKey: aliased,
|
|
481
|
+
workspaceTrusted: trusted
|
|
366
482
|
};
|
|
367
483
|
}
|
|
368
484
|
var FORWARDED_ENV_KEYS = [
|
|
@@ -410,7 +526,18 @@ async function isDir(p) {
|
|
|
410
526
|
return false;
|
|
411
527
|
}
|
|
412
528
|
}
|
|
529
|
+
async function readReferencedPluginKeys(installedPluginsJsonPath) {
|
|
530
|
+
try {
|
|
531
|
+
const raw = await readFile(installedPluginsJsonPath, "utf8");
|
|
532
|
+
return referencedPluginVersionKeys(JSON.parse(raw));
|
|
533
|
+
} catch {
|
|
534
|
+
return /* @__PURE__ */ new Set();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
413
537
|
async function scanPluginCacheForRebuild(cacheRoot) {
|
|
538
|
+
const referenced = await readReferencedPluginKeys(
|
|
539
|
+
join(cacheRoot, "..", "installed_plugins.json")
|
|
540
|
+
);
|
|
414
541
|
let marketplaces;
|
|
415
542
|
try {
|
|
416
543
|
marketplaces = await readdir(cacheRoot, { withFileTypes: true });
|
|
@@ -437,6 +564,7 @@ async function scanPluginCacheForRebuild(cacheRoot) {
|
|
|
437
564
|
}
|
|
438
565
|
for (const v of versions) {
|
|
439
566
|
if (!v.isDirectory()) continue;
|
|
567
|
+
if (referenced.size > 0 && !referenced.has(`${m.name}/${p.name}/${v.name}`)) continue;
|
|
440
568
|
const vPath = join(pPath, v.name);
|
|
441
569
|
if (!await isFile(join(vPath, "package.json"))) continue;
|
|
442
570
|
if (await isFile(join(vPath, PLUGIN_INSTALLED_MARKER))) continue;
|
|
@@ -452,13 +580,38 @@ async function resolveClaudeCacheLiveOnHost(volume) {
|
|
|
452
580
|
if (!await isDir(orbstackVolumePath(volume))) return null;
|
|
453
581
|
return orbstackVolumePath(volume, "plugins", "cache");
|
|
454
582
|
}
|
|
583
|
+
async function readBoxReferencedPluginKeys(container) {
|
|
584
|
+
const res = await execa(
|
|
585
|
+
"docker",
|
|
586
|
+
[
|
|
587
|
+
"exec",
|
|
588
|
+
"--user",
|
|
589
|
+
CONTAINER_USER,
|
|
590
|
+
container,
|
|
591
|
+
"cat",
|
|
592
|
+
`${CONTAINER_CLAUDE_DIR}/plugins/installed_plugins.json`
|
|
593
|
+
],
|
|
594
|
+
{ reject: false }
|
|
595
|
+
);
|
|
596
|
+
if (res.exitCode !== 0 || !res.stdout) return /* @__PURE__ */ new Set();
|
|
597
|
+
try {
|
|
598
|
+
return referencedPluginVersionKeys(JSON.parse(res.stdout));
|
|
599
|
+
} catch {
|
|
600
|
+
return /* @__PURE__ */ new Set();
|
|
601
|
+
}
|
|
602
|
+
}
|
|
455
603
|
async function rebuildPluginNativeDeps(container, opts = {}) {
|
|
456
604
|
if (opts.volume) {
|
|
457
605
|
const cacheRoot = await resolveClaudeCacheLiveOnHost(opts.volume);
|
|
458
606
|
if (cacheRoot && !await scanPluginCacheForRebuild(cacheRoot)) {
|
|
459
|
-
return { rebuilt: [], failed: [], skipped: true };
|
|
607
|
+
return { rebuilt: [], failed: [], pruned: [], prunedBytes: 0, skipped: true };
|
|
460
608
|
}
|
|
461
609
|
}
|
|
610
|
+
const referenced = await readBoxReferencedPluginKeys(container);
|
|
611
|
+
const refSetup = referenced.size > 0 ? `cat <<'AGENTBOX_REF_EOF' > "$WORK/referenced"
|
|
612
|
+
${[...referenced].sort().join("\n")}
|
|
613
|
+
AGENTBOX_REF_EOF
|
|
614
|
+
` : "";
|
|
462
615
|
const script = `set -u
|
|
463
616
|
PLUGINS_DIR=/home/vscode/.claude/plugins/cache
|
|
464
617
|
MARKER=${PLUGIN_INSTALLED_MARKER}
|
|
@@ -469,7 +622,12 @@ MAX=4
|
|
|
469
622
|
[ -d "$PLUGINS_DIR" ] || exit 0
|
|
470
623
|
mkdir -p "$NPM_CACHE"
|
|
471
624
|
WORK=$(mktemp -d)
|
|
472
|
-
relkey() { printf '%s' "\${1#$PLUGINS_DIR/}" | tr '/' '_'; }
|
|
625
|
+
${refSetup}relkey() { printf '%s' "\${1#$PLUGINS_DIR/}" | tr '/' '_'; }
|
|
626
|
+
# True when refs are unknown (no file) or $1 (<m>/<p>/<v>) is referenced.
|
|
627
|
+
is_referenced() {
|
|
628
|
+
[ -s "$WORK/referenced" ] || return 0
|
|
629
|
+
grep -Fxq "$1" "$WORK/referenced"
|
|
630
|
+
}
|
|
473
631
|
# Run one plugin's install. $1 is frozen by value at call time, so it's safe
|
|
474
632
|
# to read from the backgrounded subshell; the rest are set-once constants.
|
|
475
633
|
do_one() {
|
|
@@ -489,10 +647,29 @@ do_one() {
|
|
|
489
647
|
printf 'FAIL\\n' > "$WORK/$key.res"
|
|
490
648
|
fi
|
|
491
649
|
}
|
|
650
|
+
# Prune pass: every unreferenced (stale) version dir loses its node_modules and
|
|
651
|
+
# our markers. Only runs when installed_plugins.json gave us a reference set.
|
|
652
|
+
if [ -s "$WORK/referenced" ]; then
|
|
653
|
+
for dir in "$PLUGINS_DIR"/*/*/*/; do
|
|
654
|
+
[ -d "$dir" ] || continue
|
|
655
|
+
rel=\${dir%/}; rel=\${rel#$PLUGINS_DIR/}
|
|
656
|
+
grep -Fxq "$rel" "$WORK/referenced" && continue
|
|
657
|
+
if [ -d "$dir/node_modules" ]; then
|
|
658
|
+
bytes=$(du -sb "$dir/node_modules" 2>/dev/null | cut -f1)
|
|
659
|
+
[ -n "$bytes" ] || bytes=0
|
|
660
|
+
rm -rf "$dir/node_modules" "$dir/$MARKER" "$dir/$FAILMARKER"
|
|
661
|
+
echo "PRUNE_OK $rel $bytes"
|
|
662
|
+
else
|
|
663
|
+
rm -f "$dir/$MARKER" "$dir/$FAILMARKER"
|
|
664
|
+
fi
|
|
665
|
+
done
|
|
666
|
+
fi
|
|
492
667
|
n=0
|
|
493
668
|
for dir in "$PLUGINS_DIR"/*/*/*/; do
|
|
494
669
|
[ -d "$dir" ] || continue
|
|
495
670
|
[ -f "$dir/package.json" ] || continue
|
|
671
|
+
rel=\${dir%/}; rel=\${rel#$PLUGINS_DIR/}
|
|
672
|
+
is_referenced "$rel" || continue
|
|
496
673
|
[ -f "$dir/$MARKER" ] && continue
|
|
497
674
|
[ -n "$(find "$dir" -maxdepth 1 -name "$FAILMARKER" -mmin -$BACKOFF_MIN 2>/dev/null)" ] && continue
|
|
498
675
|
echo "REBUILD_START \${dir#$PLUGINS_DIR/}"
|
|
@@ -529,6 +706,8 @@ rm -rf "$WORK"
|
|
|
529
706
|
);
|
|
530
707
|
const rebuilt = [];
|
|
531
708
|
const failed = [];
|
|
709
|
+
const pruned = [];
|
|
710
|
+
let prunedBytes = 0;
|
|
532
711
|
const lines = (result.stdout ?? "").split("\n");
|
|
533
712
|
let collectingFail = null;
|
|
534
713
|
for (const line of lines) {
|
|
@@ -547,9 +726,19 @@ rm -rf "$WORK"
|
|
|
547
726
|
rebuilt.push(line.slice("REBUILD_OK ".length));
|
|
548
727
|
} else if (line.startsWith("REBUILD_FAIL ")) {
|
|
549
728
|
collectingFail = { dir: line.slice("REBUILD_FAIL ".length), stderr: [] };
|
|
729
|
+
} else if (line.startsWith("PRUNE_OK ")) {
|
|
730
|
+
const rest = line.slice("PRUNE_OK ".length);
|
|
731
|
+
const sp = rest.lastIndexOf(" ");
|
|
732
|
+
if (sp > 0) {
|
|
733
|
+
const dir = rest.slice(0, sp);
|
|
734
|
+
const bytes = Number(rest.slice(sp + 1));
|
|
735
|
+
pruned.push(dir);
|
|
736
|
+
if (Number.isFinite(bytes)) prunedBytes += bytes;
|
|
737
|
+
opts.onProgress?.(`pruning stale plugin cache ${dir}`);
|
|
738
|
+
}
|
|
550
739
|
}
|
|
551
740
|
}
|
|
552
|
-
return { rebuilt, failed, skipped: false };
|
|
741
|
+
return { rebuilt, failed, pruned, prunedBytes, skipped: false };
|
|
553
742
|
}
|
|
554
743
|
var ClaudeSessionError = class extends Error {
|
|
555
744
|
constructor(message) {
|
|
@@ -564,7 +753,6 @@ function shQuote(arg) {
|
|
|
564
753
|
}
|
|
565
754
|
async function startClaudeSession(opts) {
|
|
566
755
|
const sessionName = opts.sessionName ?? DEFAULT_CLAUDE_SESSION;
|
|
567
|
-
const boxName = opts.boxName ?? opts.container.replace(/^agentbox-/, "");
|
|
568
756
|
const cmd = ["claude", ...opts.claudeArgs].map(shQuote).join(" ");
|
|
569
757
|
const term = process.env["TERM"] ?? "xterm-256color";
|
|
570
758
|
const envFlags = ["-e", `TERM=${term}`];
|
|
@@ -586,7 +774,7 @@ async function startClaudeSession(opts) {
|
|
|
586
774
|
"-s",
|
|
587
775
|
sessionName,
|
|
588
776
|
cmd,
|
|
589
|
-
...buildClaudeStatusBarArgs(sessionName
|
|
777
|
+
...buildClaudeStatusBarArgs(sessionName)
|
|
590
778
|
],
|
|
591
779
|
{ reject: false }
|
|
592
780
|
);
|
|
@@ -660,13 +848,15 @@ function buildClaudeDashboardAttachArgv(container, sessionName) {
|
|
|
660
848
|
dash
|
|
661
849
|
];
|
|
662
850
|
}
|
|
663
|
-
function buildClaudeStatusBarArgs(sessionName
|
|
851
|
+
function buildClaudeStatusBarArgs(sessionName) {
|
|
664
852
|
const s = sessionName;
|
|
665
|
-
const name = boxName;
|
|
666
853
|
return [
|
|
667
|
-
// Server-global (no -t):
|
|
668
|
-
//
|
|
669
|
-
//
|
|
854
|
+
// Server-global (no -t): primary prefix Ctrl+a (dashboard parity), keep
|
|
855
|
+
// tmux's default Ctrl+b as a secondary prefix so users with existing
|
|
856
|
+
// muscle memory / integrations aren't broken. `q` is the same key under
|
|
857
|
+
// both prefixes (single key table) -> Ctrl+a q AND Ctrl+b q both detach.
|
|
858
|
+
// `send-prefix` / `send-prefix -2` let a double-tap of either prefix
|
|
859
|
+
// reach Claude as that literal key.
|
|
670
860
|
";",
|
|
671
861
|
"set",
|
|
672
862
|
"-g",
|
|
@@ -676,9 +866,6 @@ function buildClaudeStatusBarArgs(sessionName, boxName) {
|
|
|
676
866
|
"set",
|
|
677
867
|
"-g",
|
|
678
868
|
"prefix2",
|
|
679
|
-
"None",
|
|
680
|
-
";",
|
|
681
|
-
"unbind-key",
|
|
682
869
|
"C-b",
|
|
683
870
|
";",
|
|
684
871
|
"bind-key",
|
|
@@ -686,80 +873,88 @@ function buildClaudeStatusBarArgs(sessionName, boxName) {
|
|
|
686
873
|
"send-prefix",
|
|
687
874
|
";",
|
|
688
875
|
"bind-key",
|
|
876
|
+
"C-b",
|
|
877
|
+
"send-prefix",
|
|
878
|
+
"-2",
|
|
879
|
+
";",
|
|
880
|
+
"bind-key",
|
|
689
881
|
"q",
|
|
690
882
|
"detach-client",
|
|
883
|
+
// Hide the inner tmux status bar — the wrapped-pty footer (for
|
|
884
|
+
// `agentbox claude` / `agentbox shell`) and the dashboard's own status
|
|
885
|
+
// row already show the box name + detach hint; without `status off`
|
|
886
|
+
// they double up.
|
|
691
887
|
";",
|
|
692
888
|
"set",
|
|
693
889
|
"-t",
|
|
694
890
|
s,
|
|
695
|
-
"status
|
|
696
|
-
"
|
|
697
|
-
";",
|
|
698
|
-
"set",
|
|
699
|
-
"-t",
|
|
700
|
-
s,
|
|
701
|
-
"status-justify",
|
|
702
|
-
"left",
|
|
703
|
-
";",
|
|
704
|
-
"set",
|
|
705
|
-
"-t",
|
|
706
|
-
s,
|
|
707
|
-
"status-style",
|
|
708
|
-
"bg=colour236,fg=colour250",
|
|
709
|
-
";",
|
|
710
|
-
"set",
|
|
711
|
-
"-t",
|
|
712
|
-
s,
|
|
713
|
-
"status-left-length",
|
|
714
|
-
"60",
|
|
715
|
-
";",
|
|
716
|
-
"set",
|
|
717
|
-
"-t",
|
|
718
|
-
s,
|
|
719
|
-
"status-left",
|
|
720
|
-
`#[fg=colour16,bg=colour39,bold] agentbox \u25B8 ${name} #[default] `,
|
|
721
|
-
";",
|
|
722
|
-
"set",
|
|
723
|
-
"-t",
|
|
724
|
-
s,
|
|
725
|
-
"status-right-length",
|
|
726
|
-
"30",
|
|
727
|
-
";",
|
|
728
|
-
"set",
|
|
729
|
-
"-t",
|
|
730
|
-
s,
|
|
731
|
-
"status-right",
|
|
732
|
-
"#[fg=colour255]Control+a q#[fg=colour245]: detach ",
|
|
733
|
-
";",
|
|
734
|
-
"set",
|
|
735
|
-
"-t",
|
|
736
|
-
s,
|
|
737
|
-
"window-status-format",
|
|
738
|
-
"",
|
|
739
|
-
";",
|
|
740
|
-
"set",
|
|
741
|
-
"-t",
|
|
742
|
-
s,
|
|
743
|
-
"window-status-current-format",
|
|
744
|
-
""
|
|
891
|
+
"status",
|
|
892
|
+
"off"
|
|
745
893
|
];
|
|
746
894
|
}
|
|
747
895
|
function buildShellArgv(container) {
|
|
748
896
|
const term = process.env["TERM"] ?? "xterm-256color";
|
|
749
897
|
return ["exec", "-it", "-e", `TERM=${term}`, "--user", CONTAINER_USER, container, "bash", "-l"];
|
|
750
898
|
}
|
|
751
|
-
function
|
|
752
|
-
|
|
899
|
+
function buildClaudeLoginRunArgv(opts) {
|
|
900
|
+
const term = process.env["TERM"] ?? "xterm-256color";
|
|
901
|
+
return [
|
|
902
|
+
"run",
|
|
903
|
+
"-it",
|
|
904
|
+
"--rm",
|
|
905
|
+
"-e",
|
|
906
|
+
`TERM=${term}`,
|
|
907
|
+
"-e",
|
|
908
|
+
"DISPLAY=",
|
|
909
|
+
"-v",
|
|
910
|
+
`${opts.volume}:${CONTAINER_CLAUDE_DIR}`,
|
|
911
|
+
"--user",
|
|
912
|
+
CONTAINER_USER,
|
|
913
|
+
opts.image,
|
|
914
|
+
"claude",
|
|
915
|
+
"auth",
|
|
916
|
+
"login",
|
|
917
|
+
...opts.extraArgs
|
|
918
|
+
];
|
|
753
919
|
}
|
|
754
|
-
function
|
|
755
|
-
const child = spawnSync("docker",
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
920
|
+
function runInteractiveClaudeLogin(dockerArgv) {
|
|
921
|
+
const child = spawnSync("docker", dockerArgv, { stdio: "inherit" });
|
|
922
|
+
return { exitCode: child.status ?? 1 };
|
|
923
|
+
}
|
|
924
|
+
async function warmUpClaudeCredentials(volume, image, opts = {}) {
|
|
925
|
+
const MAX_ATTEMPTS = 6;
|
|
926
|
+
const SLEEP_MS = 5e3;
|
|
927
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
928
|
+
opts.onProgress?.(`checking credentials... ${attempt}/${MAX_ATTEMPTS}`);
|
|
929
|
+
const res = await execa(
|
|
930
|
+
"docker",
|
|
931
|
+
[
|
|
932
|
+
"run",
|
|
933
|
+
"--rm",
|
|
934
|
+
"-v",
|
|
935
|
+
`${volume}:${CONTAINER_CLAUDE_DIR}`,
|
|
936
|
+
"--user",
|
|
937
|
+
CONTAINER_USER,
|
|
938
|
+
"-e",
|
|
939
|
+
"DISABLE_AUTOUPDATER=1",
|
|
940
|
+
image,
|
|
941
|
+
"claude",
|
|
942
|
+
"--dangerously-skip-permissions",
|
|
943
|
+
"-p",
|
|
944
|
+
"ok"
|
|
945
|
+
],
|
|
946
|
+
{ reject: false, timeout: 6e4 }
|
|
947
|
+
);
|
|
948
|
+
const out = `${res.stdout ?? ""}
|
|
949
|
+
${res.stderr ?? ""}`;
|
|
950
|
+
const apiError = /API Error|is not supported on this model|"type":\s*"error"/i.test(out);
|
|
951
|
+
if (res.exitCode === 0 && !apiError) return { warmed: true, attempts: attempt };
|
|
952
|
+
if (attempt < MAX_ATTEMPTS) await delay(SLEEP_MS);
|
|
761
953
|
}
|
|
762
|
-
|
|
954
|
+
return { warmed: false, attempts: MAX_ATTEMPTS };
|
|
955
|
+
}
|
|
956
|
+
function formatDetachNotice(ref) {
|
|
957
|
+
return `Session detached. Reattach with: agentbox claude attach ${ref}`;
|
|
763
958
|
}
|
|
764
959
|
async function claudeSessionInfo(container, sessionName) {
|
|
765
960
|
const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
|
|
@@ -1053,211 +1248,281 @@ async function isGitDir(path) {
|
|
|
1053
1248
|
return false;
|
|
1054
1249
|
}
|
|
1055
1250
|
}
|
|
1056
|
-
async function
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
);
|
|
1068
|
-
const untrackedList = untracked.exitCode === 0 && untracked.stdout.length > 0 ? untracked.stdout.split("\0").filter((s) => s.length > 0) : [];
|
|
1069
|
-
const branchName = await pickFreshBranch(args.hostMainRepo, args.branchName);
|
|
1070
|
-
const wadd = await execa2(
|
|
1251
|
+
async function pickFreshBranch(hostMainRepo, base) {
|
|
1252
|
+
let candidate = base;
|
|
1253
|
+
let suffix = 2;
|
|
1254
|
+
while (await branchExists(hostMainRepo, candidate)) {
|
|
1255
|
+
candidate = `${base}-${String(suffix++)}`;
|
|
1256
|
+
if (suffix > 100) throw new GitWorktreeError(`could not find a free branch name near ${base}`);
|
|
1257
|
+
}
|
|
1258
|
+
return candidate;
|
|
1259
|
+
}
|
|
1260
|
+
async function branchExists(hostMainRepo, name) {
|
|
1261
|
+
const result = await execa2(
|
|
1071
1262
|
"git",
|
|
1072
|
-
["-C",
|
|
1263
|
+
["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
|
|
1073
1264
|
{ reject: false }
|
|
1074
1265
|
);
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1266
|
+
return result.exitCode === 0;
|
|
1267
|
+
}
|
|
1268
|
+
var GitWorktreeError = class extends Error {
|
|
1269
|
+
constructor(message) {
|
|
1270
|
+
super(message);
|
|
1271
|
+
this.name = "GitWorktreeError";
|
|
1079
1272
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1273
|
+
};
|
|
1274
|
+
var WORKTREE_ROOT = "/home/vscode/.agentbox-worktrees";
|
|
1275
|
+
function fsSafeBranch(branch) {
|
|
1276
|
+
return branch.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
1277
|
+
}
|
|
1278
|
+
function gitWorktreePathFor(branch) {
|
|
1279
|
+
return `${WORKTREE_ROOT}/${fsSafeBranch(branch)}`;
|
|
1280
|
+
}
|
|
1281
|
+
async function collectRepoCarryOver(repo, branch, containerPath, gitWorktreePath) {
|
|
1282
|
+
const stash = await execa3("git", ["-C", repo.hostMainRepo, "stash", "create"], { reject: false });
|
|
1283
|
+
const stashSha = stash.exitCode === 0 ? stash.stdout.trim() || null : null;
|
|
1284
|
+
const untracked = await execa3(
|
|
1082
1285
|
"git",
|
|
1083
|
-
["-C",
|
|
1286
|
+
["-C", repo.hostMainRepo, "ls-files", "--others", "--exclude-standard", "-z"],
|
|
1084
1287
|
{ reject: false }
|
|
1085
1288
|
);
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1289
|
+
const untrackedNul = untracked.exitCode === 0 ? untracked.stdout : "";
|
|
1290
|
+
return {
|
|
1291
|
+
repo,
|
|
1292
|
+
containerPath,
|
|
1293
|
+
gitWorktreePath,
|
|
1294
|
+
branch,
|
|
1295
|
+
stashSha,
|
|
1296
|
+
untrackedNul,
|
|
1297
|
+
hostSource: repo.hostMainRepo
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
async function dexec(container, argv, user = "vscode", cwd = "/") {
|
|
1301
|
+
const r = await execa3(
|
|
1302
|
+
"docker",
|
|
1303
|
+
["exec", "-w", cwd, "--user", user, container, ...argv],
|
|
1089
1304
|
{ reject: false }
|
|
1090
1305
|
);
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1306
|
+
if (r.exitCode !== 0) {
|
|
1307
|
+
throw new GitWorktreeError(`${argv.join(" ")} failed: ${r.stderr || r.stdout}`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async function chownGitBindParents(args) {
|
|
1311
|
+
const log = args.onLog ?? (() => {
|
|
1312
|
+
});
|
|
1313
|
+
const repos = Array.from(new Set(args.hostMainRepos));
|
|
1314
|
+
for (const repo of repos) {
|
|
1315
|
+
const result = await execInBox(args.container, ["chown", "vscode:vscode", repo], {
|
|
1316
|
+
user: "root"
|
|
1317
|
+
});
|
|
1318
|
+
if (result.exitCode === 0) {
|
|
1319
|
+
log(`chowned ${repo} to vscode:vscode (parent of bind-mounted .git)`);
|
|
1320
|
+
} else {
|
|
1321
|
+
const msg = (result.stderr || result.stdout || `exit ${result.exitCode}`).trim();
|
|
1322
|
+
log(`chown ${repo} failed (best-effort, ignoring): ${msg}`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async function bindWorktrees(container, binds, onLog) {
|
|
1327
|
+
const log = onLog ?? (() => {
|
|
1328
|
+
});
|
|
1329
|
+
const ordered = [...binds].sort(
|
|
1330
|
+
(a, b) => a.kind === "root" && b.kind !== "root" ? -1 : a.kind !== "root" && b.kind === "root" ? 1 : 0
|
|
1331
|
+
);
|
|
1332
|
+
for (const b of ordered) {
|
|
1333
|
+
await execa3(
|
|
1334
|
+
"docker",
|
|
1335
|
+
["exec", "-w", "/", "--user", "root", container, "sh", "-c", `mountpoint -q ${b.containerPath} && umount ${b.containerPath} || true`],
|
|
1336
|
+
{ reject: false }
|
|
1337
|
+
);
|
|
1338
|
+
if (b.kind === "nested") {
|
|
1339
|
+
await dexec(container, ["mkdir", "-p", ctParent(b.containerPath)], "root");
|
|
1340
|
+
await dexec(container, ["mkdir", "-p", b.containerPath], "root");
|
|
1341
|
+
}
|
|
1342
|
+
await dexec(container, ["mount", "--bind", b.gitWorktreePath, b.containerPath], "root");
|
|
1343
|
+
log(`bind-mounted ${b.containerPath} <- ${b.gitWorktreePath}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
async function seedWorkspace(opts) {
|
|
1347
|
+
const log = opts.onLog ?? (() => {
|
|
1348
|
+
});
|
|
1349
|
+
await dexec(opts.container, ["mkdir", "-p", WORKTREE_ROOT]);
|
|
1350
|
+
for (const r of opts.repos) {
|
|
1351
|
+
const main = r.repo.hostMainRepo;
|
|
1352
|
+
const wt = r.gitWorktreePath;
|
|
1353
|
+
const add = await execa3(
|
|
1354
|
+
"docker",
|
|
1355
|
+
[
|
|
1356
|
+
"exec",
|
|
1357
|
+
"--user",
|
|
1358
|
+
"vscode",
|
|
1359
|
+
opts.container,
|
|
1360
|
+
"git",
|
|
1361
|
+
"-C",
|
|
1362
|
+
main,
|
|
1363
|
+
"worktree",
|
|
1364
|
+
"add",
|
|
1365
|
+
"-b",
|
|
1366
|
+
r.branch,
|
|
1367
|
+
wt,
|
|
1368
|
+
"HEAD"
|
|
1369
|
+
],
|
|
1370
|
+
{ reject: false }
|
|
1371
|
+
);
|
|
1372
|
+
if (add.exitCode !== 0) {
|
|
1373
|
+
throw new GitWorktreeError(
|
|
1374
|
+
`git worktree add ${wt} (branch ${r.branch}) failed: ${add.stderr || add.stdout}`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
log(`worktree ${wt} on branch ${r.branch} (host main ${main})`);
|
|
1378
|
+
await execa3(
|
|
1379
|
+
"docker",
|
|
1380
|
+
[
|
|
1381
|
+
"exec",
|
|
1382
|
+
"--user",
|
|
1383
|
+
"vscode",
|
|
1384
|
+
opts.container,
|
|
1385
|
+
"git",
|
|
1386
|
+
"-C",
|
|
1387
|
+
main,
|
|
1388
|
+
"config",
|
|
1389
|
+
"extensions.worktreeConfig",
|
|
1390
|
+
"true"
|
|
1391
|
+
],
|
|
1095
1392
|
{ reject: false }
|
|
1096
1393
|
);
|
|
1097
|
-
|
|
1098
|
-
|
|
1394
|
+
await execa3(
|
|
1395
|
+
"docker",
|
|
1396
|
+
[
|
|
1397
|
+
"exec",
|
|
1398
|
+
"--user",
|
|
1399
|
+
"vscode",
|
|
1400
|
+
opts.container,
|
|
1099
1401
|
"git",
|
|
1100
|
-
|
|
1402
|
+
"-C",
|
|
1403
|
+
wt,
|
|
1404
|
+
"config",
|
|
1405
|
+
"--worktree",
|
|
1406
|
+
"commit.gpgsign",
|
|
1407
|
+
"false"
|
|
1408
|
+
],
|
|
1409
|
+
{ reject: false }
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
await bindWorktrees(
|
|
1413
|
+
opts.container,
|
|
1414
|
+
opts.repos.map((r) => ({
|
|
1415
|
+
kind: r.repo.kind,
|
|
1416
|
+
containerPath: r.containerPath,
|
|
1417
|
+
gitWorktreePath: r.gitWorktreePath
|
|
1418
|
+
})),
|
|
1419
|
+
log
|
|
1420
|
+
);
|
|
1421
|
+
for (const r of opts.repos) {
|
|
1422
|
+
const ct = r.containerPath;
|
|
1423
|
+
if (r.stashSha) {
|
|
1424
|
+
const withIndex = await execa3(
|
|
1425
|
+
"docker",
|
|
1426
|
+
[
|
|
1427
|
+
"exec",
|
|
1428
|
+
"--user",
|
|
1429
|
+
"vscode",
|
|
1430
|
+
opts.container,
|
|
1431
|
+
"git",
|
|
1432
|
+
"-C",
|
|
1433
|
+
ct,
|
|
1434
|
+
"stash",
|
|
1435
|
+
"apply",
|
|
1436
|
+
"--index",
|
|
1437
|
+
r.stashSha
|
|
1438
|
+
],
|
|
1101
1439
|
{ reject: false }
|
|
1102
1440
|
);
|
|
1103
|
-
if (
|
|
1104
|
-
|
|
1105
|
-
|
|
1441
|
+
if (withIndex.exitCode !== 0) {
|
|
1442
|
+
const noIndex = await execa3(
|
|
1443
|
+
"docker",
|
|
1444
|
+
[
|
|
1445
|
+
"exec",
|
|
1446
|
+
"--user",
|
|
1447
|
+
"vscode",
|
|
1448
|
+
opts.container,
|
|
1449
|
+
"git",
|
|
1450
|
+
"-C",
|
|
1451
|
+
ct,
|
|
1452
|
+
"stash",
|
|
1453
|
+
"apply",
|
|
1454
|
+
r.stashSha
|
|
1455
|
+
],
|
|
1456
|
+
{ reject: false }
|
|
1106
1457
|
);
|
|
1458
|
+
if (noIndex.exitCode !== 0) {
|
|
1459
|
+
log(
|
|
1460
|
+
`warning: stash apply failed in ${ct} (${withIndex.stderr || withIndex.stdout || "no message"})`
|
|
1461
|
+
);
|
|
1462
|
+
} else {
|
|
1463
|
+
log(`applied tracked changes (without index \u2014 staged state lost) in ${ct}`);
|
|
1464
|
+
}
|
|
1107
1465
|
} else {
|
|
1108
|
-
log(`applied tracked changes
|
|
1466
|
+
log(`applied tracked changes from host main into ${ct}`);
|
|
1109
1467
|
}
|
|
1110
|
-
} else {
|
|
1111
|
-
log(`applied tracked changes from host main`);
|
|
1112
1468
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
"tar",
|
|
1117
|
-
["-C", args.hostMainRepo, "--null", "-T", "-", "-cf", "-"],
|
|
1118
|
-
{
|
|
1119
|
-
input: untrackedList.join("\0"),
|
|
1469
|
+
if (r.untrackedNul.length > 0) {
|
|
1470
|
+
const tarOut = await execa3("tar", ["-C", r.hostSource, "--null", "-T", "-", "-cf", "-"], {
|
|
1471
|
+
input: r.untrackedNul.replace(/\0$/, ""),
|
|
1120
1472
|
encoding: "buffer",
|
|
1121
1473
|
reject: false
|
|
1122
|
-
}
|
|
1123
|
-
);
|
|
1124
|
-
if (tarOut.exitCode === 0) {
|
|
1125
|
-
const tarIn = await execa2("tar", ["-C", args.worktreeDir, "-xf", "-"], {
|
|
1126
|
-
input: tarOut.stdout,
|
|
1127
|
-
reject: false
|
|
1128
1474
|
});
|
|
1475
|
+
if (tarOut.exitCode !== 0) {
|
|
1476
|
+
log(`warning: tar of untracked files for ${r.repo.hostMainRepo} failed: ${tarOut.stderr}`);
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
const tarIn = await execa3(
|
|
1480
|
+
"docker",
|
|
1481
|
+
["exec", "-i", "--user", "vscode", opts.container, "tar", "-C", ct, "-xf", "-"],
|
|
1482
|
+
{ input: tarOut.stdout, reject: false }
|
|
1483
|
+
);
|
|
1129
1484
|
if (tarIn.exitCode !== 0) {
|
|
1130
|
-
log(`warning: untracked-file copy into
|
|
1485
|
+
log(`warning: untracked-file copy into ${ct} failed: ${tarIn.stderr}`);
|
|
1131
1486
|
} else {
|
|
1132
|
-
|
|
1487
|
+
const count = r.untrackedNul.split("\0").filter((s) => s.length > 0).length;
|
|
1488
|
+
log(`copied ${String(count)} untracked file(s) into ${ct}`);
|
|
1133
1489
|
}
|
|
1134
|
-
} else {
|
|
1135
|
-
log(`warning: tar of untracked files failed: ${tarOut.stderr}`);
|
|
1136
1490
|
}
|
|
1137
1491
|
}
|
|
1138
|
-
return { branchName, stashSha, untrackedCount: untrackedList.length };
|
|
1139
1492
|
}
|
|
1140
|
-
async function
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1493
|
+
async function seedWorkspaceFromDir(opts) {
|
|
1494
|
+
const log = opts.onLog ?? (() => {
|
|
1495
|
+
});
|
|
1496
|
+
const tarOut = await execa3("tar", ["-C", opts.hostSource, "-cf", "-", "."], {
|
|
1497
|
+
encoding: "buffer",
|
|
1498
|
+
reject: false
|
|
1499
|
+
});
|
|
1500
|
+
if (tarOut.exitCode !== 0) {
|
|
1501
|
+
throw new GitWorktreeError(`tar of ${opts.hostSource} failed: ${tarOut.stderr}`);
|
|
1146
1502
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
"git",
|
|
1152
|
-
["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
|
|
1153
|
-
{ reject: false }
|
|
1503
|
+
const tarIn = await execa3(
|
|
1504
|
+
"docker",
|
|
1505
|
+
["exec", "-i", "--user", "1000:1000", opts.container, "tar", "-C", "/workspace", "-xf", "-"],
|
|
1506
|
+
{ input: tarOut.stdout, reject: false }
|
|
1154
1507
|
);
|
|
1155
|
-
|
|
1508
|
+
if (tarIn.exitCode !== 0) {
|
|
1509
|
+
throw new GitWorktreeError(`tar extract into /workspace failed: ${tarIn.stderr}`);
|
|
1510
|
+
}
|
|
1511
|
+
log(`seeded /workspace from ${opts.hostSource}`);
|
|
1156
1512
|
}
|
|
1157
|
-
async function
|
|
1158
|
-
const remove = await
|
|
1513
|
+
async function removeInBoxWorktree(args) {
|
|
1514
|
+
const remove = await execa3(
|
|
1159
1515
|
"git",
|
|
1160
|
-
["-C", args.hostMainRepo, "worktree", "remove", "--force", args.
|
|
1516
|
+
["-C", args.hostMainRepo, "worktree", "remove", "--force", args.gitWorktreePath],
|
|
1161
1517
|
{ reject: false }
|
|
1162
1518
|
);
|
|
1163
1519
|
if (remove.exitCode === 0) return;
|
|
1164
|
-
await
|
|
1165
|
-
await execa2("git", ["-C", args.hostMainRepo, "worktree", "prune"], { reject: false });
|
|
1520
|
+
await execa3("git", ["-C", args.hostMainRepo, "worktree", "prune"], { reject: false });
|
|
1166
1521
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
this.name = "GitWorktreeError";
|
|
1171
|
-
}
|
|
1172
|
-
};
|
|
1173
|
-
var DEFAULT_LOWER_DIRS = ["/host-src"];
|
|
1174
|
-
var BOX_USER_UID = 1e3;
|
|
1175
|
-
var BOX_USER_GID = 1e3;
|
|
1176
|
-
async function mountOverlay(container, opts = {}) {
|
|
1177
|
-
const lowerDirs = opts.lowerDirs && opts.lowerDirs.length > 0 ? opts.lowerDirs : ["/host-src"];
|
|
1178
|
-
const mountOpts = [
|
|
1179
|
-
`lowerdir=${lowerDirs.join(":")}`,
|
|
1180
|
-
"upperdir=/upper/upper",
|
|
1181
|
-
"workdir=/upper/work",
|
|
1182
|
-
`squash_to_uid=${String(BOX_USER_UID)}`,
|
|
1183
|
-
`squash_to_gid=${String(BOX_USER_GID)}`
|
|
1184
|
-
].join(",");
|
|
1185
|
-
const lines = [
|
|
1186
|
-
"set -euo pipefail",
|
|
1187
|
-
"mkdir -p /upper/upper /upper/work /workspace",
|
|
1188
|
-
// Idempotent — if a previous attempt left a stale overlay, unmount first.
|
|
1189
|
-
"mountpoint -q /workspace && fusermount3 -u /workspace || true",
|
|
1190
|
-
`fuse-overlayfs -o ${mountOpts} /workspace`,
|
|
1191
|
-
"mountpoint -q /workspace"
|
|
1192
|
-
];
|
|
1193
|
-
for (const w of opts.nestedWorktrees ?? []) {
|
|
1194
|
-
lines.push(
|
|
1195
|
-
`mkdir -p ${shellQuote(w.containerPath)}`,
|
|
1196
|
-
`mountpoint -q ${shellQuote(w.containerPath)} && umount ${shellQuote(w.containerPath)} || true`,
|
|
1197
|
-
`mount --bind ${shellQuote(w.mountFromPath)} ${shellQuote(w.containerPath)}`,
|
|
1198
|
-
`mountpoint -q ${shellQuote(w.containerPath)}`
|
|
1199
|
-
);
|
|
1200
|
-
}
|
|
1201
|
-
const result = await execInBox(container, ["bash", "-lc", lines.join("\n")], { user: "root" });
|
|
1202
|
-
if (result.exitCode !== 0) {
|
|
1203
|
-
throw new OverlayError(
|
|
1204
|
-
`failed to mount FUSE overlay in ${container}`,
|
|
1205
|
-
result.stdout,
|
|
1206
|
-
result.stderr
|
|
1207
|
-
);
|
|
1208
|
-
}
|
|
1209
|
-
return { upperWritePath: "/upper/upper" };
|
|
1210
|
-
}
|
|
1211
|
-
function shellQuote(s) {
|
|
1212
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1522
|
+
function ctParent(p) {
|
|
1523
|
+
const i = p.lastIndexOf("/");
|
|
1524
|
+
return i <= 0 ? "/" : p.slice(0, i);
|
|
1213
1525
|
}
|
|
1214
|
-
async function verifyOverlay(container, lowerDirs = DEFAULT_LOWER_DIRS) {
|
|
1215
|
-
const sentinel = ".agentbox-overlay-check";
|
|
1216
|
-
const checks = [];
|
|
1217
|
-
const ls = await execInBox(container, ["bash", "-lc", `ls -A /workspace | head -1`], {
|
|
1218
|
-
user: "root"
|
|
1219
|
-
});
|
|
1220
|
-
checks.push({
|
|
1221
|
-
name: "workspace lists lower contents",
|
|
1222
|
-
ok: ls.exitCode === 0,
|
|
1223
|
-
detail: ls.exitCode === 0 ? `first entry: ${ls.stdout.trim() || "(empty)"}` : ls.stderr.trim()
|
|
1224
|
-
});
|
|
1225
|
-
const write = await execInBox(container, ["bash", "-lc", `touch /workspace/${sentinel}`], {
|
|
1226
|
-
user: "root"
|
|
1227
|
-
});
|
|
1228
|
-
checks.push({
|
|
1229
|
-
name: "write through overlay succeeds",
|
|
1230
|
-
ok: write.exitCode === 0,
|
|
1231
|
-
detail: write.exitCode === 0 ? `created /workspace/${sentinel}` : write.stderr.trim()
|
|
1232
|
-
});
|
|
1233
|
-
const upper = await execInBox(container, ["bash", "-lc", `test -f /upper/upper/${sentinel}`], {
|
|
1234
|
-
user: "root"
|
|
1235
|
-
});
|
|
1236
|
-
checks.push({
|
|
1237
|
-
name: "write lands in /upper (cow target)",
|
|
1238
|
-
ok: upper.exitCode === 0,
|
|
1239
|
-
detail: upper.exitCode === 0 ? `/upper/upper/${sentinel} exists` : `expected /upper/upper/${sentinel} to exist`
|
|
1240
|
-
});
|
|
1241
|
-
const lowerProbe = lowerDirs.map((d) => `test ! -e ${shellQuote(d)}/${sentinel}`).join(" && ");
|
|
1242
|
-
const lower = await execInBox(container, ["bash", "-lc", lowerProbe], { user: "root" });
|
|
1243
|
-
checks.push({
|
|
1244
|
-
name: `lower untouched (${lowerDirs.join(", ")})`,
|
|
1245
|
-
ok: lower.exitCode === 0,
|
|
1246
|
-
detail: lower.exitCode === 0 ? `${sentinel} absent from every lower` : `${sentinel} leaked into a lower layer`
|
|
1247
|
-
});
|
|
1248
|
-
await execInBox(container, ["bash", "-lc", `rm -f /workspace/${sentinel}`], { user: "root" });
|
|
1249
|
-
return checks;
|
|
1250
|
-
}
|
|
1251
|
-
var OverlayError = class extends Error {
|
|
1252
|
-
constructor(message, stdout, stderr) {
|
|
1253
|
-
super(message);
|
|
1254
|
-
this.stdout = stdout;
|
|
1255
|
-
this.stderr = stderr;
|
|
1256
|
-
this.name = "OverlayError";
|
|
1257
|
-
}
|
|
1258
|
-
stdout;
|
|
1259
|
-
stderr;
|
|
1260
|
-
};
|
|
1261
1526
|
var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
1262
1527
|
"node_modules",
|
|
1263
1528
|
".next",
|
|
@@ -1274,8 +1539,11 @@ var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
|
1274
1539
|
".parcel-cache"
|
|
1275
1540
|
]);
|
|
1276
1541
|
var SNAPSHOTS_ROOT = join3(homedir2(), ".agentbox", "snapshots");
|
|
1277
|
-
function snapshotPathFor(
|
|
1278
|
-
|
|
1542
|
+
function snapshotPathFor(box) {
|
|
1543
|
+
const mnemonic = sanitizeMnemonic(box.name);
|
|
1544
|
+
const n = box.projectIndex;
|
|
1545
|
+
const segment = typeof n === "number" && Number.isFinite(n) && n > 0 ? `${box.id}-${String(n)}-${mnemonic}` : `${box.id}-${mnemonic}`;
|
|
1546
|
+
return join3(SNAPSHOTS_ROOT, segment);
|
|
1279
1547
|
}
|
|
1280
1548
|
async function findExcludedDirs(root, excluded = EXCLUDE_DIRS) {
|
|
1281
1549
|
const matches = [];
|
|
@@ -1305,13 +1573,15 @@ async function createSnapshot(opts) {
|
|
|
1305
1573
|
const excluded = opts.excluded ?? EXCLUDE_DIRS;
|
|
1306
1574
|
await mkdir2(SNAPSHOTS_ROOT, { recursive: true });
|
|
1307
1575
|
const cpArgs = platform() === "darwin" ? ["-cR"] : ["-R"];
|
|
1308
|
-
await
|
|
1576
|
+
await execa4("cp", [...cpArgs, `${source}/`, destination]);
|
|
1309
1577
|
const toPrune = await findExcludedDirs(destination, excluded);
|
|
1310
1578
|
await Promise.all(toPrune.map((p) => rm2(p, { recursive: true, force: true })));
|
|
1311
1579
|
return { destination, prunedPaths: toPrune };
|
|
1312
1580
|
}
|
|
1581
|
+
var CTL_DAEMON_LOG = "/var/log/agentbox/ctl-daemon.log";
|
|
1313
1582
|
async function launchCtlDaemon(container, hostSocketPath, timeoutMs = 3e3) {
|
|
1314
|
-
const
|
|
1583
|
+
const wrapped = `mkdir -p ${CTL_DAEMON_LOG.replace(/\/[^/]*$/, "")} && exec agentbox-ctl daemon >>${CTL_DAEMON_LOG} 2>&1`;
|
|
1584
|
+
const result = await execInBox(container, ["sh", "-c", wrapped], {
|
|
1315
1585
|
user: "vscode",
|
|
1316
1586
|
detach: true
|
|
1317
1587
|
});
|
|
@@ -1336,6 +1606,23 @@ async function pathExists2(p) {
|
|
|
1336
1606
|
return false;
|
|
1337
1607
|
}
|
|
1338
1608
|
}
|
|
1609
|
+
async function ensureHomeOwnedByVscode(container) {
|
|
1610
|
+
await execa5(
|
|
1611
|
+
"docker",
|
|
1612
|
+
[
|
|
1613
|
+
"exec",
|
|
1614
|
+
"--user",
|
|
1615
|
+
"root",
|
|
1616
|
+
container,
|
|
1617
|
+
"chown",
|
|
1618
|
+
"-R",
|
|
1619
|
+
"--from=root",
|
|
1620
|
+
"vscode:vscode",
|
|
1621
|
+
"/home/vscode"
|
|
1622
|
+
],
|
|
1623
|
+
{ reject: false }
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1339
1626
|
var STATE_DIR = join4(homedir3(), ".agentbox");
|
|
1340
1627
|
var PID_FILE = join4(STATE_DIR, "relay.pid");
|
|
1341
1628
|
var LOG_FILE = join4(STATE_DIR, "relay.log");
|
|
@@ -1364,7 +1651,7 @@ async function ensureRelay(opts = {}) {
|
|
|
1364
1651
|
if (existingPid !== null && await processAlive(existingPid)) {
|
|
1365
1652
|
for (let i = 0; i < 10; i++) {
|
|
1366
1653
|
if (await pingHealthz(300)) return ENDPOINT;
|
|
1367
|
-
await
|
|
1654
|
+
await delay2(200);
|
|
1368
1655
|
}
|
|
1369
1656
|
log(`relay pid ${String(existingPid)} alive but /healthz unresponsive \u2014 proceeding anyway`);
|
|
1370
1657
|
return ENDPOINT;
|
|
@@ -1398,7 +1685,7 @@ async function ensureRelay(opts = {}) {
|
|
|
1398
1685
|
log(`relay reachable on ${ENDPOINT.hostUrl}`);
|
|
1399
1686
|
return ENDPOINT;
|
|
1400
1687
|
}
|
|
1401
|
-
await
|
|
1688
|
+
await delay2(200);
|
|
1402
1689
|
}
|
|
1403
1690
|
throw new Error(
|
|
1404
1691
|
`relay did not become reachable on ${ENDPOINT.hostUrl} within 5s; see ${LOG_FILE}`
|
|
@@ -1454,7 +1741,7 @@ async function stopRelay() {
|
|
|
1454
1741
|
}
|
|
1455
1742
|
for (let i = 0; i < 20; i++) {
|
|
1456
1743
|
if (!await processAlive(pid)) break;
|
|
1457
|
-
await
|
|
1744
|
+
await delay2(100);
|
|
1458
1745
|
}
|
|
1459
1746
|
if (await processAlive(pid)) {
|
|
1460
1747
|
try {
|
|
@@ -1466,6 +1753,21 @@ async function stopRelay() {
|
|
|
1466
1753
|
});
|
|
1467
1754
|
return { stopped: true, pid };
|
|
1468
1755
|
}
|
|
1756
|
+
async function getRelayStatus() {
|
|
1757
|
+
const pid = await readPidFile();
|
|
1758
|
+
const pidAlive = pid !== null && await processAlive(pid);
|
|
1759
|
+
const health = await fetchHealthz(300);
|
|
1760
|
+
return {
|
|
1761
|
+
running: health !== null,
|
|
1762
|
+
pid,
|
|
1763
|
+
pidAlive,
|
|
1764
|
+
port: PORT,
|
|
1765
|
+
endpoint: ENDPOINT,
|
|
1766
|
+
health: health === null ? null : { boxes: health.boxes, events: health.events },
|
|
1767
|
+
pidFile: PID_FILE,
|
|
1768
|
+
logFile: LOG_FILE
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1469
1771
|
function pingHealthz(timeoutMs) {
|
|
1470
1772
|
return new Promise((resolveP) => {
|
|
1471
1773
|
const req = httpRequest(
|
|
@@ -1484,6 +1786,42 @@ function pingHealthz(timeoutMs) {
|
|
|
1484
1786
|
req.end();
|
|
1485
1787
|
});
|
|
1486
1788
|
}
|
|
1789
|
+
function fetchHealthz(timeoutMs) {
|
|
1790
|
+
return new Promise((resolveP) => {
|
|
1791
|
+
const req = httpRequest(
|
|
1792
|
+
{ host: "127.0.0.1", port: PORT, method: "GET", path: "/healthz", timeout: timeoutMs },
|
|
1793
|
+
(res) => {
|
|
1794
|
+
const status = res.statusCode ?? 0;
|
|
1795
|
+
if (status < 200 || status >= 300) {
|
|
1796
|
+
res.resume();
|
|
1797
|
+
resolveP(null);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const chunks = [];
|
|
1801
|
+
res.on("data", (c) => chunks.push(c));
|
|
1802
|
+
res.on("end", () => {
|
|
1803
|
+
try {
|
|
1804
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1805
|
+
if (typeof parsed.ok === "boolean" && typeof parsed.boxes === "number" && typeof parsed.events === "number") {
|
|
1806
|
+
resolveP({ ok: parsed.ok, boxes: parsed.boxes, events: parsed.events });
|
|
1807
|
+
} else {
|
|
1808
|
+
resolveP(null);
|
|
1809
|
+
}
|
|
1810
|
+
} catch {
|
|
1811
|
+
resolveP(null);
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
res.on("error", () => resolveP(null));
|
|
1815
|
+
}
|
|
1816
|
+
);
|
|
1817
|
+
req.on("error", () => resolveP(null));
|
|
1818
|
+
req.on("timeout", () => {
|
|
1819
|
+
req.destroy();
|
|
1820
|
+
resolveP(null);
|
|
1821
|
+
});
|
|
1822
|
+
req.end();
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1487
1825
|
async function readPidFile() {
|
|
1488
1826
|
try {
|
|
1489
1827
|
const text = await readFile2(PID_FILE, "utf8");
|
|
@@ -1507,7 +1845,7 @@ function generateRelayToken() {
|
|
|
1507
1845
|
async function registerBoxWithRelay(args) {
|
|
1508
1846
|
const worktrees = (args.worktrees ?? []).map((w) => ({
|
|
1509
1847
|
containerPath: w.containerPath,
|
|
1510
|
-
|
|
1848
|
+
hostMainRepo: w.hostMainRepo,
|
|
1511
1849
|
branch: w.branch
|
|
1512
1850
|
}));
|
|
1513
1851
|
await adminPost("/admin/register-box", {
|
|
@@ -1516,6 +1854,7 @@ async function registerBoxWithRelay(args) {
|
|
|
1516
1854
|
name: args.name,
|
|
1517
1855
|
containerName: args.containerName,
|
|
1518
1856
|
createdAt: args.createdAt,
|
|
1857
|
+
projectIndex: args.projectIndex,
|
|
1519
1858
|
worktrees
|
|
1520
1859
|
});
|
|
1521
1860
|
}
|
|
@@ -1525,6 +1864,26 @@ async function forgetBoxFromRelay(boxId) {
|
|
|
1525
1864
|
} catch {
|
|
1526
1865
|
}
|
|
1527
1866
|
}
|
|
1867
|
+
async function setRelayNotice(boxId, kind, message, ttlMs) {
|
|
1868
|
+
try {
|
|
1869
|
+
const body = await adminPostForJson("/admin/notices/set", {
|
|
1870
|
+
boxId,
|
|
1871
|
+
kind,
|
|
1872
|
+
message,
|
|
1873
|
+
...typeof ttlMs === "number" ? { ttlMs } : {}
|
|
1874
|
+
});
|
|
1875
|
+
const id = body?.id;
|
|
1876
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
1877
|
+
} catch {
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
async function clearRelayNotice(boxId, id) {
|
|
1882
|
+
try {
|
|
1883
|
+
await adminPost("/admin/notices/clear", { boxId, id });
|
|
1884
|
+
} catch {
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1528
1887
|
async function adminPost(path, body) {
|
|
1529
1888
|
const json = JSON.stringify(body);
|
|
1530
1889
|
await new Promise((resolveP, rejectP) => {
|
|
@@ -1563,6 +1922,49 @@ async function adminPost(path, body) {
|
|
|
1563
1922
|
req.end();
|
|
1564
1923
|
});
|
|
1565
1924
|
}
|
|
1925
|
+
async function adminPostForJson(path, body) {
|
|
1926
|
+
const json = JSON.stringify(body);
|
|
1927
|
+
return new Promise((resolveP, rejectP) => {
|
|
1928
|
+
const req = httpRequest(
|
|
1929
|
+
{
|
|
1930
|
+
host: "127.0.0.1",
|
|
1931
|
+
port: PORT,
|
|
1932
|
+
method: "POST",
|
|
1933
|
+
path,
|
|
1934
|
+
headers: {
|
|
1935
|
+
"Content-Type": "application/json",
|
|
1936
|
+
"Content-Length": Buffer.byteLength(json).toString()
|
|
1937
|
+
},
|
|
1938
|
+
timeout: 3e3
|
|
1939
|
+
},
|
|
1940
|
+
(res) => {
|
|
1941
|
+
const chunks = [];
|
|
1942
|
+
res.on("data", (c) => chunks.push(c));
|
|
1943
|
+
res.on("end", () => {
|
|
1944
|
+
const status = res.statusCode ?? 0;
|
|
1945
|
+
if (status < 200 || status >= 300) {
|
|
1946
|
+
rejectP(new Error(`relay ${path} \u2192 ${String(status)}`));
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
1950
|
+
try {
|
|
1951
|
+
resolveP(text.length > 0 ? JSON.parse(text) : {});
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
rejectP(err instanceof Error ? err : new Error(String(err)));
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
res.on("error", rejectP);
|
|
1957
|
+
}
|
|
1958
|
+
);
|
|
1959
|
+
req.on("error", rejectP);
|
|
1960
|
+
req.on("timeout", () => {
|
|
1961
|
+
req.destroy();
|
|
1962
|
+
rejectP(new Error(`relay ${path} timeout`));
|
|
1963
|
+
});
|
|
1964
|
+
req.write(json);
|
|
1965
|
+
req.end();
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1566
1968
|
async function rehydrateRelayRegistry(boxes) {
|
|
1567
1969
|
for (const b of boxes) {
|
|
1568
1970
|
if (!b.relayToken) continue;
|
|
@@ -1573,6 +1975,7 @@ async function rehydrateRelayRegistry(boxes) {
|
|
|
1573
1975
|
name: b.name,
|
|
1574
1976
|
containerName: b.container,
|
|
1575
1977
|
createdAt: b.createdAt,
|
|
1978
|
+
projectIndex: b.projectIndex,
|
|
1576
1979
|
worktrees: b.gitWorktrees
|
|
1577
1980
|
});
|
|
1578
1981
|
} catch {
|
|
@@ -1682,10 +2085,10 @@ async function ensureAgentboxTasksFile(container, services, opts = {}) {
|
|
|
1682
2085
|
return { status: "wrote" };
|
|
1683
2086
|
}
|
|
1684
2087
|
async function writeFileInBox(container, path, content) {
|
|
1685
|
-
const { execa:
|
|
1686
|
-
const result = await
|
|
2088
|
+
const { execa: execa6 } = await import("execa");
|
|
2089
|
+
const result = await execa6(
|
|
1687
2090
|
"docker",
|
|
1688
|
-
["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${
|
|
2091
|
+
["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote(path)}`],
|
|
1689
2092
|
{ input: content, reject: false }
|
|
1690
2093
|
);
|
|
1691
2094
|
return {
|
|
@@ -1694,7 +2097,7 @@ async function writeFileInBox(container, path, content) {
|
|
|
1694
2097
|
stderr: result.stderr ?? ""
|
|
1695
2098
|
};
|
|
1696
2099
|
}
|
|
1697
|
-
function
|
|
2100
|
+
function shellQuote(s) {
|
|
1698
2101
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1699
2102
|
}
|
|
1700
2103
|
|
|
@@ -2181,19 +2584,25 @@ async function loadConfig(path) {
|
|
|
2181
2584
|
}
|
|
2182
2585
|
|
|
2183
2586
|
export {
|
|
2587
|
+
DEFAULT_RELAY_PORT,
|
|
2184
2588
|
RELAY_CONTAINER_NAME,
|
|
2185
2589
|
RELAY_NETWORK_NAME,
|
|
2186
2590
|
RELAY_IMAGE_REF,
|
|
2187
2591
|
SHARED_CLAUDE_VOLUME,
|
|
2188
2592
|
resolveClaudeVolume,
|
|
2189
2593
|
ensureClaudeVolume,
|
|
2594
|
+
seedSetupSkillIntoVolume,
|
|
2190
2595
|
buildClaudeMounts,
|
|
2191
2596
|
rebuildPluginNativeDeps,
|
|
2192
2597
|
ClaudeSessionError,
|
|
2193
2598
|
startClaudeSession,
|
|
2599
|
+
buildClaudeAttachArgv,
|
|
2194
2600
|
buildClaudeDashboardAttachArgv,
|
|
2195
2601
|
buildShellArgv,
|
|
2196
|
-
|
|
2602
|
+
buildClaudeLoginRunArgv,
|
|
2603
|
+
runInteractiveClaudeLogin,
|
|
2604
|
+
warmUpClaudeCredentials,
|
|
2605
|
+
formatDetachNotice,
|
|
2197
2606
|
claudeSessionInfo,
|
|
2198
2607
|
pullClaudeExtras,
|
|
2199
2608
|
SHARED_DOCKER_CACHE_VOLUME,
|
|
@@ -2205,20 +2614,27 @@ export {
|
|
|
2205
2614
|
buildVncUrls,
|
|
2206
2615
|
WEB_CONTAINER_PORT,
|
|
2207
2616
|
detectGitRepos,
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2617
|
+
pickFreshBranch,
|
|
2618
|
+
gitWorktreePathFor,
|
|
2619
|
+
collectRepoCarryOver,
|
|
2620
|
+
chownGitBindParents,
|
|
2621
|
+
bindWorktrees,
|
|
2622
|
+
seedWorkspace,
|
|
2623
|
+
seedWorkspaceFromDir,
|
|
2624
|
+
removeInBoxWorktree,
|
|
2213
2625
|
SNAPSHOTS_ROOT,
|
|
2214
2626
|
snapshotPathFor,
|
|
2215
2627
|
createSnapshot,
|
|
2216
2628
|
launchCtlDaemon,
|
|
2629
|
+
ensureHomeOwnedByVscode,
|
|
2217
2630
|
ensureRelay,
|
|
2218
2631
|
stopRelay,
|
|
2632
|
+
getRelayStatus,
|
|
2219
2633
|
generateRelayToken,
|
|
2220
2634
|
registerBoxWithRelay,
|
|
2221
2635
|
forgetBoxFromRelay,
|
|
2636
|
+
setRelayNotice,
|
|
2637
|
+
clearRelayNotice,
|
|
2222
2638
|
rehydrateRelayRegistry,
|
|
2223
2639
|
ideProfile,
|
|
2224
2640
|
SHARED_VSCODE_EXTENSIONS_VOLUME,
|
|
@@ -2236,4 +2652,4 @@ export {
|
|
|
2236
2652
|
ConfigError,
|
|
2237
2653
|
loadConfig
|
|
2238
2654
|
};
|
|
2239
|
-
//# sourceMappingURL=chunk-
|
|
2655
|
+
//# sourceMappingURL=chunk-HTTKML3C.js.map
|