@madarco/agentbox 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/_cloud-attach-T727ZPRV.js +13 -0
  2. package/dist/chunk-67N47KUS.js +1640 -0
  3. package/dist/chunk-67N47KUS.js.map +1 -0
  4. package/dist/chunk-6OZDFNBF.js +8114 -0
  5. package/dist/chunk-6OZDFNBF.js.map +1 -0
  6. package/dist/chunk-BGK32PZE.js +455 -0
  7. package/dist/chunk-BGK32PZE.js.map +1 -0
  8. package/dist/chunk-FODMEHD3.js +1200 -0
  9. package/dist/chunk-FODMEHD3.js.map +1 -0
  10. package/dist/chunk-G3H2L3O2.js +288 -0
  11. package/dist/chunk-G3H2L3O2.js.map +1 -0
  12. package/dist/chunk-I24B6AXR.js +600 -0
  13. package/dist/chunk-I24B6AXR.js.map +1 -0
  14. package/dist/chunk-LEV3KICD.js +738 -0
  15. package/dist/chunk-LEV3KICD.js.map +1 -0
  16. package/dist/cloud-poller-SUNA6ZQC-2RG5WPRN.js +10 -0
  17. package/dist/dist-L4LCG5SJ.js +293 -0
  18. package/dist/dist-L4LCG5SJ.js.map +1 -0
  19. package/dist/dist-LOZBWMBF.js +447 -0
  20. package/dist/dist-ZODPD2I6.js +1407 -0
  21. package/dist/dist-ZODPD2I6.js.map +1 -0
  22. package/dist/index.js +7281 -2134
  23. package/dist/index.js.map +1 -1
  24. package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js +18 -0
  25. package/package.json +8 -3
  26. package/runtime/daytona/custom-system-CLAUDE.md +39 -0
  27. package/runtime/docker/Dockerfile.box +120 -14
  28. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +15 -8
  29. package/runtime/docker/packages/ctl/dist/bin.cjs +11310 -816
  30. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +68 -0
  31. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +9 -9
  32. package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +62 -1
  33. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +15 -4
  34. package/runtime/docker/packages/sandbox-docker/scripts/gh-shim +263 -0
  35. package/runtime/docker/packages/sandbox-docker/scripts/git-shim +131 -0
  36. package/runtime/docker/packages/sandbox-docker/scripts/opencode-agentbox-plugin.js +76 -0
  37. package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
  38. package/runtime/hetzner/agentbox-codex-hooks.json +68 -0
  39. package/runtime/hetzner/agentbox-dockerd-start +132 -0
  40. package/runtime/hetzner/agentbox-open +28 -0
  41. package/runtime/hetzner/agentbox-setup-skill.md +196 -0
  42. package/runtime/hetzner/agentbox-vnc-start +77 -0
  43. package/runtime/hetzner/claude-managed-settings.json +115 -0
  44. package/runtime/hetzner/ctl.cjs +23397 -0
  45. package/runtime/hetzner/custom-system-CLAUDE.md +39 -0
  46. package/runtime/hetzner/gh-shim +263 -0
  47. package/runtime/hetzner/git-shim +131 -0
  48. package/runtime/hetzner/opencode-agentbox-plugin.js +76 -0
  49. package/runtime/hetzner/scripts/install-box.sh +374 -0
  50. package/runtime/relay/bin.cjs +10017 -817
  51. package/share/agentbox-setup/SKILL.md +15 -8
  52. package/share/host-skills/agentbox/SKILL.md +29 -0
  53. package/share/host-skills/agentbox-info/SKILL.md +211 -0
  54. package/share/host-skills/codex/agentbox.md +35 -0
  55. package/share/host-skills/opencode/agentbox.md +26 -0
  56. package/dist/chunk-BBZMA2K6.js +0 -238
  57. package/dist/chunk-BBZMA2K6.js.map +0 -1
  58. package/dist/chunk-HHMWQNLF.js +0 -1709
  59. package/dist/chunk-HHMWQNLF.js.map +0 -1
  60. package/dist/chunk-HPZMD5DE.js +0 -106
  61. package/dist/chunk-HPZMD5DE.js.map +0 -1
  62. package/dist/chunk-HTTKML3C.js +0 -2655
  63. package/dist/chunk-HTTKML3C.js.map +0 -1
  64. package/dist/chunk-KJNZP6I3.js +0 -586
  65. package/dist/chunk-KJNZP6I3.js.map +0 -1
  66. package/dist/chunk-M7I247BK.js +0 -525
  67. package/dist/chunk-M7I247BK.js.map +0 -1
  68. package/dist/create-6PWXI6HO-OWAMHBAK.js +0 -15
  69. package/dist/lifecycle-EMXR46DI-DUVBXNTV.js +0 -38
  70. package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
  71. package/dist/stats-SZXOJE3D-N7OODCHW.js +0 -19
  72. /package/dist/{create-6PWXI6HO-OWAMHBAK.js.map → _cloud-attach-T727ZPRV.js.map} +0 -0
  73. /package/dist/{lifecycle-EMXR46DI-DUVBXNTV.js.map → cloud-poller-SUNA6ZQC-2RG5WPRN.js.map} +0 -0
  74. /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-LOZBWMBF.js.map} +0 -0
  75. /package/dist/{stats-SZXOJE3D-N7OODCHW.js.map → prepared-state-CL4CWXQA-ME4HSKDE.js.map} +0 -0
