@abuaboud/box 0.7.20 → 0.7.22
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 +46 -40
- 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,13 +199,13 @@ 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;
|
|
@@ -230,7 +230,7 @@ async function api(path, init = {}, retried = false) {
|
|
|
230
230
|
save({ ...c, token: void 0 });
|
|
231
231
|
return api(path, init, true);
|
|
232
232
|
}
|
|
233
|
-
if (!res.ok) fail(body.error ?? `Request failed (${res.status})`);
|
|
233
|
+
if (!res.ok) fail(body.error?.message ?? body.error ?? `Request failed (${res.status})`);
|
|
234
234
|
return body;
|
|
235
235
|
}
|
|
236
236
|
async function login(server) {
|
|
@@ -258,10 +258,8 @@ async function login(server) {
|
|
|
258
258
|
ok(`Signed in as ${me.user.email} ${dim(`\xB7 ${me.org.name}`)}`);
|
|
259
259
|
console.log();
|
|
260
260
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return box ?? fail(`No agent named ${name}. ${brand.command} ls lists them.`);
|
|
264
|
-
}
|
|
261
|
+
var findBox = (name) => api(`/api/boxes/${encodeURIComponent(name)}`);
|
|
262
|
+
var awakeOf = (o) => o.awake === void 0 ? {} : { idleMinutes: 0, awakeDays: o.awake === true ? 7 : Number(o.awake) };
|
|
265
263
|
|
|
266
264
|
// src/environments.ts
|
|
267
265
|
import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
|
|
@@ -269,14 +267,15 @@ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:f
|
|
|
269
267
|
import { join as join3 } from "node:path";
|
|
270
268
|
import { styleText as styleText3 } from "node:util";
|
|
271
269
|
import { input, select } from "@inquirer/prompts";
|
|
270
|
+
var MACHINES = { small: "2 vCPU, 4 GB", medium: "4 vCPU, 8 GB", large: "8 vCPU, 16 GB" };
|
|
272
271
|
async function chooseEnvironment(envName, here) {
|
|
273
|
-
const envs = await api("/api/environments");
|
|
272
|
+
const { data: envs } = await api("/api/environments");
|
|
274
273
|
if (envName) return waitReady(envs.find((e) => e.name === envName) ?? fail(`No environment named ${envName}.`));
|
|
275
274
|
const mine = here ? envs.filter((e) => e.repo.toLowerCase() === here.repo.toLowerCase()) : [];
|
|
276
275
|
const setUp = async () => setUpEnvironment(here?.repo ?? await askRepo(), here?.dir, envs);
|
|
277
276
|
if (!envs.length) return waitReady(await setUp());
|
|
278
277
|
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
|
|
278
|
+
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
279
|
const create = { name: styleText3("cyan", here ? `+ Set up a new environment for ${here.repo}` : "+ Set up a new environment"), value: "" };
|
|
281
280
|
const pick = await select({
|
|
282
281
|
message: here ? `Environment for ${here.repo}${here.branch ? dim(` \xB7 branch ${here.branch}`) : ""}` : `Environment ${dim("(you're not in a repo)")}`,
|
|
@@ -360,12 +359,11 @@ async function setUpEnvironment(repo, dir, envs) {
|
|
|
360
359
|
let name = repo.split("/")[1].toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").slice(0, 28) || "env";
|
|
361
360
|
for (let i = 2, base = name; envs.some((e) => e.name === name); i++) name = `${base}-${i}`;
|
|
362
361
|
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
362
|
for (; ; ) {
|
|
365
363
|
console.log();
|
|
366
364
|
console.log(` ${dim("Name ")} ${name}`);
|
|
367
365
|
console.log(` ${dim("Repo ")} ${repo}`);
|
|
368
|
-
console.log(` ${dim("Machine ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${
|
|
366
|
+
console.log(` ${dim("Machine ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${MACHINES[size]}${size === sized.size ? ` (${sized.why})` : ""}`)}`);
|
|
369
367
|
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
368
|
if (names.length) console.log(` ${dim("Secrets ")} ${names.join(", ")} ${dim(secrets ? "\xB7 values copied from .env" : "\xB7 names from .env.example, values empty")}`);
|
|
371
369
|
console.log();
|
|
@@ -384,32 +382,33 @@ async function setUpEnvironment(repo, dir, envs) {
|
|
|
384
382
|
}
|
|
385
383
|
if (choice === "edit") {
|
|
386
384
|
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:
|
|
385
|
+
size = await select({ message: "Machine", default: size, choices: machineChoices });
|
|
388
386
|
setup2 = (await input({ message: "Setup script (runs once in the snapshot; empty for none)", default: setup2 })).trim();
|
|
389
387
|
continue;
|
|
390
388
|
}
|
|
391
389
|
break;
|
|
392
390
|
}
|
|
393
391
|
await ensureGitHub(repo);
|
|
394
|
-
const
|
|
392
|
+
const made = await api("/api/environments", { method: "POST", body: JSON.stringify({ name, repo, size, setup: setup2, secrets }) });
|
|
395
393
|
ok(`Environment ${styleText3("cyan", name)} created`);
|
|
396
|
-
return
|
|
394
|
+
return made;
|
|
397
395
|
}
|
|
396
|
+
var SIZES = ["small", "medium", "large"];
|
|
397
|
+
var machineChoices = SIZES.map((value) => ({ name: `${value.padEnd(8)}${dim(MACHINES[value])}`, value }));
|
|
398
398
|
async function ensureGitHub(repo) {
|
|
399
399
|
const open = (url) => {
|
|
400
400
|
if (process.stdout.isTTY) spawn2(process.platform === "darwin" ? "open" : "xdg-open", [url], { stdio: "ignore", detached: true }).on("error", () => {
|
|
401
401
|
}).unref();
|
|
402
402
|
};
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
if (!r.linked) {
|
|
403
|
+
const linked = async () => (await api("/api/accounts")).data.some((a) => a.id === "github" && a.status === "ready");
|
|
404
|
+
if (!await linked()) {
|
|
406
405
|
const url = new URL("/connect/github", load().server).toString();
|
|
407
406
|
console.log(`
|
|
408
407
|
${styleText3("yellow", "!")} Connect GitHub so ${brand.name} can clone ${repo} and your agents can push as you.`);
|
|
409
408
|
console.log(dim(` Opening ${url}`));
|
|
410
409
|
open(url);
|
|
411
410
|
const bar = progress([[0, "Waiting for you to connect GitHub in the browser"]], { pace: 6e4 });
|
|
412
|
-
for (const end = Date.now() + 10 * 6e4; !
|
|
411
|
+
for (const end = Date.now() + 10 * 6e4; !await linked(); ) {
|
|
413
412
|
if (Date.now() > end) {
|
|
414
413
|
bar();
|
|
415
414
|
fail(`Gave up waiting after 10 minutes. Run ${brand.command} again when it's done.`);
|
|
@@ -420,7 +419,7 @@ ${styleText3("yellow", "!")} Connect GitHub so ${brand.name} can clone ${repo} a
|
|
|
420
419
|
ok("GitHub connected");
|
|
421
420
|
}
|
|
422
421
|
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 && !
|
|
422
|
+
if (!isPublic && !(await api("/api/github/repos")).data.some((p) => p.fullName.toLowerCase() === repo.toLowerCase())) fail(`Your GitHub login can't reach ${repo}.`);
|
|
424
423
|
}
|
|
425
424
|
|
|
426
425
|
// src/local.ts
|
|
@@ -489,7 +488,7 @@ async function proxy(host) {
|
|
|
489
488
|
// src/local.ts
|
|
490
489
|
async function newLocalBox(env, branch, name, o) {
|
|
491
490
|
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,
|
|
491
|
+
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
492
|
ok(`${box.name} is ready ${dim(`\xB7 ssh ${box.name}.${brand.command}`)}`);
|
|
494
493
|
await openLocal({ name: box.name, env, here: localCopy(), branch, fresh: true });
|
|
495
494
|
}
|
|
@@ -761,7 +760,7 @@ async function prune(boxes) {
|
|
|
761
760
|
const email = load().email;
|
|
762
761
|
const mine = localBoxes().filter((b) => b.owner && b.owner === email);
|
|
763
762
|
if (!mine.length) return;
|
|
764
|
-
const alive = new Set((boxes ?? await api("/api/boxes")).filter((b) => b.state !== "gone").map((b) => b.name));
|
|
763
|
+
const alive = new Set((boxes ?? (await api("/api/boxes")).data).filter((b) => b.state !== "gone").map((b) => b.name));
|
|
765
764
|
for (const b of mine) {
|
|
766
765
|
if (alive.has(b.name)) continue;
|
|
767
766
|
await dropFolder(b);
|
|
@@ -792,7 +791,7 @@ async function openLocal({ name, env, here, branch, fresh = false }) {
|
|
|
792
791
|
const stop = progress([[0, `Opening ${name}`], [1500, "Waking the agent up"]]);
|
|
793
792
|
const [got, envs] = await Promise.all([
|
|
794
793
|
api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) }),
|
|
795
|
-
api("/api/environments")
|
|
794
|
+
api("/api/environments").then((r) => r.data)
|
|
796
795
|
]).finally(() => stop());
|
|
797
796
|
const environment = envs.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
|
|
798
797
|
void sendSetup().catch(() => void 0);
|
|
@@ -848,7 +847,7 @@ async function sync(name, direction, folder) {
|
|
|
848
847
|
await (direction === "push" ? push(name, folder) : pull(name, folder));
|
|
849
848
|
}
|
|
850
849
|
async function code(envArg) {
|
|
851
|
-
const boxes = await api("/api/boxes");
|
|
850
|
+
const { data: boxes } = await api("/api/boxes");
|
|
852
851
|
await prune(boxes);
|
|
853
852
|
const here = localBoxes().filter((b) => existsSync5(join5(b.folder, "repo")));
|
|
854
853
|
const envs = [...new Set(here.map((b) => b.env))].sort();
|
|
@@ -856,18 +855,18 @@ async function code(envArg) {
|
|
|
856
855
|
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
856
|
const group = here.filter((b) => b.env === env);
|
|
858
857
|
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}.`);
|
|
858
|
+
const awake2 = new Set(boxes.filter((b) => b.state === "running").map((b) => b.name));
|
|
859
|
+
const environment = (await api("/api/environments")).data.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
|
|
861
860
|
const clone = await ensureClone(environment);
|
|
862
861
|
const migrated = [];
|
|
863
|
-
for (const b of group.filter((g) =>
|
|
862
|
+
for (const b of group.filter((g) => awake2.has(g.name))) {
|
|
864
863
|
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
864
|
}
|
|
866
865
|
const workspace = join5(BOXES, env, `${env}.code-workspace`);
|
|
867
866
|
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
867
|
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
868
|
ok(`${group.length} ${env} agent${group.length === 1 ? "" : "s"} in one window ${dim(`\xB7 ${tilde(workspace)}`)}`);
|
|
870
|
-
await Promise.all(group.filter((b) =>
|
|
869
|
+
await Promise.all(group.filter((b) => awake2.has(b.name) && !migrated.includes(b.name)).map((b) => pull(b.name, b.folder, true)));
|
|
871
870
|
const stops = group.map((b) => watchRepo(b.name, b.folder));
|
|
872
871
|
console.log(dim("Syncing your saves to the agents. Leave this open; ctrl+c stops syncing."));
|
|
873
872
|
await new Promise((resolve) => process.once("SIGINT", resolve));
|
|
@@ -986,7 +985,6 @@ var mcpCount = (s) => {
|
|
|
986
985
|
};
|
|
987
986
|
async function newRemoteBox(env, branch, name, o) {
|
|
988
987
|
const agent = o.agent;
|
|
989
|
-
const idle = o.alwaysOn ? "0" : void 0;
|
|
990
988
|
const { mcpServers, mcpOAuth, skipped } = localMcp();
|
|
991
989
|
ok(`${mcpCount(mcpServers)} will be copied into the agent ${dim(skipped.length ? `(skipping local-only: ${skipped.join(", ")})` : "")}`);
|
|
992
990
|
const claudeHome = localClaudeHome();
|
|
@@ -1001,9 +999,9 @@ async function newRemoteBox(env, branch, name, o) {
|
|
|
1001
999
|
[9e3, "Bigger machines take a few extra seconds"]
|
|
1002
1000
|
]);
|
|
1003
1001
|
const { tar, ...home2 } = claudeHome;
|
|
1004
|
-
const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, size: o.size,
|
|
1002
|
+
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
1003
|
if (home2.pending) void sendHome(box.name, tar);
|
|
1006
|
-
await attach(box.name,
|
|
1004
|
+
await attach(box.name, box.ssh);
|
|
1007
1005
|
}
|
|
1008
1006
|
async function sendHome(name, tar) {
|
|
1009
1007
|
const buf = await tar;
|
|
@@ -1038,7 +1036,7 @@ async function dial({ user, hosts }, privateKey, attempts) {
|
|
|
1038
1036
|
}
|
|
1039
1037
|
async function attach(name, ready) {
|
|
1040
1038
|
if (!name) {
|
|
1041
|
-
const boxes = await api("/api/boxes");
|
|
1039
|
+
const { data: boxes } = await api("/api/boxes");
|
|
1042
1040
|
if (!boxes.length) return console.log(dim(`No agents yet. Run ${brand.command}.`));
|
|
1043
1041
|
name = await select3({ message: "Attach to", choices: boxes.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}`, value: b.name })) });
|
|
1044
1042
|
}
|
|
@@ -1072,7 +1070,7 @@ async function attach(name, ready) {
|
|
|
1072
1070
|
Detached. ${boxName} sleeps once Claude is done. ${brand.command} ${boxName} to return.`));
|
|
1073
1071
|
process.exit(0);
|
|
1074
1072
|
}
|
|
1075
|
-
let ssh = ready
|
|
1073
|
+
let ssh = ready, attempts = 4, announced = false;
|
|
1076
1074
|
for (; ; ) {
|
|
1077
1075
|
if (!ssh) {
|
|
1078
1076
|
attempts = 4;
|
|
@@ -1151,12 +1149,12 @@ function centered(lines) {
|
|
|
1151
1149
|
}
|
|
1152
1150
|
function sleepScreen(box) {
|
|
1153
1151
|
const since = box.asleepSince ? Date.now() - new Date(box.asleepSince).getTime() : 0;
|
|
1154
|
-
const saved = since / 36e5 *
|
|
1152
|
+
const saved = since / 36e5 * RATES[box.size];
|
|
1155
1153
|
centered([
|
|
1156
1154
|
styleText4("blue", "\u263E"),
|
|
1157
1155
|
"",
|
|
1158
1156
|
styleText4("bold", `${box.name} is asleep`),
|
|
1159
|
-
dim(`Last used ${span(Date.now() - new Date(box.lastActiveAt
|
|
1157
|
+
dim(`Last used ${span(Date.now() - new Date(box.lastActiveAt).getTime())} ago. Your files and the conversation are saved.`),
|
|
1160
1158
|
"",
|
|
1161
1159
|
`Press any key to wake it ${dim("\xB7")} ctrl+] to leave`,
|
|
1162
1160
|
"",
|
|
@@ -1258,7 +1256,7 @@ var ANIMALS = ["otter", "heron", "lynx", "moth", "yak", "orca", "wren", "newt",
|
|
|
1258
1256
|
var NAME = /^[a-z0-9][a-z0-9-]{0,30}$/;
|
|
1259
1257
|
async function newBox(o = {}) {
|
|
1260
1258
|
await updateFirst();
|
|
1261
|
-
if (o.size && !
|
|
1259
|
+
if (o.size && !Object.hasOwn(MACHINES, o.size)) fail("Pick --size small, medium or large.");
|
|
1262
1260
|
if (o.name !== void 0 && !NAME.test(o.name)) fail("Name the agent with lowercase letters, digits and dashes, like webhook-retry.");
|
|
1263
1261
|
const here = gitRepo();
|
|
1264
1262
|
const picked = await chooseEnvironment(o.env, here);
|
|
@@ -1278,11 +1276,11 @@ async function askName(branch) {
|
|
|
1278
1276
|
return picked.trim() || suggestion;
|
|
1279
1277
|
}
|
|
1280
1278
|
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 --
|
|
1279
|
+
const making = o.env || o.remote || o.agent || o.size || o.awake;
|
|
1280
|
+
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
1281
|
if (name) return openBox(name);
|
|
1284
1282
|
const email = load().email;
|
|
1285
|
-
const all = await api("/api/boxes");
|
|
1283
|
+
const { data: all } = await api("/api/boxes");
|
|
1286
1284
|
await prune(all);
|
|
1287
1285
|
const mine = all.filter((b) => b.state !== "gone" && (!email || b.owner === email));
|
|
1288
1286
|
if (!mine.length || !process.stdin.isTTY || making) return newBox(o);
|
|
@@ -1299,7 +1297,7 @@ async function openBox(name) {
|
|
|
1299
1297
|
}
|
|
1300
1298
|
async function ls() {
|
|
1301
1299
|
const email = load().email;
|
|
1302
|
-
const all = await api("/api/boxes");
|
|
1300
|
+
const { data: all } = await api("/api/boxes");
|
|
1303
1301
|
await prune(all);
|
|
1304
1302
|
const mine = all.filter((b) => !email || b.owner === email);
|
|
1305
1303
|
if (!mine.length) return console.log(dim(`No agents of yours. Run ${brand.command}.`));
|
|
@@ -1310,6 +1308,13 @@ async function ls() {
|
|
|
1310
1308
|
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
1309
|
}
|
|
1312
1310
|
}
|
|
1311
|
+
async function awake(name, days = "7") {
|
|
1312
|
+
const n = Number(days);
|
|
1313
|
+
if (!Number.isInteger(n) || n < 0 || n > 30) fail("Days must be from 1 to 30, or 0 to let it sleep.");
|
|
1314
|
+
await api(`/api/boxes/${(await findBox(name)).id}`, { method: "PATCH", body: JSON.stringify({ awakeDays: n }) });
|
|
1315
|
+
if (n === 0) return ok(`${name} sleeps when idle again`);
|
|
1316
|
+
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`)}`);
|
|
1317
|
+
}
|
|
1313
1318
|
async function setup() {
|
|
1314
1319
|
const sent = await sendSetup();
|
|
1315
1320
|
const n = (k, word) => `${k} ${word}${k === 1 ? "" : "s"}`;
|
|
@@ -1337,11 +1342,12 @@ if (!quiet) {
|
|
|
1337
1342
|
autoUpdate();
|
|
1338
1343
|
ensureSshConfig();
|
|
1339
1344
|
}
|
|
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("--
|
|
1345
|
+
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
1346
|
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
1347
|
boxOptions(cli.command("new").description("always make a fresh agent")).action((o) => newBox(o));
|
|
1343
1348
|
cli.command("ls").description("list your agents").action(ls);
|
|
1344
1349
|
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));
|
|
1350
|
+
cli.command("awake <agent> [days]").description("keep an agent awake for days (default 7), or renew it; 0 lets it sleep").action(awake);
|
|
1345
1351
|
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
1352
|
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
1353
|
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.22",
|
|
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
|
}
|