@boxcompute/cli 0.1.1 → 0.2.0
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 +2 -1
- package/dist/cli.js +41 -7
- 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,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type Connection } from "./config.js";
|
|
3
|
-
import { detectHarnesses, installSkill, readSkill, removeSkill } from "./skill.js";
|
|
3
|
+
import { detectHarnesses, installSkill, readSkill, removeSkill, syncManagedSkills } from "./skill.js";
|
|
4
4
|
type Io = {
|
|
5
5
|
stdout: NodeJS.WritableStream;
|
|
6
6
|
stderr: NodeJS.WritableStream;
|
|
@@ -20,6 +20,7 @@ export type CliDependencies = {
|
|
|
20
20
|
installSkill?: typeof installSkill;
|
|
21
21
|
removeSkill?: typeof removeSkill;
|
|
22
22
|
readSkill?: typeof readSkill;
|
|
23
|
+
syncManagedSkills?: typeof syncManagedSkills;
|
|
23
24
|
};
|
|
24
25
|
export declare function runCli(argv: string[], supplied?: CliDependencies): Promise<number>;
|
|
25
26
|
export declare function formatCliError(error: unknown): string;
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { hostname, platform } 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
|
|
@@ -229,6 +232,9 @@ async function authenticate(args, dependencies) {
|
|
|
229
232
|
function sandboxLine(sandbox) {
|
|
230
233
|
return `${sandbox.id}\t${sandbox.state}\t${sandbox.name}\n`;
|
|
231
234
|
}
|
|
235
|
+
function workspaceLine(workspace) {
|
|
236
|
+
return `${workspace.id}\t${workspace.name}\n`;
|
|
237
|
+
}
|
|
232
238
|
function executionOutput(io, json, sandboxId, result) {
|
|
233
239
|
if (json)
|
|
234
240
|
emit(io, true, { sandboxId, ...result }, "");
|
|
@@ -254,6 +260,7 @@ export async function runCli(argv, supplied = {}) {
|
|
|
254
260
|
const install = supplied.installSkill ?? installSkill;
|
|
255
261
|
const remove = supplied.removeSkill ?? removeSkill;
|
|
256
262
|
const skillText = supplied.readSkill ?? readSkill;
|
|
263
|
+
const syncSkills = supplied.syncManagedSkills ?? syncManagedSkills;
|
|
257
264
|
const args = [...argv];
|
|
258
265
|
const json = flag(args, "json");
|
|
259
266
|
const versionRequested = args[0] === "version" || anyFlag(args, "--version", "-V", "-v");
|
|
@@ -274,6 +281,26 @@ export async function runCli(argv, supplied = {}) {
|
|
|
274
281
|
command = "skill";
|
|
275
282
|
if (command === "list" || command === "ls")
|
|
276
283
|
command = "sandboxes";
|
|
284
|
+
const canAutoSync = supplied.syncManagedSkills !== undefined || supplied.env === undefined ||
|
|
285
|
+
Boolean(env.HOME || env.USERPROFILE);
|
|
286
|
+
if (command !== "skill" && canAutoSync) {
|
|
287
|
+
try {
|
|
288
|
+
const synced = await syncSkills(env);
|
|
289
|
+
const updated = synced.filter((item) => item.status === "updated");
|
|
290
|
+
const modified = synced.filter((item) => item.status === "modified");
|
|
291
|
+
if (updated.length) {
|
|
292
|
+
write(io.stderr, `Updated the BoxCompute skill for ${updated.flatMap((item) => item.agents).join(", ")}. ` +
|
|
293
|
+
"Start a new agent session to load it.\n");
|
|
294
|
+
}
|
|
295
|
+
for (const item of modified) {
|
|
296
|
+
write(io.stderr, `Kept locally modified BoxCompute skill at ${item.path}; run ` +
|
|
297
|
+
"`bxc skill install --force` to replace it.\n");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
write(io.stderr, `Could not check installed BoxCompute skills: ${error?.message ?? String(error)}\n`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
277
304
|
if (command === "logout") {
|
|
278
305
|
if (args.length)
|
|
279
306
|
throw new UsageError("logout takes no options");
|
|
@@ -367,14 +394,21 @@ export async function runCli(argv, supplied = {}) {
|
|
|
367
394
|
const client = new BoxComputeClient(connection, fetchImpl);
|
|
368
395
|
if (command === "doctor") {
|
|
369
396
|
const sandboxes = await client.list();
|
|
370
|
-
emit(io, json, { connected: true, url: connection.url, sandboxes: sandboxes.length }, `Connected to ${connection.url} · ${sandboxes.length}
|
|
397
|
+
emit(io, json, { connected: true, url: connection.url, sandboxes: sandboxes.length }, `Connected to ${connection.url} · ${sandboxes.length} sandbox${sandboxes.length === 1 ? "" : "es"}\n`);
|
|
371
398
|
return 0;
|
|
372
399
|
}
|
|
373
400
|
if (command === "sandboxes") {
|
|
374
401
|
if (args.length)
|
|
375
402
|
throw new UsageError("sandboxes takes no options");
|
|
376
403
|
const sandboxes = await client.list();
|
|
377
|
-
emit(io, json, { sandboxes }, sandboxes.length ? sandboxes.map(sandboxLine).join("") : "No
|
|
404
|
+
emit(io, json, { sandboxes }, sandboxes.length ? sandboxes.map(sandboxLine).join("") : "No sandboxes found. Start one for a BoxCompute workspace first.\n");
|
|
405
|
+
return 0;
|
|
406
|
+
}
|
|
407
|
+
if (command === "workspaces") {
|
|
408
|
+
if (args.length)
|
|
409
|
+
throw new UsageError("workspaces takes no options");
|
|
410
|
+
const workspaces = await client.listWorkspaces();
|
|
411
|
+
emit(io, json, { workspaces }, workspaces.length ? workspaces.map(workspaceLine).join("") : "No workspaces found. Create one in BoxCompute first.\n");
|
|
378
412
|
return 0;
|
|
379
413
|
}
|
|
380
414
|
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.0",
|
|
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
|
|