yolobox 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +144 -40
  2. package/dist/index.js +730 -111
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -1,75 +1,179 @@
1
1
  # yolobox
2
2
 
3
- Run Claude Code in Docker containers with `--dangerously-skip-permissions`. Each yolobox gets its own git worktree and branch, so multiple AI agents can work on the same repo simultaneously without conflicts.
3
+ Run [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with `--dangerously-skip-permissions` in disposable Docker containers. Spin up as many as you want, let them run wild, merge the results.
4
4
 
5
5
  ```bash
6
- yolobox run # Interactive Claude session
7
- yolobox run -p "fix the login bug" # Start Claude with a prompt
8
- yolobox run --shell # Drop into bash instead of Claude
9
- yolobox run --name cool-tiger # Use a specific ID instead of random
6
+ yolobox claude # Launch Claude with --dangerously-skip-permissions
7
+ yolobox claude -p "fix the login bug" # Launch with a prompt
8
+ yolobox start # Drop into a shell instead
9
+ yolobox ls # List running boxes
10
+ yolobox attach # Reattach to a running box
11
+ yolobox kill # Stop a box (keeps branch)
12
+ yolobox rm # Remove box + branch + worktree
13
+ yolobox nuke # Scorched earth — remove everything
10
14
  ```
11
15
 
12
- ---
16
+ ## Install
13
17
 
14
- ## Development
18
+ ```bash
19
+ npm install -g yolobox
20
+ ```
21
+
22
+ Requires Docker and Node.js 18+.
23
+
24
+ ## What's a yolobox?
25
+
26
+ A yolobox is three things, created together as a single unit:
27
+
28
+ - **Docker container** — Claude can install packages, delete files, run arbitrary commands. Your host machine doesn't feel a thing.
29
+ - **Git worktree** — an isolated copy of your codebase at `.yolobox/<id>`, separate from your working directory and from every other box.
30
+ - **Git branch** — a `yolo/<id>` branch forked from HEAD. Every change is tracked and easy to review, merge, or throw away.
31
+
32
+ Create a box, get all three. Remove a box, all three are cleaned up.
15
33
 
16
- ### Setup
34
+ `--dangerously-skip-permissions` makes Claude Code dramatically more productive, but running it on your actual machine requires a level of trust that borders on spiritual. And running *multiple* instances on the same repo? They'd all be editing the same files and tripping over each other.
35
+
36
+ yolobox gives each Claude instance its own container (safety) and its own worktree (isolation). Open a few terminals, fire off parallel agents, and review the branches when they're done. Keep what works, toss what doesn't.
37
+
38
+ ## Commands
39
+
40
+ ### `yolobox claude [name] [-p <prompt>]`
41
+
42
+ The main event. Spins up a fresh yolobox and drops you straight into Claude Code with `--dangerously-skip-permissions`. Full send.
17
43
 
18
44
  ```bash
19
- # Install dependencies
20
- npm install
45
+ yolobox claude # Interactive Claude session
46
+ yolobox claude -p "refactor auth to JWT" # Give Claude a prompt and watch it go
47
+ yolobox claude my-feature # Use a custom name instead of a random one
48
+ ```
49
+
50
+ ### `yolobox start [name]`
21
51
 
22
- # Build the Docker image (only needed once, takes ~5 min)
23
- npm run docker:build
52
+ Same container setup as `claude`, but drops you into a bash shell instead. For when you want to poke around, run tests, or do your own yoloing.
24
53
 
25
- # Link the CLI globally so you can use the `yolobox` command
26
- npm link
54
+ ```bash
55
+ yolobox start # Shell in a new yolobox
56
+ yolobox start my-feature # Named yolobox — reattaches if already running
27
57
  ```
28
58
 
29
- ### Testing
59
+ If you give it a name that already exists, it'll reattach to the existing container instead of creating a new one. No wasted containers, just vibes.
60
+
61
+ ### `yolobox attach [id]`
30
62
 
31
- **End-to-end test:**
63
+ Jump back into a running yolobox. If you don't specify an ID, you get a slick interactive picker. If the container was stopped, it'll restart it for you automatically — because who has time to babysit Docker.
32
64
 
33
65
  ```bash
34
- # In this repo (or any git repo)
35
- yolobox run --shell
66
+ yolobox attach # Pick from running containers
67
+ yolobox attach swift-falcon # Attach to a specific one
36
68
  ```
37
69
 
38
- This will:
39
- - ✓ Check Docker is running
40
- - Check you're in a git repo
41
- - ✓ Generate a random ID (e.g., `swift-falcon`)
42
- - ✓ Create `.yolobox/swift-falcon/` worktree
43
- - ✓ Launch you into a bash shell inside the container
70
+ ### `yolobox ls`
71
+
72
+ See what's running. Shows ID, branch, status, age, and which repo each container belongs to.
44
73
 
45
- Once inside the container, verify:
46
74
  ```bash
47
- pwd # Should be /workspace
48
- git branch # Should show your yolobox branch
49
- git config user.name # Should show your host identity
50
- ssh-add -l # Should show your SSH keys (on macOS)
75
+ yolobox ls
76
+ ```
77
+
78
+ ```
79
+ ID BRANCH STATUS CREATED PATH
80
+ swift-falcon yolo/swift-falcon running 5 min ago ~/code/myproject
81
+ bold-otter yolo/bold-otter stopped 2h ago ~/code/myproject
51
82
  ```
52
83
 
53
- Type `exit` to leave the container.
84
+ ### `yolobox kill [id]`
54
85
 
55
- **Quick verification without Docker:**
86
+ Stop and remove a container. The worktree and branch stick around so you can still see what happened.
56
87
 
57
88
  ```bash
58
- npm run build # Compile TypeScript
59
- node bin/yolobox.js --help
60
- node bin/yolobox.js run --help
61
- npm test # Run unit tests (18 tests)
89
+ yolobox kill # Pick one to kill
90
+ yolobox kill swift-falcon # Kill a specific container
62
91
  ```
63
92
 
64
- ### Build & Watch
93
+ ### `yolobox rm [id]`
94
+
95
+ The full cleanup. Kills the container, removes the git worktree, and deletes the `yolo/<id>` branch. Like it never happened.
96
+
97
+ ```bash
98
+ yolobox rm # Pick one to remove
99
+ yolobox rm swift-falcon # Remove everything for swift-falcon
100
+ ```
101
+
102
+ ### `yolobox nuke [--all]`
103
+
104
+ For when you want to start completely fresh. Shows you everything that exists (containers, branches, worktrees) and lets you choose between killing just containers or going full scorched-earth. No questions asked. (*)
105
+
106
+ ```bash
107
+ yolobox nuke # Nuke everything in the current repo
108
+ yolobox nuke --all # Nuke yolobox containers from ALL repos
109
+ ```
110
+
111
+ (*) Actually we do ask for confirmation.
112
+
113
+ ## Under the hood
114
+
115
+ When you create a yolobox, the CLI:
116
+
117
+ 1. **Creates a git worktree** at `.yolobox/<id>` on a new `yolo/<id>` branch forked from HEAD
118
+ 2. **Launches a Docker container** that mounts the worktree as `/workspace` and shares your repo's `.git` directory
119
+ 3. **Forwards your git identity** so commits show up as you
120
+ 4. **Injects your Claude auth token** so there's no login prompt
121
+
122
+ ## Authentication
123
+
124
+ Each container needs Claude Code authentication, but you only need to configure your token once — yolobox automatically injects it into every new container:
125
+
126
+ ```bash
127
+ # Step 1: Generate a long-lived token
128
+ claude setup-token
129
+
130
+ # Step 2: Store it in yolobox
131
+ yolobox auth <token>
132
+ ```
133
+
134
+ Or if you have the `CLAUDE_CODE_OAUTH_TOKEN` env var set:
135
+
136
+ ```bash
137
+ yolobox auth # Automatically picks up the env var
138
+ ```
139
+
140
+ ```bash
141
+ yolobox auth --status # Check current auth status
142
+ yolobox auth --remove # Remove stored token
143
+ ```
144
+
145
+ The token is saved to `~/.yolobox/auth.json` and automatically injected into every new container.
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+ ## Development
154
+
155
+ ```bash
156
+ npm install # Install dependencies
157
+ npm run docker:build # Build the Docker image locally (~5 min, one-time)
158
+ npm link # Link the CLI globally
159
+
160
+ npm run build # One-shot TypeScript build
161
+ npm run dev # Watch mode
162
+ npm test # Run tests
163
+ ```
164
+
165
+ ### Custom Docker image
166
+
167
+ Want to bring your own container? Set the `YOLOBOX_IMAGE` env var:
65
168
 
66
169
  ```bash
67
- npm run build # One-shot build
68
- npm run dev # Watch mode (rebuild on change)
69
- npm test # Run tests
170
+ YOLOBOX_IMAGE=my-custom-image:latest yolobox claude
70
171
  ```
71
172
 
72
- ---
173
+ Image resolution order:
174
+ 1. `YOLOBOX_IMAGE` env var (if set)
175
+ 2. `yolobox:local` (if you've built it locally)
176
+ 3. `ghcr.io/roginn/yolobox:latest` (default)
73
177
 
74
178
  ## License
75
179
 
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  // src/index.ts
2
- import { defineCommand as defineCommand7, runMain } from "citty";
2
+ import { defineCommand as defineCommand10, runMain } from "citty";
3
3
 
4
- // src/commands/claude.ts
4
+ // src/commands/attach.ts
5
5
  import { defineCommand } from "citty";
6
6
 
7
7
  // src/lib/docker.ts
8
8
  import { execSync, spawnSync } from "child_process";
9
+ import { existsSync } from "fs";
10
+ import { join } from "path";
9
11
  function isDockerRunning() {
10
12
  try {
11
13
  execSync("docker info", { stdio: ["pipe", "pipe", "pipe"] });
@@ -14,6 +16,37 @@ function isDockerRunning() {
14
16
  return false;
15
17
  }
16
18
  }
19
+ function resolveDockerImage(options) {
20
+ if (options.envImage) {
21
+ return {
22
+ image: options.envImage,
23
+ source: "env"
24
+ };
25
+ }
26
+ if (options.checkLocalImage !== false && imageExists("yolobox:local")) {
27
+ return {
28
+ image: "yolobox:local",
29
+ source: "local"
30
+ };
31
+ }
32
+ return {
33
+ image: "ghcr.io/roginn/yolobox:latest",
34
+ source: "ghcr"
35
+ };
36
+ }
37
+ function imageExists(imageName) {
38
+ try {
39
+ execSync(`docker image inspect ${imageName}`, {
40
+ stdio: ["pipe", "pipe", "pipe"]
41
+ });
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ function isYoloboxDevRepo(repoRoot) {
48
+ return existsSync(join(repoRoot, "docker", "Dockerfile"));
49
+ }
17
50
  function buildDockerArgs(opts) {
18
51
  const args = [
19
52
  "run",
@@ -39,6 +72,9 @@ function buildDockerArgs(opts) {
39
72
  args.push("-e", `GIT_AUTHOR_EMAIL=${opts.gitIdentity.email}`);
40
73
  args.push("-e", `GIT_COMMITTER_EMAIL=${opts.gitIdentity.email}`);
41
74
  }
75
+ if (opts.claudeOauthToken) {
76
+ args.push("-e", `CLAUDE_CODE_OAUTH_TOKEN=${opts.claudeOauthToken}`);
77
+ }
42
78
  args.push(opts.image);
43
79
  args.push("sleep", "infinity");
44
80
  return args;
@@ -51,6 +87,12 @@ function startContainer(opts) {
51
87
  const result = spawnSync("docker", args, { stdio: ["pipe", "pipe", "pipe"] });
52
88
  return result.status === 0;
53
89
  }
90
+ function restartContainer(id) {
91
+ const result = spawnSync("docker", ["start", `yolobox-${id}`], {
92
+ stdio: ["pipe", "pipe", "pipe"]
93
+ });
94
+ return result.status === 0;
95
+ }
54
96
  function execInContainer(id, command) {
55
97
  const args = buildExecArgs(id, command);
56
98
  const result = spawnSync("docker", args, { stdio: "inherit" });
@@ -67,6 +109,16 @@ function timeAgo(dateStr) {
67
109
  const days = Math.floor(hours / 24);
68
110
  return `${days}d ago`;
69
111
  }
112
+ function getWorktreeBranch(repoPath, id) {
113
+ try {
114
+ const worktreePath = join(repoPath, ".yolobox", id);
115
+ return execSync(`git -C "${worktreePath}" rev-parse --abbrev-ref HEAD`, {
116
+ encoding: "utf-8"
117
+ }).trim();
118
+ } catch {
119
+ return `yolo/${id}`;
120
+ }
121
+ }
70
122
  function listContainers() {
71
123
  try {
72
124
  const result = execSync(
@@ -74,14 +126,14 @@ function listContainers() {
74
126
  { encoding: "utf-8" }
75
127
  );
76
128
  return result.trim().split("\n").filter(Boolean).map((line) => {
77
- const [name, status, created, path2] = line.split(" ");
129
+ const [name, status, created, path3] = line.split(" ");
78
130
  const id = name.replace(/^yolobox-/, "");
79
131
  return {
80
132
  id,
81
- branch: id,
133
+ branch: getWorktreeBranch(path3, id),
82
134
  status: status.startsWith("Up") ? "running" : "stopped",
83
135
  created: timeAgo(created),
84
- path: path2 || ""
136
+ path: path3 || ""
85
137
  };
86
138
  });
87
139
  } catch {
@@ -99,6 +151,264 @@ function killContainer(id) {
99
151
  return rm.status === 0;
100
152
  }
101
153
 
154
+ // src/lib/ui.ts
155
+ import * as p from "@clack/prompts";
156
+ import pc from "picocolors";
157
+ function intro2() {
158
+ p.intro(pc.bgCyan(pc.black(` yolobox v${"0.1.2"} `)));
159
+ }
160
+ function success(message) {
161
+ p.log.success(message);
162
+ }
163
+ function error(message) {
164
+ p.log.error(pc.red(message));
165
+ }
166
+ function warn(message) {
167
+ p.log.warn(pc.yellow(message));
168
+ }
169
+ function info(message) {
170
+ p.log.info(message);
171
+ }
172
+ function outro2(message) {
173
+ p.outro(message);
174
+ }
175
+
176
+ // src/commands/attach.ts
177
+ var attach_default = defineCommand({
178
+ meta: {
179
+ name: "attach",
180
+ description: "Attach a shell to a running yolobox container"
181
+ },
182
+ args: {
183
+ id: {
184
+ type: "positional",
185
+ description: "The yolobox ID to attach to (interactive picker if omitted)",
186
+ required: false
187
+ }
188
+ },
189
+ run: async ({ args }) => {
190
+ if (!isDockerRunning()) {
191
+ error("Docker is not running.");
192
+ return process.exit(1);
193
+ }
194
+ let id = args.id;
195
+ if (!id) {
196
+ const containers = listContainers().filter((c) => c.status === "running");
197
+ if (containers.length === 0) {
198
+ error("No running yolobox containers found.");
199
+ return process.exit(1);
200
+ }
201
+ if (containers.length === 1) {
202
+ id = containers[0].id;
203
+ } else {
204
+ const selected = await p.select({
205
+ message: "Pick a container to attach to",
206
+ options: containers.map((c) => ({
207
+ value: c.id,
208
+ label: c.id,
209
+ hint: c.path
210
+ }))
211
+ });
212
+ if (p.isCancel(selected)) return process.exit(0);
213
+ id = selected;
214
+ }
215
+ } else {
216
+ const containers = listContainers();
217
+ const match = containers.find((c) => c.id === id);
218
+ if (!match) {
219
+ error(`No yolobox container found with ID "${id}".`);
220
+ return process.exit(1);
221
+ }
222
+ if (match.status !== "running") {
223
+ info(`Restarting stopped container "${id}"...`);
224
+ if (!restartContainer(id)) {
225
+ error(`Failed to restart container "${id}".`);
226
+ return process.exit(1);
227
+ }
228
+ }
229
+ }
230
+ outro2(`Attaching to ${id}...`);
231
+ const exitCode = execInContainer(id, ["bash"]);
232
+ process.exit(exitCode);
233
+ }
234
+ });
235
+
236
+ // src/commands/auth.ts
237
+ import { defineCommand as defineCommand2 } from "citty";
238
+
239
+ // src/lib/auth.ts
240
+ import {
241
+ existsSync as existsSync2,
242
+ mkdirSync,
243
+ readFileSync,
244
+ unlinkSync,
245
+ writeFileSync
246
+ } from "fs";
247
+ import { homedir } from "os";
248
+ import { join as join2 } from "path";
249
+ var AUTH_DIR_NAME = ".yolobox";
250
+ var AUTH_FILE_NAME = "auth.json";
251
+ function getAuthDir() {
252
+ return join2(homedir(), AUTH_DIR_NAME);
253
+ }
254
+ function getAuthFilePath() {
255
+ return join2(getAuthDir(), AUTH_FILE_NAME);
256
+ }
257
+ function saveToken(token) {
258
+ const dir = getAuthDir();
259
+ if (!existsSync2(dir)) {
260
+ mkdirSync(dir, { mode: 448, recursive: true });
261
+ }
262
+ const data = JSON.stringify({ claudeOauthToken: token }, null, 2);
263
+ writeFileSync(getAuthFilePath(), `${data}
264
+ `, { mode: 384 });
265
+ }
266
+ function loadToken() {
267
+ const filePath = getAuthFilePath();
268
+ if (!existsSync2(filePath)) {
269
+ return null;
270
+ }
271
+ try {
272
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
273
+ return data.claudeOauthToken || null;
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+ function removeToken() {
279
+ const filePath = getAuthFilePath();
280
+ if (!existsSync2(filePath)) {
281
+ return false;
282
+ }
283
+ unlinkSync(filePath);
284
+ return true;
285
+ }
286
+ function resolveToken() {
287
+ const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
288
+ if (envToken) {
289
+ return envToken;
290
+ }
291
+ return loadToken();
292
+ }
293
+ function isValidToken(token) {
294
+ return token.startsWith("sk-ant-");
295
+ }
296
+ function maskToken(token) {
297
+ if (token.length <= 12) {
298
+ return "***";
299
+ }
300
+ return `${token.slice(0, 10)}...${token.slice(-4)}`;
301
+ }
302
+
303
+ // src/commands/auth.ts
304
+ var auth_default = defineCommand2({
305
+ meta: {
306
+ name: "auth",
307
+ description: "Configure Claude Code authentication for containers"
308
+ },
309
+ args: {
310
+ token: {
311
+ type: "positional",
312
+ description: "OAuth token from `claude setup-token`",
313
+ required: false
314
+ },
315
+ remove: {
316
+ type: "boolean",
317
+ description: "Remove stored token",
318
+ default: false
319
+ },
320
+ status: {
321
+ type: "boolean",
322
+ description: "Show current auth status",
323
+ default: false
324
+ }
325
+ },
326
+ run: async ({ args }) => {
327
+ intro2();
328
+ if (args.status) {
329
+ showStatus();
330
+ return;
331
+ }
332
+ if (args.remove) {
333
+ const removed = removeToken();
334
+ if (removed) {
335
+ success("Auth token removed.");
336
+ } else {
337
+ warn("No stored token found.");
338
+ }
339
+ return;
340
+ }
341
+ if (args.token) {
342
+ const token = args.token;
343
+ if (!isValidToken(token)) {
344
+ error(
345
+ 'Invalid token. Expected a token starting with "sk-ant-".\nRun `claude setup-token` to generate a valid token.'
346
+ );
347
+ process.exit(1);
348
+ }
349
+ saveToken(token);
350
+ success(`Token saved. (${maskToken(token)})`);
351
+ info("Claude will authenticate automatically in new containers.");
352
+ return;
353
+ }
354
+ const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
355
+ if (envToken) {
356
+ if (!isValidToken(envToken)) {
357
+ error(
358
+ "CLAUDE_CODE_OAUTH_TOKEN is set but does not look like a valid token."
359
+ );
360
+ process.exit(1);
361
+ }
362
+ saveToken(envToken);
363
+ success(
364
+ `Token captured from CLAUDE_CODE_OAUTH_TOKEN. (${maskToken(envToken)})`
365
+ );
366
+ info("Claude will authenticate automatically in new containers.");
367
+ return;
368
+ }
369
+ const existing = loadToken();
370
+ if (existing) {
371
+ success(`Already authenticated. (${maskToken(existing)})`);
372
+ info(
373
+ "Run `yolobox auth --status` for details or `yolobox auth --remove` to clear."
374
+ );
375
+ return;
376
+ }
377
+ info("Set up Claude Code authentication for yolobox containers.\n");
378
+ info("Step 1: Generate a token on your host machine:");
379
+ info(" $ claude setup-token\n");
380
+ info("Step 2: Pass the token to yolobox:");
381
+ info(" $ yolobox auth <token>\n");
382
+ info("Or set the CLAUDE_CODE_OAUTH_TOKEN env var and run:");
383
+ info(" $ export CLAUDE_CODE_OAUTH_TOKEN=<token>");
384
+ info(" $ yolobox auth");
385
+ }
386
+ });
387
+ function showStatus() {
388
+ const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
389
+ const storedToken = loadToken();
390
+ const activeToken = resolveToken();
391
+ if (!activeToken) {
392
+ warn("Not authenticated. Run `yolobox auth` for setup instructions.");
393
+ return;
394
+ }
395
+ success(`Authenticated. (${maskToken(activeToken)})`);
396
+ if (envToken) {
397
+ info("Source: CLAUDE_CODE_OAUTH_TOKEN environment variable");
398
+ }
399
+ if (storedToken) {
400
+ info(
401
+ envToken ? `Stored token also available (${maskToken(storedToken)})` : "Source: ~/.yolobox/auth.json"
402
+ );
403
+ }
404
+ }
405
+
406
+ // src/commands/claude.ts
407
+ import { defineCommand as defineCommand3 } from "citty";
408
+
409
+ // src/lib/container-setup.ts
410
+ import path2 from "path";
411
+
102
412
  // src/lib/git.ts
103
413
  import { execSync as execSync2 } from "child_process";
104
414
  function exec(cmd) {
@@ -140,6 +450,22 @@ function hasCommits() {
140
450
  function createInitialCommit() {
141
451
  exec('git commit --allow-empty -m "Initial commit"');
142
452
  }
453
+ function branchExists(branch) {
454
+ try {
455
+ exec(`git rev-parse --verify "refs/heads/${branch}"`);
456
+ return true;
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+ function deleteBranch(branch) {
462
+ try {
463
+ exec(`git branch -D "${branch}"`);
464
+ return true;
465
+ } catch {
466
+ return false;
467
+ }
468
+ }
143
469
  function getGitIdentity() {
144
470
  try {
145
471
  const name = exec("git config user.name");
@@ -857,25 +1183,6 @@ function generateId(existingIds, maxRetries = 100) {
857
1183
  throw new Error("Could not generate a unique ID after 100 attempts");
858
1184
  }
859
1185
 
860
- // src/lib/ui.ts
861
- import * as p from "@clack/prompts";
862
- import pc from "picocolors";
863
- function intro2() {
864
- p.intro(pc.bgCyan(pc.black(" yolobox v0.0.1 ")));
865
- }
866
- function success(message) {
867
- p.log.success(message);
868
- }
869
- function error(message) {
870
- p.log.error(pc.red(message));
871
- }
872
- function info(message) {
873
- p.log.info(message);
874
- }
875
- function outro2(message) {
876
- p.outro(message);
877
- }
878
-
879
1186
  // src/lib/worktree.ts
880
1187
  import { execSync as execSync3 } from "child_process";
881
1188
  import fs from "fs";
@@ -887,11 +1194,19 @@ function exec2(cmd, cwd) {
887
1194
  stdio: ["pipe", "pipe", "pipe"]
888
1195
  }).trim();
889
1196
  }
890
- function createWorktree(repoRoot, id) {
1197
+ function worktreeExists(repoRoot, id) {
1198
+ const worktreePath = path.join(repoRoot, ".yolobox", id);
1199
+ return fs.existsSync(worktreePath);
1200
+ }
1201
+ function createWorktree(repoRoot, id, options) {
891
1202
  const yoloboxDir = path.join(repoRoot, ".yolobox");
892
1203
  fs.mkdirSync(yoloboxDir, { recursive: true });
893
1204
  const worktreePath = path.join(yoloboxDir, id);
894
- exec2(`git worktree add "${worktreePath}" -b "yolo/${id}"`, repoRoot);
1205
+ if (options?.branchExists) {
1206
+ exec2(`git worktree add "${worktreePath}" "yolo/${id}"`, repoRoot);
1207
+ } else {
1208
+ exec2(`git worktree add "${worktreePath}" -b "yolo/${id}"`, repoRoot);
1209
+ }
895
1210
  return worktreePath;
896
1211
  }
897
1212
  function ensureGitignore(repoRoot) {
@@ -908,6 +1223,22 @@ function ensureGitignore(repoRoot) {
908
1223
  `);
909
1224
  }
910
1225
  }
1226
+ function removeWorktree(repoRoot, id) {
1227
+ const worktreePath = path.join(repoRoot, ".yolobox", id);
1228
+ try {
1229
+ exec2(`git worktree remove --force "${worktreePath}"`, repoRoot);
1230
+ return true;
1231
+ } catch {
1232
+ try {
1233
+ exec2("git worktree prune", repoRoot);
1234
+ } catch {
1235
+ }
1236
+ if (fs.existsSync(worktreePath)) {
1237
+ fs.rmSync(worktreePath, { recursive: true, force: true });
1238
+ }
1239
+ return true;
1240
+ }
1241
+ }
911
1242
  function getExistingWorktreeIds(repoRoot) {
912
1243
  const yoloboxDir = path.join(repoRoot, ".yolobox");
913
1244
  if (!fs.existsSync(yoloboxDir)) return [];
@@ -915,14 +1246,12 @@ function getExistingWorktreeIds(repoRoot) {
915
1246
  }
916
1247
 
917
1248
  // src/lib/container-setup.ts
918
- var DOCKER_IMAGE = process.env.YOLOBOX_IMAGE || "yolobox:local";
919
1249
  async function setupContainer(options = {}) {
920
1250
  intro2();
921
1251
  if (!isDockerRunning()) {
922
- error("Docker is not running. Start Docker Desktop and try again.");
1252
+ error("Docker is not running. Start Docker and try again.");
923
1253
  process.exit(1);
924
1254
  }
925
- success("Docker is running");
926
1255
  if (!isInsideGitRepo()) {
927
1256
  const shouldInit = await p.confirm({
928
1257
  message: "No git repo found. Initialize one here?"
@@ -933,8 +1262,6 @@ async function setupContainer(options = {}) {
933
1262
  }
934
1263
  initRepo();
935
1264
  success("Initialized git repo");
936
- } else {
937
- success("Git repo detected");
938
1265
  }
939
1266
  if (!hasCommits()) {
940
1267
  createInitialCommit();
@@ -951,17 +1278,60 @@ async function setupContainer(options = {}) {
951
1278
  const taken = /* @__PURE__ */ new Set([...branches, ...existingWorktrees]);
952
1279
  id = generateId(taken);
953
1280
  }
954
- const worktreePath = createWorktree(repoRoot, id);
955
- success(`Created worktree .yolobox/${id} (branch: ${id})`);
1281
+ if (options.name) {
1282
+ const containers = listContainers();
1283
+ const existing = containers.find((c) => c.id === id);
1284
+ if (existing && existing.status === "running") {
1285
+ error(
1286
+ `Container "${id}" is already running. Use "yolobox attach ${id}" to connect.`
1287
+ );
1288
+ process.exit(1);
1289
+ }
1290
+ if (existing) {
1291
+ killContainer(id);
1292
+ }
1293
+ }
1294
+ let worktreePath;
1295
+ const worktreeAlreadyExists = worktreeExists(repoRoot, id);
1296
+ const branchAlreadyExists = branchExists(`yolo/${id}`);
1297
+ if (worktreeAlreadyExists) {
1298
+ worktreePath = path2.join(repoRoot, ".yolobox", id);
1299
+ success(`Reusing worktree .yolobox/${id} (branch: yolo/${id})`);
1300
+ } else {
1301
+ worktreePath = createWorktree(repoRoot, id, {
1302
+ branchExists: branchAlreadyExists
1303
+ });
1304
+ success(`Created worktree .yolobox/${id} (branch: yolo/${id})`);
1305
+ }
956
1306
  ensureGitignore(repoRoot);
957
1307
  const gitIdentity = getGitIdentity();
1308
+ const imageResolution = resolveDockerImage({
1309
+ envImage: process.env.YOLOBOX_IMAGE
1310
+ });
1311
+ if (imageResolution.source === "env") {
1312
+ info(`Using custom Docker image: ${imageResolution.image}`);
1313
+ } else if (imageResolution.source === "local") {
1314
+ info("Using local Docker image: yolobox:local");
1315
+ } else {
1316
+ info("Using Docker image: ghcr.io/roginn/yolobox:latest");
1317
+ if (isYoloboxDevRepo(repoRoot)) {
1318
+ info("Tip: Run 'npm run docker:build' to use local builds");
1319
+ }
1320
+ }
1321
+ const claudeOauthToken = resolveToken();
1322
+ if (claudeOauthToken) {
1323
+ success("Claude auth token configured");
1324
+ } else {
1325
+ warn('No Claude auth token. Run "yolobox auth" to set up.');
1326
+ }
958
1327
  const started = startContainer({
959
1328
  id,
960
1329
  worktreePath,
961
1330
  gitDir,
962
1331
  gitIdentity,
963
- image: DOCKER_IMAGE,
964
- repoPath: repoRoot
1332
+ image: imageResolution.image,
1333
+ repoPath: repoRoot,
1334
+ claudeOauthToken: claudeOauthToken ?? void 0
965
1335
  });
966
1336
  if (!started) {
967
1337
  error("Failed to start container.");
@@ -971,7 +1341,7 @@ async function setupContainer(options = {}) {
971
1341
  }
972
1342
 
973
1343
  // src/commands/claude.ts
974
- var claude_default = defineCommand({
1344
+ var claude_default = defineCommand3({
975
1345
  meta: {
976
1346
  name: "claude",
977
1347
  description: "Launch Claude Code with skip permissions"
@@ -983,13 +1353,15 @@ var claude_default = defineCommand({
983
1353
  description: "Pass an initial prompt to Claude"
984
1354
  },
985
1355
  name: {
986
- type: "string",
987
- alias: "n",
988
- description: "Use a specific name instead of random"
1356
+ type: "positional",
1357
+ description: "Use a specific name instead of random",
1358
+ required: false
989
1359
  }
990
1360
  },
991
1361
  run: async ({ args }) => {
992
- const { id } = await setupContainer({ name: args.name });
1362
+ const { id } = await setupContainer({
1363
+ name: args.name
1364
+ });
993
1365
  const command = args.prompt ? ["claude", "--dangerously-skip-permissions", "-p", args.prompt] : ["claude", "--dangerously-skip-permissions"];
994
1366
  outro2(`Launching Claude in ${id}...`);
995
1367
  execInContainer(id, command);
@@ -998,8 +1370,8 @@ var claude_default = defineCommand({
998
1370
 
999
1371
  // src/commands/help.ts
1000
1372
  import { spawnSync as spawnSync2 } from "child_process";
1001
- import { defineCommand as defineCommand2 } from "citty";
1002
- var help_default = defineCommand2({
1373
+ import { defineCommand as defineCommand4 } from "citty";
1374
+ var help_default = defineCommand4({
1003
1375
  meta: {
1004
1376
  name: "help",
1005
1377
  description: "Show help information"
@@ -1013,8 +1385,8 @@ var help_default = defineCommand2({
1013
1385
  });
1014
1386
 
1015
1387
  // src/commands/kill.ts
1016
- import { defineCommand as defineCommand3 } from "citty";
1017
- var kill_default = defineCommand3({
1388
+ import { defineCommand as defineCommand5 } from "citty";
1389
+ var kill_default = defineCommand5({
1018
1390
  meta: {
1019
1391
  name: "kill",
1020
1392
  description: "Stop and remove a running yolobox container"
@@ -1022,12 +1394,49 @@ var kill_default = defineCommand3({
1022
1394
  args: {
1023
1395
  id: {
1024
1396
  type: "positional",
1025
- description: "The yolobox ID to kill",
1026
- required: true
1397
+ description: "The yolobox ID to kill (interactive picker if omitted)",
1398
+ required: false
1027
1399
  }
1028
1400
  },
1029
1401
  run: async ({ args }) => {
1030
- const id = args.id;
1402
+ if (!isDockerRunning()) {
1403
+ error("Docker is not running.");
1404
+ return process.exit(1);
1405
+ }
1406
+ let id = args.id;
1407
+ if (!id) {
1408
+ const containers = listContainers();
1409
+ if (containers.length === 0) {
1410
+ error("No yolobox containers found.");
1411
+ return process.exit(1);
1412
+ }
1413
+ const selected = await p.select({
1414
+ message: "Pick a container to kill",
1415
+ options: [
1416
+ ...containers.map((c) => ({
1417
+ value: c.id,
1418
+ label: c.id,
1419
+ hint: `${c.status} \u2022 ${c.path}`
1420
+ })),
1421
+ {
1422
+ value: "__cancel__",
1423
+ label: "Cancel",
1424
+ hint: "Exit without killing"
1425
+ }
1426
+ ]
1427
+ });
1428
+ if (p.isCancel(selected) || selected === "__cancel__") {
1429
+ return process.exit(0);
1430
+ }
1431
+ id = selected;
1432
+ } else {
1433
+ const containers = listContainers();
1434
+ const match = containers.find((c) => c.id === id);
1435
+ if (!match) {
1436
+ error(`No yolobox container found with ID "${id}".`);
1437
+ return process.exit(1);
1438
+ }
1439
+ }
1031
1440
  const killed = killContainer(id);
1032
1441
  if (!killed) {
1033
1442
  error(`Failed to kill yolobox-${id}. Is it running?`);
@@ -1038,11 +1447,11 @@ var kill_default = defineCommand3({
1038
1447
  });
1039
1448
 
1040
1449
  // src/commands/ls.ts
1041
- import { homedir } from "os";
1042
- import { defineCommand as defineCommand4 } from "citty";
1043
- function shortenPath(path2, maxLen = 40) {
1044
- const home = homedir();
1045
- const display = path2.startsWith(home) ? `~${path2.slice(home.length)}` : path2;
1450
+ import { homedir as homedir2 } from "os";
1451
+ import { defineCommand as defineCommand6 } from "citty";
1452
+ function shortenPath(path3, maxLen = 40) {
1453
+ const home = homedir2();
1454
+ const display = path3.startsWith(home) ? `~${path3.slice(home.length)}` : path3;
1046
1455
  if (display.length <= maxLen) return display;
1047
1456
  const parts = display.split("/");
1048
1457
  let short = "";
@@ -1053,7 +1462,7 @@ function shortenPath(path2, maxLen = 40) {
1053
1462
  }
1054
1463
  return short ? `\u2026/${short}` : `\u2026/${parts[parts.length - 1]}`;
1055
1464
  }
1056
- var ls_default = defineCommand4({
1465
+ var ls_default = defineCommand6({
1057
1466
  meta: {
1058
1467
  name: "ls",
1059
1468
  description: "List running yolobox containers"
@@ -1086,11 +1495,11 @@ var ls_default = defineCommand4({
1086
1495
  });
1087
1496
 
1088
1497
  // src/commands/nuke.ts
1089
- import { defineCommand as defineCommand5 } from "citty";
1090
- var nuke_default = defineCommand5({
1498
+ import { defineCommand as defineCommand7 } from "citty";
1499
+ var nuke_default = defineCommand7({
1091
1500
  meta: {
1092
1501
  name: "nuke",
1093
- description: "Kill all yolobox containers from the current directory"
1502
+ description: "Kill all yolobox containers, branches, and worktrees"
1094
1503
  },
1095
1504
  args: {
1096
1505
  all: {
@@ -1100,111 +1509,321 @@ var nuke_default = defineCommand5({
1100
1509
  }
1101
1510
  },
1102
1511
  run: async ({ args }) => {
1103
- intro2("yolobox nuke");
1104
- if (!isDockerRunning()) {
1105
- error("Docker is not running. Please start Docker and try again.");
1106
- process.exit(1);
1512
+ intro2();
1513
+ const inGitRepo = isInsideGitRepo();
1514
+ const repoRoot = inGitRepo ? getRepoRoot() : null;
1515
+ const dockerRunning = isDockerRunning();
1516
+ let containers = [];
1517
+ if (dockerRunning) {
1518
+ const allContainers = listContainers();
1519
+ if (args.all) {
1520
+ containers = allContainers;
1521
+ } else if (repoRoot) {
1522
+ containers = allContainers.filter((c) => c.path === repoRoot);
1523
+ }
1107
1524
  }
1108
- const allContainers = listContainers();
1109
- let matchingContainers;
1110
- let locationDescription;
1111
- if (args.all) {
1112
- matchingContainers = allContainers;
1113
- locationDescription = "across all directories";
1114
- } else {
1115
- const currentPath = isInsideGitRepo() ? getRepoRoot() : process.cwd();
1116
- matchingContainers = allContainers.filter(
1117
- (container) => container.path === currentPath
1525
+ let yoloBranchIds = /* @__PURE__ */ new Set();
1526
+ let worktreeIds = /* @__PURE__ */ new Set();
1527
+ if (repoRoot) {
1528
+ yoloBranchIds = new Set(
1529
+ getBranches().filter((b) => b.startsWith("yolo/")).map((b) => b.slice("yolo/".length))
1118
1530
  );
1119
- locationDescription = `from ${currentPath}`;
1531
+ worktreeIds = new Set(getExistingWorktreeIds(repoRoot));
1532
+ }
1533
+ const targetMap = /* @__PURE__ */ new Map();
1534
+ for (const c of containers) {
1535
+ targetMap.set(c.id, {
1536
+ id: c.id,
1537
+ container: { status: c.status, path: c.path },
1538
+ hasBranch: yoloBranchIds.has(c.id),
1539
+ hasWorktree: worktreeIds.has(c.id)
1540
+ });
1541
+ }
1542
+ for (const id of /* @__PURE__ */ new Set([...yoloBranchIds, ...worktreeIds])) {
1543
+ if (!targetMap.has(id)) {
1544
+ targetMap.set(id, {
1545
+ id,
1546
+ hasBranch: yoloBranchIds.has(id),
1547
+ hasWorktree: worktreeIds.has(id)
1548
+ });
1549
+ }
1120
1550
  }
1121
- if (matchingContainers.length === 0) {
1122
- const message = args.all ? "No yolobox containers found." : "No yolobox containers found from this directory.";
1123
- info(message);
1551
+ const targets = [...targetMap.values()];
1552
+ if (targets.length === 0) {
1553
+ if (!dockerRunning) {
1554
+ error(
1555
+ "Docker is not running and no local branches or worktrees found."
1556
+ );
1557
+ } else {
1558
+ info(
1559
+ args.all ? "Nothing to clean up." : "No yolobox resources found for this directory."
1560
+ );
1561
+ }
1124
1562
  process.exit(0);
1125
1563
  }
1126
1564
  console.log(
1127
1565
  `
1128
- Found ${matchingContainers.length} container${matchingContainers.length === 1 ? "" : "s"} ${locationDescription}:
1566
+ Found ${targets.length} yolobox resource${targets.length === 1 ? "" : "s"}:
1129
1567
  `
1130
1568
  );
1131
- for (const container of matchingContainers) {
1132
- const pathDisplay = args.all ? ` [${container.path}]` : "";
1569
+ for (const target of targets) {
1570
+ const parts = [];
1571
+ if (target.container) {
1572
+ const statusColor = target.container.status === "running" ? pc.green : pc.dim;
1573
+ parts.push(`container ${statusColor(target.container.status)}`);
1574
+ }
1575
+ if (target.hasBranch) {
1576
+ parts.push(`branch ${pc.cyan(`yolo/${target.id}`)}`);
1577
+ }
1578
+ if (target.hasWorktree) {
1579
+ parts.push("worktree");
1580
+ }
1581
+ const pathSuffix = args.all && target.container ? ` ${pc.dim(target.container.path)}` : "";
1133
1582
  console.log(
1134
- ` ${container.id} (${container.branch}) - ${container.status}${pathDisplay}`
1583
+ ` ${pc.bold(target.id)} ${parts.join(pc.dim(" \xB7 "))}${pathSuffix}`
1135
1584
  );
1136
1585
  }
1137
1586
  console.log();
1138
- const confirmed = await p.confirm({
1139
- message: `Kill all ${matchingContainers.length} container${matchingContainers.length === 1 ? "" : "s"}?`,
1140
- initialValue: false
1587
+ const hasContainers = targets.some((t) => t.container);
1588
+ const hasGitResources = targets.some((t) => t.hasBranch || t.hasWorktree);
1589
+ const options = [
1590
+ { value: "cancel", label: "Cancel" }
1591
+ ];
1592
+ if (hasContainers && hasGitResources) {
1593
+ options.push(
1594
+ { value: "kill", label: "Kill containers only" },
1595
+ {
1596
+ value: "full",
1597
+ label: "Nuke all (containers + branches + worktrees)"
1598
+ }
1599
+ );
1600
+ } else if (hasContainers) {
1601
+ options.push({ value: "kill", label: "Kill containers" });
1602
+ } else {
1603
+ options.push({
1604
+ value: "full",
1605
+ label: "Delete all branches and worktrees"
1606
+ });
1607
+ }
1608
+ const action = await p.select({
1609
+ message: "What would you like to do?",
1610
+ options,
1611
+ initialValue: "cancel"
1141
1612
  });
1142
- if (!confirmed || p.isCancel(confirmed)) {
1613
+ if (action === "cancel" || p.isCancel(action)) {
1143
1614
  info("Cancelled.");
1144
1615
  process.exit(0);
1145
1616
  }
1146
- const results = [];
1147
- for (const container of matchingContainers) {
1148
- const success2 = killContainer(container.id);
1149
- results.push({ id: container.id, success: success2 });
1150
- if (success2) {
1151
- success(`Killed container ${container.id}`);
1152
- } else {
1153
- error(`Failed to kill container ${container.id}`);
1617
+ const shouldKill = action === "kill" || action === "full";
1618
+ const shouldCleanGit = action === "full";
1619
+ let containersKilled = 0;
1620
+ let killFailures = 0;
1621
+ if (shouldKill) {
1622
+ for (const target of targets) {
1623
+ if (!target.container) continue;
1624
+ const success2 = killContainer(target.id);
1625
+ if (success2) {
1626
+ success(`Killed container ${pc.bold(target.id)}`);
1627
+ containersKilled++;
1628
+ } else {
1629
+ error(`Failed to kill container ${target.id}`);
1630
+ killFailures++;
1631
+ }
1632
+ }
1633
+ }
1634
+ let worktreesRemoved = 0;
1635
+ let branchesDeleted = 0;
1636
+ if (shouldCleanGit && repoRoot) {
1637
+ for (const target of targets) {
1638
+ if (target.hasWorktree) {
1639
+ const removed = removeWorktree(repoRoot, target.id);
1640
+ if (removed) {
1641
+ success(
1642
+ `Removed worktree ${pc.bold(`.yolobox/${target.id}`)}`
1643
+ );
1644
+ worktreesRemoved++;
1645
+ } else {
1646
+ warn(`Could not remove worktree for ${target.id}`);
1647
+ }
1648
+ }
1649
+ if (target.hasBranch) {
1650
+ const branchName = `yolo/${target.id}`;
1651
+ const deleted = deleteBranch(branchName);
1652
+ if (deleted) {
1653
+ success(`Deleted branch ${pc.bold(branchName)}`);
1654
+ branchesDeleted++;
1655
+ } else {
1656
+ warn(`Could not delete branch ${branchName}`);
1657
+ }
1658
+ }
1154
1659
  }
1155
1660
  }
1156
- const successCount = results.filter((r) => r.success).length;
1157
- const failureCount = results.length - successCount;
1158
1661
  console.log();
1159
- if (failureCount === 0) {
1160
- success(
1161
- `Successfully killed all ${successCount} container${successCount === 1 ? "" : "s"}.`
1662
+ const s = (n) => n === 1 ? "" : "s";
1663
+ const summaryParts = [];
1664
+ if (containersKilled > 0) {
1665
+ summaryParts.push(
1666
+ `killed ${containersKilled} container${s(containersKilled)}`
1162
1667
  );
1163
- process.exit(0);
1164
- } else {
1165
- error(
1166
- `Killed ${successCount} container${successCount === 1 ? "" : "s"}, but ${failureCount} failed.`
1668
+ }
1669
+ if (branchesDeleted > 0) {
1670
+ summaryParts.push(
1671
+ `deleted ${branchesDeleted} branch${branchesDeleted === 1 ? "" : "es"}`
1672
+ );
1673
+ }
1674
+ if (worktreesRemoved > 0) {
1675
+ summaryParts.push(
1676
+ `removed ${worktreesRemoved} worktree${s(worktreesRemoved)}`
1677
+ );
1678
+ }
1679
+ if (killFailures > 0) {
1680
+ summaryParts.push(
1681
+ `${killFailures} container${s(killFailures)} failed to stop`
1167
1682
  );
1683
+ error(`${summaryParts.join(", ")}.`);
1168
1684
  process.exit(1);
1685
+ } else {
1686
+ const msg = summaryParts.join(", ");
1687
+ success(`${msg.charAt(0).toUpperCase() + msg.slice(1)}.`);
1688
+ process.exit(0);
1169
1689
  }
1170
1690
  }
1171
1691
  });
1172
1692
 
1173
- // src/commands/run.ts
1174
- import { defineCommand as defineCommand6 } from "citty";
1175
- var run_default = defineCommand6({
1693
+ // src/commands/rm.ts
1694
+ import { defineCommand as defineCommand8 } from "citty";
1695
+ var rm_default = defineCommand8({
1696
+ meta: {
1697
+ name: "rm",
1698
+ description: "Remove a yolobox: kill container, delete worktree, and delete branch"
1699
+ },
1700
+ args: {
1701
+ id: {
1702
+ type: "positional",
1703
+ description: "The yolobox ID to remove (interactive picker if omitted)",
1704
+ required: false
1705
+ }
1706
+ },
1707
+ run: async ({ args }) => {
1708
+ if (!isDockerRunning()) {
1709
+ error("Docker is not running.");
1710
+ return process.exit(1);
1711
+ }
1712
+ const repoRoot = getRepoRoot();
1713
+ let id = args.id;
1714
+ if (!id) {
1715
+ const containers = listContainers();
1716
+ if (containers.length === 0) {
1717
+ error("No yolobox containers found.");
1718
+ return process.exit(1);
1719
+ }
1720
+ const selected = await p.select({
1721
+ message: "Pick a container to remove",
1722
+ options: [
1723
+ ...containers.map((c) => ({
1724
+ value: c.id,
1725
+ label: c.id,
1726
+ hint: `${c.status} \u2022 ${c.path}`
1727
+ })),
1728
+ {
1729
+ value: "__cancel__",
1730
+ label: "Cancel",
1731
+ hint: "Exit without removing"
1732
+ }
1733
+ ]
1734
+ });
1735
+ if (p.isCancel(selected) || selected === "__cancel__") {
1736
+ return process.exit(0);
1737
+ }
1738
+ id = selected;
1739
+ } else {
1740
+ const hasContainer = listContainers().some((c) => c.id === id);
1741
+ const hasWorktree = getExistingWorktreeIds(repoRoot).includes(id);
1742
+ const hasBranch = getBranches().includes(`yolo/${id}`);
1743
+ if (!hasContainer && !hasWorktree && !hasBranch) {
1744
+ error(`No yolobox found with ID "${id}".`);
1745
+ return process.exit(1);
1746
+ }
1747
+ }
1748
+ const killed = killContainer(id);
1749
+ if (killed) {
1750
+ success(`Killed container yolobox-${id}`);
1751
+ }
1752
+ const removedWorktree = removeWorktree(repoRoot, id);
1753
+ if (removedWorktree) {
1754
+ success(`Removed worktree .yolobox/${id}`);
1755
+ }
1756
+ const branch = `yolo/${id}`;
1757
+ const deletedBranch = deleteBranch(branch);
1758
+ if (deletedBranch) {
1759
+ success(`Deleted branch ${branch}`);
1760
+ }
1761
+ }
1762
+ });
1763
+
1764
+ // src/commands/start.ts
1765
+ import { defineCommand as defineCommand9 } from "citty";
1766
+ var start_default = defineCommand9({
1176
1767
  meta: {
1177
- name: "run",
1768
+ name: "start",
1178
1769
  description: "Launch a shell in a new yolobox"
1179
1770
  },
1180
1771
  args: {
1181
1772
  name: {
1182
- type: "string",
1183
- alias: "n",
1184
- description: "Use a specific name instead of random"
1773
+ type: "positional",
1774
+ description: "Use a specific name instead of random",
1775
+ required: false
1185
1776
  }
1186
1777
  },
1187
1778
  run: async ({ args }) => {
1188
- const { id } = await setupContainer({ name: args.name });
1779
+ if (args.name) {
1780
+ if (!isDockerRunning()) {
1781
+ error("Docker is not running.");
1782
+ return process.exit(1);
1783
+ }
1784
+ const containers = listContainers();
1785
+ const existing = containers.find((c) => c.id === args.name);
1786
+ if (existing) {
1787
+ if (existing.status === "running") {
1788
+ info(`Container "${args.name}" is already running. Attaching...`);
1789
+ const exitCode2 = execInContainer(args.name, ["bash"]);
1790
+ return process.exit(exitCode2);
1791
+ }
1792
+ info(`Restarting stopped container "${args.name}"...`);
1793
+ if (!restartContainer(args.name)) {
1794
+ error(`Failed to restart container "${args.name}".`);
1795
+ return process.exit(1);
1796
+ }
1797
+ outro2(`Launching shell in ${args.name}...`);
1798
+ const exitCode = execInContainer(args.name, ["bash"]);
1799
+ return process.exit(exitCode);
1800
+ }
1801
+ }
1802
+ const { id } = await setupContainer({
1803
+ name: args.name
1804
+ });
1189
1805
  outro2(`Launching shell in ${id}...`);
1190
1806
  execInContainer(id, ["bash"]);
1191
1807
  }
1192
1808
  });
1193
1809
 
1194
1810
  // src/index.ts
1195
- var main = defineCommand7({
1811
+ var main = defineCommand10({
1196
1812
  meta: {
1197
1813
  name: "yolobox",
1198
- version: "0.0.1",
1814
+ version: "0.1.2",
1199
1815
  description: "Run Claude Code in Docker containers. YOLO safely."
1200
1816
  },
1201
1817
  subCommands: {
1202
- run: run_default,
1818
+ auth: auth_default,
1819
+ start: start_default,
1203
1820
  claude: claude_default,
1821
+ attach: attach_default,
1204
1822
  kill: kill_default,
1205
1823
  ls: ls_default,
1206
1824
  help: help_default,
1207
- nuke: nuke_default
1825
+ nuke: nuke_default,
1826
+ rm: rm_default
1208
1827
  }
1209
1828
  });
1210
1829
  runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yolobox",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Run Claude Code in a Docker container with --dangerously-skip-permissions. YOLO safely.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,8 @@
20
20
  "lint:fix": "biome check --write .",
21
21
  "test": "vitest",
22
22
  "docker:build": "docker build -t yolobox:local -f docker/Dockerfile docker/",
23
- "prepublishOnly": "npm run build"
23
+ "prepublishOnly": "npm run build",
24
+ "prepare": "lefthook install"
24
25
  },
25
26
  "dependencies": {
26
27
  "@clack/prompts": "^0.9.1",
@@ -30,6 +31,7 @@
30
31
  "devDependencies": {
31
32
  "@biomejs/biome": "^2.3.14",
32
33
  "@types/node": "^22.0.0",
34
+ "lefthook": "^2.1.0",
33
35
  "tsup": "^8.3.0",
34
36
  "typescript": "^5.7.0",
35
37
  "vitest": "^2.1.0"