@boxcompute/cli 0.1.1 → 0.2.1
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 +24 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.js +71 -17
- package/dist/client.d.ts +7 -1
- package/dist/client.js +21 -8
- package/dist/postinstall.d.ts +1 -0
- package/dist/postinstall.js +24 -0
- package/dist/skill.d.ts +5 -0
- package/dist/skill.js +90 -11
- package/package.json +2 -1
- package/skills/boxcompute-sandbox/SKILL.md +6 -8
package/README.md
CHANGED
|
@@ -33,14 +33,37 @@ compatible coding harnesses. It does not recursively scan your home directory.
|
|
|
33
33
|
Use `bxc skill install all` to target every supported harness, or name one or
|
|
34
34
|
more explicitly. Run `bxc skill remove --yes` to uninstall matching copies.
|
|
35
35
|
|
|
36
|
+
Skills installed by `bxc` are managed copies. A global CLI upgrade refreshes an
|
|
37
|
+
untouched managed copy during installation, with a safe retry on the next
|
|
38
|
+
normal `bxc` command if package lifecycle scripts were disabled. Locally
|
|
39
|
+
modified skills are never overwritten; the CLI reports the path and leaves
|
|
40
|
+
replacement behind the explicit `bxc skill install --force` command. Start a
|
|
41
|
+
new agent session after the CLI reports that a skill was updated.
|
|
42
|
+
|
|
36
43
|
## Use sandboxes
|
|
37
44
|
|
|
38
45
|
```sh
|
|
39
46
|
bxc doctor
|
|
47
|
+
bxc workspaces
|
|
40
48
|
bxc sandboxes
|
|
41
49
|
bxc sandbox start WORKSPACE_ID
|
|
42
|
-
|
|
50
|
+
# Use the returned sandbox instance ID for later commands.
|
|
51
|
+
bxc sandbox exec SANDBOX_ID -- python -m pytest
|
|
43
52
|
```
|
|
44
53
|
|
|
45
54
|
Run `bxc` or `bxc --help` for the complete command reference. The previous
|
|
46
55
|
`bcompute` executable remains available as a compatibility alias.
|
|
56
|
+
|
|
57
|
+
## Upgrade rollout
|
|
58
|
+
|
|
59
|
+
Deploy the server release with both Sandbox API v1 and v2 before publishing CLI
|
|
60
|
+
0.2. Existing CLI 0.1 clients remain on the legacy v1 workspace-sandbox
|
|
61
|
+
contract. After that, clients need only upgrade the package:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npm install --global @boxcompute/cli@latest
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The installation refreshes untouched managed skills before the client's next
|
|
68
|
+
agent session. The new CLI uses v2 for multi-instance sandboxes and gives a
|
|
69
|
+
server-first upgrade message if it reaches an older deployment.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { type Connection } from "./config.js";
|
|
3
|
-
import { detectHarnesses, installSkill, readSkill, removeSkill } from "./skill.js";
|
|
4
|
+
import { detectHarnesses, installSkill, readSkill, removeSkill, syncManagedSkills } from "./skill.js";
|
|
4
5
|
type Io = {
|
|
5
6
|
stdout: NodeJS.WritableStream;
|
|
6
7
|
stderr: NodeJS.WritableStream;
|
|
@@ -20,7 +21,20 @@ export type CliDependencies = {
|
|
|
20
21
|
installSkill?: typeof installSkill;
|
|
21
22
|
removeSkill?: typeof removeSkill;
|
|
22
23
|
readSkill?: typeof readSkill;
|
|
24
|
+
syncManagedSkills?: typeof syncManagedSkills;
|
|
23
25
|
};
|
|
26
|
+
type BrowserRuntime = {
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
kernelRelease?: string;
|
|
29
|
+
spawn?: typeof spawn;
|
|
30
|
+
system?: NodeJS.Platform;
|
|
31
|
+
};
|
|
32
|
+
export declare function browserLaunch(url: string, runtime?: Pick<BrowserRuntime, "env" | "kernelRelease" | "system">): {
|
|
33
|
+
command: string;
|
|
34
|
+
args: string[];
|
|
35
|
+
detached: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare function openBrowser(url: string, runtime?: BrowserRuntime): void;
|
|
24
38
|
export declare function runCli(argv: string[], supplied?: CliDependencies): Promise<number>;
|
|
25
39
|
export declare function formatCliError(error: unknown): string;
|
|
26
40
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { readFileSync, realpathSync } from "node:fs";
|
|
4
|
-
import { hostname, platform } from "node:os";
|
|
4
|
+
import { hostname, platform, release } from "node:os";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { BoxComputeClient, BoxComputeHttpError, publicRequest, } from "./client.js";
|
|
7
7
|
import { clearConnection, loadConnection, loadSavedUrl, saveConnection, } from "./config.js";
|
|
8
|
-
import { HARNESS_IDS, detectHarnesses, installSkill, readSkill, removeSkill, } from "./skill.js";
|
|
8
|
+
import { HARNESS_IDS, detectHarnesses, installSkill, readSkill, removeSkill, syncManagedSkills, } from "./skill.js";
|
|
9
9
|
const DEFAULT_URL = "https://app.boxcompute.ai";
|
|
10
10
|
const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
11
11
|
const rootHelp = `BoxCompute CLI
|
|
@@ -20,9 +20,10 @@ Commands:
|
|
|
20
20
|
auth [alias: login] Authentication commands
|
|
21
21
|
logout Revoke and remove the saved CLI credential
|
|
22
22
|
doctor Verify the saved connection
|
|
23
|
-
|
|
23
|
+
workspaces List workspaces that can own sandboxes
|
|
24
|
+
sandboxes [aliases: list, ls] List sandbox instances
|
|
24
25
|
sandbox Manage isolated BoxCompute sandboxes
|
|
25
|
-
start
|
|
26
|
+
start Create and start a workspace sandbox
|
|
26
27
|
status Inspect one sandbox
|
|
27
28
|
exec Execute a program inside a sandbox
|
|
28
29
|
delete [alias: rm] Destroy the runtime; the workspace remains
|
|
@@ -49,7 +50,9 @@ Examples:
|
|
|
49
50
|
$ bxc login
|
|
50
51
|
$ bxc skill detect
|
|
51
52
|
$ bxc skill install
|
|
52
|
-
$ bxc
|
|
53
|
+
$ bxc workspaces
|
|
54
|
+
$ bxc sandbox start WORKSPACE_ID
|
|
55
|
+
$ bxc sandbox exec SANDBOX_ID -- python -m pytest
|
|
53
56
|
|
|
54
57
|
Compatibility:
|
|
55
58
|
|
|
@@ -61,7 +64,7 @@ Usage: bxc sandbox <command> [options]
|
|
|
61
64
|
|
|
62
65
|
Commands:
|
|
63
66
|
|
|
64
|
-
start WORKSPACE_ID
|
|
67
|
+
start WORKSPACE_ID Create and start a new sandbox instance
|
|
65
68
|
status SANDBOX_ID Inspect one sandbox
|
|
66
69
|
exec SANDBOX_ID [options] -- PROGRAM [ARG...]
|
|
67
70
|
Execute a program inside a sandbox
|
|
@@ -102,13 +105,33 @@ Supported harnesses:
|
|
|
102
105
|
class UsageError extends Error {
|
|
103
106
|
constructor(message) { super(message); this.name = "UsageError"; }
|
|
104
107
|
}
|
|
105
|
-
function
|
|
106
|
-
const system = platform();
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
export function browserLaunch(url, runtime = {}) {
|
|
109
|
+
const system = runtime.system ?? platform();
|
|
110
|
+
const env = runtime.env ?? process.env;
|
|
111
|
+
const kernelRelease = runtime.kernelRelease ?? release();
|
|
112
|
+
const isWsl = system === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP || /microsoft/i.test(kernelRelease));
|
|
113
|
+
if (isWsl)
|
|
114
|
+
return { command: "explorer.exe", args: [url], detached: false };
|
|
115
|
+
if (system === "darwin")
|
|
116
|
+
return { command: "open", args: [url], detached: true };
|
|
117
|
+
if (system === "win32")
|
|
118
|
+
return { command: "cmd", args: ["/c", "start", "", url], detached: true };
|
|
119
|
+
return { command: "xdg-open", args: [url], detached: true };
|
|
120
|
+
}
|
|
121
|
+
export function openBrowser(url, runtime = {}) {
|
|
122
|
+
const launch = browserLaunch(url, runtime);
|
|
123
|
+
try {
|
|
124
|
+
const child = (runtime.spawn ?? spawn)(launch.command, launch.args, {
|
|
125
|
+
detached: launch.detached,
|
|
126
|
+
stdio: "ignore",
|
|
127
|
+
});
|
|
128
|
+
child.on("error", () => undefined);
|
|
129
|
+
child.unref();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// The approval URL is always printed before this best-effort launch. Keep
|
|
133
|
+
// polling so headless shells and restricted WSL interop can authenticate.
|
|
134
|
+
}
|
|
112
135
|
}
|
|
113
136
|
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
114
137
|
const write = (stream, value) => { stream.write(value); };
|
|
@@ -229,6 +252,9 @@ async function authenticate(args, dependencies) {
|
|
|
229
252
|
function sandboxLine(sandbox) {
|
|
230
253
|
return `${sandbox.id}\t${sandbox.state}\t${sandbox.name}\n`;
|
|
231
254
|
}
|
|
255
|
+
function workspaceLine(workspace) {
|
|
256
|
+
return `${workspace.id}\t${workspace.name}\n`;
|
|
257
|
+
}
|
|
232
258
|
function executionOutput(io, json, sandboxId, result) {
|
|
233
259
|
if (json)
|
|
234
260
|
emit(io, true, { sandboxId, ...result }, "");
|
|
@@ -245,7 +271,7 @@ export async function runCli(argv, supplied = {}) {
|
|
|
245
271
|
const fetchImpl = supplied.fetch ?? fetch;
|
|
246
272
|
const now = supplied.now ?? Date.now;
|
|
247
273
|
const sleep = supplied.sleep ?? delay;
|
|
248
|
-
const
|
|
274
|
+
const openBrowserImpl = supplied.openBrowser ?? openBrowser;
|
|
249
275
|
const load = supplied.loadConnection ?? loadConnection;
|
|
250
276
|
const savedUrl = supplied.loadSavedUrl ?? loadSavedUrl;
|
|
251
277
|
const save = supplied.saveConnection ?? saveConnection;
|
|
@@ -254,6 +280,7 @@ export async function runCli(argv, supplied = {}) {
|
|
|
254
280
|
const install = supplied.installSkill ?? installSkill;
|
|
255
281
|
const remove = supplied.removeSkill ?? removeSkill;
|
|
256
282
|
const skillText = supplied.readSkill ?? readSkill;
|
|
283
|
+
const syncSkills = supplied.syncManagedSkills ?? syncManagedSkills;
|
|
257
284
|
const args = [...argv];
|
|
258
285
|
const json = flag(args, "json");
|
|
259
286
|
const versionRequested = args[0] === "version" || anyFlag(args, "--version", "-V", "-v");
|
|
@@ -274,6 +301,26 @@ export async function runCli(argv, supplied = {}) {
|
|
|
274
301
|
command = "skill";
|
|
275
302
|
if (command === "list" || command === "ls")
|
|
276
303
|
command = "sandboxes";
|
|
304
|
+
const canAutoSync = supplied.syncManagedSkills !== undefined || supplied.env === undefined ||
|
|
305
|
+
Boolean(env.HOME || env.USERPROFILE);
|
|
306
|
+
if (command !== "skill" && canAutoSync) {
|
|
307
|
+
try {
|
|
308
|
+
const synced = await syncSkills(env);
|
|
309
|
+
const updated = synced.filter((item) => item.status === "updated");
|
|
310
|
+
const modified = synced.filter((item) => item.status === "modified");
|
|
311
|
+
if (updated.length) {
|
|
312
|
+
write(io.stderr, `Updated the BoxCompute skill for ${updated.flatMap((item) => item.agents).join(", ")}. ` +
|
|
313
|
+
"Start a new agent session to load it.\n");
|
|
314
|
+
}
|
|
315
|
+
for (const item of modified) {
|
|
316
|
+
write(io.stderr, `Kept locally modified BoxCompute skill at ${item.path}; run ` +
|
|
317
|
+
"`bxc skill install --force` to replace it.\n");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
write(io.stderr, `Could not check installed BoxCompute skills: ${error?.message ?? String(error)}\n`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
277
324
|
if (command === "logout") {
|
|
278
325
|
if (args.length)
|
|
279
326
|
throw new UsageError("logout takes no options");
|
|
@@ -296,7 +343,7 @@ export async function runCli(argv, supplied = {}) {
|
|
|
296
343
|
emit(io, json, { authenticated: false }, "BoxCompute CLI credential revoked and removed.\n");
|
|
297
344
|
return 0;
|
|
298
345
|
}
|
|
299
|
-
return authenticate(args, { env, io, json, fetch: fetchImpl, now, sleep, openBrowser, loadSavedUrl: savedUrl, saveConnection: save });
|
|
346
|
+
return authenticate(args, { env, io, json, fetch: fetchImpl, now, sleep, openBrowser: openBrowserImpl, loadSavedUrl: savedUrl, saveConnection: save });
|
|
300
347
|
}
|
|
301
348
|
if (command === "skill") {
|
|
302
349
|
let action = args.shift();
|
|
@@ -367,14 +414,21 @@ export async function runCli(argv, supplied = {}) {
|
|
|
367
414
|
const client = new BoxComputeClient(connection, fetchImpl);
|
|
368
415
|
if (command === "doctor") {
|
|
369
416
|
const sandboxes = await client.list();
|
|
370
|
-
emit(io, json, { connected: true, url: connection.url, sandboxes: sandboxes.length }, `Connected to ${connection.url} · ${sandboxes.length}
|
|
417
|
+
emit(io, json, { connected: true, url: connection.url, sandboxes: sandboxes.length }, `Connected to ${connection.url} · ${sandboxes.length} sandbox${sandboxes.length === 1 ? "" : "es"}\n`);
|
|
371
418
|
return 0;
|
|
372
419
|
}
|
|
373
420
|
if (command === "sandboxes") {
|
|
374
421
|
if (args.length)
|
|
375
422
|
throw new UsageError("sandboxes takes no options");
|
|
376
423
|
const sandboxes = await client.list();
|
|
377
|
-
emit(io, json, { sandboxes }, sandboxes.length ? sandboxes.map(sandboxLine).join("") : "No
|
|
424
|
+
emit(io, json, { sandboxes }, sandboxes.length ? sandboxes.map(sandboxLine).join("") : "No sandboxes found. Start one for a BoxCompute workspace first.\n");
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
if (command === "workspaces") {
|
|
428
|
+
if (args.length)
|
|
429
|
+
throw new UsageError("workspaces takes no options");
|
|
430
|
+
const workspaces = await client.listWorkspaces();
|
|
431
|
+
emit(io, json, { workspaces }, workspaces.length ? workspaces.map(workspaceLine).join("") : "No workspaces found. Create one in BoxCompute first.\n");
|
|
378
432
|
return 0;
|
|
379
433
|
}
|
|
380
434
|
if (command !== "sandbox")
|
package/dist/client.d.ts
CHANGED
|
@@ -3,12 +3,17 @@ export type Sandbox = {
|
|
|
3
3
|
id: string;
|
|
4
4
|
workspaceId: string;
|
|
5
5
|
name: string;
|
|
6
|
-
state: "
|
|
6
|
+
state: "cold" | "running";
|
|
7
7
|
runtimeId: string | null;
|
|
8
8
|
image: string | null;
|
|
9
9
|
createdAt: number;
|
|
10
10
|
lastUsedAt: number | null;
|
|
11
11
|
};
|
|
12
|
+
export type Workspace = {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
};
|
|
12
17
|
export type Execution = {
|
|
13
18
|
stdout: string;
|
|
14
19
|
stderr: string;
|
|
@@ -30,6 +35,7 @@ export declare class BoxComputeClient {
|
|
|
30
35
|
constructor(connection: Connection, fetchImpl?: typeof fetch);
|
|
31
36
|
private request;
|
|
32
37
|
list(): Promise<Sandbox[]>;
|
|
38
|
+
listWorkspaces(): Promise<Workspace[]>;
|
|
33
39
|
logout(): Promise<void>;
|
|
34
40
|
inspect(id: string): Promise<Sandbox>;
|
|
35
41
|
start(workspaceId: string): Promise<Sandbox>;
|
package/dist/client.js
CHANGED
|
@@ -25,35 +25,48 @@ export class BoxComputeClient {
|
|
|
25
25
|
this.connection = connection;
|
|
26
26
|
this.fetchImpl = fetchImpl;
|
|
27
27
|
}
|
|
28
|
-
request(pathname, init = {}) {
|
|
28
|
+
async request(pathname, init = {}) {
|
|
29
29
|
const headers = new Headers(init.headers);
|
|
30
30
|
headers.set("authorization", `Bearer ${this.connection.token}`);
|
|
31
|
-
|
|
31
|
+
try {
|
|
32
|
+
return await publicRequest(this.connection.url, pathname, { ...init, headers }, this.fetchImpl);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const isV2Discovery = pathname === "/api/v2/sandboxes" ||
|
|
36
|
+
pathname === "/api/v2/workspaces" || pathname === "/api/v2/auth";
|
|
37
|
+
if (isV2Discovery && error instanceof BoxComputeHttpError && error.status === 404) {
|
|
38
|
+
throw new BoxComputeHttpError(404, "This BoxCompute server does not support Sandbox API v2 yet. Upgrade the server before this CLI.", error.code);
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
32
42
|
}
|
|
33
43
|
async list() {
|
|
34
|
-
return (await this.request("/api/
|
|
44
|
+
return (await this.request("/api/v2/sandboxes")).sandboxes;
|
|
45
|
+
}
|
|
46
|
+
async listWorkspaces() {
|
|
47
|
+
return (await this.request("/api/v2/workspaces")).workspaces;
|
|
35
48
|
}
|
|
36
49
|
async logout() {
|
|
37
|
-
await this.request("/api/
|
|
50
|
+
await this.request("/api/v2/auth", { method: "DELETE" });
|
|
38
51
|
}
|
|
39
52
|
async inspect(id) {
|
|
40
|
-
return (await this.request(`/api/
|
|
53
|
+
return (await this.request(`/api/v2/sandboxes/${encodeURIComponent(id)}`)).sandbox;
|
|
41
54
|
}
|
|
42
55
|
async start(workspaceId) {
|
|
43
|
-
return (await this.request("/api/
|
|
56
|
+
return (await this.request("/api/v2/sandboxes", {
|
|
44
57
|
method: "POST",
|
|
45
58
|
headers: { "content-type": "application/json" },
|
|
46
59
|
body: JSON.stringify({ workspaceId }),
|
|
47
60
|
})).sandbox;
|
|
48
61
|
}
|
|
49
62
|
async execute(id, input) {
|
|
50
|
-
return (await this.request(`/api/
|
|
63
|
+
return (await this.request(`/api/v2/sandboxes/${encodeURIComponent(id)}/execute`, {
|
|
51
64
|
method: "POST",
|
|
52
65
|
headers: { "content-type": "application/json" },
|
|
53
66
|
body: JSON.stringify(input),
|
|
54
67
|
})).result;
|
|
55
68
|
}
|
|
56
69
|
async delete(id) {
|
|
57
|
-
await this.request(`/api/
|
|
70
|
+
await this.request(`/api/v2/sandboxes/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
58
71
|
}
|
|
59
72
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { syncManagedSkills } from "./skill.js";
|
|
2
|
+
// A global CLI upgrade happens before the user starts their coding harness, so
|
|
3
|
+
// this is the one point where an untouched installed skill can be refreshed in
|
|
4
|
+
// time for the next agent session. Package managers that suppress lifecycle
|
|
5
|
+
// scripts still get the same safe check on the next normal bxc invocation.
|
|
6
|
+
const globalInstall = process.env.npm_config_global === "true" ||
|
|
7
|
+
process.env.npm_config_global === "1";
|
|
8
|
+
if (globalInstall) {
|
|
9
|
+
try {
|
|
10
|
+
const results = await syncManagedSkills(process.env);
|
|
11
|
+
const updated = results.filter((item) => item.status === "updated");
|
|
12
|
+
const modified = results.filter((item) => item.status === "modified");
|
|
13
|
+
if (updated.length) {
|
|
14
|
+
console.log(`Updated the BoxCompute skill for ${updated.flatMap((item) => item.agents).join(", ")}.`);
|
|
15
|
+
}
|
|
16
|
+
for (const item of modified) {
|
|
17
|
+
console.warn(`Kept locally modified BoxCompute skill at ${item.path}; ` +
|
|
18
|
+
"run `bxc skill install --force` to replace it.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.warn(`Could not check installed BoxCompute skills: ${error?.message ?? String(error)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/skill.d.ts
CHANGED
|
@@ -19,11 +19,16 @@ export type SkillInstallationResult = SkillInstallation & {
|
|
|
19
19
|
export type SkillRemovalResult = SkillInstallation & {
|
|
20
20
|
status: "removed" | "missing";
|
|
21
21
|
};
|
|
22
|
+
export type SkillSyncResult = SkillInstallation & {
|
|
23
|
+
status: "updated" | "unchanged" | "modified";
|
|
24
|
+
};
|
|
22
25
|
export declare function detectHarnesses(env?: NodeJS.ProcessEnv): Promise<HarnessDetection[]>;
|
|
23
26
|
export declare function installSkill(requested?: AgentTarget | AgentTarget[], options?: {
|
|
24
27
|
force?: boolean;
|
|
25
28
|
env?: NodeJS.ProcessEnv;
|
|
26
29
|
}): Promise<SkillInstallationResult[]>;
|
|
30
|
+
/** Refresh untouched skill copies installed by bxc, preserving local edits. */
|
|
31
|
+
export declare function syncManagedSkills(env?: NodeJS.ProcessEnv): Promise<SkillSyncResult[]>;
|
|
27
32
|
export declare function removeSkill(requested?: AgentTarget | AgentTarget[], options?: {
|
|
28
33
|
force?: boolean;
|
|
29
34
|
env?: NodeJS.ProcessEnv;
|
package/dist/skill.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { access, cp, mkdir, readFile, readdir, rename, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { access, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -20,6 +20,14 @@ export const HARNESS_IDS = [
|
|
|
20
20
|
"agents",
|
|
21
21
|
];
|
|
22
22
|
const sourceSkill = fileURLToPath(new URL("../skills/boxcompute-sandbox", import.meta.url));
|
|
23
|
+
const MANAGED_SKILL_FILE = ".boxcompute-managed.json";
|
|
24
|
+
const MANAGED_BY = "@boxcompute/cli";
|
|
25
|
+
// @boxcompute/cli 0.1.2 predates managed manifests. Recognizing its packaged
|
|
26
|
+
// digest gives existing customers a one-time automatic bridge into managed
|
|
27
|
+
// updates without treating arbitrary skill directories as ours.
|
|
28
|
+
const LEGACY_MANAGED_DIGESTS = new Set([
|
|
29
|
+
"00d64cd0c71847976baaf6e3d449c0cf1d5fe0d3b3c752b2a05faba906916338",
|
|
30
|
+
]);
|
|
23
31
|
function locations(env) {
|
|
24
32
|
const home = env.HOME || homedir();
|
|
25
33
|
return {
|
|
@@ -51,10 +59,12 @@ async function directoryDigest(directory) {
|
|
|
51
59
|
const digest = createHash("sha256");
|
|
52
60
|
const walk = async (current, prefix = "") => {
|
|
53
61
|
const entries = await readdir(current, { withFileTypes: true });
|
|
54
|
-
entries.sort((left, right) => left.name.
|
|
62
|
+
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
55
63
|
for (const entry of entries) {
|
|
56
64
|
const relative = path.posix.join(prefix, entry.name);
|
|
57
65
|
const absolute = path.join(current, entry.name);
|
|
66
|
+
if (relative === MANAGED_SKILL_FILE)
|
|
67
|
+
continue;
|
|
58
68
|
if (entry.isDirectory())
|
|
59
69
|
await walk(absolute, relative);
|
|
60
70
|
else if (entry.isFile()) {
|
|
@@ -79,6 +89,47 @@ async function matchesPackagedSkill(candidate) {
|
|
|
79
89
|
return false;
|
|
80
90
|
}
|
|
81
91
|
}
|
|
92
|
+
async function managedManifest(candidate) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(await readFile(path.join(candidate, MANAGED_SKILL_FILE), "utf8"));
|
|
95
|
+
return parsed.schema === 1 && parsed.managedBy === MANAGED_BY &&
|
|
96
|
+
typeof parsed.contentDigest === "string"
|
|
97
|
+
? parsed
|
|
98
|
+
: null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function writeManagedManifest(candidate) {
|
|
105
|
+
const manifest = {
|
|
106
|
+
schema: 1,
|
|
107
|
+
managedBy: MANAGED_BY,
|
|
108
|
+
contentDigest: await directoryDigest(candidate),
|
|
109
|
+
};
|
|
110
|
+
await writeFile(path.join(candidate, MANAGED_SKILL_FILE), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
111
|
+
}
|
|
112
|
+
async function isUnmodifiedManagedSkill(candidate) {
|
|
113
|
+
try {
|
|
114
|
+
const currentDigest = await directoryDigest(candidate);
|
|
115
|
+
if (LEGACY_MANAGED_DIGESTS.has(currentDigest))
|
|
116
|
+
return true;
|
|
117
|
+
const manifest = await managedManifest(candidate);
|
|
118
|
+
return manifest?.contentDigest === currentDigest;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function replaceWithPackagedSkill(candidate) {
|
|
125
|
+
await mkdir(path.dirname(candidate), { recursive: true });
|
|
126
|
+
const temporary = `${candidate}.tmp-${process.pid}`;
|
|
127
|
+
await rm(temporary, { recursive: true, force: true });
|
|
128
|
+
await cp(sourceSkill, temporary, { recursive: true });
|
|
129
|
+
await writeManagedManifest(temporary);
|
|
130
|
+
await rm(candidate, { recursive: true, force: true });
|
|
131
|
+
await rename(temporary, candidate);
|
|
132
|
+
}
|
|
82
133
|
async function commandExists(command, env) {
|
|
83
134
|
if (!env.PATH)
|
|
84
135
|
return false;
|
|
@@ -158,21 +209,47 @@ export async function installSkill(requested = "auto", options = {}) {
|
|
|
158
209
|
return { ...item, status: "updated" };
|
|
159
210
|
if (await matchesPackagedSkill(item.path))
|
|
160
211
|
return { ...item, status: "unchanged" };
|
|
212
|
+
if (await isUnmodifiedManagedSkill(item.path))
|
|
213
|
+
return { ...item, status: "updated" };
|
|
161
214
|
throw new Error(`${item.path} already exists and differs; pass --force to replace it`);
|
|
162
215
|
}));
|
|
163
216
|
for (const item of planned) {
|
|
164
217
|
if (item.status === "unchanged")
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
await rm(temporary, { recursive: true, force: true });
|
|
169
|
-
await cp(sourceSkill, temporary, { recursive: true });
|
|
170
|
-
if (options.force)
|
|
171
|
-
await rm(item.path, { recursive: true, force: true });
|
|
172
|
-
await rename(temporary, item.path);
|
|
218
|
+
await writeManagedManifest(item.path);
|
|
219
|
+
else
|
|
220
|
+
await replaceWithPackagedSkill(item.path);
|
|
173
221
|
}
|
|
174
222
|
return planned;
|
|
175
223
|
}
|
|
224
|
+
/** Refresh untouched skill copies installed by bxc, preserving local edits. */
|
|
225
|
+
export async function syncManagedSkills(env = process.env) {
|
|
226
|
+
const detections = await detectHarnesses(env);
|
|
227
|
+
const destinations = new Map();
|
|
228
|
+
for (const target of detections.filter((item) => item.installed)) {
|
|
229
|
+
const current = destinations.get(target.path);
|
|
230
|
+
if (current)
|
|
231
|
+
current.agents.push(target.label);
|
|
232
|
+
else
|
|
233
|
+
destinations.set(target.path, { agents: [target.label], path: target.path });
|
|
234
|
+
}
|
|
235
|
+
const sourceDigest = await directoryDigest(sourceSkill);
|
|
236
|
+
const results = [];
|
|
237
|
+
for (const item of destinations.values()) {
|
|
238
|
+
const currentDigest = await directoryDigest(item.path);
|
|
239
|
+
if (currentDigest === sourceDigest) {
|
|
240
|
+
await writeManagedManifest(item.path);
|
|
241
|
+
results.push({ ...item, status: "unchanged" });
|
|
242
|
+
}
|
|
243
|
+
else if (await isUnmodifiedManagedSkill(item.path)) {
|
|
244
|
+
await replaceWithPackagedSkill(item.path);
|
|
245
|
+
results.push({ ...item, status: "updated" });
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
results.push({ ...item, status: "modified" });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
176
253
|
export async function removeSkill(requested = "auto", options = {}) {
|
|
177
254
|
const env = options.env ?? process.env;
|
|
178
255
|
const detections = await detectHarnesses(env);
|
|
@@ -196,7 +273,9 @@ export async function removeSkill(requested = "auto", options = {}) {
|
|
|
196
273
|
const removals = await Promise.all([...destinations.values()].map(async (item) => {
|
|
197
274
|
if (!await exists(item.path))
|
|
198
275
|
return { ...item, status: "missing" };
|
|
199
|
-
if (!options.force &&
|
|
276
|
+
if (!options.force &&
|
|
277
|
+
!await matchesPackagedSkill(item.path) &&
|
|
278
|
+
!await isUnmodifiedManagedSkill(item.path)) {
|
|
200
279
|
throw new Error(`${item.path} differs from the packaged skill; pass --force to remove it`);
|
|
201
280
|
}
|
|
202
281
|
return { ...item, status: "removed" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boxcompute/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Connect local AI agents to BoxCompute sandboxes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"boxcompute",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsc -p tsconfig.build.json",
|
|
37
37
|
"lint": "oxlint --max-warnings 0 src test",
|
|
38
|
+
"postinstall": "node --eval \"const fs = require('node:fs'); if (fs.existsSync('dist/postinstall.js')) import('./dist/postinstall.js')\"",
|
|
38
39
|
"prepack": "bun run build",
|
|
39
40
|
"test": "bun test",
|
|
40
41
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
@@ -9,20 +9,18 @@ Use `bxc` as the only interface. Authentication belongs to the human: if
|
|
|
9
9
|
`bxc doctor` says the client is not authenticated, ask the user to run
|
|
10
10
|
`bxc auth`; never request, read, print, or transmit their saved credential.
|
|
11
11
|
|
|
12
|
-
## Choose
|
|
12
|
+
## Choose or create a sandbox
|
|
13
13
|
|
|
14
|
-
Run `bxc --json
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Start or resume it with:
|
|
14
|
+
Run `bxc --json workspaces` to choose the parent workspace, including when it
|
|
15
|
+
does not have a sandbox yet. Run `bxc --json sandboxes` to inspect existing
|
|
16
|
+
instances and do not assume the first result is correct. To allocate another
|
|
17
|
+
isolated instance under a workspace, start one with:
|
|
20
18
|
|
|
21
19
|
```sh
|
|
22
20
|
bxc --json sandbox start WORKSPACE_ID
|
|
23
21
|
```
|
|
24
22
|
|
|
25
|
-
The
|
|
23
|
+
The returned sandbox `id` is the stable instance ID used by later commands.
|
|
26
24
|
|
|
27
25
|
## Execute work
|
|
28
26
|
|