withruntime 0.11.2 → 0.12.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 +2 -2
- package/COMPATIBILITY.md +74 -9
- package/DAYTONA.md +33 -5
- package/E2B.md +85 -24
- package/README.md +9 -8
- package/README.src.md +8 -7
- package/VERCEL.md +43 -5
- package/compatibility-lock.json +58 -49
- package/dist/billing-cli.d.ts +1 -1
- package/dist/billing-cli.js +77 -1
- package/dist/blaxel/errors.d.ts +14 -0
- package/dist/blaxel/errors.js +29 -0
- package/dist/blaxel/index.d.ts +2 -2
- package/dist/blaxel/index.js +4 -4
- package/dist/blaxel/interpreter.d.ts +2 -5
- package/dist/blaxel/sandbox.d.ts +14 -5
- package/dist/blaxel/sandbox.js +63 -7
- package/dist/blaxel/snapshot.d.ts +4 -0
- package/dist/blaxel/snapshot.js +4 -0
- package/dist/cli/logs.d.ts +21 -0
- package/dist/cli/logs.js +188 -0
- package/dist/cli/logs.test.d.ts +1 -0
- package/dist/cli/logs.test.js +272 -0
- package/dist/cli-extras.d.ts +3 -3
- package/dist/cli-extras.js +17 -12
- package/dist/cli-words.d.ts +10 -1
- package/dist/cli-words.js +18 -0
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +405 -204
- package/dist/client.d.ts +2 -4
- package/dist/client.js +27 -11
- package/dist/cloudflare/errors.d.ts +143 -0
- package/dist/cloudflare/errors.js +167 -0
- package/dist/cloudflare/index.d.ts +175 -6
- package/dist/cloudflare/index.js +416 -26
- package/dist/cloudflare/interpreter.d.ts +119 -0
- package/dist/cloudflare/interpreter.js +223 -0
- package/dist/cloudflare/streams.d.ts +38 -0
- package/dist/cloudflare/streams.js +145 -0
- package/dist/cloudflare/watch.d.ts +9 -0
- package/dist/cloudflare/watch.js +24 -0
- package/dist/codesandbox/index.d.ts +216 -28
- package/dist/codesandbox/index.js +478 -78
- package/dist/compat/core.d.ts +18 -0
- package/dist/compat/core.js +29 -0
- package/dist/compat/files.d.ts +1 -0
- package/dist/compat/files.js +25 -5
- package/dist/daytona/computer-use.d.ts +13 -4
- package/dist/daytona/computer-use.js +66 -8
- package/dist/daytona/daytona.d.ts +6 -0
- package/dist/daytona/daytona.js +30 -3
- package/dist/daytona/errors.d.ts +11 -0
- package/dist/daytona/errors.js +18 -0
- package/dist/daytona/filesystem.js +2 -1
- package/dist/daytona/image.d.ts +13 -9
- package/dist/daytona/image.js +18 -12
- package/dist/daytona/index.d.ts +1 -1
- package/dist/daytona/sandbox.d.ts +8 -2
- package/dist/daytona/sandbox.js +16 -2
- package/dist/domains/index.d.ts +9 -0
- package/dist/domains/index.js +10 -0
- package/dist/e2b/commands.d.ts +1 -1
- package/dist/e2b/commands.js +6 -7
- package/dist/e2b/errors.d.ts +17 -0
- package/dist/e2b/errors.js +40 -1
- package/dist/e2b/index.d.ts +2 -2
- package/dist/e2b/index.js +1 -1
- package/dist/e2b/sandbox.d.ts +27 -21
- package/dist/e2b/sandbox.js +34 -8
- package/dist/e2b/unsupported.js +1 -1
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +16 -4
- package/dist/freestyle/index.d.ts +285 -37
- package/dist/freestyle/index.js +656 -144
- package/dist/identity.d.ts +3 -0
- package/dist/identity.js +27 -0
- package/dist/images/index.d.ts +9 -0
- package/dist/images/index.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/ips/index.d.ts +9 -0
- package/dist/ips/index.js +10 -0
- package/dist/jobs/index.d.ts +9 -0
- package/dist/jobs/index.js +10 -0
- package/dist/keys.js +2 -0
- package/dist/login.js +2 -0
- package/dist/machine.d.ts +12 -3
- package/dist/machine.js +20 -3
- package/dist/mcp.js +1 -1
- package/dist/modal/index.d.ts +339 -61
- package/dist/modal/index.js +781 -121
- package/dist/network-cli.d.ts +3 -1
- package/dist/network-cli.js +78 -36
- package/dist/networks/index.d.ts +11 -0
- package/dist/networks/index.js +10 -0
- package/dist/ports/index.d.ts +9 -0
- package/dist/ports/index.js +10 -0
- package/dist/product-names.d.ts +52 -0
- package/dist/product-names.js +164 -0
- package/dist/product-renames.d.ts +103 -0
- package/dist/product-renames.js +189 -0
- package/dist/products/apps.d.ts +176 -0
- package/dist/products/apps.js +165 -0
- package/dist/products/billing.d.ts +41 -0
- package/dist/products/billing.js +13 -0
- package/dist/products/browser.d.ts +10 -0
- package/dist/products/browsers.d.ts +71 -0
- package/dist/products/browsers.js +102 -0
- package/dist/products/builds.d.ts +126 -0
- package/dist/products/builds.js +221 -0
- package/dist/products/index.d.ts +3 -16
- package/dist/products/index.js +3 -14
- package/dist/products/jobs.d.ts +17 -0
- package/dist/products/jobs.js +27 -10
- package/dist/products/logs.d.ts +245 -0
- package/dist/products/logs.js +274 -0
- package/dist/products/logs.test.d.ts +1 -0
- package/dist/products/logs.test.js +343 -0
- package/dist/products/network-products.d.ts +38 -7
- package/dist/products/network-products.js +31 -18
- package/dist/products/observability.d.ts +3 -2
- package/dist/products/private-network.js +1 -1
- package/dist/products/queues.d.ts +149 -0
- package/dist/products/queues.js +213 -0
- package/dist/products/referrals.d.ts +21 -7
- package/dist/products/referrals.js +3 -3
- package/dist/products/secrets.d.ts +7 -3
- package/dist/products/secrets.js +39 -18
- package/dist/products/signed-requests.d.ts +21 -0
- package/dist/products/signed-requests.js +11 -0
- package/dist/products/streams.d.ts +73 -0
- package/dist/products/streams.js +17 -0
- package/dist/products/switching.d.ts +35 -7
- package/dist/products/switching.js +4 -3
- package/dist/products/volumes.d.ts +3 -0
- package/dist/record-cli.d.ts +23 -0
- package/dist/record-cli.js +308 -0
- package/dist/renamed-products.d.ts +38 -0
- package/dist/renamed-products.js +34 -0
- package/dist/runloop/index.d.ts +9 -2
- package/dist/runloop/index.js +29 -8
- package/dist/runloop/sdk.d.ts +131 -0
- package/dist/runloop/sdk.js +167 -0
- package/dist/sandbox.d.ts +3 -2
- package/dist/sandbox.js +14 -9
- package/dist/sandboxes/index.d.ts +11 -0
- package/dist/sandboxes/index.js +12 -0
- package/dist/snapshots/index.d.ts +9 -0
- package/dist/snapshots/index.js +11 -0
- package/dist/snapshots.d.ts +7 -4
- package/dist/sprites/index.d.ts +343 -31
- package/dist/sprites/index.js +866 -84
- package/dist/ssh.d.ts +1 -1
- package/dist/ssh.js +11 -11
- package/dist/terminal.d.ts +24 -0
- package/dist/terminal.js +32 -0
- package/dist/transport.d.ts +15 -1
- package/dist/transport.js +56 -3
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +63 -0
- package/dist/vercel/sandbox.js +5 -3
- package/dist/vercel/snapshot.js +10 -2
- package/dist/volumes/index.d.ts +9 -0
- package/dist/volumes/index.js +10 -0
- package/package.json +48 -2
package/dist/cli.js
CHANGED
|
@@ -11,15 +11,19 @@ import { Sandbox as SandboxHandle } from "./sandbox.js";
|
|
|
11
11
|
import { NotFoundError, RuntimeError } from "./errors.js";
|
|
12
12
|
import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
|
|
13
13
|
import { billingCommand } from "./billing-cli.js";
|
|
14
|
-
import { cliRefusal, describeRecord, newestCpu, topicHelp } from "./cli-words.js";
|
|
14
|
+
import { cliRefusal, describeRecord, newestCpu, productWords, topicHelp } from "./cli-words.js";
|
|
15
15
|
import { envFetch } from "./proxy.js";
|
|
16
|
+
import { logStream, terminalSafe, terminalSafeLines } from "./terminal.js";
|
|
16
17
|
import { VERSION } from "./transport.js";
|
|
18
|
+
/** A log stream's text (a sandbox process's, an image build's, a job run's)
|
|
19
|
+
* as stdout shows it: escaped on a terminal unless --raw (terminal.ts). */
|
|
20
|
+
const logText = (args) => (text) => process.stdout.isTTY && !has(args, "raw") ? terminalSafeLines(text) : text;
|
|
17
21
|
const HELP = `Runtime Cloud CLI ${VERSION}
|
|
18
22
|
|
|
19
23
|
Usage: runtime <product> <command> [options] runtime <command> [options]
|
|
20
24
|
|
|
21
25
|
Start
|
|
22
|
-
|
|
26
|
+
sandboxes run [--keep] [create options] -- <command>
|
|
23
27
|
A fresh sandbox runs one command, then stops
|
|
24
28
|
|
|
25
29
|
Account
|
|
@@ -53,13 +57,15 @@ Account
|
|
|
53
57
|
secrets rm <NAME> Delete a secret, every copy
|
|
54
58
|
keys create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide]
|
|
55
59
|
A new API key for CI, approved by an owner in the browser
|
|
56
|
-
referrals Your referral link: you both get up to $
|
|
60
|
+
referrals Your referral link: you both get up to $1,000, then
|
|
61
|
+
you earn 10% of what they spend for 12 months
|
|
57
62
|
compare --from <provider> [--days 30] What your usage would cost at e2b, daytona, vercel,
|
|
58
63
|
modal, cloudflare, fly, blaxel, lambda-microvms,
|
|
59
64
|
freestyle, prime and others, and what you save
|
|
60
65
|
switch [--from <provider>] Moving from e2b, daytona, vercel, modal, cloudflare, fly
|
|
61
|
-
or blaxel: record it before your first top-up
|
|
62
|
-
|
|
66
|
+
or blaxel: record it before your first top-up.
|
|
67
|
+
Credit you add within 30 days of signing up is
|
|
68
|
+
matched, up to $1,000 in total
|
|
63
69
|
ls Everything you run, every product
|
|
64
70
|
feedback "<text>" [--kind bug|missing_feature|competitor_gap|migration_blocker|docs|pricing|praise|other]
|
|
65
71
|
[--detail <text>] [--competitor <name>] [--request-id <req_...>]
|
|
@@ -72,13 +78,13 @@ Account
|
|
|
72
78
|
mcp Serve MCP on stdio for agents that need it
|
|
73
79
|
|
|
74
80
|
Connect
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
sandboxes ssh <id|name> [-- <command...>] Log in over SSH; \`runtime sandboxes ssh help\` for editors
|
|
82
|
+
sandboxes ssh config [--install] Make \`ssh <id|name>.runtime\` work (VS Code, JetBrains)
|
|
83
|
+
sandboxes port-forward <id|name> <port|local:remote>...
|
|
78
84
|
Forward local ports to ports in a sandbox
|
|
79
85
|
|
|
80
86
|
Observability
|
|
81
|
-
|
|
87
|
+
sandboxes metrics <id> [--range 1h] CPU and memory over time (15m 1h 6h 24h 7d 30d)
|
|
82
88
|
events [--sandbox <id>] [--type <type>] Lifecycle events, newest first
|
|
83
89
|
webhooks ls | create <url> [--events a,b] | test <id> | deliveries <id>
|
|
84
90
|
| update <id> [--url u] [--events a,b] [--enable|--disable]
|
|
@@ -88,22 +94,23 @@ Observability
|
|
|
88
94
|
Events and metrics to an OpenTelemetry endpoint
|
|
89
95
|
|
|
90
96
|
Products
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Each has its own help: \`runtime
|
|
97
|
+
sandboxes Linux microVMs: exec, shell, files, code, previews, network, desktop, browser
|
|
98
|
+
snapshots Saved sandboxes, disk and memory, to start or fork from
|
|
99
|
+
images Custom images from a Dockerfile, any public or private image, or a package list
|
|
100
|
+
volumes Persistent disks to attach to sandboxes
|
|
101
|
+
jobs A command in a fresh sandbox, once or on a cron schedule, with its logs
|
|
102
|
+
domains Your own hostname for a sandbox's port, with HTTPS
|
|
103
|
+
ports A public TCP port to a sandbox: databases, game servers
|
|
104
|
+
ips A dedicated outbound address, for allow-lists
|
|
105
|
+
networks Your networks: a WireGuard tunnel in, your own proxy out, sandboxes reaching
|
|
106
|
+
each other by name
|
|
107
|
+
Each has its own help: \`runtime sandboxes help\`, \`runtime images help\`...
|
|
108
|
+
The singular (\`runtime sandbox\`) is a shortcut for the same product.
|
|
102
109
|
|
|
103
110
|
Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
|
|
104
111
|
Behind a proxy: HTTPS_PROXY and NO_PROXY are read, on Node and Bun alike.
|
|
105
112
|
`;
|
|
106
|
-
const SANDBOX_HELP = `runtime
|
|
113
|
+
const SANDBOX_HELP = `runtime sandboxes <command>
|
|
107
114
|
|
|
108
115
|
Every <id> below can also be the sandbox's name.
|
|
109
116
|
|
|
@@ -112,7 +119,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
112
119
|
port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
|
|
113
120
|
create [--name n] [--label k=v]... [--env K=V]... [--vcpu 2] [--memory 4096] [--disk 4096]
|
|
114
121
|
[--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
|
|
115
|
-
[--timeout <
|
|
122
|
+
[--timeout <60-86400>] [--on-timeout pause|stop|delete] [--no-wait]
|
|
116
123
|
[--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
117
124
|
[--image <name:tag|imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
|
|
118
125
|
[--idle-pause <seconds>] [--no-auto-wake] [--persistent] [--get-or-create]
|
|
@@ -123,7 +130,9 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
123
130
|
Run and stream output; exits with its code
|
|
124
131
|
spawn <id> [--pty] -- <command...> Start in the background; prints a process id
|
|
125
132
|
ps <id> Running and recent processes
|
|
126
|
-
logs <id> <processId> [-f]
|
|
133
|
+
logs <id> <processId> [-f] [--raw] A process's output so far; -f follows it to the end;
|
|
134
|
+
on a terminal control characters show as \\uXXXX
|
|
135
|
+
unless --raw
|
|
127
136
|
kill <id> <processId> [--signal SIGTERM]
|
|
128
137
|
shell <id> [--command "bash -l"] An interactive terminal
|
|
129
138
|
cp <src> <dst> Copy files or directories; name a sandbox
|
|
@@ -144,7 +153,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
144
153
|
update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
|
|
145
154
|
[--persistent on|off] [--max-total-cost <usd>|none]
|
|
146
155
|
Change its settings; persistent keeps it running until stopped
|
|
147
|
-
snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
|
|
156
|
+
snapshot <id> [--name n] [--retention days|credit] Keep its whole machine; prints the snapshot id
|
|
148
157
|
mount <id> <s3|r2|gcs>://<bucket>[/prefix] <path> [--secret NAME] [--region r]
|
|
149
158
|
[--endpoint https://...] [--account-id id] [--read-only]
|
|
150
159
|
Your bucket as a directory; the proxy signs, the sandbox never sees the key
|
|
@@ -152,7 +161,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
152
161
|
tailscale up <id> --auth-key-secret NAME [--hostname h] [--tag tag:x]...
|
|
153
162
|
Join your tailnet (paid); the key is a secret set with --jobs
|
|
154
163
|
tailscale status <id> | tailscale down <id> Its tailnet address; take it off the tailnet
|
|
155
|
-
fork <id> [--count 3] [--name n] [--keep-snapshot]
|
|
164
|
+
fork <id> [--count 3] [--name n] [--keep-snapshot]
|
|
156
165
|
Copies of it as it is now, running; prints their ids
|
|
157
166
|
run-code <id> <file|-> [--lang python|javascript|typescript|r|java|bash|go] [--context c] [--out-dir .]
|
|
158
167
|
Run a notebook-style cell; charts saved as PNG
|
|
@@ -188,10 +197,10 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
188
197
|
Chromium for CDP agents: prints its cdpUrl
|
|
189
198
|
|
|
190
199
|
Examples
|
|
191
|
-
id=$(runtime
|
|
192
|
-
runtime
|
|
193
|
-
runtime
|
|
194
|
-
runtime
|
|
200
|
+
id=$(runtime sandboxes create)
|
|
201
|
+
runtime sandboxes cp ./invoice.py $id:/workspace/invoice.py
|
|
202
|
+
runtime sandboxes exec $id -- python3 /workspace/invoice.py
|
|
203
|
+
runtime sandboxes stop $id
|
|
195
204
|
`;
|
|
196
205
|
/** A command's options. Reading one its parse() did not declare is a bug in
|
|
197
206
|
* this file, and says so, so a test that runs the command finds it. */
|
|
@@ -270,6 +279,8 @@ function unknownOption(name, known) {
|
|
|
270
279
|
}
|
|
271
280
|
const flag = (args, name) => args.flags.get(name)?.at(-1);
|
|
272
281
|
const has = (args, name) => args.flags.get(name)?.at(-1) === "true";
|
|
282
|
+
/** Days, or the word `credit` for null: kept while the account has credit. */
|
|
283
|
+
const daysOrCredit = (args, name) => flag(args, name) === "credit" ? null : integer(args, name);
|
|
273
284
|
const integer = (args, name) => {
|
|
274
285
|
const value = flag(args, name);
|
|
275
286
|
if (value === undefined)
|
|
@@ -360,16 +371,15 @@ const COMMANDS = [
|
|
|
360
371
|
"support",
|
|
361
372
|
"docs",
|
|
362
373
|
"mcp",
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
"
|
|
368
|
-
"
|
|
369
|
-
"
|
|
370
|
-
"
|
|
371
|
-
"
|
|
372
|
-
"network",
|
|
374
|
+
"sandboxes",
|
|
375
|
+
"snapshots",
|
|
376
|
+
"images",
|
|
377
|
+
"volumes",
|
|
378
|
+
"jobs",
|
|
379
|
+
"domains",
|
|
380
|
+
"ports",
|
|
381
|
+
"ips",
|
|
382
|
+
"networks",
|
|
373
383
|
"webhooks",
|
|
374
384
|
"otel",
|
|
375
385
|
"events",
|
|
@@ -378,30 +388,20 @@ const COMMANDS = [
|
|
|
378
388
|
];
|
|
379
389
|
/** Commands with a help of their own: `runtime <command> help`. */
|
|
380
390
|
const OWN_HELP = [
|
|
381
|
-
"sandbox",
|
|
382
391
|
"sandboxes",
|
|
383
392
|
"sbx",
|
|
384
|
-
"snapshot",
|
|
385
393
|
"snapshots",
|
|
386
|
-
"image",
|
|
387
394
|
"images",
|
|
388
|
-
"volume",
|
|
389
395
|
"volumes",
|
|
390
|
-
"job",
|
|
391
396
|
"jobs",
|
|
392
397
|
"keys",
|
|
393
398
|
"webhooks",
|
|
394
399
|
"webhook",
|
|
395
400
|
"billing",
|
|
396
|
-
"domain",
|
|
397
401
|
"domains",
|
|
398
|
-
"port",
|
|
399
402
|
"ports",
|
|
400
|
-
"
|
|
401
|
-
"
|
|
402
|
-
"tunnel",
|
|
403
|
-
"tunnels",
|
|
404
|
-
"network",
|
|
403
|
+
"ips",
|
|
404
|
+
"networks",
|
|
405
405
|
];
|
|
406
406
|
/** Sandbox commands that once sat at the top level. */
|
|
407
407
|
const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
|
|
@@ -506,6 +506,14 @@ function table(rows) {
|
|
|
506
506
|
.trimEnd())
|
|
507
507
|
.join("\n");
|
|
508
508
|
}
|
|
509
|
+
/** When a sandbox ends by itself (0300, 0381). A paid-ahead lease, expiresAt,
|
|
510
|
+
* is not its time limit; only an older server, which sends no endsAt, gives
|
|
511
|
+
* the end of a limited sandbox's time as expiresAt. */
|
|
512
|
+
function sandboxEnd(info) {
|
|
513
|
+
if (info.endsAt === null || (info.endsAt === undefined && !info.timeoutSeconds))
|
|
514
|
+
return "no limit";
|
|
515
|
+
return info.endsAt ?? info.expiresAt ?? "-";
|
|
516
|
+
}
|
|
509
517
|
function describe(info) {
|
|
510
518
|
return table([
|
|
511
519
|
["id", info.id],
|
|
@@ -515,19 +523,9 @@ function describe(info) {
|
|
|
515
523
|
"shape",
|
|
516
524
|
`${info.vcpu} vCPU, ${info.memoryMiB} MiB memory, ${info.diskMiB} MiB disk (${info.cpu})`,
|
|
517
525
|
],
|
|
518
|
-
["funding", info.funding],
|
|
519
526
|
["region", info.region],
|
|
520
|
-
// A sandbox with no time limit runs while it works (0300): endsAt is
|
|
521
|
-
// null. An older server sends no endsAt; its expiresAt was the end.
|
|
522
527
|
["time limit", info.timeoutSeconds ? `${info.timeoutSeconds} s` : "none"],
|
|
523
|
-
[
|
|
524
|
-
"ends",
|
|
525
|
-
info.endsAt === null
|
|
526
|
-
? info.state === "running"
|
|
527
|
-
? "when idle, stopped or out of credit"
|
|
528
|
-
: "-"
|
|
529
|
-
: (info.endsAt ?? info.expiresAt ?? "-"),
|
|
530
|
-
],
|
|
528
|
+
["ends", sandboxEnd(info)],
|
|
531
529
|
["at its end", info.onLeaseEnd ?? "-"],
|
|
532
530
|
// Why a sandbox is no longer running (troubleshooting.md, "A sandbox
|
|
533
531
|
// stopped on its own"); `get` left both out until 30 September 2026.
|
|
@@ -560,17 +558,22 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
560
558
|
// Only what comes before -- is the CLI's: a --json after it is the
|
|
561
559
|
// command's, whether or not the CLI was given its own.
|
|
562
560
|
const dash = argv.indexOf("--");
|
|
563
|
-
|
|
561
|
+
// CLOUD.md section 4: the plural names the product, the singular is a
|
|
562
|
+
// typing shortcut, and an old word works for 30 days with a warning.
|
|
563
|
+
const words = productWords(argv.filter((arg, index) => arg !== "--json" || (dash !== -1 && index > dash)));
|
|
564
|
+
if (words.warning)
|
|
565
|
+
out.error(`Warning: ${named(words.warning)}`);
|
|
566
|
+
const [first, ...remaining] = words.argv;
|
|
564
567
|
// Sandbox is one product of several, so its commands live under it
|
|
565
|
-
// (AGENTS.md, 23 September 2026): `runtime
|
|
568
|
+
// (AGENTS.md, 23 September 2026): `runtime sandboxes run`, `sandbox ssh` and
|
|
566
569
|
// `sandbox port-forward`. The old top-level forms were removed, not aliased.
|
|
567
570
|
if (first && SANDBOX_MOVED.includes(first))
|
|
568
|
-
throw usage(`\`${me} ${first}\` is now \`${me}
|
|
571
|
+
throw usage(`\`${me} ${first}\` is now \`${me} sandboxes ${first === "forward" ? "port-forward" : first}\`.`);
|
|
569
572
|
const nested = (first === "sandbox" || first === "sandboxes" || first === "sbx") &&
|
|
570
573
|
SANDBOX_MOVED.includes(remaining[0] ?? "");
|
|
571
574
|
const command = nested ? remaining[0] : first;
|
|
572
575
|
const rest = nested ? remaining.slice(1) : remaining;
|
|
573
|
-
// `runtime
|
|
576
|
+
// `runtime sandboxes create --help` is how an agent asks what a command takes:
|
|
574
577
|
// answer with the product's help, never by running the command.
|
|
575
578
|
const cut = remaining.indexOf("--");
|
|
576
579
|
const asked = (cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h") ||
|
|
@@ -603,10 +606,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
603
606
|
? JSON.stringify({ connected, usage: HELP })
|
|
604
607
|
: `Runtime Cloud ${VERSION}. ${start}
|
|
605
608
|
|
|
606
|
-
${me}
|
|
609
|
+
${me} sandboxes run -- python3 < invoice.py
|
|
607
610
|
Run a local invoice script, then stop the sandbox
|
|
608
|
-
${me}
|
|
609
|
-
${me}
|
|
611
|
+
${me} sandboxes create A sandbox that stays; prints its id
|
|
612
|
+
${me} sandboxes exec <id> -- <command> Run in it
|
|
610
613
|
${me} help Everything else
|
|
611
614
|
`);
|
|
612
615
|
return 0;
|
|
@@ -616,14 +619,15 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
616
619
|
return 0;
|
|
617
620
|
}
|
|
618
621
|
if (command === "run") {
|
|
619
|
-
const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me}
|
|
622
|
+
const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandboxes run -- python3 < invoice.py`);
|
|
620
623
|
const commandLine = args.rest ?? args.positional;
|
|
621
624
|
if (!commandLine.length)
|
|
622
|
-
throw usage(`Give the command after --: ${me}
|
|
625
|
+
throw usage(`Give the command after --: ${me} sandboxes run -- python3 < invoice.py`);
|
|
623
626
|
const runtime = await client(env);
|
|
624
627
|
const timeout = integer(args, "timeout");
|
|
625
628
|
const { timeoutSeconds: _lease, ...input } = createInput(args);
|
|
626
|
-
const
|
|
629
|
+
const onCapacityWait = waiting(out);
|
|
630
|
+
const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
|
|
627
631
|
let cleanup;
|
|
628
632
|
// SIGINT and normal completion share one stop. Keep an absolute deadline
|
|
629
633
|
// across the stop retries and final state check, so cleanup cannot hang.
|
|
@@ -642,7 +646,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
642
646
|
code: "cleanup_failed",
|
|
643
647
|
status: 0,
|
|
644
648
|
message: `Could not confirm that sandbox ${sbx.id} stopped. ${error instanceof Error ? error.message : String(error)}`,
|
|
645
|
-
hint: `Check it and retry cleanup with: ${me}
|
|
649
|
+
hint: `Check it and retry cleanup with: ${me} sandboxes stop ${sbx.id}`,
|
|
646
650
|
cause: error,
|
|
647
651
|
}), out.json));
|
|
648
652
|
return false;
|
|
@@ -658,7 +662,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
658
662
|
finally {
|
|
659
663
|
try {
|
|
660
664
|
if (has(args, "keep"))
|
|
661
|
-
out.error(`Kept sandbox ${sbx.id}. Run more with: ${me}
|
|
665
|
+
out.error(`Kept sandbox ${sbx.id}. Run more with: ${me} sandboxes exec ${sbx.id} -- <command>`);
|
|
662
666
|
else if (!(await stop()) && code === 0)
|
|
663
667
|
code = 1;
|
|
664
668
|
}
|
|
@@ -881,7 +885,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
881
885
|
product = "billing";
|
|
882
886
|
const origins = connectionOrigins(env);
|
|
883
887
|
const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
|
|
884
|
-
return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
|
|
888
|
+
return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], ["below", "amount", "monthly"]), out, () => client(env), undefined, {
|
|
885
889
|
open: (input) => {
|
|
886
890
|
checkBrowserOrigins(origins);
|
|
887
891
|
return openWalletAccount(input, { authUrl: origins.auth });
|
|
@@ -905,21 +909,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
905
909
|
});
|
|
906
910
|
}
|
|
907
911
|
if (command === "referrals") {
|
|
912
|
+
parse(rest, [], []);
|
|
908
913
|
const r = await (await client(env)).referrals.get();
|
|
909
914
|
const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
910
915
|
print([
|
|
911
916
|
`Your link: ${r.link}`,
|
|
912
917
|
"",
|
|
913
|
-
`
|
|
914
|
-
`you
|
|
915
|
-
`
|
|
918
|
+
`A company that signs up with it has each payment in its first ${r.windowDays} days matched, up to ${usd(r.maxRewardMicros)},`,
|
|
919
|
+
`and you get the same; its first payment of ${usd(r.minPurchaseMicros)} or more gives each of you at least ${usd(r.rewardMicros)}.`,
|
|
920
|
+
`Then for ${r.shareMonths} months you earn ${r.shareBasisPoints / 100}% of what it spends from credit it paid for`,
|
|
921
|
+
`(up to ${usd(r.yearlyCapMicros)} a year for you, matches and share together).`,
|
|
916
922
|
...(r.enabled ? [] : ["The program is paused: new sign-ups are not counted right now."]),
|
|
917
923
|
"",
|
|
918
924
|
table([
|
|
919
925
|
["signed up", String(r.signedUp)],
|
|
920
|
-
["waiting for a first
|
|
921
|
-
["
|
|
926
|
+
["waiting for a first payment", String(r.pending)],
|
|
927
|
+
["paying", String(r.payingCompanies)],
|
|
922
928
|
["earned", usd(r.earnedMicros)],
|
|
929
|
+
["of which share", usd(r.shareEarnedMicros)],
|
|
923
930
|
[`left this year (${r.year})`, usd(r.capRemainingMicros)],
|
|
924
931
|
]),
|
|
925
932
|
].join("\n"), r);
|
|
@@ -1001,7 +1008,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
1001
1008
|
]);
|
|
1002
1009
|
print(rows.length
|
|
1003
1010
|
? table([["ID", "PRODUCT", "NAME", "STATE", "CREATED"], ...rows])
|
|
1004
|
-
: "Nothing yet. Start with: runtime
|
|
1011
|
+
: "Nothing yet. Start with: runtime sandboxes create", items);
|
|
1005
1012
|
return 0;
|
|
1006
1013
|
}
|
|
1007
1014
|
if (command === "ssh") {
|
|
@@ -1032,11 +1039,13 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
1032
1039
|
product = "job";
|
|
1033
1040
|
return jobCommand(rest, env, out);
|
|
1034
1041
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1042
|
+
// WireGuard is inside networks: `runtime networks wireguard peer add`.
|
|
1043
|
+
const wireguard = command === "networks" && rest[0] === "wireguard";
|
|
1044
|
+
const networkProduct = wireguard ? "wireguard" : command;
|
|
1045
|
+
const networkRest = wireguard ? rest.slice(1) : rest;
|
|
1046
|
+
if (command !== "wireguard" && NETWORK_PRODUCTS.includes(networkProduct)) {
|
|
1038
1047
|
product = networkProduct;
|
|
1039
|
-
return networkProductCommand(networkProduct,
|
|
1048
|
+
return networkProductCommand(networkProduct, networkRest, parse(networkRest, ["ipv6"], ["sandbox", "subnet", "public-key", "route", "out", "host", "secret"]), out, () => client(env));
|
|
1040
1049
|
}
|
|
1041
1050
|
throw unknown("command", command, COMMANDS);
|
|
1042
1051
|
}
|
|
@@ -1112,6 +1121,43 @@ function allowanceRows(allowances) {
|
|
|
1112
1121
|
`${amount(a.left)} of ${amount(a.quantity)} ${a.unit}s left`,
|
|
1113
1122
|
]);
|
|
1114
1123
|
}
|
|
1124
|
+
/** What Runtime for Startups includes as `runtime usage` says it, in the
|
|
1125
|
+
* console's words: machine time left of what it gave, storage as the GB kept
|
|
1126
|
+
* free each month and the GB kept now, and what it runs at once. */
|
|
1127
|
+
function potRows(pot, limits) {
|
|
1128
|
+
const amount = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
1129
|
+
const day = (iso) => {
|
|
1130
|
+
const at = new Date(iso);
|
|
1131
|
+
const month = at.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
|
|
1132
|
+
return `${at.getUTCDate()} ${month} ${at.getUTCFullYear()}`;
|
|
1133
|
+
};
|
|
1134
|
+
const NAMES = {
|
|
1135
|
+
"machine-cpu": "vCPU-hours",
|
|
1136
|
+
"machine-memory": "memory",
|
|
1137
|
+
"saved-copies": "saved copies kept free",
|
|
1138
|
+
disks: "volumes",
|
|
1139
|
+
};
|
|
1140
|
+
const ORDER = ["machine-cpu", "machine-memory", "saved-copies", "disks"];
|
|
1141
|
+
const rank = (pool) => (ORDER.includes(pool) ? ORDER.indexOf(pool) : ORDER.length);
|
|
1142
|
+
const rows = [...pot]
|
|
1143
|
+
.sort((a, b) => rank(a.pool) - rank(b.pool))
|
|
1144
|
+
.map((p) => {
|
|
1145
|
+
const name = `${p.period === "month" ? "Runtime for Startups" : "startup hours"}: ${NAMES[p.pool] ?? p.pool.replace(/-/g, " ")}`;
|
|
1146
|
+
const until = p.expiresAt ? ` until ${day(p.expiresAt)}` : "";
|
|
1147
|
+
if (p.period === "month") {
|
|
1148
|
+
const kept = p.keptGB ?? 0;
|
|
1149
|
+
const over = kept > p.quantity ? `; past ${amount(p.quantity)} GB bills at the published rate` : "";
|
|
1150
|
+
return [
|
|
1151
|
+
name,
|
|
1152
|
+
`${amount(p.quantity)} GB kept free${until} · ${amount(kept)} GB kept now${over}`,
|
|
1153
|
+
];
|
|
1154
|
+
}
|
|
1155
|
+
return [name, `${amount(p.left ?? 0)} of ${amount(p.quantity)} ${p.unit}s left${until}`];
|
|
1156
|
+
});
|
|
1157
|
+
if (limits)
|
|
1158
|
+
rows.push(["Runtime for Startups: sandboxes at once", amount(limits.sandboxesAtOnce)], ["Runtime for Startups: image builds at once", amount(limits.buildsAtOnce)]);
|
|
1159
|
+
return rows;
|
|
1160
|
+
}
|
|
1115
1161
|
/** What a pilot gives, as `runtime whoami` and `runtime limits` say it: the
|
|
1116
1162
|
* sandboxes it runs free and until when, past which credit does not run
|
|
1117
1163
|
* them. */
|
|
@@ -1155,6 +1201,16 @@ function usageSummary(u) {
|
|
|
1155
1201
|
rows.push(["free time", trialLeft(u.trial)]);
|
|
1156
1202
|
if (u.allowances)
|
|
1157
1203
|
rows.push(...allowanceRows(u.allowances));
|
|
1204
|
+
// Only a data property is an older server's answer. Do not invoke SDK
|
|
1205
|
+
// compatibility getters when a server sends neither generation of fields.
|
|
1206
|
+
const pot = "startupIncluded" in u
|
|
1207
|
+
? u.startupIncluded
|
|
1208
|
+
: Object.getOwnPropertyDescriptor(u, "pot")?.value;
|
|
1209
|
+
const potLimits = "startupLimits" in u
|
|
1210
|
+
? u.startupLimits
|
|
1211
|
+
: Object.getOwnPropertyDescriptor(u, "potLimits")?.value;
|
|
1212
|
+
if (pot?.length || potLimits)
|
|
1213
|
+
rows.push(...potRows(pot ?? [], potLimits));
|
|
1158
1214
|
if (u.pilot)
|
|
1159
1215
|
rows.push(["pilot sandbox hours", pilotLeft(u.pilot)]);
|
|
1160
1216
|
if (u.outbound) {
|
|
@@ -1233,10 +1289,21 @@ async function outputSoFar(runtime, sandboxId, processId) {
|
|
|
1233
1289
|
processId,
|
|
1234
1290
|
state: latest?.state ?? null,
|
|
1235
1291
|
exitCode: latest?.exitCode ?? null,
|
|
1292
|
+
...(latest?.outOfMemory ? { outOfMemory: latest.outOfMemory } : {}),
|
|
1236
1293
|
truncated,
|
|
1237
1294
|
chunks,
|
|
1238
1295
|
};
|
|
1239
1296
|
}
|
|
1297
|
+
/** What a person reads when the sandbox's out-of-memory killer ended a
|
|
1298
|
+
* command: the API's message and hint, on standard error. A bare exit -9 (or
|
|
1299
|
+
* 137) said nothing of memory until 9 October 2026. */
|
|
1300
|
+
export function outOfMemoryLines(memory) {
|
|
1301
|
+
// The words come from an answer, not this CLI: escaped like a log line, so
|
|
1302
|
+
// nothing in them can drive the terminal (terminal.ts).
|
|
1303
|
+
return memory && typeof memory.message === "string" && typeof memory.hint === "string"
|
|
1304
|
+
? `runtime: ${terminalSafe(memory.message)}\nHint: ${terminalSafe(memory.hint)}`
|
|
1305
|
+
: undefined;
|
|
1306
|
+
}
|
|
1240
1307
|
/** Dollars from integer microdollars: cents from a dollar up, four places
|
|
1241
1308
|
* below one, and every place below a hundredth of a cent, so a short trial
|
|
1242
1309
|
* test never reads as $0.0000. */
|
|
@@ -1309,36 +1376,60 @@ const longDate = (iso) => new Date(iso).toLocaleDateString("en-GB", {
|
|
|
1309
1376
|
year: "numeric",
|
|
1310
1377
|
timeZone: "UTC",
|
|
1311
1378
|
});
|
|
1312
|
-
/** One line on the
|
|
1313
|
-
*
|
|
1379
|
+
/** One line on the welcome match: while it is open, how much of it credit
|
|
1380
|
+
* added now can still get; after, what it got. Undefined when there is
|
|
1381
|
+
* nothing to say: the offer ended unused. */
|
|
1382
|
+
function welcomeLine(summary) {
|
|
1383
|
+
const w = summary.welcome;
|
|
1384
|
+
// A server from before the welcome match (0433) answers without it.
|
|
1385
|
+
if (!w)
|
|
1386
|
+
return undefined;
|
|
1387
|
+
const whole = (micros) => dollars(micros).replace(/\.00$/, "");
|
|
1388
|
+
if (w.eligible) {
|
|
1389
|
+
// A server from before 0435 answers without what is left.
|
|
1390
|
+
const left = w.leftMicros ?? (BigInt(w.maxMicros) - BigInt(w.creditMicros)).toString();
|
|
1391
|
+
return BigInt(w.creditMicros) > 0n
|
|
1392
|
+
? `${whole(w.creditMicros)} of ${whole(w.maxMicros)} matched so far; credit you add by ${longDate(w.endsAt)} is matched, up to ${whole(left)} more.`
|
|
1393
|
+
: `Credit you add by ${longDate(w.endsAt)} is matched, up to ${whole(w.maxMicros)} in total.`;
|
|
1394
|
+
}
|
|
1395
|
+
switch (w.status) {
|
|
1396
|
+
case "paid":
|
|
1397
|
+
return `Your credit was matched: ${dollars(w.creditMicros)} of credit.`;
|
|
1398
|
+
case "superseded":
|
|
1399
|
+
return "Your payments are matched by your referral instead, which is never less.";
|
|
1400
|
+
case "reversed":
|
|
1401
|
+
return "The payments that were matched were refunded, so the match was taken back.";
|
|
1402
|
+
case "refused":
|
|
1403
|
+
return "Your credit was not matched. Ask support if you think that is wrong.";
|
|
1404
|
+
}
|
|
1405
|
+
return undefined;
|
|
1406
|
+
}
|
|
1407
|
+
/** What `switch` says: the switch on record, if any, and the welcome match.
|
|
1408
|
+
* A switch already on record is named with its provider and date, so it never
|
|
1314
1409
|
* reads as if this command recorded it, or recorded the provider it names. */
|
|
1315
1410
|
function switchingLine(summary, provider) {
|
|
1316
|
-
const cap = dollars(summary.maxMicros);
|
|
1317
1411
|
const sw = summary.switch;
|
|
1412
|
+
const welcome = welcomeLine(summary);
|
|
1318
1413
|
if (!sw) {
|
|
1319
|
-
if (!
|
|
1320
|
-
return
|
|
1321
|
-
return
|
|
1414
|
+
if (!provider || !summary.eligible)
|
|
1415
|
+
return welcome;
|
|
1416
|
+
return [
|
|
1417
|
+
`Switching from ${RIVAL_NAMES[provider]}? Run \`${me} switch --from ${provider}\` before your first top-up to record it.`,
|
|
1418
|
+
welcome,
|
|
1419
|
+
]
|
|
1420
|
+
.filter(Boolean)
|
|
1421
|
+
.join(" ");
|
|
1322
1422
|
}
|
|
1323
1423
|
const from = RIVAL_NAMES[sw.provider];
|
|
1324
|
-
const recorded = `This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}`;
|
|
1325
1424
|
const once = provider && provider !== sw.provider
|
|
1326
1425
|
? ` A switch is recorded once, so it stays ${from}, not ${RIVAL_NAMES[provider]}.`
|
|
1327
1426
|
: "";
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
return `${recorded}; its first top-up was matched by a referral instead, which is never less than the switching credit.${once}`;
|
|
1335
|
-
case "reversed":
|
|
1336
|
-
return `${recorded}; the top-up it matched was refunded, so the credit was taken back.${once}`;
|
|
1337
|
-
case "refused":
|
|
1338
|
-
return sw.refusedReason === "program_off"
|
|
1339
|
-
? `${recorded}; the switching credit was paused when its first top-up arrived.${once}`
|
|
1340
|
-
: `${recorded}; its first top-up was not matched. Ask support if you think that is wrong.${once}`;
|
|
1341
|
-
}
|
|
1427
|
+
return [
|
|
1428
|
+
`This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}.${once}`,
|
|
1429
|
+
welcome,
|
|
1430
|
+
]
|
|
1431
|
+
.filter(Boolean)
|
|
1432
|
+
.join(" ");
|
|
1342
1433
|
}
|
|
1343
1434
|
/** Running time in the largest unit that keeps it above zero: a short
|
|
1344
1435
|
* trial test is seconds, not "0 hours". */
|
|
@@ -1378,10 +1469,13 @@ async function switching(command, argv, env, out) {
|
|
|
1378
1469
|
if (command === "switch") {
|
|
1379
1470
|
if (!from) {
|
|
1380
1471
|
const summary = await api.get();
|
|
1381
|
-
|
|
1382
|
-
(summary.
|
|
1383
|
-
|
|
1384
|
-
|
|
1472
|
+
const record = summary.eligible
|
|
1473
|
+
? `Moving from a rival? Run \`${me} switch --from <provider>\` (${summary.providers.join(", ")}) before your first top-up to record it.`
|
|
1474
|
+
: undefined;
|
|
1475
|
+
print((summary.switch
|
|
1476
|
+
? switchingLine(summary)
|
|
1477
|
+
: [record, welcomeLine(summary)].filter(Boolean).join(" ")) ||
|
|
1478
|
+
"A switch is recorded before the first top-up, and this organization has made one already.", summary);
|
|
1385
1479
|
return 0;
|
|
1386
1480
|
}
|
|
1387
1481
|
const provider = SWITCH_FROM[from];
|
|
@@ -1389,7 +1483,7 @@ async function switching(command, argv, env, out) {
|
|
|
1389
1483
|
throw unknown("provider", from, Object.keys(RIVAL_NAMES));
|
|
1390
1484
|
const summary = await api.record({ provider });
|
|
1391
1485
|
print([
|
|
1392
|
-
`Recorded: switching from ${RIVAL_NAMES[provider]}. ${
|
|
1486
|
+
`Recorded: switching from ${RIVAL_NAMES[provider]}. ${welcomeLine(summary) ?? ""}`.trim(),
|
|
1393
1487
|
...dropInLines(provider),
|
|
1394
1488
|
].join("\n"), summary);
|
|
1395
1489
|
return 0;
|
|
@@ -1507,9 +1601,9 @@ async function sandbox(argv, env, out) {
|
|
|
1507
1601
|
switch (verb) {
|
|
1508
1602
|
case "create": {
|
|
1509
1603
|
const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
});
|
|
1604
|
+
const input = createInput(args);
|
|
1605
|
+
const onCapacityWait = waiting(out);
|
|
1606
|
+
const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
|
|
1513
1607
|
print(sbx.id, sbx.info);
|
|
1514
1608
|
return 0;
|
|
1515
1609
|
}
|
|
@@ -1534,24 +1628,21 @@ async function sandbox(argv, env, out) {
|
|
|
1534
1628
|
? null
|
|
1535
1629
|
: await runtime.sandboxes.list({ state: ["stopped"], limit: 50 });
|
|
1536
1630
|
const stopped = hidden?.data.length
|
|
1537
|
-
? `${hidden.data.length}${hidden.hasMore ? "+" : ""} stopped; runtime
|
|
1631
|
+
? `${hidden.data.length}${hidden.hasMore ? "+" : ""} stopped; runtime sandboxes ls --all shows them.`
|
|
1538
1632
|
: "";
|
|
1539
1633
|
const rows = all.map((s) => [
|
|
1540
1634
|
s.id,
|
|
1541
1635
|
s.info.name ?? "-",
|
|
1542
1636
|
s.state,
|
|
1543
1637
|
`${s.info.vcpu}/${s.info.memoryMiB}`,
|
|
1544
|
-
s.info
|
|
1545
|
-
// When it stops or pauses by itself (0381): its time limit's end, or
|
|
1546
|
-
// "-" when it never will. An older server sends no endsAt.
|
|
1547
|
-
(s.info.endsAt === undefined ? s.info.expiresAt : s.info.endsAt) ?? "-",
|
|
1638
|
+
sandboxEnd(s.info),
|
|
1548
1639
|
]);
|
|
1549
1640
|
print([
|
|
1550
1641
|
rows.length
|
|
1551
|
-
? table([["ID", "NAME", "STATE", "CPU/MIB", "
|
|
1642
|
+
? table([["ID", "NAME", "STATE", "CPU/MIB", "ENDS"], ...rows])
|
|
1552
1643
|
: stopped
|
|
1553
1644
|
? "No live sandboxes."
|
|
1554
|
-
: "No sandboxes. Create one: runtime
|
|
1645
|
+
: "No sandboxes. Create one: runtime sandboxes create",
|
|
1555
1646
|
stopped,
|
|
1556
1647
|
]
|
|
1557
1648
|
.filter(Boolean)
|
|
@@ -1566,31 +1657,31 @@ async function sandbox(argv, env, out) {
|
|
|
1566
1657
|
case "switch-image": {
|
|
1567
1658
|
const args = parse(rest, ["keep-workspace"], []);
|
|
1568
1659
|
const [id, image] = args.positional;
|
|
1569
|
-
const sbx = await use(need(id, `a sandbox, e.g. ${me}
|
|
1660
|
+
const sbx = await use(need(id, `a sandbox, e.g. ${me} sandboxes switch-image <id> app:v2 --keep-workspace`));
|
|
1570
1661
|
if (!args.flags.has("keep-workspace"))
|
|
1571
|
-
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}
|
|
1662
|
+
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} sandboxes snapshot ${sbx.id}) to keep everything.`);
|
|
1572
1663
|
await sbx.switchImage(need(image, "an image, e.g. app:v2"), { keep: "workspace" });
|
|
1573
1664
|
print(describe(sbx.info), sbx.info);
|
|
1574
1665
|
return 0;
|
|
1575
1666
|
}
|
|
1576
1667
|
case "resize": {
|
|
1577
1668
|
const args = parse(rest, ["restart"], ["vcpu", "memory"]);
|
|
1578
|
-
const sbx = await use(need(args.positional[0], `a sandbox, e.g. ${me}
|
|
1669
|
+
const sbx = await use(need(args.positional[0], `a sandbox, e.g. ${me} sandboxes resize <id> --memory 4096 --restart`));
|
|
1579
1670
|
const size = {
|
|
1580
1671
|
...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
|
|
1581
1672
|
...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
|
|
1582
1673
|
};
|
|
1583
1674
|
if (size.vcpu === undefined && size.memoryMiB === undefined)
|
|
1584
|
-
throw usage(`Name a new size, e.g. ${me}
|
|
1675
|
+
throw usage(`Name a new size, e.g. ${me} sandboxes resize ${sbx.id} --memory 4096 --restart`);
|
|
1585
1676
|
if (!args.flags.has("restart"))
|
|
1586
|
-
throw usage(`A resize restarts it: its whole disk stays and its programs stop. Add --restart to say so, or snapshot it first (${me}
|
|
1677
|
+
throw usage(`A resize restarts it: its whole disk stays and its programs stop. Add --restart to say so, or snapshot it first (${me} sandboxes snapshot ${sbx.id}) to keep its memory too.`);
|
|
1587
1678
|
await sbx.resize(size, { restart: true });
|
|
1588
1679
|
print(describe(sbx.info), sbx.info);
|
|
1589
1680
|
return 0;
|
|
1590
1681
|
}
|
|
1591
1682
|
case "rm":
|
|
1592
1683
|
case "delete": {
|
|
1593
|
-
const sbx = await use(need(rest[0], `a sandbox, e.g. ${me}
|
|
1684
|
+
const sbx = await use(need(rest[0], `a sandbox, e.g. ${me} sandboxes rm <id>`));
|
|
1594
1685
|
const gone = await sbx.delete();
|
|
1595
1686
|
print(`${gone.id} deleted`, gone);
|
|
1596
1687
|
return 0;
|
|
@@ -1692,15 +1783,15 @@ async function sandbox(argv, env, out) {
|
|
|
1692
1783
|
print(gone.left ? "Left the tailnet." : "Not on a tailnet.", gone);
|
|
1693
1784
|
return 0;
|
|
1694
1785
|
}
|
|
1695
|
-
throw usage("runtime
|
|
1786
|
+
throw usage("runtime sandboxes tailscale up|status|down <id>");
|
|
1696
1787
|
}
|
|
1697
1788
|
case "snapshot": {
|
|
1698
1789
|
const args = parse(rest, [], ["name", "retention"]);
|
|
1699
1790
|
const sbx = await get(args.positional[0]);
|
|
1700
1791
|
const snapshot = await sbx.snapshot({
|
|
1701
1792
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1702
|
-
...(
|
|
1703
|
-
? { retentionDays:
|
|
1793
|
+
...(daysOrCredit(args, "retention") !== undefined
|
|
1794
|
+
? { retentionDays: daysOrCredit(args, "retention") }
|
|
1704
1795
|
: {}),
|
|
1705
1796
|
});
|
|
1706
1797
|
print(snapshot.id, snapshot);
|
|
@@ -1772,26 +1863,26 @@ async function sandbox(argv, env, out) {
|
|
|
1772
1863
|
}
|
|
1773
1864
|
case "extend": {
|
|
1774
1865
|
const sbx = await get(rest[0]);
|
|
1775
|
-
const seconds = Number(need(rest[1], "seconds, e.g. runtime
|
|
1866
|
+
const seconds = Number(need(rest[1], "seconds, e.g. runtime sandboxes extend <id> 600"));
|
|
1776
1867
|
await sbx.extend(seconds);
|
|
1777
|
-
print(sbx.info
|
|
1868
|
+
print(sandboxEnd(sbx.info) === "no limit"
|
|
1778
1869
|
? `${sbx.id} has no time limit: it runs while it works, so nothing was extended`
|
|
1779
|
-
: `${sbx.id} now ends at ${sbx.info
|
|
1870
|
+
: `${sbx.id} now ends at ${sandboxEnd(sbx.info)}`, sbx.info);
|
|
1780
1871
|
return 0;
|
|
1781
1872
|
}
|
|
1782
1873
|
case "exec": {
|
|
1783
|
-
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me}
|
|
1874
|
+
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandboxes exec <id> -- ls -la`);
|
|
1784
1875
|
const command = args.rest ?? args.positional.slice(1);
|
|
1785
1876
|
if (!command.length)
|
|
1786
|
-
throw usage("Give the command after --: runtime
|
|
1877
|
+
throw usage("Give the command after --: runtime sandboxes exec <id> -- ls -la");
|
|
1787
1878
|
const sbx = await use(args.positional[0]);
|
|
1788
1879
|
return execute(sbx, command, args, out, integer(args, "timeout"));
|
|
1789
1880
|
}
|
|
1790
1881
|
case "spawn": {
|
|
1791
|
-
const args = parse(rest, ["pty"], ["cwd", "env"], `${me}
|
|
1882
|
+
const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandboxes spawn <id> -- npm run dev`);
|
|
1792
1883
|
const command = args.rest ?? [];
|
|
1793
1884
|
if (!command.length)
|
|
1794
|
-
throw usage("Give the command after --: runtime
|
|
1885
|
+
throw usage("Give the command after --: runtime sandboxes spawn <id> -- npm run dev");
|
|
1795
1886
|
const sbx = await use(args.positional[0]);
|
|
1796
1887
|
const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
|
|
1797
1888
|
...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
|
|
@@ -1807,36 +1898,54 @@ async function sandbox(argv, env, out) {
|
|
|
1807
1898
|
print(list.length
|
|
1808
1899
|
? table([
|
|
1809
1900
|
["PROCESS", "STATE", "EXIT", "COMMAND"],
|
|
1810
|
-
|
|
1901
|
+
// The command line is the guest's report: escaped, as a log is.
|
|
1902
|
+
...list.map((p) => [
|
|
1903
|
+
terminalSafe(String(p.id)),
|
|
1904
|
+
terminalSafe(String(p.state)),
|
|
1905
|
+
`${typeof p.exitCode === "number" ? p.exitCode : "-"}${p.outOfMemory ? " (out of memory)" : ""}`,
|
|
1906
|
+
terminalSafe(String(p.command ?? "")),
|
|
1907
|
+
]),
|
|
1811
1908
|
])
|
|
1812
1909
|
: "No processes.", list);
|
|
1910
|
+
const killed = list.find((p) => p.outOfMemory)?.outOfMemory;
|
|
1911
|
+
if (killed && typeof killed.hint === "string" && !out.json)
|
|
1912
|
+
out.error(`Hint: ${terminalSafe(killed.hint)}`);
|
|
1813
1913
|
return 0;
|
|
1814
1914
|
}
|
|
1815
1915
|
case "logs": {
|
|
1816
|
-
const args = parse(rest, ["f", "follow"], []);
|
|
1916
|
+
const args = parse(rest, ["f", "follow", "raw"], []);
|
|
1817
1917
|
const sbx = await use(args.positional[0]);
|
|
1818
1918
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
1919
|
+
const [stdout, stderr, shown] = [
|
|
1920
|
+
logStream(process.stdout, has(args, "raw")),
|
|
1921
|
+
logStream(process.stderr, has(args, "raw")),
|
|
1922
|
+
logText(args),
|
|
1923
|
+
];
|
|
1819
1924
|
if (has(args, "f") || has(args, "follow")) {
|
|
1820
1925
|
for await (const event of proc.output())
|
|
1821
1926
|
if (event.type === "stdout")
|
|
1822
|
-
|
|
1927
|
+
stdout(event.data);
|
|
1823
1928
|
else if (event.type === "stderr")
|
|
1824
|
-
|
|
1929
|
+
stderr(event.data);
|
|
1825
1930
|
else if (out.json)
|
|
1826
|
-
out.write(JSON.stringify(event));
|
|
1931
|
+
out.write(shown(JSON.stringify(event)));
|
|
1932
|
+
else if (event.type === "exit" && event.outOfMemory)
|
|
1933
|
+
out.error(outOfMemoryLines(event.outOfMemory));
|
|
1827
1934
|
return 0;
|
|
1828
1935
|
}
|
|
1829
1936
|
/* Without -f, what it has printed so far, then back to the prompt, as
|
|
1830
1937
|
`docker logs` does: a server's log is read, not followed forever. */
|
|
1831
1938
|
const so = await outputSoFar(runtime, sbx.id, proc.id);
|
|
1832
1939
|
if (out.json) {
|
|
1833
|
-
out.write(JSON.stringify(so));
|
|
1940
|
+
out.write(shown(JSON.stringify(so)));
|
|
1834
1941
|
return 0;
|
|
1835
1942
|
}
|
|
1836
1943
|
for (const chunk of so.chunks)
|
|
1837
|
-
(chunk.stream === "stderr" ?
|
|
1944
|
+
(chunk.stream === "stderr" ? stderr : stdout)(chunk.text);
|
|
1945
|
+
if (so.outOfMemory)
|
|
1946
|
+
out.error(outOfMemoryLines(so.outOfMemory));
|
|
1838
1947
|
if (so.state === "running")
|
|
1839
|
-
process.stderr.write(`[${proc.id} is still running; follow it: ${me}
|
|
1948
|
+
process.stderr.write(`[${proc.id} is still running; follow it: ${me} sandboxes logs ${sbx.id} ${proc.id} -f]\n`);
|
|
1840
1949
|
return 0;
|
|
1841
1950
|
}
|
|
1842
1951
|
case "kill": {
|
|
@@ -1906,7 +2015,7 @@ async function sandbox(argv, env, out) {
|
|
|
1906
2015
|
return runCode(await use(rest[0]), rest.slice(1), out);
|
|
1907
2016
|
case "preview": {
|
|
1908
2017
|
if (rest[0] === "rotate") {
|
|
1909
|
-
const port = portOf(rest[2], "runtime
|
|
2018
|
+
const port = portOf(rest[2], "runtime sandboxes preview rotate <id> 3000");
|
|
1910
2019
|
const sbx = await use(rest[1]);
|
|
1911
2020
|
const preview = await sbx.previews.rotate(port);
|
|
1912
2021
|
print(preview.visibility === "public"
|
|
@@ -1915,7 +2024,7 @@ async function sandbox(argv, env, out) {
|
|
|
1915
2024
|
return 0;
|
|
1916
2025
|
}
|
|
1917
2026
|
const args = parse(rest, ["public"], ["ttl"]);
|
|
1918
|
-
const port = portOf(args.positional[1], "runtime
|
|
2027
|
+
const port = portOf(args.positional[1], "runtime sandboxes preview <id> 3000");
|
|
1919
2028
|
const sbx = await use(args.positional[0]);
|
|
1920
2029
|
const preview = await sbx.previews.create(port, {
|
|
1921
2030
|
...(has(args, "public") ? { visibility: "public" } : {}),
|
|
@@ -1935,7 +2044,7 @@ async function sandbox(argv, env, out) {
|
|
|
1935
2044
|
["PORT", "VISIBILITY", "URL"],
|
|
1936
2045
|
...list.map((p) => [String(p.port), p.visibility, p.url]),
|
|
1937
2046
|
])
|
|
1938
|
-
: "No shared ports. Share one: runtime
|
|
2047
|
+
: "No shared ports. Share one: runtime sandboxes preview <id> 3000", list);
|
|
1939
2048
|
return 0;
|
|
1940
2049
|
}
|
|
1941
2050
|
case "session": {
|
|
@@ -1952,17 +2061,17 @@ async function sandbox(argv, env, out) {
|
|
|
1952
2061
|
one.origins.join(",") || "-",
|
|
1953
2062
|
]),
|
|
1954
2063
|
])
|
|
1955
|
-
: "No sessions. Make one: runtime
|
|
2064
|
+
: "No sessions. Make one: runtime sandboxes session create <id> --origin https://app.example.com", list);
|
|
1956
2065
|
return 0;
|
|
1957
2066
|
}
|
|
1958
2067
|
if (verb === "revoke") {
|
|
1959
|
-
const sessionId = need(rest[2], "a session id: runtime
|
|
2068
|
+
const sessionId = need(rest[2], "a session id: runtime sandboxes session revoke <id> <session-id>");
|
|
1960
2069
|
const ended = await (await use(rest[1])).sessions.revoke(sessionId);
|
|
1961
2070
|
print(`Session ${ended.id} ended.`, ended);
|
|
1962
2071
|
return 0;
|
|
1963
2072
|
}
|
|
1964
2073
|
if (verb !== "create")
|
|
1965
|
-
throw usage("runtime
|
|
2074
|
+
throw usage("runtime sandboxes session create <id> [--origin <url>]..., session ls <id>, or session revoke <id> <session-id>");
|
|
1966
2075
|
const args = parse(rest.slice(1), [], ["origin", "ttl", "name"]);
|
|
1967
2076
|
const origins = args.flags.get("origin") ?? [];
|
|
1968
2077
|
const ttl = integer(args, "ttl");
|
|
@@ -1976,7 +2085,7 @@ async function sandbox(argv, env, out) {
|
|
|
1976
2085
|
return 0;
|
|
1977
2086
|
}
|
|
1978
2087
|
case "unshare": {
|
|
1979
|
-
const port = portOf(rest[1], "runtime
|
|
2088
|
+
const port = portOf(rest[1], "runtime sandboxes unshare <id> 3000");
|
|
1980
2089
|
const sbx = await use(rest[0]);
|
|
1981
2090
|
print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
|
|
1982
2091
|
return 0;
|
|
@@ -2174,7 +2283,7 @@ read = readSecret) {
|
|
|
2174
2283
|
}
|
|
2175
2284
|
if (!hosts.length) {
|
|
2176
2285
|
const saved = await runtime.secrets.set(name, { value, jobs: true });
|
|
2177
|
-
print(`Stored ${name} for jobs, version ${saved.jobs.version}. Bind it: ${me}
|
|
2286
|
+
print(`Stored ${name} for jobs, version ${saved.jobs.version}. Bind it: ${me} jobs create ... --secret ${name}`, saved);
|
|
2178
2287
|
return 0;
|
|
2179
2288
|
}
|
|
2180
2289
|
const header = flag(args, "header");
|
|
@@ -2193,14 +2302,14 @@ read = readSecret) {
|
|
|
2193
2302
|
`Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
|
|
2194
2303
|
saved.header
|
|
2195
2304
|
? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
|
|
2196
|
-
: `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the URL
|
|
2305
|
+
: `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the Authorization header or another credential header (x-api-key, x-goog-api-key); in the URL it stays a placeholder.`,
|
|
2197
2306
|
...(saved.rules?.length ? [`Only these requests get it: ${ruleText(saved.rules)}.`] : []),
|
|
2198
2307
|
...(saved.enforced
|
|
2199
2308
|
? []
|
|
2200
2309
|
: ["A host is catching up; running sandboxes have it within a minute."]),
|
|
2201
2310
|
...(saved.jobs
|
|
2202
2311
|
? [
|
|
2203
|
-
`Also stored for jobs, version ${saved.jobs.version}: --secret ${saved.name} on ${me}
|
|
2312
|
+
`Also stored for jobs, version ${saved.jobs.version}: --secret ${saved.name} on ${me} jobs create.`,
|
|
2204
2313
|
]
|
|
2205
2314
|
: []),
|
|
2206
2315
|
].join("\n"), saved);
|
|
@@ -2339,7 +2448,7 @@ async function desktop(sbx, argv, out) {
|
|
|
2339
2448
|
return 0;
|
|
2340
2449
|
}
|
|
2341
2450
|
}
|
|
2342
|
-
throw usage(`${me}
|
|
2451
|
+
throw usage(`${me} sandboxes desktop <id> start|stop|screenshot|open|click|type|press|windows|record`);
|
|
2343
2452
|
}
|
|
2344
2453
|
async function browser(sbx, argv, out) {
|
|
2345
2454
|
const [action, ...rest] = argv;
|
|
@@ -2353,7 +2462,8 @@ async function browser(sbx, argv, out) {
|
|
|
2353
2462
|
...(integer(args, "height") !== undefined ? { height: integer(args, "height") } : {}),
|
|
2354
2463
|
});
|
|
2355
2464
|
print(`${started.version}${started.headless ? "" : `, on the desktop: ${started.streamUrl}`}\n` +
|
|
2356
|
-
`CDP: ${started.cdpUrl}\n(private; it expires ${started.expiresAt ?? "never"})
|
|
2465
|
+
`CDP: ${started.cdpUrl}\n(private; it expires ${started.expiresAt ?? "never"})` +
|
|
2466
|
+
(started.hint ? `\n${started.hint}` : ""), started);
|
|
2357
2467
|
return 0;
|
|
2358
2468
|
}
|
|
2359
2469
|
case "get":
|
|
@@ -2368,7 +2478,7 @@ async function browser(sbx, argv, out) {
|
|
|
2368
2478
|
return 0;
|
|
2369
2479
|
}
|
|
2370
2480
|
}
|
|
2371
|
-
throw usage(`${me}
|
|
2481
|
+
throw usage(`${me} sandboxes browser <id> start [--headed]|get|stop`);
|
|
2372
2482
|
}
|
|
2373
2483
|
const KEYS_HELP = `runtime keys <command>
|
|
2374
2484
|
|
|
@@ -2541,14 +2651,14 @@ async function keys(argv, env, out) {
|
|
|
2541
2651
|
await confirm();
|
|
2542
2652
|
return 0;
|
|
2543
2653
|
}
|
|
2544
|
-
const SNAPSHOT_HELP = `runtime
|
|
2654
|
+
const SNAPSHOT_HELP = `runtime snapshots <command>
|
|
2545
2655
|
|
|
2546
2656
|
ls [--sandbox <id>] [--name n] Your snapshots
|
|
2547
2657
|
get <id> One snapshot
|
|
2548
|
-
extend <id> <days>
|
|
2658
|
+
extend <id> <days|credit> Keep it longer; credit: while you have credit
|
|
2549
2659
|
rm <id> Delete it
|
|
2550
|
-
Take one with \`runtime
|
|
2551
|
-
with \`runtime
|
|
2660
|
+
Take one with \`runtime sandboxes snapshot <sandboxId>\`; start a sandbox from it
|
|
2661
|
+
with \`runtime sandboxes create --snapshot <snapshotId>\`.
|
|
2552
2662
|
`;
|
|
2553
2663
|
async function snapshot(argv, env, out) {
|
|
2554
2664
|
const [verb, ...rest] = argv;
|
|
@@ -2571,9 +2681,15 @@ async function snapshot(argv, env, out) {
|
|
|
2571
2681
|
print(all.length
|
|
2572
2682
|
? table([
|
|
2573
2683
|
["ID", "NAME", "STATUS", "FROM", "EXPIRES"],
|
|
2574
|
-
...all.map((s) => [
|
|
2684
|
+
...all.map((s) => [
|
|
2685
|
+
s.id,
|
|
2686
|
+
s.name ?? "-",
|
|
2687
|
+
s.status,
|
|
2688
|
+
s.sourceSandboxId,
|
|
2689
|
+
s.expiresAt ?? "with credit",
|
|
2690
|
+
]),
|
|
2575
2691
|
])
|
|
2576
|
-
: "No snapshots. Take one: runtime
|
|
2692
|
+
: "No snapshots. Take one: runtime sandboxes snapshot <sandboxId>", all);
|
|
2577
2693
|
return 0;
|
|
2578
2694
|
}
|
|
2579
2695
|
case "get": {
|
|
@@ -2582,9 +2698,11 @@ async function snapshot(argv, env, out) {
|
|
|
2582
2698
|
return 0;
|
|
2583
2699
|
}
|
|
2584
2700
|
case "extend": {
|
|
2585
|
-
const
|
|
2701
|
+
const word = parse(rest.slice(1), [], ["days"]);
|
|
2702
|
+
const days = daysOrCredit(word, "days") ??
|
|
2703
|
+
(need(rest[1], "days") === "credit" ? null : Number(need(rest[1], "days")));
|
|
2586
2704
|
const found = await api.extend(need(rest[0], "a snapshot id"), days);
|
|
2587
|
-
print(`${found.id} kept until ${found.expiresAt}`, found);
|
|
2705
|
+
print(`${found.id} kept ${found.expiresAt === null ? "while you have credit" : `until ${found.expiresAt}`}`, found);
|
|
2588
2706
|
return 0;
|
|
2589
2707
|
}
|
|
2590
2708
|
case "rm":
|
|
@@ -2597,7 +2715,7 @@ async function snapshot(argv, env, out) {
|
|
|
2597
2715
|
}
|
|
2598
2716
|
throw unknown("snapshot command", verb, ["ls", "get", "extend", "rm"]);
|
|
2599
2717
|
}
|
|
2600
|
-
const IMAGE_HELP = `runtime
|
|
2718
|
+
const IMAGE_HELP = `runtime images <command>
|
|
2601
2719
|
|
|
2602
2720
|
build [<folder>] [--dockerfile|-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
|
|
2603
2721
|
[--name <name>] [--tag|-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
|
|
@@ -2612,8 +2730,9 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
2612
2730
|
ls Your images
|
|
2613
2731
|
versions <name> Every version of a name, newest first
|
|
2614
2732
|
get <image> One image: an id, name, name:tag or name@version
|
|
2615
|
-
logs <image> [--follow]
|
|
2616
|
-
Its build log
|
|
2733
|
+
logs <image> [--follow] [--raw]
|
|
2734
|
+
Its build log; on a terminal control characters
|
|
2735
|
+
show as \\uXXXX unless --raw
|
|
2617
2736
|
tag <image> <tag> Point a tag of the image's name at this version
|
|
2618
2737
|
untag <image> <tag> Take a tag off
|
|
2619
2738
|
rm <image> Delete that version and its tags
|
|
@@ -2624,7 +2743,7 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
2624
2743
|
registry set <ecr registry> --access-key-id <id>
|
|
2625
2744
|
Amazon ECR: the secret access key is read from standard input
|
|
2626
2745
|
registry rm <registry>
|
|
2627
|
-
Use one: runtime
|
|
2746
|
+
Use one: runtime sandboxes create --image app:latest
|
|
2628
2747
|
`;
|
|
2629
2748
|
/** Options `image build` takes with a value. */
|
|
2630
2749
|
const IMAGE_BUILD_VALUES = [
|
|
@@ -2759,7 +2878,7 @@ read = readSecret) {
|
|
|
2759
2878
|
])
|
|
2760
2879
|
: name
|
|
2761
2880
|
? `No versions of ${name}.`
|
|
2762
|
-
: "No images. Build one: runtime
|
|
2881
|
+
: "No images. Build one: runtime images build --pip pandas", all);
|
|
2763
2882
|
return 0;
|
|
2764
2883
|
}
|
|
2765
2884
|
case "get": {
|
|
@@ -2768,13 +2887,14 @@ read = readSecret) {
|
|
|
2768
2887
|
return 0;
|
|
2769
2888
|
}
|
|
2770
2889
|
case "logs": {
|
|
2771
|
-
const args = parse(rest, ["follow", "f"], []);
|
|
2890
|
+
const args = parse(rest, ["follow", "f", "raw"], []);
|
|
2772
2891
|
const id = (await api.resolve(need(args.positional[0], "an image"))).id;
|
|
2892
|
+
const shown = logText(args);
|
|
2773
2893
|
if (has(args, "follow") || has(args, "f")) {
|
|
2774
2894
|
const lines = [];
|
|
2775
|
-
const done = await api.followLogs(id, (line) => out.json ? lines.push(line.text) : out.write(line.text));
|
|
2895
|
+
const done = await api.followLogs(id, (line) => out.json ? lines.push(line.text) : out.write(shown(line.text)));
|
|
2776
2896
|
if (out.json)
|
|
2777
|
-
|
|
2897
|
+
out.write(shown(JSON.stringify({ lines, state: done.state })));
|
|
2778
2898
|
return done.state === "failed" ? 1 : 0;
|
|
2779
2899
|
}
|
|
2780
2900
|
let after = 0;
|
|
@@ -2786,7 +2906,7 @@ read = readSecret) {
|
|
|
2786
2906
|
break;
|
|
2787
2907
|
after = page.nextAfter;
|
|
2788
2908
|
}
|
|
2789
|
-
|
|
2909
|
+
out.write(shown(out.json ? JSON.stringify(lines) : lines.join("\n") || "(no log yet)"));
|
|
2790
2910
|
return 0;
|
|
2791
2911
|
}
|
|
2792
2912
|
case "tag":
|
|
@@ -2813,7 +2933,7 @@ read = readSecret) {
|
|
|
2813
2933
|
["REGISTRY", "KIND", "USER", "SINCE"],
|
|
2814
2934
|
...all.map((r) => [r.registry, r.kind, r.username, r.createdAt]),
|
|
2815
2935
|
])
|
|
2816
|
-
: "No registry credentials. Store some: runtime
|
|
2936
|
+
: "No registry credentials. Store some: runtime images registry set ghcr.io --username you", all);
|
|
2817
2937
|
return 0;
|
|
2818
2938
|
}
|
|
2819
2939
|
if (action === "set") {
|
|
@@ -2853,7 +2973,7 @@ read = readSecret) {
|
|
|
2853
2973
|
"registry",
|
|
2854
2974
|
]);
|
|
2855
2975
|
}
|
|
2856
|
-
const JOB_HELP = `runtime
|
|
2976
|
+
const JOB_HELP = `runtime jobs <command>
|
|
2857
2977
|
|
|
2858
2978
|
A job runs a command in a fresh sandbox, once or on a schedule. Each run spends
|
|
2859
2979
|
the included usage first, then credit at the sandbox rates.
|
|
@@ -2871,14 +2991,21 @@ const JOB_HELP = `runtime job <command>
|
|
|
2871
2991
|
ls Your jobs: state, schedule and next run
|
|
2872
2992
|
get <id> One job, in full
|
|
2873
2993
|
runs <id> Its runs: state, exit code, when
|
|
2874
|
-
logs <runId|jobId> [-f]
|
|
2994
|
+
logs <runId|jobId> [-f] [--raw] A run's output (a job id: its latest run); -f follows it;
|
|
2995
|
+
on a terminal control characters show as \\uXXXX
|
|
2996
|
+
unless --raw
|
|
2875
2997
|
pause <id> | resume <id> Stop or restart scheduling; a run already going finishes
|
|
2998
|
+
trigger <id> Run it now, once; its schedule does not move, and a
|
|
2999
|
+
paused job stays paused
|
|
3000
|
+
reschedule <id> (--cron "<five fields>" [--timezone Europe/Berlin] | --at <time|now>)
|
|
3001
|
+
A new schedule; the next run is its next time
|
|
2876
3002
|
cancel <id> End it for good and stop a run in progress
|
|
2877
3003
|
|
|
2878
3004
|
Examples
|
|
2879
|
-
runtime
|
|
2880
|
-
runtime
|
|
2881
|
-
runtime
|
|
3005
|
+
runtime jobs create nightly --cron "0 3 * * *" --timezone Europe/Berlin -- python3 /workspace/report.py
|
|
3006
|
+
runtime jobs create once --at now -- bash -lc 'echo hello from a job'
|
|
3007
|
+
runtime jobs trigger <jobId> && runtime jobs logs <jobId> -f
|
|
3008
|
+
runtime jobs reschedule <jobId> --cron "30 4 * * 1-5" --timezone Europe/Berlin
|
|
2882
3009
|
`;
|
|
2883
3010
|
const when = (ms) => (ms == null ? "-" : new Date(ms).toISOString());
|
|
2884
3011
|
function scheduleText(job) {
|
|
@@ -2926,13 +3053,13 @@ async function jobCommand(argv, env, out) {
|
|
|
2926
3053
|
"max-cost",
|
|
2927
3054
|
"max-total-cost",
|
|
2928
3055
|
"region",
|
|
2929
|
-
], `${me}
|
|
3056
|
+
], `${me} jobs create <name> --cron "0 3 * * *" -- python3 -c '...'`);
|
|
2930
3057
|
const name = need(args.positional[0], "the job's name, such as nightly-report");
|
|
2931
3058
|
if (args.positional.length > 1)
|
|
2932
|
-
throw usage(`Put the command after --: ${me}
|
|
3059
|
+
throw usage(`Put the command after --: ${me} jobs create ${name} ... -- ${args.positional.slice(1).join(" ")}`);
|
|
2933
3060
|
const argv = args.rest ?? [];
|
|
2934
3061
|
if (!argv.length)
|
|
2935
|
-
throw usage(`Give the command after --: ${me}
|
|
3062
|
+
throw usage(`Give the command after --: ${me} jobs create ${name} --at now -- echo hello`);
|
|
2936
3063
|
const cron = flag(args, "cron");
|
|
2937
3064
|
const at = flag(args, "at");
|
|
2938
3065
|
if ((cron === undefined) === (at === undefined))
|
|
@@ -2989,7 +3116,7 @@ async function jobCommand(argv, env, out) {
|
|
|
2989
3116
|
: {}),
|
|
2990
3117
|
});
|
|
2991
3118
|
if (!out.json)
|
|
2992
|
-
out.error(`Scheduled ${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}. Follow it: ${me}
|
|
3119
|
+
out.error(`Scheduled ${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}. Follow it: ${me} jobs logs ${job.id} -f`);
|
|
2993
3120
|
print(job.id, job);
|
|
2994
3121
|
return 0;
|
|
2995
3122
|
}
|
|
@@ -3009,7 +3136,7 @@ async function jobCommand(argv, env, out) {
|
|
|
3009
3136
|
j.blockedReason ?? "-",
|
|
3010
3137
|
]),
|
|
3011
3138
|
])
|
|
3012
|
-
: `No jobs. Schedule one: ${me}
|
|
3139
|
+
: `No jobs. Schedule one: ${me} jobs create hello --at now -- echo hello`, all);
|
|
3013
3140
|
return 0;
|
|
3014
3141
|
}
|
|
3015
3142
|
case "get": {
|
|
@@ -3038,8 +3165,13 @@ async function jobCommand(argv, env, out) {
|
|
|
3038
3165
|
return 0;
|
|
3039
3166
|
}
|
|
3040
3167
|
case "logs": {
|
|
3041
|
-
const args = parse(rest, ["f", "follow"], []);
|
|
3168
|
+
const args = parse(rest, ["f", "follow", "raw"], []);
|
|
3042
3169
|
const id = need(args.positional[0], "a run id or a job id");
|
|
3170
|
+
const [stdout, stderr, shown] = [
|
|
3171
|
+
logStream(process.stdout, has(args, "raw")),
|
|
3172
|
+
logStream(process.stderr, has(args, "raw")),
|
|
3173
|
+
logText(args),
|
|
3174
|
+
];
|
|
3043
3175
|
let runId = id;
|
|
3044
3176
|
try {
|
|
3045
3177
|
await api.run(id);
|
|
@@ -3067,25 +3199,27 @@ async function jobCommand(argv, env, out) {
|
|
|
3067
3199
|
for (;;) {
|
|
3068
3200
|
const page = await api.logs(runId, { cursor, limitBytes: 65536 });
|
|
3069
3201
|
if (out.json)
|
|
3070
|
-
out.write(JSON.stringify(page));
|
|
3202
|
+
out.write(shown(JSON.stringify(page)));
|
|
3071
3203
|
else
|
|
3072
3204
|
for (const chunk of page.chunks)
|
|
3073
|
-
(chunk.stream === "stderr" ?
|
|
3205
|
+
(chunk.stream === "stderr" ? stderr : stdout)(chunk.text);
|
|
3074
3206
|
cursor = page.nextCursor;
|
|
3075
3207
|
if (page.complete) {
|
|
3208
|
+
if (!follow && out.json)
|
|
3209
|
+
return 0;
|
|
3210
|
+
const run = await api.run(runId);
|
|
3076
3211
|
if (!out.json) {
|
|
3077
|
-
const run = await api.run(runId);
|
|
3078
3212
|
if (page.truncated)
|
|
3079
3213
|
process.stderr.write("[the run printed more than it keeps; this is its last part]\n");
|
|
3080
3214
|
process.stderr.write(`[${run.state}${run.exitCode === null ? "" : `, exit code ${run.exitCode}`}${run.reason ? `, ${run.reason}` : ""}]\n`);
|
|
3081
3215
|
}
|
|
3082
|
-
return 0;
|
|
3216
|
+
return follow ? (run.exitCode ?? (run.state === "succeeded" ? 0 : 1)) : 0;
|
|
3083
3217
|
}
|
|
3084
3218
|
if (page.chunks.length)
|
|
3085
3219
|
continue;
|
|
3086
3220
|
if (!follow) {
|
|
3087
3221
|
if (!out.json)
|
|
3088
|
-
process.stderr.write(`[${runId} is still going; follow it: ${me}
|
|
3222
|
+
process.stderr.write(`[${runId} is still going; follow it: ${me} jobs logs ${runId} -f]\n`);
|
|
3089
3223
|
return 0;
|
|
3090
3224
|
}
|
|
3091
3225
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -3099,6 +3233,27 @@ async function jobCommand(argv, env, out) {
|
|
|
3099
3233
|
print(`${job.name} ${job.state}.`, job);
|
|
3100
3234
|
return 0;
|
|
3101
3235
|
}
|
|
3236
|
+
case "trigger": {
|
|
3237
|
+
const id = need(rest[0], "a job id");
|
|
3238
|
+
const job = await api.trigger(id);
|
|
3239
|
+
print(`${job.name} runs now${job.state === "paused" ? ", once; it stays paused" : ""}. Follow it: ${me} jobs logs ${job.id} -f`, job);
|
|
3240
|
+
return 0;
|
|
3241
|
+
}
|
|
3242
|
+
case "reschedule": {
|
|
3243
|
+
const args = parse(rest, [], ["cron", "timezone", "at"], `${me} jobs reschedule <id> --cron "0 3 * * *" [--timezone Europe/Berlin]`);
|
|
3244
|
+
const id = need(args.positional[0], "a job id");
|
|
3245
|
+
const cron = flag(args, "cron");
|
|
3246
|
+
const at = flag(args, "at");
|
|
3247
|
+
if ((cron === undefined) === (at === undefined))
|
|
3248
|
+
throw usage('Give --cron "<five fields>" for a schedule, or --at <time|now> for one run.');
|
|
3249
|
+
if (at !== undefined && flag(args, "timezone") !== undefined)
|
|
3250
|
+
throw usage("--timezone goes with --cron; --at takes a time with its own offset.");
|
|
3251
|
+
const job = await api.reschedule(id, cron !== undefined
|
|
3252
|
+
? { cron, ...(flag(args, "timezone") ? { timezone: flag(args, "timezone") } : {}) }
|
|
3253
|
+
: { at: atTime(at) });
|
|
3254
|
+
print(`${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}.`, job);
|
|
3255
|
+
return 0;
|
|
3256
|
+
}
|
|
3102
3257
|
}
|
|
3103
3258
|
throw unknown("job command", verb, [
|
|
3104
3259
|
"create",
|
|
@@ -3108,12 +3263,15 @@ async function jobCommand(argv, env, out) {
|
|
|
3108
3263
|
"logs",
|
|
3109
3264
|
"pause",
|
|
3110
3265
|
"resume",
|
|
3266
|
+
"trigger",
|
|
3267
|
+
"reschedule",
|
|
3111
3268
|
"cancel",
|
|
3112
3269
|
]);
|
|
3113
3270
|
}
|
|
3114
|
-
const VOLUME_HELP = `runtime
|
|
3271
|
+
const VOLUME_HELP = `runtime volumes <command>
|
|
3115
3272
|
|
|
3116
|
-
create --size-mib <N> [--name n] [--shared]
|
|
3273
|
+
create --size-mib <N> [--name n] [--label k=v]... [--shared]
|
|
3274
|
+
A persistent disk; prints its id
|
|
3117
3275
|
ls Your volumes
|
|
3118
3276
|
get <id> One volume, its backups and where it is attached
|
|
3119
3277
|
resize <id> --size-mib <N> Grow a detached volume on its current host
|
|
@@ -3128,7 +3286,7 @@ const VOLUME_HELP = `runtime volume <command>
|
|
|
3128
3286
|
backup-rm <backupId> Delete a backup
|
|
3129
3287
|
backup-policy <id> [--daily on|off] [--retention-days N]
|
|
3130
3288
|
Daily backups on or off, and how long each is kept
|
|
3131
|
-
Attach one: runtime
|
|
3289
|
+
Attach one: runtime sandboxes create --volume <id>:/data
|
|
3132
3290
|
Volumes are backed up off their host daily unless you turn it off.
|
|
3133
3291
|
Shared commands work where shared disks are enabled.
|
|
3134
3292
|
`;
|
|
@@ -3142,10 +3300,11 @@ async function volume(argv, env, out) {
|
|
|
3142
3300
|
const api = (await client(env)).volumes;
|
|
3143
3301
|
switch (verb) {
|
|
3144
3302
|
case "create": {
|
|
3145
|
-
const args = parse(rest, ["shared"], ["size-mib", "name"]);
|
|
3303
|
+
const args = parse(rest, ["shared"], ["size-mib", "name", "label"]);
|
|
3146
3304
|
const created = await api.create({
|
|
3147
3305
|
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
3148
3306
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
3307
|
+
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
3149
3308
|
...(has(args, "shared") ? { shared: true } : {}),
|
|
3150
3309
|
});
|
|
3151
3310
|
print(created.id, created);
|
|
@@ -3166,7 +3325,7 @@ async function volume(argv, env, out) {
|
|
|
3166
3325
|
v.attachments.map((a) => `${a.sandboxId}:${a.path}`).join(", ") || "-",
|
|
3167
3326
|
]),
|
|
3168
3327
|
])
|
|
3169
|
-
: "No volumes. Create one: runtime
|
|
3328
|
+
: "No volumes. Create one: runtime volumes create --size-mib 10240", all);
|
|
3170
3329
|
return 0;
|
|
3171
3330
|
}
|
|
3172
3331
|
case "get": {
|
|
@@ -3178,7 +3337,7 @@ async function volume(argv, env, out) {
|
|
|
3178
3337
|
const args = parse(rest, [], ["size-mib"]);
|
|
3179
3338
|
const id = need(args.positional[0], "a volume id");
|
|
3180
3339
|
if (args.positional.length !== 1 || args.rest)
|
|
3181
|
-
throw usage(`Use ${me}
|
|
3340
|
+
throw usage(`Use ${me} volumes resize <id> --size-mib <N>.`);
|
|
3182
3341
|
const sizeMiB = integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>"));
|
|
3183
3342
|
const resized = await api.resize(id, { sizeMiB });
|
|
3184
3343
|
print(describeRecord(resized, table), resized);
|
|
@@ -3190,7 +3349,7 @@ async function volume(argv, env, out) {
|
|
|
3190
3349
|
const sandboxId = need(args.positional[1], "a sandbox id");
|
|
3191
3350
|
const path = need(flag(args, "path"), "--path /data");
|
|
3192
3351
|
if (args.positional.length !== 2 || args.rest)
|
|
3193
|
-
throw usage(`Use ${me}
|
|
3352
|
+
throw usage(`Use ${me} volumes attach <id> <sandboxId> --path /data.`);
|
|
3194
3353
|
const attachment = await api.attach(id, { sandboxId, path });
|
|
3195
3354
|
print(describeRecord(attachment, table), attachment);
|
|
3196
3355
|
return attachment.error ? 1 : 0;
|
|
@@ -3201,7 +3360,7 @@ async function volume(argv, env, out) {
|
|
|
3201
3360
|
const id = need(args.positional[0], "a volume id");
|
|
3202
3361
|
const attachmentId = need(args.positional[1], "an attachment id");
|
|
3203
3362
|
if (args.positional.length !== 2 || args.rest)
|
|
3204
|
-
throw usage(`Use ${me}
|
|
3363
|
+
throw usage(`Use ${me} volumes ${verb} <id> <attachmentId>.`);
|
|
3205
3364
|
const attachment = verb === "detach"
|
|
3206
3365
|
? await api.detach(id, attachmentId)
|
|
3207
3366
|
: await api.getAttachment(id, attachmentId);
|
|
@@ -3210,7 +3369,8 @@ async function volume(argv, env, out) {
|
|
|
3210
3369
|
}
|
|
3211
3370
|
case "rm":
|
|
3212
3371
|
case "delete": {
|
|
3213
|
-
const
|
|
3372
|
+
const id = need(rest[0], "a volume id");
|
|
3373
|
+
const found = await withVolumeRelease((options) => api.delete(id, options), out);
|
|
3214
3374
|
print(`${found.id} ${found.state}`, found);
|
|
3215
3375
|
return 0;
|
|
3216
3376
|
}
|
|
@@ -3220,7 +3380,7 @@ async function volume(argv, env, out) {
|
|
|
3220
3380
|
const days = integer(args, "retention-days");
|
|
3221
3381
|
const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
|
|
3222
3382
|
if (!out.json && made.state === "pending")
|
|
3223
|
-
out.error(`Copying it off the host; check with: ${me}
|
|
3383
|
+
out.error(`Copying it off the host; check with: ${me} volumes backups ${id}`);
|
|
3224
3384
|
print(made.id, made);
|
|
3225
3385
|
return 0;
|
|
3226
3386
|
}
|
|
@@ -3248,7 +3408,7 @@ async function volume(argv, env, out) {
|
|
|
3248
3408
|
const args = parse(rest, [], ["name"]);
|
|
3249
3409
|
const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
|
|
3250
3410
|
if (!out.json && created.state === "creating")
|
|
3251
|
-
out.error(`Restoring; check with: ${me}
|
|
3411
|
+
out.error(`Restoring; check with: ${me} volumes get ${created.id}`);
|
|
3252
3412
|
print(created.id, created);
|
|
3253
3413
|
return 0;
|
|
3254
3414
|
}
|
|
@@ -3307,12 +3467,47 @@ const CREATE_FLAGS = [
|
|
|
3307
3467
|
function waiting(out) {
|
|
3308
3468
|
const said = new Set();
|
|
3309
3469
|
return (refusal) => {
|
|
3470
|
+
// The CLI's volume wait owns this refusal, including its deadline.
|
|
3471
|
+
if (refusal.code === "volume_releasing")
|
|
3472
|
+
throw refusal;
|
|
3310
3473
|
if (said.has(refusal.code))
|
|
3311
3474
|
return;
|
|
3312
3475
|
said.add(refusal.code);
|
|
3313
3476
|
out.error(`${refusal.message} Waiting for room, up to 2 minutes; Ctrl-C stops.`);
|
|
3314
3477
|
};
|
|
3315
3478
|
}
|
|
3479
|
+
/** A stopping sandbox can take a minute to let go of its volume. Retry only
|
|
3480
|
+
* that refusal, with the same key so a create cannot make two sandboxes. */
|
|
3481
|
+
async function withVolumeRelease(operation, out) {
|
|
3482
|
+
const idempotencyKey = crypto.randomUUID();
|
|
3483
|
+
let until;
|
|
3484
|
+
for (;;) {
|
|
3485
|
+
try {
|
|
3486
|
+
return await operation({
|
|
3487
|
+
idempotencyKey,
|
|
3488
|
+
...(until === undefined
|
|
3489
|
+
? {}
|
|
3490
|
+
: { signal: AbortSignal.timeout(Math.max(1, Math.ceil(until - performance.now()))) }),
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
catch (error) {
|
|
3494
|
+
if (!(error instanceof RuntimeError) ||
|
|
3495
|
+
error.code !== "volume_releasing" ||
|
|
3496
|
+
error.status !== 409)
|
|
3497
|
+
throw error;
|
|
3498
|
+
if (until === undefined) {
|
|
3499
|
+
until = performance.now() + 90_000;
|
|
3500
|
+
out.error("Waiting for the volume to be let go, up to 90 seconds; Ctrl-C stops.");
|
|
3501
|
+
}
|
|
3502
|
+
const left = until - performance.now();
|
|
3503
|
+
if (left <= 0)
|
|
3504
|
+
throw error;
|
|
3505
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1000, left)));
|
|
3506
|
+
if (performance.now() >= until)
|
|
3507
|
+
throw error;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3316
3511
|
/** Options `sandbox create` and `run` take with a value. */
|
|
3317
3512
|
const CREATE_VALUES = [
|
|
3318
3513
|
"name",
|
|
@@ -3353,6 +3548,9 @@ function usdMicros(args, name) {
|
|
|
3353
3548
|
}
|
|
3354
3549
|
/** A create body from the command line's flags. */
|
|
3355
3550
|
function createInput(args) {
|
|
3551
|
+
for (const name of ["image", "snapshot", "volume", "name", "region", "connect"])
|
|
3552
|
+
if (args.flags.get(name)?.some((value) => !value.trim()))
|
|
3553
|
+
throw usage(`--${name} needs a non-empty value.`);
|
|
3356
3554
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
3357
3555
|
return {
|
|
3358
3556
|
...(flag(args, "image") ? { image: flag(args, "image") } : {}),
|
|
@@ -3387,7 +3585,7 @@ function createInput(args) {
|
|
|
3387
3585
|
? { timeoutSeconds: integer(args, "timeout") }
|
|
3388
3586
|
: {}),
|
|
3389
3587
|
...(flag(args, "on-timeout")
|
|
3390
|
-
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
|
|
3588
|
+
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop", "delete"]) }
|
|
3391
3589
|
: {}),
|
|
3392
3590
|
...(integer(args, "idle-pause") !== undefined
|
|
3393
3591
|
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
@@ -3449,6 +3647,9 @@ ownInterrupt = true) {
|
|
|
3449
3647
|
// (Sandbox.exec); `--json` answers the result and prints no such line.
|
|
3450
3648
|
else if (result.timedOut)
|
|
3451
3649
|
out.error(`runtime: timed out after ${timeout ? `${timeout} s` : "24 h"}; the command's process group was killed.`);
|
|
3650
|
+
const memory = outOfMemoryLines(result.outOfMemory);
|
|
3651
|
+
if (memory && !out.json)
|
|
3652
|
+
out.error(memory);
|
|
3452
3653
|
// Never drop output silently: say so, and do not exit 0.
|
|
3453
3654
|
const lost = extras.lostOutput(result, sbx.id);
|
|
3454
3655
|
if (lost && !out.json)
|