@boxcompute/cli 0.2.1 → 0.2.3
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 +7 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +137 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.js +15 -0
- package/package.json +1 -1
- package/skills/boxcompute-sandbox/SKILL.md +5 -0
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ bxc workspaces
|
|
|
48
48
|
bxc sandboxes
|
|
49
49
|
bxc sandbox start WORKSPACE_ID
|
|
50
50
|
# Use the returned sandbox instance ID for later commands.
|
|
51
|
+
bxc sandbox logs SANDBOX_ID --source execute
|
|
51
52
|
bxc sandbox exec SANDBOX_ID -- python -m pytest
|
|
52
53
|
```
|
|
53
54
|
|
|
@@ -61,9 +62,11 @@ Deploy the server release with both Sandbox API v1 and v2 before publishing CLI
|
|
|
61
62
|
contract. After that, clients need only upgrade the package:
|
|
62
63
|
|
|
63
64
|
```sh
|
|
64
|
-
|
|
65
|
+
bxc update
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
`bxc up` is the short alias. If the automatic update cannot invoke npm, use
|
|
69
|
+
`npm install --global @boxcompute/cli@latest` manually. Updating preserves the
|
|
70
|
+
saved BoxCompute credential and refreshes untouched managed skills before the
|
|
71
|
+
client's next agent session. The new CLI uses v2 for multi-instance sandboxes
|
|
72
|
+
and gives a server-first upgrade message if it reaches an older deployment.
|
package/dist/cli.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type CliDependencies = {
|
|
|
13
13
|
now?: () => number;
|
|
14
14
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
15
15
|
openBrowser?: (url: string) => void;
|
|
16
|
+
installUpdate?: (version: string) => Promise<void>;
|
|
16
17
|
loadConnection?: (env: NodeJS.ProcessEnv) => Promise<Connection>;
|
|
17
18
|
loadSavedUrl?: (env: NodeJS.ProcessEnv) => Promise<string | null>;
|
|
18
19
|
saveConnection?: (url: string, token: string, env: NodeJS.ProcessEnv) => Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ Usage: bxc [options] [command]
|
|
|
15
15
|
Commands:
|
|
16
16
|
|
|
17
17
|
version Print the version number and exit
|
|
18
|
+
update [alias: up] Update the CLI to the latest npm release
|
|
18
19
|
login Log in through BoxCompute in your browser
|
|
19
20
|
logout Revoke and remove the saved CLI credential
|
|
20
21
|
auth [alias: login] Authentication commands
|
|
@@ -25,6 +26,7 @@ Commands:
|
|
|
25
26
|
sandbox Manage isolated BoxCompute sandboxes
|
|
26
27
|
start Create and start a workspace sandbox
|
|
27
28
|
status Inspect one sandbox
|
|
29
|
+
logs Read current or retained sandbox logs
|
|
28
30
|
exec Execute a program inside a sandbox
|
|
29
31
|
delete [alias: rm] Destroy the runtime; the workspace remains
|
|
30
32
|
skill [alias: skills] Manage coding-harness skills
|
|
@@ -48,10 +50,12 @@ Login options:
|
|
|
48
50
|
Examples:
|
|
49
51
|
|
|
50
52
|
$ bxc login
|
|
53
|
+
$ bxc update
|
|
51
54
|
$ bxc skill detect
|
|
52
55
|
$ bxc skill install
|
|
53
56
|
$ bxc workspaces
|
|
54
57
|
$ bxc sandbox start WORKSPACE_ID
|
|
58
|
+
$ bxc sandbox logs SANDBOX_ID --source execute
|
|
55
59
|
$ bxc sandbox exec SANDBOX_ID -- python -m pytest
|
|
56
60
|
|
|
57
61
|
Compatibility:
|
|
@@ -66,6 +70,7 @@ Commands:
|
|
|
66
70
|
|
|
67
71
|
start WORKSPACE_ID Create and start a new sandbox instance
|
|
68
72
|
status SANDBOX_ID Inspect one sandbox
|
|
73
|
+
logs SANDBOX_ID [options] Read logs without starting the runtime
|
|
69
74
|
exec SANDBOX_ID [options] -- PROGRAM [ARG...]
|
|
70
75
|
Execute a program inside a sandbox
|
|
71
76
|
delete SANDBOX_ID --yes [alias: rm] Destroy the runtime; keep the workspace
|
|
@@ -76,6 +81,15 @@ Exec options:
|
|
|
76
81
|
--env KEY=VALUE Set an environment variable; repeatable
|
|
77
82
|
--timeout SECONDS Command timeout
|
|
78
83
|
--max-output-bytes BYTES Maximum captured output
|
|
84
|
+
|
|
85
|
+
Log options:
|
|
86
|
+
|
|
87
|
+
--since TIMESTAMP Include entries at or after an RFC 3339 time
|
|
88
|
+
--until TIMESTAMP Include entries before an RFC 3339 time
|
|
89
|
+
--stream stdout|stderr Filter by output stream
|
|
90
|
+
--source workload|execute|process
|
|
91
|
+
Filter by log source
|
|
92
|
+
--limit ENTRIES Maximum entries (default: 1000, max: 5000)
|
|
79
93
|
`;
|
|
80
94
|
const skillHelp = `Manage coding-harness skills
|
|
81
95
|
|
|
@@ -133,6 +147,77 @@ export function openBrowser(url, runtime = {}) {
|
|
|
133
147
|
// polling so headless shells and restricted WSL interop can authenticate.
|
|
134
148
|
}
|
|
135
149
|
}
|
|
150
|
+
function releaseVersion(value) {
|
|
151
|
+
if (typeof value !== "string")
|
|
152
|
+
return null;
|
|
153
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
|
|
154
|
+
if (!match)
|
|
155
|
+
return null;
|
|
156
|
+
const parts = match.slice(1).map(Number);
|
|
157
|
+
return parts.every(Number.isSafeInteger) ? parts : null;
|
|
158
|
+
}
|
|
159
|
+
function compareReleaseVersions(left, right) {
|
|
160
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
161
|
+
if (left[index] !== right[index])
|
|
162
|
+
return left[index] - right[index];
|
|
163
|
+
}
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
async function installCliUpdate(version) {
|
|
167
|
+
const executable = platform() === "win32" ? "npm.cmd" : "npm";
|
|
168
|
+
await new Promise((resolve, reject) => {
|
|
169
|
+
let child;
|
|
170
|
+
try {
|
|
171
|
+
child = spawn(executable, ["install", "--global", `@boxcompute/cli@${version}`], {
|
|
172
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
reject(error);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
child.once("error", reject);
|
|
180
|
+
child.once("exit", (code) => {
|
|
181
|
+
if (code === 0)
|
|
182
|
+
resolve();
|
|
183
|
+
else
|
|
184
|
+
reject(new Error(`npm exited with code ${code ?? "unknown"}`));
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async function updateCli(dependencies) {
|
|
189
|
+
let response;
|
|
190
|
+
try {
|
|
191
|
+
response = await dependencies.fetch("https://registry.npmjs.org/%40boxcompute%2Fcli/latest", {
|
|
192
|
+
headers: { accept: "application/json" },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
throw new Error(`Could not check npm for updates: ${error?.message ?? String(error)}`);
|
|
197
|
+
}
|
|
198
|
+
if (!response.ok)
|
|
199
|
+
throw new Error(`Could not check npm for updates (HTTP ${response.status})`);
|
|
200
|
+
const latest = (await response.json()).version;
|
|
201
|
+
const currentParts = releaseVersion(CLI_VERSION);
|
|
202
|
+
const latestParts = releaseVersion(latest);
|
|
203
|
+
if (!currentParts || !latestParts || typeof latest !== "string") {
|
|
204
|
+
throw new Error("npm returned an invalid BoxCompute CLI version");
|
|
205
|
+
}
|
|
206
|
+
if (compareReleaseVersions(latestParts, currentParts) <= 0) {
|
|
207
|
+
emit(dependencies.io, dependencies.json, { updated: false, version: CLI_VERSION }, `BoxCompute CLI is already up to date (${CLI_VERSION}).\n`);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
write(dependencies.io.stderr, `Updating BoxCompute CLI from ${CLI_VERSION} to ${latest}…\n`);
|
|
211
|
+
try {
|
|
212
|
+
await dependencies.install(latest);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
throw new Error(`Could not install @boxcompute/cli@${latest}: ${error?.message ?? String(error)}. ` +
|
|
216
|
+
`Run \`npm install --global @boxcompute/cli@${latest}\` manually.`);
|
|
217
|
+
}
|
|
218
|
+
emit(dependencies.io, dependencies.json, { updated: true, previousVersion: CLI_VERSION, version: latest }, `Updated BoxCompute CLI to ${latest}.\n`);
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
136
221
|
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
137
222
|
const write = (stream, value) => { stream.write(value); };
|
|
138
223
|
const emit = (io, json, value, human) => {
|
|
@@ -170,6 +255,21 @@ function positive(value, name) {
|
|
|
170
255
|
throw new UsageError(`${name} must be a positive integer`);
|
|
171
256
|
return parsed;
|
|
172
257
|
}
|
|
258
|
+
function timestamp(value, name) {
|
|
259
|
+
if (value === undefined)
|
|
260
|
+
return undefined;
|
|
261
|
+
const parsed = new Date(value);
|
|
262
|
+
if (Number.isNaN(parsed.valueOf()))
|
|
263
|
+
throw new UsageError(`${name} must be an RFC 3339 timestamp`);
|
|
264
|
+
return parsed.toISOString();
|
|
265
|
+
}
|
|
266
|
+
function oneOf(value, name, values) {
|
|
267
|
+
if (value === undefined)
|
|
268
|
+
return undefined;
|
|
269
|
+
if (!values.includes(value))
|
|
270
|
+
throw new UsageError(`${name} must be one of: ${values.join(", ")}`);
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
173
273
|
function environment(tokens) {
|
|
174
274
|
const values = [];
|
|
175
275
|
for (;;) {
|
|
@@ -265,6 +365,21 @@ function executionOutput(io, json, sandboxId, result) {
|
|
|
265
365
|
}
|
|
266
366
|
return result.exitCode ?? 1;
|
|
267
367
|
}
|
|
368
|
+
function logsOutput(io, json, logs) {
|
|
369
|
+
if (json) {
|
|
370
|
+
emit(io, true, { logs }, "");
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
if (!logs.entries.length)
|
|
374
|
+
write(io.stdout, "No logs found.\n");
|
|
375
|
+
for (const entry of logs.entries) {
|
|
376
|
+
const process = entry.process_id ? ` ${entry.process_id}` : "";
|
|
377
|
+
write(io.stdout, `${entry.timestamp}\t${entry.stream}\t${entry.source}${process}\t${entry.message}\n`);
|
|
378
|
+
}
|
|
379
|
+
write(io.stderr, `sandbox=${logs.sandbox_id} entries=${logs.entries.length} retentionSeconds=${logs.retention_seconds}` +
|
|
380
|
+
`${logs.truncated ? " truncated=true" : ""}\n`);
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
268
383
|
export async function runCli(argv, supplied = {}) {
|
|
269
384
|
const env = supplied.env ?? process.env;
|
|
270
385
|
const io = supplied.io ?? { stdout: process.stdout, stderr: process.stderr };
|
|
@@ -272,6 +387,7 @@ export async function runCli(argv, supplied = {}) {
|
|
|
272
387
|
const now = supplied.now ?? Date.now;
|
|
273
388
|
const sleep = supplied.sleep ?? delay;
|
|
274
389
|
const openBrowserImpl = supplied.openBrowser ?? openBrowser;
|
|
390
|
+
const installUpdate = supplied.installUpdate ?? installCliUpdate;
|
|
275
391
|
const load = supplied.loadConnection ?? loadConnection;
|
|
276
392
|
const savedUrl = supplied.loadSavedUrl ?? loadSavedUrl;
|
|
277
393
|
const save = supplied.saveConnection ?? saveConnection;
|
|
@@ -297,6 +413,8 @@ export async function runCli(argv, supplied = {}) {
|
|
|
297
413
|
let command = args.shift();
|
|
298
414
|
if (command === "login")
|
|
299
415
|
command = "auth";
|
|
416
|
+
if (command === "up")
|
|
417
|
+
command = "update";
|
|
300
418
|
if (command === "skills")
|
|
301
419
|
command = "skill";
|
|
302
420
|
if (command === "list" || command === "ls")
|
|
@@ -345,6 +463,11 @@ export async function runCli(argv, supplied = {}) {
|
|
|
345
463
|
}
|
|
346
464
|
return authenticate(args, { env, io, json, fetch: fetchImpl, now, sleep, openBrowser: openBrowserImpl, loadSavedUrl: savedUrl, saveConnection: save });
|
|
347
465
|
}
|
|
466
|
+
if (command === "update") {
|
|
467
|
+
if (args.length)
|
|
468
|
+
throw new UsageError("update takes no options");
|
|
469
|
+
return updateCli({ fetch: fetchImpl, install: installUpdate, io, json });
|
|
470
|
+
}
|
|
348
471
|
if (command === "skill") {
|
|
349
472
|
let action = args.shift();
|
|
350
473
|
if (!action) {
|
|
@@ -453,6 +576,20 @@ export async function runCli(argv, supplied = {}) {
|
|
|
453
576
|
emit(io, json, { sandbox }, sandboxLine(sandbox));
|
|
454
577
|
return 0;
|
|
455
578
|
}
|
|
579
|
+
if (action === "logs") {
|
|
580
|
+
const since = timestamp(option(args, "since"), "--since");
|
|
581
|
+
const until = timestamp(option(args, "until"), "--until");
|
|
582
|
+
const stream = oneOf(option(args, "stream"), "--stream", ["stdout", "stderr"]);
|
|
583
|
+
const source = oneOf(option(args, "source"), "--source", ["workload", "execute", "process"]);
|
|
584
|
+
const limit = positive(option(args, "limit"), "--limit");
|
|
585
|
+
if (limit !== undefined && limit > 5_000)
|
|
586
|
+
throw new UsageError("--limit must be 5000 or fewer");
|
|
587
|
+
if (since && until && since >= until)
|
|
588
|
+
throw new UsageError("--since must be earlier than --until");
|
|
589
|
+
if (args.length)
|
|
590
|
+
throw new UsageError(`Unknown sandbox logs option: ${args[0]}`);
|
|
591
|
+
return logsOutput(io, json, await client.logs(id, { since, until, stream, source, limit }));
|
|
592
|
+
}
|
|
456
593
|
if (action === "delete") {
|
|
457
594
|
if (!flag(args, "yes") || args.length)
|
|
458
595
|
throw new UsageError("sandbox delete requires SANDBOX_ID --yes");
|
package/dist/client.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type Sandbox = {
|
|
|
5
5
|
name: string;
|
|
6
6
|
state: "cold" | "running";
|
|
7
7
|
runtimeId: string | null;
|
|
8
|
+
retainedRuntimeId: string | null;
|
|
8
9
|
image: string | null;
|
|
9
10
|
createdAt: number;
|
|
10
11
|
lastUsedAt: number | null;
|
|
@@ -23,6 +24,20 @@ export type Execution = {
|
|
|
23
24
|
stderrTruncated: boolean;
|
|
24
25
|
wallTimeSeconds: number;
|
|
25
26
|
};
|
|
27
|
+
export type SandboxLogEntry = {
|
|
28
|
+
timestamp: string;
|
|
29
|
+
stream: "stdout" | "stderr";
|
|
30
|
+
source: "workload" | "execute" | "process";
|
|
31
|
+
message: string;
|
|
32
|
+
pod_uid: string;
|
|
33
|
+
process_id?: string;
|
|
34
|
+
};
|
|
35
|
+
export type SandboxLogs = {
|
|
36
|
+
sandbox_id: string;
|
|
37
|
+
entries: SandboxLogEntry[];
|
|
38
|
+
truncated: boolean;
|
|
39
|
+
retention_seconds: number;
|
|
40
|
+
};
|
|
26
41
|
export declare class BoxComputeHttpError extends Error {
|
|
27
42
|
readonly status: number;
|
|
28
43
|
readonly code?: string | undefined;
|
|
@@ -46,5 +61,12 @@ export declare class BoxComputeClient {
|
|
|
46
61
|
maxOutputBytes?: number;
|
|
47
62
|
env?: Record<string, string>;
|
|
48
63
|
}): Promise<Execution>;
|
|
64
|
+
logs(id: string, input?: {
|
|
65
|
+
since?: string;
|
|
66
|
+
until?: string;
|
|
67
|
+
stream?: "stdout" | "stderr";
|
|
68
|
+
source?: "workload" | "execute" | "process";
|
|
69
|
+
limit?: number;
|
|
70
|
+
}): Promise<SandboxLogs>;
|
|
49
71
|
delete(id: string): Promise<void>;
|
|
50
72
|
}
|
package/dist/client.js
CHANGED
|
@@ -66,6 +66,21 @@ export class BoxComputeClient {
|
|
|
66
66
|
body: JSON.stringify(input),
|
|
67
67
|
})).result;
|
|
68
68
|
}
|
|
69
|
+
async logs(id, input = {}) {
|
|
70
|
+
const query = new URLSearchParams();
|
|
71
|
+
if (input.since)
|
|
72
|
+
query.set("since", input.since);
|
|
73
|
+
if (input.until)
|
|
74
|
+
query.set("until", input.until);
|
|
75
|
+
if (input.stream)
|
|
76
|
+
query.set("stream", input.stream);
|
|
77
|
+
if (input.source)
|
|
78
|
+
query.set("source", input.source);
|
|
79
|
+
if (input.limit !== undefined)
|
|
80
|
+
query.set("limit", String(input.limit));
|
|
81
|
+
const suffix = query.size ? `?${query}` : "";
|
|
82
|
+
return (await this.request(`/api/v2/sandboxes/${encodeURIComponent(id)}/logs${suffix}`)).logs;
|
|
83
|
+
}
|
|
69
84
|
async delete(id) {
|
|
70
85
|
await this.request(`/api/v2/sandboxes/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
71
86
|
}
|
package/package.json
CHANGED
|
@@ -44,6 +44,11 @@ sandbox unless the user explicitly places those secrets in scope.
|
|
|
44
44
|
## Lifecycle and safety
|
|
45
45
|
|
|
46
46
|
- Inspect uncertain state with `bxc --json sandbox status SANDBOX_ID`.
|
|
47
|
+
- Read runtime output with `bxc sandbox logs SANDBOX_ID`. Add `--json` for
|
|
48
|
+
structured entries or narrow results with `--since`, `--until`, `--stream`,
|
|
49
|
+
`--source`, and `--limit`. Log reads do not start stopped compute, and remain
|
|
50
|
+
available for the provider's nominal 30-day retention period after deletion;
|
|
51
|
+
storage pressure may shorten that period, so logs are not an archive.
|
|
47
52
|
- Sandboxes persist across commands; do not destroy one merely because the
|
|
48
53
|
current task is finished.
|
|
49
54
|
- `bxc sandbox delete SANDBOX_ID --yes` destroys the remote runtime and is
|