withruntime 0.8.4 → 0.10.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/BLAXEL.md +4 -4
- package/COMPATIBILITY.md +145 -0
- package/DAYTONA.md +13 -7
- package/E2B.md +34 -20
- package/README.md +22 -11
- package/README.src.md +22 -11
- package/VERCEL.md +13 -3
- package/compatibility-lock.json +357 -0
- package/dist/ai-harness/index.js +2 -2
- package/dist/api-defaults.d.ts +17 -0
- package/dist/api-defaults.js +22 -0
- package/dist/blaxel/errors.js +1 -1
- package/dist/blaxel/lookup.d.ts +3 -2
- package/dist/blaxel/lookup.js +3 -2
- package/dist/blaxel/pagination.d.ts +2 -2
- package/dist/blaxel/pagination.js +45 -17
- package/dist/blaxel/preview.d.ts +3 -2
- package/dist/blaxel/preview.js +16 -14
- package/dist/blaxel/process.d.ts +1 -1
- package/dist/blaxel/process.js +66 -11
- package/dist/blaxel/sandbox.d.ts +2 -0
- package/dist/blaxel/sandbox.js +2 -0
- package/dist/blaxel/snapshot.js +3 -5
- package/dist/cli-words.d.ts +27 -0
- package/dist/cli-words.js +135 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +351 -85
- package/dist/client.d.ts +33 -4
- package/dist/client.js +47 -12
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +303 -0
- package/dist/cloudflare/index.js +592 -0
- package/dist/cloudflare/worker.d.ts +419 -0
- package/dist/cloudflare/worker.js +447 -0
- package/dist/codesandbox/guest-service.d.ts +31 -0
- package/dist/codesandbox/guest-service.js +490 -0
- package/dist/codesandbox/index.d.ts +386 -0
- package/dist/codesandbox/index.js +1347 -0
- package/dist/compat/core.d.ts +40 -0
- package/dist/compat/core.js +238 -0
- package/dist/compat/files.d.ts +26 -0
- package/dist/compat/files.js +133 -0
- package/dist/compat/shell.d.ts +4 -0
- package/dist/compat/shell.js +165 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/context.js +2 -1
- package/dist/daytona/daytona.js +11 -4
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +121 -38
- package/dist/daytona/index.d.ts +1 -1
- package/dist/daytona/process.d.ts +1 -1
- package/dist/daytona/process.js +27 -4
- package/dist/daytona/sandbox.d.ts +1 -1
- package/dist/daytona/sandbox.js +16 -7
- package/dist/e2b/client.js +1 -1
- package/dist/e2b/code-interpreter.d.ts +4 -5
- package/dist/e2b/code-interpreter.js +54 -56
- package/dist/e2b/commands.d.ts +35 -12
- package/dist/e2b/commands.js +169 -73
- package/dist/e2b/errors.js +1 -1
- package/dist/e2b/filesystem.d.ts +8 -6
- package/dist/e2b/filesystem.js +127 -28
- package/dist/e2b/index.d.ts +1 -0
- package/dist/e2b/index.js +1 -0
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +80 -0
- package/dist/e2b/sandbox.d.ts +6 -4
- package/dist/e2b/sandbox.js +59 -25
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -2
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +309 -0
- package/dist/freestyle/index.js +343 -0
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.js +2 -2
- package/dist/modal/index.d.ts +205 -0
- package/dist/modal/index.js +486 -0
- package/dist/network-cli.js +112 -44
- package/dist/openai-agents/index.js +18 -71
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/audit.d.ts +3 -0
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -0
- package/dist/products/interpreter.d.ts +11 -4
- package/dist/products/interpreter.js +33 -9
- package/dist/products/jobs.js +3 -2
- package/dist/products/limits.d.ts +10 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +18 -4
- package/dist/products/previews.js +14 -5
- package/dist/products/tailscale.d.ts +57 -0
- package/dist/products/tailscale.js +17 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.d.ts +25 -4
- package/dist/products/watch.js +132 -18
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +326 -0
- package/dist/runloop/index.js +535 -0
- package/dist/runloop/pagination.d.ts +46 -0
- package/dist/runloop/pagination.js +82 -0
- package/dist/runloop/snapshots.d.ts +52 -0
- package/dist/runloop/snapshots.js +244 -0
- package/dist/sandbox.d.ts +76 -16
- package/dist/sandbox.js +696 -135
- package/dist/snapshots.d.ts +20 -5
- package/dist/snapshots.js +11 -1
- package/dist/sprites/index.d.ts +251 -0
- package/dist/sprites/index.js +588 -0
- package/dist/tar.d.ts +10 -1
- package/dist/tar.js +311 -35
- package/dist/tools/index.js +2 -1
- package/dist/transport.d.ts +23 -8
- package/dist/transport.js +247 -127
- package/dist/types.d.ts +52 -3
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +15 -3
- package/dist/vercel/errors.d.ts +2 -2
- package/dist/vercel/errors.js +19 -2
- package/dist/vercel/filesystem.d.ts +21 -18
- package/dist/vercel/filesystem.js +95 -48
- package/dist/vercel/sandbox.d.ts +8 -8
- package/dist/vercel/sandbox.js +159 -64
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +46 -3
package/dist/blaxel/preview.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { guard, isNotFound, NotSupportedError, responseError } from "./errors.js";
|
|
2
2
|
import { PREVIEW } from "./lookup.js";
|
|
3
|
-
/** Runtime's shortest and longest token, in seconds. */
|
|
4
|
-
const TOKEN_MIN = 60;
|
|
5
|
-
const TOKEN_MAX = 7 * 86_400;
|
|
6
3
|
function checkName(name) {
|
|
7
4
|
if (!/^[A-Za-z0-9._-]{1,48}$/.test(name))
|
|
8
5
|
throw new NotSupportedError(`The preview name "${name}"`, "Use 1 to 48 letters, digits, dots, dashes or underscores.");
|
|
@@ -56,19 +53,24 @@ export class SandboxPreviewTokens {
|
|
|
56
53
|
get resourceName() {
|
|
57
54
|
return this.#ctx.sandboxName;
|
|
58
55
|
}
|
|
59
|
-
/** A token
|
|
56
|
+
/** A token bounded by `expiresAt`; native signing floors milliseconds and
|
|
57
|
+
* caps it to the caller's session. Older servers refuse without a TTL fallback. */
|
|
60
58
|
async create(expiresAt) {
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
throw
|
|
64
|
-
const
|
|
65
|
-
|
|
59
|
+
const requested = expiresAt.getTime();
|
|
60
|
+
if (!Number.isFinite(requested))
|
|
61
|
+
throw responseError(400, "A preview token needs a valid expiration date.");
|
|
62
|
+
const remaining = requested - Date.now();
|
|
63
|
+
if (remaining < 60_000 || remaining > 7 * 86_400_000)
|
|
64
|
+
throw new NotSupportedError("A preview token lasting less than a minute or more than a week", "Request an expiration at least one minute and at most seven days away.");
|
|
65
|
+
const got = await this.#ctx.run((runtime) => runtime.previews.get(this.#preview.spec.port, undefined, {
|
|
66
|
+
expiresAt: expiresAt.toISOString(),
|
|
67
|
+
}));
|
|
68
|
+
const actual = got.tokenExpiresAt === null ? Number.NaN : Date.parse(got.tokenExpiresAt);
|
|
69
|
+
if (!got.token || !Number.isFinite(actual) || actual > requested)
|
|
70
|
+
throw new NotSupportedError("A preview token with the requested expiration", "The server must issue a token that never outlives the requested expiration; no duration-based fallback was requested.");
|
|
66
71
|
return new SandboxPreviewToken({
|
|
67
72
|
metadata: { name: `token-${Date.now()}`, previewName: this.previewName },
|
|
68
|
-
spec: {
|
|
69
|
-
...(got.token ? { token: got.token } : {}),
|
|
70
|
-
...(got.tokenExpiresAt ? { expiresAt: got.tokenExpiresAt } : {}),
|
|
71
|
-
},
|
|
73
|
+
spec: { token: got.token, expiresAt: got.tokenExpiresAt },
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
list() {
|
|
@@ -193,7 +195,7 @@ export class SandboxPreviews {
|
|
|
193
195
|
const found = await this.#ctx.run((runtime) => runtime.previews.get(port));
|
|
194
196
|
return new SandboxPreview(modelOf(found, previewName, this.sandboxName), this.#ctx);
|
|
195
197
|
}
|
|
196
|
-
/** Stops sharing the port; open connections
|
|
198
|
+
/** Stops sharing the port; its open connections are closed before this returns. */
|
|
197
199
|
async delete(previewName) {
|
|
198
200
|
const port = this.#portOf(previewName);
|
|
199
201
|
if (port === undefined)
|
package/dist/blaxel/process.d.ts
CHANGED
|
@@ -87,7 +87,7 @@ export declare class SandboxProcess {
|
|
|
87
87
|
* process running; with it, once the process ends, with its output. */
|
|
88
88
|
exec(request: ProcessRequest | ProcessRequestWithLog): Promise<PostProcessResponse | ProcessResponseWithLog>;
|
|
89
89
|
/** The process, with the output it has written so far. */
|
|
90
|
-
get(identifier: string,
|
|
90
|
+
get(identifier: string, options?: {
|
|
91
91
|
signal?: AbortSignal;
|
|
92
92
|
retry?: boolean;
|
|
93
93
|
}): Promise<GetProcessByIdentifierResponse>;
|
package/dist/blaxel/process.js
CHANGED
|
@@ -152,14 +152,17 @@ export class SandboxProcess {
|
|
|
152
152
|
}
|
|
153
153
|
/** A call that found the sandbox paused (autoWake off) is made again once,
|
|
154
154
|
* after a wake: it never started, so it cannot run twice. */
|
|
155
|
-
async #retryPaused(work) {
|
|
155
|
+
async #retryPaused(work, signal) {
|
|
156
|
+
signal?.throwIfAborted();
|
|
156
157
|
try {
|
|
157
158
|
return await work();
|
|
158
159
|
}
|
|
159
160
|
catch (error) {
|
|
161
|
+
signal?.throwIfAborted();
|
|
160
162
|
if (codeOf(error) !== "sandbox_paused")
|
|
161
163
|
throw translate(error);
|
|
162
164
|
await this.#ctx.wake();
|
|
165
|
+
signal?.throwIfAborted();
|
|
163
166
|
return guard(work);
|
|
164
167
|
}
|
|
165
168
|
}
|
|
@@ -182,6 +185,8 @@ export class SandboxProcess {
|
|
|
182
185
|
const startedAt = new Date().toISOString();
|
|
183
186
|
const run = async () => {
|
|
184
187
|
for await (const event of runtime.execStream(line, { ...options, signal: abort.signal })) {
|
|
188
|
+
if (event.type === "truncated")
|
|
189
|
+
throw responseError(502, "Some command output is no longer available.");
|
|
185
190
|
if (event.type === "start") {
|
|
186
191
|
id = event.processId;
|
|
187
192
|
this.#remember(id, meta);
|
|
@@ -279,16 +284,20 @@ export class SandboxProcess {
|
|
|
279
284
|
};
|
|
280
285
|
}
|
|
281
286
|
/** A process by pid or name: the registry first, then Runtime's list. */
|
|
282
|
-
async #find(identifier) {
|
|
287
|
+
async #find(identifier, signal) {
|
|
288
|
+
signal?.throwIfAborted();
|
|
283
289
|
const runtime = await this.#ctx.live();
|
|
290
|
+
signal?.throwIfAborted();
|
|
284
291
|
const registry = this.#registry;
|
|
285
292
|
const known = registry.byName.get(identifier) ?? (registry.meta.has(identifier) ? identifier : undefined);
|
|
286
293
|
if (known !== undefined) {
|
|
287
|
-
const process = await this.#retryPaused(() => runtime.processes.get(known));
|
|
294
|
+
const process = await this.#retryPaused(() => runtime.processes.get(known, { signal }), signal);
|
|
295
|
+
signal?.throwIfAborted();
|
|
288
296
|
registry.processes.set(known, process);
|
|
289
297
|
return { id: known, info: process.info, process };
|
|
290
298
|
}
|
|
291
|
-
const all = await this.#retryPaused(() => runtime.processes.list());
|
|
299
|
+
const all = await this.#retryPaused(() => runtime.processes.list({ signal }), signal);
|
|
300
|
+
signal?.throwIfAborted();
|
|
292
301
|
let found;
|
|
293
302
|
for (const info of all) {
|
|
294
303
|
const parsed = parseProcessLine(info.command);
|
|
@@ -321,15 +330,20 @@ export class SandboxProcess {
|
|
|
321
330
|
const running = found.info.state === "running";
|
|
322
331
|
if (running && !untilExit && found.info.outputBytes <= found.info.firstOffset)
|
|
323
332
|
return out;
|
|
333
|
+
signal?.throwIfAborted();
|
|
324
334
|
const runtime = await this.#ctx.live();
|
|
335
|
+
signal?.throwIfAborted();
|
|
325
336
|
const abort = new AbortController();
|
|
326
337
|
const forward = () => abort.abort();
|
|
338
|
+
signal?.throwIfAborted();
|
|
327
339
|
signal?.addEventListener("abort", forward, { once: true });
|
|
328
340
|
try {
|
|
329
341
|
for await (const event of runtime.processes.follow(found.id, {
|
|
330
342
|
cursor: 0,
|
|
331
343
|
signal: abort.signal,
|
|
332
344
|
})) {
|
|
345
|
+
if (event.type === "truncated")
|
|
346
|
+
throw responseError(502, "Some command output is no longer available.");
|
|
333
347
|
if (event.type === "stdout" || event.type === "stderr") {
|
|
334
348
|
out[event.type] += event.data;
|
|
335
349
|
out.logs += event.data;
|
|
@@ -362,10 +376,32 @@ export class SandboxProcess {
|
|
|
362
376
|
return out;
|
|
363
377
|
}
|
|
364
378
|
/** The process, with the output it has written so far. */
|
|
365
|
-
async get(identifier,
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
379
|
+
async get(identifier, options = {}) {
|
|
380
|
+
options.signal?.throwIfAborted();
|
|
381
|
+
const signal = options.signal;
|
|
382
|
+
let onAbort;
|
|
383
|
+
const interrupted = new Promise((_, reject) => {
|
|
384
|
+
onAbort = () => reject(new Error("Process read was interrupted.", { cause: signal?.reason }));
|
|
385
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
386
|
+
});
|
|
387
|
+
try {
|
|
388
|
+
const reading = (async () => {
|
|
389
|
+
const found = await this.#find(identifier, signal);
|
|
390
|
+
const out = await this.#output(found, false, signal);
|
|
391
|
+
signal?.throwIfAborted();
|
|
392
|
+
return this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
|
|
393
|
+
})();
|
|
394
|
+
return await Promise.race([reading, interrupted]);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
if (signal?.aborted)
|
|
398
|
+
throw signal.reason ?? error;
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
if (onAbort)
|
|
403
|
+
signal?.removeEventListener("abort", onAbort);
|
|
404
|
+
}
|
|
369
405
|
}
|
|
370
406
|
/** Waits for the process to end (at most `maxWait` ms, -1 for no limit) and
|
|
371
407
|
* answers it with its output. The process is never stopped by the wait. */
|
|
@@ -381,12 +417,27 @@ export class SandboxProcess {
|
|
|
381
417
|
throw late();
|
|
382
418
|
const abort = new AbortController();
|
|
383
419
|
const forward = () => abort.abort(signal?.reason);
|
|
420
|
+
signal?.throwIfAborted();
|
|
384
421
|
signal?.addEventListener("abort", forward, { once: true });
|
|
385
422
|
const timer = maxWait === -1 ? undefined : setTimeout(() => abort.abort(late()), maxWait);
|
|
423
|
+
let onAbort;
|
|
424
|
+
const interrupted = new Promise((_, reject) => {
|
|
425
|
+
onAbort = () => {
|
|
426
|
+
const reason = abort.signal.reason;
|
|
427
|
+
reject(reason instanceof Error
|
|
428
|
+
? reason
|
|
429
|
+
: new Error("Process wait was interrupted.", { cause: reason }));
|
|
430
|
+
};
|
|
431
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
432
|
+
});
|
|
386
433
|
try {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
434
|
+
const result = (async () => {
|
|
435
|
+
const found = await this.#find(identifier, abort.signal);
|
|
436
|
+
const out = await this.#output(found, true, abort.signal);
|
|
437
|
+
abort.signal.throwIfAborted();
|
|
438
|
+
return await this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
|
|
439
|
+
})();
|
|
440
|
+
return await Promise.race([result, interrupted]);
|
|
390
441
|
}
|
|
391
442
|
catch (error) {
|
|
392
443
|
if (abort.signal.aborted)
|
|
@@ -395,6 +446,8 @@ export class SandboxProcess {
|
|
|
395
446
|
}
|
|
396
447
|
finally {
|
|
397
448
|
clearTimeout(timer);
|
|
449
|
+
if (onAbort)
|
|
450
|
+
abort.signal.removeEventListener("abort", onAbort);
|
|
398
451
|
signal?.removeEventListener("abort", forward);
|
|
399
452
|
}
|
|
400
453
|
}
|
|
@@ -476,6 +529,8 @@ export class SandboxProcess {
|
|
|
476
529
|
cursor: 0,
|
|
477
530
|
signal: abort.signal,
|
|
478
531
|
})) {
|
|
532
|
+
if (event.type === "truncated")
|
|
533
|
+
throw responseError(502, "Some command output is no longer available.");
|
|
479
534
|
if (event.type === "stdout" || event.type === "stderr") {
|
|
480
535
|
const lines = (partial[event.type] + event.data).split(/\r?\n/);
|
|
481
536
|
partial[event.type] = lines.pop();
|
package/dist/blaxel/sandbox.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export type SandboxForkOptions = {
|
|
|
32
32
|
/** A snapshot's id or, of this sandbox's snapshots, its name. */
|
|
33
33
|
snapshotId?: string;
|
|
34
34
|
envs?: Env[];
|
|
35
|
+
/** Blaxel's fork override; Runtime cannot enforce this override during a fork. */
|
|
36
|
+
lifecycle?: SandboxLifecycle;
|
|
35
37
|
};
|
|
36
38
|
export type SandboxArchiveOptions = {
|
|
37
39
|
wait?: boolean;
|
package/dist/blaxel/sandbox.js
CHANGED
|
@@ -750,6 +750,8 @@ export class SandboxInstance {
|
|
|
750
750
|
* (files, memory and running processes), or started from a snapshot with
|
|
751
751
|
* `snapshotId`. `envs` are added over this sandbox's. */
|
|
752
752
|
async fork(targetName, options = {}) {
|
|
753
|
+
if (options.lifecycle !== undefined)
|
|
754
|
+
throw new NotSupportedError("Overriding lifecycle during a fork", "Omit lifecycle to use the existing Runtime fork behavior.");
|
|
753
755
|
refuseApplication(options);
|
|
754
756
|
const envs = envRecord(options.envs);
|
|
755
757
|
checkEnvNames(Object.keys(envs));
|
package/dist/blaxel/snapshot.js
CHANGED
|
@@ -22,13 +22,11 @@ export function snapshotModel(snapshot, sandboxName) {
|
|
|
22
22
|
}
|
|
23
23
|
/** Envs as a record; a nameless one is refused. */
|
|
24
24
|
export function envRecord(envs) {
|
|
25
|
-
|
|
26
|
-
for (const env of envs ?? []) {
|
|
25
|
+
return Object.fromEntries((envs ?? []).map((env) => {
|
|
27
26
|
if (!env.name)
|
|
28
27
|
throw responseError(400, "every env needs a name");
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
return out;
|
|
28
|
+
return [env.name, env.value ?? ""];
|
|
29
|
+
}));
|
|
32
30
|
}
|
|
33
31
|
/** Appends envs to a sandbox's env file: they win over the ones it has. */
|
|
34
32
|
export async function appendEnvs(runtime, envs) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RuntimeError } from "./errors.js";
|
|
2
|
+
import type { SandboxMetrics } from "./products/observability.js";
|
|
3
|
+
/** A refusal of what a command sent, reworded for the command line: every
|
|
4
|
+
* issue named by its option, since the CLI never prints details.issues, and
|
|
5
|
+
* a hint that names the options rather than the request's fields. Anything
|
|
6
|
+
* else is left as the API said it. */
|
|
7
|
+
export declare function cliRefusal(error: RuntimeError): {
|
|
8
|
+
message: string;
|
|
9
|
+
hint?: string;
|
|
10
|
+
};
|
|
11
|
+
/** A resource read as one fact a line, as `sandbox get` prints: a list
|
|
12
|
+
* joined, a nested record as its own facts, nothing as `-`. */
|
|
13
|
+
export declare function describeRecord(record: Record<string, unknown>, table: (rows: string[][]) => string): string;
|
|
14
|
+
/** The newest CPU reading the metrics hold, and how old it is. `latest` is
|
|
15
|
+
* the last sample, which can carry no CPU figure (the first after a wake has
|
|
16
|
+
* nothing to compare with), while the points before it do; "no reading"
|
|
17
|
+
* above a sparkline of readings contradicted itself. */
|
|
18
|
+
export declare function newestCpu(m: SandboxMetrics, now?: number): {
|
|
19
|
+
percent: number;
|
|
20
|
+
cores: number | null;
|
|
21
|
+
ago: string | null;
|
|
22
|
+
} | null;
|
|
23
|
+
/** The lines of the top-level help about one command, ` feedback ...` and
|
|
24
|
+
* the lines that continue it, or null when it has none. `runtime feedback
|
|
25
|
+
* help` filed "help" as feedback, and `runtime support help` sent it to
|
|
26
|
+
* support, until 30 September 2026. */
|
|
27
|
+
export declare function topicHelp(help: string, command: string): string | null;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* What the CLI says in its own words where the API's would mislead: the
|
|
2
|
+
option a refusal is about rather than the request field it was sent as,
|
|
3
|
+
one fact a line for a resource read, and the newest CPU reading a
|
|
4
|
+
sandbox's metrics hold. Kept out of cli.ts, which many hands edit. */
|
|
5
|
+
/** The request fields a command line sets, by the option that sets them.
|
|
6
|
+
* An amount in microdollars is typed in dollars. */
|
|
7
|
+
const OPTIONS = {
|
|
8
|
+
name: { flag: "--name" },
|
|
9
|
+
labels: { flag: "--label" },
|
|
10
|
+
env: { flag: "--env" },
|
|
11
|
+
region: { flag: "--region" },
|
|
12
|
+
vcpu: { flag: "--vcpu" },
|
|
13
|
+
memoryMiB: { flag: "--memory" },
|
|
14
|
+
diskMiB: { flag: "--disk" },
|
|
15
|
+
cpu: { flag: "--cpu" },
|
|
16
|
+
cpuFloorMillis: { flag: "--cpu-floor" },
|
|
17
|
+
timeoutSeconds: { flag: "--timeout" },
|
|
18
|
+
onLeaseEnd: { flag: "--on-timeout" },
|
|
19
|
+
idlePauseSeconds: { flag: "--idle-pause" },
|
|
20
|
+
autoWake: { flag: "--no-auto-wake / --auto-wake" },
|
|
21
|
+
persistent: { flag: "--persistent" },
|
|
22
|
+
funding: { flag: "--trial / --paid" },
|
|
23
|
+
image: { flag: "--image" },
|
|
24
|
+
snapshot: { flag: "--snapshot" },
|
|
25
|
+
volumes: { flag: "--volume" },
|
|
26
|
+
network: { flag: "--allow / --deny / --connect / --no-internet" },
|
|
27
|
+
maxCostMicros: { flag: "--max-cost", micros: true },
|
|
28
|
+
maxTotalCostMicros: { flag: "--max-total-cost", micros: true },
|
|
29
|
+
};
|
|
30
|
+
const usd = (micros) => `$${(micros / 1e6).toLocaleString("en-US", { maximumFractionDigits: 6 })}`;
|
|
31
|
+
/** One issue the API found in a request, in the command line's words:
|
|
32
|
+
* `body.timeoutSeconds must be at least 60` is `--timeout must be at least 60`. */
|
|
33
|
+
function issueWords(path, message) {
|
|
34
|
+
const field = /^(?:\$\.)?body\.([A-Za-z]+)/.exec(path)?.[1];
|
|
35
|
+
const option = field && Object.hasOwn(OPTIONS, field) ? OPTIONS[field] : undefined;
|
|
36
|
+
if (!option) {
|
|
37
|
+
const where = path.replace(/^\$\.?/, "").replace(/^(body|query|path)\./, "") || "the request";
|
|
38
|
+
return `${where} ${message}`;
|
|
39
|
+
}
|
|
40
|
+
const said = option.micros ? message.replace(/-?\d+(?:\.\d+)?/g, (n) => usd(Number(n))) : message;
|
|
41
|
+
return `${option.flag} ${said}`;
|
|
42
|
+
}
|
|
43
|
+
/** A refusal of what a command sent, reworded for the command line: every
|
|
44
|
+
* issue named by its option, since the CLI never prints details.issues, and
|
|
45
|
+
* a hint that names the options rather than the request's fields. Anything
|
|
46
|
+
* else is left as the API said it. */
|
|
47
|
+
export function cliRefusal(error) {
|
|
48
|
+
const issues = error.details?.issues;
|
|
49
|
+
if (error.code === "invalid_request" && Array.isArray(issues) && issues.length) {
|
|
50
|
+
const lines = issues
|
|
51
|
+
.filter((i) => typeof i.path === "string" && typeof i.message === "string")
|
|
52
|
+
.map((i) => issueWords(i.path, i.message));
|
|
53
|
+
if (lines.length)
|
|
54
|
+
return {
|
|
55
|
+
message: lines.length === 1 ? `${lines[0]}.` : lines.map((l) => `\n ${l}`).join(""),
|
|
56
|
+
hint: "Change what is named above and run the command again.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const field = typeof error.details?.field === "string" ? error.details.field : "";
|
|
60
|
+
const option = Object.hasOwn(OPTIONS, field) ? OPTIONS[field] : undefined;
|
|
61
|
+
if (option && !option.micros && error.message.includes(field)) {
|
|
62
|
+
const named = new RegExp(`\\b${field}\\b`, "g");
|
|
63
|
+
return {
|
|
64
|
+
message: error.message.replace(named, option.flag),
|
|
65
|
+
...(error.hint ? { hint: error.hint.replace(named, option.flag) } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { message: error.message, ...(error.hint ? { hint: error.hint } : {}) };
|
|
69
|
+
}
|
|
70
|
+
/** A resource read as one fact a line, as `sandbox get` prints: a list
|
|
71
|
+
* joined, a nested record as its own facts, nothing as `-`. */
|
|
72
|
+
export function describeRecord(record, table) {
|
|
73
|
+
const rows = [];
|
|
74
|
+
const cell = (value) => {
|
|
75
|
+
if (value === null || value === undefined || value === "")
|
|
76
|
+
return "-";
|
|
77
|
+
if (Array.isArray(value))
|
|
78
|
+
return value.length
|
|
79
|
+
? value.map((v) => (v && typeof v === "object" ? JSON.stringify(v) : String(v))).join(", ")
|
|
80
|
+
: "-";
|
|
81
|
+
// A string as it is; a number, a boolean or a record as JSON writes it.
|
|
82
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
83
|
+
};
|
|
84
|
+
const add = (prefix, value) => {
|
|
85
|
+
for (const [key, v] of Object.entries(value)) {
|
|
86
|
+
const name = prefix ? `${prefix}.${key}` : key;
|
|
87
|
+
if (v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length <= 8)
|
|
88
|
+
add(name, v);
|
|
89
|
+
else
|
|
90
|
+
rows.push([name, cell(v)]);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
add("", record);
|
|
94
|
+
return table(rows);
|
|
95
|
+
}
|
|
96
|
+
/** The newest CPU reading the metrics hold, and how old it is. `latest` is
|
|
97
|
+
* the last sample, which can carry no CPU figure (the first after a wake has
|
|
98
|
+
* nothing to compare with), while the points before it do; "no reading"
|
|
99
|
+
* above a sparkline of readings contradicted itself. */
|
|
100
|
+
export function newestCpu(m, now = Date.now()) {
|
|
101
|
+
if (m.latest?.cpuPercent != null)
|
|
102
|
+
return { percent: m.latest.cpuPercent, cores: m.latest.cpuCores, ago: null };
|
|
103
|
+
for (let i = m.points.length - 1; i >= 0; i--) {
|
|
104
|
+
const p = m.points[i];
|
|
105
|
+
if (p.cpuPercent == null)
|
|
106
|
+
continue;
|
|
107
|
+
const seconds = Math.max(0, Math.round((now - Date.parse(p.at)) / 1000));
|
|
108
|
+
const ago = seconds < 90
|
|
109
|
+
? `${seconds} s ago`
|
|
110
|
+
: seconds < 5400
|
|
111
|
+
? `${Math.round(seconds / 60)} min ago`
|
|
112
|
+
: `${Math.round(seconds / 3600)} h ago`;
|
|
113
|
+
return { percent: p.cpuPercent, cores: p.cpuCores, ago };
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
/** The lines of the top-level help about one command, ` feedback ...` and
|
|
118
|
+
* the lines that continue it, or null when it has none. `runtime feedback
|
|
119
|
+
* help` filed "help" as feedback, and `runtime support help` sent it to
|
|
120
|
+
* support, until 30 September 2026. */
|
|
121
|
+
export function topicHelp(help, command) {
|
|
122
|
+
const lines = help.split("\n");
|
|
123
|
+
const out = [];
|
|
124
|
+
let inside = false;
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const starts = line.startsWith(` ${command} `) || line === ` ${command}`;
|
|
127
|
+
if (starts)
|
|
128
|
+
inside = true;
|
|
129
|
+
else if (inside && !/^ {3,}\S/.test(line))
|
|
130
|
+
inside = false;
|
|
131
|
+
if (inside)
|
|
132
|
+
out.push(line);
|
|
133
|
+
}
|
|
134
|
+
return out.length ? out.join("\n") : null;
|
|
135
|
+
}
|
package/dist/cli.d.ts
CHANGED