@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.
Files changed (2) hide show
  1. package/dist/cbox.mjs +114 -185
  2. 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, spawnSync } from "node:child_process";
15
+ import { execFile, execFileSync } from "node:child_process";
16
16
  import { gzip } from "node:zlib";
17
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from "node:fs";
18
- import { homedir as homedir2, userInfo } from "node:os";
19
- import { join, sep } from "node:path";
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
- // src/ui.ts
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(homedir2(), ".claude.json");
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(homedir2(), ".claude");
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(homedir2(), ".ssh");
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(homedir2(), ".claude", ".credentials.json");
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
- const c = JSON.parse(readFileSync2(file, "utf8"));
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 { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync2, watch, writeFileSync as writeFileSync4 } from "node:fs";
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, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
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
- const renamed = now.replace(/^Include ~\/\.ssh\/craftboxes$/m, "Include ~/.ssh/box");
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
- ${renamed}`;
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
- renameSync2(legacy, folder);
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
- rmSync2(partial, { recursive: true, force: true });
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
- renameSync2(partial, clone);
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")) && statSync2(join5(repo, ".git")).isFile()) return false;
643
- if (existsSync5(repo)) renameSync2(repo, `${repo}.mirror`);
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
- rmSync2(repo, { recursive: true, force: true });
575
+ rmSync(repo, { recursive: true, force: true });
651
576
  }
652
- if (existsSync5(`${repo}.mirror`)) renameSync2(`${repo}.mirror`, repo);
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
- rmSync2(`${repo}.mirror`, { recursive: true, force: true });
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) => statSync2(b).mtimeMs - statSync2(a).mtimeMs)[0] : void 0;
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() - statSync2(newest).mtimeMs < 9e4 };
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 statSync2(path).isDirectory();
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
- rmSync2(b.folder, { recursive: true, force: true });
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 = createHash("sha256").update(buf).digest("hex");
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 as spawnSync2 } from "node:child_process";
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 INSTALLED = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
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
- function installSteps(version, prefix) {
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 || !due(latest)) return;
1189
+ if (!latest || !newer(latest, VERSION)) return;
1260
1190
  const stop = progress([[0, `Updating box ${VERSION} \u2192 ${latest}`]]);
1261
- const done = installSteps(latest, prefix).every((args) => spawnSync2(NPM, args, { stdio: "ignore" }).status === 0);
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 = spawnSync2(process.execPath, process.argv.slice(1), { stdio: "inherit", env: { ...process.env, CBOX_UPDATED: "1" } });
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 = INSTALLED.name !== PACKAGE || !state.checkedAt || Date.now() - state.checkedAt > 12 * 36e5;
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 (${JSON.stringify(INSTALLED.name === PACKAGE)} && !newer(version, ${JSON.stringify(VERSION)})) return
1293
- for (const args of ${JSON.stringify(installSteps("{v}", prefix))}) require('node:child_process').execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args.map((a) => a.replace('{v}', version)), { stdio: 'ignore' })
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 (!due(latest)) return ok(`box ${VERSION} is the latest`);
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
- for (const args of installSteps(latest, npmPrefix())) execFileSync4(NPM, args, { stdio: "inherit" });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abuaboud/box",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
4
4
  "description": "box: Claude Code on your laptop, the work on a cloud dev machine you reach as <name>.box, with your team databases through MCP.",
5
5
  "type": "module",
6
6
  "bin": {