@madarco/agentbox 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_cloud-attach-DMVH6GWO.js +12 -0
- package/dist/chunk-7KOEFGN2.js +1162 -0
- package/dist/chunk-7KOEFGN2.js.map +1 -0
- package/dist/chunk-I24B6AXR.js +600 -0
- package/dist/chunk-I24B6AXR.js.map +1 -0
- package/dist/chunk-NAVL4R34.js +7546 -0
- package/dist/chunk-NAVL4R34.js.map +1 -0
- package/dist/chunk-NW5NYTQM.js +1366 -0
- package/dist/chunk-NW5NYTQM.js.map +1 -0
- package/dist/chunk-UK72UQ5U.js +237 -0
- package/dist/chunk-UK72UQ5U.js.map +1 -0
- package/dist/chunk-V5KZGB5V.js +722 -0
- package/dist/chunk-V5KZGB5V.js.map +1 -0
- package/dist/cloud-poller-ZIWSADJB-JXFRJUEM.js +10 -0
- package/dist/dist-ETCFRVPA.js +423 -0
- package/dist/dist-QZGJIBT5.js +1339 -0
- package/dist/dist-QZGJIBT5.js.map +1 -0
- package/dist/dist-R67WMLCF.js +183 -0
- package/dist/dist-R67WMLCF.js.map +1 -0
- package/dist/index.js +4088 -1451
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/runtime/docker/Dockerfile.box +115 -19
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +34 -19
- package/runtime/docker/packages/ctl/dist/bin.cjs +10246 -758
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +13 -3
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +37 -0
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +4 -9
- package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
- package/runtime/hetzner/agentbox-codex-hooks.json +37 -0
- package/runtime/hetzner/agentbox-dockerd-start +132 -0
- package/runtime/hetzner/agentbox-open +28 -0
- package/runtime/hetzner/agentbox-setup-skill.md +196 -0
- package/runtime/hetzner/agentbox-vnc-start +77 -0
- package/runtime/hetzner/claude-managed-settings.json +54 -0
- package/runtime/hetzner/ctl.cjs +22350 -0
- package/runtime/hetzner/custom-system-CLAUDE.md +27 -0
- package/runtime/hetzner/scripts/install-box.sh +365 -0
- package/runtime/relay/bin.cjs +9182 -754
- package/share/agentbox-setup/SKILL.md +34 -19
- package/dist/chunk-6VTAPD4H.js +0 -507
- package/dist/chunk-6VTAPD4H.js.map +0 -1
- package/dist/chunk-7J5AJLWG.js +0 -238
- package/dist/chunk-7J5AJLWG.js.map +0 -1
- package/dist/chunk-FJNIFTWK.js +0 -523
- package/dist/chunk-FJNIFTWK.js.map +0 -1
- package/dist/chunk-HPZMD5DE.js +0 -106
- package/dist/chunk-HPZMD5DE.js.map +0 -1
- package/dist/chunk-PXUBE5KS.js +0 -2346
- package/dist/chunk-PXUBE5KS.js.map +0 -1
- package/dist/chunk-RFC5F5HR.js +0 -1709
- package/dist/chunk-RFC5F5HR.js.map +0 -1
- package/dist/create-AHZ3GVEZ-TGEDL7UX.js +0 -15
- package/dist/lifecycle-LFOL6YFM-TCHDX3J5.js +0 -38
- package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
- package/dist/stats-Z4BVJODD-HEC4TMUZ.js +0 -19
- package/dist/stats-Z4BVJODD-HEC4TMUZ.js.map +0 -1
- /package/dist/{create-AHZ3GVEZ-TGEDL7UX.js.map → _cloud-attach-DMVH6GWO.js.map} +0 -0
- /package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js.map → cloud-poller-ZIWSADJB-JXFRJUEM.js.map} +0 -0
- /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-ETCFRVPA.js.map} +0 -0
package/dist/chunk-PXUBE5KS.js
DELETED
|
@@ -1,2346 +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-RFC5F5HR.js";
|
|
12
|
-
|
|
13
|
-
// ../../packages/sandbox-docker/dist/chunk-LGNJND37.js
|
|
14
|
-
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
15
|
-
import { homedir, tmpdir } from "os";
|
|
16
|
-
import { join, relative } from "path";
|
|
17
|
-
import { execa } from "execa";
|
|
18
|
-
import { randomBytes } from "crypto";
|
|
19
|
-
import { execa as execa2 } from "execa";
|
|
20
|
-
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
21
|
-
import { join as join2 } from "path";
|
|
22
|
-
import { execa as execa3 } from "execa";
|
|
23
|
-
import { execa as execa4 } from "execa";
|
|
24
|
-
import { mkdir as mkdir2, readdir as readdir3, rm as rm2, stat as stat3 } from "fs/promises";
|
|
25
|
-
import { homedir as homedir2, platform } from "os";
|
|
26
|
-
import { join as join3, resolve } from "path";
|
|
27
|
-
import { stat as stat4 } from "fs/promises";
|
|
28
|
-
import { spawn } from "child_process";
|
|
29
|
-
import { randomBytes as randomBytes2 } from "crypto";
|
|
30
|
-
import { existsSync, openSync } from "fs";
|
|
31
|
-
import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
32
|
-
import { request as httpRequest } from "http";
|
|
33
|
-
import { homedir as homedir3 } from "os";
|
|
34
|
-
import { dirname, join as join4, resolve as resolve2 } from "path";
|
|
35
|
-
import { setTimeout as delay } from "timers/promises";
|
|
36
|
-
import { fileURLToPath } from "url";
|
|
37
|
-
|
|
38
|
-
// ../../packages/relay/dist/index.js
|
|
39
|
-
var DEFAULT_RELAY_PORT = 8787;
|
|
40
|
-
var RELAY_CONTAINER_NAME = "agentbox-relay";
|
|
41
|
-
var RELAY_NETWORK_NAME = "agentbox-net";
|
|
42
|
-
var RELAY_IMAGE_REF = "agentbox/relay:dev";
|
|
43
|
-
var MAX_BODY_BYTES = 1024 * 1024;
|
|
44
|
-
|
|
45
|
-
// ../../packages/sandbox-docker/dist/chunk-LGNJND37.js
|
|
46
|
-
function isHostPathHookCommand(command, hostHome) {
|
|
47
|
-
if (typeof command !== "string" || command.length === 0) return false;
|
|
48
|
-
if (hostHome.length === 0) return false;
|
|
49
|
-
return command.includes(hostHome + "/");
|
|
50
|
-
}
|
|
51
|
-
function filterHostHooks(data, hostHome) {
|
|
52
|
-
const clone = structuredClone(data);
|
|
53
|
-
const removedCommands = [];
|
|
54
|
-
if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
|
|
55
|
-
return { data: clone, removedCommands };
|
|
56
|
-
}
|
|
57
|
-
const top = clone;
|
|
58
|
-
const hooksRoot = top.hooks;
|
|
59
|
-
if (hooksRoot === null || typeof hooksRoot !== "object" || Array.isArray(hooksRoot)) {
|
|
60
|
-
return { data: clone, removedCommands };
|
|
61
|
-
}
|
|
62
|
-
for (const triggerName of Object.keys(hooksRoot)) {
|
|
63
|
-
const triggerValue = hooksRoot[triggerName];
|
|
64
|
-
if (!Array.isArray(triggerValue)) continue;
|
|
65
|
-
for (const entry of triggerValue) {
|
|
66
|
-
if (entry === null || typeof entry !== "object") continue;
|
|
67
|
-
const matcher = entry;
|
|
68
|
-
const inner = matcher.hooks;
|
|
69
|
-
if (!Array.isArray(inner)) continue;
|
|
70
|
-
for (let i = inner.length - 1; i >= 0; i--) {
|
|
71
|
-
const leaf = inner[i];
|
|
72
|
-
if (leaf === null || typeof leaf !== "object") continue;
|
|
73
|
-
if (leaf.type === "command" && typeof leaf.command === "string" && isHostPathHookCommand(leaf.command, hostHome)) {
|
|
74
|
-
removedCommands.push(leaf.command);
|
|
75
|
-
inner.splice(i, 1);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return { data: clone, removedCommands };
|
|
81
|
-
}
|
|
82
|
-
function addProjectAlias(data, fromPath, toPath) {
|
|
83
|
-
const clone = structuredClone(data);
|
|
84
|
-
if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
|
|
85
|
-
return { data: clone, aliased: false };
|
|
86
|
-
}
|
|
87
|
-
if (fromPath === toPath || fromPath.length === 0 || toPath.length === 0) {
|
|
88
|
-
return { data: clone, aliased: false };
|
|
89
|
-
}
|
|
90
|
-
const obj = clone;
|
|
91
|
-
const projects = obj.projects;
|
|
92
|
-
if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
|
|
93
|
-
return { data: clone, aliased: false };
|
|
94
|
-
}
|
|
95
|
-
const projectsMap = projects;
|
|
96
|
-
const src = projectsMap[fromPath];
|
|
97
|
-
if (src === null || typeof src !== "object" || Array.isArray(src)) {
|
|
98
|
-
return { data: clone, aliased: false };
|
|
99
|
-
}
|
|
100
|
-
const existing = projectsMap[toPath];
|
|
101
|
-
if (existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
102
|
-
projectsMap[toPath] = { ...existing, ...src };
|
|
103
|
-
} else {
|
|
104
|
-
projectsMap[toPath] = structuredClone(src);
|
|
105
|
-
}
|
|
106
|
-
return { data: clone, aliased: true };
|
|
107
|
-
}
|
|
108
|
-
function clearInstallMethod(data) {
|
|
109
|
-
const clone = structuredClone(data);
|
|
110
|
-
if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
|
|
111
|
-
return { data: clone, cleared: false };
|
|
112
|
-
}
|
|
113
|
-
const obj = clone;
|
|
114
|
-
if (Object.prototype.hasOwnProperty.call(obj, "installMethod")) {
|
|
115
|
-
delete obj.installMethod;
|
|
116
|
-
return { data: clone, cleared: true };
|
|
117
|
-
}
|
|
118
|
-
return { data: clone, cleared: false };
|
|
119
|
-
}
|
|
120
|
-
var SKILL_EXCLUDE_PREFIXES = ["agentbox-"];
|
|
121
|
-
var CONTAINER_PLUGINS_PREFIX = "/home/vscode/.claude/plugins/";
|
|
122
|
-
function pickNewItems(boxNames, hostNames, excludePrefixes = []) {
|
|
123
|
-
const host = new Set(hostNames);
|
|
124
|
-
const seen = /* @__PURE__ */ new Set();
|
|
125
|
-
const out = [];
|
|
126
|
-
for (const name of boxNames) {
|
|
127
|
-
if (name.length === 0 || host.has(name) || seen.has(name)) continue;
|
|
128
|
-
if (excludePrefixes.some((p) => name.startsWith(p))) continue;
|
|
129
|
-
seen.add(name);
|
|
130
|
-
out.push(name);
|
|
131
|
-
}
|
|
132
|
-
return out.sort();
|
|
133
|
-
}
|
|
134
|
-
function rewritePluginPaths(value, hostPluginsPrefix) {
|
|
135
|
-
if (typeof value === "string") {
|
|
136
|
-
return value.split(CONTAINER_PLUGINS_PREFIX).join(hostPluginsPrefix);
|
|
137
|
-
}
|
|
138
|
-
if (Array.isArray(value)) {
|
|
139
|
-
return value.map((v) => rewritePluginPaths(v, hostPluginsPrefix));
|
|
140
|
-
}
|
|
141
|
-
if (value !== null && typeof value === "object") {
|
|
142
|
-
const out = {};
|
|
143
|
-
for (const [k, v] of Object.entries(value)) {
|
|
144
|
-
out[k] = rewritePluginPaths(v, hostPluginsPrefix);
|
|
145
|
-
}
|
|
146
|
-
return out;
|
|
147
|
-
}
|
|
148
|
-
return value;
|
|
149
|
-
}
|
|
150
|
-
function isPlainObject(v) {
|
|
151
|
-
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
152
|
-
}
|
|
153
|
-
function additiveMerge(hostRoot, boxRoot, hostPluginsPrefix, selectMap, withMap) {
|
|
154
|
-
const hostMap = selectMap(hostRoot);
|
|
155
|
-
const boxMap = selectMap(boxRoot);
|
|
156
|
-
if (!isPlainObject(boxMap)) {
|
|
157
|
-
return { data: hostRoot, changed: false, addedKeys: [] };
|
|
158
|
-
}
|
|
159
|
-
const base = isPlainObject(hostMap) ? { ...hostMap } : {};
|
|
160
|
-
const addedKeys = [];
|
|
161
|
-
for (const [key, value] of Object.entries(boxMap)) {
|
|
162
|
-
if (Object.prototype.hasOwnProperty.call(base, key)) continue;
|
|
163
|
-
base[key] = rewritePluginPaths(value, hostPluginsPrefix);
|
|
164
|
-
addedKeys.push(key);
|
|
165
|
-
}
|
|
166
|
-
if (addedKeys.length === 0) {
|
|
167
|
-
return { data: hostRoot, changed: false, addedKeys: [] };
|
|
168
|
-
}
|
|
169
|
-
return { data: withMap(hostRoot, base), changed: true, addedKeys: addedKeys.sort() };
|
|
170
|
-
}
|
|
171
|
-
function mergeKnownMarketplaces(hostJson, boxJson, opts) {
|
|
172
|
-
const prefix = `${opts.hostHome}/.claude/plugins/`;
|
|
173
|
-
return additiveMerge(
|
|
174
|
-
isPlainObject(hostJson) ? hostJson : {},
|
|
175
|
-
boxJson,
|
|
176
|
-
prefix,
|
|
177
|
-
(root) => root,
|
|
178
|
-
(_host, merged) => merged
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
function mergeInstalledPlugins(hostJson, boxJson, opts) {
|
|
182
|
-
const prefix = `${opts.hostHome}/.claude/plugins/`;
|
|
183
|
-
const hostRoot = isPlainObject(hostJson) ? hostJson : { plugins: {} };
|
|
184
|
-
return additiveMerge(
|
|
185
|
-
hostRoot,
|
|
186
|
-
boxJson,
|
|
187
|
-
prefix,
|
|
188
|
-
(root) => isPlainObject(root) ? root["plugins"] : void 0,
|
|
189
|
-
(host, merged) => ({ ...host, plugins: merged })
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
var SHARED_CLAUDE_VOLUME = "agentbox-claude-config";
|
|
193
|
-
var DEFAULT_CLAUDE_SESSION = "claude";
|
|
194
|
-
var CONTAINER_CLAUDE_DIR = "/home/vscode/.claude";
|
|
195
|
-
var CONTAINER_USER = "vscode";
|
|
196
|
-
var CONTAINER_WORKSPACE = "/workspace";
|
|
197
|
-
var IN_BOX_SETUP_GUIDE_PATH = "/usr/local/share/agentbox/setup-guide.md";
|
|
198
|
-
var SETUP_SKILL_DST = "/dst/skills/agentbox-setup/SKILL.md";
|
|
199
|
-
function resolveClaudeVolume(opts) {
|
|
200
|
-
if (opts.isolate) {
|
|
201
|
-
return { volume: `${SHARED_CLAUDE_VOLUME}-${opts.boxId}` };
|
|
202
|
-
}
|
|
203
|
-
return { volume: SHARED_CLAUDE_VOLUME };
|
|
204
|
-
}
|
|
205
|
-
async function pathExists(p) {
|
|
206
|
-
try {
|
|
207
|
-
await stat(p);
|
|
208
|
-
return true;
|
|
209
|
-
} catch {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
async function findBrokenSymlinks(root) {
|
|
214
|
-
const broken = [];
|
|
215
|
-
async function walk(dir) {
|
|
216
|
-
let entries;
|
|
217
|
-
try {
|
|
218
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
219
|
-
} catch {
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
for (const ent of entries) {
|
|
223
|
-
const full = join(dir, ent.name);
|
|
224
|
-
if (ent.isSymbolicLink()) {
|
|
225
|
-
try {
|
|
226
|
-
await stat(full);
|
|
227
|
-
} catch {
|
|
228
|
-
broken.push(relative(root, full));
|
|
229
|
-
}
|
|
230
|
-
} else if (ent.isDirectory()) {
|
|
231
|
-
await walk(full);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
await walk(root);
|
|
236
|
-
return broken;
|
|
237
|
-
}
|
|
238
|
-
async function ensureClaudeVolume(spec, opts) {
|
|
239
|
-
const existed = await volumeExists(spec.volume);
|
|
240
|
-
await ensureVolume(spec.volume);
|
|
241
|
-
const created = !existed;
|
|
242
|
-
if (!opts.syncFromHost) return { created, synced: false };
|
|
243
|
-
const hostClaude = join(homedir(), ".claude");
|
|
244
|
-
if (!await pathExists(hostClaude)) return { created, synced: false };
|
|
245
|
-
const hostClaudeJson = join(homedir(), ".claude.json");
|
|
246
|
-
const hasJson = await pathExists(hostClaudeJson);
|
|
247
|
-
const hostHome = homedir();
|
|
248
|
-
const hostAgents = join(homedir(), ".agents");
|
|
249
|
-
const hasAgents = await pathExists(hostAgents);
|
|
250
|
-
const args = [
|
|
251
|
-
"run",
|
|
252
|
-
"--rm",
|
|
253
|
-
"--user",
|
|
254
|
-
"0",
|
|
255
|
-
// HOST_HOME used inside the shell script to rewrite host-absolute
|
|
256
|
-
// installPath values in plugins/installed_plugins.json.
|
|
257
|
-
"-e",
|
|
258
|
-
`HOST_HOME=${hostHome}`,
|
|
259
|
-
"-v",
|
|
260
|
-
`${spec.volume}:/dst`,
|
|
261
|
-
"-v",
|
|
262
|
-
`${hostClaude}:/src-claude:ro`
|
|
263
|
-
];
|
|
264
|
-
if (hasJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
|
|
265
|
-
if (hasAgents) args.push("-v", `${hostAgents}:/.agents:ro`);
|
|
266
|
-
const filterDir = await mkdtemp(join(tmpdir(), "agentbox-claude-filter-"));
|
|
267
|
-
let filteredHookCount = 0;
|
|
268
|
-
let clearedInstallMethod = false;
|
|
269
|
-
let aliasedProjectKey = false;
|
|
270
|
-
try {
|
|
271
|
-
const settingsResult = await maybeFilterTo(
|
|
272
|
-
join(hostClaude, "settings.json"),
|
|
273
|
-
join(filterDir, "settings.json"),
|
|
274
|
-
hostHome
|
|
275
|
-
);
|
|
276
|
-
filteredHookCount += settingsResult.removedHooks;
|
|
277
|
-
if (hasJson) {
|
|
278
|
-
const jsonResult = await maybeFilterTo(
|
|
279
|
-
hostClaudeJson,
|
|
280
|
-
join(filterDir, "_claude.json"),
|
|
281
|
-
hostHome,
|
|
282
|
-
{
|
|
283
|
-
clearInstallMethod: true,
|
|
284
|
-
aliasProject: opts.hostWorkspace ? { from: opts.hostWorkspace, to: CONTAINER_WORKSPACE } : void 0
|
|
285
|
-
}
|
|
286
|
-
);
|
|
287
|
-
filteredHookCount += jsonResult.removedHooks;
|
|
288
|
-
clearedInstallMethod = jsonResult.clearedInstallMethod;
|
|
289
|
-
aliasedProjectKey = jsonResult.aliasedProjectKey;
|
|
290
|
-
}
|
|
291
|
-
if (filteredHookCount > 0 || clearedInstallMethod || aliasedProjectKey) {
|
|
292
|
-
args.push("-v", `${filterDir}:/src-filter:ro`);
|
|
293
|
-
}
|
|
294
|
-
const brokenSymlinks = await findBrokenSymlinks(hostClaude);
|
|
295
|
-
const rsyncExcludes = ["--exclude=node_modules"];
|
|
296
|
-
for (const rel of brokenSymlinks) rsyncExcludes.push(`--exclude=/${rel}`);
|
|
297
|
-
const rsyncFlags = `-a --copy-unsafe-links ${rsyncExcludes.join(" ")}`;
|
|
298
|
-
args.push(
|
|
299
|
-
opts.image,
|
|
300
|
-
"sh",
|
|
301
|
-
"-c",
|
|
302
|
-
// Each step in its own brace group so a missing optional file (no
|
|
303
|
-
// .claude.json on host, no filtered overlays) doesn't short-circuit the
|
|
304
|
-
// final chown.
|
|
305
|
-
//
|
|
306
|
-
// --copy-unsafe-links: dereference symlinks pointing OUTSIDE
|
|
307
|
-
// /src-claude (e.g. ~/.claude/skills/* -> ../../.agents/skills/*),
|
|
308
|
-
// so user skills materialize as real directories inside the volume
|
|
309
|
-
// without needing to also bind-mount ~/.agents.
|
|
310
|
-
// --exclude=node_modules: skip every node_modules directory anywhere
|
|
311
|
-
// in the tree. Plugin caches (plugins/cache/<m>/<p>/<v>/node_modules)
|
|
312
|
-
// ship host-platform-specific binaries (darwin-arm64 fsevents,
|
|
313
|
-
// esbuild, rollup, sharp) that are useless on linux/amd64. The
|
|
314
|
-
// plugin source still lands; node_modules is rebuilt lazily inside
|
|
315
|
-
// the box on first claude session (see rebuildPluginNativeDeps).
|
|
316
|
-
//
|
|
317
|
-
// The top-level plugin registry JSONs (installed_plugins.json,
|
|
318
|
-
// known_marketplaces.json) carry host-absolute `installPath` /
|
|
319
|
-
// `installLocation` values; without rewriting, claude resolves them
|
|
320
|
-
// to `/Users/<you>/...` (or, when claude detects the missing path,
|
|
321
|
-
// falls back to a slug derived from `source.repo` like
|
|
322
|
-
// `microsoft-playwright-cli` — neither exists in the box, and the
|
|
323
|
-
// marketplace fails to load, which masquerades as "plugin not
|
|
324
|
-
// found in marketplace"). One sweep over every JSON directly under
|
|
325
|
-
// /dst/plugins/ catches both files (and any future registry).
|
|
326
|
-
// One-shot migration for volumes that were populated before
|
|
327
|
-
// --exclude=node_modules existed. Without it, the volume keeps
|
|
328
|
-
// host-darwin node_modules forever (rsync without --delete won't
|
|
329
|
-
// remove them). The `.agentbox-cleaned-nm-v1` sentinel makes the wipe
|
|
330
|
-
// a no-op after the first run; rebuildPluginNativeDeps repopulates
|
|
331
|
-
// linux/amd64 node_modules on the next `agentbox claude`.
|
|
332
|
-
`{ [ ! -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`
|
|
333
|
-
);
|
|
334
|
-
await execa("docker", args);
|
|
335
|
-
} finally {
|
|
336
|
-
await rm(filterDir, { recursive: true, force: true });
|
|
337
|
-
}
|
|
338
|
-
return { created, synced: true, filteredHookCount, clearedInstallMethod, aliasedProjectKey };
|
|
339
|
-
}
|
|
340
|
-
async function seedSetupSkillIntoVolume(volume, image) {
|
|
341
|
-
try {
|
|
342
|
-
const { stdout } = await execa("docker", [
|
|
343
|
-
"run",
|
|
344
|
-
"--rm",
|
|
345
|
-
"--user",
|
|
346
|
-
"0",
|
|
347
|
-
"-v",
|
|
348
|
-
`${volume}:/dst`,
|
|
349
|
-
image,
|
|
350
|
-
"sh",
|
|
351
|
-
"-c",
|
|
352
|
-
// Prints SEEDED only when it actually copies, so the caller can log
|
|
353
|
-
// accurately. The whole thing is `|| true` so an already-present skill
|
|
354
|
-
// (or missing image asset) is a clean no-op, never a non-zero exit.
|
|
355
|
-
`{ [ ! -e /dst/skills/agentbox-setup ] && [ -f ${IN_BOX_SETUP_GUIDE_PATH} ] && 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`
|
|
356
|
-
]);
|
|
357
|
-
return { seeded: stdout.includes("SEEDED") };
|
|
358
|
-
} catch {
|
|
359
|
-
return { seeded: false };
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
async function maybeFilterTo(src, dest, hostHome, opts = {}) {
|
|
363
|
-
let parsed;
|
|
364
|
-
try {
|
|
365
|
-
parsed = JSON.parse(await readFile(src, "utf8"));
|
|
366
|
-
} catch {
|
|
367
|
-
return { removedHooks: 0, clearedInstallMethod: false, aliasedProjectKey: false };
|
|
368
|
-
}
|
|
369
|
-
const filtered = filterHostHooks(parsed, hostHome);
|
|
370
|
-
let working = filtered.data;
|
|
371
|
-
let cleared = false;
|
|
372
|
-
if (opts.clearInstallMethod) {
|
|
373
|
-
const r = clearInstallMethod(working);
|
|
374
|
-
working = r.data;
|
|
375
|
-
cleared = r.cleared;
|
|
376
|
-
}
|
|
377
|
-
let aliased = false;
|
|
378
|
-
if (opts.aliasProject) {
|
|
379
|
-
const r = addProjectAlias(working, opts.aliasProject.from, opts.aliasProject.to);
|
|
380
|
-
working = r.data;
|
|
381
|
-
aliased = r.aliased;
|
|
382
|
-
}
|
|
383
|
-
if (filtered.removedCommands.length === 0 && !cleared && !aliased) {
|
|
384
|
-
return { removedHooks: 0, clearedInstallMethod: false, aliasedProjectKey: false };
|
|
385
|
-
}
|
|
386
|
-
await writeFile(dest, JSON.stringify(working, null, 2));
|
|
387
|
-
return {
|
|
388
|
-
removedHooks: filtered.removedCommands.length,
|
|
389
|
-
clearedInstallMethod: cleared,
|
|
390
|
-
aliasedProjectKey: aliased
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
var FORWARDED_ENV_KEYS = [
|
|
394
|
-
"ANTHROPIC_API_KEY",
|
|
395
|
-
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
396
|
-
"CLAUDE_EFFORT",
|
|
397
|
-
"ANTHROPIC_MODEL"
|
|
398
|
-
];
|
|
399
|
-
function buildClaudeMounts(spec, hostEnv) {
|
|
400
|
-
const env = {};
|
|
401
|
-
for (const k of FORWARDED_ENV_KEYS) {
|
|
402
|
-
const v = hostEnv[k];
|
|
403
|
-
if (typeof v === "string" && v.length > 0) env[k] = v;
|
|
404
|
-
}
|
|
405
|
-
return {
|
|
406
|
-
extraVolumes: [`${spec.volume}:${CONTAINER_CLAUDE_DIR}`],
|
|
407
|
-
env,
|
|
408
|
-
volumeName: spec.volume
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
var PLUGIN_INSTALLED_MARKER = ".agentbox-installed";
|
|
412
|
-
var PLUGIN_FAILED_MARKER = ".agentbox-install-failed";
|
|
413
|
-
var PLUGIN_INSTALL_BACKOFF_MS = 6 * 60 * 60 * 1e3;
|
|
414
|
-
var PLUGIN_INSTALL_BACKOFF_MIN = Math.round(PLUGIN_INSTALL_BACKOFF_MS / 6e4);
|
|
415
|
-
var NPM_CACHE_DIR = "/home/vscode/.claude/.agentbox-npm-cache";
|
|
416
|
-
async function isFile(p) {
|
|
417
|
-
try {
|
|
418
|
-
return (await stat(p)).isFile();
|
|
419
|
-
} catch {
|
|
420
|
-
return false;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
async function isRecentFailMarker(p) {
|
|
424
|
-
try {
|
|
425
|
-
const st = await stat(p);
|
|
426
|
-
return Date.now() - st.mtimeMs < PLUGIN_INSTALL_BACKOFF_MS;
|
|
427
|
-
} catch {
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
async function isDir(p) {
|
|
432
|
-
try {
|
|
433
|
-
return (await stat(p)).isDirectory();
|
|
434
|
-
} catch {
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
async function scanPluginCacheForRebuild(cacheRoot) {
|
|
439
|
-
let marketplaces;
|
|
440
|
-
try {
|
|
441
|
-
marketplaces = await readdir(cacheRoot, { withFileTypes: true });
|
|
442
|
-
} catch {
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
for (const m of marketplaces) {
|
|
446
|
-
if (!m.isDirectory()) continue;
|
|
447
|
-
const mPath = join(cacheRoot, m.name);
|
|
448
|
-
let plugins;
|
|
449
|
-
try {
|
|
450
|
-
plugins = await readdir(mPath, { withFileTypes: true });
|
|
451
|
-
} catch {
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
for (const p of plugins) {
|
|
455
|
-
if (!p.isDirectory()) continue;
|
|
456
|
-
const pPath = join(mPath, p.name);
|
|
457
|
-
let versions;
|
|
458
|
-
try {
|
|
459
|
-
versions = await readdir(pPath, { withFileTypes: true });
|
|
460
|
-
} catch {
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
for (const v of versions) {
|
|
464
|
-
if (!v.isDirectory()) continue;
|
|
465
|
-
const vPath = join(pPath, v.name);
|
|
466
|
-
if (!await isFile(join(vPath, "package.json"))) continue;
|
|
467
|
-
if (await isFile(join(vPath, PLUGIN_INSTALLED_MARKER))) continue;
|
|
468
|
-
if (await isRecentFailMarker(join(vPath, PLUGIN_FAILED_MARKER))) continue;
|
|
469
|
-
return true;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
async function resolveClaudeCacheLiveOnHost(volume) {
|
|
476
|
-
if (await detectEngine() !== "orbstack") return null;
|
|
477
|
-
if (!await isDir(orbstackVolumePath(volume))) return null;
|
|
478
|
-
return orbstackVolumePath(volume, "plugins", "cache");
|
|
479
|
-
}
|
|
480
|
-
async function rebuildPluginNativeDeps(container, opts = {}) {
|
|
481
|
-
if (opts.volume) {
|
|
482
|
-
const cacheRoot = await resolveClaudeCacheLiveOnHost(opts.volume);
|
|
483
|
-
if (cacheRoot && !await scanPluginCacheForRebuild(cacheRoot)) {
|
|
484
|
-
return { rebuilt: [], failed: [], skipped: true };
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
const script = `set -u
|
|
488
|
-
PLUGINS_DIR=/home/vscode/.claude/plugins/cache
|
|
489
|
-
MARKER=${PLUGIN_INSTALLED_MARKER}
|
|
490
|
-
FAILMARKER=${PLUGIN_FAILED_MARKER}
|
|
491
|
-
NPM_CACHE=${NPM_CACHE_DIR}
|
|
492
|
-
BACKOFF_MIN=${PLUGIN_INSTALL_BACKOFF_MIN}
|
|
493
|
-
MAX=4
|
|
494
|
-
[ -d "$PLUGINS_DIR" ] || exit 0
|
|
495
|
-
mkdir -p "$NPM_CACHE"
|
|
496
|
-
WORK=$(mktemp -d)
|
|
497
|
-
relkey() { printf '%s' "\${1#$PLUGINS_DIR/}" | tr '/' '_'; }
|
|
498
|
-
# Run one plugin's install. $1 is frozen by value at call time, so it's safe
|
|
499
|
-
# to read from the backgrounded subshell; the rest are set-once constants.
|
|
500
|
-
do_one() {
|
|
501
|
-
d=$1
|
|
502
|
-
key=$(relkey "$d")
|
|
503
|
-
if (cd "$d" && \\
|
|
504
|
-
if [ -f package-lock.json ]; then \\
|
|
505
|
-
npm ci --no-audit --no-fund --silent --prefer-offline --cache "$NPM_CACHE"; \\
|
|
506
|
-
else \\
|
|
507
|
-
npm install --no-audit --no-fund --silent --no-package-lock --prefer-offline --cache "$NPM_CACHE"; \\
|
|
508
|
-
fi) >"$WORK/$key.out" 2>"$WORK/$key.err"; then
|
|
509
|
-
touch "$d/$MARKER"
|
|
510
|
-
rm -f "$d/$FAILMARKER"
|
|
511
|
-
printf 'OK\\n' > "$WORK/$key.res"
|
|
512
|
-
else
|
|
513
|
-
: > "$d/$FAILMARKER"
|
|
514
|
-
printf 'FAIL\\n' > "$WORK/$key.res"
|
|
515
|
-
fi
|
|
516
|
-
}
|
|
517
|
-
n=0
|
|
518
|
-
for dir in "$PLUGINS_DIR"/*/*/*/; do
|
|
519
|
-
[ -d "$dir" ] || continue
|
|
520
|
-
[ -f "$dir/package.json" ] || continue
|
|
521
|
-
[ -f "$dir/$MARKER" ] && continue
|
|
522
|
-
[ -n "$(find "$dir" -maxdepth 1 -name "$FAILMARKER" -mmin -$BACKOFF_MIN 2>/dev/null)" ] && continue
|
|
523
|
-
echo "REBUILD_START \${dir#$PLUGINS_DIR/}"
|
|
524
|
-
n=$((n+1))
|
|
525
|
-
printf '%s\\n' "$dir" >> "$WORK/dirs"
|
|
526
|
-
done
|
|
527
|
-
if [ "$n" -eq 0 ]; then rm -rf "$WORK"; exit 0; fi
|
|
528
|
-
running=0
|
|
529
|
-
while IFS= read -r dir; do
|
|
530
|
-
do_one "$dir" &
|
|
531
|
-
running=$((running+1))
|
|
532
|
-
if [ "$running" -ge "$MAX" ]; then wait; running=0; fi
|
|
533
|
-
done < "$WORK/dirs"
|
|
534
|
-
wait
|
|
535
|
-
while IFS= read -r dir; do
|
|
536
|
-
key=$(relkey "$dir")
|
|
537
|
-
rel=\${dir#$PLUGINS_DIR/}
|
|
538
|
-
[ -f "$WORK/$key.res" ] || continue
|
|
539
|
-
read -r st < "$WORK/$key.res"
|
|
540
|
-
if [ "$st" = OK ]; then
|
|
541
|
-
echo "REBUILD_OK $rel"
|
|
542
|
-
else
|
|
543
|
-
echo "REBUILD_FAIL $rel"
|
|
544
|
-
sed 's/^/ /' "$WORK/$key.err"
|
|
545
|
-
echo "REBUILD_FAIL_END"
|
|
546
|
-
fi
|
|
547
|
-
done < "$WORK/dirs"
|
|
548
|
-
rm -rf "$WORK"
|
|
549
|
-
`;
|
|
550
|
-
const result = await execa(
|
|
551
|
-
"docker",
|
|
552
|
-
["exec", "--user", CONTAINER_USER, container, "sh", "-c", script],
|
|
553
|
-
{ reject: false }
|
|
554
|
-
);
|
|
555
|
-
const rebuilt = [];
|
|
556
|
-
const failed = [];
|
|
557
|
-
const lines = (result.stdout ?? "").split("\n");
|
|
558
|
-
let collectingFail = null;
|
|
559
|
-
for (const line of lines) {
|
|
560
|
-
if (collectingFail) {
|
|
561
|
-
if (line === "REBUILD_FAIL_END") {
|
|
562
|
-
failed.push({ dir: collectingFail.dir, stderr: collectingFail.stderr.join("\n") });
|
|
563
|
-
collectingFail = null;
|
|
564
|
-
} else {
|
|
565
|
-
collectingFail.stderr.push(line);
|
|
566
|
-
}
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
if (line.startsWith("REBUILD_START ")) {
|
|
570
|
-
opts.onProgress?.(`rebuilding ${line.slice("REBUILD_START ".length)}`);
|
|
571
|
-
} else if (line.startsWith("REBUILD_OK ")) {
|
|
572
|
-
rebuilt.push(line.slice("REBUILD_OK ".length));
|
|
573
|
-
} else if (line.startsWith("REBUILD_FAIL ")) {
|
|
574
|
-
collectingFail = { dir: line.slice("REBUILD_FAIL ".length), stderr: [] };
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
return { rebuilt, failed, skipped: false };
|
|
578
|
-
}
|
|
579
|
-
var ClaudeSessionError = class extends Error {
|
|
580
|
-
constructor(message) {
|
|
581
|
-
super(message);
|
|
582
|
-
this.name = "ClaudeSessionError";
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
function shQuote(arg) {
|
|
586
|
-
if (arg.length === 0) return `''`;
|
|
587
|
-
if (/^[A-Za-z0-9_\-./=:@%+,]+$/.test(arg)) return arg;
|
|
588
|
-
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
589
|
-
}
|
|
590
|
-
async function startClaudeSession(opts) {
|
|
591
|
-
const sessionName = opts.sessionName ?? DEFAULT_CLAUDE_SESSION;
|
|
592
|
-
const cmd = ["claude", ...opts.claudeArgs].map(shQuote).join(" ");
|
|
593
|
-
const term = process.env["TERM"] ?? "xterm-256color";
|
|
594
|
-
const envFlags = ["-e", `TERM=${term}`];
|
|
595
|
-
for (const k of FORWARDED_ENV_KEYS) {
|
|
596
|
-
const v = process.env[k];
|
|
597
|
-
if (typeof v === "string" && v.length > 0) envFlags.push("-e", `${k}=${v}`);
|
|
598
|
-
}
|
|
599
|
-
const result = await execa(
|
|
600
|
-
"docker",
|
|
601
|
-
[
|
|
602
|
-
"exec",
|
|
603
|
-
...envFlags,
|
|
604
|
-
"--user",
|
|
605
|
-
CONTAINER_USER,
|
|
606
|
-
opts.container,
|
|
607
|
-
"tmux",
|
|
608
|
-
"new-session",
|
|
609
|
-
"-d",
|
|
610
|
-
"-s",
|
|
611
|
-
sessionName,
|
|
612
|
-
cmd,
|
|
613
|
-
...buildClaudeStatusBarArgs(sessionName)
|
|
614
|
-
],
|
|
615
|
-
{ reject: false }
|
|
616
|
-
);
|
|
617
|
-
if (result.exitCode === 0) return;
|
|
618
|
-
const stderr = (result.stderr ?? "").toString();
|
|
619
|
-
if (result.exitCode === 127 || /command not found|tmux: not found/i.test(stderr)) {
|
|
620
|
-
throw new ClaudeSessionError(
|
|
621
|
-
`tmux is missing from the box image. Rebuild with: docker rmi agentbox/box:dev && retry.`
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
if (/claude.*not found|exec: "claude"/i.test(stderr)) {
|
|
625
|
-
throw new ClaudeSessionError(
|
|
626
|
-
`claude is missing from the box image. Rebuild with: docker rmi agentbox/box:dev && retry.`
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
if (/duplicate session/i.test(stderr)) {
|
|
630
|
-
throw new ClaudeSessionError(
|
|
631
|
-
`a tmux session "${sessionName}" already exists in ${opts.container}; use \`agentbox claude attach\` to reattach.`
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
throw new ClaudeSessionError(
|
|
635
|
-
`failed to start claude session in ${opts.container}: ${stderr.trim() || `exit ${String(result.exitCode)}`}`
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
function buildClaudeAttachArgv(container, sessionName) {
|
|
639
|
-
const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
|
|
640
|
-
const term = process.env["TERM"] ?? "xterm-256color";
|
|
641
|
-
return [
|
|
642
|
-
"exec",
|
|
643
|
-
"-it",
|
|
644
|
-
"-e",
|
|
645
|
-
`TERM=${term}`,
|
|
646
|
-
"--user",
|
|
647
|
-
CONTAINER_USER,
|
|
648
|
-
container,
|
|
649
|
-
"tmux",
|
|
650
|
-
"attach",
|
|
651
|
-
"-t",
|
|
652
|
-
name
|
|
653
|
-
];
|
|
654
|
-
}
|
|
655
|
-
function buildClaudeDashboardAttachArgv(container, sessionName) {
|
|
656
|
-
const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
|
|
657
|
-
const dash = `${name}-dash`;
|
|
658
|
-
const term = process.env["TERM"] ?? "xterm-256color";
|
|
659
|
-
return [
|
|
660
|
-
"exec",
|
|
661
|
-
"-it",
|
|
662
|
-
"-e",
|
|
663
|
-
`TERM=${term}`,
|
|
664
|
-
"--user",
|
|
665
|
-
CONTAINER_USER,
|
|
666
|
-
container,
|
|
667
|
-
"tmux",
|
|
668
|
-
"new-session",
|
|
669
|
-
"-A",
|
|
670
|
-
"-d",
|
|
671
|
-
"-s",
|
|
672
|
-
dash,
|
|
673
|
-
"-t",
|
|
674
|
-
name,
|
|
675
|
-
";",
|
|
676
|
-
"set",
|
|
677
|
-
"-t",
|
|
678
|
-
dash,
|
|
679
|
-
"status",
|
|
680
|
-
"off",
|
|
681
|
-
";",
|
|
682
|
-
"attach",
|
|
683
|
-
"-t",
|
|
684
|
-
dash
|
|
685
|
-
];
|
|
686
|
-
}
|
|
687
|
-
function buildClaudeStatusBarArgs(sessionName) {
|
|
688
|
-
const s = sessionName;
|
|
689
|
-
return [
|
|
690
|
-
// Server-global (no -t): primary prefix Ctrl+a (dashboard parity), keep
|
|
691
|
-
// tmux's default Ctrl+b as a secondary prefix so users with existing
|
|
692
|
-
// muscle memory / integrations aren't broken. `q` is the same key under
|
|
693
|
-
// both prefixes (single key table) -> Ctrl+a q AND Ctrl+b q both detach.
|
|
694
|
-
// `send-prefix` / `send-prefix -2` let a double-tap of either prefix
|
|
695
|
-
// reach Claude as that literal key.
|
|
696
|
-
";",
|
|
697
|
-
"set",
|
|
698
|
-
"-g",
|
|
699
|
-
"prefix",
|
|
700
|
-
"C-a",
|
|
701
|
-
";",
|
|
702
|
-
"set",
|
|
703
|
-
"-g",
|
|
704
|
-
"prefix2",
|
|
705
|
-
"C-b",
|
|
706
|
-
";",
|
|
707
|
-
"bind-key",
|
|
708
|
-
"C-a",
|
|
709
|
-
"send-prefix",
|
|
710
|
-
";",
|
|
711
|
-
"bind-key",
|
|
712
|
-
"C-b",
|
|
713
|
-
"send-prefix",
|
|
714
|
-
"-2",
|
|
715
|
-
";",
|
|
716
|
-
"bind-key",
|
|
717
|
-
"q",
|
|
718
|
-
"detach-client",
|
|
719
|
-
// Hide the inner tmux status bar — the wrapped-pty footer (for
|
|
720
|
-
// `agentbox claude` / `agentbox shell`) and the dashboard's own status
|
|
721
|
-
// row already show the box name + detach hint; without `status off`
|
|
722
|
-
// they double up.
|
|
723
|
-
";",
|
|
724
|
-
"set",
|
|
725
|
-
"-t",
|
|
726
|
-
s,
|
|
727
|
-
"status",
|
|
728
|
-
"off"
|
|
729
|
-
];
|
|
730
|
-
}
|
|
731
|
-
function buildShellArgv(container) {
|
|
732
|
-
const term = process.env["TERM"] ?? "xterm-256color";
|
|
733
|
-
return ["exec", "-it", "-e", `TERM=${term}`, "--user", CONTAINER_USER, container, "bash", "-l"];
|
|
734
|
-
}
|
|
735
|
-
function formatDetachNotice(ref) {
|
|
736
|
-
return `Session detached. Reattach with: agentbox claude attach ${ref}`;
|
|
737
|
-
}
|
|
738
|
-
async function claudeSessionInfo(container, sessionName) {
|
|
739
|
-
const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
|
|
740
|
-
const has = await execa(
|
|
741
|
-
"docker",
|
|
742
|
-
["exec", "--user", CONTAINER_USER, container, "tmux", "has-session", "-t", name],
|
|
743
|
-
{ reject: false }
|
|
744
|
-
);
|
|
745
|
-
if (has.exitCode !== 0) {
|
|
746
|
-
return { running: false, sessionName: name, startedAt: null };
|
|
747
|
-
}
|
|
748
|
-
const ts = await execa(
|
|
749
|
-
"docker",
|
|
750
|
-
[
|
|
751
|
-
"exec",
|
|
752
|
-
"--user",
|
|
753
|
-
CONTAINER_USER,
|
|
754
|
-
container,
|
|
755
|
-
"tmux",
|
|
756
|
-
"display-message",
|
|
757
|
-
"-p",
|
|
758
|
-
"-t",
|
|
759
|
-
name,
|
|
760
|
-
"#{session_created}"
|
|
761
|
-
],
|
|
762
|
-
{ reject: false }
|
|
763
|
-
);
|
|
764
|
-
let startedAt = null;
|
|
765
|
-
if (ts.exitCode === 0) {
|
|
766
|
-
const secs = Number.parseInt((ts.stdout ?? "").trim(), 10);
|
|
767
|
-
if (Number.isFinite(secs) && secs > 0) startedAt = new Date(secs * 1e3).toISOString();
|
|
768
|
-
}
|
|
769
|
-
return { running: true, sessionName: name, startedAt };
|
|
770
|
-
}
|
|
771
|
-
var PULL_DIR_CATEGORIES = ["skills", "agents", "commands"];
|
|
772
|
-
async function listChildDirs(dir) {
|
|
773
|
-
try {
|
|
774
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
775
|
-
return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
776
|
-
} catch {
|
|
777
|
-
return [];
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
async function readJsonFile(path) {
|
|
781
|
-
try {
|
|
782
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
783
|
-
} catch {
|
|
784
|
-
return void 0;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
async function pullClaudeExtras(spec, opts) {
|
|
788
|
-
const hostHome = homedir();
|
|
789
|
-
const hostClaude = join(hostHome, ".claude");
|
|
790
|
-
const inventoryScript = [
|
|
791
|
-
"for cat in skills agents commands; do",
|
|
792
|
-
' [ -d "/src/$cat" ] || continue;',
|
|
793
|
-
' for d in "/src/$cat"/*/; do',
|
|
794
|
-
' [ -d "$d" ] || continue;',
|
|
795
|
-
' printf "DIR %s %s\\n" "$cat" "$(basename "$d")";',
|
|
796
|
-
" done;",
|
|
797
|
-
"done;",
|
|
798
|
-
"if [ -d /src/plugins/cache ]; then",
|
|
799
|
-
" for m in /src/plugins/cache/*/; do",
|
|
800
|
-
' [ -d "$m" ] || continue;',
|
|
801
|
-
' for p in "$m"*/; do',
|
|
802
|
-
' [ -d "$p" ] || continue;',
|
|
803
|
-
' printf "PLUGIN %s/%s\\n" "$(basename "$m")" "$(basename "$p")";',
|
|
804
|
-
" done;",
|
|
805
|
-
" done;",
|
|
806
|
-
"fi;",
|
|
807
|
-
"for f in installed_plugins known_marketplaces; do",
|
|
808
|
-
' [ -f "/src/plugins/$f.json" ] || continue;',
|
|
809
|
-
' printf "JSON %s " "$f";',
|
|
810
|
-
' base64 -w0 "/src/plugins/$f.json";',
|
|
811
|
-
' printf "\\n";',
|
|
812
|
-
"done"
|
|
813
|
-
].join(" ");
|
|
814
|
-
const inv = await execa(
|
|
815
|
-
"docker",
|
|
816
|
-
["run", "--rm", "--user", "0", "-v", `${spec.volume}:/src:ro`, opts.image, "sh", "-c", inventoryScript],
|
|
817
|
-
{ reject: false }
|
|
818
|
-
);
|
|
819
|
-
if (inv.exitCode !== 0) {
|
|
820
|
-
throw new ClaudeSessionError(
|
|
821
|
-
`failed to read claude-config volume ${spec.volume}: ${(inv.stderr ?? "").toString().trim() || `exit ${String(inv.exitCode)}`}`
|
|
822
|
-
);
|
|
823
|
-
}
|
|
824
|
-
const boxDirs = { skills: [], agents: [], commands: [] };
|
|
825
|
-
const boxPlugins = [];
|
|
826
|
-
const boxJson = {};
|
|
827
|
-
for (const line of (inv.stdout ?? "").split("\n")) {
|
|
828
|
-
if (line.startsWith("DIR ")) {
|
|
829
|
-
const rest = line.slice(4);
|
|
830
|
-
const sp = rest.indexOf(" ");
|
|
831
|
-
if (sp === -1) continue;
|
|
832
|
-
const cat = rest.slice(0, sp);
|
|
833
|
-
const name = rest.slice(sp + 1);
|
|
834
|
-
if (cat in boxDirs) boxDirs[cat].push(name);
|
|
835
|
-
} else if (line.startsWith("PLUGIN ")) {
|
|
836
|
-
boxPlugins.push(line.slice(7));
|
|
837
|
-
} else if (line.startsWith("JSON ")) {
|
|
838
|
-
const rest = line.slice(5);
|
|
839
|
-
const sp = rest.indexOf(" ");
|
|
840
|
-
if (sp === -1) continue;
|
|
841
|
-
const which = rest.slice(0, sp);
|
|
842
|
-
try {
|
|
843
|
-
boxJson[which] = JSON.parse(Buffer.from(rest.slice(sp + 1), "base64").toString("utf8"));
|
|
844
|
-
} catch {
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
const newItems = [];
|
|
849
|
-
const applyPaths = [];
|
|
850
|
-
for (const cat of PULL_DIR_CATEGORIES) {
|
|
851
|
-
const hostNames = await listChildDirs(join(hostClaude, cat));
|
|
852
|
-
const excludes = cat === "skills" ? SKILL_EXCLUDE_PREFIXES : [];
|
|
853
|
-
for (const name of pickNewItems(boxDirs[cat] ?? [], hostNames, excludes)) {
|
|
854
|
-
newItems.push({ category: cat, name });
|
|
855
|
-
applyPaths.push({ src: `/src/${cat}/${name}`, dest: `/dst/${cat}/${name}` });
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
const hostPluginKeys = [];
|
|
859
|
-
for (const m of await listChildDirs(join(hostClaude, "plugins", "cache"))) {
|
|
860
|
-
for (const p of await listChildDirs(join(hostClaude, "plugins", "cache", m))) {
|
|
861
|
-
hostPluginKeys.push(`${m}/${p}`);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
for (const key of pickNewItems(boxPlugins, hostPluginKeys)) {
|
|
865
|
-
newItems.push({ category: "plugins", name: key });
|
|
866
|
-
applyPaths.push({ src: `/src/plugins/cache/${key}`, dest: `/dst/plugins/cache/${key}` });
|
|
867
|
-
}
|
|
868
|
-
const hostInstalled = await readJsonFile(join(hostClaude, "plugins", "installed_plugins.json"));
|
|
869
|
-
const hostMarkets = await readJsonFile(join(hostClaude, "plugins", "known_marketplaces.json"));
|
|
870
|
-
const mergedInstalled = mergeInstalledPlugins(hostInstalled, boxJson["installed_plugins"], {
|
|
871
|
-
hostHome
|
|
872
|
-
});
|
|
873
|
-
const mergedMarkets = mergeKnownMarketplaces(hostMarkets, boxJson["known_marketplaces"], {
|
|
874
|
-
hostHome
|
|
875
|
-
});
|
|
876
|
-
const mergedRegistries = [];
|
|
877
|
-
if (mergedInstalled.changed) mergedRegistries.push("installed_plugins.json");
|
|
878
|
-
if (mergedMarkets.changed) mergedRegistries.push("known_marketplaces.json");
|
|
879
|
-
if (opts.dryRun || newItems.length === 0 && mergedRegistries.length === 0) {
|
|
880
|
-
return { newItems, mergedRegistries };
|
|
881
|
-
}
|
|
882
|
-
if (applyPaths.length > 0) {
|
|
883
|
-
const cmds = applyPaths.map(({ src, dest }) => {
|
|
884
|
-
const parent = dest.slice(0, dest.lastIndexOf("/"));
|
|
885
|
-
return `mkdir -p '${parent}' && rsync -a --ignore-existing --exclude=node_modules '${src}/' '${dest}/'`;
|
|
886
|
-
});
|
|
887
|
-
const apply = await execa(
|
|
888
|
-
"docker",
|
|
889
|
-
[
|
|
890
|
-
"run",
|
|
891
|
-
"--rm",
|
|
892
|
-
"--user",
|
|
893
|
-
"0",
|
|
894
|
-
"-v",
|
|
895
|
-
`${spec.volume}:/src:ro`,
|
|
896
|
-
"-v",
|
|
897
|
-
`${hostClaude}:/dst`,
|
|
898
|
-
opts.image,
|
|
899
|
-
"sh",
|
|
900
|
-
"-c",
|
|
901
|
-
cmds.join(" && ")
|
|
902
|
-
],
|
|
903
|
-
{ reject: false }
|
|
904
|
-
);
|
|
905
|
-
if (apply.exitCode !== 0) {
|
|
906
|
-
throw new ClaudeSessionError(
|
|
907
|
-
`failed to copy extensions from ${spec.volume}: ${(apply.stderr ?? "").toString().trim() || `exit ${String(apply.exitCode)}`}`
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
if (mergedMarkets.changed || mergedInstalled.changed) {
|
|
912
|
-
await mkdir(join(hostClaude, "plugins"), { recursive: true });
|
|
913
|
-
if (mergedMarkets.changed) {
|
|
914
|
-
await writeFile(
|
|
915
|
-
join(hostClaude, "plugins", "known_marketplaces.json"),
|
|
916
|
-
`${JSON.stringify(mergedMarkets.data, null, 2)}
|
|
917
|
-
`
|
|
918
|
-
);
|
|
919
|
-
}
|
|
920
|
-
if (mergedInstalled.changed) {
|
|
921
|
-
await writeFile(
|
|
922
|
-
join(hostClaude, "plugins", "installed_plugins.json"),
|
|
923
|
-
`${JSON.stringify(mergedInstalled.data, null, 2)}
|
|
924
|
-
`
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
return { newItems, mergedRegistries };
|
|
929
|
-
}
|
|
930
|
-
var SHARED_DOCKER_CACHE_VOLUME = "agentbox-docker-cache";
|
|
931
|
-
function dockerVolumeName(boxId, shared) {
|
|
932
|
-
return shared ? SHARED_DOCKER_CACHE_VOLUME : `agentbox-docker-${boxId}`;
|
|
933
|
-
}
|
|
934
|
-
async function launchDockerdDaemon(container, timeoutMs = 3e4) {
|
|
935
|
-
const result = await execInBox(container, ["/usr/local/bin/agentbox-dockerd-start"], {
|
|
936
|
-
user: "root",
|
|
937
|
-
detach: true
|
|
938
|
-
});
|
|
939
|
-
if (result.exitCode !== 0) {
|
|
940
|
-
return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };
|
|
941
|
-
}
|
|
942
|
-
const deadline = Date.now() + timeoutMs;
|
|
943
|
-
while (Date.now() < deadline) {
|
|
944
|
-
const probe = await execInBox(
|
|
945
|
-
container,
|
|
946
|
-
[
|
|
947
|
-
"bash",
|
|
948
|
-
"-lc",
|
|
949
|
-
"[ -S /var/run/docker.sock ] && docker -H unix:///var/run/docker.sock info >/dev/null 2>&1"
|
|
950
|
-
],
|
|
951
|
-
{ user: "root" }
|
|
952
|
-
);
|
|
953
|
-
if (probe.exitCode === 0) return { up: true };
|
|
954
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
955
|
-
}
|
|
956
|
-
return { up: false, reason: `dockerd did not become ready within ${String(timeoutMs)}ms` };
|
|
957
|
-
}
|
|
958
|
-
async function launchVncDaemon(container, timeoutMs = 5e3) {
|
|
959
|
-
const result = await execInBox(container, ["/usr/local/bin/agentbox-vnc-start"], {
|
|
960
|
-
user: "vscode",
|
|
961
|
-
detach: true
|
|
962
|
-
});
|
|
963
|
-
if (result.exitCode !== 0) {
|
|
964
|
-
return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };
|
|
965
|
-
}
|
|
966
|
-
const deadline = Date.now() + timeoutMs;
|
|
967
|
-
while (Date.now() < deadline) {
|
|
968
|
-
const probe = await execInBox(
|
|
969
|
-
container,
|
|
970
|
-
["bash", "-lc", "(echo > /dev/tcp/127.0.0.1/6080) 2>/dev/null"],
|
|
971
|
-
{ user: "vscode" }
|
|
972
|
-
);
|
|
973
|
-
if (probe.exitCode === 0) return { up: true };
|
|
974
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
975
|
-
}
|
|
976
|
-
return { up: false, reason: `websockify did not bind 6080 within ${String(timeoutMs)}ms` };
|
|
977
|
-
}
|
|
978
|
-
var VNC_PASSWORD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
979
|
-
function generateVncPassword() {
|
|
980
|
-
const bytes = randomBytes(8);
|
|
981
|
-
let out = "";
|
|
982
|
-
for (let i = 0; i < 8; i++) {
|
|
983
|
-
out += VNC_PASSWORD_ALPHABET[bytes[i] % VNC_PASSWORD_ALPHABET.length];
|
|
984
|
-
}
|
|
985
|
-
return out;
|
|
986
|
-
}
|
|
987
|
-
var VNC_CONTAINER_PORT = 6080;
|
|
988
|
-
function buildVncUrls(record, engine) {
|
|
989
|
-
if (!record.vncEnabled || !record.vncPassword) return {};
|
|
990
|
-
const containerPort = record.vncContainerPort ?? VNC_CONTAINER_PORT;
|
|
991
|
-
const qs = `autoconnect=1&password=${encodeURIComponent(record.vncPassword)}`;
|
|
992
|
-
const urls = {};
|
|
993
|
-
if (engine === "orbstack") {
|
|
994
|
-
urls.orbUrl = `http://${record.container}.orb.local:${String(containerPort)}/vnc.html?${qs}`;
|
|
995
|
-
}
|
|
996
|
-
if (record.vncHostPort) {
|
|
997
|
-
urls.loopbackUrl = `http://127.0.0.1:${String(record.vncHostPort)}/vnc.html?${qs}`;
|
|
998
|
-
}
|
|
999
|
-
return urls;
|
|
1000
|
-
}
|
|
1001
|
-
var WEB_CONTAINER_PORT = 80;
|
|
1002
|
-
async function detectGitRepos(workspace) {
|
|
1003
|
-
const out = [];
|
|
1004
|
-
if (await isGitDir(join2(workspace, ".git"))) {
|
|
1005
|
-
out.push({ kind: "root", hostMainRepo: workspace, relPathFromWorkspace: "" });
|
|
1006
|
-
}
|
|
1007
|
-
let entries;
|
|
1008
|
-
try {
|
|
1009
|
-
entries = await readdir2(workspace, { withFileTypes: true });
|
|
1010
|
-
} catch {
|
|
1011
|
-
return out;
|
|
1012
|
-
}
|
|
1013
|
-
for (const e of entries) {
|
|
1014
|
-
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
1015
|
-
const sub = join2(workspace, e.name);
|
|
1016
|
-
if (await isGitDir(join2(sub, ".git"))) {
|
|
1017
|
-
out.push({ kind: "nested", hostMainRepo: sub, relPathFromWorkspace: e.name });
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
return out;
|
|
1021
|
-
}
|
|
1022
|
-
async function isGitDir(path) {
|
|
1023
|
-
try {
|
|
1024
|
-
const s = await stat2(path);
|
|
1025
|
-
return s.isDirectory();
|
|
1026
|
-
} catch {
|
|
1027
|
-
return false;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
async function pickFreshBranch(hostMainRepo, base) {
|
|
1031
|
-
let candidate = base;
|
|
1032
|
-
let suffix = 2;
|
|
1033
|
-
while (await branchExists(hostMainRepo, candidate)) {
|
|
1034
|
-
candidate = `${base}-${String(suffix++)}`;
|
|
1035
|
-
if (suffix > 100) throw new GitWorktreeError(`could not find a free branch name near ${base}`);
|
|
1036
|
-
}
|
|
1037
|
-
return candidate;
|
|
1038
|
-
}
|
|
1039
|
-
async function branchExists(hostMainRepo, name) {
|
|
1040
|
-
const result = await execa2(
|
|
1041
|
-
"git",
|
|
1042
|
-
["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
|
|
1043
|
-
{ reject: false }
|
|
1044
|
-
);
|
|
1045
|
-
return result.exitCode === 0;
|
|
1046
|
-
}
|
|
1047
|
-
var GitWorktreeError = class extends Error {
|
|
1048
|
-
constructor(message) {
|
|
1049
|
-
super(message);
|
|
1050
|
-
this.name = "GitWorktreeError";
|
|
1051
|
-
}
|
|
1052
|
-
};
|
|
1053
|
-
var WORKTREE_ROOT = "/home/vscode/.agentbox-worktrees";
|
|
1054
|
-
function fsSafeBranch(branch) {
|
|
1055
|
-
return branch.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
1056
|
-
}
|
|
1057
|
-
function gitWorktreePathFor(branch) {
|
|
1058
|
-
return `${WORKTREE_ROOT}/${fsSafeBranch(branch)}`;
|
|
1059
|
-
}
|
|
1060
|
-
async function collectRepoCarryOver(repo, branch, containerPath, gitWorktreePath) {
|
|
1061
|
-
const stash = await execa3("git", ["-C", repo.hostMainRepo, "stash", "create"], { reject: false });
|
|
1062
|
-
const stashSha = stash.exitCode === 0 ? stash.stdout.trim() || null : null;
|
|
1063
|
-
const untracked = await execa3(
|
|
1064
|
-
"git",
|
|
1065
|
-
["-C", repo.hostMainRepo, "ls-files", "--others", "--exclude-standard", "-z"],
|
|
1066
|
-
{ reject: false }
|
|
1067
|
-
);
|
|
1068
|
-
const untrackedNul = untracked.exitCode === 0 ? untracked.stdout : "";
|
|
1069
|
-
return {
|
|
1070
|
-
repo,
|
|
1071
|
-
containerPath,
|
|
1072
|
-
gitWorktreePath,
|
|
1073
|
-
branch,
|
|
1074
|
-
stashSha,
|
|
1075
|
-
untrackedNul,
|
|
1076
|
-
hostSource: repo.hostMainRepo
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
async function dexec(container, argv, user = "vscode", cwd = "/") {
|
|
1080
|
-
const r = await execa3(
|
|
1081
|
-
"docker",
|
|
1082
|
-
["exec", "-w", cwd, "--user", user, container, ...argv],
|
|
1083
|
-
{ reject: false }
|
|
1084
|
-
);
|
|
1085
|
-
if (r.exitCode !== 0) {
|
|
1086
|
-
throw new GitWorktreeError(`${argv.join(" ")} failed: ${r.stderr || r.stdout}`);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
async function chownGitBindParents(args) {
|
|
1090
|
-
const log = args.onLog ?? (() => {
|
|
1091
|
-
});
|
|
1092
|
-
const repos = Array.from(new Set(args.hostMainRepos));
|
|
1093
|
-
for (const repo of repos) {
|
|
1094
|
-
const result = await execInBox(args.container, ["chown", "vscode:vscode", repo], {
|
|
1095
|
-
user: "root"
|
|
1096
|
-
});
|
|
1097
|
-
if (result.exitCode === 0) {
|
|
1098
|
-
log(`chowned ${repo} to vscode:vscode (parent of bind-mounted .git)`);
|
|
1099
|
-
} else {
|
|
1100
|
-
const msg = (result.stderr || result.stdout || `exit ${result.exitCode}`).trim();
|
|
1101
|
-
log(`chown ${repo} failed (best-effort, ignoring): ${msg}`);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
async function bindWorktrees(container, binds, onLog) {
|
|
1106
|
-
const log = onLog ?? (() => {
|
|
1107
|
-
});
|
|
1108
|
-
const ordered = [...binds].sort(
|
|
1109
|
-
(a, b) => a.kind === "root" && b.kind !== "root" ? -1 : a.kind !== "root" && b.kind === "root" ? 1 : 0
|
|
1110
|
-
);
|
|
1111
|
-
for (const b of ordered) {
|
|
1112
|
-
await execa3(
|
|
1113
|
-
"docker",
|
|
1114
|
-
["exec", "-w", "/", "--user", "root", container, "sh", "-c", `mountpoint -q ${b.containerPath} && umount ${b.containerPath} || true`],
|
|
1115
|
-
{ reject: false }
|
|
1116
|
-
);
|
|
1117
|
-
if (b.kind === "nested") {
|
|
1118
|
-
await dexec(container, ["mkdir", "-p", ctParent(b.containerPath)], "root");
|
|
1119
|
-
await dexec(container, ["mkdir", "-p", b.containerPath], "root");
|
|
1120
|
-
}
|
|
1121
|
-
await dexec(container, ["mount", "--bind", b.gitWorktreePath, b.containerPath], "root");
|
|
1122
|
-
log(`bind-mounted ${b.containerPath} <- ${b.gitWorktreePath}`);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
async function seedWorkspace(opts) {
|
|
1126
|
-
const log = opts.onLog ?? (() => {
|
|
1127
|
-
});
|
|
1128
|
-
await dexec(opts.container, ["mkdir", "-p", WORKTREE_ROOT]);
|
|
1129
|
-
for (const r of opts.repos) {
|
|
1130
|
-
const main = r.repo.hostMainRepo;
|
|
1131
|
-
const wt = r.gitWorktreePath;
|
|
1132
|
-
const add = await execa3(
|
|
1133
|
-
"docker",
|
|
1134
|
-
[
|
|
1135
|
-
"exec",
|
|
1136
|
-
"--user",
|
|
1137
|
-
"vscode",
|
|
1138
|
-
opts.container,
|
|
1139
|
-
"git",
|
|
1140
|
-
"-C",
|
|
1141
|
-
main,
|
|
1142
|
-
"worktree",
|
|
1143
|
-
"add",
|
|
1144
|
-
"-b",
|
|
1145
|
-
r.branch,
|
|
1146
|
-
wt,
|
|
1147
|
-
"HEAD"
|
|
1148
|
-
],
|
|
1149
|
-
{ reject: false }
|
|
1150
|
-
);
|
|
1151
|
-
if (add.exitCode !== 0) {
|
|
1152
|
-
throw new GitWorktreeError(
|
|
1153
|
-
`git worktree add ${wt} (branch ${r.branch}) failed: ${add.stderr || add.stdout}`
|
|
1154
|
-
);
|
|
1155
|
-
}
|
|
1156
|
-
log(`worktree ${wt} on branch ${r.branch} (host main ${main})`);
|
|
1157
|
-
await execa3(
|
|
1158
|
-
"docker",
|
|
1159
|
-
[
|
|
1160
|
-
"exec",
|
|
1161
|
-
"--user",
|
|
1162
|
-
"vscode",
|
|
1163
|
-
opts.container,
|
|
1164
|
-
"git",
|
|
1165
|
-
"-C",
|
|
1166
|
-
main,
|
|
1167
|
-
"config",
|
|
1168
|
-
"extensions.worktreeConfig",
|
|
1169
|
-
"true"
|
|
1170
|
-
],
|
|
1171
|
-
{ reject: false }
|
|
1172
|
-
);
|
|
1173
|
-
await execa3(
|
|
1174
|
-
"docker",
|
|
1175
|
-
[
|
|
1176
|
-
"exec",
|
|
1177
|
-
"--user",
|
|
1178
|
-
"vscode",
|
|
1179
|
-
opts.container,
|
|
1180
|
-
"git",
|
|
1181
|
-
"-C",
|
|
1182
|
-
wt,
|
|
1183
|
-
"config",
|
|
1184
|
-
"--worktree",
|
|
1185
|
-
"commit.gpgsign",
|
|
1186
|
-
"false"
|
|
1187
|
-
],
|
|
1188
|
-
{ reject: false }
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
await bindWorktrees(
|
|
1192
|
-
opts.container,
|
|
1193
|
-
opts.repos.map((r) => ({
|
|
1194
|
-
kind: r.repo.kind,
|
|
1195
|
-
containerPath: r.containerPath,
|
|
1196
|
-
gitWorktreePath: r.gitWorktreePath
|
|
1197
|
-
})),
|
|
1198
|
-
log
|
|
1199
|
-
);
|
|
1200
|
-
for (const r of opts.repos) {
|
|
1201
|
-
const ct = r.containerPath;
|
|
1202
|
-
if (r.stashSha) {
|
|
1203
|
-
const withIndex = await execa3(
|
|
1204
|
-
"docker",
|
|
1205
|
-
[
|
|
1206
|
-
"exec",
|
|
1207
|
-
"--user",
|
|
1208
|
-
"vscode",
|
|
1209
|
-
opts.container,
|
|
1210
|
-
"git",
|
|
1211
|
-
"-C",
|
|
1212
|
-
ct,
|
|
1213
|
-
"stash",
|
|
1214
|
-
"apply",
|
|
1215
|
-
"--index",
|
|
1216
|
-
r.stashSha
|
|
1217
|
-
],
|
|
1218
|
-
{ reject: false }
|
|
1219
|
-
);
|
|
1220
|
-
if (withIndex.exitCode !== 0) {
|
|
1221
|
-
const noIndex = await execa3(
|
|
1222
|
-
"docker",
|
|
1223
|
-
[
|
|
1224
|
-
"exec",
|
|
1225
|
-
"--user",
|
|
1226
|
-
"vscode",
|
|
1227
|
-
opts.container,
|
|
1228
|
-
"git",
|
|
1229
|
-
"-C",
|
|
1230
|
-
ct,
|
|
1231
|
-
"stash",
|
|
1232
|
-
"apply",
|
|
1233
|
-
r.stashSha
|
|
1234
|
-
],
|
|
1235
|
-
{ reject: false }
|
|
1236
|
-
);
|
|
1237
|
-
if (noIndex.exitCode !== 0) {
|
|
1238
|
-
log(
|
|
1239
|
-
`warning: stash apply failed in ${ct} (${withIndex.stderr || withIndex.stdout || "no message"})`
|
|
1240
|
-
);
|
|
1241
|
-
} else {
|
|
1242
|
-
log(`applied tracked changes (without index \u2014 staged state lost) in ${ct}`);
|
|
1243
|
-
}
|
|
1244
|
-
} else {
|
|
1245
|
-
log(`applied tracked changes from host main into ${ct}`);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
if (r.untrackedNul.length > 0) {
|
|
1249
|
-
const tarOut = await execa3("tar", ["-C", r.hostSource, "--null", "-T", "-", "-cf", "-"], {
|
|
1250
|
-
input: r.untrackedNul.replace(/\0$/, ""),
|
|
1251
|
-
encoding: "buffer",
|
|
1252
|
-
reject: false
|
|
1253
|
-
});
|
|
1254
|
-
if (tarOut.exitCode !== 0) {
|
|
1255
|
-
log(`warning: tar of untracked files for ${r.repo.hostMainRepo} failed: ${tarOut.stderr}`);
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1258
|
-
const tarIn = await execa3(
|
|
1259
|
-
"docker",
|
|
1260
|
-
["exec", "-i", "--user", "vscode", opts.container, "tar", "-C", ct, "-xf", "-"],
|
|
1261
|
-
{ input: tarOut.stdout, reject: false }
|
|
1262
|
-
);
|
|
1263
|
-
if (tarIn.exitCode !== 0) {
|
|
1264
|
-
log(`warning: untracked-file copy into ${ct} failed: ${tarIn.stderr}`);
|
|
1265
|
-
} else {
|
|
1266
|
-
const count = r.untrackedNul.split("\0").filter((s) => s.length > 0).length;
|
|
1267
|
-
log(`copied ${String(count)} untracked file(s) into ${ct}`);
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
async function seedWorkspaceFromDir(opts) {
|
|
1273
|
-
const log = opts.onLog ?? (() => {
|
|
1274
|
-
});
|
|
1275
|
-
const tarOut = await execa3("tar", ["-C", opts.hostSource, "-cf", "-", "."], {
|
|
1276
|
-
encoding: "buffer",
|
|
1277
|
-
reject: false
|
|
1278
|
-
});
|
|
1279
|
-
if (tarOut.exitCode !== 0) {
|
|
1280
|
-
throw new GitWorktreeError(`tar of ${opts.hostSource} failed: ${tarOut.stderr}`);
|
|
1281
|
-
}
|
|
1282
|
-
const tarIn = await execa3(
|
|
1283
|
-
"docker",
|
|
1284
|
-
["exec", "-i", "--user", "1000:1000", opts.container, "tar", "-C", "/workspace", "-xf", "-"],
|
|
1285
|
-
{ input: tarOut.stdout, reject: false }
|
|
1286
|
-
);
|
|
1287
|
-
if (tarIn.exitCode !== 0) {
|
|
1288
|
-
throw new GitWorktreeError(`tar extract into /workspace failed: ${tarIn.stderr}`);
|
|
1289
|
-
}
|
|
1290
|
-
log(`seeded /workspace from ${opts.hostSource}`);
|
|
1291
|
-
}
|
|
1292
|
-
async function removeInBoxWorktree(args) {
|
|
1293
|
-
const remove = await execa3(
|
|
1294
|
-
"git",
|
|
1295
|
-
["-C", args.hostMainRepo, "worktree", "remove", "--force", args.gitWorktreePath],
|
|
1296
|
-
{ reject: false }
|
|
1297
|
-
);
|
|
1298
|
-
if (remove.exitCode === 0) return;
|
|
1299
|
-
await execa3("git", ["-C", args.hostMainRepo, "worktree", "prune"], { reject: false });
|
|
1300
|
-
}
|
|
1301
|
-
function ctParent(p) {
|
|
1302
|
-
const i = p.lastIndexOf("/");
|
|
1303
|
-
return i <= 0 ? "/" : p.slice(0, i);
|
|
1304
|
-
}
|
|
1305
|
-
var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
1306
|
-
"node_modules",
|
|
1307
|
-
".next",
|
|
1308
|
-
".nuxt",
|
|
1309
|
-
".turbo",
|
|
1310
|
-
".svelte-kit",
|
|
1311
|
-
"dist",
|
|
1312
|
-
"build",
|
|
1313
|
-
"out",
|
|
1314
|
-
"target",
|
|
1315
|
-
".venv",
|
|
1316
|
-
"__pycache__",
|
|
1317
|
-
".cache",
|
|
1318
|
-
".parcel-cache"
|
|
1319
|
-
]);
|
|
1320
|
-
var SNAPSHOTS_ROOT = join3(homedir2(), ".agentbox", "snapshots");
|
|
1321
|
-
function snapshotPathFor(box) {
|
|
1322
|
-
const mnemonic = sanitizeMnemonic(box.name);
|
|
1323
|
-
const n = box.projectIndex;
|
|
1324
|
-
const segment = typeof n === "number" && Number.isFinite(n) && n > 0 ? `${box.id}-${String(n)}-${mnemonic}` : `${box.id}-${mnemonic}`;
|
|
1325
|
-
return join3(SNAPSHOTS_ROOT, segment);
|
|
1326
|
-
}
|
|
1327
|
-
async function findExcludedDirs(root, excluded = EXCLUDE_DIRS) {
|
|
1328
|
-
const matches = [];
|
|
1329
|
-
const walk = async (dir) => {
|
|
1330
|
-
let entries;
|
|
1331
|
-
try {
|
|
1332
|
-
entries = await readdir3(dir, { withFileTypes: true });
|
|
1333
|
-
} catch {
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
for (const entry of entries) {
|
|
1337
|
-
if (!entry.isDirectory()) continue;
|
|
1338
|
-
const abs = join3(dir, entry.name);
|
|
1339
|
-
if (excluded.has(entry.name)) {
|
|
1340
|
-
matches.push(abs);
|
|
1341
|
-
continue;
|
|
1342
|
-
}
|
|
1343
|
-
await walk(abs);
|
|
1344
|
-
}
|
|
1345
|
-
};
|
|
1346
|
-
await walk(root);
|
|
1347
|
-
return matches;
|
|
1348
|
-
}
|
|
1349
|
-
async function createSnapshot(opts) {
|
|
1350
|
-
const source = resolve(opts.source);
|
|
1351
|
-
const destination = resolve(opts.destination);
|
|
1352
|
-
const excluded = opts.excluded ?? EXCLUDE_DIRS;
|
|
1353
|
-
await mkdir2(SNAPSHOTS_ROOT, { recursive: true });
|
|
1354
|
-
const cpArgs = platform() === "darwin" ? ["-cR"] : ["-R"];
|
|
1355
|
-
await execa4("cp", [...cpArgs, `${source}/`, destination]);
|
|
1356
|
-
const toPrune = await findExcludedDirs(destination, excluded);
|
|
1357
|
-
await Promise.all(toPrune.map((p) => rm2(p, { recursive: true, force: true })));
|
|
1358
|
-
return { destination, prunedPaths: toPrune };
|
|
1359
|
-
}
|
|
1360
|
-
async function launchCtlDaemon(container, hostSocketPath, timeoutMs = 3e3) {
|
|
1361
|
-
const result = await execInBox(container, ["agentbox-ctl", "daemon"], {
|
|
1362
|
-
user: "vscode",
|
|
1363
|
-
detach: true
|
|
1364
|
-
});
|
|
1365
|
-
if (result.exitCode !== 0) {
|
|
1366
|
-
return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };
|
|
1367
|
-
}
|
|
1368
|
-
const deadline = Date.now() + timeoutMs;
|
|
1369
|
-
while (Date.now() < deadline) {
|
|
1370
|
-
if (await pathExists2(hostSocketPath)) return { up: true };
|
|
1371
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
1372
|
-
}
|
|
1373
|
-
return {
|
|
1374
|
-
up: false,
|
|
1375
|
-
reason: `socket ${hostSocketPath} did not appear within ${String(timeoutMs)}ms`
|
|
1376
|
-
};
|
|
1377
|
-
}
|
|
1378
|
-
async function pathExists2(p) {
|
|
1379
|
-
try {
|
|
1380
|
-
await stat4(p);
|
|
1381
|
-
return true;
|
|
1382
|
-
} catch {
|
|
1383
|
-
return false;
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
var STATE_DIR = join4(homedir3(), ".agentbox");
|
|
1387
|
-
var PID_FILE = join4(STATE_DIR, "relay.pid");
|
|
1388
|
-
var LOG_FILE = join4(STATE_DIR, "relay.log");
|
|
1389
|
-
var PORT = DEFAULT_RELAY_PORT;
|
|
1390
|
-
var ENDPOINT = {
|
|
1391
|
-
// host.docker.internal is the Docker Desktop / OrbStack-supplied alias for
|
|
1392
|
-
// the host's loopback as seen from inside a container. The corresponding
|
|
1393
|
-
// `--add-host=host.docker.internal:host-gateway` flag in runBox makes the
|
|
1394
|
-
// resolution work on Linux native Docker too.
|
|
1395
|
-
url: `http://host.docker.internal:${String(PORT)}`,
|
|
1396
|
-
hostUrl: `http://127.0.0.1:${String(PORT)}`,
|
|
1397
|
-
port: PORT
|
|
1398
|
-
};
|
|
1399
|
-
async function ensureRelay(opts = {}) {
|
|
1400
|
-
const log = opts.onLog ?? (() => {
|
|
1401
|
-
});
|
|
1402
|
-
await mkdir3(STATE_DIR, { recursive: true });
|
|
1403
|
-
if (await containerExists(RELAY_CONTAINER_NAME)) {
|
|
1404
|
-
await removeContainer(RELAY_CONTAINER_NAME);
|
|
1405
|
-
log(`removed legacy relay container ${RELAY_CONTAINER_NAME}`);
|
|
1406
|
-
}
|
|
1407
|
-
if (await pingHealthz(500)) {
|
|
1408
|
-
return ENDPOINT;
|
|
1409
|
-
}
|
|
1410
|
-
const existingPid = await readPidFile();
|
|
1411
|
-
if (existingPid !== null && await processAlive(existingPid)) {
|
|
1412
|
-
for (let i = 0; i < 10; i++) {
|
|
1413
|
-
if (await pingHealthz(300)) return ENDPOINT;
|
|
1414
|
-
await delay(200);
|
|
1415
|
-
}
|
|
1416
|
-
log(`relay pid ${String(existingPid)} alive but /healthz unresponsive \u2014 proceeding anyway`);
|
|
1417
|
-
return ENDPOINT;
|
|
1418
|
-
}
|
|
1419
|
-
if (existingPid !== null) {
|
|
1420
|
-
await unlink(PID_FILE).catch(() => {
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
const relayBin = resolveRelayBin();
|
|
1424
|
-
const logFd = openSync(LOG_FILE, "a");
|
|
1425
|
-
const cliEntry = resolveCliEntry();
|
|
1426
|
-
const child = spawn(
|
|
1427
|
-
process.execPath,
|
|
1428
|
-
[relayBin, "serve", "--port", String(PORT), "--host", "0.0.0.0"],
|
|
1429
|
-
{
|
|
1430
|
-
detached: true,
|
|
1431
|
-
stdio: ["ignore", logFd, logFd],
|
|
1432
|
-
env: {
|
|
1433
|
-
...process.env,
|
|
1434
|
-
...cliEntry ? { AGENTBOX_CLI_ENTRY: cliEntry } : {}
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
);
|
|
1438
|
-
child.unref();
|
|
1439
|
-
if (typeof child.pid === "number") {
|
|
1440
|
-
await writeFile2(PID_FILE, String(child.pid), "utf8");
|
|
1441
|
-
log(`spawned relay host process (pid ${String(child.pid)}, port ${String(PORT)})`);
|
|
1442
|
-
}
|
|
1443
|
-
for (let i = 0; i < 25; i++) {
|
|
1444
|
-
if (await pingHealthz(300)) {
|
|
1445
|
-
log(`relay reachable on ${ENDPOINT.hostUrl}`);
|
|
1446
|
-
return ENDPOINT;
|
|
1447
|
-
}
|
|
1448
|
-
await delay(200);
|
|
1449
|
-
}
|
|
1450
|
-
throw new Error(
|
|
1451
|
-
`relay did not become reachable on ${ENDPOINT.hostUrl} within 5s; see ${LOG_FILE}`
|
|
1452
|
-
);
|
|
1453
|
-
}
|
|
1454
|
-
function resolveRelayBin() {
|
|
1455
|
-
const override = process.env.AGENTBOX_RELAY_BIN;
|
|
1456
|
-
if (override && existsSync(override)) return override;
|
|
1457
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
1458
|
-
const candidates = [
|
|
1459
|
-
resolve2(here, "..", "runtime", "relay", "bin.cjs"),
|
|
1460
|
-
resolve2(here, "..", "..", "relay", "dist", "bin.cjs"),
|
|
1461
|
-
resolve2(here, "..", "..", "..", "@agentbox", "relay", "dist", "bin.cjs"),
|
|
1462
|
-
resolve2(here, "..", "..", "node_modules", "@agentbox", "relay", "dist", "bin.cjs")
|
|
1463
|
-
];
|
|
1464
|
-
for (const c of candidates) {
|
|
1465
|
-
if (existsSync(c)) return c;
|
|
1466
|
-
}
|
|
1467
|
-
throw new Error(
|
|
1468
|
-
`could not locate @agentbox/relay bin; tried:
|
|
1469
|
-
${candidates.join("\n ")}`
|
|
1470
|
-
);
|
|
1471
|
-
}
|
|
1472
|
-
function resolveCliEntry() {
|
|
1473
|
-
const override = process.env.AGENTBOX_CLI_ENTRY;
|
|
1474
|
-
if (override && existsSync(override)) return override;
|
|
1475
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
1476
|
-
const candidates = [
|
|
1477
|
-
// Bundled CLI (dev + published): this module IS bundled into the CLI
|
|
1478
|
-
// entry, so the entry is index.js next to this file.
|
|
1479
|
-
resolve2(here, "index.js"),
|
|
1480
|
-
resolve2(here, "..", "..", "..", "apps", "cli", "dist", "index.js"),
|
|
1481
|
-
resolve2(here, "..", "..", "..", "..", "dist", "index.js")
|
|
1482
|
-
];
|
|
1483
|
-
for (const c of candidates) {
|
|
1484
|
-
if (existsSync(c)) return c;
|
|
1485
|
-
}
|
|
1486
|
-
return null;
|
|
1487
|
-
}
|
|
1488
|
-
async function stopRelay() {
|
|
1489
|
-
const pid = await readPidFile();
|
|
1490
|
-
if (pid === null) {
|
|
1491
|
-
return { stopped: false, pid: null };
|
|
1492
|
-
}
|
|
1493
|
-
if (!await processAlive(pid)) {
|
|
1494
|
-
await unlink(PID_FILE).catch(() => {
|
|
1495
|
-
});
|
|
1496
|
-
return { stopped: false, pid };
|
|
1497
|
-
}
|
|
1498
|
-
try {
|
|
1499
|
-
process.kill(pid, "SIGTERM");
|
|
1500
|
-
} catch {
|
|
1501
|
-
}
|
|
1502
|
-
for (let i = 0; i < 20; i++) {
|
|
1503
|
-
if (!await processAlive(pid)) break;
|
|
1504
|
-
await delay(100);
|
|
1505
|
-
}
|
|
1506
|
-
if (await processAlive(pid)) {
|
|
1507
|
-
try {
|
|
1508
|
-
process.kill(pid, "SIGKILL");
|
|
1509
|
-
} catch {
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
await unlink(PID_FILE).catch(() => {
|
|
1513
|
-
});
|
|
1514
|
-
return { stopped: true, pid };
|
|
1515
|
-
}
|
|
1516
|
-
async function getRelayStatus() {
|
|
1517
|
-
const pid = await readPidFile();
|
|
1518
|
-
const pidAlive = pid !== null && await processAlive(pid);
|
|
1519
|
-
const health = await fetchHealthz(300);
|
|
1520
|
-
return {
|
|
1521
|
-
running: health !== null,
|
|
1522
|
-
pid,
|
|
1523
|
-
pidAlive,
|
|
1524
|
-
port: PORT,
|
|
1525
|
-
endpoint: ENDPOINT,
|
|
1526
|
-
health: health === null ? null : { boxes: health.boxes, events: health.events },
|
|
1527
|
-
pidFile: PID_FILE,
|
|
1528
|
-
logFile: LOG_FILE
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
1531
|
-
function pingHealthz(timeoutMs) {
|
|
1532
|
-
return new Promise((resolveP) => {
|
|
1533
|
-
const req = httpRequest(
|
|
1534
|
-
{ host: "127.0.0.1", port: PORT, method: "GET", path: "/healthz", timeout: timeoutMs },
|
|
1535
|
-
(res) => {
|
|
1536
|
-
res.resume();
|
|
1537
|
-
const status = res.statusCode ?? 0;
|
|
1538
|
-
resolveP(status >= 200 && status < 300);
|
|
1539
|
-
}
|
|
1540
|
-
);
|
|
1541
|
-
req.on("error", () => resolveP(false));
|
|
1542
|
-
req.on("timeout", () => {
|
|
1543
|
-
req.destroy();
|
|
1544
|
-
resolveP(false);
|
|
1545
|
-
});
|
|
1546
|
-
req.end();
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
function fetchHealthz(timeoutMs) {
|
|
1550
|
-
return new Promise((resolveP) => {
|
|
1551
|
-
const req = httpRequest(
|
|
1552
|
-
{ host: "127.0.0.1", port: PORT, method: "GET", path: "/healthz", timeout: timeoutMs },
|
|
1553
|
-
(res) => {
|
|
1554
|
-
const status = res.statusCode ?? 0;
|
|
1555
|
-
if (status < 200 || status >= 300) {
|
|
1556
|
-
res.resume();
|
|
1557
|
-
resolveP(null);
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
const chunks = [];
|
|
1561
|
-
res.on("data", (c) => chunks.push(c));
|
|
1562
|
-
res.on("end", () => {
|
|
1563
|
-
try {
|
|
1564
|
-
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1565
|
-
if (typeof parsed.ok === "boolean" && typeof parsed.boxes === "number" && typeof parsed.events === "number") {
|
|
1566
|
-
resolveP({ ok: parsed.ok, boxes: parsed.boxes, events: parsed.events });
|
|
1567
|
-
} else {
|
|
1568
|
-
resolveP(null);
|
|
1569
|
-
}
|
|
1570
|
-
} catch {
|
|
1571
|
-
resolveP(null);
|
|
1572
|
-
}
|
|
1573
|
-
});
|
|
1574
|
-
res.on("error", () => resolveP(null));
|
|
1575
|
-
}
|
|
1576
|
-
);
|
|
1577
|
-
req.on("error", () => resolveP(null));
|
|
1578
|
-
req.on("timeout", () => {
|
|
1579
|
-
req.destroy();
|
|
1580
|
-
resolveP(null);
|
|
1581
|
-
});
|
|
1582
|
-
req.end();
|
|
1583
|
-
});
|
|
1584
|
-
}
|
|
1585
|
-
async function readPidFile() {
|
|
1586
|
-
try {
|
|
1587
|
-
const text = await readFile2(PID_FILE, "utf8");
|
|
1588
|
-
const pid = Number.parseInt(text.trim(), 10);
|
|
1589
|
-
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
1590
|
-
} catch {
|
|
1591
|
-
return null;
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
async function processAlive(pid) {
|
|
1595
|
-
try {
|
|
1596
|
-
process.kill(pid, 0);
|
|
1597
|
-
return true;
|
|
1598
|
-
} catch {
|
|
1599
|
-
return false;
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
function generateRelayToken() {
|
|
1603
|
-
return randomBytes2(32).toString("hex");
|
|
1604
|
-
}
|
|
1605
|
-
async function registerBoxWithRelay(args) {
|
|
1606
|
-
const worktrees = (args.worktrees ?? []).map((w) => ({
|
|
1607
|
-
containerPath: w.containerPath,
|
|
1608
|
-
hostMainRepo: w.hostMainRepo,
|
|
1609
|
-
branch: w.branch
|
|
1610
|
-
}));
|
|
1611
|
-
await adminPost("/admin/register-box", {
|
|
1612
|
-
boxId: args.boxId,
|
|
1613
|
-
token: args.token,
|
|
1614
|
-
name: args.name,
|
|
1615
|
-
containerName: args.containerName,
|
|
1616
|
-
createdAt: args.createdAt,
|
|
1617
|
-
projectIndex: args.projectIndex,
|
|
1618
|
-
worktrees
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
async function forgetBoxFromRelay(boxId) {
|
|
1622
|
-
try {
|
|
1623
|
-
await adminPost("/admin/forget-box", { boxId });
|
|
1624
|
-
} catch {
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
async function adminPost(path, body) {
|
|
1628
|
-
const json = JSON.stringify(body);
|
|
1629
|
-
await new Promise((resolveP, rejectP) => {
|
|
1630
|
-
const req = httpRequest(
|
|
1631
|
-
{
|
|
1632
|
-
host: "127.0.0.1",
|
|
1633
|
-
port: PORT,
|
|
1634
|
-
method: "POST",
|
|
1635
|
-
path,
|
|
1636
|
-
headers: {
|
|
1637
|
-
"Content-Type": "application/json",
|
|
1638
|
-
"Content-Length": Buffer.byteLength(json).toString()
|
|
1639
|
-
},
|
|
1640
|
-
timeout: 3e3
|
|
1641
|
-
},
|
|
1642
|
-
(res) => {
|
|
1643
|
-
const chunks = [];
|
|
1644
|
-
res.on("data", (c) => chunks.push(c));
|
|
1645
|
-
res.on("end", () => {
|
|
1646
|
-
const status = res.statusCode ?? 0;
|
|
1647
|
-
if (status >= 200 && status < 300) {
|
|
1648
|
-
resolveP();
|
|
1649
|
-
} else {
|
|
1650
|
-
const text = Buffer.concat(chunks).toString("utf8");
|
|
1651
|
-
rejectP(new Error(`relay ${path} \u2192 ${String(status)}: ${text}`));
|
|
1652
|
-
}
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
);
|
|
1656
|
-
req.on("error", rejectP);
|
|
1657
|
-
req.on("timeout", () => {
|
|
1658
|
-
req.destroy();
|
|
1659
|
-
rejectP(new Error(`relay ${path} timeout`));
|
|
1660
|
-
});
|
|
1661
|
-
req.write(json);
|
|
1662
|
-
req.end();
|
|
1663
|
-
});
|
|
1664
|
-
}
|
|
1665
|
-
async function rehydrateRelayRegistry(boxes) {
|
|
1666
|
-
for (const b of boxes) {
|
|
1667
|
-
if (!b.relayToken) continue;
|
|
1668
|
-
try {
|
|
1669
|
-
await registerBoxWithRelay({
|
|
1670
|
-
boxId: b.id,
|
|
1671
|
-
token: b.relayToken,
|
|
1672
|
-
name: b.name,
|
|
1673
|
-
containerName: b.container,
|
|
1674
|
-
createdAt: b.createdAt,
|
|
1675
|
-
projectIndex: b.projectIndex,
|
|
1676
|
-
worktrees: b.gitWorktrees
|
|
1677
|
-
});
|
|
1678
|
-
} catch {
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
var PROFILES = {
|
|
1683
|
-
vscode: {
|
|
1684
|
-
serverDir: "/home/vscode/.vscode-server",
|
|
1685
|
-
extensionsDir: "/home/vscode/.vscode-server/extensions",
|
|
1686
|
-
perBoxVolumePrefix: "agentbox-vscode-server-",
|
|
1687
|
-
sharedExtensionsVolume: "agentbox-vscode-extensions",
|
|
1688
|
-
cli: "code",
|
|
1689
|
-
displayName: "VS Code",
|
|
1690
|
-
protocolScheme: "vscode"
|
|
1691
|
-
},
|
|
1692
|
-
cursor: {
|
|
1693
|
-
serverDir: "/home/vscode/.cursor-server",
|
|
1694
|
-
extensionsDir: "/home/vscode/.cursor-server/extensions",
|
|
1695
|
-
perBoxVolumePrefix: "agentbox-cursor-server-",
|
|
1696
|
-
sharedExtensionsVolume: "agentbox-cursor-extensions",
|
|
1697
|
-
cli: "cursor",
|
|
1698
|
-
displayName: "Cursor",
|
|
1699
|
-
protocolScheme: "cursor"
|
|
1700
|
-
}
|
|
1701
|
-
};
|
|
1702
|
-
var IDE_FLAVORS = ["vscode", "cursor"];
|
|
1703
|
-
function ideProfile(flavor) {
|
|
1704
|
-
return PROFILES[flavor];
|
|
1705
|
-
}
|
|
1706
|
-
var SHARED_VSCODE_EXTENSIONS_VOLUME = PROFILES.vscode.sharedExtensionsVolume;
|
|
1707
|
-
var SHARED_CURSOR_EXTENSIONS_VOLUME = PROFILES.cursor.sharedExtensionsVolume;
|
|
1708
|
-
function vscodeServerVolumeName(boxId) {
|
|
1709
|
-
return ideServerVolumeName("vscode", boxId);
|
|
1710
|
-
}
|
|
1711
|
-
function cursorServerVolumeName(boxId) {
|
|
1712
|
-
return ideServerVolumeName("cursor", boxId);
|
|
1713
|
-
}
|
|
1714
|
-
function ideServerVolumeName(flavor, boxId) {
|
|
1715
|
-
return `${PROFILES[flavor].perBoxVolumePrefix}${boxId}`;
|
|
1716
|
-
}
|
|
1717
|
-
function buildFlavorMounts(flavor, boxId) {
|
|
1718
|
-
const profile = PROFILES[flavor];
|
|
1719
|
-
const perBox = ideServerVolumeName(flavor, boxId);
|
|
1720
|
-
return {
|
|
1721
|
-
volumes: [perBox, profile.sharedExtensionsVolume],
|
|
1722
|
-
extraVolumes: [
|
|
1723
|
-
`${perBox}:${profile.serverDir}`,
|
|
1724
|
-
`${profile.sharedExtensionsVolume}:${profile.extensionsDir}`
|
|
1725
|
-
]
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1728
|
-
function buildIdeMounts(boxId) {
|
|
1729
|
-
const merged = { volumes: [], extraVolumes: [] };
|
|
1730
|
-
for (const f of IDE_FLAVORS) {
|
|
1731
|
-
const m = buildFlavorMounts(f, boxId);
|
|
1732
|
-
merged.volumes.push(...m.volumes);
|
|
1733
|
-
merged.extraVolumes.push(...m.extraVolumes);
|
|
1734
|
-
}
|
|
1735
|
-
return merged;
|
|
1736
|
-
}
|
|
1737
|
-
async function ensureIdeVolumes(boxId) {
|
|
1738
|
-
for (const v of buildIdeMounts(boxId).volumes) await ensureVolume(v);
|
|
1739
|
-
}
|
|
1740
|
-
async function repairIdeOwnership(container) {
|
|
1741
|
-
for (const flavor of IDE_FLAVORS) {
|
|
1742
|
-
await execInBox(container, ["chown", "-R", "vscode:vscode", PROFILES[flavor].serverDir], {
|
|
1743
|
-
user: "root"
|
|
1744
|
-
});
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
function containerHex(containerName) {
|
|
1748
|
-
return Buffer.from(containerName, "utf8").toString("hex");
|
|
1749
|
-
}
|
|
1750
|
-
var SENTINEL = "// agentbox-managed: regenerated on `agentbox code`; remove this header to take ownership";
|
|
1751
|
-
async function ensureAgentboxTasksFile(container, services, opts = {}) {
|
|
1752
|
-
if (services.length === 0) return { status: "skipped-no-services" };
|
|
1753
|
-
const existing = await execInBox(container, ["cat", "/workspace/.vscode/tasks.json"], {
|
|
1754
|
-
user: "vscode"
|
|
1755
|
-
});
|
|
1756
|
-
if (existing.exitCode === 0 && !opts.regen && !existing.stdout.includes(SENTINEL)) {
|
|
1757
|
-
return { status: "skipped-user-owned" };
|
|
1758
|
-
}
|
|
1759
|
-
const tasks = services.map((s) => ({
|
|
1760
|
-
label: `agentbox: ${s.name}`,
|
|
1761
|
-
type: "shell",
|
|
1762
|
-
command: `tail -F /var/log/agentbox/${s.name}.log`,
|
|
1763
|
-
isBackground: true,
|
|
1764
|
-
presentation: { panel: "dedicated", reveal: "always", echo: false },
|
|
1765
|
-
runOptions: { runOn: "folderOpen" },
|
|
1766
|
-
problemMatcher: []
|
|
1767
|
-
}));
|
|
1768
|
-
const body = `${SENTINEL}
|
|
1769
|
-
` + JSON.stringify(
|
|
1770
|
-
{
|
|
1771
|
-
version: "2.0.0",
|
|
1772
|
-
tasks
|
|
1773
|
-
},
|
|
1774
|
-
null,
|
|
1775
|
-
2
|
|
1776
|
-
) + "\n";
|
|
1777
|
-
await execInBox(container, ["mkdir", "-p", "/workspace/.vscode"], { user: "vscode" });
|
|
1778
|
-
const write = await writeFileInBox(container, "/workspace/.vscode/tasks.json", body);
|
|
1779
|
-
if (write.exitCode !== 0) {
|
|
1780
|
-
throw new Error(`failed to write tasks.json in ${container}: ${write.stderr || write.stdout}`);
|
|
1781
|
-
}
|
|
1782
|
-
return { status: "wrote" };
|
|
1783
|
-
}
|
|
1784
|
-
async function writeFileInBox(container, path, content) {
|
|
1785
|
-
const { execa: execa5 } = await import("execa");
|
|
1786
|
-
const result = await execa5(
|
|
1787
|
-
"docker",
|
|
1788
|
-
["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote(path)}`],
|
|
1789
|
-
{ input: content, reject: false }
|
|
1790
|
-
);
|
|
1791
|
-
return {
|
|
1792
|
-
exitCode: result.exitCode ?? -1,
|
|
1793
|
-
stdout: result.stdout ?? "",
|
|
1794
|
-
stderr: result.stderr ?? ""
|
|
1795
|
-
};
|
|
1796
|
-
}
|
|
1797
|
-
function shellQuote(s) {
|
|
1798
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
// ../../packages/ctl/dist/index.js
|
|
1802
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
1803
|
-
import { parse as parseYaml } from "yaml";
|
|
1804
|
-
function renderStatusTable(rows) {
|
|
1805
|
-
if (rows.length === 0) return "(no services configured)";
|
|
1806
|
-
const headers = ["NAME", "STATE", "PID", "RESTARTS", "LAST EXIT", "BLOCKED ON", "COMMAND"];
|
|
1807
|
-
const data = rows.map((r) => [
|
|
1808
|
-
r.name,
|
|
1809
|
-
r.state,
|
|
1810
|
-
r.pid === null ? "-" : String(r.pid),
|
|
1811
|
-
String(r.restarts),
|
|
1812
|
-
r.lastExitCode === null ? "-" : String(r.lastExitCode),
|
|
1813
|
-
r.blockedOn.length === 0 ? "-" : r.blockedOn.join(","),
|
|
1814
|
-
truncate(r.command, 40)
|
|
1815
|
-
]);
|
|
1816
|
-
return renderTable(headers, data);
|
|
1817
|
-
}
|
|
1818
|
-
function renderTaskTable(rows) {
|
|
1819
|
-
if (rows.length === 0) return "(no tasks configured)";
|
|
1820
|
-
const headers = ["NAME", "STATE", "EXIT", "STARTED", "FINISHED", "COMMAND"];
|
|
1821
|
-
const data = rows.map((r) => [
|
|
1822
|
-
r.name,
|
|
1823
|
-
r.state,
|
|
1824
|
-
r.lastExitCode === null ? "-" : String(r.lastExitCode),
|
|
1825
|
-
r.startedAt ?? "-",
|
|
1826
|
-
r.finishedAt ?? "-",
|
|
1827
|
-
truncate(r.command, 40)
|
|
1828
|
-
]);
|
|
1829
|
-
return renderTable(headers, data);
|
|
1830
|
-
}
|
|
1831
|
-
function renderPortsTable(rows) {
|
|
1832
|
-
if (rows.length === 0) return "(no ports listening)";
|
|
1833
|
-
const named = rows.filter((r) => r.service);
|
|
1834
|
-
const other = rows.filter((r) => !r.service).map((r) => r.port).sort((a, b) => a - b);
|
|
1835
|
-
const lines = [];
|
|
1836
|
-
if (named.length > 0) {
|
|
1837
|
-
lines.push(
|
|
1838
|
-
renderTable(
|
|
1839
|
-
["PORT", "SERVICE"],
|
|
1840
|
-
named.map((r) => [`:${String(r.port)}`, r.service ?? "-"])
|
|
1841
|
-
)
|
|
1842
|
-
);
|
|
1843
|
-
}
|
|
1844
|
-
if (other.length > 0) {
|
|
1845
|
-
lines.push(`other (${other.length}): ${other.join(", ")}`);
|
|
1846
|
-
}
|
|
1847
|
-
return lines.join("\n");
|
|
1848
|
-
}
|
|
1849
|
-
function renderTable(headers, data) {
|
|
1850
|
-
const widths = headers.map(
|
|
1851
|
-
(h, i) => Math.max(h.length, ...data.map((row) => (row[i] ?? "").length))
|
|
1852
|
-
);
|
|
1853
|
-
const fmt = (row) => row.map((cell, i) => cell.padEnd(widths[i] ?? cell.length)).join(" ");
|
|
1854
|
-
return [fmt(headers), ...data.map(fmt)].join("\n");
|
|
1855
|
-
}
|
|
1856
|
-
function truncate(s, n) {
|
|
1857
|
-
if (s.length <= n) return s;
|
|
1858
|
-
return s.slice(0, n - 1) + "\u2026";
|
|
1859
|
-
}
|
|
1860
|
-
var DEFAULT_BACKOFF = {
|
|
1861
|
-
initialMs: 500,
|
|
1862
|
-
maxMs: 3e4,
|
|
1863
|
-
factor: 2
|
|
1864
|
-
};
|
|
1865
|
-
var DEFAULT_PROBE_INTERVAL_MS = 500;
|
|
1866
|
-
var DEFAULT_PROBE_INITIAL_DELAY_MS = 0;
|
|
1867
|
-
var DEFAULT_PROBE_TIMEOUT_MS = 6e4;
|
|
1868
|
-
var DEFAULT_PROBE_HOST = "127.0.0.1";
|
|
1869
|
-
var DEFAULT_PROBE_ON_TIMEOUT = "kill";
|
|
1870
|
-
var ConfigError = class extends Error {
|
|
1871
|
-
constructor(message) {
|
|
1872
|
-
super(message);
|
|
1873
|
-
this.name = "ConfigError";
|
|
1874
|
-
}
|
|
1875
|
-
};
|
|
1876
|
-
function isPlainObject2(v) {
|
|
1877
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1878
|
-
}
|
|
1879
|
-
function parseEnv(raw, where) {
|
|
1880
|
-
if (raw === void 0 || raw === null) return void 0;
|
|
1881
|
-
if (!isPlainObject2(raw)) {
|
|
1882
|
-
throw new ConfigError(`${where}.env must be a mapping of string \u2192 string`);
|
|
1883
|
-
}
|
|
1884
|
-
const out = {};
|
|
1885
|
-
for (const [k, v] of Object.entries(raw)) {
|
|
1886
|
-
if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean") {
|
|
1887
|
-
throw new ConfigError(`${where}.env.${k} must be a scalar`);
|
|
1888
|
-
}
|
|
1889
|
-
out[k] = String(v);
|
|
1890
|
-
}
|
|
1891
|
-
return out;
|
|
1892
|
-
}
|
|
1893
|
-
function parseCommand(raw, where) {
|
|
1894
|
-
if (typeof raw === "string") {
|
|
1895
|
-
if (raw.trim().length === 0) {
|
|
1896
|
-
throw new ConfigError(`${where}.command must not be empty`);
|
|
1897
|
-
}
|
|
1898
|
-
return raw;
|
|
1899
|
-
}
|
|
1900
|
-
if (Array.isArray(raw)) {
|
|
1901
|
-
if (raw.length === 0) {
|
|
1902
|
-
throw new ConfigError(`${where}.command array must not be empty`);
|
|
1903
|
-
}
|
|
1904
|
-
const argv = [];
|
|
1905
|
-
for (const [i, item] of raw.entries()) {
|
|
1906
|
-
if (typeof item !== "string") {
|
|
1907
|
-
throw new ConfigError(`${where}.command[${String(i)}] must be a string`);
|
|
1908
|
-
}
|
|
1909
|
-
argv.push(item);
|
|
1910
|
-
}
|
|
1911
|
-
return argv;
|
|
1912
|
-
}
|
|
1913
|
-
throw new ConfigError(`${where}.command must be a string or array of strings`);
|
|
1914
|
-
}
|
|
1915
|
-
function parseRestart(raw, where) {
|
|
1916
|
-
if (raw === void 0) return "on-failure";
|
|
1917
|
-
if (raw === "always" || raw === "on-failure" || raw === "never") return raw;
|
|
1918
|
-
throw new ConfigError(`${where}.restart must be one of: always, on-failure, never`);
|
|
1919
|
-
}
|
|
1920
|
-
var BACKOFF_KEYS = /* @__PURE__ */ new Set(["initial_ms", "max_ms", "factor"]);
|
|
1921
|
-
function parseBackoff(raw, where) {
|
|
1922
|
-
if (raw === void 0) return { ...DEFAULT_BACKOFF };
|
|
1923
|
-
if (!isPlainObject2(raw)) {
|
|
1924
|
-
throw new ConfigError(`${where}.backoff must be a mapping`);
|
|
1925
|
-
}
|
|
1926
|
-
rejectUnknownKeys(raw, BACKOFF_KEYS, `${where}.backoff`);
|
|
1927
|
-
const initialMs = parseNonNegativeInt(
|
|
1928
|
-
raw.initial_ms,
|
|
1929
|
-
`${where}.backoff.initial_ms`,
|
|
1930
|
-
DEFAULT_BACKOFF.initialMs
|
|
1931
|
-
);
|
|
1932
|
-
const maxMs = parseNonNegativeInt(raw.max_ms, `${where}.backoff.max_ms`, DEFAULT_BACKOFF.maxMs);
|
|
1933
|
-
const factor = parseFactor(raw.factor, `${where}.backoff.factor`, DEFAULT_BACKOFF.factor);
|
|
1934
|
-
if (maxMs < initialMs) {
|
|
1935
|
-
throw new ConfigError(`${where}.backoff.max_ms must be >= initial_ms`);
|
|
1936
|
-
}
|
|
1937
|
-
return { initialMs, maxMs, factor };
|
|
1938
|
-
}
|
|
1939
|
-
function rejectUnknownKeys(obj, allowed, where) {
|
|
1940
|
-
for (const key of Object.keys(obj)) {
|
|
1941
|
-
if (!allowed.has(key)) {
|
|
1942
|
-
throw new ConfigError(`${where} has unknown key "${key}"`);
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
function parseNonNegativeInt(raw, where, fallback) {
|
|
1947
|
-
if (raw === void 0) return fallback;
|
|
1948
|
-
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) {
|
|
1949
|
-
throw new ConfigError(`${where} must be a non-negative number`);
|
|
1950
|
-
}
|
|
1951
|
-
return Math.floor(raw);
|
|
1952
|
-
}
|
|
1953
|
-
function parsePositiveInt(raw, where, fallback) {
|
|
1954
|
-
if (raw === void 0) return fallback;
|
|
1955
|
-
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1) {
|
|
1956
|
-
throw new ConfigError(`${where} must be a positive integer`);
|
|
1957
|
-
}
|
|
1958
|
-
return Math.floor(raw);
|
|
1959
|
-
}
|
|
1960
|
-
function parseFactor(raw, where, fallback) {
|
|
1961
|
-
if (raw === void 0) return fallback;
|
|
1962
|
-
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1) {
|
|
1963
|
-
throw new ConfigError(`${where} must be a number >= 1`);
|
|
1964
|
-
}
|
|
1965
|
-
return raw;
|
|
1966
|
-
}
|
|
1967
|
-
function parseOnTimeout(raw, where) {
|
|
1968
|
-
if (raw === void 0) return DEFAULT_PROBE_ON_TIMEOUT;
|
|
1969
|
-
if (raw === "kill" || raw === "mark_unhealthy") return raw;
|
|
1970
|
-
throw new ConfigError(`${where} must be one of: kill, mark_unhealthy`);
|
|
1971
|
-
}
|
|
1972
|
-
function parseNeeds(raw, where) {
|
|
1973
|
-
if (raw === void 0 || raw === null) return [];
|
|
1974
|
-
if (!Array.isArray(raw)) {
|
|
1975
|
-
throw new ConfigError(`${where} must be an array of unit names`);
|
|
1976
|
-
}
|
|
1977
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1978
|
-
const out = [];
|
|
1979
|
-
for (const [i, item] of raw.entries()) {
|
|
1980
|
-
if (typeof item !== "string") {
|
|
1981
|
-
throw new ConfigError(`${where}[${String(i)}] must be a string`);
|
|
1982
|
-
}
|
|
1983
|
-
if (!/^[A-Za-z0-9_-]+$/.test(item)) {
|
|
1984
|
-
throw new ConfigError(`${where}[${String(i)}] "${item}" must match [A-Za-z0-9_-]+`);
|
|
1985
|
-
}
|
|
1986
|
-
if (seen.has(item)) continue;
|
|
1987
|
-
seen.add(item);
|
|
1988
|
-
out.push(item);
|
|
1989
|
-
}
|
|
1990
|
-
return out;
|
|
1991
|
-
}
|
|
1992
|
-
var PROBE_KEYS = /* @__PURE__ */ new Set([
|
|
1993
|
-
"port",
|
|
1994
|
-
"host",
|
|
1995
|
-
"log_match",
|
|
1996
|
-
"http",
|
|
1997
|
-
"expect_status",
|
|
1998
|
-
"interval_ms",
|
|
1999
|
-
"initial_delay_ms",
|
|
2000
|
-
"timeout_ms",
|
|
2001
|
-
"on_timeout"
|
|
2002
|
-
]);
|
|
2003
|
-
function parseReadyWhen(raw, where) {
|
|
2004
|
-
if (raw === void 0 || raw === null) return void 0;
|
|
2005
|
-
if (!isPlainObject2(raw)) {
|
|
2006
|
-
throw new ConfigError(`${where}.ready_when must be a mapping`);
|
|
2007
|
-
}
|
|
2008
|
-
rejectUnknownKeys(raw, PROBE_KEYS, `${where}.ready_when`);
|
|
2009
|
-
const kinds = [];
|
|
2010
|
-
if (raw.port !== void 0) kinds.push("port");
|
|
2011
|
-
if (raw.log_match !== void 0) kinds.push("log_match");
|
|
2012
|
-
if (raw.http !== void 0) kinds.push("http");
|
|
2013
|
-
if (kinds.length === 0) {
|
|
2014
|
-
throw new ConfigError(
|
|
2015
|
-
`${where}.ready_when must declare exactly one of: port, log_match, http`
|
|
2016
|
-
);
|
|
2017
|
-
}
|
|
2018
|
-
if (kinds.length > 1) {
|
|
2019
|
-
throw new ConfigError(
|
|
2020
|
-
`${where}.ready_when may declare only one of: port, log_match, http (got ${kinds.join(", ")})`
|
|
2021
|
-
);
|
|
2022
|
-
}
|
|
2023
|
-
const timeoutMs = parsePositiveInt(
|
|
2024
|
-
raw.timeout_ms,
|
|
2025
|
-
`${where}.ready_when.timeout_ms`,
|
|
2026
|
-
DEFAULT_PROBE_TIMEOUT_MS
|
|
2027
|
-
);
|
|
2028
|
-
const onTimeout = parseOnTimeout(raw.on_timeout, `${where}.ready_when.on_timeout`);
|
|
2029
|
-
const kind = kinds[0];
|
|
2030
|
-
if (kind === "log_match") {
|
|
2031
|
-
if (raw.host !== void 0 || raw.expect_status !== void 0 || raw.interval_ms !== void 0 || raw.initial_delay_ms !== void 0) {
|
|
2032
|
-
throw new ConfigError(
|
|
2033
|
-
`${where}.ready_when.log_match cannot be combined with host/expect_status/interval_ms/initial_delay_ms`
|
|
2034
|
-
);
|
|
2035
|
-
}
|
|
2036
|
-
const pat = assertString(raw.log_match, `${where}.ready_when.log_match`);
|
|
2037
|
-
let pattern;
|
|
2038
|
-
try {
|
|
2039
|
-
pattern = new RegExp(pat);
|
|
2040
|
-
} catch (err) {
|
|
2041
|
-
throw new ConfigError(
|
|
2042
|
-
`${where}.ready_when.log_match is not a valid regex: ${err instanceof Error ? err.message : String(err)}`
|
|
2043
|
-
);
|
|
2044
|
-
}
|
|
2045
|
-
return { kind: "log_match", pattern, timeoutMs, onTimeout };
|
|
2046
|
-
}
|
|
2047
|
-
const intervalMs = parsePositiveInt(
|
|
2048
|
-
raw.interval_ms,
|
|
2049
|
-
`${where}.ready_when.interval_ms`,
|
|
2050
|
-
DEFAULT_PROBE_INTERVAL_MS
|
|
2051
|
-
);
|
|
2052
|
-
const initialDelayMs = parseNonNegativeInt(
|
|
2053
|
-
raw.initial_delay_ms,
|
|
2054
|
-
`${where}.ready_when.initial_delay_ms`,
|
|
2055
|
-
DEFAULT_PROBE_INITIAL_DELAY_MS
|
|
2056
|
-
);
|
|
2057
|
-
if (kind === "port") {
|
|
2058
|
-
if (raw.expect_status !== void 0) {
|
|
2059
|
-
throw new ConfigError(`${where}.ready_when.expect_status only applies to http probes`);
|
|
2060
|
-
}
|
|
2061
|
-
const port = parsePositiveInt(raw.port, `${where}.ready_when.port`, 0);
|
|
2062
|
-
if (port < 1 || port > 65535) {
|
|
2063
|
-
throw new ConfigError(`${where}.ready_when.port must be between 1 and 65535`);
|
|
2064
|
-
}
|
|
2065
|
-
const host = raw.host === void 0 ? DEFAULT_PROBE_HOST : assertString(raw.host, `${where}.ready_when.host`);
|
|
2066
|
-
return { kind: "port", port, host, intervalMs, initialDelayMs, timeoutMs, onTimeout };
|
|
2067
|
-
}
|
|
2068
|
-
if (raw.host !== void 0) {
|
|
2069
|
-
throw new ConfigError(`${where}.ready_when.host only applies to port probes`);
|
|
2070
|
-
}
|
|
2071
|
-
const url = assertString(raw.http, `${where}.ready_when.http`);
|
|
2072
|
-
let parsed;
|
|
2073
|
-
try {
|
|
2074
|
-
parsed = new URL(url);
|
|
2075
|
-
} catch {
|
|
2076
|
-
throw new ConfigError(`${where}.ready_when.http must be a valid URL`);
|
|
2077
|
-
}
|
|
2078
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2079
|
-
throw new ConfigError(`${where}.ready_when.http must use http(s):// (got ${parsed.protocol})`);
|
|
2080
|
-
}
|
|
2081
|
-
let expectStatus;
|
|
2082
|
-
if (raw.expect_status !== void 0) {
|
|
2083
|
-
expectStatus = parsePositiveInt(raw.expect_status, `${where}.ready_when.expect_status`, 0);
|
|
2084
|
-
if (expectStatus < 100 || expectStatus > 599) {
|
|
2085
|
-
throw new ConfigError(`${where}.ready_when.expect_status must be between 100 and 599`);
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return { kind: "http", url, expectStatus, intervalMs, initialDelayMs, timeoutMs, onTimeout };
|
|
2089
|
-
}
|
|
2090
|
-
var RESERVED_WEB_PORT = 80;
|
|
2091
|
-
var SERVICE_KEYS = /* @__PURE__ */ new Set([
|
|
2092
|
-
"command",
|
|
2093
|
-
"cwd",
|
|
2094
|
-
"env",
|
|
2095
|
-
"autostart",
|
|
2096
|
-
"restart",
|
|
2097
|
-
"backoff",
|
|
2098
|
-
"needs",
|
|
2099
|
-
"ready_when",
|
|
2100
|
-
"expose",
|
|
2101
|
-
"ide"
|
|
2102
|
-
]);
|
|
2103
|
-
var EXPOSE_KEYS = /* @__PURE__ */ new Set(["port", "as"]);
|
|
2104
|
-
function parseExpose(raw, where) {
|
|
2105
|
-
if (raw === void 0 || raw === null) return void 0;
|
|
2106
|
-
if (!isPlainObject2(raw)) {
|
|
2107
|
-
throw new ConfigError(`${where}.expose must be a mapping`);
|
|
2108
|
-
}
|
|
2109
|
-
rejectUnknownKeys(raw, EXPOSE_KEYS, `${where}.expose`);
|
|
2110
|
-
if (raw.port === void 0) {
|
|
2111
|
-
throw new ConfigError(`${where}.expose.port is required`);
|
|
2112
|
-
}
|
|
2113
|
-
const port = parsePortNumber(raw.port, `${where}.expose.port`);
|
|
2114
|
-
const as = raw.as === void 0 ? RESERVED_WEB_PORT : parsePortNumber(raw.as, `${where}.expose.as`);
|
|
2115
|
-
if (as !== RESERVED_WEB_PORT) {
|
|
2116
|
-
throw new ConfigError(
|
|
2117
|
-
`${where}.expose.as must be ${String(RESERVED_WEB_PORT)} (the only container port AgentBox publishes)`
|
|
2118
|
-
);
|
|
2119
|
-
}
|
|
2120
|
-
return { port, as };
|
|
2121
|
-
}
|
|
2122
|
-
function parsePortNumber(raw, where) {
|
|
2123
|
-
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 65535) {
|
|
2124
|
-
throw new ConfigError(`${where} must be an integer between 1 and 65535`);
|
|
2125
|
-
}
|
|
2126
|
-
return raw;
|
|
2127
|
-
}
|
|
2128
|
-
function parseService(name, raw) {
|
|
2129
|
-
const where = `services.${name}`;
|
|
2130
|
-
if (!isPlainObject2(raw)) {
|
|
2131
|
-
throw new ConfigError(`${where} must be a mapping`);
|
|
2132
|
-
}
|
|
2133
|
-
rejectUnknownKeys(raw, SERVICE_KEYS, where);
|
|
2134
|
-
const command = parseCommand(raw.command, where);
|
|
2135
|
-
const cwd = raw.cwd === void 0 ? void 0 : assertString(raw.cwd, `${where}.cwd`);
|
|
2136
|
-
const env = parseEnv(raw.env, where);
|
|
2137
|
-
const autostart = raw.autostart === void 0 ? true : assertBool(raw.autostart, `${where}.autostart`);
|
|
2138
|
-
const restart2 = parseRestart(raw.restart, where);
|
|
2139
|
-
const backoff = parseBackoff(raw.backoff, where);
|
|
2140
|
-
const needs = parseNeeds(raw.needs, `${where}.needs`);
|
|
2141
|
-
const readyWhen = parseReadyWhen(raw.ready_when, where);
|
|
2142
|
-
const expose = parseExpose(raw.expose, where);
|
|
2143
|
-
return { name, command, cwd, env, autostart, restart: restart2, backoff, needs, readyWhen, expose };
|
|
2144
|
-
}
|
|
2145
|
-
var TASK_KEYS = /* @__PURE__ */ new Set(["command", "cwd", "env", "needs"]);
|
|
2146
|
-
function parseTask(name, raw) {
|
|
2147
|
-
const where = `tasks.${name}`;
|
|
2148
|
-
if (!isPlainObject2(raw)) {
|
|
2149
|
-
throw new ConfigError(`${where} must be a mapping`);
|
|
2150
|
-
}
|
|
2151
|
-
rejectUnknownKeys(raw, TASK_KEYS, where);
|
|
2152
|
-
const command = parseCommand(raw.command, where);
|
|
2153
|
-
const cwd = raw.cwd === void 0 ? void 0 : assertString(raw.cwd, `${where}.cwd`);
|
|
2154
|
-
const env = parseEnv(raw.env, where);
|
|
2155
|
-
const needs = parseNeeds(raw.needs, `${where}.needs`);
|
|
2156
|
-
return { name, command, cwd, env, needs };
|
|
2157
|
-
}
|
|
2158
|
-
function assertString(raw, where) {
|
|
2159
|
-
if (typeof raw !== "string") throw new ConfigError(`${where} must be a string`);
|
|
2160
|
-
return raw;
|
|
2161
|
-
}
|
|
2162
|
-
function assertBool(raw, where) {
|
|
2163
|
-
if (typeof raw !== "boolean") throw new ConfigError(`${where} must be a boolean`);
|
|
2164
|
-
return raw;
|
|
2165
|
-
}
|
|
2166
|
-
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults"]);
|
|
2167
|
-
function validateUnitGraph(tasks, services) {
|
|
2168
|
-
const names = /* @__PURE__ */ new Set();
|
|
2169
|
-
for (const t of tasks) {
|
|
2170
|
-
if (names.has(t.name)) {
|
|
2171
|
-
throw new ConfigError(`unit name "${t.name}" declared more than once (task vs service collision)`);
|
|
2172
|
-
}
|
|
2173
|
-
names.add(t.name);
|
|
2174
|
-
}
|
|
2175
|
-
for (const s of services) {
|
|
2176
|
-
if (names.has(s.name)) {
|
|
2177
|
-
throw new ConfigError(`unit name "${s.name}" declared more than once (task vs service collision)`);
|
|
2178
|
-
}
|
|
2179
|
-
names.add(s.name);
|
|
2180
|
-
}
|
|
2181
|
-
const deps = /* @__PURE__ */ new Map();
|
|
2182
|
-
for (const t of tasks) deps.set(t.name, t.needs);
|
|
2183
|
-
for (const s of services) deps.set(s.name, s.needs);
|
|
2184
|
-
for (const [unit, list] of deps) {
|
|
2185
|
-
for (const dep of list) {
|
|
2186
|
-
if (!names.has(dep)) {
|
|
2187
|
-
throw new ConfigError(`unit "${unit}" needs unknown unit "${dep}"`);
|
|
2188
|
-
}
|
|
2189
|
-
if (dep === unit) {
|
|
2190
|
-
throw new ConfigError(`unit "${unit}" cannot depend on itself`);
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
2195
|
-
const color = /* @__PURE__ */ new Map();
|
|
2196
|
-
for (const name of deps.keys()) color.set(name, WHITE);
|
|
2197
|
-
const stack = [];
|
|
2198
|
-
function visit(name) {
|
|
2199
|
-
color.set(name, GRAY);
|
|
2200
|
-
stack.push(name);
|
|
2201
|
-
for (const dep of deps.get(name)) {
|
|
2202
|
-
const c = color.get(dep) ?? WHITE;
|
|
2203
|
-
if (c === GRAY) {
|
|
2204
|
-
const startIdx = stack.indexOf(dep);
|
|
2205
|
-
const cycle = stack.slice(startIdx).concat(dep).join(" \u2192 ");
|
|
2206
|
-
throw new ConfigError(`cyclic dependency: ${cycle}`);
|
|
2207
|
-
}
|
|
2208
|
-
if (c === WHITE) visit(dep);
|
|
2209
|
-
}
|
|
2210
|
-
stack.pop();
|
|
2211
|
-
color.set(name, BLACK);
|
|
2212
|
-
}
|
|
2213
|
-
for (const name of deps.keys()) {
|
|
2214
|
-
if (color.get(name) === WHITE) visit(name);
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
function parseConfig(text) {
|
|
2218
|
-
let doc;
|
|
2219
|
-
try {
|
|
2220
|
-
doc = parseYaml(text);
|
|
2221
|
-
} catch (err) {
|
|
2222
|
-
throw new ConfigError(`yaml parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2223
|
-
}
|
|
2224
|
-
if (doc === null || doc === void 0) return { services: [], tasks: [] };
|
|
2225
|
-
if (!isPlainObject2(doc)) {
|
|
2226
|
-
throw new ConfigError("top-level config must be a mapping");
|
|
2227
|
-
}
|
|
2228
|
-
rejectUnknownKeys(doc, TOP_LEVEL_KEYS, "(root)");
|
|
2229
|
-
const services = [];
|
|
2230
|
-
const servicesRaw = doc.services;
|
|
2231
|
-
if (servicesRaw !== void 0 && servicesRaw !== null) {
|
|
2232
|
-
if (!isPlainObject2(servicesRaw)) {
|
|
2233
|
-
throw new ConfigError("services must be a mapping of name \u2192 service");
|
|
2234
|
-
}
|
|
2235
|
-
for (const [name, raw] of Object.entries(servicesRaw)) {
|
|
2236
|
-
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
2237
|
-
throw new ConfigError(`service name "${name}" must match [A-Za-z0-9_-]+`);
|
|
2238
|
-
}
|
|
2239
|
-
services.push(parseService(name, raw));
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
const tasks = [];
|
|
2243
|
-
const tasksRaw = doc.tasks;
|
|
2244
|
-
if (tasksRaw !== void 0 && tasksRaw !== null) {
|
|
2245
|
-
if (!isPlainObject2(tasksRaw)) {
|
|
2246
|
-
throw new ConfigError("tasks must be a mapping of name \u2192 task");
|
|
2247
|
-
}
|
|
2248
|
-
for (const [name, raw] of Object.entries(tasksRaw)) {
|
|
2249
|
-
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
2250
|
-
throw new ConfigError(`task name "${name}" must match [A-Za-z0-9_-]+`);
|
|
2251
|
-
}
|
|
2252
|
-
tasks.push(parseTask(name, raw));
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
if (doc.ide !== void 0 && doc.ide !== null && !isPlainObject2(doc.ide)) {
|
|
2256
|
-
throw new ConfigError("ide must be a mapping");
|
|
2257
|
-
}
|
|
2258
|
-
if (doc.defaults !== void 0 && doc.defaults !== null && !isPlainObject2(doc.defaults)) {
|
|
2259
|
-
throw new ConfigError("defaults must be a mapping");
|
|
2260
|
-
}
|
|
2261
|
-
validateUnitGraph(tasks, services);
|
|
2262
|
-
const exposed = services.filter((s) => s.expose);
|
|
2263
|
-
if (exposed.length > 1) {
|
|
2264
|
-
throw new ConfigError(
|
|
2265
|
-
`at most one service may set expose: (got: ${exposed.map((s) => s.name).join(", ")})`
|
|
2266
|
-
);
|
|
2267
|
-
}
|
|
2268
|
-
return { services, tasks };
|
|
2269
|
-
}
|
|
2270
|
-
async function loadConfig(path) {
|
|
2271
|
-
let text;
|
|
2272
|
-
try {
|
|
2273
|
-
text = await readFile3(path, "utf8");
|
|
2274
|
-
} catch (err) {
|
|
2275
|
-
if (err.code === "ENOENT") {
|
|
2276
|
-
return { services: [], tasks: [] };
|
|
2277
|
-
}
|
|
2278
|
-
throw err;
|
|
2279
|
-
}
|
|
2280
|
-
return parseConfig(text);
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
export {
|
|
2284
|
-
DEFAULT_RELAY_PORT,
|
|
2285
|
-
RELAY_CONTAINER_NAME,
|
|
2286
|
-
RELAY_NETWORK_NAME,
|
|
2287
|
-
RELAY_IMAGE_REF,
|
|
2288
|
-
SHARED_CLAUDE_VOLUME,
|
|
2289
|
-
resolveClaudeVolume,
|
|
2290
|
-
ensureClaudeVolume,
|
|
2291
|
-
seedSetupSkillIntoVolume,
|
|
2292
|
-
buildClaudeMounts,
|
|
2293
|
-
rebuildPluginNativeDeps,
|
|
2294
|
-
ClaudeSessionError,
|
|
2295
|
-
startClaudeSession,
|
|
2296
|
-
buildClaudeAttachArgv,
|
|
2297
|
-
buildClaudeDashboardAttachArgv,
|
|
2298
|
-
buildShellArgv,
|
|
2299
|
-
formatDetachNotice,
|
|
2300
|
-
claudeSessionInfo,
|
|
2301
|
-
pullClaudeExtras,
|
|
2302
|
-
SHARED_DOCKER_CACHE_VOLUME,
|
|
2303
|
-
dockerVolumeName,
|
|
2304
|
-
launchDockerdDaemon,
|
|
2305
|
-
launchVncDaemon,
|
|
2306
|
-
generateVncPassword,
|
|
2307
|
-
VNC_CONTAINER_PORT,
|
|
2308
|
-
buildVncUrls,
|
|
2309
|
-
WEB_CONTAINER_PORT,
|
|
2310
|
-
detectGitRepos,
|
|
2311
|
-
pickFreshBranch,
|
|
2312
|
-
gitWorktreePathFor,
|
|
2313
|
-
collectRepoCarryOver,
|
|
2314
|
-
chownGitBindParents,
|
|
2315
|
-
bindWorktrees,
|
|
2316
|
-
seedWorkspace,
|
|
2317
|
-
seedWorkspaceFromDir,
|
|
2318
|
-
removeInBoxWorktree,
|
|
2319
|
-
SNAPSHOTS_ROOT,
|
|
2320
|
-
snapshotPathFor,
|
|
2321
|
-
createSnapshot,
|
|
2322
|
-
launchCtlDaemon,
|
|
2323
|
-
ensureRelay,
|
|
2324
|
-
stopRelay,
|
|
2325
|
-
getRelayStatus,
|
|
2326
|
-
generateRelayToken,
|
|
2327
|
-
registerBoxWithRelay,
|
|
2328
|
-
forgetBoxFromRelay,
|
|
2329
|
-
rehydrateRelayRegistry,
|
|
2330
|
-
ideProfile,
|
|
2331
|
-
SHARED_VSCODE_EXTENSIONS_VOLUME,
|
|
2332
|
-
SHARED_CURSOR_EXTENSIONS_VOLUME,
|
|
2333
|
-
vscodeServerVolumeName,
|
|
2334
|
-
cursorServerVolumeName,
|
|
2335
|
-
buildIdeMounts,
|
|
2336
|
-
ensureIdeVolumes,
|
|
2337
|
-
repairIdeOwnership,
|
|
2338
|
-
containerHex,
|
|
2339
|
-
ensureAgentboxTasksFile,
|
|
2340
|
-
renderStatusTable,
|
|
2341
|
-
renderTaskTable,
|
|
2342
|
-
renderPortsTable,
|
|
2343
|
-
ConfigError,
|
|
2344
|
-
loadConfig
|
|
2345
|
-
};
|
|
2346
|
-
//# sourceMappingURL=chunk-PXUBE5KS.js.map
|