withruntime 0.11.1 → 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 +3 -3
- package/COMPATIBILITY.md +74 -9
- package/DAYTONA.md +33 -5
- package/E2B.md +137 -48
- package/README.md +11 -9
- package/README.src.md +11 -9
- 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 +33 -4
- 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 +15 -7
- package/dist/blaxel/sandbox.js +64 -9
- 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 +6 -2
- package/dist/cli.js +468 -219
- package/dist/client.d.ts +13 -63
- package/dist/client.js +35 -124
- 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 +7 -2
- package/dist/daytona/daytona.js +31 -5
- 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 +9 -3
- package/dist/daytona/sandbox.js +17 -3
- package/dist/domains/index.d.ts +9 -0
- package/dist/domains/index.js +10 -0
- package/dist/e2b/client.d.ts +15 -2
- package/dist/e2b/client.js +61 -7
- package/dist/e2b/commands.d.ts +4 -1
- package/dist/e2b/commands.js +17 -7
- package/dist/e2b/errors.d.ts +43 -2
- package/dist/e2b/errors.js +78 -6
- package/dist/e2b/filesystem.d.ts +1 -1
- package/dist/e2b/filesystem.js +82 -16
- package/dist/e2b/index.d.ts +3 -3
- package/dist/e2b/index.js +2 -2
- package/dist/e2b/sandbox.d.ts +50 -35
- package/dist/e2b/sandbox.js +228 -185
- package/dist/e2b/unsupported.d.ts +19 -0
- package/dist/e2b/unsupported.js +22 -1
- package/dist/e2b/users.d.ts +9 -0
- package/dist/e2b/users.js +22 -0
- package/dist/errors.d.ts +14 -5
- package/dist/errors.js +28 -14
- 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 +189 -0
- package/dist/machine.js +521 -0
- 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/openai-agents/index.d.ts +1 -1
- package/dist/openai-agents/index.js +1 -1
- 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 +19 -3
- package/dist/products/jobs.js +28 -12
- package/dist/products/limits.d.ts +4 -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/network.d.ts +1 -1
- 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 +8 -4
- 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 +37 -10
- 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 +10 -94
- package/dist/sandbox.js +22 -371
- 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 +18 -2
- package/dist/transport.js +61 -6
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +116 -8
- package/dist/vercel/sandbox.d.ts +1 -2
- package/dist/vercel/sandbox.js +6 -5
- 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
|
|
@@ -36,7 +40,7 @@ Account
|
|
|
36
40
|
The account's audit log (owner or admin keys)
|
|
37
41
|
sso Single sign-on and SCIM directory sync (owner or
|
|
38
42
|
admin keys); an owner changes it on the website
|
|
39
|
-
usage [--csv] Balance,
|
|
43
|
+
usage [--csv] Balance, included usage and charges; --csv, each resource
|
|
40
44
|
usage export --since <UTC time> --until <UTC time>
|
|
41
45
|
Every settlement in the range as CSV; --json, each page
|
|
42
46
|
limits Whether this key is read-only, and its daily spending limit
|
|
@@ -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
|
}
|
|
@@ -695,16 +699,18 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
695
699
|
const [who, spend] = await Promise.all([rt.me(), rt.usage().catch(() => undefined)]);
|
|
696
700
|
const trialLeft = spend?.trial ? spend.trial.availableMs / 3_600_000 : 0;
|
|
697
701
|
const credit = spend ? BigInt(spend.available) : 0n;
|
|
702
|
+
// A pilot's sandboxes run on the pilot, before anything else and free.
|
|
698
703
|
const funding = !spend
|
|
699
704
|
? undefined
|
|
700
705
|
: [
|
|
706
|
+
...(spend.pilot ? [pilotFunding(spend.pilot)] : []),
|
|
701
707
|
...(trialLeft > 0
|
|
702
708
|
? [
|
|
703
|
-
`
|
|
709
|
+
`included usage, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left without credit`,
|
|
704
710
|
]
|
|
705
711
|
: []),
|
|
706
712
|
...(credit > 0n ? [`${dollars(credit)} of credit`] : []),
|
|
707
|
-
].join("; ") || "no
|
|
713
|
+
].join("; ") || "no included usage or credit left: runtime billing";
|
|
708
714
|
print(table([
|
|
709
715
|
["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
|
|
710
716
|
...(who.role ? [["role", who.role]] : []),
|
|
@@ -765,7 +771,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
765
771
|
return 0;
|
|
766
772
|
}
|
|
767
773
|
if (command === "limits") {
|
|
768
|
-
const
|
|
774
|
+
const rt = await client(env);
|
|
775
|
+
// The usage call only adds the pilot's line: a key that cannot read it
|
|
776
|
+
// still gets its limits.
|
|
777
|
+
const [l, spend] = await Promise.all([rt.limits.get(), rt.usage().catch(() => undefined)]);
|
|
769
778
|
// As `usage` shows money: $0.0014 of spend read $0.00 until 30 September 2026.
|
|
770
779
|
const usd = (micros) => dollars(micros);
|
|
771
780
|
const access = l.access === "read"
|
|
@@ -783,7 +792,13 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
783
792
|
],
|
|
784
793
|
["used, last 24 hours", usd(l.daily.usedMicros)],
|
|
785
794
|
...(l.daily.remainingMicros === null ? [] : [["left", usd(l.daily.remainingMicros)]]),
|
|
786
|
-
...(l.trial ? [["free
|
|
795
|
+
...(l.trial ? [["free time", trialLeft(l.trial)]] : []),
|
|
796
|
+
...(spend?.pilot
|
|
797
|
+
? [
|
|
798
|
+
["pilot", pilotFunding(spend.pilot)],
|
|
799
|
+
["pilot sandbox hours", pilotLeft(spend.pilot)],
|
|
800
|
+
]
|
|
801
|
+
: []),
|
|
787
802
|
]), l);
|
|
788
803
|
return 0;
|
|
789
804
|
}
|
|
@@ -870,7 +885,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
870
885
|
product = "billing";
|
|
871
886
|
const origins = connectionOrigins(env);
|
|
872
887
|
const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
|
|
873
|
-
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, {
|
|
874
889
|
open: (input) => {
|
|
875
890
|
checkBrowserOrigins(origins);
|
|
876
891
|
return openWalletAccount(input, { authUrl: origins.auth });
|
|
@@ -894,21 +909,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
894
909
|
});
|
|
895
910
|
}
|
|
896
911
|
if (command === "referrals") {
|
|
912
|
+
parse(rest, [], []);
|
|
897
913
|
const r = await (await client(env)).referrals.get();
|
|
898
914
|
const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
899
915
|
print([
|
|
900
916
|
`Your link: ${r.link}`,
|
|
901
917
|
"",
|
|
902
|
-
`
|
|
903
|
-
`you
|
|
904
|
-
`
|
|
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).`,
|
|
905
922
|
...(r.enabled ? [] : ["The program is paused: new sign-ups are not counted right now."]),
|
|
906
923
|
"",
|
|
907
924
|
table([
|
|
908
925
|
["signed up", String(r.signedUp)],
|
|
909
|
-
["waiting for a first
|
|
910
|
-
["
|
|
926
|
+
["waiting for a first payment", String(r.pending)],
|
|
927
|
+
["paying", String(r.payingCompanies)],
|
|
911
928
|
["earned", usd(r.earnedMicros)],
|
|
929
|
+
["of which share", usd(r.shareEarnedMicros)],
|
|
912
930
|
[`left this year (${r.year})`, usd(r.capRemainingMicros)],
|
|
913
931
|
]),
|
|
914
932
|
].join("\n"), r);
|
|
@@ -990,7 +1008,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
990
1008
|
]);
|
|
991
1009
|
print(rows.length
|
|
992
1010
|
? table([["ID", "PRODUCT", "NAME", "STATE", "CREATED"], ...rows])
|
|
993
|
-
: "Nothing yet. Start with: runtime
|
|
1011
|
+
: "Nothing yet. Start with: runtime sandboxes create", items);
|
|
994
1012
|
return 0;
|
|
995
1013
|
}
|
|
996
1014
|
if (command === "ssh") {
|
|
@@ -1021,15 +1039,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
1021
1039
|
product = "job";
|
|
1022
1040
|
return jobCommand(rest, env, out);
|
|
1023
1041
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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)) {
|
|
1027
1047
|
product = networkProduct;
|
|
1028
|
-
return networkProductCommand(networkProduct,
|
|
1048
|
+
return networkProductCommand(networkProduct, networkRest, parse(networkRest, ["ipv6"], ["sandbox", "subnet", "public-key", "route", "out", "host", "secret"]), out, () => client(env));
|
|
1029
1049
|
}
|
|
1030
1050
|
throw unknown("command", command, COMMANDS);
|
|
1031
1051
|
}
|
|
1032
|
-
/** `runtime usage` for a person: the balance, the
|
|
1052
|
+
/** `runtime usage` for a person: the balance, the included usage and what each kind
|
|
1033
1053
|
* of resource was charged, in dollars. --json keeps every figure, and each
|
|
1034
1054
|
* resource with its rates and CPU time. */
|
|
1035
1055
|
/** One row per resource, for a spreadsheet: when it ran, how big it was, how
|
|
@@ -1083,15 +1103,79 @@ export function usageCsv(u) {
|
|
|
1083
1103
|
}
|
|
1084
1104
|
/** Milliseconds as hours, to a tenth: "8,731.2". */
|
|
1085
1105
|
const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
1086
|
-
/** The
|
|
1106
|
+
/** The machine time left without credit, as `runtime usage` and `runtime
|
|
1107
|
+
* limits` say it: this month's included usage, as hours at 2 vCPU and 4 GiB,
|
|
1108
|
+
* or for an account given free hours before 5 October 2026, those hours too. */
|
|
1087
1109
|
function trialLeft(trial) {
|
|
1088
|
-
|
|
1110
|
+
const renews = trial.renewsAt ? `, renews ${trial.renewsAt.slice(0, 10)}` : "";
|
|
1111
|
+
return trial.totalMs > 0
|
|
1112
|
+
? `${hours(trial.availableMs)} hours left (${hours(trial.totalMs)} free hours given before 5 October 2026, and the included usage${renews})`
|
|
1113
|
+
: `${hours(trial.availableMs)} hours at 2 vCPU and 4 GiB left this month${renews}`;
|
|
1114
|
+
}
|
|
1115
|
+
/** This month's included usage, one row per product, as `runtime usage`
|
|
1116
|
+
* says it. */
|
|
1117
|
+
function allowanceRows(allowances) {
|
|
1118
|
+
const amount = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
1119
|
+
return allowances.map((a) => [
|
|
1120
|
+
`included ${a.pool.replace(/-/g, " ")}`,
|
|
1121
|
+
`${amount(a.left)} of ${amount(a.quantity)} ${a.unit}s left`,
|
|
1122
|
+
]);
|
|
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
|
+
}
|
|
1161
|
+
/** What a pilot gives, as `runtime whoami` and `runtime limits` say it: the
|
|
1162
|
+
* sandboxes it runs free and until when, past which credit does not run
|
|
1163
|
+
* them. */
|
|
1164
|
+
function pilotFunding(pilot) {
|
|
1165
|
+
const at = (iso) => `${iso.slice(0, 16).replace("T", " ")} UTC`;
|
|
1166
|
+
return `a pilot: up to ${pilot.sandboxes.toLocaleString("en-US")} sandboxes free, running until ${at(pilot.graceEndsAt)}; credit does not run it; to extend it, write to support`;
|
|
1089
1167
|
}
|
|
1090
1168
|
/** A pilot's sandbox hours left, as `runtime usage` says them. */
|
|
1091
1169
|
function pilotLeft(pilot) {
|
|
1092
1170
|
const left = `${hours(pilot.leftMs)} of ${pilot.hours.toLocaleString("en-US")} left`;
|
|
1171
|
+
// How long they last at the rate its sandboxes run now (an API before
|
|
1172
|
+
// 0414 does not say how many run).
|
|
1173
|
+
const running = pilot.running ?? 0;
|
|
1174
|
+
const pace = running > 0
|
|
1175
|
+
? `; at ${running.toLocaleString("en-US")} running, about ${hours(pilot.leftMs / running)} hours`
|
|
1176
|
+
: "";
|
|
1093
1177
|
return pilot.leftMs > 0
|
|
1094
|
-
? `${left}; new pilot sandboxes until ${pilot.endsAt.slice(0, 16).replace("T", " ")} UTC`
|
|
1178
|
+
? `${left}${pace}; new pilot sandboxes until ${pilot.endsAt.slice(0, 16).replace("T", " ")} UTC`
|
|
1095
1179
|
: "This pilot's sandbox hours are used.";
|
|
1096
1180
|
}
|
|
1097
1181
|
function usageSummary(u) {
|
|
@@ -1114,7 +1198,19 @@ function usageSummary(u) {
|
|
|
1114
1198
|
if (micros(u.expired) > 0n)
|
|
1115
1199
|
rows.push(["expired or taken back", dollars(micros(u.expired))]);
|
|
1116
1200
|
if (u.trial)
|
|
1117
|
-
rows.push(["free
|
|
1201
|
+
rows.push(["free time", trialLeft(u.trial)]);
|
|
1202
|
+
if (u.allowances)
|
|
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));
|
|
1118
1214
|
if (u.pilot)
|
|
1119
1215
|
rows.push(["pilot sandbox hours", pilotLeft(u.pilot)]);
|
|
1120
1216
|
if (u.outbound) {
|
|
@@ -1126,7 +1222,15 @@ function usageSummary(u) {
|
|
|
1126
1222
|
]);
|
|
1127
1223
|
}
|
|
1128
1224
|
const kinds = new Map();
|
|
1129
|
-
|
|
1225
|
+
// Every resource, by kind, from an API that totals them (6 October 2026);
|
|
1226
|
+
// an older one sends only its hundred newest, counted here.
|
|
1227
|
+
for (const k of u.kinds ?? [])
|
|
1228
|
+
kinds.set(k.kind, {
|
|
1229
|
+
count: k.resources,
|
|
1230
|
+
charged: micros(k.chargedMicros),
|
|
1231
|
+
held: micros(k.heldMicros),
|
|
1232
|
+
});
|
|
1233
|
+
for (const resource of u.kinds ? [] : (u.resources ?? [])) {
|
|
1130
1234
|
const kind = typeof resource.kind === "string" ? resource.kind : "other";
|
|
1131
1235
|
const entry = kinds.get(kind) ?? { count: 0, charged: 0n, held: 0n };
|
|
1132
1236
|
entry.count++;
|
|
@@ -1185,10 +1289,21 @@ async function outputSoFar(runtime, sandboxId, processId) {
|
|
|
1185
1289
|
processId,
|
|
1186
1290
|
state: latest?.state ?? null,
|
|
1187
1291
|
exitCode: latest?.exitCode ?? null,
|
|
1292
|
+
...(latest?.outOfMemory ? { outOfMemory: latest.outOfMemory } : {}),
|
|
1188
1293
|
truncated,
|
|
1189
1294
|
chunks,
|
|
1190
1295
|
};
|
|
1191
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
|
+
}
|
|
1192
1307
|
/** Dollars from integer microdollars: cents from a dollar up, four places
|
|
1193
1308
|
* below one, and every place below a hundredth of a cent, so a short trial
|
|
1194
1309
|
* test never reads as $0.0000. */
|
|
@@ -1261,36 +1376,60 @@ const longDate = (iso) => new Date(iso).toLocaleDateString("en-GB", {
|
|
|
1261
1376
|
year: "numeric",
|
|
1262
1377
|
timeZone: "UTC",
|
|
1263
1378
|
});
|
|
1264
|
-
/** One line on the
|
|
1265
|
-
*
|
|
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
|
|
1266
1409
|
* reads as if this command recorded it, or recorded the provider it names. */
|
|
1267
1410
|
function switchingLine(summary, provider) {
|
|
1268
|
-
const cap = dollars(summary.maxMicros);
|
|
1269
1411
|
const sw = summary.switch;
|
|
1412
|
+
const welcome = welcomeLine(summary);
|
|
1270
1413
|
if (!sw) {
|
|
1271
|
-
if (!
|
|
1272
|
-
return
|
|
1273
|
-
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(" ");
|
|
1274
1422
|
}
|
|
1275
1423
|
const from = RIVAL_NAMES[sw.provider];
|
|
1276
|
-
const recorded = `This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}`;
|
|
1277
1424
|
const once = provider && provider !== sw.provider
|
|
1278
1425
|
? ` A switch is recorded once, so it stays ${from}, not ${RIVAL_NAMES[provider]}.`
|
|
1279
1426
|
: "";
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
return `${recorded}; its first top-up was matched by a referral instead, which is never less than the switching credit.${once}`;
|
|
1287
|
-
case "reversed":
|
|
1288
|
-
return `${recorded}; the top-up it matched was refunded, so the credit was taken back.${once}`;
|
|
1289
|
-
case "refused":
|
|
1290
|
-
return sw.refusedReason === "program_off"
|
|
1291
|
-
? `${recorded}; the switching credit was paused when its first top-up arrived.${once}`
|
|
1292
|
-
: `${recorded}; its first top-up was not matched. Ask support if you think that is wrong.${once}`;
|
|
1293
|
-
}
|
|
1427
|
+
return [
|
|
1428
|
+
`This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}.${once}`,
|
|
1429
|
+
welcome,
|
|
1430
|
+
]
|
|
1431
|
+
.filter(Boolean)
|
|
1432
|
+
.join(" ");
|
|
1294
1433
|
}
|
|
1295
1434
|
/** Running time in the largest unit that keeps it above zero: a short
|
|
1296
1435
|
* trial test is seconds, not "0 hours". */
|
|
@@ -1305,7 +1444,7 @@ function runningTime(seconds) {
|
|
|
1305
1444
|
});
|
|
1306
1445
|
return `${shown} ${unit}${shown === "1" ? "" : "s"}`;
|
|
1307
1446
|
}
|
|
1308
|
-
/** The sandboxes
|
|
1447
|
+
/** The sandboxes free usage paid for, said beside what was priced. */
|
|
1309
1448
|
function trialNote(trial, all, trialSeconds) {
|
|
1310
1449
|
if (!trial)
|
|
1311
1450
|
return "";
|
|
@@ -1314,7 +1453,7 @@ function trialNote(trial, all, trialSeconds) {
|
|
|
1314
1453
|
? "It"
|
|
1315
1454
|
: "All of them"
|
|
1316
1455
|
: `${trial} of them, ${runningTime(trialSeconds)} of that time,`;
|
|
1317
|
-
return ` ${which} ran on
|
|
1456
|
+
return ` ${which} ran on free usage, which charged nothing; Runtime's side prices ${trial === 1 ? "it" : "them"} at the standard rates, what the same work costs on paid credit.`;
|
|
1318
1457
|
}
|
|
1319
1458
|
async function switching(command, argv, env, out) {
|
|
1320
1459
|
const args = parse(argv, [], command === "compare" ? ["from", "days"] : ["from"]);
|
|
@@ -1330,10 +1469,13 @@ async function switching(command, argv, env, out) {
|
|
|
1330
1469
|
if (command === "switch") {
|
|
1331
1470
|
if (!from) {
|
|
1332
1471
|
const summary = await api.get();
|
|
1333
|
-
|
|
1334
|
-
(summary.
|
|
1335
|
-
|
|
1336
|
-
|
|
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);
|
|
1337
1479
|
return 0;
|
|
1338
1480
|
}
|
|
1339
1481
|
const provider = SWITCH_FROM[from];
|
|
@@ -1341,7 +1483,7 @@ async function switching(command, argv, env, out) {
|
|
|
1341
1483
|
throw unknown("provider", from, Object.keys(RIVAL_NAMES));
|
|
1342
1484
|
const summary = await api.record({ provider });
|
|
1343
1485
|
print([
|
|
1344
|
-
`Recorded: switching from ${RIVAL_NAMES[provider]}. ${
|
|
1486
|
+
`Recorded: switching from ${RIVAL_NAMES[provider]}. ${welcomeLine(summary) ?? ""}`.trim(),
|
|
1345
1487
|
...dropInLines(provider),
|
|
1346
1488
|
].join("\n"), summary);
|
|
1347
1489
|
return 0;
|
|
@@ -1459,9 +1601,9 @@ async function sandbox(argv, env, out) {
|
|
|
1459
1601
|
switch (verb) {
|
|
1460
1602
|
case "create": {
|
|
1461
1603
|
const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
|
|
1462
|
-
const
|
|
1463
|
-
|
|
1464
|
-
});
|
|
1604
|
+
const input = createInput(args);
|
|
1605
|
+
const onCapacityWait = waiting(out);
|
|
1606
|
+
const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
|
|
1465
1607
|
print(sbx.id, sbx.info);
|
|
1466
1608
|
return 0;
|
|
1467
1609
|
}
|
|
@@ -1486,24 +1628,21 @@ async function sandbox(argv, env, out) {
|
|
|
1486
1628
|
? null
|
|
1487
1629
|
: await runtime.sandboxes.list({ state: ["stopped"], limit: 50 });
|
|
1488
1630
|
const stopped = hidden?.data.length
|
|
1489
|
-
? `${hidden.data.length}${hidden.hasMore ? "+" : ""} stopped; runtime
|
|
1631
|
+
? `${hidden.data.length}${hidden.hasMore ? "+" : ""} stopped; runtime sandboxes ls --all shows them.`
|
|
1490
1632
|
: "";
|
|
1491
1633
|
const rows = all.map((s) => [
|
|
1492
1634
|
s.id,
|
|
1493
1635
|
s.info.name ?? "-",
|
|
1494
1636
|
s.state,
|
|
1495
1637
|
`${s.info.vcpu}/${s.info.memoryMiB}`,
|
|
1496
|
-
s.info
|
|
1497
|
-
// When it stops or pauses by itself (0381): its time limit's end, or
|
|
1498
|
-
// "-" when it never will. An older server sends no endsAt.
|
|
1499
|
-
(s.info.endsAt === undefined ? s.info.expiresAt : s.info.endsAt) ?? "-",
|
|
1638
|
+
sandboxEnd(s.info),
|
|
1500
1639
|
]);
|
|
1501
1640
|
print([
|
|
1502
1641
|
rows.length
|
|
1503
|
-
? table([["ID", "NAME", "STATE", "CPU/MIB", "
|
|
1642
|
+
? table([["ID", "NAME", "STATE", "CPU/MIB", "ENDS"], ...rows])
|
|
1504
1643
|
: stopped
|
|
1505
1644
|
? "No live sandboxes."
|
|
1506
|
-
: "No sandboxes. Create one: runtime
|
|
1645
|
+
: "No sandboxes. Create one: runtime sandboxes create",
|
|
1507
1646
|
stopped,
|
|
1508
1647
|
]
|
|
1509
1648
|
.filter(Boolean)
|
|
@@ -1518,31 +1657,31 @@ async function sandbox(argv, env, out) {
|
|
|
1518
1657
|
case "switch-image": {
|
|
1519
1658
|
const args = parse(rest, ["keep-workspace"], []);
|
|
1520
1659
|
const [id, image] = args.positional;
|
|
1521
|
-
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`));
|
|
1522
1661
|
if (!args.flags.has("keep-workspace"))
|
|
1523
|
-
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.`);
|
|
1524
1663
|
await sbx.switchImage(need(image, "an image, e.g. app:v2"), { keep: "workspace" });
|
|
1525
1664
|
print(describe(sbx.info), sbx.info);
|
|
1526
1665
|
return 0;
|
|
1527
1666
|
}
|
|
1528
1667
|
case "resize": {
|
|
1529
1668
|
const args = parse(rest, ["restart"], ["vcpu", "memory"]);
|
|
1530
|
-
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`));
|
|
1531
1670
|
const size = {
|
|
1532
1671
|
...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
|
|
1533
1672
|
...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
|
|
1534
1673
|
};
|
|
1535
1674
|
if (size.vcpu === undefined && size.memoryMiB === undefined)
|
|
1536
|
-
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`);
|
|
1537
1676
|
if (!args.flags.has("restart"))
|
|
1538
|
-
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.`);
|
|
1539
1678
|
await sbx.resize(size, { restart: true });
|
|
1540
1679
|
print(describe(sbx.info), sbx.info);
|
|
1541
1680
|
return 0;
|
|
1542
1681
|
}
|
|
1543
1682
|
case "rm":
|
|
1544
1683
|
case "delete": {
|
|
1545
|
-
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>`));
|
|
1546
1685
|
const gone = await sbx.delete();
|
|
1547
1686
|
print(`${gone.id} deleted`, gone);
|
|
1548
1687
|
return 0;
|
|
@@ -1644,15 +1783,15 @@ async function sandbox(argv, env, out) {
|
|
|
1644
1783
|
print(gone.left ? "Left the tailnet." : "Not on a tailnet.", gone);
|
|
1645
1784
|
return 0;
|
|
1646
1785
|
}
|
|
1647
|
-
throw usage("runtime
|
|
1786
|
+
throw usage("runtime sandboxes tailscale up|status|down <id>");
|
|
1648
1787
|
}
|
|
1649
1788
|
case "snapshot": {
|
|
1650
1789
|
const args = parse(rest, [], ["name", "retention"]);
|
|
1651
1790
|
const sbx = await get(args.positional[0]);
|
|
1652
1791
|
const snapshot = await sbx.snapshot({
|
|
1653
1792
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1654
|
-
...(
|
|
1655
|
-
? { retentionDays:
|
|
1793
|
+
...(daysOrCredit(args, "retention") !== undefined
|
|
1794
|
+
? { retentionDays: daysOrCredit(args, "retention") }
|
|
1656
1795
|
: {}),
|
|
1657
1796
|
});
|
|
1658
1797
|
print(snapshot.id, snapshot);
|
|
@@ -1724,26 +1863,26 @@ async function sandbox(argv, env, out) {
|
|
|
1724
1863
|
}
|
|
1725
1864
|
case "extend": {
|
|
1726
1865
|
const sbx = await get(rest[0]);
|
|
1727
|
-
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"));
|
|
1728
1867
|
await sbx.extend(seconds);
|
|
1729
|
-
print(sbx.info
|
|
1868
|
+
print(sandboxEnd(sbx.info) === "no limit"
|
|
1730
1869
|
? `${sbx.id} has no time limit: it runs while it works, so nothing was extended`
|
|
1731
|
-
: `${sbx.id} now ends at ${sbx.info
|
|
1870
|
+
: `${sbx.id} now ends at ${sandboxEnd(sbx.info)}`, sbx.info);
|
|
1732
1871
|
return 0;
|
|
1733
1872
|
}
|
|
1734
1873
|
case "exec": {
|
|
1735
|
-
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me}
|
|
1874
|
+
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandboxes exec <id> -- ls -la`);
|
|
1736
1875
|
const command = args.rest ?? args.positional.slice(1);
|
|
1737
1876
|
if (!command.length)
|
|
1738
|
-
throw usage("Give the command after --: runtime
|
|
1877
|
+
throw usage("Give the command after --: runtime sandboxes exec <id> -- ls -la");
|
|
1739
1878
|
const sbx = await use(args.positional[0]);
|
|
1740
1879
|
return execute(sbx, command, args, out, integer(args, "timeout"));
|
|
1741
1880
|
}
|
|
1742
1881
|
case "spawn": {
|
|
1743
|
-
const args = parse(rest, ["pty"], ["cwd", "env"], `${me}
|
|
1882
|
+
const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandboxes spawn <id> -- npm run dev`);
|
|
1744
1883
|
const command = args.rest ?? [];
|
|
1745
1884
|
if (!command.length)
|
|
1746
|
-
throw usage("Give the command after --: runtime
|
|
1885
|
+
throw usage("Give the command after --: runtime sandboxes spawn <id> -- npm run dev");
|
|
1747
1886
|
const sbx = await use(args.positional[0]);
|
|
1748
1887
|
const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
|
|
1749
1888
|
...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
|
|
@@ -1759,36 +1898,54 @@ async function sandbox(argv, env, out) {
|
|
|
1759
1898
|
print(list.length
|
|
1760
1899
|
? table([
|
|
1761
1900
|
["PROCESS", "STATE", "EXIT", "COMMAND"],
|
|
1762
|
-
|
|
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
|
+
]),
|
|
1763
1908
|
])
|
|
1764
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)}`);
|
|
1765
1913
|
return 0;
|
|
1766
1914
|
}
|
|
1767
1915
|
case "logs": {
|
|
1768
|
-
const args = parse(rest, ["f", "follow"], []);
|
|
1916
|
+
const args = parse(rest, ["f", "follow", "raw"], []);
|
|
1769
1917
|
const sbx = await use(args.positional[0]);
|
|
1770
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
|
+
];
|
|
1771
1924
|
if (has(args, "f") || has(args, "follow")) {
|
|
1772
1925
|
for await (const event of proc.output())
|
|
1773
1926
|
if (event.type === "stdout")
|
|
1774
|
-
|
|
1927
|
+
stdout(event.data);
|
|
1775
1928
|
else if (event.type === "stderr")
|
|
1776
|
-
|
|
1929
|
+
stderr(event.data);
|
|
1777
1930
|
else if (out.json)
|
|
1778
|
-
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));
|
|
1779
1934
|
return 0;
|
|
1780
1935
|
}
|
|
1781
1936
|
/* Without -f, what it has printed so far, then back to the prompt, as
|
|
1782
1937
|
`docker logs` does: a server's log is read, not followed forever. */
|
|
1783
1938
|
const so = await outputSoFar(runtime, sbx.id, proc.id);
|
|
1784
1939
|
if (out.json) {
|
|
1785
|
-
out.write(JSON.stringify(so));
|
|
1940
|
+
out.write(shown(JSON.stringify(so)));
|
|
1786
1941
|
return 0;
|
|
1787
1942
|
}
|
|
1788
1943
|
for (const chunk of so.chunks)
|
|
1789
|
-
(chunk.stream === "stderr" ?
|
|
1944
|
+
(chunk.stream === "stderr" ? stderr : stdout)(chunk.text);
|
|
1945
|
+
if (so.outOfMemory)
|
|
1946
|
+
out.error(outOfMemoryLines(so.outOfMemory));
|
|
1790
1947
|
if (so.state === "running")
|
|
1791
|
-
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`);
|
|
1792
1949
|
return 0;
|
|
1793
1950
|
}
|
|
1794
1951
|
case "kill": {
|
|
@@ -1858,7 +2015,7 @@ async function sandbox(argv, env, out) {
|
|
|
1858
2015
|
return runCode(await use(rest[0]), rest.slice(1), out);
|
|
1859
2016
|
case "preview": {
|
|
1860
2017
|
if (rest[0] === "rotate") {
|
|
1861
|
-
const port = portOf(rest[2], "runtime
|
|
2018
|
+
const port = portOf(rest[2], "runtime sandboxes preview rotate <id> 3000");
|
|
1862
2019
|
const sbx = await use(rest[1]);
|
|
1863
2020
|
const preview = await sbx.previews.rotate(port);
|
|
1864
2021
|
print(preview.visibility === "public"
|
|
@@ -1867,7 +2024,7 @@ async function sandbox(argv, env, out) {
|
|
|
1867
2024
|
return 0;
|
|
1868
2025
|
}
|
|
1869
2026
|
const args = parse(rest, ["public"], ["ttl"]);
|
|
1870
|
-
const port = portOf(args.positional[1], "runtime
|
|
2027
|
+
const port = portOf(args.positional[1], "runtime sandboxes preview <id> 3000");
|
|
1871
2028
|
const sbx = await use(args.positional[0]);
|
|
1872
2029
|
const preview = await sbx.previews.create(port, {
|
|
1873
2030
|
...(has(args, "public") ? { visibility: "public" } : {}),
|
|
@@ -1887,7 +2044,7 @@ async function sandbox(argv, env, out) {
|
|
|
1887
2044
|
["PORT", "VISIBILITY", "URL"],
|
|
1888
2045
|
...list.map((p) => [String(p.port), p.visibility, p.url]),
|
|
1889
2046
|
])
|
|
1890
|
-
: "No shared ports. Share one: runtime
|
|
2047
|
+
: "No shared ports. Share one: runtime sandboxes preview <id> 3000", list);
|
|
1891
2048
|
return 0;
|
|
1892
2049
|
}
|
|
1893
2050
|
case "session": {
|
|
@@ -1904,17 +2061,17 @@ async function sandbox(argv, env, out) {
|
|
|
1904
2061
|
one.origins.join(",") || "-",
|
|
1905
2062
|
]),
|
|
1906
2063
|
])
|
|
1907
|
-
: "No sessions. Make one: runtime
|
|
2064
|
+
: "No sessions. Make one: runtime sandboxes session create <id> --origin https://app.example.com", list);
|
|
1908
2065
|
return 0;
|
|
1909
2066
|
}
|
|
1910
2067
|
if (verb === "revoke") {
|
|
1911
|
-
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>");
|
|
1912
2069
|
const ended = await (await use(rest[1])).sessions.revoke(sessionId);
|
|
1913
2070
|
print(`Session ${ended.id} ended.`, ended);
|
|
1914
2071
|
return 0;
|
|
1915
2072
|
}
|
|
1916
2073
|
if (verb !== "create")
|
|
1917
|
-
throw usage("runtime
|
|
2074
|
+
throw usage("runtime sandboxes session create <id> [--origin <url>]..., session ls <id>, or session revoke <id> <session-id>");
|
|
1918
2075
|
const args = parse(rest.slice(1), [], ["origin", "ttl", "name"]);
|
|
1919
2076
|
const origins = args.flags.get("origin") ?? [];
|
|
1920
2077
|
const ttl = integer(args, "ttl");
|
|
@@ -1928,7 +2085,7 @@ async function sandbox(argv, env, out) {
|
|
|
1928
2085
|
return 0;
|
|
1929
2086
|
}
|
|
1930
2087
|
case "unshare": {
|
|
1931
|
-
const port = portOf(rest[1], "runtime
|
|
2088
|
+
const port = portOf(rest[1], "runtime sandboxes unshare <id> 3000");
|
|
1932
2089
|
const sbx = await use(rest[0]);
|
|
1933
2090
|
print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
|
|
1934
2091
|
return 0;
|
|
@@ -2126,7 +2283,7 @@ read = readSecret) {
|
|
|
2126
2283
|
}
|
|
2127
2284
|
if (!hosts.length) {
|
|
2128
2285
|
const saved = await runtime.secrets.set(name, { value, jobs: true });
|
|
2129
|
-
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);
|
|
2130
2287
|
return 0;
|
|
2131
2288
|
}
|
|
2132
2289
|
const header = flag(args, "header");
|
|
@@ -2145,14 +2302,14 @@ read = readSecret) {
|
|
|
2145
2302
|
`Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
|
|
2146
2303
|
saved.header
|
|
2147
2304
|
? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
|
|
2148
|
-
: `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.`,
|
|
2149
2306
|
...(saved.rules?.length ? [`Only these requests get it: ${ruleText(saved.rules)}.`] : []),
|
|
2150
2307
|
...(saved.enforced
|
|
2151
2308
|
? []
|
|
2152
2309
|
: ["A host is catching up; running sandboxes have it within a minute."]),
|
|
2153
2310
|
...(saved.jobs
|
|
2154
2311
|
? [
|
|
2155
|
-
`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.`,
|
|
2156
2313
|
]
|
|
2157
2314
|
: []),
|
|
2158
2315
|
].join("\n"), saved);
|
|
@@ -2291,7 +2448,7 @@ async function desktop(sbx, argv, out) {
|
|
|
2291
2448
|
return 0;
|
|
2292
2449
|
}
|
|
2293
2450
|
}
|
|
2294
|
-
throw usage(`${me}
|
|
2451
|
+
throw usage(`${me} sandboxes desktop <id> start|stop|screenshot|open|click|type|press|windows|record`);
|
|
2295
2452
|
}
|
|
2296
2453
|
async function browser(sbx, argv, out) {
|
|
2297
2454
|
const [action, ...rest] = argv;
|
|
@@ -2305,7 +2462,8 @@ async function browser(sbx, argv, out) {
|
|
|
2305
2462
|
...(integer(args, "height") !== undefined ? { height: integer(args, "height") } : {}),
|
|
2306
2463
|
});
|
|
2307
2464
|
print(`${started.version}${started.headless ? "" : `, on the desktop: ${started.streamUrl}`}\n` +
|
|
2308
|
-
`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);
|
|
2309
2467
|
return 0;
|
|
2310
2468
|
}
|
|
2311
2469
|
case "get":
|
|
@@ -2320,7 +2478,7 @@ async function browser(sbx, argv, out) {
|
|
|
2320
2478
|
return 0;
|
|
2321
2479
|
}
|
|
2322
2480
|
}
|
|
2323
|
-
throw usage(`${me}
|
|
2481
|
+
throw usage(`${me} sandboxes browser <id> start [--headed]|get|stop`);
|
|
2324
2482
|
}
|
|
2325
2483
|
const KEYS_HELP = `runtime keys <command>
|
|
2326
2484
|
|
|
@@ -2493,14 +2651,14 @@ async function keys(argv, env, out) {
|
|
|
2493
2651
|
await confirm();
|
|
2494
2652
|
return 0;
|
|
2495
2653
|
}
|
|
2496
|
-
const SNAPSHOT_HELP = `runtime
|
|
2654
|
+
const SNAPSHOT_HELP = `runtime snapshots <command>
|
|
2497
2655
|
|
|
2498
2656
|
ls [--sandbox <id>] [--name n] Your snapshots
|
|
2499
2657
|
get <id> One snapshot
|
|
2500
|
-
extend <id> <days>
|
|
2658
|
+
extend <id> <days|credit> Keep it longer; credit: while you have credit
|
|
2501
2659
|
rm <id> Delete it
|
|
2502
|
-
Take one with \`runtime
|
|
2503
|
-
with \`runtime
|
|
2660
|
+
Take one with \`runtime sandboxes snapshot <sandboxId>\`; start a sandbox from it
|
|
2661
|
+
with \`runtime sandboxes create --snapshot <snapshotId>\`.
|
|
2504
2662
|
`;
|
|
2505
2663
|
async function snapshot(argv, env, out) {
|
|
2506
2664
|
const [verb, ...rest] = argv;
|
|
@@ -2523,9 +2681,15 @@ async function snapshot(argv, env, out) {
|
|
|
2523
2681
|
print(all.length
|
|
2524
2682
|
? table([
|
|
2525
2683
|
["ID", "NAME", "STATUS", "FROM", "EXPIRES"],
|
|
2526
|
-
...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
|
+
]),
|
|
2527
2691
|
])
|
|
2528
|
-
: "No snapshots. Take one: runtime
|
|
2692
|
+
: "No snapshots. Take one: runtime sandboxes snapshot <sandboxId>", all);
|
|
2529
2693
|
return 0;
|
|
2530
2694
|
}
|
|
2531
2695
|
case "get": {
|
|
@@ -2534,9 +2698,11 @@ async function snapshot(argv, env, out) {
|
|
|
2534
2698
|
return 0;
|
|
2535
2699
|
}
|
|
2536
2700
|
case "extend": {
|
|
2537
|
-
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")));
|
|
2538
2704
|
const found = await api.extend(need(rest[0], "a snapshot id"), days);
|
|
2539
|
-
print(`${found.id} kept until ${found.expiresAt}`, found);
|
|
2705
|
+
print(`${found.id} kept ${found.expiresAt === null ? "while you have credit" : `until ${found.expiresAt}`}`, found);
|
|
2540
2706
|
return 0;
|
|
2541
2707
|
}
|
|
2542
2708
|
case "rm":
|
|
@@ -2549,7 +2715,7 @@ async function snapshot(argv, env, out) {
|
|
|
2549
2715
|
}
|
|
2550
2716
|
throw unknown("snapshot command", verb, ["ls", "get", "extend", "rm"]);
|
|
2551
2717
|
}
|
|
2552
|
-
const IMAGE_HELP = `runtime
|
|
2718
|
+
const IMAGE_HELP = `runtime images <command>
|
|
2553
2719
|
|
|
2554
2720
|
build [<folder>] [--dockerfile|-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
|
|
2555
2721
|
[--name <name>] [--tag|-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
|
|
@@ -2564,8 +2730,9 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
2564
2730
|
ls Your images
|
|
2565
2731
|
versions <name> Every version of a name, newest first
|
|
2566
2732
|
get <image> One image: an id, name, name:tag or name@version
|
|
2567
|
-
logs <image> [--follow]
|
|
2568
|
-
Its build log
|
|
2733
|
+
logs <image> [--follow] [--raw]
|
|
2734
|
+
Its build log; on a terminal control characters
|
|
2735
|
+
show as \\uXXXX unless --raw
|
|
2569
2736
|
tag <image> <tag> Point a tag of the image's name at this version
|
|
2570
2737
|
untag <image> <tag> Take a tag off
|
|
2571
2738
|
rm <image> Delete that version and its tags
|
|
@@ -2576,7 +2743,7 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
2576
2743
|
registry set <ecr registry> --access-key-id <id>
|
|
2577
2744
|
Amazon ECR: the secret access key is read from standard input
|
|
2578
2745
|
registry rm <registry>
|
|
2579
|
-
Use one: runtime
|
|
2746
|
+
Use one: runtime sandboxes create --image app:latest
|
|
2580
2747
|
`;
|
|
2581
2748
|
/** Options `image build` takes with a value. */
|
|
2582
2749
|
const IMAGE_BUILD_VALUES = [
|
|
@@ -2711,7 +2878,7 @@ read = readSecret) {
|
|
|
2711
2878
|
])
|
|
2712
2879
|
: name
|
|
2713
2880
|
? `No versions of ${name}.`
|
|
2714
|
-
: "No images. Build one: runtime
|
|
2881
|
+
: "No images. Build one: runtime images build --pip pandas", all);
|
|
2715
2882
|
return 0;
|
|
2716
2883
|
}
|
|
2717
2884
|
case "get": {
|
|
@@ -2720,13 +2887,14 @@ read = readSecret) {
|
|
|
2720
2887
|
return 0;
|
|
2721
2888
|
}
|
|
2722
2889
|
case "logs": {
|
|
2723
|
-
const args = parse(rest, ["follow", "f"], []);
|
|
2890
|
+
const args = parse(rest, ["follow", "f", "raw"], []);
|
|
2724
2891
|
const id = (await api.resolve(need(args.positional[0], "an image"))).id;
|
|
2892
|
+
const shown = logText(args);
|
|
2725
2893
|
if (has(args, "follow") || has(args, "f")) {
|
|
2726
2894
|
const lines = [];
|
|
2727
|
-
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)));
|
|
2728
2896
|
if (out.json)
|
|
2729
|
-
|
|
2897
|
+
out.write(shown(JSON.stringify({ lines, state: done.state })));
|
|
2730
2898
|
return done.state === "failed" ? 1 : 0;
|
|
2731
2899
|
}
|
|
2732
2900
|
let after = 0;
|
|
@@ -2738,7 +2906,7 @@ read = readSecret) {
|
|
|
2738
2906
|
break;
|
|
2739
2907
|
after = page.nextAfter;
|
|
2740
2908
|
}
|
|
2741
|
-
|
|
2909
|
+
out.write(shown(out.json ? JSON.stringify(lines) : lines.join("\n") || "(no log yet)"));
|
|
2742
2910
|
return 0;
|
|
2743
2911
|
}
|
|
2744
2912
|
case "tag":
|
|
@@ -2765,7 +2933,7 @@ read = readSecret) {
|
|
|
2765
2933
|
["REGISTRY", "KIND", "USER", "SINCE"],
|
|
2766
2934
|
...all.map((r) => [r.registry, r.kind, r.username, r.createdAt]),
|
|
2767
2935
|
])
|
|
2768
|
-
: "No registry credentials. Store some: runtime
|
|
2936
|
+
: "No registry credentials. Store some: runtime images registry set ghcr.io --username you", all);
|
|
2769
2937
|
return 0;
|
|
2770
2938
|
}
|
|
2771
2939
|
if (action === "set") {
|
|
@@ -2805,10 +2973,10 @@ read = readSecret) {
|
|
|
2805
2973
|
"registry",
|
|
2806
2974
|
]);
|
|
2807
2975
|
}
|
|
2808
|
-
const JOB_HELP = `runtime
|
|
2976
|
+
const JOB_HELP = `runtime jobs <command>
|
|
2809
2977
|
|
|
2810
2978
|
A job runs a command in a fresh sandbox, once or on a schedule. Each run spends
|
|
2811
|
-
the
|
|
2979
|
+
the included usage first, then credit at the sandbox rates.
|
|
2812
2980
|
|
|
2813
2981
|
create <name> (--cron "<min hour day month weekday>" [--timezone Europe/Berlin] | --at <time|now>)
|
|
2814
2982
|
[--vcpu 2] [--memory 4096] [--disk 4096] [--cpu shared|reserved] [--cpu-floor <thousandths>]
|
|
@@ -2823,14 +2991,21 @@ const JOB_HELP = `runtime job <command>
|
|
|
2823
2991
|
ls Your jobs: state, schedule and next run
|
|
2824
2992
|
get <id> One job, in full
|
|
2825
2993
|
runs <id> Its runs: state, exit code, when
|
|
2826
|
-
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
|
|
2827
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
|
|
2828
3002
|
cancel <id> End it for good and stop a run in progress
|
|
2829
3003
|
|
|
2830
3004
|
Examples
|
|
2831
|
-
runtime
|
|
2832
|
-
runtime
|
|
2833
|
-
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
|
|
2834
3009
|
`;
|
|
2835
3010
|
const when = (ms) => (ms == null ? "-" : new Date(ms).toISOString());
|
|
2836
3011
|
function scheduleText(job) {
|
|
@@ -2878,13 +3053,13 @@ async function jobCommand(argv, env, out) {
|
|
|
2878
3053
|
"max-cost",
|
|
2879
3054
|
"max-total-cost",
|
|
2880
3055
|
"region",
|
|
2881
|
-
], `${me}
|
|
3056
|
+
], `${me} jobs create <name> --cron "0 3 * * *" -- python3 -c '...'`);
|
|
2882
3057
|
const name = need(args.positional[0], "the job's name, such as nightly-report");
|
|
2883
3058
|
if (args.positional.length > 1)
|
|
2884
|
-
throw usage(`Put the command after --: ${me}
|
|
3059
|
+
throw usage(`Put the command after --: ${me} jobs create ${name} ... -- ${args.positional.slice(1).join(" ")}`);
|
|
2885
3060
|
const argv = args.rest ?? [];
|
|
2886
3061
|
if (!argv.length)
|
|
2887
|
-
throw usage(`Give the command after --: ${me}
|
|
3062
|
+
throw usage(`Give the command after --: ${me} jobs create ${name} --at now -- echo hello`);
|
|
2888
3063
|
const cron = flag(args, "cron");
|
|
2889
3064
|
const at = flag(args, "at");
|
|
2890
3065
|
if ((cron === undefined) === (at === undefined))
|
|
@@ -2941,7 +3116,7 @@ async function jobCommand(argv, env, out) {
|
|
|
2941
3116
|
: {}),
|
|
2942
3117
|
});
|
|
2943
3118
|
if (!out.json)
|
|
2944
|
-
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`);
|
|
2945
3120
|
print(job.id, job);
|
|
2946
3121
|
return 0;
|
|
2947
3122
|
}
|
|
@@ -2961,7 +3136,7 @@ async function jobCommand(argv, env, out) {
|
|
|
2961
3136
|
j.blockedReason ?? "-",
|
|
2962
3137
|
]),
|
|
2963
3138
|
])
|
|
2964
|
-
: `No jobs. Schedule one: ${me}
|
|
3139
|
+
: `No jobs. Schedule one: ${me} jobs create hello --at now -- echo hello`, all);
|
|
2965
3140
|
return 0;
|
|
2966
3141
|
}
|
|
2967
3142
|
case "get": {
|
|
@@ -2990,8 +3165,13 @@ async function jobCommand(argv, env, out) {
|
|
|
2990
3165
|
return 0;
|
|
2991
3166
|
}
|
|
2992
3167
|
case "logs": {
|
|
2993
|
-
const args = parse(rest, ["f", "follow"], []);
|
|
3168
|
+
const args = parse(rest, ["f", "follow", "raw"], []);
|
|
2994
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
|
+
];
|
|
2995
3175
|
let runId = id;
|
|
2996
3176
|
try {
|
|
2997
3177
|
await api.run(id);
|
|
@@ -3019,25 +3199,27 @@ async function jobCommand(argv, env, out) {
|
|
|
3019
3199
|
for (;;) {
|
|
3020
3200
|
const page = await api.logs(runId, { cursor, limitBytes: 65536 });
|
|
3021
3201
|
if (out.json)
|
|
3022
|
-
out.write(JSON.stringify(page));
|
|
3202
|
+
out.write(shown(JSON.stringify(page)));
|
|
3023
3203
|
else
|
|
3024
3204
|
for (const chunk of page.chunks)
|
|
3025
|
-
(chunk.stream === "stderr" ?
|
|
3205
|
+
(chunk.stream === "stderr" ? stderr : stdout)(chunk.text);
|
|
3026
3206
|
cursor = page.nextCursor;
|
|
3027
3207
|
if (page.complete) {
|
|
3208
|
+
if (!follow && out.json)
|
|
3209
|
+
return 0;
|
|
3210
|
+
const run = await api.run(runId);
|
|
3028
3211
|
if (!out.json) {
|
|
3029
|
-
const run = await api.run(runId);
|
|
3030
3212
|
if (page.truncated)
|
|
3031
3213
|
process.stderr.write("[the run printed more than it keeps; this is its last part]\n");
|
|
3032
3214
|
process.stderr.write(`[${run.state}${run.exitCode === null ? "" : `, exit code ${run.exitCode}`}${run.reason ? `, ${run.reason}` : ""}]\n`);
|
|
3033
3215
|
}
|
|
3034
|
-
return 0;
|
|
3216
|
+
return follow ? (run.exitCode ?? (run.state === "succeeded" ? 0 : 1)) : 0;
|
|
3035
3217
|
}
|
|
3036
3218
|
if (page.chunks.length)
|
|
3037
3219
|
continue;
|
|
3038
3220
|
if (!follow) {
|
|
3039
3221
|
if (!out.json)
|
|
3040
|
-
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`);
|
|
3041
3223
|
return 0;
|
|
3042
3224
|
}
|
|
3043
3225
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -3051,6 +3233,27 @@ async function jobCommand(argv, env, out) {
|
|
|
3051
3233
|
print(`${job.name} ${job.state}.`, job);
|
|
3052
3234
|
return 0;
|
|
3053
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
|
+
}
|
|
3054
3257
|
}
|
|
3055
3258
|
throw unknown("job command", verb, [
|
|
3056
3259
|
"create",
|
|
@@ -3060,12 +3263,15 @@ async function jobCommand(argv, env, out) {
|
|
|
3060
3263
|
"logs",
|
|
3061
3264
|
"pause",
|
|
3062
3265
|
"resume",
|
|
3266
|
+
"trigger",
|
|
3267
|
+
"reschedule",
|
|
3063
3268
|
"cancel",
|
|
3064
3269
|
]);
|
|
3065
3270
|
}
|
|
3066
|
-
const VOLUME_HELP = `runtime
|
|
3271
|
+
const VOLUME_HELP = `runtime volumes <command>
|
|
3067
3272
|
|
|
3068
|
-
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
|
|
3069
3275
|
ls Your volumes
|
|
3070
3276
|
get <id> One volume, its backups and where it is attached
|
|
3071
3277
|
resize <id> --size-mib <N> Grow a detached volume on its current host
|
|
@@ -3080,7 +3286,7 @@ const VOLUME_HELP = `runtime volume <command>
|
|
|
3080
3286
|
backup-rm <backupId> Delete a backup
|
|
3081
3287
|
backup-policy <id> [--daily on|off] [--retention-days N]
|
|
3082
3288
|
Daily backups on or off, and how long each is kept
|
|
3083
|
-
Attach one: runtime
|
|
3289
|
+
Attach one: runtime sandboxes create --volume <id>:/data
|
|
3084
3290
|
Volumes are backed up off their host daily unless you turn it off.
|
|
3085
3291
|
Shared commands work where shared disks are enabled.
|
|
3086
3292
|
`;
|
|
@@ -3094,10 +3300,11 @@ async function volume(argv, env, out) {
|
|
|
3094
3300
|
const api = (await client(env)).volumes;
|
|
3095
3301
|
switch (verb) {
|
|
3096
3302
|
case "create": {
|
|
3097
|
-
const args = parse(rest, ["shared"], ["size-mib", "name"]);
|
|
3303
|
+
const args = parse(rest, ["shared"], ["size-mib", "name", "label"]);
|
|
3098
3304
|
const created = await api.create({
|
|
3099
3305
|
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
3100
3306
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
3307
|
+
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
3101
3308
|
...(has(args, "shared") ? { shared: true } : {}),
|
|
3102
3309
|
});
|
|
3103
3310
|
print(created.id, created);
|
|
@@ -3118,7 +3325,7 @@ async function volume(argv, env, out) {
|
|
|
3118
3325
|
v.attachments.map((a) => `${a.sandboxId}:${a.path}`).join(", ") || "-",
|
|
3119
3326
|
]),
|
|
3120
3327
|
])
|
|
3121
|
-
: "No volumes. Create one: runtime
|
|
3328
|
+
: "No volumes. Create one: runtime volumes create --size-mib 10240", all);
|
|
3122
3329
|
return 0;
|
|
3123
3330
|
}
|
|
3124
3331
|
case "get": {
|
|
@@ -3130,7 +3337,7 @@ async function volume(argv, env, out) {
|
|
|
3130
3337
|
const args = parse(rest, [], ["size-mib"]);
|
|
3131
3338
|
const id = need(args.positional[0], "a volume id");
|
|
3132
3339
|
if (args.positional.length !== 1 || args.rest)
|
|
3133
|
-
throw usage(`Use ${me}
|
|
3340
|
+
throw usage(`Use ${me} volumes resize <id> --size-mib <N>.`);
|
|
3134
3341
|
const sizeMiB = integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>"));
|
|
3135
3342
|
const resized = await api.resize(id, { sizeMiB });
|
|
3136
3343
|
print(describeRecord(resized, table), resized);
|
|
@@ -3142,7 +3349,7 @@ async function volume(argv, env, out) {
|
|
|
3142
3349
|
const sandboxId = need(args.positional[1], "a sandbox id");
|
|
3143
3350
|
const path = need(flag(args, "path"), "--path /data");
|
|
3144
3351
|
if (args.positional.length !== 2 || args.rest)
|
|
3145
|
-
throw usage(`Use ${me}
|
|
3352
|
+
throw usage(`Use ${me} volumes attach <id> <sandboxId> --path /data.`);
|
|
3146
3353
|
const attachment = await api.attach(id, { sandboxId, path });
|
|
3147
3354
|
print(describeRecord(attachment, table), attachment);
|
|
3148
3355
|
return attachment.error ? 1 : 0;
|
|
@@ -3153,7 +3360,7 @@ async function volume(argv, env, out) {
|
|
|
3153
3360
|
const id = need(args.positional[0], "a volume id");
|
|
3154
3361
|
const attachmentId = need(args.positional[1], "an attachment id");
|
|
3155
3362
|
if (args.positional.length !== 2 || args.rest)
|
|
3156
|
-
throw usage(`Use ${me}
|
|
3363
|
+
throw usage(`Use ${me} volumes ${verb} <id> <attachmentId>.`);
|
|
3157
3364
|
const attachment = verb === "detach"
|
|
3158
3365
|
? await api.detach(id, attachmentId)
|
|
3159
3366
|
: await api.getAttachment(id, attachmentId);
|
|
@@ -3162,7 +3369,8 @@ async function volume(argv, env, out) {
|
|
|
3162
3369
|
}
|
|
3163
3370
|
case "rm":
|
|
3164
3371
|
case "delete": {
|
|
3165
|
-
const
|
|
3372
|
+
const id = need(rest[0], "a volume id");
|
|
3373
|
+
const found = await withVolumeRelease((options) => api.delete(id, options), out);
|
|
3166
3374
|
print(`${found.id} ${found.state}`, found);
|
|
3167
3375
|
return 0;
|
|
3168
3376
|
}
|
|
@@ -3172,7 +3380,7 @@ async function volume(argv, env, out) {
|
|
|
3172
3380
|
const days = integer(args, "retention-days");
|
|
3173
3381
|
const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
|
|
3174
3382
|
if (!out.json && made.state === "pending")
|
|
3175
|
-
out.error(`Copying it off the host; check with: ${me}
|
|
3383
|
+
out.error(`Copying it off the host; check with: ${me} volumes backups ${id}`);
|
|
3176
3384
|
print(made.id, made);
|
|
3177
3385
|
return 0;
|
|
3178
3386
|
}
|
|
@@ -3200,7 +3408,7 @@ async function volume(argv, env, out) {
|
|
|
3200
3408
|
const args = parse(rest, [], ["name"]);
|
|
3201
3409
|
const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
|
|
3202
3410
|
if (!out.json && created.state === "creating")
|
|
3203
|
-
out.error(`Restoring; check with: ${me}
|
|
3411
|
+
out.error(`Restoring; check with: ${me} volumes get ${created.id}`);
|
|
3204
3412
|
print(created.id, created);
|
|
3205
3413
|
return 0;
|
|
3206
3414
|
}
|
|
@@ -3254,17 +3462,52 @@ const CREATE_FLAGS = [
|
|
|
3254
3462
|
"get-or-create",
|
|
3255
3463
|
];
|
|
3256
3464
|
/** Says on standard error, once for each reason, why a create has not
|
|
3257
|
-
* answered yet: it is waiting for a
|
|
3465
|
+
* answered yet: it is waiting for a slot without credit or room, which can take up to
|
|
3258
3466
|
* two minutes, and silence reads as a hang. */
|
|
3259
3467
|
function waiting(out) {
|
|
3260
3468
|
const said = new Set();
|
|
3261
3469
|
return (refusal) => {
|
|
3470
|
+
// The CLI's volume wait owns this refusal, including its deadline.
|
|
3471
|
+
if (refusal.code === "volume_releasing")
|
|
3472
|
+
throw refusal;
|
|
3262
3473
|
if (said.has(refusal.code))
|
|
3263
3474
|
return;
|
|
3264
3475
|
said.add(refusal.code);
|
|
3265
3476
|
out.error(`${refusal.message} Waiting for room, up to 2 minutes; Ctrl-C stops.`);
|
|
3266
3477
|
};
|
|
3267
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
|
+
}
|
|
3268
3511
|
/** Options `sandbox create` and `run` take with a value. */
|
|
3269
3512
|
const CREATE_VALUES = [
|
|
3270
3513
|
"name",
|
|
@@ -3305,6 +3548,9 @@ function usdMicros(args, name) {
|
|
|
3305
3548
|
}
|
|
3306
3549
|
/** A create body from the command line's flags. */
|
|
3307
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.`);
|
|
3308
3554
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
3309
3555
|
return {
|
|
3310
3556
|
...(flag(args, "image") ? { image: flag(args, "image") } : {}),
|
|
@@ -3339,7 +3585,7 @@ function createInput(args) {
|
|
|
3339
3585
|
? { timeoutSeconds: integer(args, "timeout") }
|
|
3340
3586
|
: {}),
|
|
3341
3587
|
...(flag(args, "on-timeout")
|
|
3342
|
-
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
|
|
3588
|
+
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop", "delete"]) }
|
|
3343
3589
|
: {}),
|
|
3344
3590
|
...(integer(args, "idle-pause") !== undefined
|
|
3345
3591
|
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
@@ -3401,6 +3647,9 @@ ownInterrupt = true) {
|
|
|
3401
3647
|
// (Sandbox.exec); `--json` answers the result and prints no such line.
|
|
3402
3648
|
else if (result.timedOut)
|
|
3403
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);
|
|
3404
3653
|
// Never drop output silently: say so, and do not exit 0.
|
|
3405
3654
|
const lost = extras.lostOutput(result, sbx.id);
|
|
3406
3655
|
if (lost && !out.json)
|