withruntime 0.11.2 → 0.14.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 +11 -10
- package/README.src.md +10 -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 +85 -5
- 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 +15 -6
- package/dist/blaxel/sandbox.js +64 -8
- 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-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 +22 -3
- package/dist/cli.js +647 -273
- 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/credentials.d.ts +3 -0
- package/dist/credentials.js +15 -0
- package/dist/daytona/computer-use.d.ts +13 -4
- package/dist/daytona/computer-use.js +66 -8
- package/dist/daytona/daytona.d.ts +7 -1
- package/dist/daytona/daytona.js +31 -4
- 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 +28 -22
- package/dist/e2b/sandbox.js +34 -8
- package/dist/e2b/unsupported.js +1 -1
- package/dist/errors.d.ts +11 -2
- package/dist/errors.js +17 -5
- 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 +3 -1
- 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 +27 -4
- package/dist/machine.js +46 -14
- package/dist/mcp.js +144 -20
- 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/api-keys.d.ts +73 -0
- package/dist/products/api-keys.js +23 -0
- package/dist/products/apps.d.ts +157 -0
- package/dist/products/apps.js +60 -0
- package/dist/products/balancers.d.ts +87 -0
- package/dist/products/balancers.js +8 -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 +156 -0
- package/dist/products/builds.js +227 -0
- package/dist/products/index.d.ts +5 -16
- package/dist/products/index.js +5 -14
- package/dist/products/jobs.d.ts +19 -2
- package/dist/products/jobs.js +28 -11
- package/dist/products/logs.d.ts +245 -0
- package/dist/products/logs.js +274 -0
- package/dist/products/network-products.d.ts +38 -7
- package/dist/products/network-products.js +31 -18
- package/dist/products/observability.d.ts +8 -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/resources.d.ts +54 -0
- package/dist/products/resources.js +122 -0
- package/dist/products/secrets.d.ts +29 -3
- package/dist/products/secrets.js +47 -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 +6 -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 +7 -2
- package/dist/sandbox.js +41 -11
- 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 -1
- package/dist/transport.js +80 -6
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +71 -4
- package/dist/vercel/sandbox.d.ts +1 -1
- package/dist/vercel/sandbox.js +6 -4
- package/dist/vercel/snapshot.js +10 -2
- package/dist/volumes/index.d.ts +9 -0
- package/dist/volumes/index.js +10 -0
- package/dist/wallet.d.ts +4 -1
- package/dist/wallet.js +2 -1
- package/package.json +48 -14
package/dist/cli.js
CHANGED
|
@@ -11,21 +11,27 @@ 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
|
|
26
30
|
login [--agent-name <name>] [--no-browser] [--wait]
|
|
27
31
|
Connect this machine (no key to copy)
|
|
28
32
|
login --with-key Or paste a key from withruntime.com/account/keys
|
|
33
|
+
billing topup <usd> --new-account --accept-terms [--wait]
|
|
34
|
+
No person? Open an account paid in USDC
|
|
29
35
|
logout Revoke this machine's connection
|
|
30
36
|
whoami The organization and agent you act as
|
|
31
37
|
account [ls] The accounts this machine is connected to
|
|
@@ -36,16 +42,22 @@ Account
|
|
|
36
42
|
The account's audit log (owner or admin keys)
|
|
37
43
|
sso Single sign-on and SCIM directory sync (owner or
|
|
38
44
|
admin keys); an owner changes it on the website
|
|
39
|
-
usage [--csv] Balance,
|
|
45
|
+
usage [--csv] Balance, free usage and charges; --csv, each resource
|
|
40
46
|
usage export --since <UTC time> --until <UTC time>
|
|
41
47
|
Every settlement in the range as CSV; --json, each page
|
|
42
48
|
limits Whether this key is read-only, and its daily spending limit
|
|
43
49
|
secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
|
|
44
50
|
[--allow '[GET,HEAD] /path/*']... Store a secret sandboxes use without seeing it; the
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
[--only-label key=value[,k=v]]... value is read from standard input, never an argument.
|
|
52
|
+
[--only-sandbox <id>]... --allow (paid): only these methods and paths get it.
|
|
53
|
+
[--all-sandboxes] --only-label: only sandboxes with those labels (each
|
|
54
|
+
flag one selector, any may match); --only-sandbox:
|
|
55
|
+
only that sandbox. A new value keeps the scope;
|
|
56
|
+
--all-sandboxes gives it to every sandbox again
|
|
47
57
|
secrets set <NAME> --jobs [--host ...] A copy scheduled jobs put in their environment
|
|
48
58
|
(job create --secret NAME); with --host, both
|
|
59
|
+
secrets scope <NAME> --only-label k=v... | --only-sandbox <id>... | --all-sandboxes
|
|
60
|
+
Change which sandboxes get it; no value needed
|
|
49
61
|
secrets ls | get <NAME> Names, hosts, placeholders and jobs versions, never values
|
|
50
62
|
secrets rotate <NAME> A new version of the jobs copy, from standard input
|
|
51
63
|
secrets reveal <NAME> [--version N] Print the jobs copy's value. The sandboxes' copy is
|
|
@@ -53,13 +65,15 @@ Account
|
|
|
53
65
|
secrets rm <NAME> Delete a secret, every copy
|
|
54
66
|
keys create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide]
|
|
55
67
|
A new API key for CI, approved by an owner in the browser
|
|
56
|
-
referrals Your referral link: you both get up to $
|
|
68
|
+
referrals Your referral link: you both get up to $1,000, then
|
|
69
|
+
you earn 10% of what they spend for 12 months
|
|
57
70
|
compare --from <provider> [--days 30] What your usage would cost at e2b, daytona, vercel,
|
|
58
71
|
modal, cloudflare, fly, blaxel, lambda-microvms,
|
|
59
72
|
freestyle, prime and others, and what you save
|
|
60
73
|
switch [--from <provider>] Moving from e2b, daytona, vercel, modal, cloudflare, fly
|
|
61
|
-
or blaxel: record it before your first top-up
|
|
62
|
-
|
|
74
|
+
or blaxel: record it before your first top-up.
|
|
75
|
+
Credit you add within 30 days of signing up is
|
|
76
|
+
matched, up to $1,000 in total
|
|
63
77
|
ls Everything you run, every product
|
|
64
78
|
feedback "<text>" [--kind bug|missing_feature|competitor_gap|migration_blocker|docs|pricing|praise|other]
|
|
65
79
|
[--detail <text>] [--competitor <name>] [--request-id <req_...>]
|
|
@@ -72,13 +86,13 @@ Account
|
|
|
72
86
|
mcp Serve MCP on stdio for agents that need it
|
|
73
87
|
|
|
74
88
|
Connect
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
sandboxes ssh <id|name> [-- <command...>] Log in over SSH; \`runtime sandboxes ssh help\` for editors
|
|
90
|
+
sandboxes ssh config [--install] Make \`ssh <id|name>.runtime\` work (VS Code, JetBrains)
|
|
91
|
+
sandboxes port-forward <id|name> <port|local:remote>...
|
|
78
92
|
Forward local ports to ports in a sandbox
|
|
79
93
|
|
|
80
94
|
Observability
|
|
81
|
-
|
|
95
|
+
sandboxes metrics <id> [--range 1h] CPU and memory over time (15m 1h 6h 24h 7d 30d)
|
|
82
96
|
events [--sandbox <id>] [--type <type>] Lifecycle events, newest first
|
|
83
97
|
webhooks ls | create <url> [--events a,b] | test <id> | deliveries <id>
|
|
84
98
|
| update <id> [--url u] [--events a,b] [--enable|--disable]
|
|
@@ -88,22 +102,23 @@ Observability
|
|
|
88
102
|
Events and metrics to an OpenTelemetry endpoint
|
|
89
103
|
|
|
90
104
|
Products
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Each has its own help: \`runtime
|
|
105
|
+
sandboxes Linux microVMs: exec, shell, files, code, previews, network, desktop, browser
|
|
106
|
+
snapshots Saved sandboxes, disk and memory, to start or fork from
|
|
107
|
+
images Custom images from a Dockerfile, any public or private image, or a package list
|
|
108
|
+
volumes Persistent disks to attach to sandboxes
|
|
109
|
+
jobs A command in a fresh sandbox, once or on a cron schedule, with its logs
|
|
110
|
+
domains Your own hostname for a sandbox's port, with HTTPS
|
|
111
|
+
ports A public TCP port to a sandbox: databases, game servers
|
|
112
|
+
ips A dedicated outbound address, for allow-lists
|
|
113
|
+
networks Your networks: a WireGuard tunnel in, your own proxy out, sandboxes reaching
|
|
114
|
+
each other by name
|
|
115
|
+
Each has its own help: \`runtime sandboxes help\`, \`runtime images help\`...
|
|
116
|
+
The singular (\`runtime sandbox\`) is a shortcut for the same product.
|
|
102
117
|
|
|
103
118
|
Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
|
|
104
119
|
Behind a proxy: HTTPS_PROXY and NO_PROXY are read, on Node and Bun alike.
|
|
105
120
|
`;
|
|
106
|
-
const SANDBOX_HELP = `runtime
|
|
121
|
+
const SANDBOX_HELP = `runtime sandboxes <command>
|
|
107
122
|
|
|
108
123
|
Every <id> below can also be the sandbox's name.
|
|
109
124
|
|
|
@@ -112,7 +127,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
112
127
|
port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
|
|
113
128
|
create [--name n] [--label k=v]... [--env K=V]... [--vcpu 2] [--memory 4096] [--disk 4096]
|
|
114
129
|
[--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
|
|
115
|
-
[--timeout <
|
|
130
|
+
[--timeout <60-86400>] [--on-timeout pause|stop|delete] [--no-wait]
|
|
116
131
|
[--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
117
132
|
[--image <name:tag|imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
|
|
118
133
|
[--idle-pause <seconds>] [--no-auto-wake] [--persistent] [--get-or-create]
|
|
@@ -123,7 +138,9 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
123
138
|
Run and stream output; exits with its code
|
|
124
139
|
spawn <id> [--pty] -- <command...> Start in the background; prints a process id
|
|
125
140
|
ps <id> Running and recent processes
|
|
126
|
-
logs <id> <processId> [-f]
|
|
141
|
+
logs <id> <processId> [-f] [--raw] A process's output so far; -f follows it to the end;
|
|
142
|
+
on a terminal control characters show as \\uXXXX
|
|
143
|
+
unless --raw
|
|
127
144
|
kill <id> <processId> [--signal SIGTERM]
|
|
128
145
|
shell <id> [--command "bash -l"] An interactive terminal
|
|
129
146
|
cp <src> <dst> Copy files or directories; name a sandbox
|
|
@@ -134,6 +151,8 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
134
151
|
stop --label k=v [--label k=v]... Stop every live sandbox with all those labels
|
|
135
152
|
rm <id> Delete it for good: stops it, deletes its disk and
|
|
136
153
|
paused memory, revokes its previews; snapshots stay
|
|
154
|
+
rm --label k=v [--label k=v]... Delete for good every sandbox with all those labels,
|
|
155
|
+
stopped ones too
|
|
137
156
|
switch-image <id> <image> --keep-workspace Move it to another image, keeping /workspace (your home),
|
|
138
157
|
volumes, env and previews; processes restart, the rest of
|
|
139
158
|
its disk is lost (snapshot it first to keep everything)
|
|
@@ -144,7 +163,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
144
163
|
update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
|
|
145
164
|
[--persistent on|off] [--max-total-cost <usd>|none]
|
|
146
165
|
Change its settings; persistent keeps it running until stopped
|
|
147
|
-
snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
|
|
166
|
+
snapshot <id> [--name n] [--retention days|credit] Keep its whole machine; prints the snapshot id
|
|
148
167
|
mount <id> <s3|r2|gcs>://<bucket>[/prefix] <path> [--secret NAME] [--region r]
|
|
149
168
|
[--endpoint https://...] [--account-id id] [--read-only]
|
|
150
169
|
Your bucket as a directory; the proxy signs, the sandbox never sees the key
|
|
@@ -152,7 +171,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
152
171
|
tailscale up <id> --auth-key-secret NAME [--hostname h] [--tag tag:x]...
|
|
153
172
|
Join your tailnet (paid); the key is a secret set with --jobs
|
|
154
173
|
tailscale status <id> | tailscale down <id> Its tailnet address; take it off the tailnet
|
|
155
|
-
fork <id> [--count 3] [--name n] [--keep-snapshot]
|
|
174
|
+
fork <id> [--count 3] [--name n] [--keep-snapshot]
|
|
156
175
|
Copies of it as it is now, running; prints their ids
|
|
157
176
|
run-code <id> <file|-> [--lang python|javascript|typescript|r|java|bash|go] [--context c] [--out-dir .]
|
|
158
177
|
Run a notebook-style cell; charts saved as PNG
|
|
@@ -188,10 +207,10 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
188
207
|
Chromium for CDP agents: prints its cdpUrl
|
|
189
208
|
|
|
190
209
|
Examples
|
|
191
|
-
id=$(runtime
|
|
192
|
-
runtime
|
|
193
|
-
runtime
|
|
194
|
-
runtime
|
|
210
|
+
id=$(runtime sandboxes create)
|
|
211
|
+
runtime sandboxes cp ./invoice.py $id:/workspace/invoice.py
|
|
212
|
+
runtime sandboxes exec $id -- python3 /workspace/invoice.py
|
|
213
|
+
runtime sandboxes stop $id
|
|
195
214
|
`;
|
|
196
215
|
/** A command's options. Reading one its parse() did not declare is a bug in
|
|
197
216
|
* this file, and says so, so a test that runs the command finds it. */
|
|
@@ -233,7 +252,14 @@ commandAfter) {
|
|
|
233
252
|
break;
|
|
234
253
|
}
|
|
235
254
|
if (arg.startsWith("--") || (arg.startsWith("-") && arg.length === 2 && !/^-\d/.test(arg))) {
|
|
236
|
-
const [
|
|
255
|
+
const [typed, inline] = arg.replace(/^-+/, "").split(/=(.*)/s, 2);
|
|
256
|
+
// A plural of an option is the option once per comma: --hosts a,b is
|
|
257
|
+
// --host a --host b (13 October 2026: Marc's first try at a secret).
|
|
258
|
+
const plural = !known.has(typed) &&
|
|
259
|
+
typed.endsWith("s") &&
|
|
260
|
+
LISTS.has(typed.slice(0, -1)) &&
|
|
261
|
+
known.has(typed.slice(0, -1));
|
|
262
|
+
const name = plural ? typed.slice(0, -1) : typed;
|
|
237
263
|
if (!known.has(name))
|
|
238
264
|
throw commandAfter && !meantOption(name, known)
|
|
239
265
|
? usage(`Unknown option ${arg.split("=")[0]}. If it belongs to the command, put the command after --: ${commandAfter}`)
|
|
@@ -245,13 +271,16 @@ commandAfter) {
|
|
|
245
271
|
throw usage(`--${name} needs a value.`);
|
|
246
272
|
if (boolean && value !== "true" && value !== "false")
|
|
247
273
|
throw usage(`--${name} takes true or false.`);
|
|
248
|
-
|
|
274
|
+
const values = plural ? value.split(",").filter(Boolean) : [value];
|
|
275
|
+
flags.set(name, [...(flags.get(name) ?? []), ...values]);
|
|
249
276
|
}
|
|
250
277
|
else
|
|
251
278
|
positional.push(arg);
|
|
252
279
|
}
|
|
253
280
|
return { positional, flags, rest };
|
|
254
281
|
}
|
|
282
|
+
/** Options given once per item, which a plural takes as a comma list. */
|
|
283
|
+
const LISTS = new Set(["host", "allow", "deny", "connect"]);
|
|
255
284
|
/** The declared option a mistyped one most likely meant. --memory-mib for
|
|
256
285
|
* --memory, --timeout-seconds for --timeout: the SDK's spelling of an option is
|
|
257
286
|
* the CLI's with its unit added. */
|
|
@@ -270,6 +299,8 @@ function unknownOption(name, known) {
|
|
|
270
299
|
}
|
|
271
300
|
const flag = (args, name) => args.flags.get(name)?.at(-1);
|
|
272
301
|
const has = (args, name) => args.flags.get(name)?.at(-1) === "true";
|
|
302
|
+
/** Days, or the word `credit` for null: kept while the account has credit. */
|
|
303
|
+
const daysOrCredit = (args, name) => flag(args, name) === "credit" ? null : integer(args, name);
|
|
273
304
|
const integer = (args, name) => {
|
|
274
305
|
const value = flag(args, name);
|
|
275
306
|
if (value === undefined)
|
|
@@ -360,16 +391,15 @@ const COMMANDS = [
|
|
|
360
391
|
"support",
|
|
361
392
|
"docs",
|
|
362
393
|
"mcp",
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
"
|
|
368
|
-
"
|
|
369
|
-
"
|
|
370
|
-
"
|
|
371
|
-
"
|
|
372
|
-
"network",
|
|
394
|
+
"sandboxes",
|
|
395
|
+
"snapshots",
|
|
396
|
+
"images",
|
|
397
|
+
"volumes",
|
|
398
|
+
"jobs",
|
|
399
|
+
"domains",
|
|
400
|
+
"ports",
|
|
401
|
+
"ips",
|
|
402
|
+
"networks",
|
|
373
403
|
"webhooks",
|
|
374
404
|
"otel",
|
|
375
405
|
"events",
|
|
@@ -378,30 +408,20 @@ const COMMANDS = [
|
|
|
378
408
|
];
|
|
379
409
|
/** Commands with a help of their own: `runtime <command> help`. */
|
|
380
410
|
const OWN_HELP = [
|
|
381
|
-
"sandbox",
|
|
382
411
|
"sandboxes",
|
|
383
412
|
"sbx",
|
|
384
|
-
"snapshot",
|
|
385
413
|
"snapshots",
|
|
386
|
-
"image",
|
|
387
414
|
"images",
|
|
388
|
-
"volume",
|
|
389
415
|
"volumes",
|
|
390
|
-
"job",
|
|
391
416
|
"jobs",
|
|
392
417
|
"keys",
|
|
393
418
|
"webhooks",
|
|
394
419
|
"webhook",
|
|
395
420
|
"billing",
|
|
396
|
-
"domain",
|
|
397
421
|
"domains",
|
|
398
|
-
"port",
|
|
399
422
|
"ports",
|
|
400
|
-
"
|
|
401
|
-
"
|
|
402
|
-
"tunnel",
|
|
403
|
-
"tunnels",
|
|
404
|
-
"network",
|
|
423
|
+
"ips",
|
|
424
|
+
"networks",
|
|
405
425
|
];
|
|
406
426
|
/** Sandbox commands that once sat at the top level. */
|
|
407
427
|
const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
|
|
@@ -506,6 +526,14 @@ function table(rows) {
|
|
|
506
526
|
.trimEnd())
|
|
507
527
|
.join("\n");
|
|
508
528
|
}
|
|
529
|
+
/** When a sandbox ends by itself (0300, 0381). A paid-ahead lease, expiresAt,
|
|
530
|
+
* is not its time limit; only an older server, which sends no endsAt, gives
|
|
531
|
+
* the end of a limited sandbox's time as expiresAt. */
|
|
532
|
+
function sandboxEnd(info) {
|
|
533
|
+
if (info.endsAt === null || (info.endsAt === undefined && !info.timeoutSeconds))
|
|
534
|
+
return "no limit";
|
|
535
|
+
return info.endsAt ?? info.expiresAt ?? "-";
|
|
536
|
+
}
|
|
509
537
|
function describe(info) {
|
|
510
538
|
return table([
|
|
511
539
|
["id", info.id],
|
|
@@ -515,19 +543,9 @@ function describe(info) {
|
|
|
515
543
|
"shape",
|
|
516
544
|
`${info.vcpu} vCPU, ${info.memoryMiB} MiB memory, ${info.diskMiB} MiB disk (${info.cpu})`,
|
|
517
545
|
],
|
|
518
|
-
["funding", info.funding],
|
|
519
546
|
["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
547
|
["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
|
-
],
|
|
548
|
+
["ends", sandboxEnd(info)],
|
|
531
549
|
["at its end", info.onLeaseEnd ?? "-"],
|
|
532
550
|
// Why a sandbox is no longer running (troubleshooting.md, "A sandbox
|
|
533
551
|
// stopped on its own"); `get` left both out until 30 September 2026.
|
|
@@ -560,17 +578,22 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
560
578
|
// Only what comes before -- is the CLI's: a --json after it is the
|
|
561
579
|
// command's, whether or not the CLI was given its own.
|
|
562
580
|
const dash = argv.indexOf("--");
|
|
563
|
-
|
|
581
|
+
// CLOUD.md section 4: the plural names the product, the singular is a
|
|
582
|
+
// typing shortcut, and an old word works for 30 days with a warning.
|
|
583
|
+
const words = productWords(argv.filter((arg, index) => arg !== "--json" || (dash !== -1 && index > dash)));
|
|
584
|
+
if (words.warning)
|
|
585
|
+
out.error(`Warning: ${named(words.warning)}`);
|
|
586
|
+
const [first, ...remaining] = words.argv;
|
|
564
587
|
// Sandbox is one product of several, so its commands live under it
|
|
565
|
-
// (AGENTS.md, 23 September 2026): `runtime
|
|
588
|
+
// (AGENTS.md, 23 September 2026): `runtime sandboxes run`, `sandbox ssh` and
|
|
566
589
|
// `sandbox port-forward`. The old top-level forms were removed, not aliased.
|
|
567
590
|
if (first && SANDBOX_MOVED.includes(first))
|
|
568
|
-
throw usage(`\`${me} ${first}\` is now \`${me}
|
|
591
|
+
throw usage(`\`${me} ${first}\` is now \`${me} sandboxes ${first === "forward" ? "port-forward" : first}\`.`);
|
|
569
592
|
const nested = (first === "sandbox" || first === "sandboxes" || first === "sbx") &&
|
|
570
593
|
SANDBOX_MOVED.includes(remaining[0] ?? "");
|
|
571
594
|
const command = nested ? remaining[0] : first;
|
|
572
595
|
const rest = nested ? remaining.slice(1) : remaining;
|
|
573
|
-
// `runtime
|
|
596
|
+
// `runtime sandboxes create --help` is how an agent asks what a command takes:
|
|
574
597
|
// answer with the product's help, never by running the command.
|
|
575
598
|
const cut = remaining.indexOf("--");
|
|
576
599
|
const asked = (cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h") ||
|
|
@@ -603,10 +626,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
603
626
|
? JSON.stringify({ connected, usage: HELP })
|
|
604
627
|
: `Runtime Cloud ${VERSION}. ${start}
|
|
605
628
|
|
|
606
|
-
${me}
|
|
629
|
+
${me} sandboxes run -- python3 < invoice.py
|
|
607
630
|
Run a local invoice script, then stop the sandbox
|
|
608
|
-
${me}
|
|
609
|
-
${me}
|
|
631
|
+
${me} sandboxes create A sandbox that stays; prints its id
|
|
632
|
+
${me} sandboxes exec <id> -- <command> Run in it
|
|
610
633
|
${me} help Everything else
|
|
611
634
|
`);
|
|
612
635
|
return 0;
|
|
@@ -616,14 +639,15 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
616
639
|
return 0;
|
|
617
640
|
}
|
|
618
641
|
if (command === "run") {
|
|
619
|
-
const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me}
|
|
642
|
+
const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandboxes run -- python3 < invoice.py`);
|
|
620
643
|
const commandLine = args.rest ?? args.positional;
|
|
621
644
|
if (!commandLine.length)
|
|
622
|
-
throw usage(`Give the command after --: ${me}
|
|
645
|
+
throw usage(`Give the command after --: ${me} sandboxes run -- python3 < invoice.py`);
|
|
623
646
|
const runtime = await client(env);
|
|
624
647
|
const timeout = integer(args, "timeout");
|
|
625
648
|
const { timeoutSeconds: _lease, ...input } = createInput(args);
|
|
626
|
-
const
|
|
649
|
+
const onCapacityWait = waiting(out);
|
|
650
|
+
const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
|
|
627
651
|
let cleanup;
|
|
628
652
|
// SIGINT and normal completion share one stop. Keep an absolute deadline
|
|
629
653
|
// across the stop retries and final state check, so cleanup cannot hang.
|
|
@@ -642,7 +666,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
642
666
|
code: "cleanup_failed",
|
|
643
667
|
status: 0,
|
|
644
668
|
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}
|
|
669
|
+
hint: `Check it and retry cleanup with: ${me} sandboxes stop ${sbx.id}`,
|
|
646
670
|
cause: error,
|
|
647
671
|
}), out.json));
|
|
648
672
|
return false;
|
|
@@ -658,7 +682,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
658
682
|
finally {
|
|
659
683
|
try {
|
|
660
684
|
if (has(args, "keep"))
|
|
661
|
-
out.error(`Kept sandbox ${sbx.id}. Run more with: ${me}
|
|
685
|
+
out.error(`Kept sandbox ${sbx.id}. Run more with: ${me} sandboxes exec ${sbx.id} -- <command>`);
|
|
662
686
|
else if (!(await stop()) && code === 0)
|
|
663
687
|
code = 1;
|
|
664
688
|
}
|
|
@@ -702,11 +726,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
702
726
|
...(spend.pilot ? [pilotFunding(spend.pilot)] : []),
|
|
703
727
|
...(trialLeft > 0
|
|
704
728
|
? [
|
|
705
|
-
`
|
|
729
|
+
`free usage, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left without credit`,
|
|
706
730
|
]
|
|
707
731
|
: []),
|
|
708
732
|
...(credit > 0n ? [`${dollars(credit)} of credit`] : []),
|
|
709
|
-
].join("; ") || "no
|
|
733
|
+
].join("; ") || "no free usage or credit left: runtime billing";
|
|
710
734
|
print(table([
|
|
711
735
|
["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
|
|
712
736
|
...(who.role ? [["role", who.role]] : []),
|
|
@@ -881,7 +905,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
881
905
|
product = "billing";
|
|
882
906
|
const origins = connectionOrigins(env);
|
|
883
907
|
const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
|
|
884
|
-
return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
|
|
908
|
+
return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], ["below", "amount", "monthly"]), out, () => client(env), undefined, {
|
|
885
909
|
open: (input) => {
|
|
886
910
|
checkBrowserOrigins(origins);
|
|
887
911
|
return openWalletAccount(input, { authUrl: origins.auth });
|
|
@@ -905,21 +929,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
905
929
|
});
|
|
906
930
|
}
|
|
907
931
|
if (command === "referrals") {
|
|
932
|
+
parse(rest, [], []);
|
|
908
933
|
const r = await (await client(env)).referrals.get();
|
|
909
934
|
const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
910
935
|
print([
|
|
911
936
|
`Your link: ${r.link}`,
|
|
912
937
|
"",
|
|
913
|
-
`
|
|
914
|
-
`you
|
|
915
|
-
`
|
|
938
|
+
`A company that signs up with it has each payment in its first ${r.windowDays} days matched, up to ${usd(r.maxRewardMicros)},`,
|
|
939
|
+
`and you get the same; its first payment of ${usd(r.minPurchaseMicros)} or more gives each of you at least ${usd(r.rewardMicros)}.`,
|
|
940
|
+
`Then for ${r.shareMonths} months you earn ${r.shareBasisPoints / 100}% of what it spends from credit it paid for`,
|
|
941
|
+
`(up to ${usd(r.yearlyCapMicros)} a year for you, matches and share together).`,
|
|
916
942
|
...(r.enabled ? [] : ["The program is paused: new sign-ups are not counted right now."]),
|
|
917
943
|
"",
|
|
918
944
|
table([
|
|
919
945
|
["signed up", String(r.signedUp)],
|
|
920
|
-
["waiting for a first
|
|
921
|
-
["
|
|
946
|
+
["waiting for a first payment", String(r.pending)],
|
|
947
|
+
["paying", String(r.payingCompanies)],
|
|
922
948
|
["earned", usd(r.earnedMicros)],
|
|
949
|
+
["of which share", usd(r.shareEarnedMicros)],
|
|
923
950
|
[`left this year (${r.year})`, usd(r.capRemainingMicros)],
|
|
924
951
|
]),
|
|
925
952
|
].join("\n"), r);
|
|
@@ -1001,7 +1028,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
1001
1028
|
]);
|
|
1002
1029
|
print(rows.length
|
|
1003
1030
|
? table([["ID", "PRODUCT", "NAME", "STATE", "CREATED"], ...rows])
|
|
1004
|
-
: "Nothing yet. Start with: runtime
|
|
1031
|
+
: "Nothing yet. Start with: runtime sandboxes create", items);
|
|
1005
1032
|
return 0;
|
|
1006
1033
|
}
|
|
1007
1034
|
if (command === "ssh") {
|
|
@@ -1032,15 +1059,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
1032
1059
|
product = "job";
|
|
1033
1060
|
return jobCommand(rest, env, out);
|
|
1034
1061
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1062
|
+
// WireGuard is inside networks: `runtime networks wireguard peer add`.
|
|
1063
|
+
const wireguard = command === "networks" && rest[0] === "wireguard";
|
|
1064
|
+
const networkProduct = wireguard ? "wireguard" : command;
|
|
1065
|
+
const networkRest = wireguard ? rest.slice(1) : rest;
|
|
1066
|
+
if (command !== "wireguard" && NETWORK_PRODUCTS.includes(networkProduct)) {
|
|
1038
1067
|
product = networkProduct;
|
|
1039
|
-
return networkProductCommand(networkProduct,
|
|
1068
|
+
return networkProductCommand(networkProduct, networkRest, parse(networkRest, ["ipv6"], ["sandbox", "subnet", "public-key", "route", "out", "host", "secret"]), out, () => client(env));
|
|
1040
1069
|
}
|
|
1041
1070
|
throw unknown("command", command, COMMANDS);
|
|
1042
1071
|
}
|
|
1043
|
-
/** `runtime usage` for a person: the balance, the
|
|
1072
|
+
/** `runtime usage` for a person: the balance, the free usage and what each kind
|
|
1044
1073
|
* of resource was charged, in dollars. --json keeps every figure, and each
|
|
1045
1074
|
* resource with its rates and CPU time. */
|
|
1046
1075
|
/** One row per resource, for a spreadsheet: when it ran, how big it was, how
|
|
@@ -1095,15 +1124,15 @@ export function usageCsv(u) {
|
|
|
1095
1124
|
/** Milliseconds as hours, to a tenth: "8,731.2". */
|
|
1096
1125
|
const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
1097
1126
|
/** The machine time left without credit, as `runtime usage` and `runtime
|
|
1098
|
-
* limits` say it: this month's
|
|
1127
|
+
* limits` say it: this month's free usage, as hours at 2 vCPU and 4 GiB,
|
|
1099
1128
|
* or for an account given free hours before 5 October 2026, those hours too. */
|
|
1100
1129
|
function trialLeft(trial) {
|
|
1101
1130
|
const renews = trial.renewsAt ? `, renews ${trial.renewsAt.slice(0, 10)}` : "";
|
|
1102
1131
|
return trial.totalMs > 0
|
|
1103
|
-
? `${hours(trial.availableMs)} hours left (${hours(trial.totalMs)} free hours given before 5 October 2026, and the
|
|
1132
|
+
? `${hours(trial.availableMs)} hours left (${hours(trial.totalMs)} free hours given before 5 October 2026, and the free usage${renews})`
|
|
1104
1133
|
: `${hours(trial.availableMs)} hours at 2 vCPU and 4 GiB left this month${renews}`;
|
|
1105
1134
|
}
|
|
1106
|
-
/** This month's
|
|
1135
|
+
/** This month's free usage, one row per product, as `runtime usage`
|
|
1107
1136
|
* says it. */
|
|
1108
1137
|
function allowanceRows(allowances) {
|
|
1109
1138
|
const amount = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
@@ -1112,6 +1141,43 @@ function allowanceRows(allowances) {
|
|
|
1112
1141
|
`${amount(a.left)} of ${amount(a.quantity)} ${a.unit}s left`,
|
|
1113
1142
|
]);
|
|
1114
1143
|
}
|
|
1144
|
+
/** What Runtime for Startups includes as `runtime usage` says it, in the
|
|
1145
|
+
* console's words: machine time left of what it gave, storage as the GB kept
|
|
1146
|
+
* free each month and the GB kept now, and what it runs at once. */
|
|
1147
|
+
function potRows(pot, limits) {
|
|
1148
|
+
const amount = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
1149
|
+
const day = (iso) => {
|
|
1150
|
+
const at = new Date(iso);
|
|
1151
|
+
const month = at.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
|
|
1152
|
+
return `${at.getUTCDate()} ${month} ${at.getUTCFullYear()}`;
|
|
1153
|
+
};
|
|
1154
|
+
const NAMES = {
|
|
1155
|
+
"machine-cpu": "vCPU-hours",
|
|
1156
|
+
"machine-memory": "memory",
|
|
1157
|
+
"saved-copies": "saved copies kept free",
|
|
1158
|
+
disks: "volumes",
|
|
1159
|
+
};
|
|
1160
|
+
const ORDER = ["machine-cpu", "machine-memory", "saved-copies", "disks"];
|
|
1161
|
+
const rank = (pool) => (ORDER.includes(pool) ? ORDER.indexOf(pool) : ORDER.length);
|
|
1162
|
+
const rows = [...pot]
|
|
1163
|
+
.sort((a, b) => rank(a.pool) - rank(b.pool))
|
|
1164
|
+
.map((p) => {
|
|
1165
|
+
const name = `${p.period === "month" ? "Runtime for Startups" : "startup hours"}: ${NAMES[p.pool] ?? p.pool.replace(/-/g, " ")}`;
|
|
1166
|
+
const until = p.expiresAt ? ` until ${day(p.expiresAt)}` : "";
|
|
1167
|
+
if (p.period === "month") {
|
|
1168
|
+
const kept = p.keptGB ?? 0;
|
|
1169
|
+
const over = kept > p.quantity ? `; past ${amount(p.quantity)} GB bills at the published rate` : "";
|
|
1170
|
+
return [
|
|
1171
|
+
name,
|
|
1172
|
+
`${amount(p.quantity)} GB kept free${until} · ${amount(kept)} GB kept now${over}`,
|
|
1173
|
+
];
|
|
1174
|
+
}
|
|
1175
|
+
return [name, `${amount(p.left ?? 0)} of ${amount(p.quantity)} ${p.unit}s left${until}`];
|
|
1176
|
+
});
|
|
1177
|
+
if (limits)
|
|
1178
|
+
rows.push(["Runtime for Startups: sandboxes at once", amount(limits.sandboxesAtOnce)], ["Runtime for Startups: image builds at once", amount(limits.buildsAtOnce)]);
|
|
1179
|
+
return rows;
|
|
1180
|
+
}
|
|
1115
1181
|
/** What a pilot gives, as `runtime whoami` and `runtime limits` say it: the
|
|
1116
1182
|
* sandboxes it runs free and until when, past which credit does not run
|
|
1117
1183
|
* them. */
|
|
@@ -1155,6 +1221,16 @@ function usageSummary(u) {
|
|
|
1155
1221
|
rows.push(["free time", trialLeft(u.trial)]);
|
|
1156
1222
|
if (u.allowances)
|
|
1157
1223
|
rows.push(...allowanceRows(u.allowances));
|
|
1224
|
+
// Only a data property is an older server's answer. Do not invoke SDK
|
|
1225
|
+
// compatibility getters when a server sends neither generation of fields.
|
|
1226
|
+
const pot = "startupIncluded" in u
|
|
1227
|
+
? u.startupIncluded
|
|
1228
|
+
: Object.getOwnPropertyDescriptor(u, "pot")?.value;
|
|
1229
|
+
const potLimits = "startupLimits" in u
|
|
1230
|
+
? u.startupLimits
|
|
1231
|
+
: Object.getOwnPropertyDescriptor(u, "potLimits")?.value;
|
|
1232
|
+
if (pot?.length || potLimits)
|
|
1233
|
+
rows.push(...potRows(pot ?? [], potLimits));
|
|
1158
1234
|
if (u.pilot)
|
|
1159
1235
|
rows.push(["pilot sandbox hours", pilotLeft(u.pilot)]);
|
|
1160
1236
|
if (u.outbound) {
|
|
@@ -1233,10 +1309,21 @@ async function outputSoFar(runtime, sandboxId, processId) {
|
|
|
1233
1309
|
processId,
|
|
1234
1310
|
state: latest?.state ?? null,
|
|
1235
1311
|
exitCode: latest?.exitCode ?? null,
|
|
1312
|
+
...(latest?.outOfMemory ? { outOfMemory: latest.outOfMemory } : {}),
|
|
1236
1313
|
truncated,
|
|
1237
1314
|
chunks,
|
|
1238
1315
|
};
|
|
1239
1316
|
}
|
|
1317
|
+
/** What a person reads when the sandbox's out-of-memory killer ended a
|
|
1318
|
+
* command: the API's message and hint, on standard error. A bare exit -9 (or
|
|
1319
|
+
* 137) said nothing of memory until 9 October 2026. */
|
|
1320
|
+
export function outOfMemoryLines(memory) {
|
|
1321
|
+
// The words come from an answer, not this CLI: escaped like a log line, so
|
|
1322
|
+
// nothing in them can drive the terminal (terminal.ts).
|
|
1323
|
+
return memory && typeof memory.message === "string" && typeof memory.hint === "string"
|
|
1324
|
+
? `runtime: ${terminalSafe(memory.message)}\nHint: ${terminalSafe(memory.hint)}`
|
|
1325
|
+
: undefined;
|
|
1326
|
+
}
|
|
1240
1327
|
/** Dollars from integer microdollars: cents from a dollar up, four places
|
|
1241
1328
|
* below one, and every place below a hundredth of a cent, so a short trial
|
|
1242
1329
|
* test never reads as $0.0000. */
|
|
@@ -1309,36 +1396,60 @@ const longDate = (iso) => new Date(iso).toLocaleDateString("en-GB", {
|
|
|
1309
1396
|
year: "numeric",
|
|
1310
1397
|
timeZone: "UTC",
|
|
1311
1398
|
});
|
|
1312
|
-
/** One line on the
|
|
1313
|
-
*
|
|
1399
|
+
/** One line on the welcome match: while it is open, how much of it credit
|
|
1400
|
+
* added now can still get; after, what it got. Undefined when there is
|
|
1401
|
+
* nothing to say: the offer ended unused. */
|
|
1402
|
+
function welcomeLine(summary) {
|
|
1403
|
+
const w = summary.welcome;
|
|
1404
|
+
// A server from before the welcome match (0433) answers without it.
|
|
1405
|
+
if (!w)
|
|
1406
|
+
return undefined;
|
|
1407
|
+
const whole = (micros) => dollars(micros).replace(/\.00$/, "");
|
|
1408
|
+
if (w.eligible) {
|
|
1409
|
+
// A server from before 0435 answers without what is left.
|
|
1410
|
+
const left = w.leftMicros ?? (BigInt(w.maxMicros) - BigInt(w.creditMicros)).toString();
|
|
1411
|
+
return BigInt(w.creditMicros) > 0n
|
|
1412
|
+
? `${whole(w.creditMicros)} of ${whole(w.maxMicros)} matched so far; credit you add by ${longDate(w.endsAt)} is matched, up to ${whole(left)} more.`
|
|
1413
|
+
: `Credit you add by ${longDate(w.endsAt)} is matched, up to ${whole(w.maxMicros)} in total.`;
|
|
1414
|
+
}
|
|
1415
|
+
switch (w.status) {
|
|
1416
|
+
case "paid":
|
|
1417
|
+
return `Your credit was matched: ${dollars(w.creditMicros)} of credit.`;
|
|
1418
|
+
case "superseded":
|
|
1419
|
+
return "Your payments are matched by your referral instead, which is never less.";
|
|
1420
|
+
case "reversed":
|
|
1421
|
+
return "The payments that were matched were refunded, so the match was taken back.";
|
|
1422
|
+
case "refused":
|
|
1423
|
+
return "Your credit was not matched. Ask support if you think that is wrong.";
|
|
1424
|
+
}
|
|
1425
|
+
return undefined;
|
|
1426
|
+
}
|
|
1427
|
+
/** What `switch` says: the switch on record, if any, and the welcome match.
|
|
1428
|
+
* A switch already on record is named with its provider and date, so it never
|
|
1314
1429
|
* reads as if this command recorded it, or recorded the provider it names. */
|
|
1315
1430
|
function switchingLine(summary, provider) {
|
|
1316
|
-
const cap = dollars(summary.maxMicros);
|
|
1317
1431
|
const sw = summary.switch;
|
|
1432
|
+
const welcome = welcomeLine(summary);
|
|
1318
1433
|
if (!sw) {
|
|
1319
|
-
if (!
|
|
1320
|
-
return
|
|
1321
|
-
return
|
|
1434
|
+
if (!provider || !summary.eligible)
|
|
1435
|
+
return welcome;
|
|
1436
|
+
return [
|
|
1437
|
+
`Switching from ${RIVAL_NAMES[provider]}? Run \`${me} switch --from ${provider}\` before your first top-up to record it.`,
|
|
1438
|
+
welcome,
|
|
1439
|
+
]
|
|
1440
|
+
.filter(Boolean)
|
|
1441
|
+
.join(" ");
|
|
1322
1442
|
}
|
|
1323
1443
|
const from = RIVAL_NAMES[sw.provider];
|
|
1324
|
-
const recorded = `This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}`;
|
|
1325
1444
|
const once = provider && provider !== sw.provider
|
|
1326
1445
|
? ` A switch is recorded once, so it stays ${from}, not ${RIVAL_NAMES[provider]}.`
|
|
1327
1446
|
: "";
|
|
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
|
-
}
|
|
1447
|
+
return [
|
|
1448
|
+
`This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}.${once}`,
|
|
1449
|
+
welcome,
|
|
1450
|
+
]
|
|
1451
|
+
.filter(Boolean)
|
|
1452
|
+
.join(" ");
|
|
1342
1453
|
}
|
|
1343
1454
|
/** Running time in the largest unit that keeps it above zero: a short
|
|
1344
1455
|
* trial test is seconds, not "0 hours". */
|
|
@@ -1378,10 +1489,13 @@ async function switching(command, argv, env, out) {
|
|
|
1378
1489
|
if (command === "switch") {
|
|
1379
1490
|
if (!from) {
|
|
1380
1491
|
const summary = await api.get();
|
|
1381
|
-
|
|
1382
|
-
(summary.
|
|
1383
|
-
|
|
1384
|
-
|
|
1492
|
+
const record = summary.eligible
|
|
1493
|
+
? `Moving from a rival? Run \`${me} switch --from <provider>\` (${summary.providers.join(", ")}) before your first top-up to record it.`
|
|
1494
|
+
: undefined;
|
|
1495
|
+
print((summary.switch
|
|
1496
|
+
? switchingLine(summary)
|
|
1497
|
+
: [record, welcomeLine(summary)].filter(Boolean).join(" ")) ||
|
|
1498
|
+
"A switch is recorded before the first top-up, and this organization has made one already.", summary);
|
|
1385
1499
|
return 0;
|
|
1386
1500
|
}
|
|
1387
1501
|
const provider = SWITCH_FROM[from];
|
|
@@ -1389,7 +1503,7 @@ async function switching(command, argv, env, out) {
|
|
|
1389
1503
|
throw unknown("provider", from, Object.keys(RIVAL_NAMES));
|
|
1390
1504
|
const summary = await api.record({ provider });
|
|
1391
1505
|
print([
|
|
1392
|
-
`Recorded: switching from ${RIVAL_NAMES[provider]}. ${
|
|
1506
|
+
`Recorded: switching from ${RIVAL_NAMES[provider]}. ${welcomeLine(summary) ?? ""}`.trim(),
|
|
1393
1507
|
...dropInLines(provider),
|
|
1394
1508
|
].join("\n"), summary);
|
|
1395
1509
|
return 0;
|
|
@@ -1504,12 +1618,34 @@ async function sandbox(argv, env, out) {
|
|
|
1504
1618
|
const use = async (id) => SANDBOX_UUID.test(id ?? "")
|
|
1505
1619
|
? new SandboxHandle(runtime.transport, { id })
|
|
1506
1620
|
: get(id);
|
|
1621
|
+
/* `stop --label` and `rm --label`: every sandbox with all the labels, each
|
|
1622
|
+
named with what happened to it; exits 1 if any failed. */
|
|
1623
|
+
const byLabel = (words) => words.some((word) => /^--label(=|$)/.test(word));
|
|
1624
|
+
const everyLabelled = async (act, words) => {
|
|
1625
|
+
const args = parse(words, [], ["label"]);
|
|
1626
|
+
if (args.positional.length)
|
|
1627
|
+
throw usage("Give a sandbox id or --label, not both.");
|
|
1628
|
+
const labels = pairs(args, "label");
|
|
1629
|
+
const { done, failed } = act === "stop"
|
|
1630
|
+
? await runtime.sandboxes.stopAll({ labels }).then((r) => ({ ...r, done: r.stopped }))
|
|
1631
|
+
: await runtime.sandboxes.deleteAll({ labels }).then((r) => ({ ...r, done: r.deleted }));
|
|
1632
|
+
const past = act === "stop" ? "stopped" : "deleted";
|
|
1633
|
+
const said = (error) => (error instanceof Error ? error.message : String(error));
|
|
1634
|
+
print([
|
|
1635
|
+
done.length || failed.length
|
|
1636
|
+
? `${done.length} ${past}${failed.length ? `, ${failed.length} failed` : ""}.`
|
|
1637
|
+
: `No ${act === "stop" ? "live " : ""}sandbox has those labels.`,
|
|
1638
|
+
...done.map((id) => `${id} ${past}`),
|
|
1639
|
+
...failed.map((f) => `${f.id} failed: ${said(f.error)}`),
|
|
1640
|
+
].join("\n"), { [past]: done, failed: failed.map((f) => ({ id: f.id, error: said(f.error) })) });
|
|
1641
|
+
return failed.length ? 1 : 0;
|
|
1642
|
+
};
|
|
1507
1643
|
switch (verb) {
|
|
1508
1644
|
case "create": {
|
|
1509
1645
|
const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
});
|
|
1646
|
+
const input = createInput(args);
|
|
1647
|
+
const onCapacityWait = waiting(out);
|
|
1648
|
+
const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
|
|
1513
1649
|
print(sbx.id, sbx.info);
|
|
1514
1650
|
return 0;
|
|
1515
1651
|
}
|
|
@@ -1534,24 +1670,21 @@ async function sandbox(argv, env, out) {
|
|
|
1534
1670
|
? null
|
|
1535
1671
|
: await runtime.sandboxes.list({ state: ["stopped"], limit: 50 });
|
|
1536
1672
|
const stopped = hidden?.data.length
|
|
1537
|
-
? `${hidden.data.length}${hidden.hasMore ? "+" : ""} stopped; runtime
|
|
1673
|
+
? `${hidden.data.length}${hidden.hasMore ? "+" : ""} stopped; runtime sandboxes ls --all shows them.`
|
|
1538
1674
|
: "";
|
|
1539
1675
|
const rows = all.map((s) => [
|
|
1540
1676
|
s.id,
|
|
1541
1677
|
s.info.name ?? "-",
|
|
1542
1678
|
s.state,
|
|
1543
1679
|
`${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) ?? "-",
|
|
1680
|
+
sandboxEnd(s.info),
|
|
1548
1681
|
]);
|
|
1549
1682
|
print([
|
|
1550
1683
|
rows.length
|
|
1551
|
-
? table([["ID", "NAME", "STATE", "CPU/MIB", "
|
|
1684
|
+
? table([["ID", "NAME", "STATE", "CPU/MIB", "ENDS"], ...rows])
|
|
1552
1685
|
: stopped
|
|
1553
1686
|
? "No live sandboxes."
|
|
1554
|
-
: "No sandboxes. Create one: runtime
|
|
1687
|
+
: "No sandboxes. Create one: runtime sandboxes create",
|
|
1555
1688
|
stopped,
|
|
1556
1689
|
]
|
|
1557
1690
|
.filter(Boolean)
|
|
@@ -1566,31 +1699,33 @@ async function sandbox(argv, env, out) {
|
|
|
1566
1699
|
case "switch-image": {
|
|
1567
1700
|
const args = parse(rest, ["keep-workspace"], []);
|
|
1568
1701
|
const [id, image] = args.positional;
|
|
1569
|
-
const sbx = await use(need(id, `a sandbox, e.g. ${me}
|
|
1702
|
+
const sbx = await use(need(id, `a sandbox, e.g. ${me} sandboxes switch-image <id> app:v2 --keep-workspace`));
|
|
1570
1703
|
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}
|
|
1704
|
+
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
1705
|
await sbx.switchImage(need(image, "an image, e.g. app:v2"), { keep: "workspace" });
|
|
1573
1706
|
print(describe(sbx.info), sbx.info);
|
|
1574
1707
|
return 0;
|
|
1575
1708
|
}
|
|
1576
1709
|
case "resize": {
|
|
1577
1710
|
const args = parse(rest, ["restart"], ["vcpu", "memory"]);
|
|
1578
|
-
const sbx = await use(need(args.positional[0], `a sandbox, e.g. ${me}
|
|
1711
|
+
const sbx = await use(need(args.positional[0], `a sandbox, e.g. ${me} sandboxes resize <id> --memory 4096 --restart`));
|
|
1579
1712
|
const size = {
|
|
1580
1713
|
...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
|
|
1581
1714
|
...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
|
|
1582
1715
|
};
|
|
1583
1716
|
if (size.vcpu === undefined && size.memoryMiB === undefined)
|
|
1584
|
-
throw usage(`Name a new size, e.g. ${me}
|
|
1717
|
+
throw usage(`Name a new size, e.g. ${me} sandboxes resize ${sbx.id} --memory 4096 --restart`);
|
|
1585
1718
|
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}
|
|
1719
|
+
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
1720
|
await sbx.resize(size, { restart: true });
|
|
1588
1721
|
print(describe(sbx.info), sbx.info);
|
|
1589
1722
|
return 0;
|
|
1590
1723
|
}
|
|
1591
1724
|
case "rm":
|
|
1592
1725
|
case "delete": {
|
|
1593
|
-
|
|
1726
|
+
if (byLabel(rest))
|
|
1727
|
+
return everyLabelled("delete", rest);
|
|
1728
|
+
const sbx = await use(need(rest[0], `a sandbox, e.g. ${me} sandboxes rm <id>`));
|
|
1594
1729
|
const gone = await sbx.delete();
|
|
1595
1730
|
print(`${gone.id} deleted`, gone);
|
|
1596
1731
|
return 0;
|
|
@@ -1599,23 +1734,8 @@ async function sandbox(argv, env, out) {
|
|
|
1599
1734
|
case "pause":
|
|
1600
1735
|
case "wake":
|
|
1601
1736
|
case "restart": {
|
|
1602
|
-
if (verb === "stop" && rest
|
|
1603
|
-
|
|
1604
|
-
if (args.positional.length)
|
|
1605
|
-
throw usage("Give a sandbox id or --label, not both.");
|
|
1606
|
-
const { stopped, failed } = await runtime.sandboxes.stopAll({
|
|
1607
|
-
labels: pairs(args, "label"),
|
|
1608
|
-
});
|
|
1609
|
-
const said = (error) => (error instanceof Error ? error.message : String(error));
|
|
1610
|
-
print([
|
|
1611
|
-
stopped.length || failed.length
|
|
1612
|
-
? `${stopped.length} stopped${failed.length ? `, ${failed.length} failed` : ""}.`
|
|
1613
|
-
: "No live sandbox has those labels.",
|
|
1614
|
-
...stopped.map((id) => `${id} stopped`),
|
|
1615
|
-
...failed.map((f) => `${f.id} failed: ${said(f.error)}`),
|
|
1616
|
-
].join("\n"), { stopped, failed: failed.map((f) => ({ id: f.id, error: said(f.error) })) });
|
|
1617
|
-
return failed.length ? 1 : 0;
|
|
1618
|
-
}
|
|
1737
|
+
if (verb === "stop" && byLabel(rest))
|
|
1738
|
+
return everyLabelled("stop", rest);
|
|
1619
1739
|
const sbx = await get(rest[0]);
|
|
1620
1740
|
await sbx[verb]();
|
|
1621
1741
|
print(`${sbx.id} ${sbx.state}`, sbx.info);
|
|
@@ -1692,15 +1812,15 @@ async function sandbox(argv, env, out) {
|
|
|
1692
1812
|
print(gone.left ? "Left the tailnet." : "Not on a tailnet.", gone);
|
|
1693
1813
|
return 0;
|
|
1694
1814
|
}
|
|
1695
|
-
throw usage("runtime
|
|
1815
|
+
throw usage("runtime sandboxes tailscale up|status|down <id>");
|
|
1696
1816
|
}
|
|
1697
1817
|
case "snapshot": {
|
|
1698
1818
|
const args = parse(rest, [], ["name", "retention"]);
|
|
1699
1819
|
const sbx = await get(args.positional[0]);
|
|
1700
1820
|
const snapshot = await sbx.snapshot({
|
|
1701
1821
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1702
|
-
...(
|
|
1703
|
-
? { retentionDays:
|
|
1822
|
+
...(daysOrCredit(args, "retention") !== undefined
|
|
1823
|
+
? { retentionDays: daysOrCredit(args, "retention") }
|
|
1704
1824
|
: {}),
|
|
1705
1825
|
});
|
|
1706
1826
|
print(snapshot.id, snapshot);
|
|
@@ -1772,26 +1892,26 @@ async function sandbox(argv, env, out) {
|
|
|
1772
1892
|
}
|
|
1773
1893
|
case "extend": {
|
|
1774
1894
|
const sbx = await get(rest[0]);
|
|
1775
|
-
const seconds = Number(need(rest[1], "seconds, e.g. runtime
|
|
1895
|
+
const seconds = Number(need(rest[1], "seconds, e.g. runtime sandboxes extend <id> 600"));
|
|
1776
1896
|
await sbx.extend(seconds);
|
|
1777
|
-
print(sbx.info
|
|
1897
|
+
print(sandboxEnd(sbx.info) === "no limit"
|
|
1778
1898
|
? `${sbx.id} has no time limit: it runs while it works, so nothing was extended`
|
|
1779
|
-
: `${sbx.id} now ends at ${sbx.info
|
|
1899
|
+
: `${sbx.id} now ends at ${sandboxEnd(sbx.info)}`, sbx.info);
|
|
1780
1900
|
return 0;
|
|
1781
1901
|
}
|
|
1782
1902
|
case "exec": {
|
|
1783
|
-
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me}
|
|
1903
|
+
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandboxes exec <id> -- ls -la`);
|
|
1784
1904
|
const command = args.rest ?? args.positional.slice(1);
|
|
1785
1905
|
if (!command.length)
|
|
1786
|
-
throw usage("Give the command after --: runtime
|
|
1906
|
+
throw usage("Give the command after --: runtime sandboxes exec <id> -- ls -la");
|
|
1787
1907
|
const sbx = await use(args.positional[0]);
|
|
1788
1908
|
return execute(sbx, command, args, out, integer(args, "timeout"));
|
|
1789
1909
|
}
|
|
1790
1910
|
case "spawn": {
|
|
1791
|
-
const args = parse(rest, ["pty"], ["cwd", "env"], `${me}
|
|
1911
|
+
const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandboxes spawn <id> -- npm run dev`);
|
|
1792
1912
|
const command = args.rest ?? [];
|
|
1793
1913
|
if (!command.length)
|
|
1794
|
-
throw usage("Give the command after --: runtime
|
|
1914
|
+
throw usage("Give the command after --: runtime sandboxes spawn <id> -- npm run dev");
|
|
1795
1915
|
const sbx = await use(args.positional[0]);
|
|
1796
1916
|
const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
|
|
1797
1917
|
...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
|
|
@@ -1807,36 +1927,54 @@ async function sandbox(argv, env, out) {
|
|
|
1807
1927
|
print(list.length
|
|
1808
1928
|
? table([
|
|
1809
1929
|
["PROCESS", "STATE", "EXIT", "COMMAND"],
|
|
1810
|
-
|
|
1930
|
+
// The command line is the guest's report: escaped, as a log is.
|
|
1931
|
+
...list.map((p) => [
|
|
1932
|
+
terminalSafe(String(p.id)),
|
|
1933
|
+
terminalSafe(String(p.state)),
|
|
1934
|
+
`${typeof p.exitCode === "number" ? p.exitCode : "-"}${p.outOfMemory ? " (out of memory)" : ""}`,
|
|
1935
|
+
terminalSafe(String(p.command ?? "")),
|
|
1936
|
+
]),
|
|
1811
1937
|
])
|
|
1812
1938
|
: "No processes.", list);
|
|
1939
|
+
const killed = list.find((p) => p.outOfMemory)?.outOfMemory;
|
|
1940
|
+
if (killed && typeof killed.hint === "string" && !out.json)
|
|
1941
|
+
out.error(`Hint: ${terminalSafe(killed.hint)}`);
|
|
1813
1942
|
return 0;
|
|
1814
1943
|
}
|
|
1815
1944
|
case "logs": {
|
|
1816
|
-
const args = parse(rest, ["f", "follow"], []);
|
|
1945
|
+
const args = parse(rest, ["f", "follow", "raw"], []);
|
|
1817
1946
|
const sbx = await use(args.positional[0]);
|
|
1818
1947
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
1948
|
+
const [stdout, stderr, shown] = [
|
|
1949
|
+
logStream(process.stdout, has(args, "raw")),
|
|
1950
|
+
logStream(process.stderr, has(args, "raw")),
|
|
1951
|
+
logText(args),
|
|
1952
|
+
];
|
|
1819
1953
|
if (has(args, "f") || has(args, "follow")) {
|
|
1820
1954
|
for await (const event of proc.output())
|
|
1821
1955
|
if (event.type === "stdout")
|
|
1822
|
-
|
|
1956
|
+
stdout(event.data);
|
|
1823
1957
|
else if (event.type === "stderr")
|
|
1824
|
-
|
|
1958
|
+
stderr(event.data);
|
|
1825
1959
|
else if (out.json)
|
|
1826
|
-
out.write(JSON.stringify(event));
|
|
1960
|
+
out.write(shown(JSON.stringify(event)));
|
|
1961
|
+
else if (event.type === "exit" && event.outOfMemory)
|
|
1962
|
+
out.error(outOfMemoryLines(event.outOfMemory));
|
|
1827
1963
|
return 0;
|
|
1828
1964
|
}
|
|
1829
1965
|
/* Without -f, what it has printed so far, then back to the prompt, as
|
|
1830
1966
|
`docker logs` does: a server's log is read, not followed forever. */
|
|
1831
1967
|
const so = await outputSoFar(runtime, sbx.id, proc.id);
|
|
1832
1968
|
if (out.json) {
|
|
1833
|
-
out.write(JSON.stringify(so));
|
|
1969
|
+
out.write(shown(JSON.stringify(so)));
|
|
1834
1970
|
return 0;
|
|
1835
1971
|
}
|
|
1836
1972
|
for (const chunk of so.chunks)
|
|
1837
|
-
(chunk.stream === "stderr" ?
|
|
1973
|
+
(chunk.stream === "stderr" ? stderr : stdout)(chunk.text);
|
|
1974
|
+
if (so.outOfMemory)
|
|
1975
|
+
out.error(outOfMemoryLines(so.outOfMemory));
|
|
1838
1976
|
if (so.state === "running")
|
|
1839
|
-
process.stderr.write(`[${proc.id} is still running; follow it: ${me}
|
|
1977
|
+
process.stderr.write(`[${proc.id} is still running; follow it: ${me} sandboxes logs ${sbx.id} ${proc.id} -f]\n`);
|
|
1840
1978
|
return 0;
|
|
1841
1979
|
}
|
|
1842
1980
|
case "kill": {
|
|
@@ -1906,7 +2044,7 @@ async function sandbox(argv, env, out) {
|
|
|
1906
2044
|
return runCode(await use(rest[0]), rest.slice(1), out);
|
|
1907
2045
|
case "preview": {
|
|
1908
2046
|
if (rest[0] === "rotate") {
|
|
1909
|
-
const port = portOf(rest[2], "runtime
|
|
2047
|
+
const port = portOf(rest[2], "runtime sandboxes preview rotate <id> 3000");
|
|
1910
2048
|
const sbx = await use(rest[1]);
|
|
1911
2049
|
const preview = await sbx.previews.rotate(port);
|
|
1912
2050
|
print(preview.visibility === "public"
|
|
@@ -1915,7 +2053,7 @@ async function sandbox(argv, env, out) {
|
|
|
1915
2053
|
return 0;
|
|
1916
2054
|
}
|
|
1917
2055
|
const args = parse(rest, ["public"], ["ttl"]);
|
|
1918
|
-
const port = portOf(args.positional[1], "runtime
|
|
2056
|
+
const port = portOf(args.positional[1], "runtime sandboxes preview <id> 3000");
|
|
1919
2057
|
const sbx = await use(args.positional[0]);
|
|
1920
2058
|
const preview = await sbx.previews.create(port, {
|
|
1921
2059
|
...(has(args, "public") ? { visibility: "public" } : {}),
|
|
@@ -1935,7 +2073,7 @@ async function sandbox(argv, env, out) {
|
|
|
1935
2073
|
["PORT", "VISIBILITY", "URL"],
|
|
1936
2074
|
...list.map((p) => [String(p.port), p.visibility, p.url]),
|
|
1937
2075
|
])
|
|
1938
|
-
: "No shared ports. Share one: runtime
|
|
2076
|
+
: "No shared ports. Share one: runtime sandboxes preview <id> 3000", list);
|
|
1939
2077
|
return 0;
|
|
1940
2078
|
}
|
|
1941
2079
|
case "session": {
|
|
@@ -1952,17 +2090,17 @@ async function sandbox(argv, env, out) {
|
|
|
1952
2090
|
one.origins.join(",") || "-",
|
|
1953
2091
|
]),
|
|
1954
2092
|
])
|
|
1955
|
-
: "No sessions. Make one: runtime
|
|
2093
|
+
: "No sessions. Make one: runtime sandboxes session create <id> --origin https://app.example.com", list);
|
|
1956
2094
|
return 0;
|
|
1957
2095
|
}
|
|
1958
2096
|
if (verb === "revoke") {
|
|
1959
|
-
const sessionId = need(rest[2], "a session id: runtime
|
|
2097
|
+
const sessionId = need(rest[2], "a session id: runtime sandboxes session revoke <id> <session-id>");
|
|
1960
2098
|
const ended = await (await use(rest[1])).sessions.revoke(sessionId);
|
|
1961
2099
|
print(`Session ${ended.id} ended.`, ended);
|
|
1962
2100
|
return 0;
|
|
1963
2101
|
}
|
|
1964
2102
|
if (verb !== "create")
|
|
1965
|
-
throw usage("runtime
|
|
2103
|
+
throw usage("runtime sandboxes session create <id> [--origin <url>]..., session ls <id>, or session revoke <id> <session-id>");
|
|
1966
2104
|
const args = parse(rest.slice(1), [], ["origin", "ttl", "name"]);
|
|
1967
2105
|
const origins = args.flags.get("origin") ?? [];
|
|
1968
2106
|
const ttl = integer(args, "ttl");
|
|
@@ -1976,7 +2114,7 @@ async function sandbox(argv, env, out) {
|
|
|
1976
2114
|
return 0;
|
|
1977
2115
|
}
|
|
1978
2116
|
case "unshare": {
|
|
1979
|
-
const port = portOf(rest[1], "runtime
|
|
2117
|
+
const port = portOf(rest[1], "runtime sandboxes unshare <id> 3000");
|
|
1980
2118
|
const sbx = await use(rest[0]);
|
|
1981
2119
|
print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
|
|
1982
2120
|
return 0;
|
|
@@ -2051,15 +2189,24 @@ function attachment(spec) {
|
|
|
2051
2189
|
/** A secret's value, from standard input only, so it never sits in shell
|
|
2052
2190
|
* history or a process list. Piped: everything, less one trailing newline.
|
|
2053
2191
|
* A terminal: one line, not shown. */
|
|
2192
|
+
/** A pasted secret as the header it becomes: no line breaks (a token copied
|
|
2193
|
+
* from a terminal arrives wrapped), no bracketed-paste markers, no space at
|
|
2194
|
+
* either end. 13 October 2026: Marc's pasted Claude token was refused as
|
|
2195
|
+
* "not visible ASCII". A header cannot hold a line break, so none is lost. */
|
|
2196
|
+
export function cleanSecret(raw) {
|
|
2197
|
+
return raw
|
|
2198
|
+
.replaceAll("\u001b[200~", "")
|
|
2199
|
+
.replaceAll("\u001b[201~", "")
|
|
2200
|
+
.replace(/[\r\n]/g, "")
|
|
2201
|
+
.trim();
|
|
2202
|
+
}
|
|
2054
2203
|
async function readSecret() {
|
|
2055
2204
|
const stdin = process.stdin;
|
|
2056
2205
|
if (!stdin.isTTY) {
|
|
2057
2206
|
const chunks = [];
|
|
2058
2207
|
for await (const chunk of stdin)
|
|
2059
2208
|
chunks.push(chunk);
|
|
2060
|
-
return Buffer.concat(chunks)
|
|
2061
|
-
.toString("utf8")
|
|
2062
|
-
.replace(/\r?\n$/, "");
|
|
2209
|
+
return cleanSecret(Buffer.concat(chunks).toString("utf8"));
|
|
2063
2210
|
}
|
|
2064
2211
|
process.stderr.write("Value (not shown), then Enter: ");
|
|
2065
2212
|
stdin.setRawMode(true);
|
|
@@ -2072,7 +2219,7 @@ async function readSecret() {
|
|
|
2072
2219
|
if (char === "\r" || char === "\n") {
|
|
2073
2220
|
stdin.off("data", onData);
|
|
2074
2221
|
process.stderr.write("\n");
|
|
2075
|
-
return done(value);
|
|
2222
|
+
return done(cleanSecret(value));
|
|
2076
2223
|
}
|
|
2077
2224
|
if (char === "\u0003") {
|
|
2078
2225
|
stdin.off("data", onData);
|
|
@@ -2115,6 +2262,48 @@ export function parseSecretRules(values) {
|
|
|
2115
2262
|
}
|
|
2116
2263
|
return [...byMethods.values()];
|
|
2117
2264
|
}
|
|
2265
|
+
/** `--only-label role=hub,tier=x --only-sandbox <id>`: a sandbox whose labels
|
|
2266
|
+
* hold every pair of any one --only-label gets the secret, and so does each
|
|
2267
|
+
* --only-sandbox. `--all-sandboxes`: every sandbox again. Nothing: undefined,
|
|
2268
|
+
* so a replacement keeps the scope it has. */
|
|
2269
|
+
export function parseSecretScope(labels, sandboxes, all) {
|
|
2270
|
+
if (all && (labels?.length || sandboxes?.length))
|
|
2271
|
+
throw usage("--all-sandboxes gives the secret to every sandbox; leave out --only-label and --only-sandbox.");
|
|
2272
|
+
if (all)
|
|
2273
|
+
return {};
|
|
2274
|
+
if (!labels?.length && !sandboxes?.length)
|
|
2275
|
+
return undefined;
|
|
2276
|
+
const selectors = (labels ?? []).map((text) => {
|
|
2277
|
+
const selector = {};
|
|
2278
|
+
for (const pair of text.split(",").filter(Boolean)) {
|
|
2279
|
+
const at = pair.indexOf("=");
|
|
2280
|
+
if (at < 1)
|
|
2281
|
+
throw usage(`--only-label takes key=value pairs, such as role=hub; got "${text}".`);
|
|
2282
|
+
selector[pair.slice(0, at).trim()] = pair.slice(at + 1).trim();
|
|
2283
|
+
}
|
|
2284
|
+
if (!Object.keys(selector).length)
|
|
2285
|
+
throw usage(`--only-label takes key=value pairs, such as role=hub; got "${text}".`);
|
|
2286
|
+
return selector;
|
|
2287
|
+
});
|
|
2288
|
+
for (const id of sandboxes ?? [])
|
|
2289
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id))
|
|
2290
|
+
throw usage(`--only-sandbox takes a sandbox id; "${id}" is not one.`);
|
|
2291
|
+
return {
|
|
2292
|
+
...(selectors.length ? { labels: selectors } : {}),
|
|
2293
|
+
...(sandboxes?.length
|
|
2294
|
+
? { sandboxes: [...new Set(sandboxes.map((id) => id.toLowerCase()))] }
|
|
2295
|
+
: {}),
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
/** Who gets a secret, in a few words. */
|
|
2299
|
+
export const scopeText = (only) => only && (only.labels?.length || only.sandboxes?.length)
|
|
2300
|
+
? [
|
|
2301
|
+
...(only.labels ?? []).map((selector) => Object.entries(selector)
|
|
2302
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
2303
|
+
.join(",")),
|
|
2304
|
+
...(only.sandboxes ?? []),
|
|
2305
|
+
].join(" | ")
|
|
2306
|
+
: "all";
|
|
2118
2307
|
const ruleText = (rules) => rules?.length
|
|
2119
2308
|
? rules
|
|
2120
2309
|
.map((rule) => `${rule.methods ? `${rule.methods.join(",")} ` : ""}${rule.paths.join(" ")}`)
|
|
@@ -2124,17 +2313,18 @@ export async function secretsCommand(argv, env, out,
|
|
|
2124
2313
|
/** Tests give the value here; the CLI reads standard input. */
|
|
2125
2314
|
read = readSecret) {
|
|
2126
2315
|
const [verb, ...rest] = argv;
|
|
2127
|
-
const args = parse(rest, ["jobs"], ["host", "header", "format", "allow", "value", "version"]);
|
|
2316
|
+
const args = parse(rest, ["jobs", "all-sandboxes"], ["host", "header", "format", "allow", "value", "version", "only-label", "only-sandbox"]);
|
|
2128
2317
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
2129
2318
|
const rows = (list) => list.length
|
|
2130
2319
|
? table([
|
|
2131
|
-
["name", "hosts", "header", "placeholder", "allow", "jobs"],
|
|
2320
|
+
["name", "hosts", "header", "placeholder", "allow", "sandboxes", "jobs"],
|
|
2132
2321
|
...list.map(({ name, sandboxes: secret, jobs }) => [
|
|
2133
2322
|
name,
|
|
2134
2323
|
secret ? secret.hosts.join(",") : "-",
|
|
2135
2324
|
secret?.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
|
|
2136
2325
|
secret ? secret.placeholder : "-",
|
|
2137
2326
|
secret ? ruleText(secret.rules) : "-",
|
|
2327
|
+
secret ? scopeText(secret.only) : "-",
|
|
2138
2328
|
jobs ? `v${jobs.version}` : "-",
|
|
2139
2329
|
]),
|
|
2140
2330
|
])
|
|
@@ -2157,7 +2347,13 @@ read = readSecret) {
|
|
|
2157
2347
|
const name = need(args.positional[0], "the secret's name, such as OPENAI_API_KEY");
|
|
2158
2348
|
const hosts = args.flags.get("host") ?? [];
|
|
2159
2349
|
const jobs = verb === "rotate" || has(args, "jobs");
|
|
2160
|
-
if (verb === "rotate" &&
|
|
2350
|
+
if (verb === "rotate" &&
|
|
2351
|
+
(hosts.length ||
|
|
2352
|
+
args.flags.has("header") ||
|
|
2353
|
+
args.flags.has("allow") ||
|
|
2354
|
+
args.flags.has("only-label") ||
|
|
2355
|
+
args.flags.has("only-sandbox") ||
|
|
2356
|
+
args.flags.has("all-sandboxes")))
|
|
2161
2357
|
throw usage(`rotate replaces the copy for jobs. ${jobsOnly}; to change the sandboxes' copy, set it again with --host.`);
|
|
2162
2358
|
if (!hosts.length && !jobs)
|
|
2163
2359
|
throw usage("Give --host once for each host the value may go to, such as --host api.openai.com, and/or --jobs to let scheduled jobs use it.");
|
|
@@ -2174,38 +2370,51 @@ read = readSecret) {
|
|
|
2174
2370
|
}
|
|
2175
2371
|
if (!hosts.length) {
|
|
2176
2372
|
const saved = await runtime.secrets.set(name, { value, jobs: true });
|
|
2177
|
-
print(`Stored ${name} for jobs, version ${saved.jobs.version}. Bind it: ${me}
|
|
2373
|
+
print(`Stored ${name} for jobs, version ${saved.jobs.version}. Bind it: ${me} jobs create ... --secret ${name}`, saved);
|
|
2178
2374
|
return 0;
|
|
2179
2375
|
}
|
|
2180
2376
|
const header = flag(args, "header");
|
|
2181
2377
|
const format = flag(args, "format");
|
|
2182
2378
|
const allow = args.flags.get("allow");
|
|
2183
2379
|
const rules = allow ? parseSecretRules(allow) : undefined;
|
|
2380
|
+
const only = parseSecretScope(args.flags.get("only-label"), args.flags.get("only-sandbox"), has(args, "all-sandboxes"));
|
|
2184
2381
|
const saved = await runtime.secrets.set(name, {
|
|
2185
2382
|
value,
|
|
2186
2383
|
hosts,
|
|
2187
2384
|
...(header ? { header } : {}),
|
|
2188
2385
|
...(format ? { format } : {}),
|
|
2189
2386
|
...(rules ? { rules } : {}),
|
|
2387
|
+
...(only ? { only } : {}),
|
|
2190
2388
|
...(jobs ? { jobs: true } : {}),
|
|
2191
2389
|
});
|
|
2192
2390
|
print([
|
|
2193
|
-
|
|
2391
|
+
saved.only
|
|
2392
|
+
? `Stored ${saved.name}. Only these sandboxes get it and see ${saved.name}=${saved.placeholder}: ${scopeText(saved.only)}.`
|
|
2393
|
+
: `Stored ${saved.name}. Every sandbox sees ${saved.name}=${saved.placeholder}.`,
|
|
2194
2394
|
saved.header
|
|
2195
2395
|
? `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
|
|
2396
|
+
: `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
2397
|
...(saved.rules?.length ? [`Only these requests get it: ${ruleText(saved.rules)}.`] : []),
|
|
2198
2398
|
...(saved.enforced
|
|
2199
2399
|
? []
|
|
2200
2400
|
: ["A host is catching up; running sandboxes have it within a minute."]),
|
|
2201
2401
|
...(saved.jobs
|
|
2202
2402
|
? [
|
|
2203
|
-
`Also stored for jobs, version ${saved.jobs.version}: --secret ${saved.name} on ${me}
|
|
2403
|
+
`Also stored for jobs, version ${saved.jobs.version}: --secret ${saved.name} on ${me} jobs create.`,
|
|
2204
2404
|
]
|
|
2205
2405
|
: []),
|
|
2206
2406
|
].join("\n"), saved);
|
|
2207
2407
|
return 0;
|
|
2208
2408
|
}
|
|
2409
|
+
if (verb === "scope") {
|
|
2410
|
+
const name = need(args.positional[0], "the secret's name");
|
|
2411
|
+
const only = parseSecretScope(args.flags.get("only-label"), args.flags.get("only-sandbox"), has(args, "all-sandboxes"));
|
|
2412
|
+
if (!only)
|
|
2413
|
+
throw usage("Say who gets it: --only-label key=value (once per selector), --only-sandbox <id>, or --all-sandboxes.");
|
|
2414
|
+
const saved = await (await client(env)).secrets.scope(name, only);
|
|
2415
|
+
print(`${saved.name}: ${saved.only ? `only ${scopeText(saved.only)}` : "every sandbox"}.${saved.enforced ? "" : " A host is catching up; running sandboxes have it within a minute."}`, saved);
|
|
2416
|
+
return 0;
|
|
2417
|
+
}
|
|
2209
2418
|
if (verb === "reveal") {
|
|
2210
2419
|
const name = need(args.positional[0], "the secret's name");
|
|
2211
2420
|
const version = flag(args, "version");
|
|
@@ -2221,7 +2430,7 @@ read = readSecret) {
|
|
|
2221
2430
|
print(`Deleted ${name}${removed.from.length > 1 ? " for sandboxes and jobs" : removed.from[0] === "jobs" ? " for jobs" : ""}.`, removed);
|
|
2222
2431
|
return 0;
|
|
2223
2432
|
}
|
|
2224
|
-
throw usage("secrets takes set, ls, get, rotate, reveal or rm. Run `runtime help`.");
|
|
2433
|
+
throw usage("secrets takes set, scope, ls, get, rotate, reveal or rm. Run `runtime help`.");
|
|
2225
2434
|
}
|
|
2226
2435
|
async function readInput(source) {
|
|
2227
2436
|
if (source !== "-")
|
|
@@ -2339,7 +2548,7 @@ async function desktop(sbx, argv, out) {
|
|
|
2339
2548
|
return 0;
|
|
2340
2549
|
}
|
|
2341
2550
|
}
|
|
2342
|
-
throw usage(`${me}
|
|
2551
|
+
throw usage(`${me} sandboxes desktop <id> start|stop|screenshot|open|click|type|press|windows|record`);
|
|
2343
2552
|
}
|
|
2344
2553
|
async function browser(sbx, argv, out) {
|
|
2345
2554
|
const [action, ...rest] = argv;
|
|
@@ -2353,7 +2562,8 @@ async function browser(sbx, argv, out) {
|
|
|
2353
2562
|
...(integer(args, "height") !== undefined ? { height: integer(args, "height") } : {}),
|
|
2354
2563
|
});
|
|
2355
2564
|
print(`${started.version}${started.headless ? "" : `, on the desktop: ${started.streamUrl}`}\n` +
|
|
2356
|
-
`CDP: ${started.cdpUrl}\n(private; it expires ${started.expiresAt ?? "never"})
|
|
2565
|
+
`CDP: ${started.cdpUrl}\n(private; it expires ${started.expiresAt ?? "never"})` +
|
|
2566
|
+
(started.hint ? `\n${started.hint}` : ""), started);
|
|
2357
2567
|
return 0;
|
|
2358
2568
|
}
|
|
2359
2569
|
case "get":
|
|
@@ -2368,27 +2578,34 @@ async function browser(sbx, argv, out) {
|
|
|
2368
2578
|
return 0;
|
|
2369
2579
|
}
|
|
2370
2580
|
}
|
|
2371
|
-
throw usage(`${me}
|
|
2581
|
+
throw usage(`${me} sandboxes browser <id> start [--headed]|get|stop`);
|
|
2372
2582
|
}
|
|
2373
2583
|
const KEYS_HELP = `runtime keys <command>
|
|
2374
2584
|
|
|
2375
|
-
create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide]
|
|
2376
|
-
A new API key
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
--
|
|
2381
|
-
|
|
2382
|
-
|
|
2585
|
+
create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide] [--browser]
|
|
2586
|
+
A new API key. With a key saved on this machine (or RUNTIME_API_KEY), that
|
|
2587
|
+
key makes it at once, with no browser: a key per worker in an agent swarm.
|
|
2588
|
+
It never has more than the key that made it, and when that key has a daily
|
|
2589
|
+
limit, the keys it made share it, so each needs --daily-limit. Without a
|
|
2590
|
+
saved key, or with --browser, it prints a link and a code and an account
|
|
2591
|
+
owner approves the key in the browser. The key is printed once, alone on
|
|
2592
|
+
stdout, and lasts until revoked. --json prints it as JSON, still alone on
|
|
2593
|
+
stdout. --account-wide makes a key that sees and uses every sandbox and
|
|
2594
|
+
other resource in the account; without it, a key sees only what it makes.
|
|
2595
|
+
ls This key and the keys it made, with what each spent today
|
|
2596
|
+
usage <keyId> What one of them has spent and holds
|
|
2597
|
+
revoke <keyId> Revoke one: a worker's key when the worker is done
|
|
2383
2598
|
|
|
2384
|
-
runtime keys create --name
|
|
2599
|
+
runtime keys create --name worker-1 --daily-limit 5
|
|
2600
|
+
runtime keys create --name ci --daily-limit 25 --browser | gh secret set RUNTIME_API_KEY
|
|
2385
2601
|
|
|
2386
|
-
|
|
2602
|
+
Making, listing and revoking keys with a key needs one an owner or admin made.
|
|
2603
|
+
Every key is also at https://withruntime.com/account/keys.
|
|
2387
2604
|
`;
|
|
2388
|
-
/* `runtime keys
|
|
2389
|
-
browser
|
|
2390
|
-
|
|
2391
|
-
|
|
2605
|
+
/* `runtime keys`: a key made from the terminal, by this machine's key with no
|
|
2606
|
+
browser (Marc, 14 October 2026, agent swarms), or approved in the browser
|
|
2607
|
+
(Marc, 23 September 2026; ARCHITECTURE.md section 10, "Keys from the
|
|
2608
|
+
terminal"). Only the options the keys page offers. */
|
|
2392
2609
|
/* `runtime account`: the accounts this machine is connected to, and which one
|
|
2393
2610
|
every command uses (ARCHITECTURE.md section 3.9). Each is its own saved
|
|
2394
2611
|
connection with its own key; the SDK itself stays key-based. */
|
|
@@ -2494,19 +2711,63 @@ async function nameSaved(env) {
|
|
|
2494
2711
|
await store.save({ ...again, orgName: who.orgName });
|
|
2495
2712
|
}
|
|
2496
2713
|
}
|
|
2714
|
+
/** Dollars from integer microdollars, for a person to read: "$5.00". */
|
|
2715
|
+
const usdOf = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
2497
2716
|
async function keys(argv, env, out) {
|
|
2498
2717
|
product = "keys";
|
|
2499
2718
|
const [verb, ...rest] = argv;
|
|
2719
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
2500
2720
|
if (!verb || verb === "help" || verb === "--help" || verb === "-h") {
|
|
2501
2721
|
out.write(out.json ? JSON.stringify({ usage: named(KEYS_HELP) }) : named(KEYS_HELP));
|
|
2502
2722
|
return 0;
|
|
2503
2723
|
}
|
|
2504
|
-
if (verb === "ls" || verb === "list"
|
|
2505
|
-
|
|
2724
|
+
if (verb === "ls" || verb === "list") {
|
|
2725
|
+
const args = parse(rest, [], []);
|
|
2726
|
+
if (args.flags.size || args.positional.length)
|
|
2727
|
+
throw usage(`Usage: ${named("runtime keys ls")}`);
|
|
2728
|
+
const all = await (await client(env)).keys.list();
|
|
2729
|
+
print(all.length
|
|
2730
|
+
? table([
|
|
2731
|
+
["KEY", "NAME", "ACCESS", "REACH", "DAILY LIMIT", "LAST 24H", "MADE BY", "STATE"],
|
|
2732
|
+
...all.map((k) => [
|
|
2733
|
+
k.keyId,
|
|
2734
|
+
k.name,
|
|
2735
|
+
k.access,
|
|
2736
|
+
k.reach,
|
|
2737
|
+
k.dailyLimitMicros === null ? "none" : usdOf(k.dailyLimitMicros),
|
|
2738
|
+
usdOf(k.dailyUsedMicros),
|
|
2739
|
+
k.madeBy ?? "website",
|
|
2740
|
+
k.revokedAt ? "revoked" : "live",
|
|
2741
|
+
]),
|
|
2742
|
+
])
|
|
2743
|
+
: "No keys.", all);
|
|
2744
|
+
return 0;
|
|
2745
|
+
}
|
|
2746
|
+
if (verb === "revoke" || verb === "rm" || verb === "usage") {
|
|
2747
|
+
const args = parse(rest, [], []);
|
|
2748
|
+
const id = args.positional[0];
|
|
2749
|
+
if (!id || args.positional.length > 1 || args.flags.size)
|
|
2750
|
+
throw usage(`Usage: ${named(`runtime keys ${verb} <keyId>`)}`);
|
|
2751
|
+
const api = (await client(env)).keys;
|
|
2752
|
+
if (verb === "usage") {
|
|
2753
|
+
const used = await api.usage(id);
|
|
2754
|
+
print([
|
|
2755
|
+
`${used.name} (${used.keyId})${used.revokedAt ? ", revoked" : ""}`,
|
|
2756
|
+
` last 24 hours ${usdOf(used.dailyUsedMicros)}${used.dailyLimitMicros === null ? "" : ` of ${usdOf(used.dailyLimitMicros)}`}`,
|
|
2757
|
+
` spent ${usdOf(used.spentMicros)}`,
|
|
2758
|
+
` held now ${usdOf(used.heldMicros)}`,
|
|
2759
|
+
` sandboxes ${used.runningSandboxes} running, ${used.resources} kept in all`,
|
|
2760
|
+
].join("\n"), used);
|
|
2761
|
+
return 0;
|
|
2762
|
+
}
|
|
2763
|
+
const revoked = await api.revoke(id);
|
|
2764
|
+
print(`Revoked ${revoked.keyId}. It stops working within ten seconds.`, revoked);
|
|
2765
|
+
return 0;
|
|
2766
|
+
}
|
|
2506
2767
|
if (verb !== "create")
|
|
2507
|
-
throw unknown("keys command", verb, ["create"]);
|
|
2508
|
-
const args = parse(rest, ["read-only", "account-wide"], ["name", "daily-limit"]);
|
|
2509
|
-
const known = new Set(["name", "read-only", "daily-limit", "account-wide"]);
|
|
2768
|
+
throw unknown("keys command", verb, ["create", "ls", "usage", "revoke"]);
|
|
2769
|
+
const args = parse(rest, ["read-only", "account-wide", "browser"], ["name", "daily-limit"]);
|
|
2770
|
+
const known = new Set(["name", "read-only", "daily-limit", "account-wide", "browser"]);
|
|
2510
2771
|
for (const name of args.flags.keys())
|
|
2511
2772
|
if (!known.has(name))
|
|
2512
2773
|
throw usage(`Unknown option --${name}.`);
|
|
@@ -2519,36 +2780,58 @@ async function keys(argv, env, out) {
|
|
|
2519
2780
|
if (readOnly && has(args, "account-wide"))
|
|
2520
2781
|
throw usage("A read-only key already sees the whole account, so it takes no --account-wide.");
|
|
2521
2782
|
const { createKey, dailyLimitMicros } = await import("./keys.js");
|
|
2522
|
-
const
|
|
2783
|
+
const request = {
|
|
2523
2784
|
name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
|
|
2524
2785
|
access: readOnly ? "read" : "full",
|
|
2525
2786
|
...(has(args, "account-wide") ? { reach: "account" } : {}),
|
|
2526
2787
|
dailyLimitMicros: limit === undefined ? null : dailyLimitMicros(limit),
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
|
|
2788
|
+
};
|
|
2789
|
+
/* A key on this machine makes the new one itself, with no browser: an
|
|
2790
|
+
agent with no person, a swarm's coordinator (Marc, 14 October 2026). */
|
|
2791
|
+
const saved = has(args, "browser")
|
|
2792
|
+
? null
|
|
2793
|
+
: env.RUNTIME_API_KEY || (await connectionStore(env).read())?.key || null;
|
|
2794
|
+
let key;
|
|
2795
|
+
if (saved) {
|
|
2796
|
+
key = await new Runtime({
|
|
2797
|
+
apiKey: saved,
|
|
2798
|
+
baseUrl: env.RUNTIME_API_URL,
|
|
2799
|
+
maxRetries: 4,
|
|
2800
|
+
}).keys.create({
|
|
2801
|
+
name: request.name,
|
|
2802
|
+
access: request.access,
|
|
2803
|
+
reach: request.reach ?? "agent",
|
|
2804
|
+
dailyLimitMicros: request.dailyLimitMicros,
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
else
|
|
2808
|
+
key = await createKey(request, env, { notify: out.error });
|
|
2809
|
+
const { confirm, ...made } = key;
|
|
2810
|
+
out.write(out.json ? JSON.stringify(made) : made.key);
|
|
2530
2811
|
const terms = [
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2812
|
+
made.access === "read" ? "read only" : "full access",
|
|
2813
|
+
made.reach === "account" ? "account-wide" : undefined,
|
|
2814
|
+
made.access === "read"
|
|
2534
2815
|
? undefined
|
|
2535
|
-
:
|
|
2816
|
+
: made.dailyLimitMicros === null
|
|
2536
2817
|
? "no daily limit"
|
|
2537
|
-
: `daily limit
|
|
2538
|
-
"until revoked",
|
|
2818
|
+
: `daily limit ${usdOf(made.dailyLimitMicros)}`,
|
|
2819
|
+
made.expires ? `until ${made.expires}` : "until revoked",
|
|
2539
2820
|
].filter(Boolean);
|
|
2540
|
-
out.error(
|
|
2541
|
-
|
|
2821
|
+
out.error(saved
|
|
2822
|
+
? `Created key "${made.name}" (${terms.join(", ")}), made by this machine's key. Store it now: it is not shown again, and Runtime keeps only a hash of it.\nGive it to one worker as RUNTIME_API_KEY; revoke it with ${named("runtime keys revoke")} ${made.keyId} when that worker is done.`
|
|
2823
|
+
: `Created key "${made.name}" (${terms.join(", ")}). Store it now: it is not shown again, and Runtime keeps only a hash of it.\nIn CI, give it to your jobs as RUNTIME_API_KEY from the secret store.`);
|
|
2824
|
+
await confirm?.();
|
|
2542
2825
|
return 0;
|
|
2543
2826
|
}
|
|
2544
|
-
const SNAPSHOT_HELP = `runtime
|
|
2827
|
+
const SNAPSHOT_HELP = `runtime snapshots <command>
|
|
2545
2828
|
|
|
2546
2829
|
ls [--sandbox <id>] [--name n] Your snapshots
|
|
2547
2830
|
get <id> One snapshot
|
|
2548
|
-
extend <id> <days>
|
|
2831
|
+
extend <id> <days|credit> Keep it longer; credit: while you have credit
|
|
2549
2832
|
rm <id> Delete it
|
|
2550
|
-
Take one with \`runtime
|
|
2551
|
-
with \`runtime
|
|
2833
|
+
Take one with \`runtime sandboxes snapshot <sandboxId>\`; start a sandbox from it
|
|
2834
|
+
with \`runtime sandboxes create --snapshot <snapshotId>\`.
|
|
2552
2835
|
`;
|
|
2553
2836
|
async function snapshot(argv, env, out) {
|
|
2554
2837
|
const [verb, ...rest] = argv;
|
|
@@ -2571,9 +2854,15 @@ async function snapshot(argv, env, out) {
|
|
|
2571
2854
|
print(all.length
|
|
2572
2855
|
? table([
|
|
2573
2856
|
["ID", "NAME", "STATUS", "FROM", "EXPIRES"],
|
|
2574
|
-
...all.map((s) => [
|
|
2857
|
+
...all.map((s) => [
|
|
2858
|
+
s.id,
|
|
2859
|
+
s.name ?? "-",
|
|
2860
|
+
s.status,
|
|
2861
|
+
s.sourceSandboxId,
|
|
2862
|
+
s.expiresAt ?? "with credit",
|
|
2863
|
+
]),
|
|
2575
2864
|
])
|
|
2576
|
-
: "No snapshots. Take one: runtime
|
|
2865
|
+
: "No snapshots. Take one: runtime sandboxes snapshot <sandboxId>", all);
|
|
2577
2866
|
return 0;
|
|
2578
2867
|
}
|
|
2579
2868
|
case "get": {
|
|
@@ -2582,9 +2871,11 @@ async function snapshot(argv, env, out) {
|
|
|
2582
2871
|
return 0;
|
|
2583
2872
|
}
|
|
2584
2873
|
case "extend": {
|
|
2585
|
-
const
|
|
2874
|
+
const word = parse(rest.slice(1), [], ["days"]);
|
|
2875
|
+
const days = daysOrCredit(word, "days") ??
|
|
2876
|
+
(need(rest[1], "days") === "credit" ? null : Number(need(rest[1], "days")));
|
|
2586
2877
|
const found = await api.extend(need(rest[0], "a snapshot id"), days);
|
|
2587
|
-
print(`${found.id} kept until ${found.expiresAt}`, found);
|
|
2878
|
+
print(`${found.id} kept ${found.expiresAt === null ? "while you have credit" : `until ${found.expiresAt}`}`, found);
|
|
2588
2879
|
return 0;
|
|
2589
2880
|
}
|
|
2590
2881
|
case "rm":
|
|
@@ -2597,7 +2888,7 @@ async function snapshot(argv, env, out) {
|
|
|
2597
2888
|
}
|
|
2598
2889
|
throw unknown("snapshot command", verb, ["ls", "get", "extend", "rm"]);
|
|
2599
2890
|
}
|
|
2600
|
-
const IMAGE_HELP = `runtime
|
|
2891
|
+
const IMAGE_HELP = `runtime images <command>
|
|
2601
2892
|
|
|
2602
2893
|
build [<folder>] [--dockerfile|-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
|
|
2603
2894
|
[--name <name>] [--tag|-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
|
|
@@ -2612,8 +2903,9 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
2612
2903
|
ls Your images
|
|
2613
2904
|
versions <name> Every version of a name, newest first
|
|
2614
2905
|
get <image> One image: an id, name, name:tag or name@version
|
|
2615
|
-
logs <image> [--follow]
|
|
2616
|
-
Its build log
|
|
2906
|
+
logs <image> [--follow] [--raw]
|
|
2907
|
+
Its build log; on a terminal control characters
|
|
2908
|
+
show as \\uXXXX unless --raw
|
|
2617
2909
|
tag <image> <tag> Point a tag of the image's name at this version
|
|
2618
2910
|
untag <image> <tag> Take a tag off
|
|
2619
2911
|
rm <image> Delete that version and its tags
|
|
@@ -2624,7 +2916,7 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
2624
2916
|
registry set <ecr registry> --access-key-id <id>
|
|
2625
2917
|
Amazon ECR: the secret access key is read from standard input
|
|
2626
2918
|
registry rm <registry>
|
|
2627
|
-
Use one: runtime
|
|
2919
|
+
Use one: runtime sandboxes create --image app:latest
|
|
2628
2920
|
`;
|
|
2629
2921
|
/** Options `image build` takes with a value. */
|
|
2630
2922
|
const IMAGE_BUILD_VALUES = [
|
|
@@ -2759,7 +3051,7 @@ read = readSecret) {
|
|
|
2759
3051
|
])
|
|
2760
3052
|
: name
|
|
2761
3053
|
? `No versions of ${name}.`
|
|
2762
|
-
: "No images. Build one: runtime
|
|
3054
|
+
: "No images. Build one: runtime images build --pip pandas", all);
|
|
2763
3055
|
return 0;
|
|
2764
3056
|
}
|
|
2765
3057
|
case "get": {
|
|
@@ -2768,13 +3060,14 @@ read = readSecret) {
|
|
|
2768
3060
|
return 0;
|
|
2769
3061
|
}
|
|
2770
3062
|
case "logs": {
|
|
2771
|
-
const args = parse(rest, ["follow", "f"], []);
|
|
3063
|
+
const args = parse(rest, ["follow", "f", "raw"], []);
|
|
2772
3064
|
const id = (await api.resolve(need(args.positional[0], "an image"))).id;
|
|
3065
|
+
const shown = logText(args);
|
|
2773
3066
|
if (has(args, "follow") || has(args, "f")) {
|
|
2774
3067
|
const lines = [];
|
|
2775
|
-
const done = await api.followLogs(id, (line) => out.json ? lines.push(line.text) : out.write(line.text));
|
|
3068
|
+
const done = await api.followLogs(id, (line) => out.json ? lines.push(line.text) : out.write(shown(line.text)));
|
|
2776
3069
|
if (out.json)
|
|
2777
|
-
|
|
3070
|
+
out.write(shown(JSON.stringify({ lines, state: done.state })));
|
|
2778
3071
|
return done.state === "failed" ? 1 : 0;
|
|
2779
3072
|
}
|
|
2780
3073
|
let after = 0;
|
|
@@ -2786,7 +3079,7 @@ read = readSecret) {
|
|
|
2786
3079
|
break;
|
|
2787
3080
|
after = page.nextAfter;
|
|
2788
3081
|
}
|
|
2789
|
-
|
|
3082
|
+
out.write(shown(out.json ? JSON.stringify(lines) : lines.join("\n") || "(no log yet)"));
|
|
2790
3083
|
return 0;
|
|
2791
3084
|
}
|
|
2792
3085
|
case "tag":
|
|
@@ -2813,7 +3106,7 @@ read = readSecret) {
|
|
|
2813
3106
|
["REGISTRY", "KIND", "USER", "SINCE"],
|
|
2814
3107
|
...all.map((r) => [r.registry, r.kind, r.username, r.createdAt]),
|
|
2815
3108
|
])
|
|
2816
|
-
: "No registry credentials. Store some: runtime
|
|
3109
|
+
: "No registry credentials. Store some: runtime images registry set ghcr.io --username you", all);
|
|
2817
3110
|
return 0;
|
|
2818
3111
|
}
|
|
2819
3112
|
if (action === "set") {
|
|
@@ -2853,10 +3146,10 @@ read = readSecret) {
|
|
|
2853
3146
|
"registry",
|
|
2854
3147
|
]);
|
|
2855
3148
|
}
|
|
2856
|
-
const JOB_HELP = `runtime
|
|
3149
|
+
const JOB_HELP = `runtime jobs <command>
|
|
2857
3150
|
|
|
2858
3151
|
A job runs a command in a fresh sandbox, once or on a schedule. Each run spends
|
|
2859
|
-
the
|
|
3152
|
+
the free usage first, then credit at the sandbox rates.
|
|
2860
3153
|
|
|
2861
3154
|
create <name> (--cron "<min hour day month weekday>" [--timezone Europe/Berlin] | --at <time|now>)
|
|
2862
3155
|
[--vcpu 2] [--memory 4096] [--disk 4096] [--cpu shared|reserved] [--cpu-floor <thousandths>]
|
|
@@ -2871,14 +3164,21 @@ const JOB_HELP = `runtime job <command>
|
|
|
2871
3164
|
ls Your jobs: state, schedule and next run
|
|
2872
3165
|
get <id> One job, in full
|
|
2873
3166
|
runs <id> Its runs: state, exit code, when
|
|
2874
|
-
logs <runId|jobId> [-f]
|
|
3167
|
+
logs <runId|jobId> [-f] [--raw] A run's output (a job id: its latest run); -f follows it;
|
|
3168
|
+
on a terminal control characters show as \\uXXXX
|
|
3169
|
+
unless --raw
|
|
2875
3170
|
pause <id> | resume <id> Stop or restart scheduling; a run already going finishes
|
|
3171
|
+
trigger <id> Run it now, once; its schedule does not move, and a
|
|
3172
|
+
paused job stays paused
|
|
3173
|
+
reschedule <id> (--cron "<five fields>" [--timezone Europe/Berlin] | --at <time|now>)
|
|
3174
|
+
A new schedule; the next run is its next time
|
|
2876
3175
|
cancel <id> End it for good and stop a run in progress
|
|
2877
3176
|
|
|
2878
3177
|
Examples
|
|
2879
|
-
runtime
|
|
2880
|
-
runtime
|
|
2881
|
-
runtime
|
|
3178
|
+
runtime jobs create nightly --cron "0 3 * * *" --timezone Europe/Berlin -- python3 /workspace/report.py
|
|
3179
|
+
runtime jobs create once --at now -- bash -lc 'echo hello from a job'
|
|
3180
|
+
runtime jobs trigger <jobId> && runtime jobs logs <jobId> -f
|
|
3181
|
+
runtime jobs reschedule <jobId> --cron "30 4 * * 1-5" --timezone Europe/Berlin
|
|
2882
3182
|
`;
|
|
2883
3183
|
const when = (ms) => (ms == null ? "-" : new Date(ms).toISOString());
|
|
2884
3184
|
function scheduleText(job) {
|
|
@@ -2926,13 +3226,13 @@ async function jobCommand(argv, env, out) {
|
|
|
2926
3226
|
"max-cost",
|
|
2927
3227
|
"max-total-cost",
|
|
2928
3228
|
"region",
|
|
2929
|
-
], `${me}
|
|
3229
|
+
], `${me} jobs create <name> --cron "0 3 * * *" -- python3 -c '...'`);
|
|
2930
3230
|
const name = need(args.positional[0], "the job's name, such as nightly-report");
|
|
2931
3231
|
if (args.positional.length > 1)
|
|
2932
|
-
throw usage(`Put the command after --: ${me}
|
|
3232
|
+
throw usage(`Put the command after --: ${me} jobs create ${name} ... -- ${args.positional.slice(1).join(" ")}`);
|
|
2933
3233
|
const argv = args.rest ?? [];
|
|
2934
3234
|
if (!argv.length)
|
|
2935
|
-
throw usage(`Give the command after --: ${me}
|
|
3235
|
+
throw usage(`Give the command after --: ${me} jobs create ${name} --at now -- echo hello`);
|
|
2936
3236
|
const cron = flag(args, "cron");
|
|
2937
3237
|
const at = flag(args, "at");
|
|
2938
3238
|
if ((cron === undefined) === (at === undefined))
|
|
@@ -2989,7 +3289,7 @@ async function jobCommand(argv, env, out) {
|
|
|
2989
3289
|
: {}),
|
|
2990
3290
|
});
|
|
2991
3291
|
if (!out.json)
|
|
2992
|
-
out.error(`Scheduled ${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}. Follow it: ${me}
|
|
3292
|
+
out.error(`Scheduled ${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}. Follow it: ${me} jobs logs ${job.id} -f`);
|
|
2993
3293
|
print(job.id, job);
|
|
2994
3294
|
return 0;
|
|
2995
3295
|
}
|
|
@@ -3009,7 +3309,7 @@ async function jobCommand(argv, env, out) {
|
|
|
3009
3309
|
j.blockedReason ?? "-",
|
|
3010
3310
|
]),
|
|
3011
3311
|
])
|
|
3012
|
-
: `No jobs. Schedule one: ${me}
|
|
3312
|
+
: `No jobs. Schedule one: ${me} jobs create hello --at now -- echo hello`, all);
|
|
3013
3313
|
return 0;
|
|
3014
3314
|
}
|
|
3015
3315
|
case "get": {
|
|
@@ -3038,8 +3338,13 @@ async function jobCommand(argv, env, out) {
|
|
|
3038
3338
|
return 0;
|
|
3039
3339
|
}
|
|
3040
3340
|
case "logs": {
|
|
3041
|
-
const args = parse(rest, ["f", "follow"], []);
|
|
3341
|
+
const args = parse(rest, ["f", "follow", "raw"], []);
|
|
3042
3342
|
const id = need(args.positional[0], "a run id or a job id");
|
|
3343
|
+
const [stdout, stderr, shown] = [
|
|
3344
|
+
logStream(process.stdout, has(args, "raw")),
|
|
3345
|
+
logStream(process.stderr, has(args, "raw")),
|
|
3346
|
+
logText(args),
|
|
3347
|
+
];
|
|
3043
3348
|
let runId = id;
|
|
3044
3349
|
try {
|
|
3045
3350
|
await api.run(id);
|
|
@@ -3067,25 +3372,27 @@ async function jobCommand(argv, env, out) {
|
|
|
3067
3372
|
for (;;) {
|
|
3068
3373
|
const page = await api.logs(runId, { cursor, limitBytes: 65536 });
|
|
3069
3374
|
if (out.json)
|
|
3070
|
-
out.write(JSON.stringify(page));
|
|
3375
|
+
out.write(shown(JSON.stringify(page)));
|
|
3071
3376
|
else
|
|
3072
3377
|
for (const chunk of page.chunks)
|
|
3073
|
-
(chunk.stream === "stderr" ?
|
|
3378
|
+
(chunk.stream === "stderr" ? stderr : stdout)(chunk.text);
|
|
3074
3379
|
cursor = page.nextCursor;
|
|
3075
3380
|
if (page.complete) {
|
|
3381
|
+
if (!follow && out.json)
|
|
3382
|
+
return 0;
|
|
3383
|
+
const run = await api.run(runId);
|
|
3076
3384
|
if (!out.json) {
|
|
3077
|
-
const run = await api.run(runId);
|
|
3078
3385
|
if (page.truncated)
|
|
3079
3386
|
process.stderr.write("[the run printed more than it keeps; this is its last part]\n");
|
|
3080
3387
|
process.stderr.write(`[${run.state}${run.exitCode === null ? "" : `, exit code ${run.exitCode}`}${run.reason ? `, ${run.reason}` : ""}]\n`);
|
|
3081
3388
|
}
|
|
3082
|
-
return 0;
|
|
3389
|
+
return follow ? (run.exitCode ?? (run.state === "succeeded" ? 0 : 1)) : 0;
|
|
3083
3390
|
}
|
|
3084
3391
|
if (page.chunks.length)
|
|
3085
3392
|
continue;
|
|
3086
3393
|
if (!follow) {
|
|
3087
3394
|
if (!out.json)
|
|
3088
|
-
process.stderr.write(`[${runId} is still going; follow it: ${me}
|
|
3395
|
+
process.stderr.write(`[${runId} is still going; follow it: ${me} jobs logs ${runId} -f]\n`);
|
|
3089
3396
|
return 0;
|
|
3090
3397
|
}
|
|
3091
3398
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -3099,6 +3406,27 @@ async function jobCommand(argv, env, out) {
|
|
|
3099
3406
|
print(`${job.name} ${job.state}.`, job);
|
|
3100
3407
|
return 0;
|
|
3101
3408
|
}
|
|
3409
|
+
case "trigger": {
|
|
3410
|
+
const id = need(rest[0], "a job id");
|
|
3411
|
+
const job = await api.trigger(id);
|
|
3412
|
+
print(`${job.name} runs now${job.state === "paused" ? ", once; it stays paused" : ""}. Follow it: ${me} jobs logs ${job.id} -f`, job);
|
|
3413
|
+
return 0;
|
|
3414
|
+
}
|
|
3415
|
+
case "reschedule": {
|
|
3416
|
+
const args = parse(rest, [], ["cron", "timezone", "at"], `${me} jobs reschedule <id> --cron "0 3 * * *" [--timezone Europe/Berlin]`);
|
|
3417
|
+
const id = need(args.positional[0], "a job id");
|
|
3418
|
+
const cron = flag(args, "cron");
|
|
3419
|
+
const at = flag(args, "at");
|
|
3420
|
+
if ((cron === undefined) === (at === undefined))
|
|
3421
|
+
throw usage('Give --cron "<five fields>" for a schedule, or --at <time|now> for one run.');
|
|
3422
|
+
if (at !== undefined && flag(args, "timezone") !== undefined)
|
|
3423
|
+
throw usage("--timezone goes with --cron; --at takes a time with its own offset.");
|
|
3424
|
+
const job = await api.reschedule(id, cron !== undefined
|
|
3425
|
+
? { cron, ...(flag(args, "timezone") ? { timezone: flag(args, "timezone") } : {}) }
|
|
3426
|
+
: { at: atTime(at) });
|
|
3427
|
+
print(`${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}.`, job);
|
|
3428
|
+
return 0;
|
|
3429
|
+
}
|
|
3102
3430
|
}
|
|
3103
3431
|
throw unknown("job command", verb, [
|
|
3104
3432
|
"create",
|
|
@@ -3108,12 +3436,15 @@ async function jobCommand(argv, env, out) {
|
|
|
3108
3436
|
"logs",
|
|
3109
3437
|
"pause",
|
|
3110
3438
|
"resume",
|
|
3439
|
+
"trigger",
|
|
3440
|
+
"reschedule",
|
|
3111
3441
|
"cancel",
|
|
3112
3442
|
]);
|
|
3113
3443
|
}
|
|
3114
|
-
const VOLUME_HELP = `runtime
|
|
3444
|
+
const VOLUME_HELP = `runtime volumes <command>
|
|
3115
3445
|
|
|
3116
|
-
create --size-mib <N> [--name n] [--shared]
|
|
3446
|
+
create --size-mib <N> [--name n] [--label k=v]... [--shared]
|
|
3447
|
+
A persistent disk; prints its id
|
|
3117
3448
|
ls Your volumes
|
|
3118
3449
|
get <id> One volume, its backups and where it is attached
|
|
3119
3450
|
resize <id> --size-mib <N> Grow a detached volume on its current host
|
|
@@ -3128,7 +3459,7 @@ const VOLUME_HELP = `runtime volume <command>
|
|
|
3128
3459
|
backup-rm <backupId> Delete a backup
|
|
3129
3460
|
backup-policy <id> [--daily on|off] [--retention-days N]
|
|
3130
3461
|
Daily backups on or off, and how long each is kept
|
|
3131
|
-
Attach one: runtime
|
|
3462
|
+
Attach one: runtime sandboxes create --volume <id>:/data
|
|
3132
3463
|
Volumes are backed up off their host daily unless you turn it off.
|
|
3133
3464
|
Shared commands work where shared disks are enabled.
|
|
3134
3465
|
`;
|
|
@@ -3142,10 +3473,11 @@ async function volume(argv, env, out) {
|
|
|
3142
3473
|
const api = (await client(env)).volumes;
|
|
3143
3474
|
switch (verb) {
|
|
3144
3475
|
case "create": {
|
|
3145
|
-
const args = parse(rest, ["shared"], ["size-mib", "name"]);
|
|
3476
|
+
const args = parse(rest, ["shared"], ["size-mib", "name", "label"]);
|
|
3146
3477
|
const created = await api.create({
|
|
3147
3478
|
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
3148
3479
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
3480
|
+
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
3149
3481
|
...(has(args, "shared") ? { shared: true } : {}),
|
|
3150
3482
|
});
|
|
3151
3483
|
print(created.id, created);
|
|
@@ -3166,7 +3498,7 @@ async function volume(argv, env, out) {
|
|
|
3166
3498
|
v.attachments.map((a) => `${a.sandboxId}:${a.path}`).join(", ") || "-",
|
|
3167
3499
|
]),
|
|
3168
3500
|
])
|
|
3169
|
-
: "No volumes. Create one: runtime
|
|
3501
|
+
: "No volumes. Create one: runtime volumes create --size-mib 10240", all);
|
|
3170
3502
|
return 0;
|
|
3171
3503
|
}
|
|
3172
3504
|
case "get": {
|
|
@@ -3178,7 +3510,7 @@ async function volume(argv, env, out) {
|
|
|
3178
3510
|
const args = parse(rest, [], ["size-mib"]);
|
|
3179
3511
|
const id = need(args.positional[0], "a volume id");
|
|
3180
3512
|
if (args.positional.length !== 1 || args.rest)
|
|
3181
|
-
throw usage(`Use ${me}
|
|
3513
|
+
throw usage(`Use ${me} volumes resize <id> --size-mib <N>.`);
|
|
3182
3514
|
const sizeMiB = integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>"));
|
|
3183
3515
|
const resized = await api.resize(id, { sizeMiB });
|
|
3184
3516
|
print(describeRecord(resized, table), resized);
|
|
@@ -3190,7 +3522,7 @@ async function volume(argv, env, out) {
|
|
|
3190
3522
|
const sandboxId = need(args.positional[1], "a sandbox id");
|
|
3191
3523
|
const path = need(flag(args, "path"), "--path /data");
|
|
3192
3524
|
if (args.positional.length !== 2 || args.rest)
|
|
3193
|
-
throw usage(`Use ${me}
|
|
3525
|
+
throw usage(`Use ${me} volumes attach <id> <sandboxId> --path /data.`);
|
|
3194
3526
|
const attachment = await api.attach(id, { sandboxId, path });
|
|
3195
3527
|
print(describeRecord(attachment, table), attachment);
|
|
3196
3528
|
return attachment.error ? 1 : 0;
|
|
@@ -3201,7 +3533,7 @@ async function volume(argv, env, out) {
|
|
|
3201
3533
|
const id = need(args.positional[0], "a volume id");
|
|
3202
3534
|
const attachmentId = need(args.positional[1], "an attachment id");
|
|
3203
3535
|
if (args.positional.length !== 2 || args.rest)
|
|
3204
|
-
throw usage(`Use ${me}
|
|
3536
|
+
throw usage(`Use ${me} volumes ${verb} <id> <attachmentId>.`);
|
|
3205
3537
|
const attachment = verb === "detach"
|
|
3206
3538
|
? await api.detach(id, attachmentId)
|
|
3207
3539
|
: await api.getAttachment(id, attachmentId);
|
|
@@ -3210,7 +3542,8 @@ async function volume(argv, env, out) {
|
|
|
3210
3542
|
}
|
|
3211
3543
|
case "rm":
|
|
3212
3544
|
case "delete": {
|
|
3213
|
-
const
|
|
3545
|
+
const id = need(rest[0], "a volume id");
|
|
3546
|
+
const found = await withVolumeRelease((options) => api.delete(id, options), out);
|
|
3214
3547
|
print(`${found.id} ${found.state}`, found);
|
|
3215
3548
|
return 0;
|
|
3216
3549
|
}
|
|
@@ -3220,7 +3553,7 @@ async function volume(argv, env, out) {
|
|
|
3220
3553
|
const days = integer(args, "retention-days");
|
|
3221
3554
|
const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
|
|
3222
3555
|
if (!out.json && made.state === "pending")
|
|
3223
|
-
out.error(`Copying it off the host; check with: ${me}
|
|
3556
|
+
out.error(`Copying it off the host; check with: ${me} volumes backups ${id}`);
|
|
3224
3557
|
print(made.id, made);
|
|
3225
3558
|
return 0;
|
|
3226
3559
|
}
|
|
@@ -3248,7 +3581,7 @@ async function volume(argv, env, out) {
|
|
|
3248
3581
|
const args = parse(rest, [], ["name"]);
|
|
3249
3582
|
const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
|
|
3250
3583
|
if (!out.json && created.state === "creating")
|
|
3251
|
-
out.error(`Restoring; check with: ${me}
|
|
3584
|
+
out.error(`Restoring; check with: ${me} volumes get ${created.id}`);
|
|
3252
3585
|
print(created.id, created);
|
|
3253
3586
|
return 0;
|
|
3254
3587
|
}
|
|
@@ -3307,12 +3640,47 @@ const CREATE_FLAGS = [
|
|
|
3307
3640
|
function waiting(out) {
|
|
3308
3641
|
const said = new Set();
|
|
3309
3642
|
return (refusal) => {
|
|
3643
|
+
// The CLI's volume wait owns this refusal, including its deadline.
|
|
3644
|
+
if (refusal.code === "volume_releasing")
|
|
3645
|
+
throw refusal;
|
|
3310
3646
|
if (said.has(refusal.code))
|
|
3311
3647
|
return;
|
|
3312
3648
|
said.add(refusal.code);
|
|
3313
3649
|
out.error(`${refusal.message} Waiting for room, up to 2 minutes; Ctrl-C stops.`);
|
|
3314
3650
|
};
|
|
3315
3651
|
}
|
|
3652
|
+
/** A stopping sandbox can take a minute to let go of its volume. Retry only
|
|
3653
|
+
* that refusal, with the same key so a create cannot make two sandboxes. */
|
|
3654
|
+
async function withVolumeRelease(operation, out) {
|
|
3655
|
+
const idempotencyKey = crypto.randomUUID();
|
|
3656
|
+
let until;
|
|
3657
|
+
for (;;) {
|
|
3658
|
+
try {
|
|
3659
|
+
return await operation({
|
|
3660
|
+
idempotencyKey,
|
|
3661
|
+
...(until === undefined
|
|
3662
|
+
? {}
|
|
3663
|
+
: { signal: AbortSignal.timeout(Math.max(1, Math.ceil(until - performance.now()))) }),
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
catch (error) {
|
|
3667
|
+
if (!(error instanceof RuntimeError) ||
|
|
3668
|
+
error.code !== "volume_releasing" ||
|
|
3669
|
+
error.status !== 409)
|
|
3670
|
+
throw error;
|
|
3671
|
+
if (until === undefined) {
|
|
3672
|
+
until = performance.now() + 90_000;
|
|
3673
|
+
out.error("Waiting for the volume to be let go, up to 90 seconds; Ctrl-C stops.");
|
|
3674
|
+
}
|
|
3675
|
+
const left = until - performance.now();
|
|
3676
|
+
if (left <= 0)
|
|
3677
|
+
throw error;
|
|
3678
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1000, left)));
|
|
3679
|
+
if (performance.now() >= until)
|
|
3680
|
+
throw error;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3316
3684
|
/** Options `sandbox create` and `run` take with a value. */
|
|
3317
3685
|
const CREATE_VALUES = [
|
|
3318
3686
|
"name",
|
|
@@ -3353,6 +3721,9 @@ function usdMicros(args, name) {
|
|
|
3353
3721
|
}
|
|
3354
3722
|
/** A create body from the command line's flags. */
|
|
3355
3723
|
function createInput(args) {
|
|
3724
|
+
for (const name of ["image", "snapshot", "volume", "name", "region", "connect"])
|
|
3725
|
+
if (args.flags.get(name)?.some((value) => !value.trim()))
|
|
3726
|
+
throw usage(`--${name} needs a non-empty value.`);
|
|
3356
3727
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
3357
3728
|
return {
|
|
3358
3729
|
...(flag(args, "image") ? { image: flag(args, "image") } : {}),
|
|
@@ -3387,7 +3758,7 @@ function createInput(args) {
|
|
|
3387
3758
|
? { timeoutSeconds: integer(args, "timeout") }
|
|
3388
3759
|
: {}),
|
|
3389
3760
|
...(flag(args, "on-timeout")
|
|
3390
|
-
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
|
|
3761
|
+
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop", "delete"]) }
|
|
3391
3762
|
: {}),
|
|
3392
3763
|
...(integer(args, "idle-pause") !== undefined
|
|
3393
3764
|
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
@@ -3449,6 +3820,9 @@ ownInterrupt = true) {
|
|
|
3449
3820
|
// (Sandbox.exec); `--json` answers the result and prints no such line.
|
|
3450
3821
|
else if (result.timedOut)
|
|
3451
3822
|
out.error(`runtime: timed out after ${timeout ? `${timeout} s` : "24 h"}; the command's process group was killed.`);
|
|
3823
|
+
const memory = outOfMemoryLines(result.outOfMemory);
|
|
3824
|
+
if (memory && !out.json)
|
|
3825
|
+
out.error(memory);
|
|
3452
3826
|
// Never drop output silently: say so, and do not exit 0.
|
|
3453
3827
|
const lost = extras.lostOutput(result, sbx.id);
|
|
3454
3828
|
if (lost && !out.json)
|