@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 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
- bxc sandbox exec WORKSPACE_ID -- python -m pytest
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
- sandboxes [aliases: list, ls] List workspaces and sandbox state
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 Start or resume a workspace sandbox
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 sandbox exec WORKSPACE_ID -- python -m pytest
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 Start or resume a workspace sandbox
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} workspace${sandboxes.length === 1 ? "" : "s"}\n`);
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 workspaces found. Create one in BoxCompute first.\n");
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: "not-created" | "cold" | "running";
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
- return publicRequest(this.connection.url, pathname, { ...init, headers }, this.fetchImpl);
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/v1/sandboxes")).sandboxes;
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/v1/auth", { method: "DELETE" });
50
+ await this.request("/api/v2/auth", { method: "DELETE" });
38
51
  }
39
52
  async inspect(id) {
40
- return (await this.request(`/api/v1/sandboxes/${encodeURIComponent(id)}`)).sandbox;
53
+ return (await this.request(`/api/v2/sandboxes/${encodeURIComponent(id)}`)).sandbox;
41
54
  }
42
55
  async start(workspaceId) {
43
- return (await this.request("/api/v1/sandboxes", {
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/v1/sandboxes/${encodeURIComponent(id)}/execute`, {
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/v1/sandboxes/${encodeURIComponent(id)}`, { method: "DELETE" });
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.localeCompare(right.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
- continue;
166
- await mkdir(path.dirname(item.path), { recursive: true });
167
- const temporary = `${item.path}.tmp-${process.pid}`;
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 && !await matchesPackagedSkill(item.path)) {
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.1.1",
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 the workspace
12
+ ## Choose or create a sandbox
13
13
 
14
- Run `bxc --json sandboxes` and select the workspace whose name matches the
15
- task. Do not assume the first result is correct. If no workspace fits, tell the
16
- user to create one in BoxCompute; the CLI deliberately does not create account
17
- workspaces.
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 stable workspace ID is also the sandbox ID used by later commands.
23
+ The returned sandbox `id` is the stable instance ID used by later commands.
26
24
 
27
25
  ## Execute work
28
26