@abuaboud/box 0.7.8 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cbox.mjs +114 -185
- package/package.json +1 -1
package/dist/cbox.mjs
CHANGED
|
@@ -12,151 +12,17 @@ import { join as join2 } from "node:path";
|
|
|
12
12
|
import { styleText as styleText2 } from "node:util";
|
|
13
13
|
|
|
14
14
|
// src/laptop.ts
|
|
15
|
-
import { execFile, execFileSync
|
|
15
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
16
16
|
import { gzip } from "node:zlib";
|
|
17
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync,
|
|
18
|
-
import { homedir
|
|
19
|
-
import { join
|
|
17
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { homedir, userInfo } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
20
|
import ssh2 from "ssh2";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import { homedir } from "node:os";
|
|
24
|
-
import { styleText } from "node:util";
|
|
25
|
-
var dim = (s) => styleText("dim", s);
|
|
26
|
-
var ok = (s) => console.log(`${styleText("green", "\u2713")} ${s}`);
|
|
27
|
-
var miss = (s) => console.log(`${styleText("yellow", "!")} ${s}`);
|
|
28
|
-
var fail = (s) => {
|
|
29
|
-
active?.();
|
|
30
|
-
console.error(`${styleText("red", "\u2717")} ${s}`);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
};
|
|
33
|
-
var active;
|
|
34
|
-
var stopProgress = () => active?.();
|
|
35
|
-
function progress(steps, { pace = 5e3 } = {}) {
|
|
36
|
-
const tty = process.stdout.isTTY;
|
|
37
|
-
if (!tty) {
|
|
38
|
-
console.log(dim(`${steps[0][1]}\u2026`));
|
|
39
|
-
return Object.assign((done) => {
|
|
40
|
-
if (done) ok(done);
|
|
41
|
-
}, { say: (label2) => console.log(dim(`${label2}\u2026`)) });
|
|
42
|
-
}
|
|
43
|
-
const spin = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F", t0 = Date.now(), width = 16;
|
|
44
|
-
let i = 0, said;
|
|
45
|
-
process.stdout.write("\x1B[?25l");
|
|
46
|
-
const restore = () => process.stdout.write("\x1B[?25h");
|
|
47
|
-
process.once("exit", restore);
|
|
48
|
-
const draw = () => {
|
|
49
|
-
const ms = Date.now() - t0;
|
|
50
|
-
const label2 = said ?? steps.filter(([at2]) => at2 <= ms).at(-1)[1];
|
|
51
|
-
const at = Math.min(width - 1, Math.floor((1 - Math.exp(-ms / pace)) * width));
|
|
52
|
-
const track = [...Array(width)].map((_, k) => k < at ? styleText("cyan", "\u2501") : k === at ? "\u{1F4E6}" : styleText("dim", "\u2500")).join("");
|
|
53
|
-
const secs = ms / 1e3, clock = secs < 60 ? `${secs.toFixed(1)}s` : `${Math.floor(secs / 60)}m ${String(Math.floor(secs % 60)).padStart(2, "0")}s`;
|
|
54
|
-
process.stdout.write(`\r\x1B[2K${styleText("cyan", spin[i++ % spin.length])} ${track} ${label2.slice(0, Math.max(20, (process.stdout.columns ?? 100) - width - 16))}\u2026 ${dim(clock)}`);
|
|
55
|
-
};
|
|
56
|
-
draw();
|
|
57
|
-
const timer = setInterval(draw, 80);
|
|
58
|
-
const stop = (done) => {
|
|
59
|
-
clearInterval(timer);
|
|
60
|
-
process.stdout.write("\r\x1B[2K");
|
|
61
|
-
restore();
|
|
62
|
-
active = void 0;
|
|
63
|
-
if (done) ok(done);
|
|
64
|
-
};
|
|
65
|
-
active = stop;
|
|
66
|
-
return Object.assign(stop, { say: (label2) => {
|
|
67
|
-
said = label2;
|
|
68
|
-
} });
|
|
69
|
-
}
|
|
70
|
-
var span = (ms) => {
|
|
71
|
-
const m = Math.max(0, Math.round(ms / 6e4));
|
|
72
|
-
return m < 60 ? `${m}m` : m < 1440 ? `${Math.floor(m / 60)}h ${m % 60}m` : `${Math.round(m / 1440)} days`;
|
|
73
|
-
};
|
|
74
|
-
var tilde = (p) => p.startsWith(homedir()) ? `~${p.slice(homedir().length)}` : p;
|
|
75
|
-
function label(b) {
|
|
76
|
-
if (b.state === "paused") return styleText("blue", b.asleepSince ? `asleep ${span(Date.now() - new Date(b.asleepSince).getTime())}` : "asleep");
|
|
77
|
-
if (b.state !== "running") return styleText("yellow", b.state);
|
|
78
|
-
if (b.busy) return styleText("green", "\u25CF working");
|
|
79
|
-
if (!b.idleMinutes) return styleText("green", "awake");
|
|
80
|
-
const left = b.idleMinutes * 6e4 - (Date.now() - new Date(b.lastActiveAt ?? Date.now()).getTime());
|
|
81
|
-
return styleText("yellow", `\u263E sleeps in ${span(Math.max(6e4, left))}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// src/laptop.ts
|
|
85
|
-
var OLD_CONFIG = join(homedir2(), ".config", "craftboxes");
|
|
86
|
-
var OLD_BOXES = join(homedir2(), ".craftboxes");
|
|
87
|
-
var NEW_CONFIG = join(homedir2(), ".config", "box");
|
|
88
|
-
var NEW_BOXES = join(homedir2(), ".box");
|
|
89
|
-
var CONFIG_DIR = existsSync(OLD_CONFIG) ? OLD_CONFIG : NEW_CONFIG;
|
|
90
|
-
var BOXES = existsSync(OLD_BOXES) ? OLD_BOXES : NEW_BOXES;
|
|
21
|
+
var CONFIG_DIR = join(homedir(), ".config", "box");
|
|
22
|
+
var BOXES = join(homedir(), ".box");
|
|
91
23
|
var SERVICES = { 5432: "Postgres", 6379: "Redis", 3306: "MySQL", 27017: "MongoDB", 8123: "ClickHouse" };
|
|
92
|
-
function moveHome() {
|
|
93
|
-
const moves = [[OLD_CONFIG, NEW_CONFIG], [OLD_BOXES, NEW_BOXES]].filter(([from]) => existsSync(from));
|
|
94
|
-
if (!moves.length) return;
|
|
95
|
-
if (otherBoxRunning()) return miss(`Close your other box windows so box can move ~/.craftboxes to ~/.box`);
|
|
96
|
-
const clash = moves.flatMap(([from, to]) => existsSync(to) ? readdirSync(from).filter((f) => existsSync(join(to, f))).map((f) => join(to, f)) : []);
|
|
97
|
-
if (clash.length) return miss(`box can't move ~/.craftboxes to ~/.box while ${clash.join(", ")} exists`);
|
|
98
|
-
try {
|
|
99
|
-
for (const [from, to] of moves) {
|
|
100
|
-
if (!existsSync(to)) {
|
|
101
|
-
renameSync(from, to);
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
for (const f of readdirSync(from)) renameSync(join(from, f), join(to, f));
|
|
105
|
-
rmdirSync(from);
|
|
106
|
-
}
|
|
107
|
-
} catch (e) {
|
|
108
|
-
return miss(`Couldn't move ~/.craftboxes to ~/.box yet: ${e.message}`);
|
|
109
|
-
}
|
|
110
|
-
if (moves.some(([from]) => from === OLD_BOXES)) {
|
|
111
|
-
repairWorktrees();
|
|
112
|
-
moveClaudeProjects();
|
|
113
|
-
}
|
|
114
|
-
ok(`Moved ~/.craftboxes to ~/.box ${dim("\xB7 reopen VS Code windows with box code")}`);
|
|
115
|
-
const again = spawnSync(process.execPath, process.argv.slice(1), { stdio: "inherit", env: { ...process.env, CBOX_UPDATED: "1" } });
|
|
116
|
-
process.exit(again.status ?? 0);
|
|
117
|
-
}
|
|
118
|
-
function otherBoxRunning() {
|
|
119
|
-
try {
|
|
120
|
-
return execFileSync("ps", ["-A", "-o", "pid=,args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3e3 }).split("\n").some((line) => {
|
|
121
|
-
const [pid, ...args] = line.trim().split(/\s+/);
|
|
122
|
-
const command = args.join(" ");
|
|
123
|
-
return ![process.pid, process.ppid].includes(Number(pid)) && /(^|[\s/])c?box(\.mjs)?(\s|$)/.test(command) && !/\s(proxy|sync)(\s|$)/.test(command);
|
|
124
|
-
});
|
|
125
|
-
} catch {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function repairWorktrees() {
|
|
130
|
-
for (const env of readdirSync(NEW_BOXES)) {
|
|
131
|
-
const clone = join(NEW_BOXES, env, ".clone");
|
|
132
|
-
if (env.startsWith(".") || !existsSync(join(clone, ".git"))) continue;
|
|
133
|
-
const worktrees = readdirSync(join(NEW_BOXES, env)).map((d) => join(NEW_BOXES, env, d, "repo")).filter((r) => existsSync(join(r, ".git")) && statSync(join(r, ".git")).isFile());
|
|
134
|
-
try {
|
|
135
|
-
if (worktrees.length) execFileSync("git", ["-C", clone, "worktree", "repair", ...worktrees], { stdio: "ignore", timeout: 3e4 });
|
|
136
|
-
} catch {
|
|
137
|
-
miss(`Couldn't repair the ${env} worktrees. Run: git -C ${clone} worktree repair`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function moveClaudeProjects() {
|
|
142
|
-
const projects = join(homedir2(), ".claude", "projects");
|
|
143
|
-
const [from, to] = [OLD_BOXES, NEW_BOXES].map((p) => p.replace(/[^a-zA-Z0-9]/g, "-"));
|
|
144
|
-
for (const dir of existsSync(projects) ? readdirSync(projects) : []) {
|
|
145
|
-
const target = join(projects, to + dir.slice(from.length));
|
|
146
|
-
if ((dir === from || dir.startsWith(`${from}-`)) && !existsSync(target)) renameSync(join(projects, dir), target);
|
|
147
|
-
}
|
|
148
|
-
const file2 = join(homedir2(), ".claude.json");
|
|
149
|
-
try {
|
|
150
|
-
const config = JSON.parse(readFileSync(file2, "utf8"));
|
|
151
|
-
const keys = Object.keys(config.projects ?? {});
|
|
152
|
-
if (!keys.some((k) => k.startsWith(OLD_BOXES + sep))) return;
|
|
153
|
-
config.projects = Object.fromEntries(Object.entries(config.projects).map(([k, v]) => [k.startsWith(OLD_BOXES + sep) ? NEW_BOXES + k.slice(OLD_BOXES.length) : k, v]));
|
|
154
|
-
writeFileSync(file2, JSON.stringify(config, null, 2));
|
|
155
|
-
} catch {
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
24
|
function localMcp() {
|
|
159
|
-
const claudeJson = join(
|
|
25
|
+
const claudeJson = join(homedir(), ".claude.json");
|
|
160
26
|
const servers = existsSync(claudeJson) ? JSON.parse(readFileSync(claudeJson, "utf8")).mcpServers ?? {} : {};
|
|
161
27
|
const skipped = [];
|
|
162
28
|
const mcpServers = {};
|
|
@@ -172,7 +38,7 @@ function localMcp() {
|
|
|
172
38
|
return { mcpServers, mcpOAuth, skipped };
|
|
173
39
|
}
|
|
174
40
|
function localClaudeHome() {
|
|
175
|
-
const root = join(
|
|
41
|
+
const root = join(homedir(), ".claude");
|
|
176
42
|
const json = (p) => {
|
|
177
43
|
try {
|
|
178
44
|
return JSON.parse(readFileSync(join(root, p), "utf8"));
|
|
@@ -209,12 +75,12 @@ function laptopKey() {
|
|
|
209
75
|
return { privateKey: readFileSync(priv), publicKey: readFileSync(priv + ".pub", "utf8").trim() };
|
|
210
76
|
}
|
|
211
77
|
function sshKeys(boxKey) {
|
|
212
|
-
const home2 = join(
|
|
78
|
+
const home2 = join(homedir(), ".ssh");
|
|
213
79
|
const mine = existsSync(home2) ? readdirSync(home2).filter((f) => f.endsWith(".pub")).map((f) => readFileSync(join(home2, f), "utf8").trim().split(/\s+/).slice(0, 2).join(" ")) : [];
|
|
214
80
|
return [boxKey, ...mine.filter((k) => /^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(256|384|521)|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com) [A-Za-z0-9+/=]+$/.test(k))].join("\n");
|
|
215
81
|
}
|
|
216
82
|
function localCreds() {
|
|
217
|
-
const file2 = join(
|
|
83
|
+
const file2 = join(homedir(), ".claude", ".credentials.json");
|
|
218
84
|
if (process.platform !== "darwin") return existsSync(file2) ? JSON.parse(readFileSync(file2, "utf8")) : {};
|
|
219
85
|
for (const account of [["-a", userInfo().username], []]) {
|
|
220
86
|
try {
|
|
@@ -274,12 +140,73 @@ function localCopy() {
|
|
|
274
140
|
return { dir: top, repo, branch: git2("-C", top, "branch", "--show-current") || void 0, files, services };
|
|
275
141
|
}
|
|
276
142
|
|
|
143
|
+
// src/ui.ts
|
|
144
|
+
import { homedir as homedir2 } from "node:os";
|
|
145
|
+
import { styleText } from "node:util";
|
|
146
|
+
var dim = (s) => styleText("dim", s);
|
|
147
|
+
var ok = (s) => console.log(`${styleText("green", "\u2713")} ${s}`);
|
|
148
|
+
var miss = (s) => console.log(`${styleText("yellow", "!")} ${s}`);
|
|
149
|
+
var fail = (s) => {
|
|
150
|
+
active?.();
|
|
151
|
+
console.error(`${styleText("red", "\u2717")} ${s}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
};
|
|
154
|
+
var active;
|
|
155
|
+
var stopProgress = () => active?.();
|
|
156
|
+
function progress(steps, { pace = 5e3 } = {}) {
|
|
157
|
+
const tty = process.stdout.isTTY;
|
|
158
|
+
if (!tty) {
|
|
159
|
+
console.log(dim(`${steps[0][1]}\u2026`));
|
|
160
|
+
return Object.assign((done) => {
|
|
161
|
+
if (done) ok(done);
|
|
162
|
+
}, { say: (label2) => console.log(dim(`${label2}\u2026`)) });
|
|
163
|
+
}
|
|
164
|
+
const spin = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F", t0 = Date.now(), width = 16;
|
|
165
|
+
let i = 0, said;
|
|
166
|
+
process.stdout.write("\x1B[?25l");
|
|
167
|
+
const restore = () => process.stdout.write("\x1B[?25h");
|
|
168
|
+
process.once("exit", restore);
|
|
169
|
+
const draw = () => {
|
|
170
|
+
const ms = Date.now() - t0;
|
|
171
|
+
const label2 = said ?? steps.filter(([at2]) => at2 <= ms).at(-1)[1];
|
|
172
|
+
const at = Math.min(width - 1, Math.floor((1 - Math.exp(-ms / pace)) * width));
|
|
173
|
+
const track = [...Array(width)].map((_, k) => k < at ? styleText("cyan", "\u2501") : k === at ? "\u{1F4E6}" : styleText("dim", "\u2500")).join("");
|
|
174
|
+
const secs = ms / 1e3, clock = secs < 60 ? `${secs.toFixed(1)}s` : `${Math.floor(secs / 60)}m ${String(Math.floor(secs % 60)).padStart(2, "0")}s`;
|
|
175
|
+
process.stdout.write(`\r\x1B[2K${styleText("cyan", spin[i++ % spin.length])} ${track} ${label2.slice(0, Math.max(20, (process.stdout.columns ?? 100) - width - 16))}\u2026 ${dim(clock)}`);
|
|
176
|
+
};
|
|
177
|
+
draw();
|
|
178
|
+
const timer = setInterval(draw, 80);
|
|
179
|
+
const stop = (done) => {
|
|
180
|
+
clearInterval(timer);
|
|
181
|
+
process.stdout.write("\r\x1B[2K");
|
|
182
|
+
restore();
|
|
183
|
+
active = void 0;
|
|
184
|
+
if (done) ok(done);
|
|
185
|
+
};
|
|
186
|
+
active = stop;
|
|
187
|
+
return Object.assign(stop, { say: (label2) => {
|
|
188
|
+
said = label2;
|
|
189
|
+
} });
|
|
190
|
+
}
|
|
191
|
+
var span = (ms) => {
|
|
192
|
+
const m = Math.max(0, Math.round(ms / 6e4));
|
|
193
|
+
return m < 60 ? `${m}m` : m < 1440 ? `${Math.floor(m / 60)}h ${m % 60}m` : `${Math.round(m / 1440)} days`;
|
|
194
|
+
};
|
|
195
|
+
var tilde = (p) => p.startsWith(homedir2()) ? `~${p.slice(homedir2().length)}` : p;
|
|
196
|
+
function label(b) {
|
|
197
|
+
if (b.state === "paused") return styleText("blue", b.asleepSince ? `asleep ${span(Date.now() - new Date(b.asleepSince).getTime())}` : "asleep");
|
|
198
|
+
if (b.state !== "running") return styleText("yellow", b.state);
|
|
199
|
+
if (b.busy) return styleText("green", "\u25CF working");
|
|
200
|
+
if (!b.idleMinutes) return styleText("green", "awake");
|
|
201
|
+
const left = b.idleMinutes * 6e4 - (Date.now() - new Date(b.lastActiveAt ?? Date.now()).getTime());
|
|
202
|
+
return styleText("yellow", `\u263E sleeps in ${span(Math.max(6e4, left))}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
277
205
|
// src/api.ts
|
|
278
206
|
var file = join2(CONFIG_DIR, "config.json");
|
|
279
207
|
var load = () => {
|
|
280
208
|
if (!existsSync2(file)) return { server: process.env.CB_SERVER ?? "https://box.abuaboud.me" };
|
|
281
|
-
|
|
282
|
-
return { ...c, server: c.server.replace("://craftboxes.abuaboud.me", "://box.abuaboud.me") };
|
|
209
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
283
210
|
};
|
|
284
211
|
var save = (c) => {
|
|
285
212
|
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
@@ -490,13 +417,14 @@ ${styleText3("yellow", "!")} Connect GitHub so Box can clone ${repo} and your bo
|
|
|
490
417
|
|
|
491
418
|
// src/local.ts
|
|
492
419
|
import { execFileSync as execFileSync3, spawn as spawn3 } from "node:child_process";
|
|
493
|
-
import {
|
|
420
|
+
import { createHash } from "node:crypto";
|
|
421
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync, rmSync, statSync, watch, writeFileSync as writeFileSync4 } from "node:fs";
|
|
494
422
|
import { homedir as homedir4 } from "node:os";
|
|
495
423
|
import { basename, dirname, join as join5 } from "node:path";
|
|
496
424
|
import { select as select2 } from "@inquirer/prompts";
|
|
497
425
|
|
|
498
426
|
// src/ssh.ts
|
|
499
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4,
|
|
427
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
500
428
|
import { connect } from "node:net";
|
|
501
429
|
import { homedir as homedir3 } from "node:os";
|
|
502
430
|
import { join as join4 } from "node:path";
|
|
@@ -519,12 +447,9 @@ function ensureSshConfig() {
|
|
|
519
447
|
].join("\n"));
|
|
520
448
|
const config = join4(sshDir, "config");
|
|
521
449
|
const now = existsSync4(config) ? readFileSync4(config, "utf8") : "";
|
|
522
|
-
|
|
523
|
-
const next = /^Include ~\/\.ssh\/box$/m.test(renamed) ? renamed : `Include ~/.ssh/box
|
|
450
|
+
if (!/^Include ~\/\.ssh\/box$/m.test(now)) writeFileSync3(config, `Include ~/.ssh/box
|
|
524
451
|
|
|
525
|
-
${
|
|
526
|
-
if (next !== now) writeFileSync3(config, next, { mode: 384 });
|
|
527
|
-
rmSync(join4(sshDir, "craftboxes"), { force: true });
|
|
452
|
+
${now}`, { mode: 384 });
|
|
528
453
|
}
|
|
529
454
|
async function proxy(host) {
|
|
530
455
|
const name = host.replace(/\.box$/, "");
|
|
@@ -601,7 +526,7 @@ function prepareFolder(name, env, here, mcp) {
|
|
|
601
526
|
const legacy = join5(BOXES, name);
|
|
602
527
|
if (!existsSync5(folder) && existsSync5(join5(legacy, ".box.json"))) {
|
|
603
528
|
mkdirSync4(dirname(folder), { recursive: true });
|
|
604
|
-
|
|
529
|
+
renameSync(legacy, folder);
|
|
605
530
|
}
|
|
606
531
|
mkdirSync4(join5(folder, ".claude"), { recursive: true });
|
|
607
532
|
const stateFile = join5(folder, ".box.json");
|
|
@@ -624,7 +549,7 @@ async function ensureClone(env) {
|
|
|
624
549
|
return clone;
|
|
625
550
|
}
|
|
626
551
|
const partial = `${clone}.partial`;
|
|
627
|
-
|
|
552
|
+
rmSync(partial, { recursive: true, force: true });
|
|
628
553
|
mkdirSync4(dirname(clone), { recursive: true });
|
|
629
554
|
const bar = progress([[0, `Cloning ${env.repo} once for every ${env.name} box`]], { pace: 6e4 });
|
|
630
555
|
const cloned = await git([...auth, "clone", "-q", "--no-checkout", "--filter=blob:none", token ? `https://github.com/${env.repo}.git` : `git@github.com:${env.repo}.git`, partial]);
|
|
@@ -633,23 +558,23 @@ async function ensureClone(env) {
|
|
|
633
558
|
fail(`Couldn't clone ${env.repo}: ${cloned.err.split("\n").at(-1)}. Run gh auth login and try again.`);
|
|
634
559
|
}
|
|
635
560
|
await git(["-C", partial, "update-ref", "--no-deref", "HEAD", "HEAD"]);
|
|
636
|
-
|
|
561
|
+
renameSync(partial, clone);
|
|
637
562
|
bar(`Cloned ${env.repo} into ${tilde(clone)}`);
|
|
638
563
|
return clone;
|
|
639
564
|
}
|
|
640
565
|
async function ensureWorktree({ clone, folder, name, branch }) {
|
|
641
566
|
const repo = join5(folder, "repo");
|
|
642
|
-
if (existsSync5(join5(repo, ".git")) &&
|
|
643
|
-
if (existsSync5(repo))
|
|
567
|
+
if (existsSync5(join5(repo, ".git")) && statSync(join5(repo, ".git")).isFile()) return false;
|
|
568
|
+
if (existsSync5(repo)) renameSync(repo, `${repo}.mirror`);
|
|
644
569
|
await git(["-C", clone, "worktree", "prune"]);
|
|
645
570
|
const onOrigin = branch && (await git(["-C", clone, "rev-parse", "-q", "--verify", `refs/remotes/origin/${branch}`])).ok;
|
|
646
571
|
const tries = onOrigin ? [["worktree", "add", "-q", repo, branch], ["worktree", "add", "-q", "--detach", repo, `origin/${branch}`]] : [["worktree", "add", "-q", "--no-track", "-b", `cb/${name}`, repo, "origin/HEAD"], ["worktree", "add", "-q", repo, `cb/${name}`]];
|
|
647
572
|
for (const args of tries) {
|
|
648
573
|
const r = await git(["-C", clone, ...args]);
|
|
649
574
|
if (r.ok) return true;
|
|
650
|
-
|
|
575
|
+
rmSync(repo, { recursive: true, force: true });
|
|
651
576
|
}
|
|
652
|
-
if (existsSync5(`${repo}.mirror`))
|
|
577
|
+
if (existsSync5(`${repo}.mirror`)) renameSync(`${repo}.mirror`, repo);
|
|
653
578
|
return fail(`Couldn't make a worktree for ${name} in ${tilde(clone)}.`);
|
|
654
579
|
}
|
|
655
580
|
async function setUpWorktree({ clone, folder, name, branch, fresh }) {
|
|
@@ -661,7 +586,7 @@ async function setUpWorktree({ clone, folder, name, branch, fresh }) {
|
|
|
661
586
|
if (copied && fresh) await alignBox(name, repo);
|
|
662
587
|
bar(copied ? `${tilde(repo)} is a worktree of ${tilde(clone)}` : void 0);
|
|
663
588
|
if (!copied) fail(`Couldn't sync ${tilde(repo)} with ${name}. Check that ssh ${name}.box works.`);
|
|
664
|
-
|
|
589
|
+
rmSync(`${repo}.mirror`, { recursive: true, force: true });
|
|
665
590
|
return true;
|
|
666
591
|
}
|
|
667
592
|
async function alignBox(name, repo) {
|
|
@@ -673,7 +598,7 @@ async function alignBox(name, repo) {
|
|
|
673
598
|
}
|
|
674
599
|
function newestTranscript(folder) {
|
|
675
600
|
const sessions = join5(homedir4(), ".claude", "projects", folder.replace(/[^a-zA-Z0-9]/g, "-"));
|
|
676
|
-
return existsSync5(sessions) ? readdirSync2(sessions).filter((f) => f.endsWith(".jsonl")).map((f) => join5(sessions, f)).sort((a, b) =>
|
|
601
|
+
return existsSync5(sessions) ? readdirSync2(sessions).filter((f) => f.endsWith(".jsonl")).map((f) => join5(sessions, f)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0] : void 0;
|
|
677
602
|
}
|
|
678
603
|
function backgroundSession(folder) {
|
|
679
604
|
try {
|
|
@@ -700,7 +625,7 @@ function laptopState(folder) {
|
|
|
700
625
|
return void 0;
|
|
701
626
|
}
|
|
702
627
|
})() : void 0;
|
|
703
|
-
return { branch: branch || void 0, task, working: !!newest && Date.now() -
|
|
628
|
+
return { branch: branch || void 0, task, working: !!newest && Date.now() - statSync(newest).mtimeMs < 9e4 };
|
|
704
629
|
}
|
|
705
630
|
var git = (args) => new Promise((done) => {
|
|
706
631
|
const p = spawn3("git", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
|
|
@@ -814,7 +739,7 @@ function ignored(repo, path) {
|
|
|
814
739
|
}
|
|
815
740
|
function isDirectory(path) {
|
|
816
741
|
try {
|
|
817
|
-
return
|
|
742
|
+
return statSync(path).isDirectory();
|
|
818
743
|
} catch {
|
|
819
744
|
return false;
|
|
820
745
|
}
|
|
@@ -831,7 +756,7 @@ async function prune(boxes) {
|
|
|
831
756
|
}
|
|
832
757
|
}
|
|
833
758
|
async function dropFolder(b) {
|
|
834
|
-
|
|
759
|
+
rmSync(b.folder, { recursive: true, force: true });
|
|
835
760
|
const clone = join5(BOXES, b.env, CLONE);
|
|
836
761
|
if (existsSync5(clone)) await git(["-C", clone, "worktree", "prune"]);
|
|
837
762
|
}
|
|
@@ -857,6 +782,7 @@ async function openLocal({ name, env, here, branch, fresh = false }) {
|
|
|
857
782
|
api("/api/environments")
|
|
858
783
|
]).finally(() => stop());
|
|
859
784
|
const environment = envs.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
|
|
785
|
+
void sendSetup();
|
|
860
786
|
const folder = prepareFolder(name, env, here, got.mcp);
|
|
861
787
|
try {
|
|
862
788
|
writeFileSync4(join5(BOXES, "llms.txt"), await fetch(new URL("/llms.txt", load().server)).then((r) => r.ok ? r.text() : Promise.reject()));
|
|
@@ -878,6 +804,16 @@ async function openLocal({ name, env, here, branch, fresh = false }) {
|
|
|
878
804
|
ok(`Claude closed \xB7 repo synced \xB7 ${name} sleeps once it's quiet ${dim(`\xB7 come back with: box ${name}`)}`);
|
|
879
805
|
process.exit(code2);
|
|
880
806
|
}
|
|
807
|
+
async function sendSetup() {
|
|
808
|
+
const { tar, ...home2 } = localClaudeHome();
|
|
809
|
+
const buf = await tar;
|
|
810
|
+
const sha = buf && buf.length <= 30 << 20 ? createHash("sha256").update(buf).digest("hex") : void 0;
|
|
811
|
+
const put = (body) => api("/api/laptop-setup", { method: "PUT", body: JSON.stringify({ home: home2, sha, ...body }) });
|
|
812
|
+
try {
|
|
813
|
+
if ((await put({})).need && buf) await put({ tgz: buf.toString("base64") });
|
|
814
|
+
} catch {
|
|
815
|
+
}
|
|
816
|
+
}
|
|
881
817
|
var claude = (folder, args) => new Promise((done) => spawn3("claude", args, { cwd: folder, stdio: "inherit", env: { ...process.env, ...process.getuid?.() === 0 && { IS_SANDBOX: "1" } } }).on("error", () => fail("Claude Code is not installed on this laptop. npm i -g @anthropic-ai/claude-code, or use box --remote.")).on("close", (c) => done(c ?? 0)));
|
|
882
818
|
function trustFolder(folder) {
|
|
883
819
|
const file2 = join5(homedir4(), ".claude.json");
|
|
@@ -938,7 +874,7 @@ async function rm(name, opts = {}) {
|
|
|
938
874
|
}
|
|
939
875
|
|
|
940
876
|
// src/remote.ts
|
|
941
|
-
import { createHash } from "node:crypto";
|
|
877
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
942
878
|
import { styleText as styleText4 } from "node:util";
|
|
943
879
|
import { select as select3 } from "@inquirer/prompts";
|
|
944
880
|
import ssh22 from "ssh2";
|
|
@@ -1061,7 +997,7 @@ async function newRemoteBox(env, branch, name, o) {
|
|
|
1061
997
|
async function sendHome(name, tar) {
|
|
1062
998
|
const buf = await tar;
|
|
1063
999
|
if (!buf || buf.length > 30 << 20) return api(`/api/boxes/${name}/home`, { method: "POST", body: "{}" }).catch(() => void 0);
|
|
1064
|
-
const sha =
|
|
1000
|
+
const sha = createHash2("sha256").update(buf).digest("hex");
|
|
1065
1001
|
const post = (body) => api(`/api/boxes/${name}/home`, { method: "POST", body: JSON.stringify(body) });
|
|
1066
1002
|
if ((await post({ sha }).catch(() => ({ need: false }))).need) await post({ sha, tgz: buf.toString("base64") }).catch(() => void 0);
|
|
1067
1003
|
}
|
|
@@ -1226,14 +1162,13 @@ function wakingScreen(box, keys) {
|
|
|
1226
1162
|
var TERMINAL_RESET = "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?1004l\x1B[?2004l\x1B[<u\x1B[?1049l\x1B[?25h\x1B[0m";
|
|
1227
1163
|
|
|
1228
1164
|
// src/update.ts
|
|
1229
|
-
import { execFileSync as execFileSync4, spawn as spawn4, spawnSync
|
|
1165
|
+
import { execFileSync as execFileSync4, spawn as spawn4, spawnSync } from "node:child_process";
|
|
1230
1166
|
import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1231
1167
|
import { basename as basename2, dirname as dirname2, join as join6 } from "node:path";
|
|
1232
1168
|
import { fileURLToPath } from "node:url";
|
|
1233
1169
|
var PACKAGE = "@abuaboud/box";
|
|
1234
1170
|
var LATEST = `https://registry.npmjs.org/${PACKAGE.replace("/", "%2f")}/latest`;
|
|
1235
|
-
var
|
|
1236
|
-
var VERSION = INSTALLED.version;
|
|
1171
|
+
var VERSION = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
1237
1172
|
function npmPrefix() {
|
|
1238
1173
|
const pkgDir = dirname2(dirname2(fileURLToPath(import.meta.url)));
|
|
1239
1174
|
const modules = basename2(dirname2(pkgDir)).startsWith("@") ? dirname2(dirname2(pkgDir)) : dirname2(pkgDir);
|
|
@@ -1245,20 +1180,15 @@ var newer = (a, b) => {
|
|
|
1245
1180
|
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
|
|
1246
1181
|
return false;
|
|
1247
1182
|
};
|
|
1248
|
-
var due = (latest) => INSTALLED.name !== PACKAGE || newer(latest, VERSION);
|
|
1249
1183
|
var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1250
|
-
|
|
1251
|
-
const at = prefix ? ["--prefix", prefix] : [];
|
|
1252
|
-
const box = `${PACKAGE}@${version}`;
|
|
1253
|
-
return INSTALLED.name === PACKAGE ? [["i", "-g", "--prefer-online", ...at, box]] : [["i", "-g", "--prefer-online", "--force", ...at, box], ["rm", "-g", ...at, INSTALLED.name], ["i", "-g", "--prefer-offline", "--force", ...at, box]];
|
|
1254
|
-
}
|
|
1184
|
+
var installArgs = (version, prefix) => ["i", "-g", "--prefer-online", ...prefix ? ["--prefix", prefix] : [], `${PACKAGE}@${version}`];
|
|
1255
1185
|
async function updateFirst() {
|
|
1256
1186
|
const prefix = npmPrefix();
|
|
1257
1187
|
if (!prefix || process.env.CBOX_NO_UPDATE || process.env.CBOX_UPDATED) return;
|
|
1258
1188
|
const latest = await fetch(LATEST, { signal: AbortSignal.timeout(2500) }).then((r) => r.json()).then((j) => j.version, () => void 0);
|
|
1259
|
-
if (!latest || !
|
|
1189
|
+
if (!latest || !newer(latest, VERSION)) return;
|
|
1260
1190
|
const stop = progress([[0, `Updating box ${VERSION} \u2192 ${latest}`]]);
|
|
1261
|
-
const done =
|
|
1191
|
+
const done = spawnSync(NPM, installArgs(latest, prefix), { stdio: "ignore" }).status === 0;
|
|
1262
1192
|
stop();
|
|
1263
1193
|
if (!done) return miss(`Couldn't update box to ${latest}, so carrying on with ${VERSION}`);
|
|
1264
1194
|
ok(`Updated box ${dim(VERSION)} \u2192 ${latest}`);
|
|
@@ -1266,7 +1196,7 @@ async function updateFirst() {
|
|
|
1266
1196
|
writeFileSync5(join6(CONFIG_DIR, "update.json"), JSON.stringify({ version: latest, checkedAt: Date.now() }));
|
|
1267
1197
|
} catch {
|
|
1268
1198
|
}
|
|
1269
|
-
const again =
|
|
1199
|
+
const again = spawnSync(process.execPath, process.argv.slice(1), { stdio: "inherit", env: { ...process.env, CBOX_UPDATED: "1" } });
|
|
1270
1200
|
process.exit(again.status ?? 0);
|
|
1271
1201
|
}
|
|
1272
1202
|
function autoUpdate() {
|
|
@@ -1279,7 +1209,7 @@ function autoUpdate() {
|
|
|
1279
1209
|
} catch {
|
|
1280
1210
|
}
|
|
1281
1211
|
if (state.version && state.version !== VERSION) ok(`Updated box ${dim(state.version)} \u2192 ${VERSION}`);
|
|
1282
|
-
const check =
|
|
1212
|
+
const check = !state.checkedAt || Date.now() - state.checkedAt > 12 * 36e5;
|
|
1283
1213
|
try {
|
|
1284
1214
|
mkdirSync5(CONFIG_DIR, { recursive: true });
|
|
1285
1215
|
writeFileSync5(file2, JSON.stringify({ version: VERSION, checkedAt: check ? Date.now() : state.checkedAt }));
|
|
@@ -1289,18 +1219,18 @@ function autoUpdate() {
|
|
|
1289
1219
|
if (!check) return;
|
|
1290
1220
|
const job = `fetch(${JSON.stringify(LATEST)}).then(r => r.json()).then(({ version }) => {
|
|
1291
1221
|
const newer = (a, b) => { const x = a.split('.').map(Number), y = b.split('.').map(Number); for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0); return false }
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1222
|
+
if (!newer(version, ${JSON.stringify(VERSION)})) return
|
|
1223
|
+
require('node:child_process').execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ${JSON.stringify(installArgs("{v}", prefix))}.map((a) => a.replace('{v}', version)), { stdio: 'ignore' })
|
|
1294
1224
|
}).catch(() => {})`;
|
|
1295
1225
|
spawn4(process.execPath, ["-e", job], { detached: true, stdio: "ignore" }).on("error", () => {
|
|
1296
1226
|
}).unref();
|
|
1297
1227
|
}
|
|
1298
1228
|
async function upgrade() {
|
|
1299
1229
|
const latest = await fetch(LATEST).then((r) => r.json()).then((j) => j.version).catch(() => fail("Could not reach the npm registry."));
|
|
1300
|
-
if (!
|
|
1230
|
+
if (!newer(latest, VERSION)) return ok(`box ${VERSION} is the latest`);
|
|
1301
1231
|
console.log(dim(`Updating box ${VERSION} \u2192 ${latest}\u2026`));
|
|
1302
1232
|
try {
|
|
1303
|
-
|
|
1233
|
+
execFileSync4(NPM, installArgs(latest, npmPrefix()), { stdio: "inherit" });
|
|
1304
1234
|
} catch {
|
|
1305
1235
|
fail(`npm could not update box. Try: npm i -g ${PACKAGE}@latest`);
|
|
1306
1236
|
}
|
|
@@ -1368,7 +1298,6 @@ var quiet = ["proxy", "sync"].includes(process.argv[2] ?? "");
|
|
|
1368
1298
|
if (quiet) console.log = console.error;
|
|
1369
1299
|
if (!quiet && !process.env.CBOX_UPDATED && !process.argv.slice(2).some((a) => ["-V", "--version", "-h", "--help", "help"].includes(a))) console.log(dim(`box ${VERSION}`));
|
|
1370
1300
|
if (!quiet) {
|
|
1371
|
-
moveHome();
|
|
1372
1301
|
autoUpdate();
|
|
1373
1302
|
ensureSshConfig();
|
|
1374
1303
|
}
|
package/package.json
CHANGED