@@ -1,2655 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- containerExists,
4
- detectEngine,
5
- ensureVolume,
6
- execInBox,
7
- orbstackVolumePath,
8
- removeContainer,
9
- sanitizeMnemonic,
10
- volumeExists
11
- } from "./chunk-HHMWQNLF.js";
12
-
13
- // ../../packages/sandbox-docker/dist/chunk-SRQIM7LG.js
14
- import { spawnSync } from "child_process";
15
- import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "fs/promises";
16
- import { homedir, tmpdir } from "os";
17
- import { join, relative } from "path";
18
- import { setTimeout as delay } from "timers/promises";
19
- import { execa } from "execa";
20
- import { randomBytes } from "crypto";
21
- import { execa as execa2 } from "execa";
22
- import { readdir as readdir2, stat as stat2 } from "fs/promises";
23
- import { join as join2 } from "path";
24
- import { execa as execa3 } from "execa";
25
- import { execa as execa4 } from "execa";
26
- import { mkdir as mkdir2, readdir as readdir3, rm as rm2, stat as stat3 } from "fs/promises";
27
- import { homedir as homedir2, platform } from "os";
28
- import { join as join3, resolve } from "path";
29
- import { stat as stat4 } from "fs/promises";
30
- import { execa as execa5 } from "execa";
31
- import { spawn } from "child_process";
32
- import { randomBytes as randomBytes2 } from "crypto";
33
- import { existsSync, openSync } from "fs";
34
- import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2 } from "fs/promises";
35
- import { request as httpRequest } from "http";
36
- import { homedir as homedir3 } from "os";
37
- import { dirname, join as join4, resolve as resolve2 } from "path";
38
- import { setTimeout as delay2 } from "timers/promises";
39
- import { fileURLToPath } from "url";
40
-
41
- // ../../packages/relay/dist/index.js
42
- var DEFAULT_RELAY_PORT = 8787;
43
- var RELAY_CONTAINER_NAME = "agentbox-relay";
44
- var RELAY_NETWORK_NAME = "agentbox-net";
45
- var RELAY_IMAGE_REF = "agentbox/relay:dev";
46
- var MAX_BODY_BYTES = 1024 * 1024;
47
-
48
- // ../../packages/sandbox-docker/dist/chunk-SRQIM7LG.js
49
- function isHostPathHookCommand(command, hostHome) {
50
- if (typeof command !== "string" || command.length === 0) return false;
51
- if (hostHome.length === 0) return false;
52
- return command.includes(hostHome + "/");
53
- }
54
- function filterHostHooks(data, hostHome) {
55
- const clone = structuredClone(data);
56
- const removedCommands = [];
57
- if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
58
- return { data: clone, removedCommands };
59
- }
60
- const top = clone;
61
- const hooksRoot = top.hooks;
62
- if (hooksRoot === null || typeof hooksRoot !== "object" || Array.isArray(hooksRoot)) {
63
- return { data: clone, removedCommands };
64
- }
65
- for (const triggerName of Object.keys(hooksRoot)) {
66
- const triggerValue = hooksRoot[triggerName];
67
- if (!Array.isArray(triggerValue)) continue;
68
- for (const entry of triggerValue) {
69
- if (entry === null || typeof entry !== "object") continue;
70
- const matcher = entry;
71
- const inner = matcher.hooks;
72
- if (!Array.isArray(inner)) continue;
73
- for (let i = inner.length - 1; i >= 0; i--) {
74
- const leaf = inner[i];
75
- if (leaf === null || typeof leaf !== "object") continue;
76
- if (leaf.type === "command" && typeof leaf.command === "string" && isHostPathHookCommand(leaf.command, hostHome)) {
77
- removedCommands.push(leaf.command);
78
- inner.splice(i, 1);
79
- }
80
- }
81
- }
82
- }
83
- return { data: clone, removedCommands };
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
- }
106
- function addProjectAlias(data, fromPath, toPath) {
107
- const clone = structuredClone(data);
108
- if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
109
- return { data: clone, aliased: false };
110
- }
111
- if (fromPath === toPath || fromPath.length === 0 || toPath.length === 0) {
112
- return { data: clone, aliased: false };
113
- }
114
- const obj = clone;
115
- const projects = obj.projects;
116
- if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
117
- return { data: clone, aliased: false };
118
- }
119
- const projectsMap = projects;
120
- const src = projectsMap[fromPath];
121
- if (src === null || typeof src !== "object" || Array.isArray(src)) {
122
- return { data: clone, aliased: false };
123
- }
124
- const existing = projectsMap[toPath];
125
- if (existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
126
- projectsMap[toPath] = { ...existing, ...src };
127
- } else {
128
- projectsMap[toPath] = structuredClone(src);
129
- }
130
- return { data: clone, aliased: true };
131
- }
132
- function setInstallMethodNative(data) {
133
- const clone = structuredClone(data);
134
- if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
135
- return { data: clone, applied: false };
136
- }
137
- const obj = 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 };
143
- }
144
- var SKILL_EXCLUDE_PREFIXES = ["agentbox-"];
145
- var CONTAINER_PLUGINS_PREFIX = "/home/vscode/.claude/plugins/";
146
- function pickNewItems(boxNames, hostNames, excludePrefixes = []) {
147
- const host = new Set(hostNames);
148
- const seen = /* @__PURE__ */ new Set();
149
- const out = [];
150
- for (const name of boxNames) {
151
- if (name.length === 0 || host.has(name) || seen.has(name)) continue;
152
- if (excludePrefixes.some((p) => name.startsWith(p))) continue;
153
- seen.add(name);
154
- out.push(name);
155
- }
156
- return out.sort();
157
- }
158
- function rewritePluginPaths(value, hostPluginsPrefix) {
159
- if (typeof value === "string") {
160
- return value.split(CONTAINER_PLUGINS_PREFIX).join(hostPluginsPrefix);
161
- }
162
- if (Array.isArray(value)) {
163
- return value.map((v) => rewritePluginPaths(v, hostPluginsPrefix));
164
- }
165
- if (value !== null && typeof value === "object") {
166
- const out = {};
167
- for (const [k, v] of Object.entries(value)) {
168
- out[k] = rewritePluginPaths(v, hostPluginsPrefix);
169
- }
170
- return out;
171
- }
172
- return value;
173
- }
174
- function isPlainObject(v) {
175
- return v !== null && typeof v === "object" && !Array.isArray(v);
176
- }
177
- function additiveMerge(hostRoot, boxRoot, hostPluginsPrefix, selectMap, withMap) {
178
- const hostMap = selectMap(hostRoot);
179
- const boxMap = selectMap(boxRoot);
180
- if (!isPlainObject(boxMap)) {
181
- return { data: hostRoot, changed: false, addedKeys: [] };
182
- }
183
- const base = isPlainObject(hostMap) ? { ...hostMap } : {};
184
- const addedKeys = [];
185
- for (const [key, value] of Object.entries(boxMap)) {
186
- if (Object.prototype.hasOwnProperty.call(base, key)) continue;
187
- base[key] = rewritePluginPaths(value, hostPluginsPrefix);
188
- addedKeys.push(key);
189
- }
190
- if (addedKeys.length === 0) {
191
- return { data: hostRoot, changed: false, addedKeys: [] };
192
- }
193
- return { data: withMap(hostRoot, base), changed: true, addedKeys: addedKeys.sort() };
194
- }
195
- function mergeKnownMarketplaces(hostJson, boxJson, opts) {
196
- const prefix = `${opts.hostHome}/.claude/plugins/`;
197
- return additiveMerge(
198
- isPlainObject(hostJson) ? hostJson : {},
199
- boxJson,
200
- prefix,
201
- (root) => root,
202
- (_host, merged) => merged
203
- );
204
- }
205
- function mergeInstalledPlugins(hostJson, boxJson, opts) {
206
- const prefix = `${opts.hostHome}/.claude/plugins/`;
207
- const hostRoot = isPlainObject(hostJson) ? hostJson : { plugins: {} };
208
- return additiveMerge(
209
- hostRoot,
210
- boxJson,
211
- prefix,
212
- (root) => isPlainObject(root) ? root["plugins"] : void 0,
213
- (host, merged) => ({ ...host, plugins: merged })
214
- );
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
- }
234
- var SHARED_CLAUDE_VOLUME = "agentbox-claude-config";
235
- var DEFAULT_CLAUDE_SESSION = "claude";
236
- var CONTAINER_CLAUDE_DIR = "/home/vscode/.claude";
237
- var CONTAINER_USER = "vscode";
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";
241
- function resolveClaudeVolume(opts) {
242
- if (opts.isolate) {
243
- return { volume: `${SHARED_CLAUDE_VOLUME}-${opts.boxId}` };
244
- }
245
- return { volume: SHARED_CLAUDE_VOLUME };
246
- }
247
- async function pathExists(p) {
248
- try {
249
- await stat(p);
250
- return true;
251
- } catch {
252
- return false;
253
- }
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
- }
263
- async function findBrokenSymlinks(root) {
264
- const broken = [];
265
- async function walk(dir) {
266
- let entries;
267
- try {
268
- entries = await readdir(dir, { withFileTypes: true });
269
- } catch {
270
- return;
271
- }
272
- for (const ent of entries) {
273
- const full = join(dir, ent.name);
274
- if (ent.isSymbolicLink()) {
275
- try {
276
- await stat(full);
277
- } catch {
278
- broken.push(relative(root, full));
279
- }
280
- } else if (ent.isDirectory()) {
281
- await walk(full);
282
- }
283
- }
284
- }
285
- await walk(root);
286
- return broken;
287
- }
288
- async function ensureClaudeVolume(spec, opts) {
289
- const existed = await volumeExists(spec.volume);
290
- await ensureVolume(spec.volume);
291
- const created = !existed;
292
- if (!opts.syncFromHost) return { created, synced: false };
293
- const hostClaude = join(homedir(), ".claude");
294
- if (!await pathExists(hostClaude)) return { created, synced: false };
295
- const hostClaudeJson = join(homedir(), ".claude.json");
296
- const hasJson = await pathExists(hostClaudeJson);
297
- const seedClaudeJson = !await volumeHasClaudeJson(spec.volume, opts.image);
298
- const hostHome = homedir();
299
- const hostAgents = join(homedir(), ".agents");
300
- const hasAgents = await pathExists(hostAgents);
301
- const args = [
302
- "run",
303
- "--rm",
304
- "--user",
305
- "0",
306
- // HOST_HOME used inside the shell script to rewrite host-absolute
307
- // installPath values in plugins/installed_plugins.json.
308
- "-e",
309
- `HOST_HOME=${hostHome}`,
310
- "-v",
311
- `${spec.volume}:/dst`,
312
- "-v",
313
- `${hostClaude}:/src-claude:ro`
314
- ];
315
- if (hasJson && seedClaudeJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
316
- if (hasAgents) args.push("-v", `${hostAgents}:/.agents:ro`);
317
- const filterDir = await mkdtemp(join(tmpdir(), "agentbox-claude-filter-"));
318
- let filteredHookCount = 0;
319
- let installMethodFixed = false;
320
- let aliasedProjectKey = false;
321
- let workspaceTrusted = false;
322
- try {
323
- const settingsResult = await maybeFilterTo(
324
- join(hostClaude, "settings.json"),
325
- join(filterDir, "settings.json"),
326
- hostHome
327
- );
328
- filteredHookCount += settingsResult.removedHooks;
329
- if (!seedClaudeJson) {
330
- } else if (hasJson) {
331
- const jsonResult = await maybeFilterTo(
332
- hostClaudeJson,
333
- join(filterDir, "_claude.json"),
334
- hostHome,
335
- {
336
- setInstallMethodNative: true,
337
- aliasProject: opts.hostWorkspace ? { from: opts.hostWorkspace, to: CONTAINER_WORKSPACE } : void 0,
338
- trustWorkspacePath: CONTAINER_WORKSPACE
339
- }
340
- );
341
- filteredHookCount += jsonResult.removedHooks;
342
- installMethodFixed = jsonResult.installMethodFixed;
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;
361
- }
362
- if (filteredHookCount > 0 || installMethodFixed || aliasedProjectKey || workspaceTrusted) {
363
- args.push("-v", `${filterDir}:/src-filter:ro`);
364
- }
365
- const brokenSymlinks = await findBrokenSymlinks(hostClaude);
366
- const rsyncExcludes = ["--exclude=node_modules"];
367
- for (const rel of brokenSymlinks) rsyncExcludes.push(`--exclude=/${rel}`);
368
- const rsyncFlags = `-a --copy-unsafe-links ${rsyncExcludes.join(" ")}`;
369
- args.push(
370
- opts.image,
371
- "sh",
372
- "-c",
373
- // Each step in its own brace group so a missing optional file (no
374
- // .claude.json on host, no filtered overlays) doesn't short-circuit the
375
- // final chown.
376
- //
377
- // --copy-unsafe-links: dereference symlinks pointing OUTSIDE
378
- // /src-claude (e.g. ~/.claude/skills/* -> ../../.agents/skills/*),
379
- // so user skills materialize as real directories inside the volume
380
- // without needing to also bind-mount ~/.agents.
381
- // --exclude=node_modules: skip every node_modules directory anywhere
382
- // in the tree. Plugin caches (plugins/cache/<m>/<p>/<v>/node_modules)
383
- // ship host-platform-specific binaries (darwin-arm64 fsevents,
384
- // esbuild, rollup, sharp) that are useless on linux/amd64. The
385
- // plugin source still lands; node_modules is rebuilt lazily inside
386
- // the box on first claude session (see rebuildPluginNativeDeps).
387
- //
388
- // The top-level plugin registry JSONs (installed_plugins.json,
389
- // known_marketplaces.json) carry host-absolute `installPath` /
390
- // `installLocation` values; without rewriting, claude resolves them
391
- // to `/Users/<you>/...` (or, when claude detects the missing path,
392
- // falls back to a slug derived from `source.repo` like
393
- // `microsoft-playwright-cli` — neither exists in the box, and the
394
- // marketplace fails to load, which masquerades as "plugin not
395
- // found in marketplace"). One sweep over every JSON directly under
396
- // /dst/plugins/ catches both files (and any future registry).
397
- // One-shot migration for volumes that were populated before
398
- // --exclude=node_modules existed. Without it, the volume keeps
399
- // host-darwin node_modules forever (rsync without --delete won't
400
- // remove them). The `.agentbox-cleaned-nm-v1` sentinel makes the wipe
401
- // a no-op after the first run; rebuildPluginNativeDeps repopulates
402
- // linux/amd64 node_modules on the next `agentbox claude`.
403
- `{ [ ! -f /dst/.agentbox-cleaned-nm-v1 ] && find /dst -name node_modules -type d -prune -exec rm -rf {} + && touch /dst/.agentbox-cleaned-nm-v1; true; } && rsync ${rsyncFlags} /src-claude/ /dst/ && { [ -f /src-claude-json ] && cp -a /src-claude-json /dst/_claude.json; true; } && { [ -f /src-filter/settings.json ] && cp -a /src-filter/settings.json /dst/settings.json; true; } && { [ -f /src-filter/_claude.json ] && cp -a /src-filter/_claude.json /dst/_claude.json; true; } && { [ -d /dst/plugins ] && [ -n "$HOST_HOME" ] && find /dst/plugins -maxdepth 1 -type f -name "*.json" -exec sed -i "s|$HOST_HOME/.claude/plugins/|/home/vscode/.claude/plugins/|g" {} +; true; } && chown -R 1000:1000 /dst`
404
- );
405
- await execa("docker", args);
406
- } finally {
407
- await rm(filterDir, { recursive: true, force: true });
408
- }
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
- }
439
- }
440
- async function maybeFilterTo(src, dest, hostHome, opts = {}) {
441
- const zero = {
442
- removedHooks: 0,
443
- installMethodFixed: false,
444
- aliasedProjectKey: false,
445
- workspaceTrusted: false
446
- };
447
- let parsed;
448
- try {
449
- parsed = JSON.parse(await readFile(src, "utf8"));
450
- } catch {
451
- return zero;
452
- }
453
- const filtered = filterHostHooks(parsed, hostHome);
454
- let working = filtered.data;
455
- let installFixed = false;
456
- if (opts.setInstallMethodNative) {
457
- const r = setInstallMethodNative(working);
458
- working = r.data;
459
- installFixed = r.applied;
460
- }
461
- let aliased = false;
462
- if (opts.aliasProject) {
463
- const r = addProjectAlias(working, opts.aliasProject.from, opts.aliasProject.to);
464
- working = r.data;
465
- aliased = r.aliased;
466
- }
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;
475
- }
476
- await writeFile(dest, JSON.stringify(working, null, 2));
477
- return {
478
- removedHooks: filtered.removedCommands.length,
479
- installMethodFixed: installFixed,
480
- aliasedProjectKey: aliased,
481
- workspaceTrusted: trusted
482
- };
483
- }
484
- var FORWARDED_ENV_KEYS = [
485
- "ANTHROPIC_API_KEY",
486
- "CLAUDE_CODE_OAUTH_TOKEN",
487
- "CLAUDE_EFFORT",
488
- "ANTHROPIC_MODEL"
489
- ];
490
- function buildClaudeMounts(spec, hostEnv) {
491
- const env = {};
492
- for (const k of FORWARDED_ENV_KEYS) {
493
- const v = hostEnv[k];
494
- if (typeof v === "string" && v.length > 0) env[k] = v;
495
- }
496
- return {
497
- extraVolumes: [`${spec.volume}:${CONTAINER_CLAUDE_DIR}`],
498
- env,
499
- volumeName: spec.volume
500
- };
501
- }
502
- var PLUGIN_INSTALLED_MARKER = ".agentbox-installed";
503
- var PLUGIN_FAILED_MARKER = ".agentbox-install-failed";
504
- var PLUGIN_INSTALL_BACKOFF_MS = 6 * 60 * 60 * 1e3;
505
- var PLUGIN_INSTALL_BACKOFF_MIN = Math.round(PLUGIN_INSTALL_BACKOFF_MS / 6e4);
506
- var NPM_CACHE_DIR = "/home/vscode/.claude/.agentbox-npm-cache";
507
- async function isFile(p) {
508
- try {
509
- return (await stat(p)).isFile();
510
- } catch {
511
- return false;
512
- }
513
- }
514
- async function isRecentFailMarker(p) {
515
- try {
516
- const st = await stat(p);
517
- return Date.now() - st.mtimeMs < PLUGIN_INSTALL_BACKOFF_MS;
518
- } catch {
519
- return false;
520
- }
521
- }
522
- async function isDir(p) {
523
- try {
524
- return (await stat(p)).isDirectory();
525
- } catch {
526
- return false;
527
- }
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
- }
537
- async function scanPluginCacheForRebuild(cacheRoot) {
538
- const referenced = await readReferencedPluginKeys(
539
- join(cacheRoot, "..", "installed_plugins.json")
540
- );
541
- let marketplaces;
542
- try {
543
- marketplaces = await readdir(cacheRoot, { withFileTypes: true });
544
- } catch {
545
- return false;
546
- }
547
- for (const m of marketplaces) {
548
- if (!m.isDirectory()) continue;
549
- const mPath = join(cacheRoot, m.name);
550
- let plugins;
551
- try {
552
- plugins = await readdir(mPath, { withFileTypes: true });
553
- } catch {
554
- continue;
555
- }
556
- for (const p of plugins) {
557
- if (!p.isDirectory()) continue;
558
- const pPath = join(mPath, p.name);
559
- let versions;
560
- try {
561
- versions = await readdir(pPath, { withFileTypes: true });
562
- } catch {
563
- continue;
564
- }
565
- for (const v of versions) {
566
- if (!v.isDirectory()) continue;
567
- if (referenced.size > 0 && !referenced.has(`${m.name}/${p.name}/${v.name}`)) continue;
568
- const vPath = join(pPath, v.name);
569
- if (!await isFile(join(vPath, "package.json"))) continue;
570
- if (await isFile(join(vPath, PLUGIN_INSTALLED_MARKER))) continue;
571
- if (await isRecentFailMarker(join(vPath, PLUGIN_FAILED_MARKER))) continue;
572
- return true;
573
- }
574
- }
575
- }
576
- return false;
577
- }
578
- async function resolveClaudeCacheLiveOnHost(volume) {
579
- if (await detectEngine() !== "orbstack") return null;
580
- if (!await isDir(orbstackVolumePath(volume))) return null;
581
- return orbstackVolumePath(volume, "plugins", "cache");
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
- }
603
- async function rebuildPluginNativeDeps(container, opts = {}) {
604
- if (opts.volume) {
605
- const cacheRoot = await resolveClaudeCacheLiveOnHost(opts.volume);
606
- if (cacheRoot && !await scanPluginCacheForRebuild(cacheRoot)) {
607
- return { rebuilt: [], failed: [], pruned: [], prunedBytes: 0, skipped: true };
608
- }
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
- ` : "";
615
- const script = `set -u
616
- PLUGINS_DIR=/home/vscode/.claude/plugins/cache
617
- MARKER=${PLUGIN_INSTALLED_MARKER}
618
- FAILMARKER=${PLUGIN_FAILED_MARKER}
619
- NPM_CACHE=${NPM_CACHE_DIR}
620
- BACKOFF_MIN=${PLUGIN_INSTALL_BACKOFF_MIN}
621
- MAX=4
622
- [ -d "$PLUGINS_DIR" ] || exit 0
623
- mkdir -p "$NPM_CACHE"
624
- WORK=$(mktemp -d)
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
- }
631
- # Run one plugin's install. $1 is frozen by value at call time, so it's safe
632
- # to read from the backgrounded subshell; the rest are set-once constants.
633
- do_one() {
634
- d=$1
635
- key=$(relkey "$d")
636
- if (cd "$d" && \\
637
- if [ -f package-lock.json ]; then \\
638
- npm ci --no-audit --no-fund --silent --prefer-offline --cache "$NPM_CACHE"; \\
639
- else \\
640
- npm install --no-audit --no-fund --silent --no-package-lock --prefer-offline --cache "$NPM_CACHE"; \\
641
- fi) >"$WORK/$key.out" 2>"$WORK/$key.err"; then
642
- touch "$d/$MARKER"
643
- rm -f "$d/$FAILMARKER"
644
- printf 'OK\\n' > "$WORK/$key.res"
645
- else
646
- : > "$d/$FAILMARKER"
647
- printf 'FAIL\\n' > "$WORK/$key.res"
648
- fi
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
667
- n=0
668
- for dir in "$PLUGINS_DIR"/*/*/*/; do
669
- [ -d "$dir" ] || continue
670
- [ -f "$dir/package.json" ] || continue
671
- rel=\${dir%/}; rel=\${rel#$PLUGINS_DIR/}
672
- is_referenced "$rel" || continue
673
- [ -f "$dir/$MARKER" ] && continue
674
- [ -n "$(find "$dir" -maxdepth 1 -name "$FAILMARKER" -mmin -$BACKOFF_MIN 2>/dev/null)" ] && continue
675
- echo "REBUILD_START \${dir#$PLUGINS_DIR/}"
676
- n=$((n+1))
677
- printf '%s\\n' "$dir" >> "$WORK/dirs"
678
- done
679
- if [ "$n" -eq 0 ]; then rm -rf "$WORK"; exit 0; fi
680
- running=0
681
- while IFS= read -r dir; do
682
- do_one "$dir" &
683
- running=$((running+1))
684
- if [ "$running" -ge "$MAX" ]; then wait; running=0; fi
685
- done < "$WORK/dirs"
686
- wait
687
- while IFS= read -r dir; do
688
- key=$(relkey "$dir")
689
- rel=\${dir#$PLUGINS_DIR/}
690
- [ -f "$WORK/$key.res" ] || continue
691
- read -r st < "$WORK/$key.res"
692
- if [ "$st" = OK ]; then
693
- echo "REBUILD_OK $rel"
694
- else
695
- echo "REBUILD_FAIL $rel"
696
- sed 's/^/ /' "$WORK/$key.err"
697
- echo "REBUILD_FAIL_END"
698
- fi
699
- done < "$WORK/dirs"
700
- rm -rf "$WORK"
701
- `;
702
- const result = await execa(
703
- "docker",
704
- ["exec", "--user", CONTAINER_USER, container, "sh", "-c", script],
705
- { reject: false }
706
- );
707
- const rebuilt = [];
708
- const failed = [];
709
- const pruned = [];
710
- let prunedBytes = 0;
711
- const lines = (result.stdout ?? "").split("\n");
712
- let collectingFail = null;
713
- for (const line of lines) {
714
- if (collectingFail) {
715
- if (line === "REBUILD_FAIL_END") {
716
- failed.push({ dir: collectingFail.dir, stderr: collectingFail.stderr.join("\n") });
717
- collectingFail = null;
718
- } else {
719
- collectingFail.stderr.push(line);
720
- }
721
- continue;
722
- }
723
- if (line.startsWith("REBUILD_START ")) {
724
- opts.onProgress?.(`rebuilding ${line.slice("REBUILD_START ".length)}`);
725
- } else if (line.startsWith("REBUILD_OK ")) {
726
- rebuilt.push(line.slice("REBUILD_OK ".length));
727
- } else if (line.startsWith("REBUILD_FAIL ")) {
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
- }
739
- }
740
- }
741
- return { rebuilt, failed, pruned, prunedBytes, skipped: false };
742
- }
743
- var ClaudeSessionError = class extends Error {
744
- constructor(message) {
745
- super(message);
746
- this.name = "ClaudeSessionError";
747
- }
748
- };
749
- function shQuote(arg) {
750
- if (arg.length === 0) return `''`;
751
- if (/^[A-Za-z0-9_\-./=:@%+,]+$/.test(arg)) return arg;
752
- return `'${arg.replace(/'/g, `'\\''`)}'`;
753
- }
754
- async function startClaudeSession(opts) {
755
- const sessionName = opts.sessionName ?? DEFAULT_CLAUDE_SESSION;
756
- const cmd = ["claude", ...opts.claudeArgs].map(shQuote).join(" ");
757
- const term = process.env["TERM"] ?? "xterm-256color";
758
- const envFlags = ["-e", `TERM=${term}`];
759
- for (const k of FORWARDED_ENV_KEYS) {
760
- const v = process.env[k];
761
- if (typeof v === "string" && v.length > 0) envFlags.push("-e", `${k}=${v}`);
762
- }
763
- const result = await execa(
764
- "docker",
765
- [
766
- "exec",
767
- ...envFlags,
768
- "--user",
769
- CONTAINER_USER,
770
- opts.container,
771
- "tmux",
772
- "new-session",
773
- "-d",
774
- "-s",
775
- sessionName,
776
- cmd,
777
- ...buildClaudeStatusBarArgs(sessionName)
778
- ],
779
- { reject: false }
780
- );
781
- if (result.exitCode === 0) return;
782
- const stderr = (result.stderr ?? "").toString();
783
- if (result.exitCode === 127 || /command not found|tmux: not found/i.test(stderr)) {
784
- throw new ClaudeSessionError(
785
- `tmux is missing from the box image. Rebuild with: docker rmi agentbox/box:dev && retry.`
786
- );
787
- }
788
- if (/claude.*not found|exec: "claude"/i.test(stderr)) {
789
- throw new ClaudeSessionError(
790
- `claude is missing from the box image. Rebuild with: docker rmi agentbox/box:dev && retry.`
791
- );
792
- }
793
- if (/duplicate session/i.test(stderr)) {
794
- throw new ClaudeSessionError(
795
- `a tmux session "${sessionName}" already exists in ${opts.container}; use \`agentbox claude attach\` to reattach.`
796
- );
797
- }
798
- throw new ClaudeSessionError(
799
- `failed to start claude session in ${opts.container}: ${stderr.trim() || `exit ${String(result.exitCode)}`}`
800
- );
801
- }
802
- function buildClaudeAttachArgv(container, sessionName) {
803
- const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
804
- const term = process.env["TERM"] ?? "xterm-256color";
805
- return [
806
- "exec",
807
- "-it",
808
- "-e",
809
- `TERM=${term}`,
810
- "--user",
811
- CONTAINER_USER,
812
- container,
813
- "tmux",
814
- "attach",
815
- "-t",
816
- name
817
- ];
818
- }
819
- function buildClaudeDashboardAttachArgv(container, sessionName) {
820
- const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
821
- const dash = `${name}-dash`;
822
- const term = process.env["TERM"] ?? "xterm-256color";
823
- return [
824
- "exec",
825
- "-it",
826
- "-e",
827
- `TERM=${term}`,
828
- "--user",
829
- CONTAINER_USER,
830
- container,
831
- "tmux",
832
- "new-session",
833
- "-A",
834
- "-d",
835
- "-s",
836
- dash,
837
- "-t",
838
- name,
839
- ";",
840
- "set",
841
- "-t",
842
- dash,
843
- "status",
844
- "off",
845
- ";",
846
- "attach",
847
- "-t",
848
- dash
849
- ];
850
- }
851
- function buildClaudeStatusBarArgs(sessionName) {
852
- const s = sessionName;
853
- return [
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.
860
- ";",
861
- "set",
862
- "-g",
863
- "prefix",
864
- "C-a",
865
- ";",
866
- "set",
867
- "-g",
868
- "prefix2",
869
- "C-b",
870
- ";",
871
- "bind-key",
872
- "C-a",
873
- "send-prefix",
874
- ";",
875
- "bind-key",
876
- "C-b",
877
- "send-prefix",
878
- "-2",
879
- ";",
880
- "bind-key",
881
- "q",
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.
887
- ";",
888
- "set",
889
- "-t",
890
- s,
891
- "status",
892
- "off"
893
- ];
894
- }
895
- function buildShellArgv(container) {
896
- const term = process.env["TERM"] ?? "xterm-256color";
897
- return ["exec", "-it", "-e", `TERM=${term}`, "--user", CONTAINER_USER, container, "bash", "-l"];
898
- }
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
- ];
919
- }
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);
953
- }
954
- return { warmed: false, attempts: MAX_ATTEMPTS };
955
- }
956
- function formatDetachNotice(ref) {
957
- return `Session detached. Reattach with: agentbox claude attach ${ref}`;
958
- }
959
- async function claudeSessionInfo(container, sessionName) {
960
- const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
961
- const has = await execa(
962
- "docker",
963
- ["exec", "--user", CONTAINER_USER, container, "tmux", "has-session", "-t", name],
964
- { reject: false }
965
- );
966
- if (has.exitCode !== 0) {
967
- return { running: false, sessionName: name, startedAt: null };
968
- }
969
- const ts = await execa(
970
- "docker",
971
- [
972
- "exec",
973
- "--user",
974
- CONTAINER_USER,
975
- container,
976
- "tmux",
977
- "display-message",
978
- "-p",
979
- "-t",
980
- name,
981
- "#{session_created}"
982
- ],
983
- { reject: false }
984
- );
985
- let startedAt = null;
986
- if (ts.exitCode === 0) {
987
- const secs = Number.parseInt((ts.stdout ?? "").trim(), 10);
988
- if (Number.isFinite(secs) && secs > 0) startedAt = new Date(secs * 1e3).toISOString();
989
- }
990
- return { running: true, sessionName: name, startedAt };
991
- }
992
- var PULL_DIR_CATEGORIES = ["skills", "agents", "commands"];
993
- async function listChildDirs(dir) {
994
- try {
995
- const entries = await readdir(dir, { withFileTypes: true });
996
- return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
997
- } catch {
998
- return [];
999
- }
1000
- }
1001
- async function readJsonFile(path) {
1002
- try {
1003
- return JSON.parse(await readFile(path, "utf8"));
1004
- } catch {
1005
- return void 0;
1006
- }
1007
- }
1008
- async function pullClaudeExtras(spec, opts) {
1009
- const hostHome = homedir();
1010
- const hostClaude = join(hostHome, ".claude");
1011
- const inventoryScript = [
1012
- "for cat in skills agents commands; do",
1013
- ' [ -d "/src/$cat" ] || continue;',
1014
- ' for d in "/src/$cat"/*/; do',
1015
- ' [ -d "$d" ] || continue;',
1016
- ' printf "DIR %s %s\\n" "$cat" "$(basename "$d")";',
1017
- " done;",
1018
- "done;",
1019
- "if [ -d /src/plugins/cache ]; then",
1020
- " for m in /src/plugins/cache/*/; do",
1021
- ' [ -d "$m" ] || continue;',
1022
- ' for p in "$m"*/; do',
1023
- ' [ -d "$p" ] || continue;',
1024
- ' printf "PLUGIN %s/%s\\n" "$(basename "$m")" "$(basename "$p")";',
1025
- " done;",
1026
- " done;",
1027
- "fi;",
1028
- "for f in installed_plugins known_marketplaces; do",
1029
- ' [ -f "/src/plugins/$f.json" ] || continue;',
1030
- ' printf "JSON %s " "$f";',
1031
- ' base64 -w0 "/src/plugins/$f.json";',
1032
- ' printf "\\n";',
1033
- "done"
1034
- ].join(" ");
1035
- const inv = await execa(
1036
- "docker",
1037
- ["run", "--rm", "--user", "0", "-v", `${spec.volume}:/src:ro`, opts.image, "sh", "-c", inventoryScript],
1038
- { reject: false }
1039
- );
1040
- if (inv.exitCode !== 0) {
1041
- throw new ClaudeSessionError(
1042
- `failed to read claude-config volume ${spec.volume}: ${(inv.stderr ?? "").toString().trim() || `exit ${String(inv.exitCode)}`}`
1043
- );
1044
- }
1045
- const boxDirs = { skills: [], agents: [], commands: [] };
1046
- const boxPlugins = [];
1047
- const boxJson = {};
1048
- for (const line of (inv.stdout ?? "").split("\n")) {
1049
- if (line.startsWith("DIR ")) {
1050
- const rest = line.slice(4);
1051
- const sp = rest.indexOf(" ");
1052
- if (sp === -1) continue;
1053
- const cat = rest.slice(0, sp);
1054
- const name = rest.slice(sp + 1);
1055
- if (cat in boxDirs) boxDirs[cat].push(name);
1056
- } else if (line.startsWith("PLUGIN ")) {
1057
- boxPlugins.push(line.slice(7));
1058
- } else if (line.startsWith("JSON ")) {
1059
- const rest = line.slice(5);
1060
- const sp = rest.indexOf(" ");
1061
- if (sp === -1) continue;
1062
- const which = rest.slice(0, sp);
1063
- try {
1064
- boxJson[which] = JSON.parse(Buffer.from(rest.slice(sp + 1), "base64").toString("utf8"));
1065
- } catch {
1066
- }
1067
- }
1068
- }
1069
- const newItems = [];
1070
- const applyPaths = [];
1071
- for (const cat of PULL_DIR_CATEGORIES) {
1072
- const hostNames = await listChildDirs(join(hostClaude, cat));
1073
- const excludes = cat === "skills" ? SKILL_EXCLUDE_PREFIXES : [];
1074
- for (const name of pickNewItems(boxDirs[cat] ?? [], hostNames, excludes)) {
1075
- newItems.push({ category: cat, name });
1076
- applyPaths.push({ src: `/src/${cat}/${name}`, dest: `/dst/${cat}/${name}` });
1077
- }
1078
- }
1079
- const hostPluginKeys = [];
1080
- for (const m of await listChildDirs(join(hostClaude, "plugins", "cache"))) {
1081
- for (const p of await listChildDirs(join(hostClaude, "plugins", "cache", m))) {
1082
- hostPluginKeys.push(`${m}/${p}`);
1083
- }
1084
- }
1085
- for (const key of pickNewItems(boxPlugins, hostPluginKeys)) {
1086
- newItems.push({ category: "plugins", name: key });
1087
- applyPaths.push({ src: `/src/plugins/cache/${key}`, dest: `/dst/plugins/cache/${key}` });
1088
- }
1089
- const hostInstalled = await readJsonFile(join(hostClaude, "plugins", "installed_plugins.json"));
1090
- const hostMarkets = await readJsonFile(join(hostClaude, "plugins", "known_marketplaces.json"));
1091
- const mergedInstalled = mergeInstalledPlugins(hostInstalled, boxJson["installed_plugins"], {
1092
- hostHome
1093
- });
1094
- const mergedMarkets = mergeKnownMarketplaces(hostMarkets, boxJson["known_marketplaces"], {
1095
- hostHome
1096
- });
1097
- const mergedRegistries = [];
1098
- if (mergedInstalled.changed) mergedRegistries.push("installed_plugins.json");
1099
- if (mergedMarkets.changed) mergedRegistries.push("known_marketplaces.json");
1100
- if (opts.dryRun || newItems.length === 0 && mergedRegistries.length === 0) {
1101
- return { newItems, mergedRegistries };
1102
- }
1103
- if (applyPaths.length > 0) {
1104
- const cmds = applyPaths.map(({ src, dest }) => {
1105
- const parent = dest.slice(0, dest.lastIndexOf("/"));
1106
- return `mkdir -p '${parent}' && rsync -a --ignore-existing --exclude=node_modules '${src}/' '${dest}/'`;
1107
- });
1108
- const apply = await execa(
1109
- "docker",
1110
- [
1111
- "run",
1112
- "--rm",
1113
- "--user",
1114
- "0",
1115
- "-v",
1116
- `${spec.volume}:/src:ro`,
1117
- "-v",
1118
- `${hostClaude}:/dst`,
1119
- opts.image,
1120
- "sh",
1121
- "-c",
1122
- cmds.join(" && ")
1123
- ],
1124
- { reject: false }
1125
- );
1126
- if (apply.exitCode !== 0) {
1127
- throw new ClaudeSessionError(
1128
- `failed to copy extensions from ${spec.volume}: ${(apply.stderr ?? "").toString().trim() || `exit ${String(apply.exitCode)}`}`
1129
- );
1130
- }
1131
- }
1132
- if (mergedMarkets.changed || mergedInstalled.changed) {
1133
- await mkdir(join(hostClaude, "plugins"), { recursive: true });
1134
- if (mergedMarkets.changed) {
1135
- await writeFile(
1136
- join(hostClaude, "plugins", "known_marketplaces.json"),
1137
- `${JSON.stringify(mergedMarkets.data, null, 2)}
1138
- `
1139
- );
1140
- }
1141
- if (mergedInstalled.changed) {
1142
- await writeFile(
1143
- join(hostClaude, "plugins", "installed_plugins.json"),
1144
- `${JSON.stringify(mergedInstalled.data, null, 2)}
1145
- `
1146
- );
1147
- }
1148
- }
1149
- return { newItems, mergedRegistries };
1150
- }
1151
- var SHARED_DOCKER_CACHE_VOLUME = "agentbox-docker-cache";
1152
- function dockerVolumeName(boxId, shared) {
1153
- return shared ? SHARED_DOCKER_CACHE_VOLUME : `agentbox-docker-${boxId}`;
1154
- }
1155
- async function launchDockerdDaemon(container, timeoutMs = 3e4) {
1156
- const result = await execInBox(container, ["/usr/local/bin/agentbox-dockerd-start"], {
1157
- user: "root",
1158
- detach: true
1159
- });
1160
- if (result.exitCode !== 0) {
1161
- return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };
1162
- }
1163
- const deadline = Date.now() + timeoutMs;
1164
- while (Date.now() < deadline) {
1165
- const probe = await execInBox(
1166
- container,
1167
- [
1168
- "bash",
1169
- "-lc",
1170
- "[ -S /var/run/docker.sock ] && docker -H unix:///var/run/docker.sock info >/dev/null 2>&1"
1171
- ],
1172
- { user: "root" }
1173
- );
1174
- if (probe.exitCode === 0) return { up: true };
1175
- await new Promise((r) => setTimeout(r, 200));
1176
- }
1177
- return { up: false, reason: `dockerd did not become ready within ${String(timeoutMs)}ms` };
1178
- }
1179
- async function launchVncDaemon(container, timeoutMs = 5e3) {
1180
- const result = await execInBox(container, ["/usr/local/bin/agentbox-vnc-start"], {
1181
- user: "vscode",
1182
- detach: true
1183
- });
1184
- if (result.exitCode !== 0) {
1185
- return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };
1186
- }
1187
- const deadline = Date.now() + timeoutMs;
1188
- while (Date.now() < deadline) {
1189
- const probe = await execInBox(
1190
- container,
1191
- ["bash", "-lc", "(echo > /dev/tcp/127.0.0.1/6080) 2>/dev/null"],
1192
- { user: "vscode" }
1193
- );
1194
- if (probe.exitCode === 0) return { up: true };
1195
- await new Promise((r) => setTimeout(r, 150));
1196
- }
1197
- return { up: false, reason: `websockify did not bind 6080 within ${String(timeoutMs)}ms` };
1198
- }
1199
- var VNC_PASSWORD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1200
- function generateVncPassword() {
1201
- const bytes = randomBytes(8);
1202
- let out = "";
1203
- for (let i = 0; i < 8; i++) {
1204
- out += VNC_PASSWORD_ALPHABET[bytes[i] % VNC_PASSWORD_ALPHABET.length];
1205
- }
1206
- return out;
1207
- }
1208
- var VNC_CONTAINER_PORT = 6080;
1209
- function buildVncUrls(record, engine) {
1210
- if (!record.vncEnabled || !record.vncPassword) return {};
1211
- const containerPort = record.vncContainerPort ?? VNC_CONTAINER_PORT;
1212
- const qs = `autoconnect=1&password=${encodeURIComponent(record.vncPassword)}`;
1213
- const urls = {};
1214
- if (engine === "orbstack") {
1215
- urls.orbUrl = `http://${record.container}.orb.local:${String(containerPort)}/vnc.html?${qs}`;
1216
- }
1217
- if (record.vncHostPort) {
1218
- urls.loopbackUrl = `http://127.0.0.1:${String(record.vncHostPort)}/vnc.html?${qs}`;
1219
- }
1220
- return urls;
1221
- }
1222
- var WEB_CONTAINER_PORT = 80;
1223
- async function detectGitRepos(workspace) {
1224
- const out = [];
1225
- if (await isGitDir(join2(workspace, ".git"))) {
1226
- out.push({ kind: "root", hostMainRepo: workspace, relPathFromWorkspace: "" });
1227
- }
1228
- let entries;
1229
- try {
1230
- entries = await readdir2(workspace, { withFileTypes: true });
1231
- } catch {
1232
- return out;
1233
- }
1234
- for (const e of entries) {
1235
- if (!e.isDirectory() || e.name.startsWith(".")) continue;
1236
- const sub = join2(workspace, e.name);
1237
- if (await isGitDir(join2(sub, ".git"))) {
1238
- out.push({ kind: "nested", hostMainRepo: sub, relPathFromWorkspace: e.name });
1239
- }
1240
- }
1241
- return out;
1242
- }
1243
- async function isGitDir(path) {
1244
- try {
1245
- const s = await stat2(path);
1246
- return s.isDirectory();
1247
- } catch {
1248
- return false;
1249
- }
1250
- }
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(
1262
- "git",
1263
- ["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
1264
- { reject: false }
1265
- );
1266
- return result.exitCode === 0;
1267
- }
1268
- var GitWorktreeError = class extends Error {
1269
- constructor(message) {
1270
- super(message);
1271
- this.name = "GitWorktreeError";
1272
- }
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(
1285
- "git",
1286
- ["-C", repo.hostMainRepo, "ls-files", "--others", "--exclude-standard", "-z"],
1287
- { reject: false }
1288
- );
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],
1304
- { reject: false }
1305
- );
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
- ],
1392
- { reject: false }
1393
- );
1394
- await execa3(
1395
- "docker",
1396
- [
1397
- "exec",
1398
- "--user",
1399
- "vscode",
1400
- opts.container,
1401
- "git",
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
- ],
1439
- { reject: false }
1440
- );
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 }
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
- }
1465
- } else {
1466
- log(`applied tracked changes from host main into ${ct}`);
1467
- }
1468
- }
1469
- if (r.untrackedNul.length > 0) {
1470
- const tarOut = await execa3("tar", ["-C", r.hostSource, "--null", "-T", "-", "-cf", "-"], {
1471
- input: r.untrackedNul.replace(/\0$/, ""),
1472
- encoding: "buffer",
1473
- reject: false
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
- );
1484
- if (tarIn.exitCode !== 0) {
1485
- log(`warning: untracked-file copy into ${ct} failed: ${tarIn.stderr}`);
1486
- } else {
1487
- const count = r.untrackedNul.split("\0").filter((s) => s.length > 0).length;
1488
- log(`copied ${String(count)} untracked file(s) into ${ct}`);
1489
- }
1490
- }
1491
- }
1492
- }
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}`);
1502
- }
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 }
1507
- );
1508
- if (tarIn.exitCode !== 0) {
1509
- throw new GitWorktreeError(`tar extract into /workspace failed: ${tarIn.stderr}`);
1510
- }
1511
- log(`seeded /workspace from ${opts.hostSource}`);
1512
- }
1513
- async function removeInBoxWorktree(args) {
1514
- const remove = await execa3(
1515
- "git",
1516
- ["-C", args.hostMainRepo, "worktree", "remove", "--force", args.gitWorktreePath],
1517
- { reject: false }
1518
- );
1519
- if (remove.exitCode === 0) return;
1520
- await execa3("git", ["-C", args.hostMainRepo, "worktree", "prune"], { reject: false });
1521
- }
1522
- function ctParent(p) {
1523
- const i = p.lastIndexOf("/");
1524
- return i <= 0 ? "/" : p.slice(0, i);
1525
- }
1526
- var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1527
- "node_modules",
1528
- ".next",
1529
- ".nuxt",
1530
- ".turbo",
1531
- ".svelte-kit",
1532
- "dist",
1533
- "build",
1534
- "out",
1535
- "target",
1536
- ".venv",
1537
- "__pycache__",
1538
- ".cache",
1539
- ".parcel-cache"
1540
- ]);
1541
- var SNAPSHOTS_ROOT = join3(homedir2(), ".agentbox", "snapshots");
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);
1547
- }
1548
- async function findExcludedDirs(root, excluded = EXCLUDE_DIRS) {
1549
- const matches = [];
1550
- const walk = async (dir) => {
1551
- let entries;
1552
- try {
1553
- entries = await readdir3(dir, { withFileTypes: true });
1554
- } catch {
1555
- return;
1556
- }
1557
- for (const entry of entries) {
1558
- if (!entry.isDirectory()) continue;
1559
- const abs = join3(dir, entry.name);
1560
- if (excluded.has(entry.name)) {
1561
- matches.push(abs);
1562
- continue;
1563
- }
1564
- await walk(abs);
1565
- }
1566
- };
1567
- await walk(root);
1568
- return matches;
1569
- }
1570
- async function createSnapshot(opts) {
1571
- const source = resolve(opts.source);
1572
- const destination = resolve(opts.destination);
1573
- const excluded = opts.excluded ?? EXCLUDE_DIRS;
1574
- await mkdir2(SNAPSHOTS_ROOT, { recursive: true });
1575
- const cpArgs = platform() === "darwin" ? ["-cR"] : ["-R"];
1576
- await execa4("cp", [...cpArgs, `${source}/`, destination]);
1577
- const toPrune = await findExcludedDirs(destination, excluded);
1578
- await Promise.all(toPrune.map((p) => rm2(p, { recursive: true, force: true })));
1579
- return { destination, prunedPaths: toPrune };
1580
- }
1581
- var CTL_DAEMON_LOG = "/var/log/agentbox/ctl-daemon.log";
1582
- async function launchCtlDaemon(container, hostSocketPath, timeoutMs = 3e3) {
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], {
1585
- user: "vscode",
1586
- detach: true
1587
- });
1588
- if (result.exitCode !== 0) {
1589
- return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };
1590
- }
1591
- const deadline = Date.now() + timeoutMs;
1592
- while (Date.now() < deadline) {
1593
- if (await pathExists2(hostSocketPath)) return { up: true };
1594
- await new Promise((r) => setTimeout(r, 100));
1595
- }
1596
- return {
1597
- up: false,
1598
- reason: `socket ${hostSocketPath} did not appear within ${String(timeoutMs)}ms`
1599
- };
1600
- }
1601
- async function pathExists2(p) {
1602
- try {
1603
- await stat4(p);
1604
- return true;
1605
- } catch {
1606
- return false;
1607
- }
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
- }
1626
- var STATE_DIR = join4(homedir3(), ".agentbox");
1627
- var PID_FILE = join4(STATE_DIR, "relay.pid");
1628
- var LOG_FILE = join4(STATE_DIR, "relay.log");
1629
- var PORT = DEFAULT_RELAY_PORT;
1630
- var ENDPOINT = {
1631
- // host.docker.internal is the Docker Desktop / OrbStack-supplied alias for
1632
- // the host's loopback as seen from inside a container. The corresponding
1633
- // `--add-host=host.docker.internal:host-gateway` flag in runBox makes the
1634
- // resolution work on Linux native Docker too.
1635
- url: `http://host.docker.internal:${String(PORT)}`,
1636
- hostUrl: `http://127.0.0.1:${String(PORT)}`,
1637
- port: PORT
1638
- };
1639
- async function ensureRelay(opts = {}) {
1640
- const log = opts.onLog ?? (() => {
1641
- });
1642
- await mkdir3(STATE_DIR, { recursive: true });
1643
- if (await containerExists(RELAY_CONTAINER_NAME)) {
1644
- await removeContainer(RELAY_CONTAINER_NAME);
1645
- log(`removed legacy relay container ${RELAY_CONTAINER_NAME}`);
1646
- }
1647
- if (await pingHealthz(500)) {
1648
- return ENDPOINT;
1649
- }
1650
- const existingPid = await readPidFile();
1651
- if (existingPid !== null && await processAlive(existingPid)) {
1652
- for (let i = 0; i < 10; i++) {
1653
- if (await pingHealthz(300)) return ENDPOINT;
1654
- await delay2(200);
1655
- }
1656
- log(`relay pid ${String(existingPid)} alive but /healthz unresponsive \u2014 proceeding anyway`);
1657
- return ENDPOINT;
1658
- }
1659
- if (existingPid !== null) {
1660
- await unlink(PID_FILE).catch(() => {
1661
- });
1662
- }
1663
- const relayBin = resolveRelayBin();
1664
- const logFd = openSync(LOG_FILE, "a");
1665
- const cliEntry = resolveCliEntry();
1666
- const child = spawn(
1667
- process.execPath,
1668
- [relayBin, "serve", "--port", String(PORT), "--host", "0.0.0.0"],
1669
- {
1670
- detached: true,
1671
- stdio: ["ignore", logFd, logFd],
1672
- env: {
1673
- ...process.env,
1674
- ...cliEntry ? { AGENTBOX_CLI_ENTRY: cliEntry } : {}
1675
- }
1676
- }
1677
- );
1678
- child.unref();
1679
- if (typeof child.pid === "number") {
1680
- await writeFile2(PID_FILE, String(child.pid), "utf8");
1681
- log(`spawned relay host process (pid ${String(child.pid)}, port ${String(PORT)})`);
1682
- }
1683
- for (let i = 0; i < 25; i++) {
1684
- if (await pingHealthz(300)) {
1685
- log(`relay reachable on ${ENDPOINT.hostUrl}`);
1686
- return ENDPOINT;
1687
- }
1688
- await delay2(200);
1689
- }
1690
- throw new Error(
1691
- `relay did not become reachable on ${ENDPOINT.hostUrl} within 5s; see ${LOG_FILE}`
1692
- );
1693
- }
1694
- function resolveRelayBin() {
1695
- const override = process.env.AGENTBOX_RELAY_BIN;
1696
- if (override && existsSync(override)) return override;
1697
- const here = dirname(fileURLToPath(import.meta.url));
1698
- const candidates = [
1699
- resolve2(here, "..", "runtime", "relay", "bin.cjs"),
1700
- resolve2(here, "..", "..", "relay", "dist", "bin.cjs"),
1701
- resolve2(here, "..", "..", "..", "@agentbox", "relay", "dist", "bin.cjs"),
1702
- resolve2(here, "..", "..", "node_modules", "@agentbox", "relay", "dist", "bin.cjs")
1703
- ];
1704
- for (const c of candidates) {
1705
- if (existsSync(c)) return c;
1706
- }
1707
- throw new Error(
1708
- `could not locate @agentbox/relay bin; tried:
1709
- ${candidates.join("\n ")}`
1710
- );
1711
- }
1712
- function resolveCliEntry() {
1713
- const override = process.env.AGENTBOX_CLI_ENTRY;
1714
- if (override && existsSync(override)) return override;
1715
- const here = dirname(fileURLToPath(import.meta.url));
1716
- const candidates = [
1717
- // Bundled CLI (dev + published): this module IS bundled into the CLI
1718
- // entry, so the entry is index.js next to this file.
1719
- resolve2(here, "index.js"),
1720
- resolve2(here, "..", "..", "..", "apps", "cli", "dist", "index.js"),
1721
- resolve2(here, "..", "..", "..", "..", "dist", "index.js")
1722
- ];
1723
- for (const c of candidates) {
1724
- if (existsSync(c)) return c;
1725
- }
1726
- return null;
1727
- }
1728
- async function stopRelay() {
1729
- const pid = await readPidFile();
1730
- if (pid === null) {
1731
- return { stopped: false, pid: null };
1732
- }
1733
- if (!await processAlive(pid)) {
1734
- await unlink(PID_FILE).catch(() => {
1735
- });
1736
- return { stopped: false, pid };
1737
- }
1738
- try {
1739
- process.kill(pid, "SIGTERM");
1740
- } catch {
1741
- }
1742
- for (let i = 0; i < 20; i++) {
1743
- if (!await processAlive(pid)) break;
1744
- await delay2(100);
1745
- }
1746
- if (await processAlive(pid)) {
1747
- try {
1748
- process.kill(pid, "SIGKILL");
1749
- } catch {
1750
- }
1751
- }
1752
- await unlink(PID_FILE).catch(() => {
1753
- });
1754
- return { stopped: true, pid };
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
- }
1771
- function pingHealthz(timeoutMs) {
1772
- return new Promise((resolveP) => {
1773
- const req = httpRequest(
1774
- { host: "127.0.0.1", port: PORT, method: "GET", path: "/healthz", timeout: timeoutMs },
1775
- (res) => {
1776
- res.resume();
1777
- const status = res.statusCode ?? 0;
1778
- resolveP(status >= 200 && status < 300);
1779
- }
1780
- );
1781
- req.on("error", () => resolveP(false));
1782
- req.on("timeout", () => {
1783
- req.destroy();
1784
- resolveP(false);
1785
- });
1786
- req.end();
1787
- });
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
- }
1825
- async function readPidFile() {
1826
- try {
1827
- const text = await readFile2(PID_FILE, "utf8");
1828
- const pid = Number.parseInt(text.trim(), 10);
1829
- return Number.isFinite(pid) && pid > 0 ? pid : null;
1830
- } catch {
1831
- return null;
1832
- }
1833
- }
1834
- async function processAlive(pid) {
1835
- try {
1836
- process.kill(pid, 0);
1837
- return true;
1838
- } catch {
1839
- return false;
1840
- }
1841
- }
1842
- function generateRelayToken() {
1843
- return randomBytes2(32).toString("hex");
1844
- }
1845
- async function registerBoxWithRelay(args) {
1846
- const worktrees = (args.worktrees ?? []).map((w) => ({
1847
- containerPath: w.containerPath,
1848
- hostMainRepo: w.hostMainRepo,
1849
- branch: w.branch
1850
- }));
1851
- await adminPost("/admin/register-box", {
1852
- boxId: args.boxId,
1853
- token: args.token,
1854
- name: args.name,
1855
- containerName: args.containerName,
1856
- createdAt: args.createdAt,
1857
- projectIndex: args.projectIndex,
1858
- worktrees
1859
- });
1860
- }
1861
- async function forgetBoxFromRelay(boxId) {
1862
- try {
1863
- await adminPost("/admin/forget-box", { boxId });
1864
- } catch {
1865
- }
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
- }
1887
- async function adminPost(path, body) {
1888
- const json = JSON.stringify(body);
1889
- await new Promise((resolveP, rejectP) => {
1890
- const req = httpRequest(
1891
- {
1892
- host: "127.0.0.1",
1893
- port: PORT,
1894
- method: "POST",
1895
- path,
1896
- headers: {
1897
- "Content-Type": "application/json",
1898
- "Content-Length": Buffer.byteLength(json).toString()
1899
- },
1900
- timeout: 3e3
1901
- },
1902
- (res) => {
1903
- const chunks = [];
1904
- res.on("data", (c) => chunks.push(c));
1905
- res.on("end", () => {
1906
- const status = res.statusCode ?? 0;
1907
- if (status >= 200 && status < 300) {
1908
- resolveP();
1909
- } else {
1910
- const text = Buffer.concat(chunks).toString("utf8");
1911
- rejectP(new Error(`relay ${path} \u2192 ${String(status)}: ${text}`));
1912
- }
1913
- });
1914
- }
1915
- );
1916
- req.on("error", rejectP);
1917
- req.on("timeout", () => {
1918
- req.destroy();
1919
- rejectP(new Error(`relay ${path} timeout`));
1920
- });
1921
- req.write(json);
1922
- req.end();
1923
- });
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
- }
1968
- async function rehydrateRelayRegistry(boxes) {
1969
- for (const b of boxes) {
1970
- if (!b.relayToken) continue;
1971
- try {
1972
- await registerBoxWithRelay({
1973
- boxId: b.id,
1974
- token: b.relayToken,
1975
- name: b.name,
1976
- containerName: b.container,
1977
- createdAt: b.createdAt,
1978
- projectIndex: b.projectIndex,
1979
- worktrees: b.gitWorktrees
1980
- });
1981
- } catch {
1982
- }
1983
- }
1984
- }
1985
- var PROFILES = {
1986
- vscode: {
1987
- serverDir: "/home/vscode/.vscode-server",
1988
- extensionsDir: "/home/vscode/.vscode-server/extensions",
1989
- perBoxVolumePrefix: "agentbox-vscode-server-",
1990
- sharedExtensionsVolume: "agentbox-vscode-extensions",
1991
- cli: "code",
1992
- displayName: "VS Code",
1993
- protocolScheme: "vscode"
1994
- },
1995
- cursor: {
1996
- serverDir: "/home/vscode/.cursor-server",
1997
- extensionsDir: "/home/vscode/.cursor-server/extensions",
1998
- perBoxVolumePrefix: "agentbox-cursor-server-",
1999
- sharedExtensionsVolume: "agentbox-cursor-extensions",
2000
- cli: "cursor",
2001
- displayName: "Cursor",
2002
- protocolScheme: "cursor"
2003
- }
2004
- };
2005
- var IDE_FLAVORS = ["vscode", "cursor"];
2006
- function ideProfile(flavor) {
2007
- return PROFILES[flavor];
2008
- }
2009
- var SHARED_VSCODE_EXTENSIONS_VOLUME = PROFILES.vscode.sharedExtensionsVolume;
2010
- var SHARED_CURSOR_EXTENSIONS_VOLUME = PROFILES.cursor.sharedExtensionsVolume;
2011
- function vscodeServerVolumeName(boxId) {
2012
- return ideServerVolumeName("vscode", boxId);
2013
- }
2014
- function cursorServerVolumeName(boxId) {
2015
- return ideServerVolumeName("cursor", boxId);
2016
- }
2017
- function ideServerVolumeName(flavor, boxId) {
2018
- return `${PROFILES[flavor].perBoxVolumePrefix}${boxId}`;
2019
- }
2020
- function buildFlavorMounts(flavor, boxId) {
2021
- const profile = PROFILES[flavor];
2022
- const perBox = ideServerVolumeName(flavor, boxId);
2023
- return {
2024
- volumes: [perBox, profile.sharedExtensionsVolume],
2025
- extraVolumes: [
2026
- `${perBox}:${profile.serverDir}`,
2027
- `${profile.sharedExtensionsVolume}:${profile.extensionsDir}`
2028
- ]
2029
- };
2030
- }
2031
- function buildIdeMounts(boxId) {
2032
- const merged = { volumes: [], extraVolumes: [] };
2033
- for (const f of IDE_FLAVORS) {
2034
- const m = buildFlavorMounts(f, boxId);
2035
- merged.volumes.push(...m.volumes);
2036
- merged.extraVolumes.push(...m.extraVolumes);
2037
- }
2038
- return merged;
2039
- }
2040
- async function ensureIdeVolumes(boxId) {
2041
- for (const v of buildIdeMounts(boxId).volumes) await ensureVolume(v);
2042
- }
2043
- async function repairIdeOwnership(container) {
2044
- for (const flavor of IDE_FLAVORS) {
2045
- await execInBox(container, ["chown", "-R", "vscode:vscode", PROFILES[flavor].serverDir], {
2046
- user: "root"
2047
- });
2048
- }
2049
- }
2050
- function containerHex(containerName) {
2051
- return Buffer.from(containerName, "utf8").toString("hex");
2052
- }
2053
- var SENTINEL = "// agentbox-managed: regenerated on `agentbox code`; remove this header to take ownership";
2054
- async function ensureAgentboxTasksFile(container, services, opts = {}) {
2055
- if (services.length === 0) return { status: "skipped-no-services" };
2056
- const existing = await execInBox(container, ["cat", "/workspace/.vscode/tasks.json"], {
2057
- user: "vscode"
2058
- });
2059
- if (existing.exitCode === 0 && !opts.regen && !existing.stdout.includes(SENTINEL)) {
2060
- return { status: "skipped-user-owned" };
2061
- }
2062
- const tasks = services.map((s) => ({
2063
- label: `agentbox: ${s.name}`,
2064
- type: "shell",
2065
- command: `tail -F /var/log/agentbox/${s.name}.log`,
2066
- isBackground: true,
2067
- presentation: { panel: "dedicated", reveal: "always", echo: false },
2068
- runOptions: { runOn: "folderOpen" },
2069
- problemMatcher: []
2070
- }));
2071
- const body = `${SENTINEL}
2072
- ` + JSON.stringify(
2073
- {
2074
- version: "2.0.0",
2075
- tasks
2076
- },
2077
- null,
2078
- 2
2079
- ) + "\n";
2080
- await execInBox(container, ["mkdir", "-p", "/workspace/.vscode"], { user: "vscode" });
2081
- const write = await writeFileInBox(container, "/workspace/.vscode/tasks.json", body);
2082
- if (write.exitCode !== 0) {
2083
- throw new Error(`failed to write tasks.json in ${container}: ${write.stderr || write.stdout}`);
2084
- }
2085
- return { status: "wrote" };
2086
- }
2087
- async function writeFileInBox(container, path, content) {
2088
- const { execa: execa6 } = await import("execa");
2089
- const result = await execa6(
2090
- "docker",
2091
- ["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote(path)}`],
2092
- { input: content, reject: false }
2093
- );
2094
- return {
2095
- exitCode: result.exitCode ?? -1,
2096
- stdout: result.stdout ?? "",
2097
- stderr: result.stderr ?? ""
2098
- };
2099
- }
2100
- function shellQuote(s) {
2101
- return `'${s.replace(/'/g, `'\\''`)}'`;
2102
- }
2103
-
2104
- // ../../packages/ctl/dist/index.js
2105
- import { readFile as readFile3 } from "fs/promises";
2106
- import { parse as parseYaml } from "yaml";
2107
- function renderStatusTable(rows) {
2108
- if (rows.length === 0) return "(no services configured)";
2109
- const headers = ["NAME", "STATE", "PID", "RESTARTS", "LAST EXIT", "BLOCKED ON", "COMMAND"];
2110
- const data = rows.map((r) => [
2111
- r.name,
2112
- r.state,
2113
- r.pid === null ? "-" : String(r.pid),
2114
- String(r.restarts),
2115
- r.lastExitCode === null ? "-" : String(r.lastExitCode),
2116
- r.blockedOn.length === 0 ? "-" : r.blockedOn.join(","),
2117
- truncate(r.command, 40)
2118
- ]);
2119
- return renderTable(headers, data);
2120
- }
2121
- function renderTaskTable(rows) {
2122
- if (rows.length === 0) return "(no tasks configured)";
2123
- const headers = ["NAME", "STATE", "EXIT", "STARTED", "FINISHED", "COMMAND"];
2124
- const data = rows.map((r) => [
2125
- r.name,
2126
- r.state,
2127
- r.lastExitCode === null ? "-" : String(r.lastExitCode),
2128
- r.startedAt ?? "-",
2129
- r.finishedAt ?? "-",
2130
- truncate(r.command, 40)
2131
- ]);
2132
- return renderTable(headers, data);
2133
- }
2134
- function renderPortsTable(rows) {
2135
- if (rows.length === 0) return "(no ports listening)";
2136
- const named = rows.filter((r) => r.service);
2137
- const other = rows.filter((r) => !r.service).map((r) => r.port).sort((a, b) => a - b);
2138
- const lines = [];
2139
- if (named.length > 0) {
2140
- lines.push(
2141
- renderTable(
2142
- ["PORT", "SERVICE"],
2143
- named.map((r) => [`:${String(r.port)}`, r.service ?? "-"])
2144
- )
2145
- );
2146
- }
2147
- if (other.length > 0) {
2148
- lines.push(`other (${other.length}): ${other.join(", ")}`);
2149
- }
2150
- return lines.join("\n");
2151
- }
2152
- function renderTable(headers, data) {
2153
- const widths = headers.map(
2154
- (h, i) => Math.max(h.length, ...data.map((row) => (row[i] ?? "").length))
2155
- );
2156
- const fmt = (row) => row.map((cell, i) => cell.padEnd(widths[i] ?? cell.length)).join(" ");
2157
- return [fmt(headers), ...data.map(fmt)].join("\n");
2158
- }
2159
- function truncate(s, n) {
2160
- if (s.length <= n) return s;
2161
- return s.slice(0, n - 1) + "\u2026";
2162
- }
2163
- var DEFAULT_BACKOFF = {
2164
- initialMs: 500,
2165
- maxMs: 3e4,
2166
- factor: 2
2167
- };
2168
- var DEFAULT_PROBE_INTERVAL_MS = 500;
2169
- var DEFAULT_PROBE_INITIAL_DELAY_MS = 0;
2170
- var DEFAULT_PROBE_TIMEOUT_MS = 6e4;
2171
- var DEFAULT_PROBE_HOST = "127.0.0.1";
2172
- var DEFAULT_PROBE_ON_TIMEOUT = "kill";
2173
- var ConfigError = class extends Error {
2174
- constructor(message) {
2175
- super(message);
2176
- this.name = "ConfigError";
2177
- }
2178
- };
2179
- function isPlainObject2(v) {
2180
- return typeof v === "object" && v !== null && !Array.isArray(v);
2181
- }
2182
- function parseEnv(raw, where) {
2183
- if (raw === void 0 || raw === null) return void 0;
2184
- if (!isPlainObject2(raw)) {
2185
- throw new ConfigError(`${where}.env must be a mapping of string \u2192 string`);
2186
- }
2187
- const out = {};
2188
- for (const [k, v] of Object.entries(raw)) {
2189
- if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean") {
2190
- throw new ConfigError(`${where}.env.${k} must be a scalar`);
2191
- }
2192
- out[k] = String(v);
2193
- }
2194
- return out;
2195
- }
2196
- function parseCommand(raw, where) {
2197
- if (typeof raw === "string") {
2198
- if (raw.trim().length === 0) {
2199
- throw new ConfigError(`${where}.command must not be empty`);
2200
- }
2201
- return raw;
2202
- }
2203
- if (Array.isArray(raw)) {
2204
- if (raw.length === 0) {
2205
- throw new ConfigError(`${where}.command array must not be empty`);
2206
- }
2207
- const argv = [];
2208
- for (const [i, item] of raw.entries()) {
2209
- if (typeof item !== "string") {
2210
- throw new ConfigError(`${where}.command[${String(i)}] must be a string`);
2211
- }
2212
- argv.push(item);
2213
- }
2214
- return argv;
2215
- }
2216
- throw new ConfigError(`${where}.command must be a string or array of strings`);
2217
- }
2218
- function parseRestart(raw, where) {
2219
- if (raw === void 0) return "on-failure";
2220
- if (raw === "always" || raw === "on-failure" || raw === "never") return raw;
2221
- throw new ConfigError(`${where}.restart must be one of: always, on-failure, never`);
2222
- }
2223
- var BACKOFF_KEYS = /* @__PURE__ */ new Set(["initial_ms", "max_ms", "factor"]);
2224
- function parseBackoff(raw, where) {
2225
- if (raw === void 0) return { ...DEFAULT_BACKOFF };
2226
- if (!isPlainObject2(raw)) {
2227
- throw new ConfigError(`${where}.backoff must be a mapping`);
2228
- }
2229
- rejectUnknownKeys(raw, BACKOFF_KEYS, `${where}.backoff`);
2230
- const initialMs = parseNonNegativeInt(
2231
- raw.initial_ms,
2232
- `${where}.backoff.initial_ms`,
2233
- DEFAULT_BACKOFF.initialMs
2234
- );
2235
- const maxMs = parseNonNegativeInt(raw.max_ms, `${where}.backoff.max_ms`, DEFAULT_BACKOFF.maxMs);
2236
- const factor = parseFactor(raw.factor, `${where}.backoff.factor`, DEFAULT_BACKOFF.factor);
2237
- if (maxMs < initialMs) {
2238
- throw new ConfigError(`${where}.backoff.max_ms must be >= initial_ms`);
2239
- }
2240
- return { initialMs, maxMs, factor };
2241
- }
2242
- function rejectUnknownKeys(obj, allowed, where) {
2243
- for (const key of Object.keys(obj)) {
2244
- if (!allowed.has(key)) {
2245
- throw new ConfigError(`${where} has unknown key "${key}"`);
2246
- }
2247
- }
2248
- }
2249
- function parseNonNegativeInt(raw, where, fallback) {
2250
- if (raw === void 0) return fallback;
2251
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) {
2252
- throw new ConfigError(`${where} must be a non-negative number`);
2253
- }
2254
- return Math.floor(raw);
2255
- }
2256
- function parsePositiveInt(raw, where, fallback) {
2257
- if (raw === void 0) return fallback;
2258
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1) {
2259
- throw new ConfigError(`${where} must be a positive integer`);
2260
- }
2261
- return Math.floor(raw);
2262
- }
2263
- function parseFactor(raw, where, fallback) {
2264
- if (raw === void 0) return fallback;
2265
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1) {
2266
- throw new ConfigError(`${where} must be a number >= 1`);
2267
- }
2268
- return raw;
2269
- }
2270
- function parseOnTimeout(raw, where) {
2271
- if (raw === void 0) return DEFAULT_PROBE_ON_TIMEOUT;
2272
- if (raw === "kill" || raw === "mark_unhealthy") return raw;
2273
- throw new ConfigError(`${where} must be one of: kill, mark_unhealthy`);
2274
- }
2275
- function parseNeeds(raw, where) {
2276
- if (raw === void 0 || raw === null) return [];
2277
- if (!Array.isArray(raw)) {
2278
- throw new ConfigError(`${where} must be an array of unit names`);
2279
- }
2280
- const seen = /* @__PURE__ */ new Set();
2281
- const out = [];
2282
- for (const [i, item] of raw.entries()) {
2283
- if (typeof item !== "string") {
2284
- throw new ConfigError(`${where}[${String(i)}] must be a string`);
2285
- }
2286
- if (!/^[A-Za-z0-9_-]+$/.test(item)) {
2287
- throw new ConfigError(`${where}[${String(i)}] "${item}" must match [A-Za-z0-9_-]+`);
2288
- }
2289
- if (seen.has(item)) continue;
2290
- seen.add(item);
2291
- out.push(item);
2292
- }
2293
- return out;
2294
- }
2295
- var PROBE_KEYS = /* @__PURE__ */ new Set([
2296
- "port",
2297
- "host",
2298
- "log_match",
2299
- "http",
2300
- "expect_status",
2301
- "interval_ms",
2302
- "initial_delay_ms",
2303
- "timeout_ms",
2304
- "on_timeout"
2305
- ]);
2306
- function parseReadyWhen(raw, where) {
2307
- if (raw === void 0 || raw === null) return void 0;
2308
- if (!isPlainObject2(raw)) {
2309
- throw new ConfigError(`${where}.ready_when must be a mapping`);
2310
- }
2311
- rejectUnknownKeys(raw, PROBE_KEYS, `${where}.ready_when`);
2312
- const kinds = [];
2313
- if (raw.port !== void 0) kinds.push("port");
2314
- if (raw.log_match !== void 0) kinds.push("log_match");
2315
- if (raw.http !== void 0) kinds.push("http");
2316
- if (kinds.length === 0) {
2317
- throw new ConfigError(
2318
- `${where}.ready_when must declare exactly one of: port, log_match, http`
2319
- );
2320
- }
2321
- if (kinds.length > 1) {
2322
- throw new ConfigError(
2323
- `${where}.ready_when may declare only one of: port, log_match, http (got ${kinds.join(", ")})`
2324
- );
2325
- }
2326
- const timeoutMs = parsePositiveInt(
2327
- raw.timeout_ms,
2328
- `${where}.ready_when.timeout_ms`,
2329
- DEFAULT_PROBE_TIMEOUT_MS
2330
- );
2331
- const onTimeout = parseOnTimeout(raw.on_timeout, `${where}.ready_when.on_timeout`);
2332
- const kind = kinds[0];
2333
- if (kind === "log_match") {
2334
- if (raw.host !== void 0 || raw.expect_status !== void 0 || raw.interval_ms !== void 0 || raw.initial_delay_ms !== void 0) {
2335
- throw new ConfigError(
2336
- `${where}.ready_when.log_match cannot be combined with host/expect_status/interval_ms/initial_delay_ms`
2337
- );
2338
- }
2339
- const pat = assertString(raw.log_match, `${where}.ready_when.log_match`);
2340
- let pattern;
2341
- try {
2342
- pattern = new RegExp(pat);
2343
- } catch (err) {
2344
- throw new ConfigError(
2345
- `${where}.ready_when.log_match is not a valid regex: ${err instanceof Error ? err.message : String(err)}`
2346
- );
2347
- }
2348
- return { kind: "log_match", pattern, timeoutMs, onTimeout };
2349
- }
2350
- const intervalMs = parsePositiveInt(
2351
- raw.interval_ms,
2352
- `${where}.ready_when.interval_ms`,
2353
- DEFAULT_PROBE_INTERVAL_MS
2354
- );
2355
- const initialDelayMs = parseNonNegativeInt(
2356
- raw.initial_delay_ms,
2357
- `${where}.ready_when.initial_delay_ms`,
2358
- DEFAULT_PROBE_INITIAL_DELAY_MS
2359
- );
2360
- if (kind === "port") {
2361
- if (raw.expect_status !== void 0) {
2362
- throw new ConfigError(`${where}.ready_when.expect_status only applies to http probes`);
2363
- }
2364
- const port = parsePositiveInt(raw.port, `${where}.ready_when.port`, 0);
2365
- if (port < 1 || port > 65535) {
2366
- throw new ConfigError(`${where}.ready_when.port must be between 1 and 65535`);
2367
- }
2368
- const host = raw.host === void 0 ? DEFAULT_PROBE_HOST : assertString(raw.host, `${where}.ready_when.host`);
2369
- return { kind: "port", port, host, intervalMs, initialDelayMs, timeoutMs, onTimeout };
2370
- }
2371
- if (raw.host !== void 0) {
2372
- throw new ConfigError(`${where}.ready_when.host only applies to port probes`);
2373
- }
2374
- const url = assertString(raw.http, `${where}.ready_when.http`);
2375
- let parsed;
2376
- try {
2377
- parsed = new URL(url);
2378
- } catch {
2379
- throw new ConfigError(`${where}.ready_when.http must be a valid URL`);
2380
- }
2381
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2382
- throw new ConfigError(`${where}.ready_when.http must use http(s):// (got ${parsed.protocol})`);
2383
- }
2384
- let expectStatus;
2385
- if (raw.expect_status !== void 0) {
2386
- expectStatus = parsePositiveInt(raw.expect_status, `${where}.ready_when.expect_status`, 0);
2387
- if (expectStatus < 100 || expectStatus > 599) {
2388
- throw new ConfigError(`${where}.ready_when.expect_status must be between 100 and 599`);
2389
- }
2390
- }
2391
- return { kind: "http", url, expectStatus, intervalMs, initialDelayMs, timeoutMs, onTimeout };
2392
- }
2393
- var RESERVED_WEB_PORT = 80;
2394
- var SERVICE_KEYS = /* @__PURE__ */ new Set([
2395
- "command",
2396
- "cwd",
2397
- "env",
2398
- "autostart",
2399
- "restart",
2400
- "backoff",
2401
- "needs",
2402
- "ready_when",
2403
- "expose",
2404
- "ide"
2405
- ]);
2406
- var EXPOSE_KEYS = /* @__PURE__ */ new Set(["port", "as"]);
2407
- function parseExpose(raw, where) {
2408
- if (raw === void 0 || raw === null) return void 0;
2409
- if (!isPlainObject2(raw)) {
2410
- throw new ConfigError(`${where}.expose must be a mapping`);
2411
- }
2412
- rejectUnknownKeys(raw, EXPOSE_KEYS, `${where}.expose`);
2413
- if (raw.port === void 0) {
2414
- throw new ConfigError(`${where}.expose.port is required`);
2415
- }
2416
- const port = parsePortNumber(raw.port, `${where}.expose.port`);
2417
- const as = raw.as === void 0 ? RESERVED_WEB_PORT : parsePortNumber(raw.as, `${where}.expose.as`);
2418
- if (as !== RESERVED_WEB_PORT) {
2419
- throw new ConfigError(
2420
- `${where}.expose.as must be ${String(RESERVED_WEB_PORT)} (the only container port AgentBox publishes)`
2421
- );
2422
- }
2423
- return { port, as };
2424
- }
2425
- function parsePortNumber(raw, where) {
2426
- if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 65535) {
2427
- throw new ConfigError(`${where} must be an integer between 1 and 65535`);
2428
- }
2429
- return raw;
2430
- }
2431
- function parseService(name, raw) {
2432
- const where = `services.${name}`;
2433
- if (!isPlainObject2(raw)) {
2434
- throw new ConfigError(`${where} must be a mapping`);
2435
- }
2436
- rejectUnknownKeys(raw, SERVICE_KEYS, where);
2437
- const command = parseCommand(raw.command, where);
2438
- const cwd = raw.cwd === void 0 ? void 0 : assertString(raw.cwd, `${where}.cwd`);
2439
- const env = parseEnv(raw.env, where);
2440
- const autostart = raw.autostart === void 0 ? true : assertBool(raw.autostart, `${where}.autostart`);
2441
- const restart2 = parseRestart(raw.restart, where);
2442
- const backoff = parseBackoff(raw.backoff, where);
2443
- const needs = parseNeeds(raw.needs, `${where}.needs`);
2444
- const readyWhen = parseReadyWhen(raw.ready_when, where);
2445
- const expose = parseExpose(raw.expose, where);
2446
- return { name, command, cwd, env, autostart, restart: restart2, backoff, needs, readyWhen, expose };
2447
- }
2448
- var TASK_KEYS = /* @__PURE__ */ new Set(["command", "cwd", "env", "needs"]);
2449
- function parseTask(name, raw) {
2450
- const where = `tasks.${name}`;
2451
- if (!isPlainObject2(raw)) {
2452
- throw new ConfigError(`${where} must be a mapping`);
2453
- }
2454
- rejectUnknownKeys(raw, TASK_KEYS, where);
2455
- const command = parseCommand(raw.command, where);
2456
- const cwd = raw.cwd === void 0 ? void 0 : assertString(raw.cwd, `${where}.cwd`);
2457
- const env = parseEnv(raw.env, where);
2458
- const needs = parseNeeds(raw.needs, `${where}.needs`);
2459
- return { name, command, cwd, env, needs };
2460
- }
2461
- function assertString(raw, where) {
2462
- if (typeof raw !== "string") throw new ConfigError(`${where} must be a string`);
2463
- return raw;
2464
- }
2465
- function assertBool(raw, where) {
2466
- if (typeof raw !== "boolean") throw new ConfigError(`${where} must be a boolean`);
2467
- return raw;
2468
- }
2469
- var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults"]);
2470
- function validateUnitGraph(tasks, services) {
2471
- const names = /* @__PURE__ */ new Set();
2472
- for (const t of tasks) {
2473
- if (names.has(t.name)) {
2474
- throw new ConfigError(`unit name "${t.name}" declared more than once (task vs service collision)`);
2475
- }
2476
- names.add(t.name);
2477
- }
2478
- for (const s of services) {
2479
- if (names.has(s.name)) {
2480
- throw new ConfigError(`unit name "${s.name}" declared more than once (task vs service collision)`);
2481
- }
2482
- names.add(s.name);
2483
- }
2484
- const deps = /* @__PURE__ */ new Map();
2485
- for (const t of tasks) deps.set(t.name, t.needs);
2486
- for (const s of services) deps.set(s.name, s.needs);
2487
- for (const [unit, list] of deps) {
2488
- for (const dep of list) {
2489
- if (!names.has(dep)) {
2490
- throw new ConfigError(`unit "${unit}" needs unknown unit "${dep}"`);
2491
- }
2492
- if (dep === unit) {
2493
- throw new ConfigError(`unit "${unit}" cannot depend on itself`);
2494
- }
2495
- }
2496
- }
2497
- const WHITE = 0, GRAY = 1, BLACK = 2;
2498
- const color = /* @__PURE__ */ new Map();
2499
- for (const name of deps.keys()) color.set(name, WHITE);
2500
- const stack = [];
2501
- function visit(name) {
2502
- color.set(name, GRAY);
2503
- stack.push(name);
2504
- for (const dep of deps.get(name)) {
2505
- const c = color.get(dep) ?? WHITE;
2506
- if (c === GRAY) {
2507
- const startIdx = stack.indexOf(dep);
2508
- const cycle = stack.slice(startIdx).concat(dep).join(" \u2192 ");
2509
- throw new ConfigError(`cyclic dependency: ${cycle}`);
2510
- }
2511
- if (c === WHITE) visit(dep);
2512
- }
2513
- stack.pop();
2514
- color.set(name, BLACK);
2515
- }
2516
- for (const name of deps.keys()) {
2517
- if (color.get(name) === WHITE) visit(name);
2518
- }
2519
- }
2520
- function parseConfig(text) {
2521
- let doc;
2522
- try {
2523
- doc = parseYaml(text);
2524
- } catch (err) {
2525
- throw new ConfigError(`yaml parse error: ${err instanceof Error ? err.message : String(err)}`);
2526
- }
2527
- if (doc === null || doc === void 0) return { services: [], tasks: [] };
2528
- if (!isPlainObject2(doc)) {
2529
- throw new ConfigError("top-level config must be a mapping");
2530
- }
2531
- rejectUnknownKeys(doc, TOP_LEVEL_KEYS, "(root)");
2532
- const services = [];
2533
- const servicesRaw = doc.services;
2534
- if (servicesRaw !== void 0 && servicesRaw !== null) {
2535
- if (!isPlainObject2(servicesRaw)) {
2536
- throw new ConfigError("services must be a mapping of name \u2192 service");
2537
- }
2538
- for (const [name, raw] of Object.entries(servicesRaw)) {
2539
- if (!/^[A-Za-z0-9_-]+$/.test(name)) {
2540
- throw new ConfigError(`service name "${name}" must match [A-Za-z0-9_-]+`);
2541
- }
2542
- services.push(parseService(name, raw));
2543
- }
2544
- }
2545
- const tasks = [];
2546
- const tasksRaw = doc.tasks;
2547
- if (tasksRaw !== void 0 && tasksRaw !== null) {
2548
- if (!isPlainObject2(tasksRaw)) {
2549
- throw new ConfigError("tasks must be a mapping of name \u2192 task");
2550
- }
2551
- for (const [name, raw] of Object.entries(tasksRaw)) {
2552
- if (!/^[A-Za-z0-9_-]+$/.test(name)) {
2553
- throw new ConfigError(`task name "${name}" must match [A-Za-z0-9_-]+`);
2554
- }
2555
- tasks.push(parseTask(name, raw));
2556
- }
2557
- }
2558
- if (doc.ide !== void 0 && doc.ide !== null && !isPlainObject2(doc.ide)) {
2559
- throw new ConfigError("ide must be a mapping");
2560
- }
2561
- if (doc.defaults !== void 0 && doc.defaults !== null && !isPlainObject2(doc.defaults)) {
2562
- throw new ConfigError("defaults must be a mapping");
2563
- }
2564
- validateUnitGraph(tasks, services);
2565
- const exposed = services.filter((s) => s.expose);
2566
- if (exposed.length > 1) {
2567
- throw new ConfigError(
2568
- `at most one service may set expose: (got: ${exposed.map((s) => s.name).join(", ")})`
2569
- );
2570
- }
2571
- return { services, tasks };
2572
- }
2573
- async function loadConfig(path) {
2574
- let text;
2575
- try {
2576
- text = await readFile3(path, "utf8");
2577
- } catch (err) {
2578
- if (err.code === "ENOENT") {
2579
- return { services: [], tasks: [] };
2580
- }
2581
- throw err;
2582
- }
2583
- return parseConfig(text);
2584
- }
2585
-
2586
- export {
2587
- DEFAULT_RELAY_PORT,
2588
- RELAY_CONTAINER_NAME,
2589
- RELAY_NETWORK_NAME,
2590
- RELAY_IMAGE_REF,
2591
- SHARED_CLAUDE_VOLUME,
2592
- resolveClaudeVolume,
2593
- ensureClaudeVolume,
2594
- seedSetupSkillIntoVolume,
2595
- buildClaudeMounts,
2596
- rebuildPluginNativeDeps,
2597
- ClaudeSessionError,
2598
- startClaudeSession,
2599
- buildClaudeAttachArgv,
2600
- buildClaudeDashboardAttachArgv,
2601
- buildShellArgv,
2602
- buildClaudeLoginRunArgv,
2603
- runInteractiveClaudeLogin,
2604
- warmUpClaudeCredentials,
2605
- formatDetachNotice,
2606
- claudeSessionInfo,
2607
- pullClaudeExtras,
2608
- SHARED_DOCKER_CACHE_VOLUME,
2609
- dockerVolumeName,
2610
- launchDockerdDaemon,
2611
- launchVncDaemon,
2612
- generateVncPassword,
2613
- VNC_CONTAINER_PORT,
2614
- buildVncUrls,
2615
- WEB_CONTAINER_PORT,
2616
- detectGitRepos,
2617
- pickFreshBranch,
2618
- gitWorktreePathFor,
2619
- collectRepoCarryOver,
2620
- chownGitBindParents,
2621
- bindWorktrees,
2622
- seedWorkspace,
2623
- seedWorkspaceFromDir,
2624
- removeInBoxWorktree,
2625
- SNAPSHOTS_ROOT,
2626
- snapshotPathFor,
2627
- createSnapshot,
2628
- launchCtlDaemon,
2629
- ensureHomeOwnedByVscode,
2630
- ensureRelay,
2631
- stopRelay,
2632
- getRelayStatus,
2633
- generateRelayToken,
2634
- registerBoxWithRelay,
2635
- forgetBoxFromRelay,
2636
- setRelayNotice,
2637
- clearRelayNotice,
2638
- rehydrateRelayRegistry,
2639
- ideProfile,
2640
- SHARED_VSCODE_EXTENSIONS_VOLUME,
2641
- SHARED_CURSOR_EXTENSIONS_VOLUME,
2642
- vscodeServerVolumeName,
2643
- cursorServerVolumeName,
2644
- buildIdeMounts,
2645
- ensureIdeVolumes,
2646
- repairIdeOwnership,
2647
- containerHex,
2648
- ensureAgentboxTasksFile,
2649
- renderStatusTable,
2650
- renderTaskTable,
2651
- renderPortsTable,
2652
- ConfigError,
2653
- loadConfig
2654
- };
2655
- //# sourceMappingURL=chunk-HTTKML3C.js.map