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/cli.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
+
import { once } from "node:events";
|
|
3
4
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
5
|
import { dirname, join, resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { connectionOrigins, connectionStore, resolveCredential } from "./credentials.js";
|
|
7
|
+
import { checkBrowserOrigins, connectionOrigins, connectionStore, resolveCredential, } from "./credentials.js";
|
|
7
8
|
import { me, named } from "./cli-name.js";
|
|
8
9
|
import { Runtime } from "./client.js";
|
|
9
10
|
import { Sandbox as SandboxHandle } from "./sandbox.js";
|
|
10
11
|
import { NotFoundError, RuntimeError } from "./errors.js";
|
|
11
12
|
import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
|
|
12
13
|
import { billingCommand } from "./billing-cli.js";
|
|
14
|
+
import { cliRefusal, describeRecord, newestCpu, topicHelp } from "./cli-words.js";
|
|
13
15
|
import { envFetch } from "./proxy.js";
|
|
14
16
|
import { VERSION } from "./transport.js";
|
|
15
17
|
const HELP = `Runtime Cloud CLI ${VERSION}
|
|
@@ -35,12 +37,16 @@ Account
|
|
|
35
37
|
sso Single sign-on and SCIM directory sync (owner or
|
|
36
38
|
admin keys); an owner changes it on the website
|
|
37
39
|
usage [--csv] Balance, trial time and charges; --csv, each resource
|
|
40
|
+
usage export --since <UTC time> --until <UTC time>
|
|
41
|
+
Every settlement in the range as CSV; --json, each page
|
|
38
42
|
limits Whether this key is read-only, and its daily spending limit
|
|
39
43
|
secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
|
|
40
44
|
[--allow '[GET,HEAD] /path/*']... Store a secret sandboxes use without seeing it; the
|
|
41
45
|
value is read from standard input, never an argument.
|
|
42
46
|
--allow (paid): only these methods and paths get it
|
|
43
|
-
secrets
|
|
47
|
+
secrets set <NAME> --jobs [--host ...] A copy scheduled jobs put in their environment
|
|
48
|
+
(job create --secret NAME); with --host, both
|
|
49
|
+
secrets ls | get <NAME> Names, hosts, placeholders and jobs versions, never values
|
|
44
50
|
secrets rotate <NAME> A new version of the jobs copy, from standard input
|
|
45
51
|
secrets reveal <NAME> [--version N] Print the jobs copy's value. The sandboxes' copy is
|
|
46
52
|
never readable, by anyone
|
|
@@ -86,6 +92,7 @@ Products
|
|
|
86
92
|
snapshot Saved sandboxes, disk and memory, to start or fork from
|
|
87
93
|
image Custom images from a Dockerfile, any public or private image, or a package list
|
|
88
94
|
volume Persistent disks to attach to sandboxes
|
|
95
|
+
job A command in a fresh sandbox, once or on a cron schedule, with its logs
|
|
89
96
|
domain Your own hostname for a sandbox's port, with HTTPS
|
|
90
97
|
port A public TCP port to a sandbox: databases, game servers
|
|
91
98
|
address A dedicated outbound address, for allow-lists
|
|
@@ -103,7 +110,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
103
110
|
run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
|
|
104
111
|
ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
|
|
105
112
|
port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
|
|
106
|
-
create [--name n] [--label k=v]... [--vcpu 2] [--memory 4096] [--disk 4096]
|
|
113
|
+
create [--name n] [--label k=v]... [--env K=V]... [--vcpu 2] [--memory 4096] [--disk 4096]
|
|
107
114
|
[--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
|
|
108
115
|
[--timeout <seconds>] [--on-timeout pause|stop] [--trial|--paid] [--no-wait]
|
|
109
116
|
[--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
@@ -122,10 +129,15 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
122
129
|
cp <src> <dst> Copy files or directories; name a sandbox
|
|
123
130
|
path as <id>:/path, e.g. cp ./app sbx:/workspace/app
|
|
124
131
|
cat <id> <path> Print a file
|
|
125
|
-
files <id> [path] [--depth
|
|
132
|
+
files <id> [path] [--depth 1] [--glob '**/*.py']
|
|
126
133
|
stop <id> | pause <id> | wake <id> | restart <id>
|
|
134
|
+
rm <id> Delete it for good: stops it, deletes its disk and
|
|
135
|
+
paused memory, revokes its previews; snapshots stay
|
|
136
|
+
switch-image <id> <image> --keep-workspace Move it to another image, keeping /workspace (your home),
|
|
137
|
+
volumes, env and previews; processes restart, the rest of
|
|
138
|
+
its disk is lost (snapshot it first to keep everything)
|
|
127
139
|
extend <id> <seconds> More time before the lease ends
|
|
128
|
-
update <id> [--name n] [--label k=v]... [--idle-pause <seconds>] [--auto-wake on|off]
|
|
140
|
+
update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
|
|
129
141
|
[--persistent on|off] [--max-total-cost <usd>|none]
|
|
130
142
|
Change its settings; persistent keeps it running
|
|
131
143
|
snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
|
|
@@ -133,6 +145,9 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
133
145
|
[--endpoint https://...] [--account-id id] [--read-only]
|
|
134
146
|
Your bucket as a directory; the proxy signs, the sandbox never sees the key
|
|
135
147
|
mounts <id> | unmount <id> <path> Its bucket mounts; unmount one
|
|
148
|
+
tailscale up <id> --auth-key-secret NAME [--hostname h] [--tag tag:x]...
|
|
149
|
+
Join your tailnet (paid); the key is a secret set with --jobs
|
|
150
|
+
tailscale status <id> | tailscale down <id> Its tailnet address; take it off the tailnet
|
|
136
151
|
fork <id> [--count 3] [--name n] [--keep-snapshot] [--trial|--paid]
|
|
137
152
|
Copies of it as it is now, running; prints their ids
|
|
138
153
|
run-code <id> <file|-> [--lang python|javascript|typescript|r|java|bash|go] [--context c] [--out-dir .]
|
|
@@ -168,8 +183,8 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
168
183
|
|
|
169
184
|
Examples
|
|
170
185
|
id=$(runtime sandbox create)
|
|
171
|
-
runtime sandbox
|
|
172
|
-
runtime sandbox
|
|
186
|
+
runtime sandbox cp ./invoice.py $id:/workspace/invoice.py
|
|
187
|
+
runtime sandbox exec $id -- python3 /workspace/invoice.py
|
|
173
188
|
runtime sandbox stop $id
|
|
174
189
|
`;
|
|
175
190
|
/** A command's options. Reading one its parse() did not declare is a bug in
|
|
@@ -217,9 +232,13 @@ commandAfter) {
|
|
|
217
232
|
throw commandAfter && !meantOption(name, known)
|
|
218
233
|
? usage(`Unknown option ${arg.split("=")[0]}. If it belongs to the command, put the command after --: ${commandAfter}`)
|
|
219
234
|
: unknownOption(name, known);
|
|
220
|
-
const
|
|
221
|
-
|
|
235
|
+
const boolean = booleans.includes(name);
|
|
236
|
+
const value = inline ?? (boolean ? "true" : argv[++i]);
|
|
237
|
+
if (value === undefined ||
|
|
238
|
+
(!boolean && inline === undefined && (value === "--" || /^--?[^\d]/.test(value))))
|
|
222
239
|
throw usage(`--${name} needs a value.`);
|
|
240
|
+
if (boolean && value !== "true" && value !== "false")
|
|
241
|
+
throw usage(`--${name} takes true or false.`);
|
|
223
242
|
flags.set(name, [...(flags.get(name) ?? []), value]);
|
|
224
243
|
}
|
|
225
244
|
else
|
|
@@ -251,7 +270,10 @@ const integer = (args, name) => {
|
|
|
251
270
|
return undefined;
|
|
252
271
|
if (!/^\d+$/.test(value))
|
|
253
272
|
throw usage(`--${name} must be a whole number.`);
|
|
254
|
-
|
|
273
|
+
const parsed = Number(value);
|
|
274
|
+
if (!Number.isSafeInteger(parsed))
|
|
275
|
+
throw usage(`--${name} is too large to represent exactly.`);
|
|
276
|
+
return parsed;
|
|
255
277
|
};
|
|
256
278
|
/** A flag that takes one of a few words, checked before anything is sent. */
|
|
257
279
|
function oneOf(args, name, choices) {
|
|
@@ -385,6 +407,9 @@ const SANDBOX_VERBS = [
|
|
|
385
407
|
"ls",
|
|
386
408
|
"get",
|
|
387
409
|
"stop",
|
|
410
|
+
"rm",
|
|
411
|
+
"delete",
|
|
412
|
+
"switch-image",
|
|
388
413
|
"pause",
|
|
389
414
|
"wake",
|
|
390
415
|
"restart",
|
|
@@ -392,6 +417,7 @@ const SANDBOX_VERBS = [
|
|
|
392
417
|
"mount",
|
|
393
418
|
"mounts",
|
|
394
419
|
"unmount",
|
|
420
|
+
"tailscale",
|
|
395
421
|
"fork",
|
|
396
422
|
"extend",
|
|
397
423
|
"update",
|
|
@@ -484,6 +510,12 @@ function describe(info) {
|
|
|
484
510
|
["funding", info.funding],
|
|
485
511
|
["region", info.region],
|
|
486
512
|
["expires", info.expiresAt ?? "-"],
|
|
513
|
+
["at lease end", info.onLeaseEnd ?? "-"],
|
|
514
|
+
// Why a sandbox is no longer running (troubleshooting.md, "A sandbox
|
|
515
|
+
// stopped on its own"); `get` left both out until 30 September 2026.
|
|
516
|
+
...(info.pausedAt ? [["paused at", info.pausedAt]] : []),
|
|
517
|
+
...(info.endedAt ? [["ended", info.endedAt]] : []),
|
|
518
|
+
...(info.stopReason ? [["stop reason", info.stopReason]] : []),
|
|
487
519
|
["auto wake", info.autoWake === false ? "off" : "on"],
|
|
488
520
|
[
|
|
489
521
|
"idle pause",
|
|
@@ -523,6 +555,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
523
555
|
// `runtime sandbox create --help` is how an agent asks what a command takes:
|
|
524
556
|
// answer with the product's help, never by running the command.
|
|
525
557
|
const cut = remaining.indexOf("--");
|
|
558
|
+
const asked = (cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h") ||
|
|
559
|
+
(remaining.length === 1 && remaining[0] === "help");
|
|
560
|
+
// A command without a help of its own answers `help` with its lines of the
|
|
561
|
+
// top-level help, and never takes the word as its input.
|
|
562
|
+
if (first && first !== "help" && asked && !nested && !OWN_HELP.includes(first)) {
|
|
563
|
+
const lines = topicHelp(named(HELP), first);
|
|
564
|
+
if (lines) {
|
|
565
|
+
out.write(out.json ? JSON.stringify({ usage: lines }) : lines);
|
|
566
|
+
return 0;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
526
569
|
if (first &&
|
|
527
570
|
(cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h"))
|
|
528
571
|
return run(nested && command === "ssh"
|
|
@@ -542,8 +585,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
542
585
|
? JSON.stringify({ connected, usage: HELP })
|
|
543
586
|
: `Runtime Cloud ${VERSION}. ${start}
|
|
544
587
|
|
|
545
|
-
${me} sandbox run -- python3
|
|
546
|
-
|
|
588
|
+
${me} sandbox run -- python3 < invoice.py
|
|
589
|
+
Run a local invoice script, then stop the sandbox
|
|
547
590
|
${me} sandbox create A sandbox that stays; prints its id
|
|
548
591
|
${me} sandbox exec <id> -- <command> Run in it
|
|
549
592
|
${me} help Everything else
|
|
@@ -555,10 +598,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
555
598
|
return 0;
|
|
556
599
|
}
|
|
557
600
|
if (command === "run") {
|
|
558
|
-
const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandbox run -- python3
|
|
601
|
+
const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandbox run -- python3 < invoice.py`);
|
|
559
602
|
const commandLine = args.rest ?? args.positional;
|
|
560
603
|
if (!commandLine.length)
|
|
561
|
-
throw usage(`Give the command after --: ${me} sandbox run -- python3
|
|
604
|
+
throw usage(`Give the command after --: ${me} sandbox run -- python3 < invoice.py`);
|
|
562
605
|
const runtime = await client(env);
|
|
563
606
|
const timeout = integer(args, "timeout");
|
|
564
607
|
const { timeoutSeconds: _lease, ...input } = createInput(args);
|
|
@@ -655,6 +698,40 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
655
698
|
return 0;
|
|
656
699
|
}
|
|
657
700
|
if (command === "usage") {
|
|
701
|
+
if (rest[0] === "export") {
|
|
702
|
+
const args = parse(rest.slice(1), [], ["since", "until"]);
|
|
703
|
+
if (args.positional.length || args.rest)
|
|
704
|
+
throw usage(`Use ${me} usage export --since <UTC time> --until <UTC time>.`);
|
|
705
|
+
const since = need(flag(args, "since"), "--since <UTC time>, such as 2026-09-01T00:00:00Z");
|
|
706
|
+
const until = need(flag(args, "until"), "--until <UTC time>, such as 2026-10-01T00:00:00Z");
|
|
707
|
+
const rt = await client(env);
|
|
708
|
+
const cursors = new Set();
|
|
709
|
+
let cursor;
|
|
710
|
+
for (;;) {
|
|
711
|
+
const page = await rt.usageExport({
|
|
712
|
+
since,
|
|
713
|
+
until,
|
|
714
|
+
...(cursor ? { cursor } : {}),
|
|
715
|
+
limit: 500,
|
|
716
|
+
});
|
|
717
|
+
if (page.nextCursor !== null && cursors.has(page.nextCursor))
|
|
718
|
+
throw new RuntimeError({
|
|
719
|
+
code: "usage_export_incomplete",
|
|
720
|
+
status: 0,
|
|
721
|
+
message: "The usage export repeated a page cursor and could not finish.",
|
|
722
|
+
hint: "Run the export again; do not treat the partial output as complete.",
|
|
723
|
+
});
|
|
724
|
+
if (out.json)
|
|
725
|
+
out.write(JSON.stringify(page));
|
|
726
|
+
else if (page.csv)
|
|
727
|
+
out.write(page.csv);
|
|
728
|
+
await out.drain?.();
|
|
729
|
+
if (page.nextCursor === null)
|
|
730
|
+
return 0;
|
|
731
|
+
cursors.add(page.nextCursor);
|
|
732
|
+
cursor = page.nextCursor;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
658
735
|
const csv = rest.includes("--csv");
|
|
659
736
|
const unknown = rest.find((arg) => arg !== "--csv");
|
|
660
737
|
if (unknown)
|
|
@@ -671,7 +748,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
671
748
|
}
|
|
672
749
|
if (command === "limits") {
|
|
673
750
|
const l = await (await client(env)).limits.get();
|
|
674
|
-
|
|
751
|
+
// As `usage` shows money: $0.0014 of spend read $0.00 until 30 September 2026.
|
|
752
|
+
const usd = (micros) => dollars(micros);
|
|
675
753
|
const access = l.access === "read"
|
|
676
754
|
? "read only: reads every product, cannot start, change or spend"
|
|
677
755
|
: l.access === "full"
|
|
@@ -687,6 +765,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
687
765
|
],
|
|
688
766
|
["used, last 24 hours", usd(l.daily.usedMicros)],
|
|
689
767
|
...(l.daily.remainingMicros === null ? [] : [["left", usd(l.daily.remainingMicros)]]),
|
|
768
|
+
...(l.trial ? [["free trial", trialLeft(l.trial)]] : []),
|
|
690
769
|
]), l);
|
|
691
770
|
return 0;
|
|
692
771
|
}
|
|
@@ -703,7 +782,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
703
782
|
const page = await rt.events.list({
|
|
704
783
|
...(sandbox ? { resourceId: await sandboxIdOf(rt, sandbox) } : {}),
|
|
705
784
|
...(flag(args, "type") ? { type: flag(args, "type") } : {}),
|
|
706
|
-
...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
|
|
785
|
+
...(integer(args, "limit") !== undefined ? { limit: integer(args, "limit") } : {}),
|
|
707
786
|
});
|
|
708
787
|
print(page.data.length
|
|
709
788
|
? table(page.data.map((e) => {
|
|
@@ -748,7 +827,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
748
827
|
throw usage(`Unknown option --${name}.`);
|
|
749
828
|
const page = await (await client(env)).audit.list({
|
|
750
829
|
...(flag(args, "action") ? { action: flag(args, "action") } : {}),
|
|
751
|
-
...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
|
|
830
|
+
...(integer(args, "limit") !== undefined ? { limit: integer(args, "limit") } : {}),
|
|
752
831
|
...(flag(args, "before") ? { before: flag(args, "before") } : {}),
|
|
753
832
|
});
|
|
754
833
|
print(page.events.length
|
|
@@ -774,17 +853,26 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
774
853
|
const origins = connectionOrigins(env);
|
|
775
854
|
const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
|
|
776
855
|
return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
|
|
777
|
-
open: (input) =>
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
856
|
+
open: (input) => {
|
|
857
|
+
checkBrowserOrigins(origins);
|
|
858
|
+
return openWalletAccount(input, { authUrl: origins.auth });
|
|
859
|
+
},
|
|
860
|
+
claim: (code) => {
|
|
861
|
+
checkBrowserOrigins(origins);
|
|
862
|
+
return claimWalletAccount(code, { authUrl: origins.auth });
|
|
863
|
+
},
|
|
864
|
+
save: (key, orgId) => {
|
|
865
|
+
checkBrowserOrigins(origins);
|
|
866
|
+
return connectionStore(env).save({
|
|
867
|
+
version: 1,
|
|
868
|
+
apiOrigin: origins.api,
|
|
869
|
+
authOrigin: origins.auth,
|
|
870
|
+
key,
|
|
871
|
+
connectionId: "pasted",
|
|
872
|
+
orgId,
|
|
873
|
+
agentName: "Wallet agent",
|
|
874
|
+
});
|
|
875
|
+
},
|
|
788
876
|
});
|
|
789
877
|
}
|
|
790
878
|
if (command === "referrals") {
|
|
@@ -975,11 +1063,15 @@ export function usageCsv(u) {
|
|
|
975
1063
|
.join(","));
|
|
976
1064
|
return [head.join(","), ...rows].join("\n");
|
|
977
1065
|
}
|
|
1066
|
+
/** The free trial's time left, as `runtime usage` and `runtime limits` say it. */
|
|
1067
|
+
function trialLeft(trial) {
|
|
1068
|
+
const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
1069
|
+
return `${hours(trial.availableMs)} of ${hours(trial.totalMs)} hours left${trial.reservedMs > 0 ? `, ${hours(trial.reservedMs)} held by running sandboxes` : ""}`;
|
|
1070
|
+
}
|
|
978
1071
|
function usageSummary(u) {
|
|
979
1072
|
const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
|
|
980
1073
|
? BigInt(value)
|
|
981
1074
|
: 0n;
|
|
982
|
-
const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
983
1075
|
const takenBack = micros(u.takenBack);
|
|
984
1076
|
const rows = [
|
|
985
1077
|
["available", dollars(micros(u.available))],
|
|
@@ -996,10 +1088,7 @@ function usageSummary(u) {
|
|
|
996
1088
|
if (micros(u.expired) > 0n)
|
|
997
1089
|
rows.push(["expired or taken back", dollars(micros(u.expired))]);
|
|
998
1090
|
if (u.trial)
|
|
999
|
-
rows.push([
|
|
1000
|
-
"free trial",
|
|
1001
|
-
`${hours(u.trial.availableMs)} of ${hours(u.trial.totalMs)} hours left${u.trial.reservedMs > 0 ? `, ${hours(u.trial.reservedMs)} held by running sandboxes` : ""}`,
|
|
1002
|
-
]);
|
|
1091
|
+
rows.push(["free trial", trialLeft(u.trial)]);
|
|
1003
1092
|
if (u.outbound) {
|
|
1004
1093
|
const gib = (bytes) => `${(bytes / 1_073_741_824).toLocaleString("en-US", { maximumFractionDigits: 1 })} GiB`;
|
|
1005
1094
|
const left = Math.max(0, u.outbound.allowanceBytes - u.outbound.freeBytes);
|
|
@@ -1357,8 +1446,12 @@ async function sandbox(argv, env, out) {
|
|
|
1357
1446
|
...(args.flags.has("state")
|
|
1358
1447
|
? { state: args.flags.get("state") }
|
|
1359
1448
|
: {}),
|
|
1449
|
+
limit: 100,
|
|
1360
1450
|
});
|
|
1361
|
-
|
|
1451
|
+
// Every page, oldest first, so the newest are last on the screen. Until
|
|
1452
|
+
// 30 September 2026 this stopped at 1,000 and silently left out the
|
|
1453
|
+
// newest sandboxes of a busy account.
|
|
1454
|
+
const all = await page.toArray(Infinity);
|
|
1362
1455
|
// Stopped sandboxes are hidden by default; say so, or an agent that
|
|
1363
1456
|
// stopped everything reads "No sandboxes" as "never had any".
|
|
1364
1457
|
const hidden = out.json || args.flags.size
|
|
@@ -1392,6 +1485,23 @@ async function sandbox(argv, env, out) {
|
|
|
1392
1485
|
print(describe(sbx.info), sbx.info);
|
|
1393
1486
|
return 0;
|
|
1394
1487
|
}
|
|
1488
|
+
case "switch-image": {
|
|
1489
|
+
const args = parse(rest, ["keep-workspace"], []);
|
|
1490
|
+
const [id, image] = args.positional;
|
|
1491
|
+
const sbx = await use(need(id, `a sandbox, e.g. ${me} sandbox switch-image <id> app:v2 --keep-workspace`));
|
|
1492
|
+
if (!args.flags.has("keep-workspace"))
|
|
1493
|
+
throw usage(`A switch keeps only /workspace (your home), volumes, env and previews; processes restart and everything else on its disk is lost. Add --keep-workspace to say so, or snapshot it first (${me} sandbox snapshot ${sbx.id}) to keep everything.`);
|
|
1494
|
+
await sbx.switchImage(need(image, "an image, e.g. app:v2"), { keep: "workspace" });
|
|
1495
|
+
print(describe(sbx.info), sbx.info);
|
|
1496
|
+
return 0;
|
|
1497
|
+
}
|
|
1498
|
+
case "rm":
|
|
1499
|
+
case "delete": {
|
|
1500
|
+
const sbx = await use(need(rest[0], `a sandbox, e.g. ${me} sandbox rm <id>`));
|
|
1501
|
+
const gone = await sbx.delete();
|
|
1502
|
+
print(`${gone.id} deleted`, gone);
|
|
1503
|
+
return 0;
|
|
1504
|
+
}
|
|
1395
1505
|
case "stop":
|
|
1396
1506
|
case "pause":
|
|
1397
1507
|
case "wake":
|
|
@@ -1445,12 +1555,43 @@ async function sandbox(argv, env, out) {
|
|
|
1445
1555
|
print(`${gone.path} unmounted`, gone);
|
|
1446
1556
|
return 0;
|
|
1447
1557
|
}
|
|
1558
|
+
case "tailscale": {
|
|
1559
|
+
const action = rest[0];
|
|
1560
|
+
if (action === "up") {
|
|
1561
|
+
const args = parse(rest.slice(1), [], ["auth-key-secret", "hostname", "tag"]);
|
|
1562
|
+
const sbx = await use(args.positional[0]);
|
|
1563
|
+
const joined = await sbx.tailscale.up({
|
|
1564
|
+
authKeySecret: need(flag(args, "auth-key-secret"), "--auth-key-secret, the name of a secret set with --jobs that holds your Tailscale auth key"),
|
|
1565
|
+
...(flag(args, "hostname") ? { hostname: flag(args, "hostname") } : {}),
|
|
1566
|
+
...(args.flags.has("tag") ? { tags: args.flags.get("tag") } : {}),
|
|
1567
|
+
});
|
|
1568
|
+
print(`${joined.hostname} ${joined.addresses.join(" ")}${joined.proxy ? ` (reach the tailnet through ${joined.proxy})` : ""}`, joined);
|
|
1569
|
+
return 0;
|
|
1570
|
+
}
|
|
1571
|
+
if (action === "status") {
|
|
1572
|
+
const sbx = await use(rest[1]);
|
|
1573
|
+
const now = await sbx.tailscale.status();
|
|
1574
|
+
print(now.tailnet
|
|
1575
|
+
? `${now.tailnet.hostname} ${now.running ? now.addresses.join(" ") : (now.state ?? "not running")}`
|
|
1576
|
+
: "Not on a tailnet.", now);
|
|
1577
|
+
return 0;
|
|
1578
|
+
}
|
|
1579
|
+
if (action === "down") {
|
|
1580
|
+
const sbx = await use(rest[1]);
|
|
1581
|
+
const gone = await sbx.tailscale.down();
|
|
1582
|
+
print(gone.left ? "Left the tailnet." : "Not on a tailnet.", gone);
|
|
1583
|
+
return 0;
|
|
1584
|
+
}
|
|
1585
|
+
throw usage("runtime sandbox tailscale up|status|down <id>");
|
|
1586
|
+
}
|
|
1448
1587
|
case "snapshot": {
|
|
1449
1588
|
const args = parse(rest, [], ["name", "retention"]);
|
|
1450
1589
|
const sbx = await get(args.positional[0]);
|
|
1451
1590
|
const snapshot = await sbx.snapshot({
|
|
1452
1591
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1453
|
-
...(integer(args, "retention")
|
|
1592
|
+
...(integer(args, "retention") !== undefined
|
|
1593
|
+
? { retentionDays: integer(args, "retention") }
|
|
1594
|
+
: {}),
|
|
1454
1595
|
});
|
|
1455
1596
|
print(snapshot.id, snapshot);
|
|
1456
1597
|
return 0;
|
|
@@ -1472,7 +1613,16 @@ async function sandbox(argv, env, out) {
|
|
|
1472
1613
|
return 0;
|
|
1473
1614
|
}
|
|
1474
1615
|
case "update": {
|
|
1475
|
-
const args = parse(rest, [], [
|
|
1616
|
+
const args = parse(rest, [], [
|
|
1617
|
+
"name",
|
|
1618
|
+
"label",
|
|
1619
|
+
"env",
|
|
1620
|
+
"unset-env",
|
|
1621
|
+
"idle-pause",
|
|
1622
|
+
"auto-wake",
|
|
1623
|
+
"persistent",
|
|
1624
|
+
"max-total-cost",
|
|
1625
|
+
]);
|
|
1476
1626
|
const sbx = await get(args.positional[0]);
|
|
1477
1627
|
const onOff = (name) => {
|
|
1478
1628
|
const value = flag(args, name);
|
|
@@ -1485,9 +1635,14 @@ async function sandbox(argv, env, out) {
|
|
|
1485
1635
|
const cost = flag(args, "max-total-cost");
|
|
1486
1636
|
if (cost !== undefined && cost !== "none" && !/^\d+(\.\d{1,6})?$/.test(cost))
|
|
1487
1637
|
throw usage("--max-total-cost takes dollars, such as 25 or 2.50, or none.");
|
|
1638
|
+
const env = {
|
|
1639
|
+
...(args.flags.has("env") ? pairs(args, "env") : {}),
|
|
1640
|
+
...Object.fromEntries((args.flags.get("unset-env") ?? []).map((name) => [name, null])),
|
|
1641
|
+
};
|
|
1488
1642
|
const settings = {
|
|
1489
1643
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1490
1644
|
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
1645
|
+
...(Object.keys(env).length ? { env } : {}),
|
|
1491
1646
|
...(integer(args, "idle-pause") !== undefined
|
|
1492
1647
|
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
1493
1648
|
: {}),
|
|
@@ -1496,11 +1651,11 @@ async function sandbox(argv, env, out) {
|
|
|
1496
1651
|
...(cost === undefined
|
|
1497
1652
|
? {}
|
|
1498
1653
|
: {
|
|
1499
|
-
maxTotalCostMicros: cost === "none" ? null :
|
|
1654
|
+
maxTotalCostMicros: cost === "none" ? null : usdMicros(args, "max-total-cost"),
|
|
1500
1655
|
}),
|
|
1501
1656
|
};
|
|
1502
1657
|
if (!Object.keys(settings).length)
|
|
1503
|
-
throw usage("Say what to change: --name, --label, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
|
|
1658
|
+
throw usage("Say what to change: --name, --label, --env, --unset-env, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
|
|
1504
1659
|
await sbx.update(settings);
|
|
1505
1660
|
print(describe(sbx.info), sbx.info);
|
|
1506
1661
|
return 0;
|
|
@@ -1576,8 +1731,21 @@ async function sandbox(argv, env, out) {
|
|
|
1576
1731
|
const args = parse(rest, [], ["signal"]);
|
|
1577
1732
|
const sbx = await use(args.positional[0]);
|
|
1578
1733
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
1734
|
+
// A process that has ended has nothing to signal; "Signalled" said it
|
|
1735
|
+
// had been until 30 September 2026.
|
|
1736
|
+
if (proc.info.state === "exited" ||
|
|
1737
|
+
proc.info.state === "killed" ||
|
|
1738
|
+
proc.info.state === "timed_out") {
|
|
1739
|
+
print(`${proc.id} had already ${proc.info.state === "exited" ? "exited" : "ended"}${proc.info.exitCode == null ? "" : ` (exit code ${proc.info.exitCode})`}; nothing to signal.`, {
|
|
1740
|
+
processId: proc.id,
|
|
1741
|
+
state: proc.info.state,
|
|
1742
|
+
exitCode: proc.info.exitCode ?? null,
|
|
1743
|
+
signalled: false,
|
|
1744
|
+
});
|
|
1745
|
+
return 0;
|
|
1746
|
+
}
|
|
1579
1747
|
await proc.kill((flag(args, "signal") ?? "SIGTERM"));
|
|
1580
|
-
print(`Signalled ${proc.id}.`, { processId: proc.id });
|
|
1748
|
+
print(`Signalled ${proc.id}.`, { processId: proc.id, signalled: true });
|
|
1581
1749
|
return 0;
|
|
1582
1750
|
}
|
|
1583
1751
|
case "shell": {
|
|
@@ -1596,7 +1764,7 @@ async function sandbox(argv, env, out) {
|
|
|
1596
1764
|
const args = parse(rest, [], ["depth", "glob"]);
|
|
1597
1765
|
const sbx = await use(args.positional[0]);
|
|
1598
1766
|
const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
|
|
1599
|
-
...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
|
|
1767
|
+
...(integer(args, "depth") !== undefined ? { depth: integer(args, "depth") } : {}),
|
|
1600
1768
|
...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
|
|
1601
1769
|
});
|
|
1602
1770
|
print(entries
|
|
@@ -1639,7 +1807,7 @@ async function sandbox(argv, env, out) {
|
|
|
1639
1807
|
const sbx = await use(args.positional[0]);
|
|
1640
1808
|
const preview = await sbx.previews.create(port, {
|
|
1641
1809
|
...(has(args, "public") ? { visibility: "public" } : {}),
|
|
1642
|
-
...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
|
|
1810
|
+
...(integer(args, "ttl") !== undefined ? { ttlSeconds: integer(args, "ttl") } : {}),
|
|
1643
1811
|
});
|
|
1644
1812
|
// The link and how to use it, nothing more: the API's `hint` (which
|
|
1645
1813
|
// carries a referral line) stays in --json, not in the command's words.
|
|
@@ -1971,7 +2139,7 @@ async function runCode(sbx, argv, out) {
|
|
|
1971
2139
|
const execution = await sbx.interpreter.run(code, {
|
|
1972
2140
|
language,
|
|
1973
2141
|
...(flag(args, "context") ? { context: flag(args, "context") } : {}),
|
|
1974
|
-
...(timeout ? { timeoutMs: timeout * 1000 } : {}),
|
|
2142
|
+
...(timeout !== undefined ? { timeoutMs: timeout * 1000 } : {}),
|
|
1975
2143
|
...(out.json
|
|
1976
2144
|
? {}
|
|
1977
2145
|
: {
|
|
@@ -2020,8 +2188,8 @@ async function desktop(sbx, argv, out) {
|
|
|
2020
2188
|
case "start": {
|
|
2021
2189
|
const args = parse(rest, [], ["width", "height"]);
|
|
2022
2190
|
const started = await d.start({
|
|
2023
|
-
...(integer(args, "width") ? { width: integer(args, "width") } : {}),
|
|
2024
|
-
...(integer(args, "height") ? { height: integer(args, "height") } : {}),
|
|
2191
|
+
...(integer(args, "width") !== undefined ? { width: integer(args, "width") } : {}),
|
|
2192
|
+
...(integer(args, "height") !== undefined ? { height: integer(args, "height") } : {}),
|
|
2025
2193
|
});
|
|
2026
2194
|
print(`Desktop ${started.size} on ${started.display}. Watch it: ${started.streamUrl}`, started);
|
|
2027
2195
|
return 0;
|
|
@@ -2256,7 +2424,7 @@ async function snapshot(argv, env, out) {
|
|
|
2256
2424
|
? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
|
|
2257
2425
|
: {}),
|
|
2258
2426
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
2259
|
-
})).toArray(
|
|
2427
|
+
})).toArray(Infinity);
|
|
2260
2428
|
print(all.length
|
|
2261
2429
|
? table([
|
|
2262
2430
|
["ID", "NAME", "STATUS", "FROM", "EXPIRES"],
|
|
@@ -2360,9 +2528,13 @@ read = readSecret) {
|
|
|
2360
2528
|
if (sources.filter((s) => s !== undefined).length !== 1)
|
|
2361
2529
|
throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
|
|
2362
2530
|
const build = {
|
|
2363
|
-
...(integer(args, "max-image-mib")
|
|
2364
|
-
|
|
2365
|
-
|
|
2531
|
+
...(integer(args, "max-image-mib") !== undefined
|
|
2532
|
+
? { maxImageMiB: integer(args, "max-image-mib") }
|
|
2533
|
+
: {}),
|
|
2534
|
+
...(integer(args, "disk-mib") !== undefined ? { diskMiB: integer(args, "disk-mib") } : {}),
|
|
2535
|
+
...(integer(args, "timeout") !== undefined
|
|
2536
|
+
? { timeoutSeconds: integer(args, "timeout") }
|
|
2537
|
+
: {}),
|
|
2366
2538
|
};
|
|
2367
2539
|
// -t app, -t app:v2 -t app:latest: one name, its tags.
|
|
2368
2540
|
let name = flag(args, "name");
|
|
@@ -2379,9 +2551,11 @@ read = readSecret) {
|
|
|
2379
2551
|
}
|
|
2380
2552
|
const start = {
|
|
2381
2553
|
...(flag(args, "start") ? { command: flag(args, "start") } : {}),
|
|
2382
|
-
...(integer(args, "ready-port")
|
|
2554
|
+
...(integer(args, "ready-port") !== undefined
|
|
2555
|
+
? { readyPort: integer(args, "ready-port") }
|
|
2556
|
+
: {}),
|
|
2383
2557
|
...(flag(args, "ready-command") ? { readyCommand: flag(args, "ready-command") } : {}),
|
|
2384
|
-
...(integer(args, "ready-timeout")
|
|
2558
|
+
...(integer(args, "ready-timeout") !== undefined
|
|
2385
2559
|
? { readyTimeoutSeconds: integer(args, "ready-timeout") }
|
|
2386
2560
|
: {}),
|
|
2387
2561
|
};
|
|
@@ -2426,7 +2600,7 @@ read = readSecret) {
|
|
|
2426
2600
|
case "list":
|
|
2427
2601
|
case "versions": {
|
|
2428
2602
|
const name = verb === "versions" ? need(rest[0], "a name") : undefined;
|
|
2429
|
-
const all = await (await api.list(name ? { name } : {})).toArray(
|
|
2603
|
+
const all = await (await api.list(name ? { name } : {})).toArray(Infinity);
|
|
2430
2604
|
print(all.length
|
|
2431
2605
|
? table([
|
|
2432
2606
|
["ID", "NAME", "VERSION", "TAGS", "STATE", "MIB", "CREATED"],
|
|
@@ -2447,7 +2621,7 @@ read = readSecret) {
|
|
|
2447
2621
|
}
|
|
2448
2622
|
case "get": {
|
|
2449
2623
|
const found = await api.resolve(need(rest[0], "an image"));
|
|
2450
|
-
print(
|
|
2624
|
+
print(describeRecord(found, table), found);
|
|
2451
2625
|
return 0;
|
|
2452
2626
|
}
|
|
2453
2627
|
case "logs": {
|
|
@@ -2645,23 +2819,29 @@ async function jobCommand(argv, env, out) {
|
|
|
2645
2819
|
? { cron, ...(flag(args, "timezone") ? { timezone: flag(args, "timezone") } : {}) }
|
|
2646
2820
|
: { at: atTime(at) },
|
|
2647
2821
|
command: cwd ? { argv, cwd } : argv,
|
|
2648
|
-
...(integer(args, "timeout")
|
|
2822
|
+
...(integer(args, "timeout") !== undefined
|
|
2823
|
+
? { timeoutSeconds: integer(args, "timeout") }
|
|
2824
|
+
: {}),
|
|
2649
2825
|
compute: {
|
|
2650
|
-
...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
|
|
2651
|
-
...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
|
|
2652
|
-
...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
|
|
2826
|
+
...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
|
|
2827
|
+
...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
|
|
2828
|
+
...(integer(args, "disk") !== undefined ? { diskMiB: integer(args, "disk") } : {}),
|
|
2653
2829
|
...(flag(args, "cpu")
|
|
2654
2830
|
? { cpuMode: oneOf(args, "cpu", ["shared", "reserved"]) }
|
|
2655
2831
|
: {}),
|
|
2656
|
-
...(integer(args, "cpu-floor")
|
|
2832
|
+
...(integer(args, "cpu-floor") !== undefined
|
|
2833
|
+
? { cpuFloorMillis: integer(args, "cpu-floor") }
|
|
2834
|
+
: {}),
|
|
2657
2835
|
...(flag(args, "region") ? { region: flag(args, "region") } : {}),
|
|
2658
|
-
...(usdMicros(args, "max-cost")
|
|
2836
|
+
...(usdMicros(args, "max-cost") !== undefined
|
|
2837
|
+
? { maxCostMicros: usdMicros(args, "max-cost") }
|
|
2838
|
+
: {}),
|
|
2659
2839
|
},
|
|
2660
2840
|
...(attempts !== undefined
|
|
2661
2841
|
? { retry: { maxAttempts: attempts, backoffSeconds: backoff ?? 30 } }
|
|
2662
2842
|
: {}),
|
|
2663
2843
|
...(secrets ? { secrets } : {}),
|
|
2664
|
-
...(usdMicros(args, "max-total-cost")
|
|
2844
|
+
...(usdMicros(args, "max-total-cost") !== undefined
|
|
2665
2845
|
? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
|
|
2666
2846
|
: {}),
|
|
2667
2847
|
});
|
|
@@ -2672,7 +2852,7 @@ async function jobCommand(argv, env, out) {
|
|
|
2672
2852
|
}
|
|
2673
2853
|
case "ls":
|
|
2674
2854
|
case "list": {
|
|
2675
|
-
const all = await (await api.list({ limit: 100 })).toArray(
|
|
2855
|
+
const all = await (await api.list({ limit: 100 })).toArray(Infinity);
|
|
2676
2856
|
print(all.length
|
|
2677
2857
|
? table([
|
|
2678
2858
|
["ID", "NAME", "STATE", "SCHEDULE", "NEXT RUN", "BLOCKED"],
|
|
@@ -2681,7 +2861,8 @@ async function jobCommand(argv, env, out) {
|
|
|
2681
2861
|
j.name,
|
|
2682
2862
|
j.state,
|
|
2683
2863
|
scheduleText(j),
|
|
2684
|
-
|
|
2864
|
+
// A canceled or finished job keeps its last nextRunAt, but runs no more.
|
|
2865
|
+
j.state === "active" || j.state === "paused" ? when(j.nextRunAt) : "-",
|
|
2685
2866
|
j.blockedReason ?? "-",
|
|
2686
2867
|
]),
|
|
2687
2868
|
])
|
|
@@ -2695,7 +2876,7 @@ async function jobCommand(argv, env, out) {
|
|
|
2695
2876
|
}
|
|
2696
2877
|
case "runs": {
|
|
2697
2878
|
const jobId = need(rest[0], "a job id");
|
|
2698
|
-
const all = await (await api.runs(jobId, { limit: 100 })).toArray(
|
|
2879
|
+
const all = await (await api.runs(jobId, { limit: 100 })).toArray(Infinity);
|
|
2699
2880
|
print(all.length
|
|
2700
2881
|
? table([
|
|
2701
2882
|
["RUN", "ATTEMPT", "STATE", "EXIT", "SCHEDULED", "STARTED", "ENDED", "REASON"],
|
|
@@ -2724,7 +2905,7 @@ async function jobCommand(argv, env, out) {
|
|
|
2724
2905
|
if (!(error instanceof NotFoundError))
|
|
2725
2906
|
throw error;
|
|
2726
2907
|
// A job's id: its latest run.
|
|
2727
|
-
const runs = await (await api.runs(id, { limit: 100 })).toArray(
|
|
2908
|
+
const runs = await (await api.runs(id, { limit: 100 })).toArray(Infinity);
|
|
2728
2909
|
const latest = runs.at(-1);
|
|
2729
2910
|
if (!latest) {
|
|
2730
2911
|
const job = await api.get(id);
|
|
@@ -2789,9 +2970,13 @@ async function jobCommand(argv, env, out) {
|
|
|
2789
2970
|
}
|
|
2790
2971
|
const VOLUME_HELP = `runtime volume <command>
|
|
2791
2972
|
|
|
2792
|
-
create --size-mib <N> [--name n]
|
|
2973
|
+
create --size-mib <N> [--name n] [--shared] A persistent disk; prints its id
|
|
2793
2974
|
ls Your volumes
|
|
2794
2975
|
get <id> One volume, its backups and where it is attached
|
|
2976
|
+
resize <id> --size-mib <N> Grow a detached volume on its current host
|
|
2977
|
+
attach <id> <sandboxId> --path /data Mount a shared disk in a running sandbox
|
|
2978
|
+
detach <id> <attachmentId> Flush and unmount its shared attachment
|
|
2979
|
+
attachment <id> <attachmentId> Read the attachment's state and error
|
|
2795
2980
|
rm <id> Delete it and everything on it (its backups stay)
|
|
2796
2981
|
backup <id> [--retention-days N] [--no-wait]
|
|
2797
2982
|
Back it up now, off its host; prints the backup id
|
|
@@ -2802,6 +2987,7 @@ const VOLUME_HELP = `runtime volume <command>
|
|
|
2802
2987
|
Daily backups on or off, and how long each is kept
|
|
2803
2988
|
Attach one: runtime sandbox create --volume <id>:/data
|
|
2804
2989
|
Volumes are backed up off their host daily unless you turn it off.
|
|
2990
|
+
Shared commands work where shared disks are enabled.
|
|
2805
2991
|
`;
|
|
2806
2992
|
async function volume(argv, env, out) {
|
|
2807
2993
|
const [verb, ...rest] = argv;
|
|
@@ -2813,17 +2999,18 @@ async function volume(argv, env, out) {
|
|
|
2813
2999
|
const api = (await client(env)).volumes;
|
|
2814
3000
|
switch (verb) {
|
|
2815
3001
|
case "create": {
|
|
2816
|
-
const args = parse(rest, [], ["size-mib", "name"]);
|
|
3002
|
+
const args = parse(rest, ["shared"], ["size-mib", "name"]);
|
|
2817
3003
|
const created = await api.create({
|
|
2818
3004
|
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
2819
3005
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
3006
|
+
...(has(args, "shared") ? { shared: true } : {}),
|
|
2820
3007
|
});
|
|
2821
3008
|
print(created.id, created);
|
|
2822
3009
|
return 0;
|
|
2823
3010
|
}
|
|
2824
3011
|
case "ls":
|
|
2825
3012
|
case "list": {
|
|
2826
|
-
const all = await (await api.list()).toArray(
|
|
3013
|
+
const all = await (await api.list()).toArray(Infinity);
|
|
2827
3014
|
print(all.length
|
|
2828
3015
|
? table([
|
|
2829
3016
|
["ID", "NAME", "STATE", "MIB", "BACKED UP", "ATTACHED"],
|
|
@@ -2841,9 +3028,43 @@ async function volume(argv, env, out) {
|
|
|
2841
3028
|
}
|
|
2842
3029
|
case "get": {
|
|
2843
3030
|
const found = await api.get(need(rest[0], "a volume id"));
|
|
2844
|
-
print(
|
|
3031
|
+
print(describeRecord(found, table), found);
|
|
2845
3032
|
return 0;
|
|
2846
3033
|
}
|
|
3034
|
+
case "resize": {
|
|
3035
|
+
const args = parse(rest, [], ["size-mib"]);
|
|
3036
|
+
const id = need(args.positional[0], "a volume id");
|
|
3037
|
+
if (args.positional.length !== 1 || args.rest)
|
|
3038
|
+
throw usage(`Use ${me} volume resize <id> --size-mib <N>.`);
|
|
3039
|
+
const sizeMiB = integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>"));
|
|
3040
|
+
const resized = await api.resize(id, { sizeMiB });
|
|
3041
|
+
print(describeRecord(resized, table), resized);
|
|
3042
|
+
return resized.resize?.state === "failed" ? 1 : 0;
|
|
3043
|
+
}
|
|
3044
|
+
case "attach": {
|
|
3045
|
+
const args = parse(rest, [], ["path"]);
|
|
3046
|
+
const id = need(args.positional[0], "a volume id");
|
|
3047
|
+
const sandboxId = need(args.positional[1], "a sandbox id");
|
|
3048
|
+
const path = need(flag(args, "path"), "--path /data");
|
|
3049
|
+
if (args.positional.length !== 2 || args.rest)
|
|
3050
|
+
throw usage(`Use ${me} volume attach <id> <sandboxId> --path /data.`);
|
|
3051
|
+
const attachment = await api.attach(id, { sandboxId, path });
|
|
3052
|
+
print(describeRecord(attachment, table), attachment);
|
|
3053
|
+
return attachment.error ? 1 : 0;
|
|
3054
|
+
}
|
|
3055
|
+
case "detach":
|
|
3056
|
+
case "attachment": {
|
|
3057
|
+
const args = parse(rest, [], []);
|
|
3058
|
+
const id = need(args.positional[0], "a volume id");
|
|
3059
|
+
const attachmentId = need(args.positional[1], "an attachment id");
|
|
3060
|
+
if (args.positional.length !== 2 || args.rest)
|
|
3061
|
+
throw usage(`Use ${me} volume ${verb} <id> <attachmentId>.`);
|
|
3062
|
+
const attachment = verb === "detach"
|
|
3063
|
+
? await api.detach(id, attachmentId)
|
|
3064
|
+
: await api.getAttachment(id, attachmentId);
|
|
3065
|
+
print(describeRecord(attachment, table), attachment);
|
|
3066
|
+
return verb === "detach" && attachment.error ? 1 : 0;
|
|
3067
|
+
}
|
|
2847
3068
|
case "rm":
|
|
2848
3069
|
case "delete": {
|
|
2849
3070
|
const found = await api.delete(need(rest[0], "a volume id"));
|
|
@@ -2862,7 +3083,7 @@ async function volume(argv, env, out) {
|
|
|
2862
3083
|
}
|
|
2863
3084
|
case "backups": {
|
|
2864
3085
|
const volumeId = rest[0];
|
|
2865
|
-
const all = await (await api.backups(volumeId ? { volumeId } : {})).toArray(
|
|
3086
|
+
const all = await (await api.backups(volumeId ? { volumeId } : {})).toArray(Infinity);
|
|
2866
3087
|
print(all.length
|
|
2867
3088
|
? table([
|
|
2868
3089
|
["ID", "VOLUME", "STATE", "TRIGGER", "MIB", "STORED", "TAKEN", "EXPIRES"],
|
|
@@ -2915,6 +3136,10 @@ async function volume(argv, env, out) {
|
|
|
2915
3136
|
"create",
|
|
2916
3137
|
"ls",
|
|
2917
3138
|
"get",
|
|
3139
|
+
"resize",
|
|
3140
|
+
"attach",
|
|
3141
|
+
"detach",
|
|
3142
|
+
"attachment",
|
|
2918
3143
|
"rm",
|
|
2919
3144
|
"backup",
|
|
2920
3145
|
"backups",
|
|
@@ -2966,6 +3191,7 @@ const CREATE_VALUES = [
|
|
|
2966
3191
|
"image",
|
|
2967
3192
|
"snapshot",
|
|
2968
3193
|
"volume",
|
|
3194
|
+
"env",
|
|
2969
3195
|
];
|
|
2970
3196
|
/** Dollars from the command line as integer microdollars: 25, 2.50 or 0.000001. */
|
|
2971
3197
|
function usdMicros(args, name) {
|
|
@@ -2974,7 +3200,13 @@ function usdMicros(args, name) {
|
|
|
2974
3200
|
return undefined;
|
|
2975
3201
|
if (!/^\d+(\.\d{1,6})?$/.test(value))
|
|
2976
3202
|
throw usage(`--${name} takes dollars, such as 25 or 2.50.`);
|
|
2977
|
-
|
|
3203
|
+
// ARCHITECTURE.md section 10: integer microdollars throughout, with no
|
|
3204
|
+
// floating-point money path or rounding drift.
|
|
3205
|
+
const [whole, fraction = ""] = value.split(".");
|
|
3206
|
+
const micros = BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
|
|
3207
|
+
if (micros > BigInt(Number.MAX_SAFE_INTEGER))
|
|
3208
|
+
throw usage(`--${name} is too large to represent exactly in microdollars.`);
|
|
3209
|
+
return Number(micros);
|
|
2978
3210
|
}
|
|
2979
3211
|
/** A create body from the command line's flags. */
|
|
2980
3212
|
function createInput(args) {
|
|
@@ -2993,17 +3225,24 @@ function createInput(args) {
|
|
|
2993
3225
|
: {}),
|
|
2994
3226
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
2995
3227
|
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
2996
|
-
...(
|
|
2997
|
-
...(integer(args, "
|
|
2998
|
-
...(integer(args, "
|
|
3228
|
+
...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
|
|
3229
|
+
...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
|
|
3230
|
+
...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
|
|
3231
|
+
...(integer(args, "disk") !== undefined ? { diskMiB: integer(args, "disk") } : {}),
|
|
2999
3232
|
...(flag(args, "cpu") ? { cpu: oneOf(args, "cpu", ["shared", "reserved"]) } : {}),
|
|
3000
|
-
...(integer(args, "cpu-floor")
|
|
3233
|
+
...(integer(args, "cpu-floor") !== undefined
|
|
3234
|
+
? { cpuFloorMillis: integer(args, "cpu-floor") }
|
|
3235
|
+
: {}),
|
|
3001
3236
|
...(flag(args, "region") ? { region: flag(args, "region") } : {}),
|
|
3002
|
-
...(usdMicros(args, "max-cost")
|
|
3003
|
-
|
|
3237
|
+
...(usdMicros(args, "max-cost") !== undefined
|
|
3238
|
+
? { maxCostMicros: usdMicros(args, "max-cost") }
|
|
3239
|
+
: {}),
|
|
3240
|
+
...(usdMicros(args, "max-total-cost") !== undefined
|
|
3004
3241
|
? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
|
|
3005
3242
|
: {}),
|
|
3006
|
-
...(integer(args, "timeout")
|
|
3243
|
+
...(integer(args, "timeout") !== undefined
|
|
3244
|
+
? { timeoutSeconds: integer(args, "timeout") }
|
|
3245
|
+
: {}),
|
|
3007
3246
|
...(flag(args, "on-timeout")
|
|
3008
3247
|
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
|
|
3009
3248
|
: {}),
|
|
@@ -3033,7 +3272,7 @@ ownInterrupt = true) {
|
|
|
3033
3272
|
...(stdin ? { stdin } : {}),
|
|
3034
3273
|
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
3035
3274
|
...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
|
|
3036
|
-
...(timeout ? { timeoutMs: timeout * 1000 } : {}),
|
|
3275
|
+
...(timeout !== undefined ? { timeoutMs: timeout * 1000 } : {}),
|
|
3037
3276
|
};
|
|
3038
3277
|
// Ctrl-C stops the command in the sandbox too, and says so; without this
|
|
3039
3278
|
// the CLI exited and the command ran on unseen.
|
|
@@ -3071,7 +3310,11 @@ ownInterrupt = true) {
|
|
|
3071
3310
|
const lost = extras.lostOutput(result, sbx.id);
|
|
3072
3311
|
if (lost && !out.json)
|
|
3073
3312
|
out.error(lost);
|
|
3074
|
-
|
|
3313
|
+
/* A command a signal ended comes back as minus the signal (-9 after the
|
|
3314
|
+
out-of-memory killer's SIGKILL); a shell says 128 + the signal. Clamped
|
|
3315
|
+
to 0 until 30 September 2026, a killed command exited 0. */
|
|
3316
|
+
const exit = result.exitCode ?? 1;
|
|
3317
|
+
const code = result.timedOut ? 124 : Math.min(255, exit < 0 ? 128 - exit : exit);
|
|
3075
3318
|
return lost && code === 0 ? 1 : code;
|
|
3076
3319
|
}
|
|
3077
3320
|
async function shell(sbx, command) {
|
|
@@ -3102,6 +3345,10 @@ function defaultOut(argv) {
|
|
|
3102
3345
|
return {
|
|
3103
3346
|
json,
|
|
3104
3347
|
write: (text) => process.stdout.write(text.endsWith("\n") ? text : `${text}\n`),
|
|
3348
|
+
drain: async () => {
|
|
3349
|
+
if (process.stdout.writableNeedDrain)
|
|
3350
|
+
await once(process.stdout, "drain");
|
|
3351
|
+
},
|
|
3105
3352
|
error: (text) => process.stderr.write(`${text}\n`),
|
|
3106
3353
|
};
|
|
3107
3354
|
}
|
|
@@ -3119,7 +3366,9 @@ export function describeError(error, json) {
|
|
|
3119
3366
|
details: error.details,
|
|
3120
3367
|
},
|
|
3121
3368
|
});
|
|
3122
|
-
|
|
3369
|
+
// In the command line's words: the option, not the request's field.
|
|
3370
|
+
const { message, hint } = cliRefusal(error);
|
|
3371
|
+
return `Error [${error.code}]: ${message}${hint ? `\nHint: ${hint}` : ""}${error.requestId
|
|
3123
3372
|
? // A refusal of what was sent is the caller's to fix; only a failure
|
|
3124
3373
|
// of ours is worth a report.
|
|
3125
3374
|
error.status >= 400 && error.status < 500
|
|
@@ -3157,21 +3406,38 @@ function spark(values) {
|
|
|
3157
3406
|
}
|
|
3158
3407
|
const mib = (bytes) => `${Math.round(bytes / 1_048_576).toLocaleString("en-US")} MiB`;
|
|
3159
3408
|
function metricsText(m) {
|
|
3409
|
+
const waiting = (value) => value == null ? "not measured" : `${value}%`;
|
|
3410
|
+
const waitingHistory = (field) => m.points.some((point) => point[field] != null)
|
|
3411
|
+
? spark(m.points.map((point) => point[field] ?? null))
|
|
3412
|
+
: "not measured";
|
|
3160
3413
|
const cpu = m.points.map((p) => p.cpuPercent);
|
|
3161
3414
|
const memory = m.points.map((p) => p.memoryBytes);
|
|
3162
3415
|
const peak = Math.max(0, ...m.points.map((p) => p.cpuPeakPercent ?? p.cpuPercent ?? 0));
|
|
3163
|
-
|
|
3416
|
+
// `latest` is the newest reading of the last 15 minutes whatever the state,
|
|
3417
|
+
// so a sandbox paused a minute ago still has one from before it slept. Only a
|
|
3418
|
+
// running sandbox has a now.
|
|
3419
|
+
const latest = m.state === "running" ? m.latest : null;
|
|
3420
|
+
const still = m.state === "running" ? null : `none: it is ${m.state}`;
|
|
3421
|
+
const cpuNow = newestCpu(m);
|
|
3164
3422
|
return [
|
|
3165
3423
|
table([
|
|
3166
3424
|
["state", m.state],
|
|
3167
3425
|
[
|
|
3168
3426
|
"cpu now",
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3427
|
+
still ??
|
|
3428
|
+
(cpuNow
|
|
3429
|
+
? `${cpuNow.percent}% of ${m.vcpu} vCPU${cpuNow.cores == null ? "" : ` (${cpuNow.cores} cores)`}${cpuNow.ago ? `, ${cpuNow.ago}` : ""}`
|
|
3430
|
+
: "no reading in this range"),
|
|
3431
|
+
],
|
|
3432
|
+
[
|
|
3433
|
+
"memory now",
|
|
3434
|
+
still ?? (latest ? `${mib(latest.memoryBytes)} of ${mib(m.memoryLimitBytes)}` : "-"),
|
|
3172
3435
|
],
|
|
3173
|
-
["
|
|
3436
|
+
["cpu wait now", still ?? waiting(latest?.cpuWaitPercent)],
|
|
3437
|
+
["memory stalls now", still ?? waiting(latest?.memoryStallPercent)],
|
|
3174
3438
|
[`cpu, ${m.range}`, m.points.length ? `${spark(cpu)} peak ${peak}%` : "no readings"],
|
|
3439
|
+
[`cpu wait, ${m.range}`, waitingHistory("cpuWaitPercent")],
|
|
3440
|
+
[`memory stalls, ${m.range}`, waitingHistory("memoryStallPercent")],
|
|
3175
3441
|
[
|
|
3176
3442
|
`memory, ${m.range}`,
|
|
3177
3443
|
m.points.length
|
|
@@ -3266,7 +3532,7 @@ async function webhooksCommand(argv, env, out) {
|
|
|
3266
3532
|
}
|
|
3267
3533
|
case "deliveries": {
|
|
3268
3534
|
const page = await hooks.deliveries(id(), {
|
|
3269
|
-
...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
|
|
3535
|
+
...(integer(args, "limit") !== undefined ? { limit: integer(args, "limit") } : {}),
|
|
3270
3536
|
});
|
|
3271
3537
|
print(page.data.length
|
|
3272
3538
|
? table(page.data.map((d) => [
|