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.
- package/README.md +144 -40
- package/dist/index.js +730 -111
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,75 +1,179 @@
|
|
|
1
1
|
# yolobox
|
|
2
2
|
|
|
3
|
-
Run Claude Code
|
|
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
|
|
7
|
-
yolobox
|
|
8
|
-
yolobox
|
|
9
|
-
yolobox
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
35
|
-
yolobox
|
|
66
|
+
yolobox attach # Pick from running containers
|
|
67
|
+
yolobox attach swift-falcon # Attach to a specific one
|
|
36
68
|
```
|
|
37
69
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
84
|
+
### `yolobox kill [id]`
|
|
54
85
|
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
2
|
+
import { defineCommand as defineCommand10, runMain } from "citty";
|
|
3
3
|
|
|
4
|
-
// src/commands/
|
|
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,
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
955
|
-
|
|
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:
|
|
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 =
|
|
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: "
|
|
987
|
-
|
|
988
|
-
|
|
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({
|
|
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
|
|
1002
|
-
var help_default =
|
|
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
|
|
1017
|
-
var kill_default =
|
|
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:
|
|
1397
|
+
description: "The yolobox ID to kill (interactive picker if omitted)",
|
|
1398
|
+
required: false
|
|
1027
1399
|
}
|
|
1028
1400
|
},
|
|
1029
1401
|
run: async ({ args }) => {
|
|
1030
|
-
|
|
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
|
|
1043
|
-
function shortenPath(
|
|
1044
|
-
const home =
|
|
1045
|
-
const display =
|
|
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 =
|
|
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
|
|
1090
|
-
var nuke_default =
|
|
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
|
|
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(
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
1109
|
-
let
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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 ${
|
|
1566
|
+
Found ${targets.length} yolobox resource${targets.length === 1 ? "" : "s"}:
|
|
1129
1567
|
`
|
|
1130
1568
|
);
|
|
1131
|
-
for (const
|
|
1132
|
-
const
|
|
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
|
-
` ${
|
|
1583
|
+
` ${pc.bold(target.id)} ${parts.join(pc.dim(" \xB7 "))}${pathSuffix}`
|
|
1135
1584
|
);
|
|
1136
1585
|
}
|
|
1137
1586
|
console.log();
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
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 (
|
|
1613
|
+
if (action === "cancel" || p.isCancel(action)) {
|
|
1143
1614
|
info("Cancelled.");
|
|
1144
1615
|
process.exit(0);
|
|
1145
1616
|
}
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
`
|
|
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/
|
|
1174
|
-
import { defineCommand as
|
|
1175
|
-
var
|
|
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: "
|
|
1768
|
+
name: "start",
|
|
1178
1769
|
description: "Launch a shell in a new yolobox"
|
|
1179
1770
|
},
|
|
1180
1771
|
args: {
|
|
1181
1772
|
name: {
|
|
1182
|
-
type: "
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
-
|
|
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 =
|
|
1811
|
+
var main = defineCommand10({
|
|
1196
1812
|
meta: {
|
|
1197
1813
|
name: "yolobox",
|
|
1198
|
-
version: "0.
|
|
1814
|
+
version: "0.1.2",
|
|
1199
1815
|
description: "Run Claude Code in Docker containers. YOLO safely."
|
|
1200
1816
|
},
|
|
1201
1817
|
subCommands: {
|
|
1202
|
-
|
|
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.
|
|
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"
|