@abuaboud/box 0.7.21 → 0.7.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cbox.mjs +49 -41
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -13,10 +13,11 @@ your laptop in a git worktree whose builds, tests and servers run on the agent.
|
|
|
13
13
|
| Command | What it does |
|
|
14
14
|
| --- | --- |
|
|
15
15
|
| `skyfleet` | Pick one of your agents, or make a new one |
|
|
16
|
-
| `skyfleet new [-e env] [--size small\|medium\|large] [--
|
|
16
|
+
| `skyfleet new [-e env] [--size small\|medium\|large] [--awake [days]]` | Always make a fresh agent |
|
|
17
17
|
| `skyfleet <name>` | Back to an agent |
|
|
18
18
|
| `skyfleet ls` | List your agents and where Claude runs |
|
|
19
19
|
| `skyfleet code [env]` | Open every agent of an environment in one VS Code window |
|
|
20
|
+
| `skyfleet awake <name> [days]` | Keep an agent awake for days (default 7), or renew it; `0` lets it sleep |
|
|
20
21
|
| `skyfleet rm <name>` | Delete an agent; unpushed work goes to `cb/<name>` first |
|
|
21
22
|
| `skyfleet --remote` | Run Claude on the agent instead, so it keeps going with the laptop shut |
|
|
22
23
|
| `skyfleet upgrade` | Update now (skyfleet also updates itself; `CBOX_NO_UPDATE=1` turns that off) |
|
|
@@ -24,4 +25,4 @@ your laptop in a git worktree whose builds, tests and servers run on the agent.
|
|
|
24
25
|
Every agent is also an SSH host, `<name>.skyfleet`, for ssh, scp, rsync and VS Code Remote-SSH (`<name>.box` still
|
|
25
26
|
works). Local agents live in `~/.box`. `box` still runs the same command.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
Node 22.18 or newer.
|
package/dist/cbox.mjs
CHANGED
|
@@ -199,22 +199,24 @@ function label(b) {
|
|
|
199
199
|
if (b.state !== "running") return styleText("yellow", b.state);
|
|
200
200
|
if (b.busy) return styleText("green", "\u25CF working");
|
|
201
201
|
if (!b.idleMinutes) return styleText("green", "awake");
|
|
202
|
-
const left = b.idleMinutes * 6e4 - (Date.now() - new Date(b.lastActiveAt
|
|
202
|
+
const left = b.idleMinutes * 6e4 - (Date.now() - new Date(b.lastActiveAt).getTime());
|
|
203
203
|
return styleText("yellow", `\u263E sleeps in ${span(Math.max(6e4, left))}`);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
// src/brand.ts
|
|
207
207
|
var BRANDS = {
|
|
208
|
-
box: { name: "Box", command: "box", server: "https://box.
|
|
208
|
+
box: { name: "Box", command: "box", server: "https://box.activepieces.dev" },
|
|
209
209
|
skyfleet: { name: "Skyfleet", command: "skyfleet", server: "https://skyfleet.run" }
|
|
210
210
|
};
|
|
211
211
|
var brand = process.env.CB_BRAND === "skyfleet" ? BRANDS.skyfleet : BRANDS.box;
|
|
212
212
|
|
|
213
213
|
// src/api.ts
|
|
214
214
|
var file = join2(CONFIG_DIR, "config.json");
|
|
215
|
+
var MOVED = { "https://box.abuaboud.me": "https://box.activepieces.dev" };
|
|
215
216
|
var load = () => {
|
|
216
217
|
if (!existsSync2(file)) return { server: process.env.CB_SERVER ?? brand.server };
|
|
217
|
-
|
|
218
|
+
const config = JSON.parse(readFileSync2(file, "utf8"));
|
|
219
|
+
return { ...config, server: MOVED[config.server] ?? config.server };
|
|
218
220
|
};
|
|
219
221
|
var save = (c) => {
|
|
220
222
|
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
@@ -230,7 +232,7 @@ async function api(path, init = {}, retried = false) {
|
|
|
230
232
|
save({ ...c, token: void 0 });
|
|
231
233
|
return api(path, init, true);
|
|
232
234
|
}
|
|
233
|
-
if (!res.ok) fail(body.error ?? `Request failed (${res.status})`);
|
|
235
|
+
if (!res.ok) fail(body.error?.message ?? body.error ?? `Request failed (${res.status})`);
|
|
234
236
|
return body;
|
|
235
237
|
}
|
|
236
238
|
async function login(server) {
|
|
@@ -258,10 +260,8 @@ async function login(server) {
|
|
|
258
260
|
ok(`Signed in as ${me.user.email} ${dim(`\xB7 ${me.org.name}`)}`);
|
|
259
261
|
console.log();
|
|
260
262
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return box ?? fail(`No agent named ${name}. ${brand.command} ls lists them.`);
|
|
264
|
-
}
|
|
263
|
+
var findBox = (name) => api(`/api/boxes/${encodeURIComponent(name)}`);
|
|
264
|
+
var awakeOf = (o) => o.awake === void 0 ? {} : { idleMinutes: 0, awakeDays: o.awake === true ? 7 : Number(o.awake) };
|
|
265
265
|
|
|
266
266
|
// src/environments.ts
|
|
267
267
|
import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
|
|
@@ -269,14 +269,15 @@ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:f
|
|
|
269
269
|
import { join as join3 } from "node:path";
|
|
270
270
|
import { styleText as styleText3 } from "node:util";
|
|
271
271
|
import { input, select } from "@inquirer/prompts";
|
|
272
|
+
var MACHINES = { small: "2 vCPU, 4 GB", medium: "4 vCPU, 8 GB", large: "8 vCPU, 16 GB" };
|
|
272
273
|
async function chooseEnvironment(envName, here) {
|
|
273
|
-
const envs = await api("/api/environments");
|
|
274
|
+
const { data: envs } = await api("/api/environments");
|
|
274
275
|
if (envName) return waitReady(envs.find((e) => e.name === envName) ?? fail(`No environment named ${envName}.`));
|
|
275
276
|
const mine = here ? envs.filter((e) => e.repo.toLowerCase() === here.repo.toLowerCase()) : [];
|
|
276
277
|
const setUp = async () => setUpEnvironment(here?.repo ?? await askRepo(), here?.dir, envs);
|
|
277
278
|
if (!envs.length) return waitReady(await setUp());
|
|
278
279
|
if (!process.stdin.isTTY && mine.length === 1) return waitReady(mine[0]);
|
|
279
|
-
const label2 = (e) => `${e.name.padEnd(16)}${dim(`${mine.includes(e) ? "this repo" : e.repo} \xB7 ${e.size
|
|
280
|
+
const label2 = (e) => `${e.name.padEnd(16)}${dim(`${mine.includes(e) ? "this repo" : e.repo} \xB7 ${e.size}${e.status === "ready" ? "" : ` \xB7 ${e.status}`}`)}`;
|
|
280
281
|
const create = { name: styleText3("cyan", here ? `+ Set up a new environment for ${here.repo}` : "+ Set up a new environment"), value: "" };
|
|
281
282
|
const pick = await select({
|
|
282
283
|
message: here ? `Environment for ${here.repo}${here.branch ? dim(` \xB7 branch ${here.branch}`) : ""}` : `Environment ${dim("(you're not in a repo)")}`,
|
|
@@ -360,12 +361,11 @@ async function setUpEnvironment(repo, dir, envs) {
|
|
|
360
361
|
let name = repo.split("/")[1].toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").slice(0, 28) || "env";
|
|
361
362
|
for (let i = 2, base = name; envs.some((e) => e.name === name); i++) name = `${base}-${i}`;
|
|
362
363
|
let setup2 = detected?.setup ?? "", size = sized.size, secrets = "";
|
|
363
|
-
const sizes = { small: "2 vCPU, 4 GB", medium: "4 vCPU, 8 GB", large: "8 vCPU, 16 GB" };
|
|
364
364
|
for (; ; ) {
|
|
365
365
|
console.log();
|
|
366
366
|
console.log(` ${dim("Name ")} ${name}`);
|
|
367
367
|
console.log(` ${dim("Repo ")} ${repo}`);
|
|
368
|
-
console.log(` ${dim("Machine ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${
|
|
368
|
+
console.log(` ${dim("Machine ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${MACHINES[size]}${size === sized.size ? ` (${sized.why})` : ""}`)}`);
|
|
369
369
|
console.log(` ${dim("Setup ")} ${setup2 ? styleText3("cyan", setup2) : dim("none")} ${dim(detected && setup2 === detected.setup ? `\xB7 from ${detected.from}` : setup2 ? "" : "\xB7 agents start from a plain clone")}`);
|
|
370
370
|
if (names.length) console.log(` ${dim("Secrets ")} ${names.join(", ")} ${dim(secrets ? "\xB7 values copied from .env" : "\xB7 names from .env.example, values empty")}`);
|
|
371
371
|
console.log();
|
|
@@ -384,32 +384,33 @@ async function setUpEnvironment(repo, dir, envs) {
|
|
|
384
384
|
}
|
|
385
385
|
if (choice === "edit") {
|
|
386
386
|
name = await input({ message: "Name", default: name, validate: (v) => /^[a-z0-9][a-z0-9-]{0,30}$/.test(v) || "Lowercase letters, digits and dashes" });
|
|
387
|
-
size = await select({ message: "Machine", default: size, choices:
|
|
387
|
+
size = await select({ message: "Machine", default: size, choices: machineChoices });
|
|
388
388
|
setup2 = (await input({ message: "Setup script (runs once in the snapshot; empty for none)", default: setup2 })).trim();
|
|
389
389
|
continue;
|
|
390
390
|
}
|
|
391
391
|
break;
|
|
392
392
|
}
|
|
393
393
|
await ensureGitHub(repo);
|
|
394
|
-
const
|
|
394
|
+
const made = await api("/api/environments", { method: "POST", body: JSON.stringify({ name, repo, size, setup: setup2, secrets }) });
|
|
395
395
|
ok(`Environment ${styleText3("cyan", name)} created`);
|
|
396
|
-
return
|
|
396
|
+
return made;
|
|
397
397
|
}
|
|
398
|
+
var SIZES = ["small", "medium", "large"];
|
|
399
|
+
var machineChoices = SIZES.map((value) => ({ name: `${value.padEnd(8)}${dim(MACHINES[value])}`, value }));
|
|
398
400
|
async function ensureGitHub(repo) {
|
|
399
401
|
const open = (url) => {
|
|
400
402
|
if (process.stdout.isTTY) spawn2(process.platform === "darwin" ? "open" : "xdg-open", [url], { stdio: "ignore", detached: true }).on("error", () => {
|
|
401
403
|
}).unref();
|
|
402
404
|
};
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
if (!r.linked) {
|
|
405
|
+
const linked = async () => (await api("/api/accounts")).data.some((a) => a.id === "github" && a.status === "ready");
|
|
406
|
+
if (!await linked()) {
|
|
406
407
|
const url = new URL("/connect/github", load().server).toString();
|
|
407
408
|
console.log(`
|
|
408
409
|
${styleText3("yellow", "!")} Connect GitHub so ${brand.name} can clone ${repo} and your agents can push as you.`);
|
|
409
410
|
console.log(dim(` Opening ${url}`));
|
|
410
411
|
open(url);
|
|
411
412
|
const bar = progress([[0, "Waiting for you to connect GitHub in the browser"]], { pace: 6e4 });
|
|
412
|
-
for (const end = Date.now() + 10 * 6e4; !
|
|
413
|
+
for (const end = Date.now() + 10 * 6e4; !await linked(); ) {
|
|
413
414
|
if (Date.now() > end) {
|
|
414
415
|
bar();
|
|
415
416
|
fail(`Gave up waiting after 10 minutes. Run ${brand.command} again when it's done.`);
|
|
@@ -420,7 +421,7 @@ ${styleText3("yellow", "!")} Connect GitHub so ${brand.name} can clone ${repo} a
|
|
|
420
421
|
ok("GitHub connected");
|
|
421
422
|
}
|
|
422
423
|
const isPublic = await fetch(`https://api.github.com/repos/${repo}`, { signal: AbortSignal.timeout(5e3) }).then((res) => res.ok ? res.json() : null).then((j) => j && !j.private).catch(() => false);
|
|
423
|
-
if (!isPublic && !
|
|
424
|
+
if (!isPublic && !(await api("/api/github/repos")).data.some((p) => p.fullName.toLowerCase() === repo.toLowerCase())) fail(`Your GitHub login can't reach ${repo}.`);
|
|
424
425
|
}
|
|
425
426
|
|
|
426
427
|
// src/local.ts
|
|
@@ -489,7 +490,7 @@ async function proxy(host) {
|
|
|
489
490
|
// src/local.ts
|
|
490
491
|
async function newLocalBox(env, branch, name, o) {
|
|
491
492
|
const stop = progress([[0, "Finding a machine"], [900, `Unpacking the ${env} snapshot`], [2500, "Connecting GitHub"], [9e3, "Bigger machines take a few extra seconds"]]);
|
|
492
|
-
const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, agent: "local", size: o.size,
|
|
493
|
+
const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, agent: "local", size: o.size, ...awakeOf(o), sshKey: sshKeys(laptopKey().publicKey) }) }).finally(() => stop());
|
|
493
494
|
ok(`${box.name} is ready ${dim(`\xB7 ssh ${box.name}.${brand.command}`)}`);
|
|
494
495
|
await openLocal({ name: box.name, env, here: localCopy(), branch, fresh: true });
|
|
495
496
|
}
|
|
@@ -761,7 +762,7 @@ async function prune(boxes) {
|
|
|
761
762
|
const email = load().email;
|
|
762
763
|
const mine = localBoxes().filter((b) => b.owner && b.owner === email);
|
|
763
764
|
if (!mine.length) return;
|
|
764
|
-
const alive = new Set((boxes ?? await api("/api/boxes")).filter((b) => b.state !== "gone").map((b) => b.name));
|
|
765
|
+
const alive = new Set((boxes ?? (await api("/api/boxes")).data).filter((b) => b.state !== "gone").map((b) => b.name));
|
|
765
766
|
for (const b of mine) {
|
|
766
767
|
if (alive.has(b.name)) continue;
|
|
767
768
|
await dropFolder(b);
|
|
@@ -792,7 +793,7 @@ async function openLocal({ name, env, here, branch, fresh = false }) {
|
|
|
792
793
|
const stop = progress([[0, `Opening ${name}`], [1500, "Waking the agent up"]]);
|
|
793
794
|
const [got, envs] = await Promise.all([
|
|
794
795
|
api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) }),
|
|
795
|
-
api("/api/environments")
|
|
796
|
+
api("/api/environments").then((r) => r.data)
|
|
796
797
|
]).finally(() => stop());
|
|
797
798
|
const environment = envs.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
|
|
798
799
|
void sendSetup().catch(() => void 0);
|
|
@@ -848,7 +849,7 @@ async function sync(name, direction, folder) {
|
|
|
848
849
|
await (direction === "push" ? push(name, folder) : pull(name, folder));
|
|
849
850
|
}
|
|
850
851
|
async function code(envArg) {
|
|
851
|
-
const boxes = await api("/api/boxes");
|
|
852
|
+
const { data: boxes } = await api("/api/boxes");
|
|
852
853
|
await prune(boxes);
|
|
853
854
|
const here = localBoxes().filter((b) => existsSync5(join5(b.folder, "repo")));
|
|
854
855
|
const envs = [...new Set(here.map((b) => b.env))].sort();
|
|
@@ -856,18 +857,18 @@ async function code(envArg) {
|
|
|
856
857
|
const env = envArg ?? (envs.length === 1 ? envs[0] : await select2({ message: "Open the agents of which environment?", choices: envs.map((e) => ({ name: `${e.padEnd(18)}${dim(`${here.filter((b) => b.env === e).length} agents`)}`, value: e })) }));
|
|
857
858
|
const group = here.filter((b) => b.env === env);
|
|
858
859
|
if (!group.length) fail(`No agents from ${env} on this laptop.`);
|
|
859
|
-
const
|
|
860
|
-
const environment = (await api("/api/environments")).find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
|
|
860
|
+
const awake2 = new Set(boxes.filter((b) => b.state === "running").map((b) => b.name));
|
|
861
|
+
const environment = (await api("/api/environments")).data.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
|
|
861
862
|
const clone = await ensureClone(environment);
|
|
862
863
|
const migrated = [];
|
|
863
|
-
for (const b of group.filter((g) =>
|
|
864
|
+
for (const b of group.filter((g) => awake2.has(g.name))) {
|
|
864
865
|
if (await setUpWorktree({ clone, folder: b.folder, name: b.name, branch: boxes.find((x) => x.name === b.name)?.branch ?? void 0, fresh: false })) migrated.push(b.name);
|
|
865
866
|
}
|
|
866
867
|
const workspace = join5(BOXES, env, `${env}.code-workspace`);
|
|
867
868
|
writeFileSync4(workspace, JSON.stringify({ folders: group.map((b) => ({ name: basename(b.folder), path: `${basename(b.folder)}/repo` })), settings: { "files.exclude": { "**/node_modules": true } } }, null, 2));
|
|
868
869
|
spawn3("code", [workspace], { stdio: "ignore", detached: true }).on("error", () => miss(`VS Code's code command isn't installed. In VS Code run: Shell Command: Install 'code' command in PATH, or open ${tilde(workspace)}.`)).unref();
|
|
869
870
|
ok(`${group.length} ${env} agent${group.length === 1 ? "" : "s"} in one window ${dim(`\xB7 ${tilde(workspace)}`)}`);
|
|
870
|
-
await Promise.all(group.filter((b) =>
|
|
871
|
+
await Promise.all(group.filter((b) => awake2.has(b.name) && !migrated.includes(b.name)).map((b) => pull(b.name, b.folder, true)));
|
|
871
872
|
const stops = group.map((b) => watchRepo(b.name, b.folder));
|
|
872
873
|
console.log(dim("Syncing your saves to the agents. Leave this open; ctrl+c stops syncing."));
|
|
873
874
|
await new Promise((resolve) => process.once("SIGINT", resolve));
|
|
@@ -986,7 +987,6 @@ var mcpCount = (s) => {
|
|
|
986
987
|
};
|
|
987
988
|
async function newRemoteBox(env, branch, name, o) {
|
|
988
989
|
const agent = o.agent;
|
|
989
|
-
const idle = o.alwaysOn ? "0" : void 0;
|
|
990
990
|
const { mcpServers, mcpOAuth, skipped } = localMcp();
|
|
991
991
|
ok(`${mcpCount(mcpServers)} will be copied into the agent ${dim(skipped.length ? `(skipping local-only: ${skipped.join(", ")})` : "")}`);
|
|
992
992
|
const claudeHome = localClaudeHome();
|
|
@@ -1001,9 +1001,9 @@ async function newRemoteBox(env, branch, name, o) {
|
|
|
1001
1001
|
[9e3, "Bigger machines take a few extra seconds"]
|
|
1002
1002
|
]);
|
|
1003
1003
|
const { tar, ...home2 } = claudeHome;
|
|
1004
|
-
const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, size: o.size,
|
|
1004
|
+
const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, size: o.size, ...awakeOf(o), mcpServers, mcpOAuth, claudeHome: home2, sshKey: sshKeys(laptopKey().publicKey), agent }) }).finally(() => stop());
|
|
1005
1005
|
if (home2.pending) void sendHome(box.name, tar);
|
|
1006
|
-
await attach(box.name,
|
|
1006
|
+
await attach(box.name, box.ssh);
|
|
1007
1007
|
}
|
|
1008
1008
|
async function sendHome(name, tar) {
|
|
1009
1009
|
const buf = await tar;
|
|
@@ -1038,7 +1038,7 @@ async function dial({ user, hosts }, privateKey, attempts) {
|
|
|
1038
1038
|
}
|
|
1039
1039
|
async function attach(name, ready) {
|
|
1040
1040
|
if (!name) {
|
|
1041
|
-
const boxes = await api("/api/boxes");
|
|
1041
|
+
const { data: boxes } = await api("/api/boxes");
|
|
1042
1042
|
if (!boxes.length) return console.log(dim(`No agents yet. Run ${brand.command}.`));
|
|
1043
1043
|
name = await select3({ message: "Attach to", choices: boxes.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}`, value: b.name })) });
|
|
1044
1044
|
}
|
|
@@ -1072,7 +1072,7 @@ async function attach(name, ready) {
|
|
|
1072
1072
|
Detached. ${boxName} sleeps once Claude is done. ${brand.command} ${boxName} to return.`));
|
|
1073
1073
|
process.exit(0);
|
|
1074
1074
|
}
|
|
1075
|
-
let ssh = ready
|
|
1075
|
+
let ssh = ready, attempts = 4, announced = false;
|
|
1076
1076
|
for (; ; ) {
|
|
1077
1077
|
if (!ssh) {
|
|
1078
1078
|
attempts = 4;
|
|
@@ -1151,12 +1151,12 @@ function centered(lines) {
|
|
|
1151
1151
|
}
|
|
1152
1152
|
function sleepScreen(box) {
|
|
1153
1153
|
const since = box.asleepSince ? Date.now() - new Date(box.asleepSince).getTime() : 0;
|
|
1154
|
-
const saved = since / 36e5 *
|
|
1154
|
+
const saved = since / 36e5 * RATES[box.size];
|
|
1155
1155
|
centered([
|
|
1156
1156
|
styleText4("blue", "\u263E"),
|
|
1157
1157
|
"",
|
|
1158
1158
|
styleText4("bold", `${box.name} is asleep`),
|
|
1159
|
-
dim(`Last used ${span(Date.now() - new Date(box.lastActiveAt
|
|
1159
|
+
dim(`Last used ${span(Date.now() - new Date(box.lastActiveAt).getTime())} ago. Your files and the conversation are saved.`),
|
|
1160
1160
|
"",
|
|
1161
1161
|
`Press any key to wake it ${dim("\xB7")} ctrl+] to leave`,
|
|
1162
1162
|
"",
|
|
@@ -1258,7 +1258,7 @@ var ANIMALS = ["otter", "heron", "lynx", "moth", "yak", "orca", "wren", "newt",
|
|
|
1258
1258
|
var NAME = /^[a-z0-9][a-z0-9-]{0,30}$/;
|
|
1259
1259
|
async function newBox(o = {}) {
|
|
1260
1260
|
await updateFirst();
|
|
1261
|
-
if (o.size && !
|
|
1261
|
+
if (o.size && !Object.hasOwn(MACHINES, o.size)) fail("Pick --size small, medium or large.");
|
|
1262
1262
|
if (o.name !== void 0 && !NAME.test(o.name)) fail("Name the agent with lowercase letters, digits and dashes, like webhook-retry.");
|
|
1263
1263
|
const here = gitRepo();
|
|
1264
1264
|
const picked = await chooseEnvironment(o.env, here);
|
|
@@ -1278,11 +1278,11 @@ async function askName(branch) {
|
|
|
1278
1278
|
return picked.trim() || suggestion;
|
|
1279
1279
|
}
|
|
1280
1280
|
async function home(name, o) {
|
|
1281
|
-
const making = o.env || o.remote || o.agent || o.size || o.
|
|
1282
|
-
if (name && making) fail(`--env, --remote, --agent, --size and --
|
|
1281
|
+
const making = o.env || o.remote || o.agent || o.size || o.awake;
|
|
1282
|
+
if (name && making) fail(`--env, --remote, --agent, --size and --awake only shape a new agent. Open ${name} with ${brand.command} ${name}, or make a new one with ${brand.command} new --name <name> and the options.`);
|
|
1283
1283
|
if (name) return openBox(name);
|
|
1284
1284
|
const email = load().email;
|
|
1285
|
-
const all = await api("/api/boxes");
|
|
1285
|
+
const { data: all } = await api("/api/boxes");
|
|
1286
1286
|
await prune(all);
|
|
1287
1287
|
const mine = all.filter((b) => b.state !== "gone" && (!email || b.owner === email));
|
|
1288
1288
|
if (!mine.length || !process.stdin.isTTY || making) return newBox(o);
|
|
@@ -1299,7 +1299,7 @@ async function openBox(name) {
|
|
|
1299
1299
|
}
|
|
1300
1300
|
async function ls() {
|
|
1301
1301
|
const email = load().email;
|
|
1302
|
-
const all = await api("/api/boxes");
|
|
1302
|
+
const { data: all } = await api("/api/boxes");
|
|
1303
1303
|
await prune(all);
|
|
1304
1304
|
const mine = all.filter((b) => !email || b.owner === email);
|
|
1305
1305
|
if (!mine.length) return console.log(dim(`No agents of yours. Run ${brand.command}.`));
|
|
@@ -1310,6 +1310,13 @@ async function ls() {
|
|
|
1310
1310
|
console.log(`${b.name.padEnd(width)}${l}${" ".repeat(Math.max(1, 20 - l.replace(/\x1b\[[0-9;]*m/g, "").length))}${dim(b.agent === "local" ? "laptop" : "remote")}`);
|
|
1311
1311
|
}
|
|
1312
1312
|
}
|
|
1313
|
+
async function awake(name, days = "7") {
|
|
1314
|
+
const n = Number(days);
|
|
1315
|
+
if (!Number.isInteger(n) || n < 0 || n > 30) fail("Days must be from 1 to 30, or 0 to let it sleep.");
|
|
1316
|
+
await api(`/api/boxes/${(await findBox(name)).id}`, { method: "PATCH", body: JSON.stringify({ awakeDays: n }) });
|
|
1317
|
+
if (n === 0) return ok(`${name} sleeps when idle again`);
|
|
1318
|
+
ok(`${name} stays awake until ${new Date(Date.now() + n * 864e5).toLocaleDateString(void 0, { month: "short", day: "numeric" })} ${dim(`\xB7 run ${brand.command} awake ${name} again to renew`)}`);
|
|
1319
|
+
}
|
|
1313
1320
|
async function setup() {
|
|
1314
1321
|
const sent = await sendSetup();
|
|
1315
1322
|
const n = (k, word) => `${k} ${word}${k === 1 ? "" : "s"}`;
|
|
@@ -1337,11 +1344,12 @@ if (!quiet) {
|
|
|
1337
1344
|
autoUpdate();
|
|
1338
1345
|
ensureSshConfig();
|
|
1339
1346
|
}
|
|
1340
|
-
var boxOptions = (c) => c.option("-e, --env <name>", "environment").option("-n, --name <name>", "name the agent, like webhook-retry (default: asked, or generated)").option("--remote", "run Claude on the machine instead of this laptop").option("--agent <name>", "claude (default) or codex; codex implies --remote").option("--size <size>", "small, medium or large (default: the environment's)").option("--
|
|
1347
|
+
var boxOptions = (c) => c.option("-e, --env <name>", "environment").option("-n, --name <name>", "name the agent, like webhook-retry (default: asked, or generated)").option("--remote", "run Claude on the machine instead of this laptop").option("--agent <name>", "claude (default) or codex; codex implies --remote").option("--size <size>", "small, medium or large (default: the environment's)").option("--awake [days]", "stay awake this many days (default 7), for example for a demo; renew with awake <agent>");
|
|
1341
1348
|
var cli = boxOptions(new Command(brand.command).enablePositionalOptions().description("Dev machines for Claude: Claude on your laptop, the work in the cloud").version(VERSION, "-v, --version").argument("[agent]", "open this agent")).action((name, o) => home(name, o));
|
|
1342
1349
|
boxOptions(cli.command("new").description("always make a fresh agent")).action((o) => newBox(o));
|
|
1343
1350
|
cli.command("ls").description("list your agents").action(ls);
|
|
1344
1351
|
cli.command("code [env]").description("open an environment's agents in one VS Code window, each a git worktree, and sync your saves").action((env) => code(env));
|
|
1352
|
+
cli.command("awake <agent> [days]").description("keep an agent awake for days (default 7), or renew it; 0 lets it sleep").action(awake);
|
|
1345
1353
|
cli.command("rm <agent>").option("-f, --force", "delete even while someone is attached or Claude is working").description("delete an agent, pushing unpushed work to cb/<agent>").action(rm);
|
|
1346
1354
|
cli.command("submit <issue...>").option("-e, --env <name>", "environment (default: this repo's, or asked)").description("start one agent per issue; the first line of each issue is its title").action(submit);
|
|
1347
1355
|
cli.command("setup").description(`send your skills, plugins and CLAUDE.md for @${brand.name} and webhooks, and check Claude and GitHub`).action(setup);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abuaboud/box",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.23",
|
|
4
4
|
"description": "skyfleet: Claude Code on your laptop, the work on a cloud dev machine (a ship) you reach as <name>.skyfleet, with your team databases through MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,10 +25,9 @@
|
|
|
25
25
|
"cli",
|
|
26
26
|
"mcp"
|
|
27
27
|
],
|
|
28
|
-
"homepage": "https://skyfleet.run",
|
|
29
28
|
"repository": {
|
|
30
29
|
"type": "git",
|
|
31
|
-
"url": "git+https://github.com/abuaboud/
|
|
30
|
+
"url": "git+https://github.com/abuaboud/skyfleet.git",
|
|
32
31
|
"directory": "packages/cli"
|
|
33
32
|
},
|
|
34
33
|
"author": "Mohammad AbuAboud",
|
|
@@ -40,6 +39,7 @@
|
|
|
40
39
|
"ssh2": "^1.17.0"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
42
|
+
"@box/common": "*",
|
|
43
43
|
"esbuild": "^0.25.12"
|
|
44
44
|
}
|
|
45
45
|
}
|