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/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
+ }