yolobox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/yolobox.js +2 -0
- package/dist/index.js +1210 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roger Garcia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# yolobox
|
|
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.
|
|
4
|
+
|
|
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
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Development
|
|
15
|
+
|
|
16
|
+
### Setup
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install dependencies
|
|
20
|
+
npm install
|
|
21
|
+
|
|
22
|
+
# Build the Docker image (only needed once, takes ~5 min)
|
|
23
|
+
npm run docker:build
|
|
24
|
+
|
|
25
|
+
# Link the CLI globally so you can use the `yolobox` command
|
|
26
|
+
npm link
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Testing
|
|
30
|
+
|
|
31
|
+
**End-to-end test:**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# In this repo (or any git repo)
|
|
35
|
+
yolobox run --shell
|
|
36
|
+
```
|
|
37
|
+
|
|
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
|
|
44
|
+
|
|
45
|
+
Once inside the container, verify:
|
|
46
|
+
```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)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Type `exit` to leave the container.
|
|
54
|
+
|
|
55
|
+
**Quick verification without Docker:**
|
|
56
|
+
|
|
57
|
+
```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)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Build & Watch
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm run build # One-shot build
|
|
68
|
+
npm run dev # Watch mode (rebuild on change)
|
|
69
|
+
npm test # Run tests
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/bin/yolobox.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,1210 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { defineCommand as defineCommand7, runMain } from "citty";
|
|
3
|
+
|
|
4
|
+
// src/commands/claude.ts
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
|
|
7
|
+
// src/lib/docker.ts
|
|
8
|
+
import { execSync, spawnSync } from "child_process";
|
|
9
|
+
function isDockerRunning() {
|
|
10
|
+
try {
|
|
11
|
+
execSync("docker info", { stdio: ["pipe", "pipe", "pipe"] });
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function buildDockerArgs(opts) {
|
|
18
|
+
const args = [
|
|
19
|
+
"run",
|
|
20
|
+
"-d",
|
|
21
|
+
"--name",
|
|
22
|
+
`yolobox-${opts.id}`,
|
|
23
|
+
"-v",
|
|
24
|
+
`${opts.worktreePath}:/workspace`,
|
|
25
|
+
"-v",
|
|
26
|
+
`${opts.gitDir}:/repo/.git`,
|
|
27
|
+
"-e",
|
|
28
|
+
`YOLOBOX_ID=${opts.id}`,
|
|
29
|
+
"--label",
|
|
30
|
+
"yolobox=true",
|
|
31
|
+
"--label",
|
|
32
|
+
`yolobox.path=${opts.repoPath}`
|
|
33
|
+
];
|
|
34
|
+
if (opts.gitIdentity.name) {
|
|
35
|
+
args.push("-e", `GIT_AUTHOR_NAME=${opts.gitIdentity.name}`);
|
|
36
|
+
args.push("-e", `GIT_COMMITTER_NAME=${opts.gitIdentity.name}`);
|
|
37
|
+
}
|
|
38
|
+
if (opts.gitIdentity.email) {
|
|
39
|
+
args.push("-e", `GIT_AUTHOR_EMAIL=${opts.gitIdentity.email}`);
|
|
40
|
+
args.push("-e", `GIT_COMMITTER_EMAIL=${opts.gitIdentity.email}`);
|
|
41
|
+
}
|
|
42
|
+
args.push(opts.image);
|
|
43
|
+
args.push("sleep", "infinity");
|
|
44
|
+
return args;
|
|
45
|
+
}
|
|
46
|
+
function buildExecArgs(id, command) {
|
|
47
|
+
return ["exec", "-it", `yolobox-${id}`, ...command];
|
|
48
|
+
}
|
|
49
|
+
function startContainer(opts) {
|
|
50
|
+
const args = buildDockerArgs(opts);
|
|
51
|
+
const result = spawnSync("docker", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
52
|
+
return result.status === 0;
|
|
53
|
+
}
|
|
54
|
+
function execInContainer(id, command) {
|
|
55
|
+
const args = buildExecArgs(id, command);
|
|
56
|
+
const result = spawnSync("docker", args, { stdio: "inherit" });
|
|
57
|
+
return result.status ?? 1;
|
|
58
|
+
}
|
|
59
|
+
function timeAgo(dateStr) {
|
|
60
|
+
const date = new Date(dateStr);
|
|
61
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
|
|
62
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
63
|
+
const minutes = Math.floor(seconds / 60);
|
|
64
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
65
|
+
const hours = Math.floor(minutes / 60);
|
|
66
|
+
if (hours < 24) return `${hours}h ago`;
|
|
67
|
+
const days = Math.floor(hours / 24);
|
|
68
|
+
return `${days}d ago`;
|
|
69
|
+
}
|
|
70
|
+
function listContainers() {
|
|
71
|
+
try {
|
|
72
|
+
const result = execSync(
|
|
73
|
+
'docker ps -a --filter "label=yolobox" --format "{{.Names}} {{.Status}} {{.CreatedAt}} {{.Label \\"yolobox.path\\"}}"',
|
|
74
|
+
{ encoding: "utf-8" }
|
|
75
|
+
);
|
|
76
|
+
return result.trim().split("\n").filter(Boolean).map((line) => {
|
|
77
|
+
const [name, status, created, path2] = line.split(" ");
|
|
78
|
+
const id = name.replace(/^yolobox-/, "");
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
branch: id,
|
|
82
|
+
status: status.startsWith("Up") ? "running" : "stopped",
|
|
83
|
+
created: timeAgo(created),
|
|
84
|
+
path: path2 || ""
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function killContainer(id) {
|
|
92
|
+
const stop = spawnSync("docker", ["stop", `yolobox-${id}`], {
|
|
93
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
94
|
+
});
|
|
95
|
+
if (stop.status !== 0) return false;
|
|
96
|
+
const rm = spawnSync("docker", ["rm", `yolobox-${id}`], {
|
|
97
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
98
|
+
});
|
|
99
|
+
return rm.status === 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/lib/git.ts
|
|
103
|
+
import { execSync as execSync2 } from "child_process";
|
|
104
|
+
function exec(cmd) {
|
|
105
|
+
return execSync2(cmd, {
|
|
106
|
+
encoding: "utf-8",
|
|
107
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
108
|
+
}).trim();
|
|
109
|
+
}
|
|
110
|
+
function isInsideGitRepo() {
|
|
111
|
+
try {
|
|
112
|
+
exec("git rev-parse --is-inside-work-tree");
|
|
113
|
+
return true;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function getRepoRoot() {
|
|
119
|
+
return exec("git rev-parse --show-toplevel");
|
|
120
|
+
}
|
|
121
|
+
function getGitDir() {
|
|
122
|
+
return exec("git rev-parse --git-dir");
|
|
123
|
+
}
|
|
124
|
+
function getBranches() {
|
|
125
|
+
const output = exec('git branch --list --format="%(refname:short)"');
|
|
126
|
+
return output.split("\n").filter(Boolean);
|
|
127
|
+
}
|
|
128
|
+
function initRepo() {
|
|
129
|
+
exec("git init");
|
|
130
|
+
createInitialCommit();
|
|
131
|
+
}
|
|
132
|
+
function hasCommits() {
|
|
133
|
+
try {
|
|
134
|
+
exec("git rev-parse HEAD");
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function createInitialCommit() {
|
|
141
|
+
exec('git commit --allow-empty -m "Initial commit"');
|
|
142
|
+
}
|
|
143
|
+
function getGitIdentity() {
|
|
144
|
+
try {
|
|
145
|
+
const name = exec("git config user.name");
|
|
146
|
+
const email = exec("git config user.email");
|
|
147
|
+
return { name, email };
|
|
148
|
+
} catch {
|
|
149
|
+
return { name: "", email: "" };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/lib/id.ts
|
|
154
|
+
var ADJECTIVES = [
|
|
155
|
+
"able",
|
|
156
|
+
"adept",
|
|
157
|
+
"agile",
|
|
158
|
+
"alert",
|
|
159
|
+
"alive",
|
|
160
|
+
"ample",
|
|
161
|
+
"apt",
|
|
162
|
+
"avid",
|
|
163
|
+
"aware",
|
|
164
|
+
"azure",
|
|
165
|
+
"bare",
|
|
166
|
+
"basic",
|
|
167
|
+
"bliss",
|
|
168
|
+
"blunt",
|
|
169
|
+
"bold",
|
|
170
|
+
"brash",
|
|
171
|
+
"brave",
|
|
172
|
+
"brief",
|
|
173
|
+
"brisk",
|
|
174
|
+
"broad",
|
|
175
|
+
"busy",
|
|
176
|
+
"calm",
|
|
177
|
+
"civic",
|
|
178
|
+
"clean",
|
|
179
|
+
"clear",
|
|
180
|
+
"close",
|
|
181
|
+
"cool",
|
|
182
|
+
"core",
|
|
183
|
+
"cozy",
|
|
184
|
+
"crisp",
|
|
185
|
+
"curly",
|
|
186
|
+
"daily",
|
|
187
|
+
"dark",
|
|
188
|
+
"dear",
|
|
189
|
+
"deep",
|
|
190
|
+
"deft",
|
|
191
|
+
"dense",
|
|
192
|
+
"dizzy",
|
|
193
|
+
"dry",
|
|
194
|
+
"dual",
|
|
195
|
+
"eager",
|
|
196
|
+
"early",
|
|
197
|
+
"easy",
|
|
198
|
+
"edgy",
|
|
199
|
+
"elfin",
|
|
200
|
+
"elite",
|
|
201
|
+
"epic",
|
|
202
|
+
"equal",
|
|
203
|
+
"exact",
|
|
204
|
+
"extra",
|
|
205
|
+
"faint",
|
|
206
|
+
"fair",
|
|
207
|
+
"fancy",
|
|
208
|
+
"fast",
|
|
209
|
+
"feral",
|
|
210
|
+
"fierce",
|
|
211
|
+
"fiery",
|
|
212
|
+
"fine",
|
|
213
|
+
"firm",
|
|
214
|
+
"first",
|
|
215
|
+
"fit",
|
|
216
|
+
"fleet",
|
|
217
|
+
"fluid",
|
|
218
|
+
"flush",
|
|
219
|
+
"focal",
|
|
220
|
+
"fond",
|
|
221
|
+
"frank",
|
|
222
|
+
"free",
|
|
223
|
+
"fresh",
|
|
224
|
+
"full",
|
|
225
|
+
"fun",
|
|
226
|
+
"fuzzy",
|
|
227
|
+
"game",
|
|
228
|
+
"glad",
|
|
229
|
+
"gold",
|
|
230
|
+
"good",
|
|
231
|
+
"grand",
|
|
232
|
+
"great",
|
|
233
|
+
"green",
|
|
234
|
+
"grim",
|
|
235
|
+
"gross",
|
|
236
|
+
"grown",
|
|
237
|
+
"gusty",
|
|
238
|
+
"happy",
|
|
239
|
+
"hardy",
|
|
240
|
+
"hasty",
|
|
241
|
+
"hazel",
|
|
242
|
+
"hefty",
|
|
243
|
+
"high",
|
|
244
|
+
"home",
|
|
245
|
+
"hot",
|
|
246
|
+
"huge",
|
|
247
|
+
"human",
|
|
248
|
+
"icy",
|
|
249
|
+
"ideal",
|
|
250
|
+
"inner",
|
|
251
|
+
"ionic",
|
|
252
|
+
"iron",
|
|
253
|
+
"ivory",
|
|
254
|
+
"jade",
|
|
255
|
+
"jazzy",
|
|
256
|
+
"jolly",
|
|
257
|
+
"just",
|
|
258
|
+
"keen",
|
|
259
|
+
"kind",
|
|
260
|
+
"known",
|
|
261
|
+
"lanky",
|
|
262
|
+
"large",
|
|
263
|
+
"laser",
|
|
264
|
+
"late",
|
|
265
|
+
"leafy",
|
|
266
|
+
"lean",
|
|
267
|
+
"level",
|
|
268
|
+
"light",
|
|
269
|
+
"lithe",
|
|
270
|
+
"live",
|
|
271
|
+
"local",
|
|
272
|
+
"lone",
|
|
273
|
+
"long",
|
|
274
|
+
"loud",
|
|
275
|
+
"loyal",
|
|
276
|
+
"lucid",
|
|
277
|
+
"lucky",
|
|
278
|
+
"lunar",
|
|
279
|
+
"lusty",
|
|
280
|
+
"magic",
|
|
281
|
+
"main",
|
|
282
|
+
"major",
|
|
283
|
+
"maple",
|
|
284
|
+
"meek",
|
|
285
|
+
"merry",
|
|
286
|
+
"mild",
|
|
287
|
+
"mint",
|
|
288
|
+
"misty",
|
|
289
|
+
"mixed",
|
|
290
|
+
"modal",
|
|
291
|
+
"moist",
|
|
292
|
+
"moody",
|
|
293
|
+
"moral",
|
|
294
|
+
"muddy",
|
|
295
|
+
"muted",
|
|
296
|
+
"naive",
|
|
297
|
+
"naval",
|
|
298
|
+
"neat",
|
|
299
|
+
"new",
|
|
300
|
+
"next",
|
|
301
|
+
"nice",
|
|
302
|
+
"nimble",
|
|
303
|
+
"noble",
|
|
304
|
+
"noted",
|
|
305
|
+
"novel",
|
|
306
|
+
"oaken",
|
|
307
|
+
"odd",
|
|
308
|
+
"olive",
|
|
309
|
+
"only",
|
|
310
|
+
"open",
|
|
311
|
+
"opted",
|
|
312
|
+
"oral",
|
|
313
|
+
"other",
|
|
314
|
+
"outer",
|
|
315
|
+
"oval",
|
|
316
|
+
"own",
|
|
317
|
+
"pale",
|
|
318
|
+
"pasty",
|
|
319
|
+
"peak",
|
|
320
|
+
"perky",
|
|
321
|
+
"petty",
|
|
322
|
+
"pilot",
|
|
323
|
+
"pine",
|
|
324
|
+
"pink",
|
|
325
|
+
"plain",
|
|
326
|
+
"plumb",
|
|
327
|
+
"plump",
|
|
328
|
+
"plush",
|
|
329
|
+
"polar",
|
|
330
|
+
"polka",
|
|
331
|
+
"prime",
|
|
332
|
+
"prior",
|
|
333
|
+
"proud",
|
|
334
|
+
"pure",
|
|
335
|
+
"pushy",
|
|
336
|
+
"quick",
|
|
337
|
+
"quiet",
|
|
338
|
+
"rabid",
|
|
339
|
+
"rapid",
|
|
340
|
+
"rare",
|
|
341
|
+
"raspy",
|
|
342
|
+
"raw",
|
|
343
|
+
"ready",
|
|
344
|
+
"real",
|
|
345
|
+
"red",
|
|
346
|
+
"regal",
|
|
347
|
+
"rich",
|
|
348
|
+
"rigid",
|
|
349
|
+
"ripe",
|
|
350
|
+
"risen",
|
|
351
|
+
"risky",
|
|
352
|
+
"ritzy",
|
|
353
|
+
"rival",
|
|
354
|
+
"roast",
|
|
355
|
+
"rocky",
|
|
356
|
+
"roman",
|
|
357
|
+
"roomy",
|
|
358
|
+
"rosy",
|
|
359
|
+
"rough",
|
|
360
|
+
"round",
|
|
361
|
+
"royal",
|
|
362
|
+
"ruby",
|
|
363
|
+
"runic",
|
|
364
|
+
"rural",
|
|
365
|
+
"rusty",
|
|
366
|
+
"safe",
|
|
367
|
+
"sage",
|
|
368
|
+
"salty",
|
|
369
|
+
"same",
|
|
370
|
+
"sandy",
|
|
371
|
+
"sane",
|
|
372
|
+
"savvy",
|
|
373
|
+
"scaly",
|
|
374
|
+
"sharp",
|
|
375
|
+
"sheer",
|
|
376
|
+
"shiny",
|
|
377
|
+
"short",
|
|
378
|
+
"shy",
|
|
379
|
+
"silky",
|
|
380
|
+
"slim",
|
|
381
|
+
"slow",
|
|
382
|
+
"small",
|
|
383
|
+
"smart",
|
|
384
|
+
"smoky",
|
|
385
|
+
"snowy",
|
|
386
|
+
"snug",
|
|
387
|
+
"sober",
|
|
388
|
+
"soft",
|
|
389
|
+
"solar",
|
|
390
|
+
"solid",
|
|
391
|
+
"sonic",
|
|
392
|
+
"sorry",
|
|
393
|
+
"sound",
|
|
394
|
+
"south",
|
|
395
|
+
"spare",
|
|
396
|
+
"spicy",
|
|
397
|
+
"spry",
|
|
398
|
+
"squat",
|
|
399
|
+
"stale",
|
|
400
|
+
"stark",
|
|
401
|
+
"steel",
|
|
402
|
+
"steep",
|
|
403
|
+
"stern",
|
|
404
|
+
"stiff",
|
|
405
|
+
"still",
|
|
406
|
+
"stoic",
|
|
407
|
+
"stone",
|
|
408
|
+
"stout",
|
|
409
|
+
"suave",
|
|
410
|
+
"sunny",
|
|
411
|
+
"super",
|
|
412
|
+
"sure",
|
|
413
|
+
"sweet",
|
|
414
|
+
"swift",
|
|
415
|
+
"tall",
|
|
416
|
+
"tame",
|
|
417
|
+
"tan",
|
|
418
|
+
"tangy",
|
|
419
|
+
"tart",
|
|
420
|
+
"taut",
|
|
421
|
+
"teal",
|
|
422
|
+
"tepid",
|
|
423
|
+
"thick",
|
|
424
|
+
"thin",
|
|
425
|
+
"tidy",
|
|
426
|
+
"tight",
|
|
427
|
+
"tiny",
|
|
428
|
+
"token",
|
|
429
|
+
"top",
|
|
430
|
+
"total",
|
|
431
|
+
"tough",
|
|
432
|
+
"trim",
|
|
433
|
+
"true",
|
|
434
|
+
"tubby",
|
|
435
|
+
"twin",
|
|
436
|
+
"ultra",
|
|
437
|
+
"uncut",
|
|
438
|
+
"under",
|
|
439
|
+
"undue",
|
|
440
|
+
"unfit",
|
|
441
|
+
"upper",
|
|
442
|
+
"urban",
|
|
443
|
+
"used",
|
|
444
|
+
"usual",
|
|
445
|
+
"utter",
|
|
446
|
+
"vague",
|
|
447
|
+
"valid",
|
|
448
|
+
"vast",
|
|
449
|
+
"velvet",
|
|
450
|
+
"vivid",
|
|
451
|
+
"vocal",
|
|
452
|
+
"wacky",
|
|
453
|
+
"warm",
|
|
454
|
+
"wary",
|
|
455
|
+
"wavy",
|
|
456
|
+
"waxy",
|
|
457
|
+
"weak",
|
|
458
|
+
"weary",
|
|
459
|
+
"weird",
|
|
460
|
+
"west",
|
|
461
|
+
"wet",
|
|
462
|
+
"white",
|
|
463
|
+
"whole",
|
|
464
|
+
"wide",
|
|
465
|
+
"wild",
|
|
466
|
+
"wily",
|
|
467
|
+
"windy",
|
|
468
|
+
"wired",
|
|
469
|
+
"wise",
|
|
470
|
+
"witty",
|
|
471
|
+
"woke",
|
|
472
|
+
"woody",
|
|
473
|
+
"wry",
|
|
474
|
+
"young",
|
|
475
|
+
"zany",
|
|
476
|
+
"zappy",
|
|
477
|
+
"zeal",
|
|
478
|
+
"zen",
|
|
479
|
+
"zesty",
|
|
480
|
+
"zippy"
|
|
481
|
+
];
|
|
482
|
+
var NOUNS = [
|
|
483
|
+
"ace",
|
|
484
|
+
"acorn",
|
|
485
|
+
"agent",
|
|
486
|
+
"alder",
|
|
487
|
+
"alloy",
|
|
488
|
+
"amber",
|
|
489
|
+
"anvil",
|
|
490
|
+
"apex",
|
|
491
|
+
"arch",
|
|
492
|
+
"arrow",
|
|
493
|
+
"aspen",
|
|
494
|
+
"atlas",
|
|
495
|
+
"atom",
|
|
496
|
+
"axle",
|
|
497
|
+
"badge",
|
|
498
|
+
"basin",
|
|
499
|
+
"bass",
|
|
500
|
+
"beam",
|
|
501
|
+
"bench",
|
|
502
|
+
"birch",
|
|
503
|
+
"blade",
|
|
504
|
+
"blaze",
|
|
505
|
+
"bloom",
|
|
506
|
+
"bluff",
|
|
507
|
+
"board",
|
|
508
|
+
"bolt",
|
|
509
|
+
"bond",
|
|
510
|
+
"bower",
|
|
511
|
+
"brace",
|
|
512
|
+
"brand",
|
|
513
|
+
"brass",
|
|
514
|
+
"brick",
|
|
515
|
+
"brook",
|
|
516
|
+
"brush",
|
|
517
|
+
"bugle",
|
|
518
|
+
"cairn",
|
|
519
|
+
"canal",
|
|
520
|
+
"cape",
|
|
521
|
+
"cargo",
|
|
522
|
+
"cedar",
|
|
523
|
+
"chain",
|
|
524
|
+
"charm",
|
|
525
|
+
"chess",
|
|
526
|
+
"chime",
|
|
527
|
+
"chord",
|
|
528
|
+
"clamp",
|
|
529
|
+
"clay",
|
|
530
|
+
"cliff",
|
|
531
|
+
"clock",
|
|
532
|
+
"cloud",
|
|
533
|
+
"clove",
|
|
534
|
+
"coach",
|
|
535
|
+
"coast",
|
|
536
|
+
"comet",
|
|
537
|
+
"coral",
|
|
538
|
+
"core",
|
|
539
|
+
"crane",
|
|
540
|
+
"crate",
|
|
541
|
+
"creek",
|
|
542
|
+
"crest",
|
|
543
|
+
"cross",
|
|
544
|
+
"crown",
|
|
545
|
+
"crush",
|
|
546
|
+
"crypt",
|
|
547
|
+
"cube",
|
|
548
|
+
"curve",
|
|
549
|
+
"cycle",
|
|
550
|
+
"darts",
|
|
551
|
+
"dawn",
|
|
552
|
+
"delta",
|
|
553
|
+
"depot",
|
|
554
|
+
"dew",
|
|
555
|
+
"digit",
|
|
556
|
+
"dingo",
|
|
557
|
+
"disk",
|
|
558
|
+
"ditch",
|
|
559
|
+
"dock",
|
|
560
|
+
"dome",
|
|
561
|
+
"dove",
|
|
562
|
+
"draft",
|
|
563
|
+
"drake",
|
|
564
|
+
"drift",
|
|
565
|
+
"drone",
|
|
566
|
+
"drum",
|
|
567
|
+
"dune",
|
|
568
|
+
"dwarf",
|
|
569
|
+
"eagle",
|
|
570
|
+
"earth",
|
|
571
|
+
"easel",
|
|
572
|
+
"edge",
|
|
573
|
+
"elm",
|
|
574
|
+
"ember",
|
|
575
|
+
"facet",
|
|
576
|
+
"fawn",
|
|
577
|
+
"ferry",
|
|
578
|
+
"fiber",
|
|
579
|
+
"finch",
|
|
580
|
+
"fjord",
|
|
581
|
+
"flame",
|
|
582
|
+
"flask",
|
|
583
|
+
"flax",
|
|
584
|
+
"fleet",
|
|
585
|
+
"flint",
|
|
586
|
+
"flock",
|
|
587
|
+
"flora",
|
|
588
|
+
"flute",
|
|
589
|
+
"focus",
|
|
590
|
+
"forge",
|
|
591
|
+
"fort",
|
|
592
|
+
"frame",
|
|
593
|
+
"frost",
|
|
594
|
+
"fruit",
|
|
595
|
+
"gale",
|
|
596
|
+
"gauge",
|
|
597
|
+
"gavel",
|
|
598
|
+
"gecko",
|
|
599
|
+
"geyser",
|
|
600
|
+
"ghost",
|
|
601
|
+
"glade",
|
|
602
|
+
"gleam",
|
|
603
|
+
"glint",
|
|
604
|
+
"globe",
|
|
605
|
+
"glyph",
|
|
606
|
+
"gnome",
|
|
607
|
+
"goose",
|
|
608
|
+
"gorge",
|
|
609
|
+
"grain",
|
|
610
|
+
"grape",
|
|
611
|
+
"graph",
|
|
612
|
+
"grasp",
|
|
613
|
+
"grove",
|
|
614
|
+
"guild",
|
|
615
|
+
"gull",
|
|
616
|
+
"gusto",
|
|
617
|
+
"haven",
|
|
618
|
+
"hawk",
|
|
619
|
+
"hazel",
|
|
620
|
+
"heath",
|
|
621
|
+
"hedge",
|
|
622
|
+
"heron",
|
|
623
|
+
"hinge",
|
|
624
|
+
"holly",
|
|
625
|
+
"hound",
|
|
626
|
+
"hull",
|
|
627
|
+
"hyena",
|
|
628
|
+
"index",
|
|
629
|
+
"ingot",
|
|
630
|
+
"inlet",
|
|
631
|
+
"ivory",
|
|
632
|
+
"jay",
|
|
633
|
+
"jewel",
|
|
634
|
+
"joint",
|
|
635
|
+
"kelp",
|
|
636
|
+
"ketch",
|
|
637
|
+
"knack",
|
|
638
|
+
"knoll",
|
|
639
|
+
"knot",
|
|
640
|
+
"kraft",
|
|
641
|
+
"lance",
|
|
642
|
+
"larch",
|
|
643
|
+
"lark",
|
|
644
|
+
"latch",
|
|
645
|
+
"lathe",
|
|
646
|
+
"ledge",
|
|
647
|
+
"lever",
|
|
648
|
+
"light",
|
|
649
|
+
"lilac",
|
|
650
|
+
"linen",
|
|
651
|
+
"links",
|
|
652
|
+
"llama",
|
|
653
|
+
"lodge",
|
|
654
|
+
"loft",
|
|
655
|
+
"lotus",
|
|
656
|
+
"lunar",
|
|
657
|
+
"lynch",
|
|
658
|
+
"lynx",
|
|
659
|
+
"mango",
|
|
660
|
+
"manor",
|
|
661
|
+
"maple",
|
|
662
|
+
"marsh",
|
|
663
|
+
"mast",
|
|
664
|
+
"maxim",
|
|
665
|
+
"mesa",
|
|
666
|
+
"meter",
|
|
667
|
+
"mills",
|
|
668
|
+
"miner",
|
|
669
|
+
"mint",
|
|
670
|
+
"mirth",
|
|
671
|
+
"moat",
|
|
672
|
+
"model",
|
|
673
|
+
"molar",
|
|
674
|
+
"moose",
|
|
675
|
+
"morph",
|
|
676
|
+
"morse",
|
|
677
|
+
"motor",
|
|
678
|
+
"mound",
|
|
679
|
+
"mulch",
|
|
680
|
+
"mural",
|
|
681
|
+
"myrrh",
|
|
682
|
+
"nerve",
|
|
683
|
+
"nexus",
|
|
684
|
+
"niche",
|
|
685
|
+
"node",
|
|
686
|
+
"notch",
|
|
687
|
+
"novel",
|
|
688
|
+
"oak",
|
|
689
|
+
"oasis",
|
|
690
|
+
"ocean",
|
|
691
|
+
"olive",
|
|
692
|
+
"onset",
|
|
693
|
+
"onyx",
|
|
694
|
+
"opera",
|
|
695
|
+
"orbit",
|
|
696
|
+
"orca",
|
|
697
|
+
"osprey",
|
|
698
|
+
"otter",
|
|
699
|
+
"oxide",
|
|
700
|
+
"ozone",
|
|
701
|
+
"panda",
|
|
702
|
+
"panel",
|
|
703
|
+
"patch",
|
|
704
|
+
"path",
|
|
705
|
+
"pearl",
|
|
706
|
+
"pecan",
|
|
707
|
+
"perch",
|
|
708
|
+
"phase",
|
|
709
|
+
"pilot",
|
|
710
|
+
"pine",
|
|
711
|
+
"pixel",
|
|
712
|
+
"plank",
|
|
713
|
+
"plant",
|
|
714
|
+
"plaza",
|
|
715
|
+
"plume",
|
|
716
|
+
"point",
|
|
717
|
+
"polar",
|
|
718
|
+
"pond",
|
|
719
|
+
"poppy",
|
|
720
|
+
"port",
|
|
721
|
+
"pouch",
|
|
722
|
+
"press",
|
|
723
|
+
"prism",
|
|
724
|
+
"probe",
|
|
725
|
+
"prose",
|
|
726
|
+
"prowl",
|
|
727
|
+
"pulse",
|
|
728
|
+
"pylon",
|
|
729
|
+
"quail",
|
|
730
|
+
"quest",
|
|
731
|
+
"quill",
|
|
732
|
+
"quota",
|
|
733
|
+
"rafter",
|
|
734
|
+
"rail",
|
|
735
|
+
"range",
|
|
736
|
+
"raven",
|
|
737
|
+
"realm",
|
|
738
|
+
"reed",
|
|
739
|
+
"reef",
|
|
740
|
+
"reign",
|
|
741
|
+
"relay",
|
|
742
|
+
"ridge",
|
|
743
|
+
"rivet",
|
|
744
|
+
"robin",
|
|
745
|
+
"roost",
|
|
746
|
+
"rover",
|
|
747
|
+
"ruby",
|
|
748
|
+
"sage",
|
|
749
|
+
"sail",
|
|
750
|
+
"salon",
|
|
751
|
+
"scale",
|
|
752
|
+
"scone",
|
|
753
|
+
"scope",
|
|
754
|
+
"scout",
|
|
755
|
+
"seal",
|
|
756
|
+
"sedge",
|
|
757
|
+
"shade",
|
|
758
|
+
"shaft",
|
|
759
|
+
"shell",
|
|
760
|
+
"shelf",
|
|
761
|
+
"shoal",
|
|
762
|
+
"shore",
|
|
763
|
+
"shrew",
|
|
764
|
+
"shrub",
|
|
765
|
+
"sigma",
|
|
766
|
+
"siren",
|
|
767
|
+
"skimp",
|
|
768
|
+
"slate",
|
|
769
|
+
"sleet",
|
|
770
|
+
"slope",
|
|
771
|
+
"smith",
|
|
772
|
+
"snare",
|
|
773
|
+
"solar",
|
|
774
|
+
"sonic",
|
|
775
|
+
"spark",
|
|
776
|
+
"spear",
|
|
777
|
+
"spire",
|
|
778
|
+
"spoke",
|
|
779
|
+
"spore",
|
|
780
|
+
"spray",
|
|
781
|
+
"squid",
|
|
782
|
+
"staff",
|
|
783
|
+
"stage",
|
|
784
|
+
"stake",
|
|
785
|
+
"stalk",
|
|
786
|
+
"steam",
|
|
787
|
+
"steel",
|
|
788
|
+
"stern",
|
|
789
|
+
"stone",
|
|
790
|
+
"storm",
|
|
791
|
+
"stove",
|
|
792
|
+
"stump",
|
|
793
|
+
"surge",
|
|
794
|
+
"swamp",
|
|
795
|
+
"swift",
|
|
796
|
+
"thorn",
|
|
797
|
+
"tiger",
|
|
798
|
+
"tile",
|
|
799
|
+
"timer",
|
|
800
|
+
"token",
|
|
801
|
+
"torch",
|
|
802
|
+
"totem",
|
|
803
|
+
"tower",
|
|
804
|
+
"trace",
|
|
805
|
+
"trail",
|
|
806
|
+
"trend",
|
|
807
|
+
"tribe",
|
|
808
|
+
"trout",
|
|
809
|
+
"trunk",
|
|
810
|
+
"tulip",
|
|
811
|
+
"tundra",
|
|
812
|
+
"tuner",
|
|
813
|
+
"tweed",
|
|
814
|
+
"twig",
|
|
815
|
+
"umbra",
|
|
816
|
+
"union",
|
|
817
|
+
"valve",
|
|
818
|
+
"vapor",
|
|
819
|
+
"vault",
|
|
820
|
+
"venom",
|
|
821
|
+
"verge",
|
|
822
|
+
"verse",
|
|
823
|
+
"vigor",
|
|
824
|
+
"vine",
|
|
825
|
+
"viola",
|
|
826
|
+
"viper",
|
|
827
|
+
"visor",
|
|
828
|
+
"vivid",
|
|
829
|
+
"vortex",
|
|
830
|
+
"wafer",
|
|
831
|
+
"warden",
|
|
832
|
+
"watch",
|
|
833
|
+
"wedge",
|
|
834
|
+
"whale",
|
|
835
|
+
"wheat",
|
|
836
|
+
"wheel",
|
|
837
|
+
"whirl",
|
|
838
|
+
"wicker",
|
|
839
|
+
"willow",
|
|
840
|
+
"wing",
|
|
841
|
+
"wren",
|
|
842
|
+
"yacht",
|
|
843
|
+
"yew",
|
|
844
|
+
"yield",
|
|
845
|
+
"zebra",
|
|
846
|
+
"zinc",
|
|
847
|
+
"zone"
|
|
848
|
+
];
|
|
849
|
+
function randomElement(arr) {
|
|
850
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
851
|
+
}
|
|
852
|
+
function generateId(existingIds, maxRetries = 100) {
|
|
853
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
854
|
+
const id = `${randomElement(ADJECTIVES)}-${randomElement(NOUNS)}`;
|
|
855
|
+
if (!existingIds.has(id)) return id;
|
|
856
|
+
}
|
|
857
|
+
throw new Error("Could not generate a unique ID after 100 attempts");
|
|
858
|
+
}
|
|
859
|
+
|
|
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
|
+
// src/lib/worktree.ts
|
|
880
|
+
import { execSync as execSync3 } from "child_process";
|
|
881
|
+
import fs from "fs";
|
|
882
|
+
import path from "path";
|
|
883
|
+
function exec2(cmd, cwd) {
|
|
884
|
+
return execSync3(cmd, {
|
|
885
|
+
encoding: "utf-8",
|
|
886
|
+
cwd,
|
|
887
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
888
|
+
}).trim();
|
|
889
|
+
}
|
|
890
|
+
function createWorktree(repoRoot, id) {
|
|
891
|
+
const yoloboxDir = path.join(repoRoot, ".yolobox");
|
|
892
|
+
fs.mkdirSync(yoloboxDir, { recursive: true });
|
|
893
|
+
const worktreePath = path.join(yoloboxDir, id);
|
|
894
|
+
exec2(`git worktree add "${worktreePath}" -b "yolo/${id}"`, repoRoot);
|
|
895
|
+
return worktreePath;
|
|
896
|
+
}
|
|
897
|
+
function ensureGitignore(repoRoot) {
|
|
898
|
+
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
899
|
+
const entry = ".yolobox/";
|
|
900
|
+
if (fs.existsSync(gitignorePath)) {
|
|
901
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
902
|
+
if (content.includes(entry)) return;
|
|
903
|
+
const separator = content.endsWith("\n") ? "" : "\n";
|
|
904
|
+
fs.appendFileSync(gitignorePath, `${separator}${entry}
|
|
905
|
+
`);
|
|
906
|
+
} else {
|
|
907
|
+
fs.writeFileSync(gitignorePath, `${entry}
|
|
908
|
+
`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
function getExistingWorktreeIds(repoRoot) {
|
|
912
|
+
const yoloboxDir = path.join(repoRoot, ".yolobox");
|
|
913
|
+
if (!fs.existsSync(yoloboxDir)) return [];
|
|
914
|
+
return fs.readdirSync(yoloboxDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/lib/container-setup.ts
|
|
918
|
+
var DOCKER_IMAGE = process.env.YOLOBOX_IMAGE || "yolobox:local";
|
|
919
|
+
async function setupContainer(options = {}) {
|
|
920
|
+
intro2();
|
|
921
|
+
if (!isDockerRunning()) {
|
|
922
|
+
error("Docker is not running. Start Docker Desktop and try again.");
|
|
923
|
+
process.exit(1);
|
|
924
|
+
}
|
|
925
|
+
success("Docker is running");
|
|
926
|
+
if (!isInsideGitRepo()) {
|
|
927
|
+
const shouldInit = await p.confirm({
|
|
928
|
+
message: "No git repo found. Initialize one here?"
|
|
929
|
+
});
|
|
930
|
+
if (p.isCancel(shouldInit) || !shouldInit) {
|
|
931
|
+
error("yolobox needs a git repo for worktrees.");
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
initRepo();
|
|
935
|
+
success("Initialized git repo");
|
|
936
|
+
} else {
|
|
937
|
+
success("Git repo detected");
|
|
938
|
+
}
|
|
939
|
+
if (!hasCommits()) {
|
|
940
|
+
createInitialCommit();
|
|
941
|
+
success("Created initial commit");
|
|
942
|
+
}
|
|
943
|
+
const repoRoot = getRepoRoot();
|
|
944
|
+
const gitDir = getGitDir();
|
|
945
|
+
let id;
|
|
946
|
+
if (options.name) {
|
|
947
|
+
id = options.name;
|
|
948
|
+
} else {
|
|
949
|
+
const branches = new Set(getBranches());
|
|
950
|
+
const existingWorktrees = new Set(getExistingWorktreeIds(repoRoot));
|
|
951
|
+
const taken = /* @__PURE__ */ new Set([...branches, ...existingWorktrees]);
|
|
952
|
+
id = generateId(taken);
|
|
953
|
+
}
|
|
954
|
+
const worktreePath = createWorktree(repoRoot, id);
|
|
955
|
+
success(`Created worktree .yolobox/${id} (branch: ${id})`);
|
|
956
|
+
ensureGitignore(repoRoot);
|
|
957
|
+
const gitIdentity = getGitIdentity();
|
|
958
|
+
const started = startContainer({
|
|
959
|
+
id,
|
|
960
|
+
worktreePath,
|
|
961
|
+
gitDir,
|
|
962
|
+
gitIdentity,
|
|
963
|
+
image: DOCKER_IMAGE,
|
|
964
|
+
repoPath: repoRoot
|
|
965
|
+
});
|
|
966
|
+
if (!started) {
|
|
967
|
+
error("Failed to start container.");
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
return { id, repoRoot };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/commands/claude.ts
|
|
974
|
+
var claude_default = defineCommand({
|
|
975
|
+
meta: {
|
|
976
|
+
name: "claude",
|
|
977
|
+
description: "Launch Claude Code with skip permissions"
|
|
978
|
+
},
|
|
979
|
+
args: {
|
|
980
|
+
prompt: {
|
|
981
|
+
type: "string",
|
|
982
|
+
alias: "p",
|
|
983
|
+
description: "Pass an initial prompt to Claude"
|
|
984
|
+
},
|
|
985
|
+
name: {
|
|
986
|
+
type: "string",
|
|
987
|
+
alias: "n",
|
|
988
|
+
description: "Use a specific name instead of random"
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
run: async ({ args }) => {
|
|
992
|
+
const { id } = await setupContainer({ name: args.name });
|
|
993
|
+
const command = args.prompt ? ["claude", "--dangerously-skip-permissions", "-p", args.prompt] : ["claude", "--dangerously-skip-permissions"];
|
|
994
|
+
outro2(`Launching Claude in ${id}...`);
|
|
995
|
+
execInContainer(id, command);
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// src/commands/help.ts
|
|
1000
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1001
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
1002
|
+
var help_default = defineCommand2({
|
|
1003
|
+
meta: {
|
|
1004
|
+
name: "help",
|
|
1005
|
+
description: "Show help information"
|
|
1006
|
+
},
|
|
1007
|
+
run: async () => {
|
|
1008
|
+
const result = spawnSync2(process.argv[0], [process.argv[1], "--help"], {
|
|
1009
|
+
stdio: "inherit"
|
|
1010
|
+
});
|
|
1011
|
+
process.exit(result.status || 0);
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// src/commands/kill.ts
|
|
1016
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
1017
|
+
var kill_default = defineCommand3({
|
|
1018
|
+
meta: {
|
|
1019
|
+
name: "kill",
|
|
1020
|
+
description: "Stop and remove a running yolobox container"
|
|
1021
|
+
},
|
|
1022
|
+
args: {
|
|
1023
|
+
id: {
|
|
1024
|
+
type: "positional",
|
|
1025
|
+
description: "The yolobox ID to kill",
|
|
1026
|
+
required: true
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
run: async ({ args }) => {
|
|
1030
|
+
const id = args.id;
|
|
1031
|
+
const killed = killContainer(id);
|
|
1032
|
+
if (!killed) {
|
|
1033
|
+
error(`Failed to kill yolobox-${id}. Is it running?`);
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
success(`Killed yolobox-${id}`);
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
// 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;
|
|
1046
|
+
if (display.length <= maxLen) return display;
|
|
1047
|
+
const parts = display.split("/");
|
|
1048
|
+
let short = "";
|
|
1049
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
1050
|
+
const candidate = parts.slice(i).join("/");
|
|
1051
|
+
if (candidate.length > maxLen - 2) break;
|
|
1052
|
+
short = candidate;
|
|
1053
|
+
}
|
|
1054
|
+
return short ? `\u2026/${short}` : `\u2026/${parts[parts.length - 1]}`;
|
|
1055
|
+
}
|
|
1056
|
+
var ls_default = defineCommand4({
|
|
1057
|
+
meta: {
|
|
1058
|
+
name: "ls",
|
|
1059
|
+
description: "List running yolobox containers"
|
|
1060
|
+
},
|
|
1061
|
+
run: async () => {
|
|
1062
|
+
if (!isDockerRunning()) {
|
|
1063
|
+
error("Docker is not running.");
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
const containers = listContainers();
|
|
1067
|
+
if (containers.length === 0) {
|
|
1068
|
+
info("No yolobox containers found.");
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const paths = containers.map((c) => shortenPath(c.path));
|
|
1072
|
+
const idW = Math.max(4, ...containers.map((c) => c.id.length)) + 2;
|
|
1073
|
+
const branchW = Math.max(8, ...containers.map((c) => c.branch.length)) + 2;
|
|
1074
|
+
const statusW = Math.max(8, ...containers.map((c) => c.status.length)) + 2;
|
|
1075
|
+
const createdW = Math.max(9, ...containers.map((c) => c.created.length)) + 2;
|
|
1076
|
+
const header = `${"ID".padEnd(idW)}${"BRANCH".padEnd(branchW)}${"STATUS".padEnd(statusW)}${"CREATED".padEnd(createdW)}PATH`;
|
|
1077
|
+
console.log(pc.dim(header));
|
|
1078
|
+
for (let i = 0; i < containers.length; i++) {
|
|
1079
|
+
const c = containers[i];
|
|
1080
|
+
const statusColor = c.status === "running" ? pc.green : pc.yellow;
|
|
1081
|
+
console.log(
|
|
1082
|
+
`${c.id.padEnd(idW)}${c.branch.padEnd(branchW)}${statusColor(c.status.padEnd(statusW))}${pc.dim(c.created.padEnd(createdW))}${pc.dim(paths[i])}`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// src/commands/nuke.ts
|
|
1089
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
1090
|
+
var nuke_default = defineCommand5({
|
|
1091
|
+
meta: {
|
|
1092
|
+
name: "nuke",
|
|
1093
|
+
description: "Kill all yolobox containers from the current directory"
|
|
1094
|
+
},
|
|
1095
|
+
args: {
|
|
1096
|
+
all: {
|
|
1097
|
+
type: "boolean",
|
|
1098
|
+
description: "Kill all yolobox containers from all directories",
|
|
1099
|
+
default: false
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
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);
|
|
1107
|
+
}
|
|
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
|
|
1118
|
+
);
|
|
1119
|
+
locationDescription = `from ${currentPath}`;
|
|
1120
|
+
}
|
|
1121
|
+
if (matchingContainers.length === 0) {
|
|
1122
|
+
const message = args.all ? "No yolobox containers found." : "No yolobox containers found from this directory.";
|
|
1123
|
+
info(message);
|
|
1124
|
+
process.exit(0);
|
|
1125
|
+
}
|
|
1126
|
+
console.log(
|
|
1127
|
+
`
|
|
1128
|
+
Found ${matchingContainers.length} container${matchingContainers.length === 1 ? "" : "s"} ${locationDescription}:
|
|
1129
|
+
`
|
|
1130
|
+
);
|
|
1131
|
+
for (const container of matchingContainers) {
|
|
1132
|
+
const pathDisplay = args.all ? ` [${container.path}]` : "";
|
|
1133
|
+
console.log(
|
|
1134
|
+
` ${container.id} (${container.branch}) - ${container.status}${pathDisplay}`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
console.log();
|
|
1138
|
+
const confirmed = await p.confirm({
|
|
1139
|
+
message: `Kill all ${matchingContainers.length} container${matchingContainers.length === 1 ? "" : "s"}?`,
|
|
1140
|
+
initialValue: false
|
|
1141
|
+
});
|
|
1142
|
+
if (!confirmed || p.isCancel(confirmed)) {
|
|
1143
|
+
info("Cancelled.");
|
|
1144
|
+
process.exit(0);
|
|
1145
|
+
}
|
|
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}`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const successCount = results.filter((r) => r.success).length;
|
|
1157
|
+
const failureCount = results.length - successCount;
|
|
1158
|
+
console.log();
|
|
1159
|
+
if (failureCount === 0) {
|
|
1160
|
+
success(
|
|
1161
|
+
`Successfully killed all ${successCount} container${successCount === 1 ? "" : "s"}.`
|
|
1162
|
+
);
|
|
1163
|
+
process.exit(0);
|
|
1164
|
+
} else {
|
|
1165
|
+
error(
|
|
1166
|
+
`Killed ${successCount} container${successCount === 1 ? "" : "s"}, but ${failureCount} failed.`
|
|
1167
|
+
);
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// src/commands/run.ts
|
|
1174
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
1175
|
+
var run_default = defineCommand6({
|
|
1176
|
+
meta: {
|
|
1177
|
+
name: "run",
|
|
1178
|
+
description: "Launch a shell in a new yolobox"
|
|
1179
|
+
},
|
|
1180
|
+
args: {
|
|
1181
|
+
name: {
|
|
1182
|
+
type: "string",
|
|
1183
|
+
alias: "n",
|
|
1184
|
+
description: "Use a specific name instead of random"
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
run: async ({ args }) => {
|
|
1188
|
+
const { id } = await setupContainer({ name: args.name });
|
|
1189
|
+
outro2(`Launching shell in ${id}...`);
|
|
1190
|
+
execInContainer(id, ["bash"]);
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
// src/index.ts
|
|
1195
|
+
var main = defineCommand7({
|
|
1196
|
+
meta: {
|
|
1197
|
+
name: "yolobox",
|
|
1198
|
+
version: "0.0.1",
|
|
1199
|
+
description: "Run Claude Code in Docker containers. YOLO safely."
|
|
1200
|
+
},
|
|
1201
|
+
subCommands: {
|
|
1202
|
+
run: run_default,
|
|
1203
|
+
claude: claude_default,
|
|
1204
|
+
kill: kill_default,
|
|
1205
|
+
ls: ls_default,
|
|
1206
|
+
help: help_default,
|
|
1207
|
+
nuke: nuke_default
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
runMain(main);
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yolobox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run Claude Code in a Docker container with --dangerously-skip-permissions. YOLO safely.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"yolobox": "./bin/yolobox.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"lint": "biome check .",
|
|
20
|
+
"lint:fix": "biome check --write .",
|
|
21
|
+
"test": "vitest",
|
|
22
|
+
"docker:build": "docker build -t yolobox:local -f docker/Dockerfile docker/",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@clack/prompts": "^0.9.1",
|
|
27
|
+
"citty": "^0.1.6",
|
|
28
|
+
"picocolors": "^1.1.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@biomejs/biome": "^2.3.14",
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"tsup": "^8.3.0",
|
|
34
|
+
"typescript": "^5.7.0",
|
|
35
|
+
"vitest": "^2.1.0"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/roginn/yolobox.git"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"claude",
|
|
43
|
+
"code",
|
|
44
|
+
"sandbox",
|
|
45
|
+
"docker",
|
|
46
|
+
"container",
|
|
47
|
+
"cli"
|
|
48
|
+
],
|
|
49
|
+
"author": "Roger Garcia <npm@rawrger.com>",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/roginn/yolobox/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/roginn/yolobox#readme"
|
|
55
|
+
}
|