vibebox 0.0.0 → 0.0.2
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/Dockerfile +86 -0
- package/LICENSE.md +187 -0
- package/README.md +155 -2
- package/container-scripts/port-monitor.sh +37 -0
- package/container-scripts/startup.sh +109 -0
- package/container-scripts/watcher.sh +46 -0
- package/dist/agents/auth.js +77 -0
- package/dist/agents/claude/index.js +176 -0
- package/dist/agents/index.js +24 -0
- package/dist/agents/types.js +2 -0
- package/dist/auth.js +11 -0
- package/dist/index.js +883 -0
- package/dist/update.js +256 -0
- package/package.json +25 -24
- package/.dockerignore +0 -13
- package/LICENSE +0 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.logo = void 0;
|
|
5
|
+
exports.buildImage = buildImage;
|
|
6
|
+
exports.ensureSandbox = ensureSandbox;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const node_os_1 = require("node:os");
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const node_readline_1 = require("node:readline");
|
|
14
|
+
const update_1 = require("./update");
|
|
15
|
+
const agents_1 = require("./agents");
|
|
16
|
+
exports.logo = `
|
|
17
|
+
█ █ █ █▄▄ █▀▀ █▄▄ █▀█ ▀▄▀
|
|
18
|
+
▀▄▀ █ █▄█ ██▄ █▄█ █▄█ █ █
|
|
19
|
+
`;
|
|
20
|
+
// ============ CLI ============
|
|
21
|
+
commander_1.program
|
|
22
|
+
.name("vibebox")
|
|
23
|
+
.version("0.1.0")
|
|
24
|
+
.description("Run CLI agents in sandboxes. Each folder gets its own sandbox.")
|
|
25
|
+
.enablePositionalOptions();
|
|
26
|
+
// Agent command group
|
|
27
|
+
const agent = commander_1.program.command("agent").description("Manage and run agents");
|
|
28
|
+
agent
|
|
29
|
+
.command("run <name> [args...]")
|
|
30
|
+
.description("Run an agent in the sandbox")
|
|
31
|
+
.option("--temp", "Temporary new workspace, prompts to save on exit")
|
|
32
|
+
.option("-w, --workspace <path>", "Use specific workspace directory")
|
|
33
|
+
.allowUnknownOption()
|
|
34
|
+
.action(async (name, args, opts) => {
|
|
35
|
+
await runAgent(name, args, opts);
|
|
36
|
+
});
|
|
37
|
+
agent
|
|
38
|
+
.command("ls")
|
|
39
|
+
.description("List available agents with install status")
|
|
40
|
+
.action(() => {
|
|
41
|
+
const workspace = process.cwd();
|
|
42
|
+
const containerName = getSandboxName(workspace);
|
|
43
|
+
for (const [name, agentDef] of Object.entries(agents_1.agents)) {
|
|
44
|
+
const hostInstalled = agentDef.isInstalledOnHost();
|
|
45
|
+
const sandboxInstalled = containerName ? (0, agents_1.isAgentInstalled)(containerName, agentDef) : false;
|
|
46
|
+
let status = "○ not installed";
|
|
47
|
+
if (sandboxInstalled)
|
|
48
|
+
status = "✓ installed in sandbox";
|
|
49
|
+
else if (hostInstalled)
|
|
50
|
+
status = "○ not in sandbox (host: detected)";
|
|
51
|
+
console.log(` ${name.padEnd(10)} ${status}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
agent
|
|
55
|
+
.command("install <name>")
|
|
56
|
+
.description("Install an agent in the current sandbox")
|
|
57
|
+
.action(async (name) => {
|
|
58
|
+
const agentDef = agents_1.agents[name];
|
|
59
|
+
if (!agentDef) {
|
|
60
|
+
console.error(`Unknown agent: ${name}`);
|
|
61
|
+
console.log(`Available: ${Object.keys(agents_1.agents).join(", ")}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
await ensureDocker();
|
|
65
|
+
const workspace = process.cwd();
|
|
66
|
+
const hostInstalled = agentDef.isInstalledOnHost();
|
|
67
|
+
let containerOnly = false;
|
|
68
|
+
if (!hostInstalled) {
|
|
69
|
+
if (!await promptContainerOnly(agentDef.name))
|
|
70
|
+
process.exit(0);
|
|
71
|
+
containerOnly = true;
|
|
72
|
+
}
|
|
73
|
+
agentDef.setup?.({ workspace, containerOnly });
|
|
74
|
+
const containerName = ensureSandbox({ workspace, agents: [agentDef], containerOnly });
|
|
75
|
+
agentDef.install(containerName);
|
|
76
|
+
console.log(`✓ ${agentDef.name} installed`);
|
|
77
|
+
});
|
|
78
|
+
async function runAgent(name, args, opts) {
|
|
79
|
+
const agent = agents_1.agents[name];
|
|
80
|
+
if (!agent) {
|
|
81
|
+
console.error(`Unknown agent: ${name}`);
|
|
82
|
+
console.log(`Available: ${Object.keys(agents_1.agents).join(", ")}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
await ensureDocker();
|
|
86
|
+
await (0, update_1.checkForUpdates)();
|
|
87
|
+
let workspace = opts.workspace ?? process.cwd();
|
|
88
|
+
if (opts.temp) {
|
|
89
|
+
workspace = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vibebox-"));
|
|
90
|
+
console.log(`Temp workspace: ${workspace}`);
|
|
91
|
+
const handleSignal = async () => {
|
|
92
|
+
await promptKeep(workspace);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
};
|
|
95
|
+
process.on("SIGINT", handleSignal);
|
|
96
|
+
process.on("SIGTERM", handleSignal);
|
|
97
|
+
}
|
|
98
|
+
// Check if we need container-only mode
|
|
99
|
+
const existing = getSandboxName(workspace);
|
|
100
|
+
const existingRunning = existing && isContainerRunning(existing);
|
|
101
|
+
const needsInstall = !existingRunning || !(0, agents_1.isAgentInstalled)(existing, agent);
|
|
102
|
+
const hostInstalled = agent.isInstalledOnHost();
|
|
103
|
+
let containerOnly = false;
|
|
104
|
+
if (needsInstall && !hostInstalled) {
|
|
105
|
+
if (!await promptContainerOnly(agent.name))
|
|
106
|
+
process.exit(0);
|
|
107
|
+
containerOnly = true;
|
|
108
|
+
}
|
|
109
|
+
// Run agent setup (auth, config files)
|
|
110
|
+
agent.setup?.({ workspace, containerOnly });
|
|
111
|
+
const containerName = ensureSandbox({ workspace, agents: [agent], containerOnly });
|
|
112
|
+
if (agent.versionCommand && !containerOnly) {
|
|
113
|
+
await (0, update_1.checkVersions)(containerName, agent);
|
|
114
|
+
}
|
|
115
|
+
// Auto-install if not present
|
|
116
|
+
if (!(0, agents_1.isAgentInstalled)(containerName, agent)) {
|
|
117
|
+
if (hostInstalled) {
|
|
118
|
+
const answer = await promptConfirm(`${agent.name} detected on host. Install in sandbox?`);
|
|
119
|
+
if (answer)
|
|
120
|
+
agent.install(containerName);
|
|
121
|
+
else
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Container-only mode - install without host
|
|
126
|
+
agent.install(containerName);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const r = withSessionLock({
|
|
130
|
+
name: containerName,
|
|
131
|
+
fn: () => (0, node_child_process_1.spawnSync)("docker", ["exec", "-it", containerName, agent.command, ...args], { stdio: "inherit" }),
|
|
132
|
+
});
|
|
133
|
+
if (opts.temp)
|
|
134
|
+
await promptKeep(workspace);
|
|
135
|
+
process.exit(r.status ?? 0);
|
|
136
|
+
}
|
|
137
|
+
async function promptContainerOnly(agentName) {
|
|
138
|
+
console.log(`\n${agentName} is not installed on your host machine.`);
|
|
139
|
+
console.log("\nContainer-only mode:");
|
|
140
|
+
console.log(" - Agent will be installed only inside the sandbox");
|
|
141
|
+
console.log(" - You'll need to log in separately (no shared auth with host)");
|
|
142
|
+
console.log(" - Settings/credentials won't sync across workspaces");
|
|
143
|
+
console.log(" - Each sandbox manages its own config\n");
|
|
144
|
+
return promptConfirm(`Install ${agentName} in container-only mode?`);
|
|
145
|
+
}
|
|
146
|
+
async function promptConfirm(question) {
|
|
147
|
+
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
rl.question(`${question} [Y/n]: `, (answer) => {
|
|
150
|
+
rl.close();
|
|
151
|
+
resolve(answer.toLowerCase() !== "n");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
commander_1.program
|
|
156
|
+
.command("enter")
|
|
157
|
+
.description("Shell into sandbox for current directory")
|
|
158
|
+
.action(async () => {
|
|
159
|
+
console.log(exports.logo);
|
|
160
|
+
await ensureDocker();
|
|
161
|
+
await (0, update_1.checkForUpdates)();
|
|
162
|
+
const workspace = process.cwd();
|
|
163
|
+
const installedAgents = setupInstalledAgents({ workspace });
|
|
164
|
+
const name = ensureSandbox({ workspace, agents: installedAgents });
|
|
165
|
+
await (0, update_1.checkVersions)(name);
|
|
166
|
+
withSessionLock({
|
|
167
|
+
name,
|
|
168
|
+
fn: () => (0, node_child_process_1.spawnSync)("docker", ["exec", "-it", name, "/bin/zsh"], { stdio: "inherit" }),
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
commander_1.program
|
|
172
|
+
.command("exec <cmd...>")
|
|
173
|
+
.description("Run command in sandbox")
|
|
174
|
+
.allowUnknownOption()
|
|
175
|
+
.passThroughOptions()
|
|
176
|
+
.action(async (cmd) => {
|
|
177
|
+
await ensureDocker();
|
|
178
|
+
const workspace = process.cwd();
|
|
179
|
+
const installedAgents = setupInstalledAgents({ workspace });
|
|
180
|
+
const name = ensureSandbox({ workspace, agents: installedAgents });
|
|
181
|
+
const r = withSessionLock({
|
|
182
|
+
name,
|
|
183
|
+
fn: () => (0, node_child_process_1.spawnSync)("docker", ["exec", name, ...cmd], { stdio: "inherit" }),
|
|
184
|
+
});
|
|
185
|
+
process.exit(r.status ?? 0);
|
|
186
|
+
});
|
|
187
|
+
commander_1.program
|
|
188
|
+
.command("ls")
|
|
189
|
+
.description("List sandboxes")
|
|
190
|
+
.action(() => {
|
|
191
|
+
try {
|
|
192
|
+
const out = (0, node_child_process_1.execSync)('docker ps -a --filter "label=docker/sandbox=true" --format "table {{.Names}}\t{{.Status}}\t{{.Label \\"com.docker.sandbox.workingDirectory\\"}}"', { encoding: "utf8" });
|
|
193
|
+
console.log(out || "No sandboxes");
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
console.log("No sandboxes");
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
commander_1.program
|
|
200
|
+
.command("stop")
|
|
201
|
+
.description("Stop sandbox(es)")
|
|
202
|
+
.option("--all", "Stop all sandboxes")
|
|
203
|
+
.action((opts) => {
|
|
204
|
+
if (opts.all) {
|
|
205
|
+
try {
|
|
206
|
+
const names = (0, node_child_process_1.execSync)('docker ps -q --filter "label=docker/sandbox=true"', { encoding: "utf8" }).trim();
|
|
207
|
+
if (names) {
|
|
208
|
+
(0, node_child_process_1.spawnSync)("docker", ["stop", ...names.split("\n")], { stdio: "inherit" });
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
console.log("No running sandboxes");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
console.log("No running sandboxes");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const name = getSandboxName(process.cwd());
|
|
220
|
+
if (!name) {
|
|
221
|
+
console.error("No sandbox");
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
(0, node_child_process_1.execFileSync)("docker", ["stop", name], { stdio: "pipe" });
|
|
225
|
+
console.log("Stopped");
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
commander_1.program
|
|
229
|
+
.command("rm")
|
|
230
|
+
.description("Remove sandbox(es)")
|
|
231
|
+
.option("--all", "Remove all sandboxes")
|
|
232
|
+
.action((opts) => {
|
|
233
|
+
if (opts.all) {
|
|
234
|
+
try {
|
|
235
|
+
const names = (0, node_child_process_1.execSync)('docker ps -a --filter "label=docker/sandbox=true" --format "{{.Names}}"', { encoding: "utf8" }).trim();
|
|
236
|
+
if (names) {
|
|
237
|
+
for (const name of names.split("\n")) {
|
|
238
|
+
const ws = (0, node_child_process_1.execFileSync)("docker", ["inspect", name, "--format", '{{index .Config.Labels "com.docker.sandbox.workingDirectory"}}'], { encoding: "utf8" }).trim();
|
|
239
|
+
(0, node_child_process_1.execFileSync)("docker", ["rm", "-f", name], { stdio: "pipe" });
|
|
240
|
+
if (ws && (0, node_fs_1.existsSync)((0, node_path_1.join)(ws, ".vibebox"))) {
|
|
241
|
+
(0, node_fs_1.rmSync)((0, node_path_1.join)(ws, ".vibebox"), { recursive: true });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
console.log("Removed all sandboxes");
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log("No sandboxes");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
console.log("No sandboxes");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
const name = getSandboxName(process.cwd());
|
|
256
|
+
if (!name) {
|
|
257
|
+
console.error("No sandbox");
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const ws = process.cwd();
|
|
261
|
+
(0, node_child_process_1.execFileSync)("docker", ["rm", "-f", name], { stdio: "pipe" });
|
|
262
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(ws, ".vibebox"))) {
|
|
263
|
+
(0, node_fs_1.rmSync)((0, node_path_1.join)(ws, ".vibebox"), { recursive: true });
|
|
264
|
+
}
|
|
265
|
+
console.log("Removed");
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
commander_1.program
|
|
269
|
+
.command("rebuild")
|
|
270
|
+
.description("Force rebuild image with current host config")
|
|
271
|
+
.action(async () => {
|
|
272
|
+
await ensureDocker();
|
|
273
|
+
await (0, update_1.checkForUpdates)();
|
|
274
|
+
console.log("Rebuilding vibebox image...");
|
|
275
|
+
try {
|
|
276
|
+
(0, node_child_process_1.execSync)("docker rmi vibebox", { stdio: "pipe" });
|
|
277
|
+
}
|
|
278
|
+
catch { }
|
|
279
|
+
buildImage();
|
|
280
|
+
console.log("Image rebuilt");
|
|
281
|
+
});
|
|
282
|
+
// ============ Ports Commands ============
|
|
283
|
+
const DEFAULT_PORTS = ["5173", "3000", "3001", "4173", "8080"];
|
|
284
|
+
const ports = commander_1.program.command("ports").description("Manage port mappings");
|
|
285
|
+
ports
|
|
286
|
+
.command("ls")
|
|
287
|
+
.description("Show current port mappings")
|
|
288
|
+
.action(() => {
|
|
289
|
+
const name = getSandboxName(process.cwd());
|
|
290
|
+
if (!name) {
|
|
291
|
+
console.error("No sandbox");
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const mappings = getPortMappings(name);
|
|
296
|
+
// Get listening ports
|
|
297
|
+
const ssOut = (0, node_child_process_1.execFileSync)("docker", ["exec", name, "ss", "-tln"], { encoding: "utf8" });
|
|
298
|
+
const listening = new Set();
|
|
299
|
+
for (const line of ssOut.split("\n").slice(1)) {
|
|
300
|
+
const match = line.match(/:(\d+)\s*$/);
|
|
301
|
+
if (match)
|
|
302
|
+
listening.add(match[1]);
|
|
303
|
+
}
|
|
304
|
+
if (mappings.size === 0) {
|
|
305
|
+
console.log("No ports mapped");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// Show mapped ports with listening status
|
|
309
|
+
console.log("Mapped ports:");
|
|
310
|
+
for (const [container, host] of mappings) {
|
|
311
|
+
const status = listening.has(container) ? "● listening" : "";
|
|
312
|
+
console.log(` ${container} → http://localhost:${host} ${status}`);
|
|
313
|
+
}
|
|
314
|
+
// Show unmapped listening ports
|
|
315
|
+
const unmapped = [...listening].filter((p) => !mappings.has(p) && parseInt(p) > 1024);
|
|
316
|
+
if (unmapped.length > 0) {
|
|
317
|
+
console.log("\nUnmapped listening:");
|
|
318
|
+
for (const p of unmapped) {
|
|
319
|
+
console.log(` ${p} (add with: vibebox ports add ${p})`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
console.log("No ports (container may not be running)");
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
ports
|
|
328
|
+
.command("add <ports...>")
|
|
329
|
+
.description("Add custom ports")
|
|
330
|
+
.action((add) => {
|
|
331
|
+
const ws = process.cwd();
|
|
332
|
+
const cfg = loadConfig(ws);
|
|
333
|
+
const current = new Set(cfg.ports ?? DEFAULT_PORTS);
|
|
334
|
+
add.forEach((p) => current.add(p));
|
|
335
|
+
cfg.ports = [...current];
|
|
336
|
+
saveConfig(ws, cfg);
|
|
337
|
+
console.log(`Ports: ${cfg.ports.join(", ")}`);
|
|
338
|
+
if (getSandboxName(ws))
|
|
339
|
+
console.log("Restart for changes: vibebox stop && vibebox claude");
|
|
340
|
+
});
|
|
341
|
+
ports
|
|
342
|
+
.command("rm <ports...>")
|
|
343
|
+
.description("Remove custom ports")
|
|
344
|
+
.action((rem) => {
|
|
345
|
+
const ws = process.cwd();
|
|
346
|
+
const cfg = loadConfig(ws);
|
|
347
|
+
const toRem = new Set(rem);
|
|
348
|
+
cfg.ports = (cfg.ports ?? DEFAULT_PORTS).filter((p) => !toRem.has(p));
|
|
349
|
+
saveConfig(ws, cfg);
|
|
350
|
+
console.log(`Ports: ${cfg.ports.join(", ")}`);
|
|
351
|
+
if (getSandboxName(ws))
|
|
352
|
+
console.log("Restart for changes: vibebox stop && vibebox claude");
|
|
353
|
+
});
|
|
354
|
+
// ============ Customize Command ============
|
|
355
|
+
const TEMPLATES = {
|
|
356
|
+
setupGlobal: `#!/bin/bash
|
|
357
|
+
# Runs once when a sandbox is created (all sandboxes)
|
|
358
|
+
# Example: git clone https://github.com/YOU/dotfiles ~/.dotfiles && ~/.dotfiles/install.sh
|
|
359
|
+
# Example: sudo apt-get update && sudo apt-get install -y ripgrep
|
|
360
|
+
`,
|
|
361
|
+
setupLocal: `#!/bin/bash
|
|
362
|
+
# Runs once when this project's sandbox is created
|
|
363
|
+
# Example: pip install -r requirements.txt
|
|
364
|
+
# Example: echo 'export DATABASE_URL=...' >> ~/.bashrc
|
|
365
|
+
`,
|
|
366
|
+
imageGlobal: `# Baked into image, applies to ALL sandboxes. Run 'vibebox rebuild' after editing.
|
|
367
|
+
FROM vibebox
|
|
368
|
+
|
|
369
|
+
# Example: Install apt packages
|
|
370
|
+
# RUN apt-get update && apt-get install -y ripgrep bat
|
|
371
|
+
|
|
372
|
+
# Example: Install oh-my-zsh
|
|
373
|
+
# RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
|
|
374
|
+
`,
|
|
375
|
+
imageLocal: `# Baked into image for THIS project. Run 'vibebox rebuild' after editing.
|
|
376
|
+
FROM vibebox:user
|
|
377
|
+
# Or use 'FROM vibebox' to skip global customizations
|
|
378
|
+
|
|
379
|
+
# Example: Copy config files
|
|
380
|
+
# COPY some-config.json /home/user/.config/
|
|
381
|
+
|
|
382
|
+
# Example: Install project tools
|
|
383
|
+
# RUN pip install -r requirements.txt
|
|
384
|
+
`,
|
|
385
|
+
};
|
|
386
|
+
function openInEditor({ file, template, isExecutable }) {
|
|
387
|
+
const dir = (0, node_path_1.dirname)(file);
|
|
388
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
389
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
390
|
+
if (!(0, node_fs_1.existsSync)(file)) {
|
|
391
|
+
(0, node_fs_1.writeFileSync)(file, template, isExecutable ? { mode: 0o755 } : undefined);
|
|
392
|
+
console.log(`Created ${file}`);
|
|
393
|
+
}
|
|
394
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
395
|
+
const r = (0, node_child_process_1.spawnSync)(editor, [file], { stdio: "inherit" });
|
|
396
|
+
if (r.status !== 0)
|
|
397
|
+
console.error(`Failed to open editor. Edit manually: ${file}`);
|
|
398
|
+
}
|
|
399
|
+
const customize = commander_1.program.command("customize").description("Customize sandbox setup");
|
|
400
|
+
customize
|
|
401
|
+
.command("image [scope]")
|
|
402
|
+
.description("Edit custom Dockerfile (global or local)")
|
|
403
|
+
.action(async (scope) => {
|
|
404
|
+
const targetScope = scope || await promptScope("Dockerfile", "~/.vibebox/Dockerfile", ".vibebox/Dockerfile");
|
|
405
|
+
if (targetScope !== "global" && targetScope !== "local") {
|
|
406
|
+
console.error("Usage: vibebox customize image <global|local>");
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
const isGlobal = targetScope === "global";
|
|
410
|
+
const file = (0, node_path_1.join)(isGlobal ? (0, node_os_1.homedir)() : process.cwd(), ".vibebox", "Dockerfile");
|
|
411
|
+
openInEditor({ file, template: isGlobal ? TEMPLATES.imageGlobal : TEMPLATES.imageLocal });
|
|
412
|
+
console.log(`\nDockerfile saved: ${file}`);
|
|
413
|
+
console.log("Run 'vibebox rebuild' to apply changes.");
|
|
414
|
+
});
|
|
415
|
+
customize
|
|
416
|
+
.command("setup [scope]")
|
|
417
|
+
.description("Edit setup script (global or local)")
|
|
418
|
+
.action(async (scope) => {
|
|
419
|
+
const targetScope = scope || await promptScope("setup script", "~/.vibebox/setup.sh", ".vibebox/setup.sh");
|
|
420
|
+
if (targetScope !== "global" && targetScope !== "local") {
|
|
421
|
+
console.error("Usage: vibebox customize setup <global|local>");
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
const isGlobal = targetScope === "global";
|
|
425
|
+
const file = (0, node_path_1.join)(isGlobal ? (0, node_os_1.homedir)() : process.cwd(), ".vibebox", "setup.sh");
|
|
426
|
+
openInEditor({ file, template: isGlobal ? TEMPLATES.setupGlobal : TEMPLATES.setupLocal, isExecutable: true });
|
|
427
|
+
console.log(`\nSetup script saved: ${file}`);
|
|
428
|
+
if (getSandboxName(process.cwd()))
|
|
429
|
+
console.log("Note: Runs on new sandboxes. To re-run: vibebox rm");
|
|
430
|
+
});
|
|
431
|
+
async function promptScope(type, globalPath, localPath) {
|
|
432
|
+
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
433
|
+
console.log(`\n global ${globalPath.padEnd(24)} All sandboxes`);
|
|
434
|
+
console.log(` local ${localPath.padEnd(24)} This project only\n`);
|
|
435
|
+
const answer = await new Promise((r) => rl.question(`Which ${type}? [global/local]: `, r));
|
|
436
|
+
rl.close();
|
|
437
|
+
return answer.trim().toLowerCase();
|
|
438
|
+
}
|
|
439
|
+
// ============ Help Display & Parse ============
|
|
440
|
+
// Only parse CLI when run directly (not when imported)
|
|
441
|
+
if (require.main === module) {
|
|
442
|
+
const isSubcommand = process.argv.length > 2 && !process.argv[2].startsWith("-");
|
|
443
|
+
if (process.argv.length === 2 ||
|
|
444
|
+
(!isSubcommand && (process.argv.includes("--help") || process.argv.includes("-h")))) {
|
|
445
|
+
console.log(exports.logo);
|
|
446
|
+
}
|
|
447
|
+
commander_1.program.parse();
|
|
448
|
+
}
|
|
449
|
+
// ============ Library code =========
|
|
450
|
+
// ============ Constants ============
|
|
451
|
+
const LOCKS_DIR = `${(0, node_os_1.homedir)()}/.vibebox/locks`;
|
|
452
|
+
const DETACHED_LOCK = `${LOCKS_DIR}/detached.lock`;
|
|
453
|
+
const WATCHER_SCRIPT = `${(0, node_os_1.homedir)()}/.local/bin/watcher.sh`;
|
|
454
|
+
const IDLE_TIMEOUT = 300; // 5 minutes
|
|
455
|
+
function loadConfig(ws) {
|
|
456
|
+
const p = (0, node_path_1.join)(ws, ".vibebox", "config.json");
|
|
457
|
+
if (!(0, node_fs_1.existsSync)(p))
|
|
458
|
+
return {};
|
|
459
|
+
try {
|
|
460
|
+
return JSON.parse((0, node_fs_1.readFileSync)(p, "utf8"));
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return {};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function saveConfig(ws, cfg) {
|
|
467
|
+
const dir = (0, node_path_1.join)(ws, ".vibebox");
|
|
468
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
469
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
470
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(cfg, null, 2));
|
|
471
|
+
}
|
|
472
|
+
function setupInstalledAgents({ workspace }) {
|
|
473
|
+
const installedAgentNames = (0, agents_1.detectInstalledAgents)();
|
|
474
|
+
const installedAgents = installedAgentNames.map((n) => agents_1.agents[n]);
|
|
475
|
+
for (const agent of installedAgents) {
|
|
476
|
+
agent.setup?.({ workspace });
|
|
477
|
+
}
|
|
478
|
+
return installedAgents;
|
|
479
|
+
}
|
|
480
|
+
// ============ Helpers ============
|
|
481
|
+
function getWorkspaceInode(workspace) {
|
|
482
|
+
const flag = process.platform === "darwin" ? "-f" : "-c";
|
|
483
|
+
return (0, node_child_process_1.execSync)(`stat ${flag} %i "${workspace}"`, { encoding: "utf8" }).trim();
|
|
484
|
+
}
|
|
485
|
+
function withSessionLock({ name, fn }) {
|
|
486
|
+
const sessionId = (0, node_crypto_1.randomBytes)(8).toString("hex");
|
|
487
|
+
const lockFile = `${LOCKS_DIR}/session-${sessionId}.lock`;
|
|
488
|
+
(0, node_child_process_1.execFileSync)("docker", ["exec", name, "mkdir", "-p", LOCKS_DIR], { stdio: "pipe" });
|
|
489
|
+
(0, node_child_process_1.execFileSync)("docker", ["exec", name, "touch", lockFile], { stdio: "pipe" });
|
|
490
|
+
try {
|
|
491
|
+
return fn();
|
|
492
|
+
}
|
|
493
|
+
finally {
|
|
494
|
+
// Remove this session's lock
|
|
495
|
+
try {
|
|
496
|
+
(0, node_child_process_1.execFileSync)("docker", ["exec", name, "rm", "-f", lockFile], { stdio: "pipe" });
|
|
497
|
+
}
|
|
498
|
+
catch { } // container stopped
|
|
499
|
+
// Check if other sessions exist, remove detached lock if none
|
|
500
|
+
let hasOthers = false;
|
|
501
|
+
try {
|
|
502
|
+
const out = (0, node_child_process_1.execFileSync)("docker", ["exec", name, "ls", LOCKS_DIR], { encoding: "utf8" });
|
|
503
|
+
const locks = out.split("\n").filter((f) => f.startsWith("session-") && f.endsWith(".lock"));
|
|
504
|
+
hasOthers = locks.filter((f) => f !== `session-${sessionId}.lock`).length > 0;
|
|
505
|
+
}
|
|
506
|
+
catch { } // container stopped
|
|
507
|
+
if (!hasOthers) {
|
|
508
|
+
try {
|
|
509
|
+
(0, node_child_process_1.execFileSync)("docker", ["exec", name, "rm", "-f", DETACHED_LOCK], { stdio: "pipe" });
|
|
510
|
+
}
|
|
511
|
+
catch { } // container stopped
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function buildImage() {
|
|
516
|
+
const info = (0, node_os_1.userInfo)();
|
|
517
|
+
const nodeVersion = (0, node_child_process_1.execSync)("node --version", { encoding: "utf8" }).trim().replace(/^v/, "");
|
|
518
|
+
const npmVersion = (0, node_child_process_1.execSync)("npm --version", { encoding: "utf8" }).trim();
|
|
519
|
+
const contextDir = (0, node_path_1.dirname)((0, node_path_1.join)((0, node_path_1.dirname)(__dirname), "Dockerfile"));
|
|
520
|
+
// Hash all files that affect the Docker image
|
|
521
|
+
const buildFiles = [
|
|
522
|
+
"Dockerfile",
|
|
523
|
+
"container-scripts/startup.sh",
|
|
524
|
+
"container-scripts/port-monitor.sh",
|
|
525
|
+
"container-scripts/watcher.sh",
|
|
526
|
+
];
|
|
527
|
+
const buildHash = (0, update_1.hashString)(buildFiles.map(f => (0, node_fs_1.readFileSync)((0, node_path_1.join)(contextDir, f), "utf8")).join("\n"));
|
|
528
|
+
const r = (0, node_child_process_1.spawnSync)("docker", [
|
|
529
|
+
"build", "-t", "vibebox",
|
|
530
|
+
"--build-arg", `NODE_VERSION=${nodeVersion}`,
|
|
531
|
+
"--build-arg", `NPM_VERSION=${npmVersion}`,
|
|
532
|
+
"--build-arg", `LOCAL_USER=${info.username}`,
|
|
533
|
+
"--build-arg", `LOCAL_UID=${info.uid ?? 1000}`,
|
|
534
|
+
"--build-arg", `LOCAL_GID=${info.gid ?? 1000}`,
|
|
535
|
+
"--build-arg", `LOCAL_HOME=${(0, node_os_1.homedir)()}`,
|
|
536
|
+
"--label", `vibebox.version.build=${buildHash}`,
|
|
537
|
+
contextDir,
|
|
538
|
+
], { stdio: "inherit" });
|
|
539
|
+
if (r.status !== 0) {
|
|
540
|
+
console.error("Failed to build vibebox image");
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function getImageHash(tag) {
|
|
545
|
+
try {
|
|
546
|
+
return (0, node_child_process_1.execFileSync)("docker", ["inspect", tag, "--format", '{{index .Config.Labels "vibebox.hash"}}'], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim() || null;
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function buildCustomImage({ tag, contextDir, fromTag }) {
|
|
553
|
+
const dockerfilePath = (0, node_path_1.join)(contextDir, "Dockerfile");
|
|
554
|
+
if (!(0, node_fs_1.existsSync)(dockerfilePath))
|
|
555
|
+
return false;
|
|
556
|
+
const content = (0, node_fs_1.readFileSync)(dockerfilePath, "utf8");
|
|
557
|
+
const hash = (0, update_1.hashString)(content + fromTag);
|
|
558
|
+
if (getImageHash(tag) === hash)
|
|
559
|
+
return true; // Already up to date
|
|
560
|
+
console.log(`Building ${tag}...`);
|
|
561
|
+
const r = (0, node_child_process_1.spawnSync)("docker", ["build", "-t", tag, "--label", `vibebox.hash=${hash}`, contextDir], { stdio: "inherit" });
|
|
562
|
+
return r.status === 0;
|
|
563
|
+
}
|
|
564
|
+
function getImageTag({ workspace }) {
|
|
565
|
+
const globalDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".vibebox");
|
|
566
|
+
const localDir = (0, node_path_1.join)(workspace, ".vibebox");
|
|
567
|
+
const hasGlobal = (0, node_fs_1.existsSync)((0, node_path_1.join)(globalDir, "Dockerfile"));
|
|
568
|
+
const hasLocal = (0, node_fs_1.existsSync)((0, node_path_1.join)(localDir, "Dockerfile"));
|
|
569
|
+
const wsHash = (0, update_1.hashString)(workspace).slice(0, 8);
|
|
570
|
+
if (hasGlobal)
|
|
571
|
+
buildCustomImage({ tag: "vibebox:user", contextDir: globalDir, fromTag: "vibebox" });
|
|
572
|
+
if (hasLocal)
|
|
573
|
+
buildCustomImage({ tag: `vibebox:${wsHash}`, contextDir: localDir, fromTag: hasGlobal ? "vibebox:user" : "vibebox" });
|
|
574
|
+
if (hasLocal)
|
|
575
|
+
return `vibebox:${wsHash}`;
|
|
576
|
+
if (hasGlobal)
|
|
577
|
+
return "vibebox:user";
|
|
578
|
+
return "vibebox";
|
|
579
|
+
}
|
|
580
|
+
function ensureImage() {
|
|
581
|
+
try {
|
|
582
|
+
(0, node_child_process_1.execSync)("docker image inspect vibebox", { stdio: "pipe" });
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
console.log("Building vibebox image...");
|
|
586
|
+
buildImage();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function getSandboxName(workspace) {
|
|
590
|
+
try {
|
|
591
|
+
const out = (0, node_child_process_1.execFileSync)("docker", ["ps", "-a", "--filter", `label=com.docker.sandbox.workingDirectory=${workspace}`, "--format", "{{.Names}}"], { encoding: "utf8" }).trim();
|
|
592
|
+
return out || null;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function isContainerRunning(name) {
|
|
599
|
+
try {
|
|
600
|
+
const state = (0, node_child_process_1.execFileSync)("docker", ["inspect", name, "--format={{.State.Running}}"], {
|
|
601
|
+
encoding: "utf8",
|
|
602
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
603
|
+
}).trim();
|
|
604
|
+
return state === "true";
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function getPortMappings(name) {
|
|
611
|
+
const mappings = new Map();
|
|
612
|
+
try {
|
|
613
|
+
const out = (0, node_child_process_1.execFileSync)("docker", ["port", name], { encoding: "utf8" }).trim();
|
|
614
|
+
for (const line of out.split("\n")) {
|
|
615
|
+
const m = line.match(/^(\d+)\/tcp -> 0\.0\.0\.0:(\d+)$/);
|
|
616
|
+
if (m)
|
|
617
|
+
mappings.set(m[1], m[2]);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch { } // container not running
|
|
621
|
+
return mappings;
|
|
622
|
+
}
|
|
623
|
+
function writePortMappings(workspace, name) {
|
|
624
|
+
const mappings = getPortMappings(name);
|
|
625
|
+
if (mappings.size === 0)
|
|
626
|
+
return;
|
|
627
|
+
const vibeboxDir = (0, node_path_1.join)(workspace, ".vibebox");
|
|
628
|
+
if (!(0, node_fs_1.existsSync)(vibeboxDir))
|
|
629
|
+
(0, node_fs_1.mkdirSync)(vibeboxDir, { recursive: true });
|
|
630
|
+
const parsed = [...mappings].map(([c, h]) => `${c}:${h}`).join("\n");
|
|
631
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(vibeboxDir, "port-mappings.txt"), parsed);
|
|
632
|
+
}
|
|
633
|
+
function ensureSandbox({ workspace, agents: requestedAgents = [], containerOnly = false, }) {
|
|
634
|
+
const existing = getSandboxName(workspace);
|
|
635
|
+
if (existing) {
|
|
636
|
+
try {
|
|
637
|
+
const state = (0, node_child_process_1.execFileSync)("docker", ["inspect", existing, "--format={{.State.Running}}"], {
|
|
638
|
+
encoding: "utf8",
|
|
639
|
+
}).trim();
|
|
640
|
+
if (state !== "true") {
|
|
641
|
+
(0, node_child_process_1.execFileSync)("docker", ["start", existing], { stdio: "pipe" });
|
|
642
|
+
writePortMappings(workspace, existing);
|
|
643
|
+
}
|
|
644
|
+
// Verify container is now running
|
|
645
|
+
const newState = (0, node_child_process_1.execFileSync)("docker", ["inspect", existing, "--format={{.State.Running}}"], {
|
|
646
|
+
encoding: "utf8",
|
|
647
|
+
}).trim();
|
|
648
|
+
if (newState === "true")
|
|
649
|
+
return existing;
|
|
650
|
+
// Container failed to start, remove it and create fresh
|
|
651
|
+
(0, node_child_process_1.execFileSync)("docker", ["rm", "-f", existing], { stdio: "pipe" });
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Container might not exist anymore or is in bad state, try to remove and fall through
|
|
655
|
+
try {
|
|
656
|
+
(0, node_child_process_1.execFileSync)("docker", ["rm", "-f", existing], { stdio: "pipe" });
|
|
657
|
+
}
|
|
658
|
+
catch { }
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
ensureImage();
|
|
662
|
+
const imageTag = getImageTag({ workspace });
|
|
663
|
+
// ---- Placeholders ----
|
|
664
|
+
// Create placeholders for mount overlays (agents may need these)
|
|
665
|
+
const emptyDir = (0, node_path_1.join)(workspace, ".vibebox", "empty");
|
|
666
|
+
const emptyFile = (0, node_path_1.join)(workspace, ".vibebox", "empty-file");
|
|
667
|
+
if (!(0, node_fs_1.existsSync)(emptyDir))
|
|
668
|
+
(0, node_fs_1.mkdirSync)(emptyDir, { recursive: true });
|
|
669
|
+
if (!(0, node_fs_1.existsSync)(emptyFile))
|
|
670
|
+
(0, node_fs_1.writeFileSync)(emptyFile, "");
|
|
671
|
+
const home = (0, node_os_1.homedir)();
|
|
672
|
+
const inode = getWorkspaceInode(workspace);
|
|
673
|
+
const ports = loadConfig(workspace).ports ?? DEFAULT_PORTS;
|
|
674
|
+
// ---- Agent args ----
|
|
675
|
+
const agentArgs = requestedAgents.flatMap((agent) => agent.dockerArgs({ workspace, home, containerOnly }));
|
|
676
|
+
// ---- Symlinks ----
|
|
677
|
+
const symlinkMounts = new Set();
|
|
678
|
+
const scanForSymlinks = (dir) => {
|
|
679
|
+
try {
|
|
680
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir)) {
|
|
681
|
+
if (entry.startsWith("."))
|
|
682
|
+
continue;
|
|
683
|
+
const entryPath = (0, node_path_1.join)(dir, entry);
|
|
684
|
+
try {
|
|
685
|
+
const stat = (0, node_fs_1.lstatSync)(entryPath);
|
|
686
|
+
if (stat.isSymbolicLink()) {
|
|
687
|
+
symlinkMounts.add((0, node_fs_1.realpathSync)(entryPath));
|
|
688
|
+
}
|
|
689
|
+
else if (stat.isDirectory()) {
|
|
690
|
+
scanForSymlinks(entryPath);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch { } // permission denied or inaccessible
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
catch { } // dir unreadable
|
|
697
|
+
};
|
|
698
|
+
for (let i = 0; i < agentArgs.length; i++) {
|
|
699
|
+
if (agentArgs[i] === "-v" && agentArgs[i + 1]) {
|
|
700
|
+
const hostPath = agentArgs[i + 1].split(":")[0];
|
|
701
|
+
if ((0, node_fs_1.existsSync)(hostPath) && (0, node_fs_1.lstatSync)(hostPath).isDirectory()) {
|
|
702
|
+
scanForSymlinks(hostPath);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// Agent names for label
|
|
707
|
+
const agentNames = requestedAgents.map((a) => a.name).join(",") || "none";
|
|
708
|
+
// Generate sandbox name
|
|
709
|
+
const now = new Date();
|
|
710
|
+
const name = `vibebox-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
|
|
711
|
+
// ---- SSH ----
|
|
712
|
+
// Windows named pipes (\\.\pipe\...) can't be mounted with Docker -v
|
|
713
|
+
const sshAuthSock = process.env.SSH_AUTH_SOCK;
|
|
714
|
+
const sshArgs = sshAuthSock && process.platform !== "win32" && (0, node_fs_1.existsSync)(sshAuthSock)
|
|
715
|
+
? ["-v", `${sshAuthSock}:/ssh-agent`, "-e", "SSH_AUTH_SOCK=/ssh-agent"]
|
|
716
|
+
: [];
|
|
717
|
+
// Global vibebox config dir (for setup.sh)
|
|
718
|
+
const vibeboxGlobalDir = (0, node_path_1.join)(home, ".vibebox");
|
|
719
|
+
const vibeboxGlobalArgs = (0, node_fs_1.existsSync)(vibeboxGlobalDir)
|
|
720
|
+
? ["-v", `${vibeboxGlobalDir}:${vibeboxGlobalDir}:ro`]
|
|
721
|
+
: [];
|
|
722
|
+
// ---- GitHub CLI ----
|
|
723
|
+
const ghConfigDir = process.platform === "win32" && process.env.APPDATA
|
|
724
|
+
? (0, node_path_1.join)(process.env.APPDATA, "GitHub CLI")
|
|
725
|
+
: (0, node_path_1.join)(home, ".config", "gh");
|
|
726
|
+
const ghArgs = (0, node_fs_1.existsSync)((0, node_path_1.join)(ghConfigDir, "hosts.yml"))
|
|
727
|
+
? ["-v", `${ghConfigDir}:${(0, node_path_1.join)(home, ".config", "gh")}:ro`]
|
|
728
|
+
: [];
|
|
729
|
+
// ---- Git config ----
|
|
730
|
+
// Read effective git config from workspace context (respects includeIf)
|
|
731
|
+
// Uses execFileSync to avoid shell injection
|
|
732
|
+
const gitEnvArgs = [];
|
|
733
|
+
const safeGitConfigs = ["user.name", "user.email", "init.defaultBranch", "push.autoSetupRemote", "pull.rebase", "help.autocorrect"];
|
|
734
|
+
for (const key of safeGitConfigs) {
|
|
735
|
+
try {
|
|
736
|
+
// Try workspace context first (respects includeIf), fall back to global
|
|
737
|
+
let val = "";
|
|
738
|
+
try {
|
|
739
|
+
val = (0, node_child_process_1.execFileSync)("git", ["-C", workspace, "config", key], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
val = (0, node_child_process_1.execFileSync)("git", ["config", "--global", key], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
743
|
+
}
|
|
744
|
+
if (val)
|
|
745
|
+
gitEnvArgs.push("-e", `VIBEBOX_GIT_${key.replace(/\./g, "_").toUpperCase()}=${val}`);
|
|
746
|
+
}
|
|
747
|
+
catch { } // config not set
|
|
748
|
+
}
|
|
749
|
+
// Git aliases (pass as single JSON env var)
|
|
750
|
+
try {
|
|
751
|
+
const aliasOut = (0, node_child_process_1.execFileSync)("git", ["config", "--global", "--get-regexp", "^alias\\."], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
752
|
+
if (aliasOut) {
|
|
753
|
+
const aliases = {};
|
|
754
|
+
for (const line of aliasOut.split("\n")) {
|
|
755
|
+
const match = line.match(/^alias\.(\S+)\s+(.+)$/);
|
|
756
|
+
if (match)
|
|
757
|
+
aliases[match[1]] = match[2];
|
|
758
|
+
}
|
|
759
|
+
gitEnvArgs.push("-e", `VIBEBOX_GIT_ALIASES=${JSON.stringify(aliases)}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch { } // no aliases
|
|
763
|
+
// ---- Docker run ----
|
|
764
|
+
const args = [
|
|
765
|
+
"run", "-d",
|
|
766
|
+
"--name", name,
|
|
767
|
+
// Labels for container lookup
|
|
768
|
+
"--label", "docker/sandbox=true",
|
|
769
|
+
"--label", `com.docker.sandbox.agent=${agentNames}`,
|
|
770
|
+
"--label", `com.docker.sandbox.workingDirectory=${workspace}`,
|
|
771
|
+
"--label", `com.docker.sandbox.workingDirectoryInode=${inode}`,
|
|
772
|
+
// Port mappings (dynamic allocation)
|
|
773
|
+
...ports.flatMap((p) => ["-p", p]),
|
|
774
|
+
// SSH agent forwarding
|
|
775
|
+
...sshArgs,
|
|
776
|
+
// Global vibebox config
|
|
777
|
+
...vibeboxGlobalArgs,
|
|
778
|
+
// GitHub CLI config
|
|
779
|
+
...ghArgs,
|
|
780
|
+
// Environment
|
|
781
|
+
"-e", `TZ=${process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone}`,
|
|
782
|
+
"-e", `VIBEBOX_PROJECT_ROOT=${workspace}`,
|
|
783
|
+
// Git config env vars
|
|
784
|
+
...gitEnvArgs,
|
|
785
|
+
// Agent-specific mounts
|
|
786
|
+
...agentArgs,
|
|
787
|
+
// Symlink targets from mounted directories
|
|
788
|
+
...[...symlinkMounts].flatMap((p) => ["-v", `${p}:${p}`]),
|
|
789
|
+
// Mount workspace at same path
|
|
790
|
+
"-v", `${workspace}:${workspace}`,
|
|
791
|
+
"-w", workspace,
|
|
792
|
+
imageTag,
|
|
793
|
+
// Run startup script with watcher
|
|
794
|
+
`${home}/.local/bin/startup.sh`, "bash", "-c", `mkdir -p ${LOCKS_DIR} && touch ${DETACHED_LOCK} && ${WATCHER_SCRIPT} ${IDLE_TIMEOUT}`,
|
|
795
|
+
];
|
|
796
|
+
const result = (0, node_child_process_1.spawnSync)("docker", args, { stdio: "pipe", encoding: "utf-8" });
|
|
797
|
+
if (result.status !== 0) {
|
|
798
|
+
console.error("Failed to create sandbox:", result.stderr || result.stdout);
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
const created = getSandboxName(workspace);
|
|
802
|
+
if (!created) {
|
|
803
|
+
console.error("Failed to create sandbox");
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
writePortMappings(workspace, created);
|
|
807
|
+
return created;
|
|
808
|
+
}
|
|
809
|
+
async function ensureDocker() {
|
|
810
|
+
try {
|
|
811
|
+
(0, node_child_process_1.execSync)("docker --version", { stdio: "pipe" });
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
console.error("Docker not installed.\nDownload: https://www.docker.com/products/docker-desktop/");
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
(0, node_child_process_1.execSync)("docker info", { stdio: "pipe" });
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
catch { } // not running, try to start
|
|
822
|
+
const cmds = { darwin: "open -gj -a Docker", win32: 'start /min "" "Docker Desktop"' };
|
|
823
|
+
const launchCmd = cmds[process.platform];
|
|
824
|
+
if (!launchCmd) {
|
|
825
|
+
console.error("Docker not running.");
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
console.log("Starting Docker...");
|
|
829
|
+
try {
|
|
830
|
+
(0, node_child_process_1.spawnSync)(launchCmd, { stdio: "pipe", shell: true });
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
console.error("Could not start Docker.");
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
for (let i = 0; i < 60; i++) {
|
|
837
|
+
try {
|
|
838
|
+
(0, node_child_process_1.execSync)("docker info", { stdio: "pipe" });
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
catch { } // still starting
|
|
842
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
843
|
+
}
|
|
844
|
+
console.error("Docker failed to start.");
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
async function promptKeep(dir) {
|
|
848
|
+
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
849
|
+
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
850
|
+
const now = new Date();
|
|
851
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
852
|
+
const month = now.toLocaleString("en-US", { month: "short" });
|
|
853
|
+
const year = now.getFullYear();
|
|
854
|
+
const hour = String(now.getHours()).padStart(2, "0");
|
|
855
|
+
const minute = String(now.getMinutes()).padStart(2, "0");
|
|
856
|
+
const defaultName = `claude-session-${day}${month}${year}${hour}${minute}`;
|
|
857
|
+
const defaultPath = `~/${defaultName}`;
|
|
858
|
+
const keep = await ask("\nKeep workspace? [y/N]: ");
|
|
859
|
+
if (keep.toLowerCase() === "y") {
|
|
860
|
+
const dest = await ask(`Copy to [${defaultPath}]: `);
|
|
861
|
+
const finalDest = dest.trim() || defaultPath;
|
|
862
|
+
const resolvedDest = finalDest.startsWith("~/") ? (0, node_path_1.join)((0, node_os_1.homedir)(), finalDest.slice(2)) : finalDest;
|
|
863
|
+
if (resolvedDest.startsWith("/") && !(0, node_fs_1.existsSync)(resolvedDest)) {
|
|
864
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(resolvedDest), { recursive: true });
|
|
865
|
+
(0, node_fs_1.cpSync)(dir, resolvedDest, { recursive: true });
|
|
866
|
+
(0, node_fs_1.rmSync)(dir, { recursive: true });
|
|
867
|
+
console.log(`Saved to ${resolvedDest}`);
|
|
868
|
+
}
|
|
869
|
+
else if ((0, node_fs_1.existsSync)(resolvedDest)) {
|
|
870
|
+
console.error(`Path already exists: ${resolvedDest}`);
|
|
871
|
+
(0, node_fs_1.rmSync)(dir, { recursive: true });
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
console.error("Invalid path (must be absolute)");
|
|
875
|
+
(0, node_fs_1.rmSync)(dir, { recursive: true });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
(0, node_fs_1.rmSync)(dir, { recursive: true });
|
|
880
|
+
console.log("Deleted");
|
|
881
|
+
}
|
|
882
|
+
rl.close();
|
|
883
|
+
}
|