withruntime 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/BLAXEL.md +3 -3
  2. package/COMPATIBILITY.md +74 -9
  3. package/DAYTONA.md +33 -5
  4. package/E2B.md +137 -48
  5. package/README.md +11 -9
  6. package/README.src.md +11 -9
  7. package/VERCEL.md +43 -5
  8. package/compatibility-lock.json +58 -49
  9. package/dist/billing-cli.d.ts +1 -1
  10. package/dist/billing-cli.js +77 -1
  11. package/dist/blaxel/errors.d.ts +14 -0
  12. package/dist/blaxel/errors.js +33 -4
  13. package/dist/blaxel/index.d.ts +2 -2
  14. package/dist/blaxel/index.js +4 -4
  15. package/dist/blaxel/interpreter.d.ts +2 -5
  16. package/dist/blaxel/sandbox.d.ts +15 -7
  17. package/dist/blaxel/sandbox.js +64 -9
  18. package/dist/blaxel/snapshot.d.ts +4 -0
  19. package/dist/blaxel/snapshot.js +4 -0
  20. package/dist/cli/logs.d.ts +21 -0
  21. package/dist/cli/logs.js +188 -0
  22. package/dist/cli/logs.test.d.ts +1 -0
  23. package/dist/cli/logs.test.js +272 -0
  24. package/dist/cli-extras.d.ts +3 -3
  25. package/dist/cli-extras.js +17 -12
  26. package/dist/cli-words.d.ts +10 -1
  27. package/dist/cli-words.js +18 -0
  28. package/dist/cli.d.ts +6 -2
  29. package/dist/cli.js +468 -219
  30. package/dist/client.d.ts +13 -63
  31. package/dist/client.js +35 -124
  32. package/dist/cloudflare/errors.d.ts +143 -0
  33. package/dist/cloudflare/errors.js +167 -0
  34. package/dist/cloudflare/index.d.ts +175 -6
  35. package/dist/cloudflare/index.js +416 -26
  36. package/dist/cloudflare/interpreter.d.ts +119 -0
  37. package/dist/cloudflare/interpreter.js +223 -0
  38. package/dist/cloudflare/streams.d.ts +38 -0
  39. package/dist/cloudflare/streams.js +145 -0
  40. package/dist/cloudflare/watch.d.ts +9 -0
  41. package/dist/cloudflare/watch.js +24 -0
  42. package/dist/codesandbox/index.d.ts +216 -28
  43. package/dist/codesandbox/index.js +478 -78
  44. package/dist/compat/core.d.ts +18 -0
  45. package/dist/compat/core.js +29 -0
  46. package/dist/compat/files.d.ts +1 -0
  47. package/dist/compat/files.js +25 -5
  48. package/dist/daytona/computer-use.d.ts +13 -4
  49. package/dist/daytona/computer-use.js +66 -8
  50. package/dist/daytona/daytona.d.ts +7 -2
  51. package/dist/daytona/daytona.js +31 -5
  52. package/dist/daytona/errors.d.ts +11 -0
  53. package/dist/daytona/errors.js +18 -0
  54. package/dist/daytona/filesystem.js +2 -1
  55. package/dist/daytona/image.d.ts +13 -9
  56. package/dist/daytona/image.js +18 -12
  57. package/dist/daytona/index.d.ts +1 -1
  58. package/dist/daytona/sandbox.d.ts +9 -3
  59. package/dist/daytona/sandbox.js +17 -3
  60. package/dist/domains/index.d.ts +9 -0
  61. package/dist/domains/index.js +10 -0
  62. package/dist/e2b/client.d.ts +15 -2
  63. package/dist/e2b/client.js +61 -7
  64. package/dist/e2b/commands.d.ts +4 -1
  65. package/dist/e2b/commands.js +17 -7
  66. package/dist/e2b/errors.d.ts +43 -2
  67. package/dist/e2b/errors.js +78 -6
  68. package/dist/e2b/filesystem.d.ts +1 -1
  69. package/dist/e2b/filesystem.js +82 -16
  70. package/dist/e2b/index.d.ts +3 -3
  71. package/dist/e2b/index.js +2 -2
  72. package/dist/e2b/sandbox.d.ts +50 -35
  73. package/dist/e2b/sandbox.js +228 -185
  74. package/dist/e2b/unsupported.d.ts +19 -0
  75. package/dist/e2b/unsupported.js +22 -1
  76. package/dist/e2b/users.d.ts +9 -0
  77. package/dist/e2b/users.js +22 -0
  78. package/dist/errors.d.ts +14 -5
  79. package/dist/errors.js +28 -14
  80. package/dist/freestyle/index.d.ts +285 -37
  81. package/dist/freestyle/index.js +656 -144
  82. package/dist/identity.d.ts +3 -0
  83. package/dist/identity.js +27 -0
  84. package/dist/images/index.d.ts +9 -0
  85. package/dist/images/index.js +10 -0
  86. package/dist/index.d.ts +1 -0
  87. package/dist/ips/index.d.ts +9 -0
  88. package/dist/ips/index.js +10 -0
  89. package/dist/jobs/index.d.ts +9 -0
  90. package/dist/jobs/index.js +10 -0
  91. package/dist/keys.js +2 -0
  92. package/dist/login.js +2 -0
  93. package/dist/machine.d.ts +189 -0
  94. package/dist/machine.js +521 -0
  95. package/dist/mcp.js +1 -1
  96. package/dist/modal/index.d.ts +339 -61
  97. package/dist/modal/index.js +781 -121
  98. package/dist/network-cli.d.ts +3 -1
  99. package/dist/network-cli.js +78 -36
  100. package/dist/networks/index.d.ts +11 -0
  101. package/dist/networks/index.js +10 -0
  102. package/dist/openai-agents/index.d.ts +1 -1
  103. package/dist/openai-agents/index.js +1 -1
  104. package/dist/ports/index.d.ts +9 -0
  105. package/dist/ports/index.js +10 -0
  106. package/dist/product-names.d.ts +52 -0
  107. package/dist/product-names.js +164 -0
  108. package/dist/product-renames.d.ts +103 -0
  109. package/dist/product-renames.js +189 -0
  110. package/dist/products/apps.d.ts +176 -0
  111. package/dist/products/apps.js +165 -0
  112. package/dist/products/billing.d.ts +41 -0
  113. package/dist/products/billing.js +13 -0
  114. package/dist/products/browser.d.ts +10 -0
  115. package/dist/products/browsers.d.ts +71 -0
  116. package/dist/products/browsers.js +102 -0
  117. package/dist/products/builds.d.ts +126 -0
  118. package/dist/products/builds.js +221 -0
  119. package/dist/products/index.d.ts +3 -16
  120. package/dist/products/index.js +3 -14
  121. package/dist/products/jobs.d.ts +19 -3
  122. package/dist/products/jobs.js +28 -12
  123. package/dist/products/limits.d.ts +4 -10
  124. package/dist/products/logs.d.ts +245 -0
  125. package/dist/products/logs.js +274 -0
  126. package/dist/products/logs.test.d.ts +1 -0
  127. package/dist/products/logs.test.js +343 -0
  128. package/dist/products/network-products.d.ts +38 -7
  129. package/dist/products/network-products.js +31 -18
  130. package/dist/products/network.d.ts +1 -1
  131. package/dist/products/observability.d.ts +3 -2
  132. package/dist/products/private-network.js +1 -1
  133. package/dist/products/queues.d.ts +149 -0
  134. package/dist/products/queues.js +213 -0
  135. package/dist/products/referrals.d.ts +21 -7
  136. package/dist/products/referrals.js +3 -3
  137. package/dist/products/secrets.d.ts +8 -4
  138. package/dist/products/secrets.js +39 -18
  139. package/dist/products/signed-requests.d.ts +21 -0
  140. package/dist/products/signed-requests.js +11 -0
  141. package/dist/products/streams.d.ts +73 -0
  142. package/dist/products/streams.js +17 -0
  143. package/dist/products/switching.d.ts +37 -10
  144. package/dist/products/switching.js +4 -3
  145. package/dist/products/volumes.d.ts +3 -0
  146. package/dist/record-cli.d.ts +23 -0
  147. package/dist/record-cli.js +308 -0
  148. package/dist/renamed-products.d.ts +38 -0
  149. package/dist/renamed-products.js +34 -0
  150. package/dist/runloop/index.d.ts +9 -2
  151. package/dist/runloop/index.js +29 -8
  152. package/dist/runloop/sdk.d.ts +131 -0
  153. package/dist/runloop/sdk.js +167 -0
  154. package/dist/sandbox.d.ts +10 -94
  155. package/dist/sandbox.js +22 -371
  156. package/dist/sandboxes/index.d.ts +11 -0
  157. package/dist/sandboxes/index.js +12 -0
  158. package/dist/snapshots/index.d.ts +9 -0
  159. package/dist/snapshots/index.js +11 -0
  160. package/dist/snapshots.d.ts +7 -4
  161. package/dist/sprites/index.d.ts +343 -31
  162. package/dist/sprites/index.js +866 -84
  163. package/dist/ssh.d.ts +1 -1
  164. package/dist/ssh.js +11 -11
  165. package/dist/terminal.d.ts +24 -0
  166. package/dist/terminal.js +32 -0
  167. package/dist/transport.d.ts +18 -2
  168. package/dist/transport.js +61 -6
  169. package/dist/tunnel.js +1 -1
  170. package/dist/types.d.ts +116 -8
  171. package/dist/vercel/sandbox.d.ts +1 -2
  172. package/dist/vercel/sandbox.js +6 -5
  173. package/dist/vercel/snapshot.js +10 -2
  174. package/dist/volumes/index.d.ts +9 -0
  175. package/dist/volumes/index.js +10 -0
  176. package/package.json +48 -2
package/dist/cli.js CHANGED
@@ -11,15 +11,19 @@ import { Sandbox as SandboxHandle } from "./sandbox.js";
11
11
  import { NotFoundError, RuntimeError } from "./errors.js";
12
12
  import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
13
13
  import { billingCommand } from "./billing-cli.js";
14
- import { cliRefusal, describeRecord, newestCpu, topicHelp } from "./cli-words.js";
14
+ import { cliRefusal, describeRecord, newestCpu, productWords, topicHelp } from "./cli-words.js";
15
15
  import { envFetch } from "./proxy.js";
16
+ import { logStream, terminalSafe, terminalSafeLines } from "./terminal.js";
16
17
  import { VERSION } from "./transport.js";
18
+ /** A log stream's text (a sandbox process's, an image build's, a job run's)
19
+ * as stdout shows it: escaped on a terminal unless --raw (terminal.ts). */
20
+ const logText = (args) => (text) => process.stdout.isTTY && !has(args, "raw") ? terminalSafeLines(text) : text;
17
21
  const HELP = `Runtime Cloud CLI ${VERSION}
18
22
 
19
23
  Usage: runtime <product> <command> [options] runtime <command> [options]
20
24
 
21
25
  Start
22
- sandbox run [--keep] [create options] -- <command>
26
+ sandboxes run [--keep] [create options] -- <command>
23
27
  A fresh sandbox runs one command, then stops
24
28
 
25
29
  Account
@@ -36,7 +40,7 @@ Account
36
40
  The account's audit log (owner or admin keys)
37
41
  sso Single sign-on and SCIM directory sync (owner or
38
42
  admin keys); an owner changes it on the website
39
- usage [--csv] Balance, trial time and charges; --csv, each resource
43
+ usage [--csv] Balance, included usage and charges; --csv, each resource
40
44
  usage export --since <UTC time> --until <UTC time>
41
45
  Every settlement in the range as CSV; --json, each page
42
46
  limits Whether this key is read-only, and its daily spending limit
@@ -53,13 +57,15 @@ Account
53
57
  secrets rm <NAME> Delete a secret, every copy
54
58
  keys create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide]
55
59
  A new API key for CI, approved by an owner in the browser
56
- referrals Your referral link: you both get up to $500
60
+ referrals Your referral link: you both get up to $1,000, then
61
+ you earn 10% of what they spend for 12 months
57
62
  compare --from <provider> [--days 30] What your usage would cost at e2b, daytona, vercel,
58
63
  modal, cloudflare, fly, blaxel, lambda-microvms,
59
64
  freestyle, prime and others, and what you save
60
65
  switch [--from <provider>] Moving from e2b, daytona, vercel, modal, cloudflare, fly
61
- or blaxel: record it before your first top-up, and
62
- that top-up is matched, up to $100
66
+ or blaxel: record it before your first top-up.
67
+ Credit you add within 30 days of signing up is
68
+ matched, up to $1,000 in total
63
69
  ls Everything you run, every product
64
70
  feedback "<text>" [--kind bug|missing_feature|competitor_gap|migration_blocker|docs|pricing|praise|other]
65
71
  [--detail <text>] [--competitor <name>] [--request-id <req_...>]
@@ -72,13 +78,13 @@ Account
72
78
  mcp Serve MCP on stdio for agents that need it
73
79
 
74
80
  Connect
75
- sandbox ssh <id|name> [-- <command...>] Log in over SSH; \`runtime sandbox ssh help\` for editors
76
- sandbox ssh config [--install] Make \`ssh <id|name>.runtime\` work (VS Code, JetBrains)
77
- sandbox port-forward <id|name> <port|local:remote>...
81
+ sandboxes ssh <id|name> [-- <command...>] Log in over SSH; \`runtime sandboxes ssh help\` for editors
82
+ sandboxes ssh config [--install] Make \`ssh <id|name>.runtime\` work (VS Code, JetBrains)
83
+ sandboxes port-forward <id|name> <port|local:remote>...
78
84
  Forward local ports to ports in a sandbox
79
85
 
80
86
  Observability
81
- sandbox metrics <id> [--range 1h] CPU and memory over time (15m 1h 6h 24h 7d 30d)
87
+ sandboxes metrics <id> [--range 1h] CPU and memory over time (15m 1h 6h 24h 7d 30d)
82
88
  events [--sandbox <id>] [--type <type>] Lifecycle events, newest first
83
89
  webhooks ls | create <url> [--events a,b] | test <id> | deliveries <id>
84
90
  | update <id> [--url u] [--events a,b] [--enable|--disable]
@@ -88,22 +94,23 @@ Observability
88
94
  Events and metrics to an OpenTelemetry endpoint
89
95
 
90
96
  Products
91
- sandbox Linux microVMs: exec, shell, files, code, previews, network, desktop, browser
92
- snapshot Saved sandboxes, disk and memory, to start or fork from
93
- image Custom images from a Dockerfile, any public or private image, or a package list
94
- volume Persistent disks to attach to sandboxes
95
- job A command in a fresh sandbox, once or on a cron schedule, with its logs
96
- domain Your own hostname for a sandbox's port, with HTTPS
97
- port A public TCP port to a sandbox: databases, game servers
98
- address A dedicated outbound address, for allow-lists
99
- tunnel A WireGuard tunnel from your own network into your sandboxes
100
- network Account-wide network settings: your own proxy, sandboxes reaching each other by name
101
- Each has its own help: \`runtime sandbox help\`, \`runtime image help\`...
97
+ sandboxes Linux microVMs: exec, shell, files, code, previews, network, desktop, browser
98
+ snapshots Saved sandboxes, disk and memory, to start or fork from
99
+ images Custom images from a Dockerfile, any public or private image, or a package list
100
+ volumes Persistent disks to attach to sandboxes
101
+ jobs A command in a fresh sandbox, once or on a cron schedule, with its logs
102
+ domains Your own hostname for a sandbox's port, with HTTPS
103
+ ports A public TCP port to a sandbox: databases, game servers
104
+ ips A dedicated outbound address, for allow-lists
105
+ networks Your networks: a WireGuard tunnel in, your own proxy out, sandboxes reaching
106
+ each other by name
107
+ Each has its own help: \`runtime sandboxes help\`, \`runtime images help\`...
108
+ The singular (\`runtime sandbox\`) is a shortcut for the same product.
102
109
 
103
110
  Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
104
111
  Behind a proxy: HTTPS_PROXY and NO_PROXY are read, on Node and Bun alike.
105
112
  `;
106
- const SANDBOX_HELP = `runtime sandbox <command>
113
+ const SANDBOX_HELP = `runtime sandboxes <command>
107
114
 
108
115
  Every <id> below can also be the sandbox's name.
109
116
 
@@ -112,7 +119,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
112
119
  port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
113
120
  create [--name n] [--label k=v]... [--env K=V]... [--vcpu 2] [--memory 4096] [--disk 4096]
114
121
  [--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
115
- [--timeout <seconds>] [--on-timeout pause|stop] [--trial|--paid] [--no-wait]
122
+ [--timeout <60-86400>] [--on-timeout pause|stop|delete] [--no-wait]
116
123
  [--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
117
124
  [--image <name:tag|imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
118
125
  [--idle-pause <seconds>] [--no-auto-wake] [--persistent] [--get-or-create]
@@ -123,7 +130,9 @@ const SANDBOX_HELP = `runtime sandbox <command>
123
130
  Run and stream output; exits with its code
124
131
  spawn <id> [--pty] -- <command...> Start in the background; prints a process id
125
132
  ps <id> Running and recent processes
126
- logs <id> <processId> [-f] A process's output so far; -f follows it to the end
133
+ logs <id> <processId> [-f] [--raw] A process's output so far; -f follows it to the end;
134
+ on a terminal control characters show as \\uXXXX
135
+ unless --raw
127
136
  kill <id> <processId> [--signal SIGTERM]
128
137
  shell <id> [--command "bash -l"] An interactive terminal
129
138
  cp <src> <dst> Copy files or directories; name a sandbox
@@ -144,7 +153,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
144
153
  update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
145
154
  [--persistent on|off] [--max-total-cost <usd>|none]
146
155
  Change its settings; persistent keeps it running until stopped
147
- snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
156
+ snapshot <id> [--name n] [--retention days|credit] Keep its whole machine; prints the snapshot id
148
157
  mount <id> <s3|r2|gcs>://<bucket>[/prefix] <path> [--secret NAME] [--region r]
149
158
  [--endpoint https://...] [--account-id id] [--read-only]
150
159
  Your bucket as a directory; the proxy signs, the sandbox never sees the key
@@ -152,7 +161,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
152
161
  tailscale up <id> --auth-key-secret NAME [--hostname h] [--tag tag:x]...
153
162
  Join your tailnet (paid); the key is a secret set with --jobs
154
163
  tailscale status <id> | tailscale down <id> Its tailnet address; take it off the tailnet
155
- fork <id> [--count 3] [--name n] [--keep-snapshot] [--trial|--paid]
164
+ fork <id> [--count 3] [--name n] [--keep-snapshot]
156
165
  Copies of it as it is now, running; prints their ids
157
166
  run-code <id> <file|-> [--lang python|javascript|typescript|r|java|bash|go] [--context c] [--out-dir .]
158
167
  Run a notebook-style cell; charts saved as PNG
@@ -188,10 +197,10 @@ const SANDBOX_HELP = `runtime sandbox <command>
188
197
  Chromium for CDP agents: prints its cdpUrl
189
198
 
190
199
  Examples
191
- id=$(runtime sandbox create)
192
- runtime sandbox cp ./invoice.py $id:/workspace/invoice.py
193
- runtime sandbox exec $id -- python3 /workspace/invoice.py
194
- runtime sandbox stop $id
200
+ id=$(runtime sandboxes create)
201
+ runtime sandboxes cp ./invoice.py $id:/workspace/invoice.py
202
+ runtime sandboxes exec $id -- python3 /workspace/invoice.py
203
+ runtime sandboxes stop $id
195
204
  `;
196
205
  /** A command's options. Reading one its parse() did not declare is a bug in
197
206
  * this file, and says so, so a test that runs the command finds it. */
@@ -270,6 +279,8 @@ function unknownOption(name, known) {
270
279
  }
271
280
  const flag = (args, name) => args.flags.get(name)?.at(-1);
272
281
  const has = (args, name) => args.flags.get(name)?.at(-1) === "true";
282
+ /** Days, or the word `credit` for null: kept while the account has credit. */
283
+ const daysOrCredit = (args, name) => flag(args, name) === "credit" ? null : integer(args, name);
273
284
  const integer = (args, name) => {
274
285
  const value = flag(args, name);
275
286
  if (value === undefined)
@@ -360,16 +371,15 @@ const COMMANDS = [
360
371
  "support",
361
372
  "docs",
362
373
  "mcp",
363
- "sandbox",
364
- "snapshot",
365
- "image",
366
- "volume",
367
- "job",
368
- "domain",
369
- "port",
370
- "address",
371
- "tunnel",
372
- "network",
374
+ "sandboxes",
375
+ "snapshots",
376
+ "images",
377
+ "volumes",
378
+ "jobs",
379
+ "domains",
380
+ "ports",
381
+ "ips",
382
+ "networks",
373
383
  "webhooks",
374
384
  "otel",
375
385
  "events",
@@ -378,30 +388,20 @@ const COMMANDS = [
378
388
  ];
379
389
  /** Commands with a help of their own: `runtime <command> help`. */
380
390
  const OWN_HELP = [
381
- "sandbox",
382
391
  "sandboxes",
383
392
  "sbx",
384
- "snapshot",
385
393
  "snapshots",
386
- "image",
387
394
  "images",
388
- "volume",
389
395
  "volumes",
390
- "job",
391
396
  "jobs",
392
397
  "keys",
393
398
  "webhooks",
394
399
  "webhook",
395
400
  "billing",
396
- "domain",
397
401
  "domains",
398
- "port",
399
402
  "ports",
400
- "address",
401
- "addresses",
402
- "tunnel",
403
- "tunnels",
404
- "network",
403
+ "ips",
404
+ "networks",
405
405
  ];
406
406
  /** Sandbox commands that once sat at the top level. */
407
407
  const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
@@ -506,6 +506,14 @@ function table(rows) {
506
506
  .trimEnd())
507
507
  .join("\n");
508
508
  }
509
+ /** When a sandbox ends by itself (0300, 0381). A paid-ahead lease, expiresAt,
510
+ * is not its time limit; only an older server, which sends no endsAt, gives
511
+ * the end of a limited sandbox's time as expiresAt. */
512
+ function sandboxEnd(info) {
513
+ if (info.endsAt === null || (info.endsAt === undefined && !info.timeoutSeconds))
514
+ return "no limit";
515
+ return info.endsAt ?? info.expiresAt ?? "-";
516
+ }
509
517
  function describe(info) {
510
518
  return table([
511
519
  ["id", info.id],
@@ -515,19 +523,9 @@ function describe(info) {
515
523
  "shape",
516
524
  `${info.vcpu} vCPU, ${info.memoryMiB} MiB memory, ${info.diskMiB} MiB disk (${info.cpu})`,
517
525
  ],
518
- ["funding", info.funding],
519
526
  ["region", info.region],
520
- // A sandbox with no time limit runs while it works (0300): endsAt is
521
- // null. An older server sends no endsAt; its expiresAt was the end.
522
527
  ["time limit", info.timeoutSeconds ? `${info.timeoutSeconds} s` : "none"],
523
- [
524
- "ends",
525
- info.endsAt === null
526
- ? info.state === "running"
527
- ? "when idle, stopped or out of credit"
528
- : "-"
529
- : (info.endsAt ?? info.expiresAt ?? "-"),
530
- ],
528
+ ["ends", sandboxEnd(info)],
531
529
  ["at its end", info.onLeaseEnd ?? "-"],
532
530
  // Why a sandbox is no longer running (troubleshooting.md, "A sandbox
533
531
  // stopped on its own"); `get` left both out until 30 September 2026.
@@ -560,17 +558,22 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
560
558
  // Only what comes before -- is the CLI's: a --json after it is the
561
559
  // command's, whether or not the CLI was given its own.
562
560
  const dash = argv.indexOf("--");
563
- const [first, ...remaining] = argv.filter((arg, index) => arg !== "--json" || (dash !== -1 && index > dash));
561
+ // CLOUD.md section 4: the plural names the product, the singular is a
562
+ // typing shortcut, and an old word works for 30 days with a warning.
563
+ const words = productWords(argv.filter((arg, index) => arg !== "--json" || (dash !== -1 && index > dash)));
564
+ if (words.warning)
565
+ out.error(`Warning: ${named(words.warning)}`);
566
+ const [first, ...remaining] = words.argv;
564
567
  // Sandbox is one product of several, so its commands live under it
565
- // (AGENTS.md, 23 September 2026): `runtime sandbox run`, `sandbox ssh` and
568
+ // (AGENTS.md, 23 September 2026): `runtime sandboxes run`, `sandbox ssh` and
566
569
  // `sandbox port-forward`. The old top-level forms were removed, not aliased.
567
570
  if (first && SANDBOX_MOVED.includes(first))
568
- throw usage(`\`${me} ${first}\` is now \`${me} sandbox ${first === "forward" ? "port-forward" : first}\`.`);
571
+ throw usage(`\`${me} ${first}\` is now \`${me} sandboxes ${first === "forward" ? "port-forward" : first}\`.`);
569
572
  const nested = (first === "sandbox" || first === "sandboxes" || first === "sbx") &&
570
573
  SANDBOX_MOVED.includes(remaining[0] ?? "");
571
574
  const command = nested ? remaining[0] : first;
572
575
  const rest = nested ? remaining.slice(1) : remaining;
573
- // `runtime sandbox create --help` is how an agent asks what a command takes:
576
+ // `runtime sandboxes create --help` is how an agent asks what a command takes:
574
577
  // answer with the product's help, never by running the command.
575
578
  const cut = remaining.indexOf("--");
576
579
  const asked = (cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h") ||
@@ -603,10 +606,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
603
606
  ? JSON.stringify({ connected, usage: HELP })
604
607
  : `Runtime Cloud ${VERSION}. ${start}
605
608
 
606
- ${me} sandbox run -- python3 < invoice.py
609
+ ${me} sandboxes run -- python3 < invoice.py
607
610
  Run a local invoice script, then stop the sandbox
608
- ${me} sandbox create A sandbox that stays; prints its id
609
- ${me} sandbox exec <id> -- <command> Run in it
611
+ ${me} sandboxes create A sandbox that stays; prints its id
612
+ ${me} sandboxes exec <id> -- <command> Run in it
610
613
  ${me} help Everything else
611
614
  `);
612
615
  return 0;
@@ -616,14 +619,15 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
616
619
  return 0;
617
620
  }
618
621
  if (command === "run") {
619
- const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandbox run -- python3 < invoice.py`);
622
+ const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandboxes run -- python3 < invoice.py`);
620
623
  const commandLine = args.rest ?? args.positional;
621
624
  if (!commandLine.length)
622
- throw usage(`Give the command after --: ${me} sandbox run -- python3 < invoice.py`);
625
+ throw usage(`Give the command after --: ${me} sandboxes run -- python3 < invoice.py`);
623
626
  const runtime = await client(env);
624
627
  const timeout = integer(args, "timeout");
625
628
  const { timeoutSeconds: _lease, ...input } = createInput(args);
626
- const sbx = await runtime.sandboxes.create(input, { onCapacityWait: waiting(out) });
629
+ const onCapacityWait = waiting(out);
630
+ const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
627
631
  let cleanup;
628
632
  // SIGINT and normal completion share one stop. Keep an absolute deadline
629
633
  // across the stop retries and final state check, so cleanup cannot hang.
@@ -642,7 +646,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
642
646
  code: "cleanup_failed",
643
647
  status: 0,
644
648
  message: `Could not confirm that sandbox ${sbx.id} stopped. ${error instanceof Error ? error.message : String(error)}`,
645
- hint: `Check it and retry cleanup with: ${me} sandbox stop ${sbx.id}`,
649
+ hint: `Check it and retry cleanup with: ${me} sandboxes stop ${sbx.id}`,
646
650
  cause: error,
647
651
  }), out.json));
648
652
  return false;
@@ -658,7 +662,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
658
662
  finally {
659
663
  try {
660
664
  if (has(args, "keep"))
661
- out.error(`Kept sandbox ${sbx.id}. Run more with: ${me} sandbox exec ${sbx.id} -- <command>`);
665
+ out.error(`Kept sandbox ${sbx.id}. Run more with: ${me} sandboxes exec ${sbx.id} -- <command>`);
662
666
  else if (!(await stop()) && code === 0)
663
667
  code = 1;
664
668
  }
@@ -695,16 +699,18 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
695
699
  const [who, spend] = await Promise.all([rt.me(), rt.usage().catch(() => undefined)]);
696
700
  const trialLeft = spend?.trial ? spend.trial.availableMs / 3_600_000 : 0;
697
701
  const credit = spend ? BigInt(spend.available) : 0n;
702
+ // A pilot's sandboxes run on the pilot, before anything else and free.
698
703
  const funding = !spend
699
704
  ? undefined
700
705
  : [
706
+ ...(spend.pilot ? [pilotFunding(spend.pilot)] : []),
701
707
  ...(trialLeft > 0
702
708
  ? [
703
- `free trial, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left`,
709
+ `included usage, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left without credit`,
704
710
  ]
705
711
  : []),
706
712
  ...(credit > 0n ? [`${dollars(credit)} of credit`] : []),
707
- ].join("; ") || "no trial time or credit left: runtime billing";
713
+ ].join("; ") || "no included usage or credit left: runtime billing";
708
714
  print(table([
709
715
  ["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
710
716
  ...(who.role ? [["role", who.role]] : []),
@@ -765,7 +771,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
765
771
  return 0;
766
772
  }
767
773
  if (command === "limits") {
768
- const l = await (await client(env)).limits.get();
774
+ const rt = await client(env);
775
+ // The usage call only adds the pilot's line: a key that cannot read it
776
+ // still gets its limits.
777
+ const [l, spend] = await Promise.all([rt.limits.get(), rt.usage().catch(() => undefined)]);
769
778
  // As `usage` shows money: $0.0014 of spend read $0.00 until 30 September 2026.
770
779
  const usd = (micros) => dollars(micros);
771
780
  const access = l.access === "read"
@@ -783,7 +792,13 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
783
792
  ],
784
793
  ["used, last 24 hours", usd(l.daily.usedMicros)],
785
794
  ...(l.daily.remainingMicros === null ? [] : [["left", usd(l.daily.remainingMicros)]]),
786
- ...(l.trial ? [["free trial", trialLeft(l.trial)]] : []),
795
+ ...(l.trial ? [["free time", trialLeft(l.trial)]] : []),
796
+ ...(spend?.pilot
797
+ ? [
798
+ ["pilot", pilotFunding(spend.pilot)],
799
+ ["pilot sandbox hours", pilotLeft(spend.pilot)],
800
+ ]
801
+ : []),
787
802
  ]), l);
788
803
  return 0;
789
804
  }
@@ -870,7 +885,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
870
885
  product = "billing";
871
886
  const origins = connectionOrigins(env);
872
887
  const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
873
- return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
888
+ return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], ["below", "amount", "monthly"]), out, () => client(env), undefined, {
874
889
  open: (input) => {
875
890
  checkBrowserOrigins(origins);
876
891
  return openWalletAccount(input, { authUrl: origins.auth });
@@ -894,21 +909,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
894
909
  });
895
910
  }
896
911
  if (command === "referrals") {
912
+ parse(rest, [], []);
897
913
  const r = await (await client(env)).referrals.get();
898
914
  const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
899
915
  print([
900
916
  `Your link: ${r.link}`,
901
917
  "",
902
- `When a company signs up with it and makes its first top-up of ${usd(r.minPurchaseMicros)} or more,`,
903
- `you each get credit equal to that top-up: at least ${usd(r.rewardMicros)}, at most ${usd(r.maxRewardMicros)}`,
904
- `(up to ${usd(r.yearlyCapMicros)} a year for you).`,
918
+ `A company that signs up with it has each payment in its first ${r.windowDays} days matched, up to ${usd(r.maxRewardMicros)},`,
919
+ `and you get the same; its first payment of ${usd(r.minPurchaseMicros)} or more gives each of you at least ${usd(r.rewardMicros)}.`,
920
+ `Then for ${r.shareMonths} months you earn ${r.shareBasisPoints / 100}% of what it spends from credit it paid for`,
921
+ `(up to ${usd(r.yearlyCapMicros)} a year for you, matches and share together).`,
905
922
  ...(r.enabled ? [] : ["The program is paused: new sign-ups are not counted right now."]),
906
923
  "",
907
924
  table([
908
925
  ["signed up", String(r.signedUp)],
909
- ["waiting for a first top-up", String(r.pending)],
910
- ["paid", String(r.paid)],
926
+ ["waiting for a first payment", String(r.pending)],
927
+ ["paying", String(r.payingCompanies)],
911
928
  ["earned", usd(r.earnedMicros)],
929
+ ["of which share", usd(r.shareEarnedMicros)],
912
930
  [`left this year (${r.year})`, usd(r.capRemainingMicros)],
913
931
  ]),
914
932
  ].join("\n"), r);
@@ -990,7 +1008,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
990
1008
  ]);
991
1009
  print(rows.length
992
1010
  ? table([["ID", "PRODUCT", "NAME", "STATE", "CREATED"], ...rows])
993
- : "Nothing yet. Start with: runtime sandbox create", items);
1011
+ : "Nothing yet. Start with: runtime sandboxes create", items);
994
1012
  return 0;
995
1013
  }
996
1014
  if (command === "ssh") {
@@ -1021,15 +1039,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
1021
1039
  product = "job";
1022
1040
  return jobCommand(rest, env, out);
1023
1041
  }
1024
- const networkProduct = { domains: "domain", ports: "port", addresses: "address", tunnels: "tunnel" }[command] ??
1025
- command;
1026
- if (NETWORK_PRODUCTS.includes(networkProduct)) {
1042
+ // WireGuard is inside networks: `runtime networks wireguard peer add`.
1043
+ const wireguard = command === "networks" && rest[0] === "wireguard";
1044
+ const networkProduct = wireguard ? "wireguard" : command;
1045
+ const networkRest = wireguard ? rest.slice(1) : rest;
1046
+ if (command !== "wireguard" && NETWORK_PRODUCTS.includes(networkProduct)) {
1027
1047
  product = networkProduct;
1028
- return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"], ["sandbox", "subnet", "public-key", "route", "out", "host", "secret"]), out, () => client(env));
1048
+ return networkProductCommand(networkProduct, networkRest, parse(networkRest, ["ipv6"], ["sandbox", "subnet", "public-key", "route", "out", "host", "secret"]), out, () => client(env));
1029
1049
  }
1030
1050
  throw unknown("command", command, COMMANDS);
1031
1051
  }
1032
- /** `runtime usage` for a person: the balance, the trial and what each kind
1052
+ /** `runtime usage` for a person: the balance, the included usage and what each kind
1033
1053
  * of resource was charged, in dollars. --json keeps every figure, and each
1034
1054
  * resource with its rates and CPU time. */
1035
1055
  /** One row per resource, for a spreadsheet: when it ran, how big it was, how
@@ -1083,15 +1103,79 @@ export function usageCsv(u) {
1083
1103
  }
1084
1104
  /** Milliseconds as hours, to a tenth: "8,731.2". */
1085
1105
  const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
1086
- /** The free trial's time left, as `runtime usage` and `runtime limits` say it. */
1106
+ /** The machine time left without credit, as `runtime usage` and `runtime
1107
+ * limits` say it: this month's included usage, as hours at 2 vCPU and 4 GiB,
1108
+ * or for an account given free hours before 5 October 2026, those hours too. */
1087
1109
  function trialLeft(trial) {
1088
- return `${hours(trial.availableMs)} of ${hours(trial.totalMs)} hours left`;
1110
+ const renews = trial.renewsAt ? `, renews ${trial.renewsAt.slice(0, 10)}` : "";
1111
+ return trial.totalMs > 0
1112
+ ? `${hours(trial.availableMs)} hours left (${hours(trial.totalMs)} free hours given before 5 October 2026, and the included usage${renews})`
1113
+ : `${hours(trial.availableMs)} hours at 2 vCPU and 4 GiB left this month${renews}`;
1114
+ }
1115
+ /** This month's included usage, one row per product, as `runtime usage`
1116
+ * says it. */
1117
+ function allowanceRows(allowances) {
1118
+ const amount = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 1 });
1119
+ return allowances.map((a) => [
1120
+ `included ${a.pool.replace(/-/g, " ")}`,
1121
+ `${amount(a.left)} of ${amount(a.quantity)} ${a.unit}s left`,
1122
+ ]);
1123
+ }
1124
+ /** What Runtime for Startups includes as `runtime usage` says it, in the
1125
+ * console's words: machine time left of what it gave, storage as the GB kept
1126
+ * free each month and the GB kept now, and what it runs at once. */
1127
+ function potRows(pot, limits) {
1128
+ const amount = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 1 });
1129
+ const day = (iso) => {
1130
+ const at = new Date(iso);
1131
+ const month = at.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
1132
+ return `${at.getUTCDate()} ${month} ${at.getUTCFullYear()}`;
1133
+ };
1134
+ const NAMES = {
1135
+ "machine-cpu": "vCPU-hours",
1136
+ "machine-memory": "memory",
1137
+ "saved-copies": "saved copies kept free",
1138
+ disks: "volumes",
1139
+ };
1140
+ const ORDER = ["machine-cpu", "machine-memory", "saved-copies", "disks"];
1141
+ const rank = (pool) => (ORDER.includes(pool) ? ORDER.indexOf(pool) : ORDER.length);
1142
+ const rows = [...pot]
1143
+ .sort((a, b) => rank(a.pool) - rank(b.pool))
1144
+ .map((p) => {
1145
+ const name = `${p.period === "month" ? "Runtime for Startups" : "startup hours"}: ${NAMES[p.pool] ?? p.pool.replace(/-/g, " ")}`;
1146
+ const until = p.expiresAt ? ` until ${day(p.expiresAt)}` : "";
1147
+ if (p.period === "month") {
1148
+ const kept = p.keptGB ?? 0;
1149
+ const over = kept > p.quantity ? `; past ${amount(p.quantity)} GB bills at the published rate` : "";
1150
+ return [
1151
+ name,
1152
+ `${amount(p.quantity)} GB kept free${until} · ${amount(kept)} GB kept now${over}`,
1153
+ ];
1154
+ }
1155
+ return [name, `${amount(p.left ?? 0)} of ${amount(p.quantity)} ${p.unit}s left${until}`];
1156
+ });
1157
+ if (limits)
1158
+ rows.push(["Runtime for Startups: sandboxes at once", amount(limits.sandboxesAtOnce)], ["Runtime for Startups: image builds at once", amount(limits.buildsAtOnce)]);
1159
+ return rows;
1160
+ }
1161
+ /** What a pilot gives, as `runtime whoami` and `runtime limits` say it: the
1162
+ * sandboxes it runs free and until when, past which credit does not run
1163
+ * them. */
1164
+ function pilotFunding(pilot) {
1165
+ const at = (iso) => `${iso.slice(0, 16).replace("T", " ")} UTC`;
1166
+ return `a pilot: up to ${pilot.sandboxes.toLocaleString("en-US")} sandboxes free, running until ${at(pilot.graceEndsAt)}; credit does not run it; to extend it, write to support`;
1089
1167
  }
1090
1168
  /** A pilot's sandbox hours left, as `runtime usage` says them. */
1091
1169
  function pilotLeft(pilot) {
1092
1170
  const left = `${hours(pilot.leftMs)} of ${pilot.hours.toLocaleString("en-US")} left`;
1171
+ // How long they last at the rate its sandboxes run now (an API before
1172
+ // 0414 does not say how many run).
1173
+ const running = pilot.running ?? 0;
1174
+ const pace = running > 0
1175
+ ? `; at ${running.toLocaleString("en-US")} running, about ${hours(pilot.leftMs / running)} hours`
1176
+ : "";
1093
1177
  return pilot.leftMs > 0
1094
- ? `${left}; new pilot sandboxes until ${pilot.endsAt.slice(0, 16).replace("T", " ")} UTC`
1178
+ ? `${left}${pace}; new pilot sandboxes until ${pilot.endsAt.slice(0, 16).replace("T", " ")} UTC`
1095
1179
  : "This pilot's sandbox hours are used.";
1096
1180
  }
1097
1181
  function usageSummary(u) {
@@ -1114,7 +1198,19 @@ function usageSummary(u) {
1114
1198
  if (micros(u.expired) > 0n)
1115
1199
  rows.push(["expired or taken back", dollars(micros(u.expired))]);
1116
1200
  if (u.trial)
1117
- rows.push(["free trial", trialLeft(u.trial)]);
1201
+ rows.push(["free time", trialLeft(u.trial)]);
1202
+ if (u.allowances)
1203
+ rows.push(...allowanceRows(u.allowances));
1204
+ // Only a data property is an older server's answer. Do not invoke SDK
1205
+ // compatibility getters when a server sends neither generation of fields.
1206
+ const pot = "startupIncluded" in u
1207
+ ? u.startupIncluded
1208
+ : Object.getOwnPropertyDescriptor(u, "pot")?.value;
1209
+ const potLimits = "startupLimits" in u
1210
+ ? u.startupLimits
1211
+ : Object.getOwnPropertyDescriptor(u, "potLimits")?.value;
1212
+ if (pot?.length || potLimits)
1213
+ rows.push(...potRows(pot ?? [], potLimits));
1118
1214
  if (u.pilot)
1119
1215
  rows.push(["pilot sandbox hours", pilotLeft(u.pilot)]);
1120
1216
  if (u.outbound) {
@@ -1126,7 +1222,15 @@ function usageSummary(u) {
1126
1222
  ]);
1127
1223
  }
1128
1224
  const kinds = new Map();
1129
- for (const resource of u.resources ?? []) {
1225
+ // Every resource, by kind, from an API that totals them (6 October 2026);
1226
+ // an older one sends only its hundred newest, counted here.
1227
+ for (const k of u.kinds ?? [])
1228
+ kinds.set(k.kind, {
1229
+ count: k.resources,
1230
+ charged: micros(k.chargedMicros),
1231
+ held: micros(k.heldMicros),
1232
+ });
1233
+ for (const resource of u.kinds ? [] : (u.resources ?? [])) {
1130
1234
  const kind = typeof resource.kind === "string" ? resource.kind : "other";
1131
1235
  const entry = kinds.get(kind) ?? { count: 0, charged: 0n, held: 0n };
1132
1236
  entry.count++;
@@ -1185,10 +1289,21 @@ async function outputSoFar(runtime, sandboxId, processId) {
1185
1289
  processId,
1186
1290
  state: latest?.state ?? null,
1187
1291
  exitCode: latest?.exitCode ?? null,
1292
+ ...(latest?.outOfMemory ? { outOfMemory: latest.outOfMemory } : {}),
1188
1293
  truncated,
1189
1294
  chunks,
1190
1295
  };
1191
1296
  }
1297
+ /** What a person reads when the sandbox's out-of-memory killer ended a
1298
+ * command: the API's message and hint, on standard error. A bare exit -9 (or
1299
+ * 137) said nothing of memory until 9 October 2026. */
1300
+ export function outOfMemoryLines(memory) {
1301
+ // The words come from an answer, not this CLI: escaped like a log line, so
1302
+ // nothing in them can drive the terminal (terminal.ts).
1303
+ return memory && typeof memory.message === "string" && typeof memory.hint === "string"
1304
+ ? `runtime: ${terminalSafe(memory.message)}\nHint: ${terminalSafe(memory.hint)}`
1305
+ : undefined;
1306
+ }
1192
1307
  /** Dollars from integer microdollars: cents from a dollar up, four places
1193
1308
  * below one, and every place below a hundredth of a cent, so a short trial
1194
1309
  * test never reads as $0.0000. */
@@ -1261,36 +1376,60 @@ const longDate = (iso) => new Date(iso).toLocaleDateString("en-GB", {
1261
1376
  year: "numeric",
1262
1377
  timeZone: "UTC",
1263
1378
  });
1264
- /** One line on the switching credit: how to claim it, or what it did. A
1265
- * switch already on record is named with its provider and date, so it never
1379
+ /** One line on the welcome match: while it is open, how much of it credit
1380
+ * added now can still get; after, what it got. Undefined when there is
1381
+ * nothing to say: the offer ended unused. */
1382
+ function welcomeLine(summary) {
1383
+ const w = summary.welcome;
1384
+ // A server from before the welcome match (0433) answers without it.
1385
+ if (!w)
1386
+ return undefined;
1387
+ const whole = (micros) => dollars(micros).replace(/\.00$/, "");
1388
+ if (w.eligible) {
1389
+ // A server from before 0435 answers without what is left.
1390
+ const left = w.leftMicros ?? (BigInt(w.maxMicros) - BigInt(w.creditMicros)).toString();
1391
+ return BigInt(w.creditMicros) > 0n
1392
+ ? `${whole(w.creditMicros)} of ${whole(w.maxMicros)} matched so far; credit you add by ${longDate(w.endsAt)} is matched, up to ${whole(left)} more.`
1393
+ : `Credit you add by ${longDate(w.endsAt)} is matched, up to ${whole(w.maxMicros)} in total.`;
1394
+ }
1395
+ switch (w.status) {
1396
+ case "paid":
1397
+ return `Your credit was matched: ${dollars(w.creditMicros)} of credit.`;
1398
+ case "superseded":
1399
+ return "Your payments are matched by your referral instead, which is never less.";
1400
+ case "reversed":
1401
+ return "The payments that were matched were refunded, so the match was taken back.";
1402
+ case "refused":
1403
+ return "Your credit was not matched. Ask support if you think that is wrong.";
1404
+ }
1405
+ return undefined;
1406
+ }
1407
+ /** What `switch` says: the switch on record, if any, and the welcome match.
1408
+ * A switch already on record is named with its provider and date, so it never
1266
1409
  * reads as if this command recorded it, or recorded the provider it names. */
1267
1410
  function switchingLine(summary, provider) {
1268
- const cap = dollars(summary.maxMicros);
1269
1411
  const sw = summary.switch;
1412
+ const welcome = welcomeLine(summary);
1270
1413
  if (!sw) {
1271
- if (!summary.eligible || !provider)
1272
- return undefined;
1273
- return `Switching from ${RIVAL_NAMES[provider]}? Run \`${me} switch --from ${provider}\` before your first top-up and we match that top-up, up to ${cap}.`;
1414
+ if (!provider || !summary.eligible)
1415
+ return welcome;
1416
+ return [
1417
+ `Switching from ${RIVAL_NAMES[provider]}? Run \`${me} switch --from ${provider}\` before your first top-up to record it.`,
1418
+ welcome,
1419
+ ]
1420
+ .filter(Boolean)
1421
+ .join(" ");
1274
1422
  }
1275
1423
  const from = RIVAL_NAMES[sw.provider];
1276
- const recorded = `This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}`;
1277
1424
  const once = provider && provider !== sw.provider
1278
1425
  ? ` A switch is recorded once, so it stays ${from}, not ${RIVAL_NAMES[provider]}.`
1279
1426
  : "";
1280
- switch (sw.status) {
1281
- case "pending":
1282
- return `${recorded}: its first top-up is matched, up to ${cap}, once the payment settles.${once}`;
1283
- case "paid":
1284
- return `${recorded} and matched its first top-up: ${dollars(sw.creditMicros)} of credit.${once}`;
1285
- case "superseded":
1286
- return `${recorded}; its first top-up was matched by a referral instead, which is never less than the switching credit.${once}`;
1287
- case "reversed":
1288
- return `${recorded}; the top-up it matched was refunded, so the credit was taken back.${once}`;
1289
- case "refused":
1290
- return sw.refusedReason === "program_off"
1291
- ? `${recorded}; the switching credit was paused when its first top-up arrived.${once}`
1292
- : `${recorded}; its first top-up was not matched. Ask support if you think that is wrong.${once}`;
1293
- }
1427
+ return [
1428
+ `This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}.${once}`,
1429
+ welcome,
1430
+ ]
1431
+ .filter(Boolean)
1432
+ .join(" ");
1294
1433
  }
1295
1434
  /** Running time in the largest unit that keeps it above zero: a short
1296
1435
  * trial test is seconds, not "0 hours". */
@@ -1305,7 +1444,7 @@ function runningTime(seconds) {
1305
1444
  });
1306
1445
  return `${shown} ${unit}${shown === "1" ? "" : "s"}`;
1307
1446
  }
1308
- /** The sandboxes the free trial paid for, said beside what was priced. */
1447
+ /** The sandboxes free usage paid for, said beside what was priced. */
1309
1448
  function trialNote(trial, all, trialSeconds) {
1310
1449
  if (!trial)
1311
1450
  return "";
@@ -1314,7 +1453,7 @@ function trialNote(trial, all, trialSeconds) {
1314
1453
  ? "It"
1315
1454
  : "All of them"
1316
1455
  : `${trial} of them, ${runningTime(trialSeconds)} of that time,`;
1317
- return ` ${which} ran on the free trial, which charged nothing; Runtime's side prices ${trial === 1 ? "it" : "them"} at the standard rates, what the same work costs on paid credit.`;
1456
+ return ` ${which} ran on free usage, which charged nothing; Runtime's side prices ${trial === 1 ? "it" : "them"} at the standard rates, what the same work costs on paid credit.`;
1318
1457
  }
1319
1458
  async function switching(command, argv, env, out) {
1320
1459
  const args = parse(argv, [], command === "compare" ? ["from", "days"] : ["from"]);
@@ -1330,10 +1469,13 @@ async function switching(command, argv, env, out) {
1330
1469
  if (command === "switch") {
1331
1470
  if (!from) {
1332
1471
  const summary = await api.get();
1333
- print(switchingLine(summary) ??
1334
- (summary.eligible
1335
- ? `Moving from a rival? Run \`${me} switch --from <provider>\` (${summary.providers.join(", ")}) before your first top-up, and that top-up is matched, up to ${dollars(summary.maxMicros)}.`
1336
- : "The switching credit matches a first top-up, and this organization has made one already."), summary);
1472
+ const record = summary.eligible
1473
+ ? `Moving from a rival? Run \`${me} switch --from <provider>\` (${summary.providers.join(", ")}) before your first top-up to record it.`
1474
+ : undefined;
1475
+ print((summary.switch
1476
+ ? switchingLine(summary)
1477
+ : [record, welcomeLine(summary)].filter(Boolean).join(" ")) ||
1478
+ "A switch is recorded before the first top-up, and this organization has made one already.", summary);
1337
1479
  return 0;
1338
1480
  }
1339
1481
  const provider = SWITCH_FROM[from];
@@ -1341,7 +1483,7 @@ async function switching(command, argv, env, out) {
1341
1483
  throw unknown("provider", from, Object.keys(RIVAL_NAMES));
1342
1484
  const summary = await api.record({ provider });
1343
1485
  print([
1344
- `Recorded: switching from ${RIVAL_NAMES[provider]}. ${switchingLine(summary) ?? ""}`.trim(),
1486
+ `Recorded: switching from ${RIVAL_NAMES[provider]}. ${welcomeLine(summary) ?? ""}`.trim(),
1345
1487
  ...dropInLines(provider),
1346
1488
  ].join("\n"), summary);
1347
1489
  return 0;
@@ -1459,9 +1601,9 @@ async function sandbox(argv, env, out) {
1459
1601
  switch (verb) {
1460
1602
  case "create": {
1461
1603
  const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
1462
- const sbx = await runtime.sandboxes.create(createInput(args), {
1463
- onCapacityWait: waiting(out),
1464
- });
1604
+ const input = createInput(args);
1605
+ const onCapacityWait = waiting(out);
1606
+ const sbx = await withVolumeRelease((options) => runtime.sandboxes.create(input, { ...options, onCapacityWait }), out);
1465
1607
  print(sbx.id, sbx.info);
1466
1608
  return 0;
1467
1609
  }
@@ -1486,24 +1628,21 @@ async function sandbox(argv, env, out) {
1486
1628
  ? null
1487
1629
  : await runtime.sandboxes.list({ state: ["stopped"], limit: 50 });
1488
1630
  const stopped = hidden?.data.length
1489
- ? `${hidden.data.length}${hidden.hasMore ? "+" : ""} stopped; runtime sandbox ls --all shows them.`
1631
+ ? `${hidden.data.length}${hidden.hasMore ? "+" : ""} stopped; runtime sandboxes ls --all shows them.`
1490
1632
  : "";
1491
1633
  const rows = all.map((s) => [
1492
1634
  s.id,
1493
1635
  s.info.name ?? "-",
1494
1636
  s.state,
1495
1637
  `${s.info.vcpu}/${s.info.memoryMiB}`,
1496
- s.info.funding,
1497
- // When it stops or pauses by itself (0381): its time limit's end, or
1498
- // "-" when it never will. An older server sends no endsAt.
1499
- (s.info.endsAt === undefined ? s.info.expiresAt : s.info.endsAt) ?? "-",
1638
+ sandboxEnd(s.info),
1500
1639
  ]);
1501
1640
  print([
1502
1641
  rows.length
1503
- ? table([["ID", "NAME", "STATE", "CPU/MIB", "FUNDING", "ENDS"], ...rows])
1642
+ ? table([["ID", "NAME", "STATE", "CPU/MIB", "ENDS"], ...rows])
1504
1643
  : stopped
1505
1644
  ? "No live sandboxes."
1506
- : "No sandboxes. Create one: runtime sandbox create",
1645
+ : "No sandboxes. Create one: runtime sandboxes create",
1507
1646
  stopped,
1508
1647
  ]
1509
1648
  .filter(Boolean)
@@ -1518,31 +1657,31 @@ async function sandbox(argv, env, out) {
1518
1657
  case "switch-image": {
1519
1658
  const args = parse(rest, ["keep-workspace"], []);
1520
1659
  const [id, image] = args.positional;
1521
- const sbx = await use(need(id, `a sandbox, e.g. ${me} sandbox switch-image <id> app:v2 --keep-workspace`));
1660
+ const sbx = await use(need(id, `a sandbox, e.g. ${me} sandboxes switch-image <id> app:v2 --keep-workspace`));
1522
1661
  if (!args.flags.has("keep-workspace"))
1523
- throw usage(`A switch keeps only /workspace (your home), volumes, env and previews; processes restart and everything else on its disk is lost. Add --keep-workspace to say so, or snapshot it first (${me} sandbox snapshot ${sbx.id}) to keep everything.`);
1662
+ throw usage(`A switch keeps only /workspace (your home), volumes, env and previews; processes restart and everything else on its disk is lost. Add --keep-workspace to say so, or snapshot it first (${me} sandboxes snapshot ${sbx.id}) to keep everything.`);
1524
1663
  await sbx.switchImage(need(image, "an image, e.g. app:v2"), { keep: "workspace" });
1525
1664
  print(describe(sbx.info), sbx.info);
1526
1665
  return 0;
1527
1666
  }
1528
1667
  case "resize": {
1529
1668
  const args = parse(rest, ["restart"], ["vcpu", "memory"]);
1530
- const sbx = await use(need(args.positional[0], `a sandbox, e.g. ${me} sandbox resize <id> --memory 4096 --restart`));
1669
+ const sbx = await use(need(args.positional[0], `a sandbox, e.g. ${me} sandboxes resize <id> --memory 4096 --restart`));
1531
1670
  const size = {
1532
1671
  ...(integer(args, "vcpu") !== undefined ? { vcpu: integer(args, "vcpu") } : {}),
1533
1672
  ...(integer(args, "memory") !== undefined ? { memoryMiB: integer(args, "memory") } : {}),
1534
1673
  };
1535
1674
  if (size.vcpu === undefined && size.memoryMiB === undefined)
1536
- throw usage(`Name a new size, e.g. ${me} sandbox resize ${sbx.id} --memory 4096 --restart`);
1675
+ throw usage(`Name a new size, e.g. ${me} sandboxes resize ${sbx.id} --memory 4096 --restart`);
1537
1676
  if (!args.flags.has("restart"))
1538
- throw usage(`A resize restarts it: its whole disk stays and its programs stop. Add --restart to say so, or snapshot it first (${me} sandbox snapshot ${sbx.id}) to keep its memory too.`);
1677
+ throw usage(`A resize restarts it: its whole disk stays and its programs stop. Add --restart to say so, or snapshot it first (${me} sandboxes snapshot ${sbx.id}) to keep its memory too.`);
1539
1678
  await sbx.resize(size, { restart: true });
1540
1679
  print(describe(sbx.info), sbx.info);
1541
1680
  return 0;
1542
1681
  }
1543
1682
  case "rm":
1544
1683
  case "delete": {
1545
- const sbx = await use(need(rest[0], `a sandbox, e.g. ${me} sandbox rm <id>`));
1684
+ const sbx = await use(need(rest[0], `a sandbox, e.g. ${me} sandboxes rm <id>`));
1546
1685
  const gone = await sbx.delete();
1547
1686
  print(`${gone.id} deleted`, gone);
1548
1687
  return 0;
@@ -1644,15 +1783,15 @@ async function sandbox(argv, env, out) {
1644
1783
  print(gone.left ? "Left the tailnet." : "Not on a tailnet.", gone);
1645
1784
  return 0;
1646
1785
  }
1647
- throw usage("runtime sandbox tailscale up|status|down <id>");
1786
+ throw usage("runtime sandboxes tailscale up|status|down <id>");
1648
1787
  }
1649
1788
  case "snapshot": {
1650
1789
  const args = parse(rest, [], ["name", "retention"]);
1651
1790
  const sbx = await get(args.positional[0]);
1652
1791
  const snapshot = await sbx.snapshot({
1653
1792
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
1654
- ...(integer(args, "retention") !== undefined
1655
- ? { retentionDays: integer(args, "retention") }
1793
+ ...(daysOrCredit(args, "retention") !== undefined
1794
+ ? { retentionDays: daysOrCredit(args, "retention") }
1656
1795
  : {}),
1657
1796
  });
1658
1797
  print(snapshot.id, snapshot);
@@ -1724,26 +1863,26 @@ async function sandbox(argv, env, out) {
1724
1863
  }
1725
1864
  case "extend": {
1726
1865
  const sbx = await get(rest[0]);
1727
- const seconds = Number(need(rest[1], "seconds, e.g. runtime sandbox extend <id> 600"));
1866
+ const seconds = Number(need(rest[1], "seconds, e.g. runtime sandboxes extend <id> 600"));
1728
1867
  await sbx.extend(seconds);
1729
- print(sbx.info.endsAt === null
1868
+ print(sandboxEnd(sbx.info) === "no limit"
1730
1869
  ? `${sbx.id} has no time limit: it runs while it works, so nothing was extended`
1731
- : `${sbx.id} now ends at ${sbx.info.endsAt ?? sbx.info.expiresAt}`, sbx.info);
1870
+ : `${sbx.id} now ends at ${sandboxEnd(sbx.info)}`, sbx.info);
1732
1871
  return 0;
1733
1872
  }
1734
1873
  case "exec": {
1735
- const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandbox exec <id> -- ls -la`);
1874
+ const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandboxes exec <id> -- ls -la`);
1736
1875
  const command = args.rest ?? args.positional.slice(1);
1737
1876
  if (!command.length)
1738
- throw usage("Give the command after --: runtime sandbox exec <id> -- ls -la");
1877
+ throw usage("Give the command after --: runtime sandboxes exec <id> -- ls -la");
1739
1878
  const sbx = await use(args.positional[0]);
1740
1879
  return execute(sbx, command, args, out, integer(args, "timeout"));
1741
1880
  }
1742
1881
  case "spawn": {
1743
- const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandbox spawn <id> -- npm run dev`);
1882
+ const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandboxes spawn <id> -- npm run dev`);
1744
1883
  const command = args.rest ?? [];
1745
1884
  if (!command.length)
1746
- throw usage("Give the command after --: runtime sandbox spawn <id> -- npm run dev");
1885
+ throw usage("Give the command after --: runtime sandboxes spawn <id> -- npm run dev");
1747
1886
  const sbx = await use(args.positional[0]);
1748
1887
  const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
1749
1888
  ...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
@@ -1759,36 +1898,54 @@ async function sandbox(argv, env, out) {
1759
1898
  print(list.length
1760
1899
  ? table([
1761
1900
  ["PROCESS", "STATE", "EXIT", "COMMAND"],
1762
- ...list.map((p) => [p.id, p.state, String(p.exitCode ?? "-"), p.command]),
1901
+ // The command line is the guest's report: escaped, as a log is.
1902
+ ...list.map((p) => [
1903
+ terminalSafe(String(p.id)),
1904
+ terminalSafe(String(p.state)),
1905
+ `${typeof p.exitCode === "number" ? p.exitCode : "-"}${p.outOfMemory ? " (out of memory)" : ""}`,
1906
+ terminalSafe(String(p.command ?? "")),
1907
+ ]),
1763
1908
  ])
1764
1909
  : "No processes.", list);
1910
+ const killed = list.find((p) => p.outOfMemory)?.outOfMemory;
1911
+ if (killed && typeof killed.hint === "string" && !out.json)
1912
+ out.error(`Hint: ${terminalSafe(killed.hint)}`);
1765
1913
  return 0;
1766
1914
  }
1767
1915
  case "logs": {
1768
- const args = parse(rest, ["f", "follow"], []);
1916
+ const args = parse(rest, ["f", "follow", "raw"], []);
1769
1917
  const sbx = await use(args.positional[0]);
1770
1918
  const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
1919
+ const [stdout, stderr, shown] = [
1920
+ logStream(process.stdout, has(args, "raw")),
1921
+ logStream(process.stderr, has(args, "raw")),
1922
+ logText(args),
1923
+ ];
1771
1924
  if (has(args, "f") || has(args, "follow")) {
1772
1925
  for await (const event of proc.output())
1773
1926
  if (event.type === "stdout")
1774
- process.stdout.write(event.data);
1927
+ stdout(event.data);
1775
1928
  else if (event.type === "stderr")
1776
- process.stderr.write(event.data);
1929
+ stderr(event.data);
1777
1930
  else if (out.json)
1778
- out.write(JSON.stringify(event));
1931
+ out.write(shown(JSON.stringify(event)));
1932
+ else if (event.type === "exit" && event.outOfMemory)
1933
+ out.error(outOfMemoryLines(event.outOfMemory));
1779
1934
  return 0;
1780
1935
  }
1781
1936
  /* Without -f, what it has printed so far, then back to the prompt, as
1782
1937
  `docker logs` does: a server's log is read, not followed forever. */
1783
1938
  const so = await outputSoFar(runtime, sbx.id, proc.id);
1784
1939
  if (out.json) {
1785
- out.write(JSON.stringify(so));
1940
+ out.write(shown(JSON.stringify(so)));
1786
1941
  return 0;
1787
1942
  }
1788
1943
  for (const chunk of so.chunks)
1789
- (chunk.stream === "stderr" ? process.stderr : process.stdout).write(chunk.text);
1944
+ (chunk.stream === "stderr" ? stderr : stdout)(chunk.text);
1945
+ if (so.outOfMemory)
1946
+ out.error(outOfMemoryLines(so.outOfMemory));
1790
1947
  if (so.state === "running")
1791
- process.stderr.write(`[${proc.id} is still running; follow it: ${me} sandbox logs ${sbx.id} ${proc.id} -f]\n`);
1948
+ process.stderr.write(`[${proc.id} is still running; follow it: ${me} sandboxes logs ${sbx.id} ${proc.id} -f]\n`);
1792
1949
  return 0;
1793
1950
  }
1794
1951
  case "kill": {
@@ -1858,7 +2015,7 @@ async function sandbox(argv, env, out) {
1858
2015
  return runCode(await use(rest[0]), rest.slice(1), out);
1859
2016
  case "preview": {
1860
2017
  if (rest[0] === "rotate") {
1861
- const port = portOf(rest[2], "runtime sandbox preview rotate <id> 3000");
2018
+ const port = portOf(rest[2], "runtime sandboxes preview rotate <id> 3000");
1862
2019
  const sbx = await use(rest[1]);
1863
2020
  const preview = await sbx.previews.rotate(port);
1864
2021
  print(preview.visibility === "public"
@@ -1867,7 +2024,7 @@ async function sandbox(argv, env, out) {
1867
2024
  return 0;
1868
2025
  }
1869
2026
  const args = parse(rest, ["public"], ["ttl"]);
1870
- const port = portOf(args.positional[1], "runtime sandbox preview <id> 3000");
2027
+ const port = portOf(args.positional[1], "runtime sandboxes preview <id> 3000");
1871
2028
  const sbx = await use(args.positional[0]);
1872
2029
  const preview = await sbx.previews.create(port, {
1873
2030
  ...(has(args, "public") ? { visibility: "public" } : {}),
@@ -1887,7 +2044,7 @@ async function sandbox(argv, env, out) {
1887
2044
  ["PORT", "VISIBILITY", "URL"],
1888
2045
  ...list.map((p) => [String(p.port), p.visibility, p.url]),
1889
2046
  ])
1890
- : "No shared ports. Share one: runtime sandbox preview <id> 3000", list);
2047
+ : "No shared ports. Share one: runtime sandboxes preview <id> 3000", list);
1891
2048
  return 0;
1892
2049
  }
1893
2050
  case "session": {
@@ -1904,17 +2061,17 @@ async function sandbox(argv, env, out) {
1904
2061
  one.origins.join(",") || "-",
1905
2062
  ]),
1906
2063
  ])
1907
- : "No sessions. Make one: runtime sandbox session create <id> --origin https://app.example.com", list);
2064
+ : "No sessions. Make one: runtime sandboxes session create <id> --origin https://app.example.com", list);
1908
2065
  return 0;
1909
2066
  }
1910
2067
  if (verb === "revoke") {
1911
- const sessionId = need(rest[2], "a session id: runtime sandbox session revoke <id> <session-id>");
2068
+ const sessionId = need(rest[2], "a session id: runtime sandboxes session revoke <id> <session-id>");
1912
2069
  const ended = await (await use(rest[1])).sessions.revoke(sessionId);
1913
2070
  print(`Session ${ended.id} ended.`, ended);
1914
2071
  return 0;
1915
2072
  }
1916
2073
  if (verb !== "create")
1917
- throw usage("runtime sandbox session create <id> [--origin <url>]..., session ls <id>, or session revoke <id> <session-id>");
2074
+ throw usage("runtime sandboxes session create <id> [--origin <url>]..., session ls <id>, or session revoke <id> <session-id>");
1918
2075
  const args = parse(rest.slice(1), [], ["origin", "ttl", "name"]);
1919
2076
  const origins = args.flags.get("origin") ?? [];
1920
2077
  const ttl = integer(args, "ttl");
@@ -1928,7 +2085,7 @@ async function sandbox(argv, env, out) {
1928
2085
  return 0;
1929
2086
  }
1930
2087
  case "unshare": {
1931
- const port = portOf(rest[1], "runtime sandbox unshare <id> 3000");
2088
+ const port = portOf(rest[1], "runtime sandboxes unshare <id> 3000");
1932
2089
  const sbx = await use(rest[0]);
1933
2090
  print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
1934
2091
  return 0;
@@ -2126,7 +2283,7 @@ read = readSecret) {
2126
2283
  }
2127
2284
  if (!hosts.length) {
2128
2285
  const saved = await runtime.secrets.set(name, { value, jobs: true });
2129
- print(`Stored ${name} for jobs, version ${saved.jobs.version}. Bind it: ${me} job create ... --secret ${name}`, saved);
2286
+ print(`Stored ${name} for jobs, version ${saved.jobs.version}. Bind it: ${me} jobs create ... --secret ${name}`, saved);
2130
2287
  return 0;
2131
2288
  }
2132
2289
  const header = flag(args, "header");
@@ -2145,14 +2302,14 @@ read = readSecret) {
2145
2302
  `Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
2146
2303
  saved.header
2147
2304
  ? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
2148
- : `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the URL and headers.`,
2305
+ : `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the Authorization header or another credential header (x-api-key, x-goog-api-key); in the URL it stays a placeholder.`,
2149
2306
  ...(saved.rules?.length ? [`Only these requests get it: ${ruleText(saved.rules)}.`] : []),
2150
2307
  ...(saved.enforced
2151
2308
  ? []
2152
2309
  : ["A host is catching up; running sandboxes have it within a minute."]),
2153
2310
  ...(saved.jobs
2154
2311
  ? [
2155
- `Also stored for jobs, version ${saved.jobs.version}: --secret ${saved.name} on ${me} job create.`,
2312
+ `Also stored for jobs, version ${saved.jobs.version}: --secret ${saved.name} on ${me} jobs create.`,
2156
2313
  ]
2157
2314
  : []),
2158
2315
  ].join("\n"), saved);
@@ -2291,7 +2448,7 @@ async function desktop(sbx, argv, out) {
2291
2448
  return 0;
2292
2449
  }
2293
2450
  }
2294
- throw usage(`${me} sandbox desktop <id> start|stop|screenshot|open|click|type|press|windows|record`);
2451
+ throw usage(`${me} sandboxes desktop <id> start|stop|screenshot|open|click|type|press|windows|record`);
2295
2452
  }
2296
2453
  async function browser(sbx, argv, out) {
2297
2454
  const [action, ...rest] = argv;
@@ -2305,7 +2462,8 @@ async function browser(sbx, argv, out) {
2305
2462
  ...(integer(args, "height") !== undefined ? { height: integer(args, "height") } : {}),
2306
2463
  });
2307
2464
  print(`${started.version}${started.headless ? "" : `, on the desktop: ${started.streamUrl}`}\n` +
2308
- `CDP: ${started.cdpUrl}\n(private; it expires ${started.expiresAt ?? "never"})`, started);
2465
+ `CDP: ${started.cdpUrl}\n(private; it expires ${started.expiresAt ?? "never"})` +
2466
+ (started.hint ? `\n${started.hint}` : ""), started);
2309
2467
  return 0;
2310
2468
  }
2311
2469
  case "get":
@@ -2320,7 +2478,7 @@ async function browser(sbx, argv, out) {
2320
2478
  return 0;
2321
2479
  }
2322
2480
  }
2323
- throw usage(`${me} sandbox browser <id> start [--headed]|get|stop`);
2481
+ throw usage(`${me} sandboxes browser <id> start [--headed]|get|stop`);
2324
2482
  }
2325
2483
  const KEYS_HELP = `runtime keys <command>
2326
2484
 
@@ -2493,14 +2651,14 @@ async function keys(argv, env, out) {
2493
2651
  await confirm();
2494
2652
  return 0;
2495
2653
  }
2496
- const SNAPSHOT_HELP = `runtime snapshot <command>
2654
+ const SNAPSHOT_HELP = `runtime snapshots <command>
2497
2655
 
2498
2656
  ls [--sandbox <id>] [--name n] Your snapshots
2499
2657
  get <id> One snapshot
2500
- extend <id> <days> Keep it longer
2658
+ extend <id> <days|credit> Keep it longer; credit: while you have credit
2501
2659
  rm <id> Delete it
2502
- Take one with \`runtime sandbox snapshot <sandboxId>\`; start a sandbox from it
2503
- with \`runtime sandbox create --snapshot <snapshotId>\`.
2660
+ Take one with \`runtime sandboxes snapshot <sandboxId>\`; start a sandbox from it
2661
+ with \`runtime sandboxes create --snapshot <snapshotId>\`.
2504
2662
  `;
2505
2663
  async function snapshot(argv, env, out) {
2506
2664
  const [verb, ...rest] = argv;
@@ -2523,9 +2681,15 @@ async function snapshot(argv, env, out) {
2523
2681
  print(all.length
2524
2682
  ? table([
2525
2683
  ["ID", "NAME", "STATUS", "FROM", "EXPIRES"],
2526
- ...all.map((s) => [s.id, s.name ?? "-", s.status, s.sourceSandboxId, s.expiresAt]),
2684
+ ...all.map((s) => [
2685
+ s.id,
2686
+ s.name ?? "-",
2687
+ s.status,
2688
+ s.sourceSandboxId,
2689
+ s.expiresAt ?? "with credit",
2690
+ ]),
2527
2691
  ])
2528
- : "No snapshots. Take one: runtime sandbox snapshot <sandboxId>", all);
2692
+ : "No snapshots. Take one: runtime sandboxes snapshot <sandboxId>", all);
2529
2693
  return 0;
2530
2694
  }
2531
2695
  case "get": {
@@ -2534,9 +2698,11 @@ async function snapshot(argv, env, out) {
2534
2698
  return 0;
2535
2699
  }
2536
2700
  case "extend": {
2537
- const days = integer(parse(rest.slice(1), [], ["days"]), "days") ?? Number(need(rest[1], "days"));
2701
+ const word = parse(rest.slice(1), [], ["days"]);
2702
+ const days = daysOrCredit(word, "days") ??
2703
+ (need(rest[1], "days") === "credit" ? null : Number(need(rest[1], "days")));
2538
2704
  const found = await api.extend(need(rest[0], "a snapshot id"), days);
2539
- print(`${found.id} kept until ${found.expiresAt}`, found);
2705
+ print(`${found.id} kept ${found.expiresAt === null ? "while you have credit" : `until ${found.expiresAt}`}`, found);
2540
2706
  return 0;
2541
2707
  }
2542
2708
  case "rm":
@@ -2549,7 +2715,7 @@ async function snapshot(argv, env, out) {
2549
2715
  }
2550
2716
  throw unknown("snapshot command", verb, ["ls", "get", "extend", "rm"]);
2551
2717
  }
2552
- const IMAGE_HELP = `runtime image <command>
2718
+ const IMAGE_HELP = `runtime images <command>
2553
2719
 
2554
2720
  build [<folder>] [--dockerfile|-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
2555
2721
  [--name <name>] [--tag|-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
@@ -2564,8 +2730,9 @@ const IMAGE_HELP = `runtime image <command>
2564
2730
  ls Your images
2565
2731
  versions <name> Every version of a name, newest first
2566
2732
  get <image> One image: an id, name, name:tag or name@version
2567
- logs <image> [--follow]
2568
- Its build log
2733
+ logs <image> [--follow] [--raw]
2734
+ Its build log; on a terminal control characters
2735
+ show as \\uXXXX unless --raw
2569
2736
  tag <image> <tag> Point a tag of the image's name at this version
2570
2737
  untag <image> <tag> Take a tag off
2571
2738
  rm <image> Delete that version and its tags
@@ -2576,7 +2743,7 @@ const IMAGE_HELP = `runtime image <command>
2576
2743
  registry set <ecr registry> --access-key-id <id>
2577
2744
  Amazon ECR: the secret access key is read from standard input
2578
2745
  registry rm <registry>
2579
- Use one: runtime sandbox create --image app:latest
2746
+ Use one: runtime sandboxes create --image app:latest
2580
2747
  `;
2581
2748
  /** Options `image build` takes with a value. */
2582
2749
  const IMAGE_BUILD_VALUES = [
@@ -2711,7 +2878,7 @@ read = readSecret) {
2711
2878
  ])
2712
2879
  : name
2713
2880
  ? `No versions of ${name}.`
2714
- : "No images. Build one: runtime image build --pip pandas", all);
2881
+ : "No images. Build one: runtime images build --pip pandas", all);
2715
2882
  return 0;
2716
2883
  }
2717
2884
  case "get": {
@@ -2720,13 +2887,14 @@ read = readSecret) {
2720
2887
  return 0;
2721
2888
  }
2722
2889
  case "logs": {
2723
- const args = parse(rest, ["follow", "f"], []);
2890
+ const args = parse(rest, ["follow", "f", "raw"], []);
2724
2891
  const id = (await api.resolve(need(args.positional[0], "an image"))).id;
2892
+ const shown = logText(args);
2725
2893
  if (has(args, "follow") || has(args, "f")) {
2726
2894
  const lines = [];
2727
- const done = await api.followLogs(id, (line) => out.json ? lines.push(line.text) : out.write(line.text));
2895
+ const done = await api.followLogs(id, (line) => out.json ? lines.push(line.text) : out.write(shown(line.text)));
2728
2896
  if (out.json)
2729
- print("", { lines, state: done.state });
2897
+ out.write(shown(JSON.stringify({ lines, state: done.state })));
2730
2898
  return done.state === "failed" ? 1 : 0;
2731
2899
  }
2732
2900
  let after = 0;
@@ -2738,7 +2906,7 @@ read = readSecret) {
2738
2906
  break;
2739
2907
  after = page.nextAfter;
2740
2908
  }
2741
- print(lines.join("\n") || "(no log yet)", lines);
2909
+ out.write(shown(out.json ? JSON.stringify(lines) : lines.join("\n") || "(no log yet)"));
2742
2910
  return 0;
2743
2911
  }
2744
2912
  case "tag":
@@ -2765,7 +2933,7 @@ read = readSecret) {
2765
2933
  ["REGISTRY", "KIND", "USER", "SINCE"],
2766
2934
  ...all.map((r) => [r.registry, r.kind, r.username, r.createdAt]),
2767
2935
  ])
2768
- : "No registry credentials. Store some: runtime image registry set ghcr.io --username you", all);
2936
+ : "No registry credentials. Store some: runtime images registry set ghcr.io --username you", all);
2769
2937
  return 0;
2770
2938
  }
2771
2939
  if (action === "set") {
@@ -2805,10 +2973,10 @@ read = readSecret) {
2805
2973
  "registry",
2806
2974
  ]);
2807
2975
  }
2808
- const JOB_HELP = `runtime job <command>
2976
+ const JOB_HELP = `runtime jobs <command>
2809
2977
 
2810
2978
  A job runs a command in a fresh sandbox, once or on a schedule. Each run spends
2811
- the free trial's hours first, then credit at the sandbox rates.
2979
+ the included usage first, then credit at the sandbox rates.
2812
2980
 
2813
2981
  create <name> (--cron "<min hour day month weekday>" [--timezone Europe/Berlin] | --at <time|now>)
2814
2982
  [--vcpu 2] [--memory 4096] [--disk 4096] [--cpu shared|reserved] [--cpu-floor <thousandths>]
@@ -2823,14 +2991,21 @@ const JOB_HELP = `runtime job <command>
2823
2991
  ls Your jobs: state, schedule and next run
2824
2992
  get <id> One job, in full
2825
2993
  runs <id> Its runs: state, exit code, when
2826
- logs <runId|jobId> [-f] A run's output (a job id: its latest run); -f follows it
2994
+ logs <runId|jobId> [-f] [--raw] A run's output (a job id: its latest run); -f follows it;
2995
+ on a terminal control characters show as \\uXXXX
2996
+ unless --raw
2827
2997
  pause <id> | resume <id> Stop or restart scheduling; a run already going finishes
2998
+ trigger <id> Run it now, once; its schedule does not move, and a
2999
+ paused job stays paused
3000
+ reschedule <id> (--cron "<five fields>" [--timezone Europe/Berlin] | --at <time|now>)
3001
+ A new schedule; the next run is its next time
2828
3002
  cancel <id> End it for good and stop a run in progress
2829
3003
 
2830
3004
  Examples
2831
- runtime job create nightly --cron "0 3 * * *" --timezone Europe/Berlin -- python3 /workspace/report.py
2832
- runtime job create once --at now -- bash -lc 'echo hello from a job'
2833
- runtime job logs <jobId> -f
3005
+ runtime jobs create nightly --cron "0 3 * * *" --timezone Europe/Berlin -- python3 /workspace/report.py
3006
+ runtime jobs create once --at now -- bash -lc 'echo hello from a job'
3007
+ runtime jobs trigger <jobId> && runtime jobs logs <jobId> -f
3008
+ runtime jobs reschedule <jobId> --cron "30 4 * * 1-5" --timezone Europe/Berlin
2834
3009
  `;
2835
3010
  const when = (ms) => (ms == null ? "-" : new Date(ms).toISOString());
2836
3011
  function scheduleText(job) {
@@ -2878,13 +3053,13 @@ async function jobCommand(argv, env, out) {
2878
3053
  "max-cost",
2879
3054
  "max-total-cost",
2880
3055
  "region",
2881
- ], `${me} job create <name> --cron "0 3 * * *" -- python3 -c '...'`);
3056
+ ], `${me} jobs create <name> --cron "0 3 * * *" -- python3 -c '...'`);
2882
3057
  const name = need(args.positional[0], "the job's name, such as nightly-report");
2883
3058
  if (args.positional.length > 1)
2884
- throw usage(`Put the command after --: ${me} job create ${name} ... -- ${args.positional.slice(1).join(" ")}`);
3059
+ throw usage(`Put the command after --: ${me} jobs create ${name} ... -- ${args.positional.slice(1).join(" ")}`);
2885
3060
  const argv = args.rest ?? [];
2886
3061
  if (!argv.length)
2887
- throw usage(`Give the command after --: ${me} job create ${name} --at now -- echo hello`);
3062
+ throw usage(`Give the command after --: ${me} jobs create ${name} --at now -- echo hello`);
2888
3063
  const cron = flag(args, "cron");
2889
3064
  const at = flag(args, "at");
2890
3065
  if ((cron === undefined) === (at === undefined))
@@ -2941,7 +3116,7 @@ async function jobCommand(argv, env, out) {
2941
3116
  : {}),
2942
3117
  });
2943
3118
  if (!out.json)
2944
- out.error(`Scheduled ${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}. Follow it: ${me} job logs ${job.id} -f`);
3119
+ out.error(`Scheduled ${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}. Follow it: ${me} jobs logs ${job.id} -f`);
2945
3120
  print(job.id, job);
2946
3121
  return 0;
2947
3122
  }
@@ -2961,7 +3136,7 @@ async function jobCommand(argv, env, out) {
2961
3136
  j.blockedReason ?? "-",
2962
3137
  ]),
2963
3138
  ])
2964
- : `No jobs. Schedule one: ${me} job create hello --at now -- echo hello`, all);
3139
+ : `No jobs. Schedule one: ${me} jobs create hello --at now -- echo hello`, all);
2965
3140
  return 0;
2966
3141
  }
2967
3142
  case "get": {
@@ -2990,8 +3165,13 @@ async function jobCommand(argv, env, out) {
2990
3165
  return 0;
2991
3166
  }
2992
3167
  case "logs": {
2993
- const args = parse(rest, ["f", "follow"], []);
3168
+ const args = parse(rest, ["f", "follow", "raw"], []);
2994
3169
  const id = need(args.positional[0], "a run id or a job id");
3170
+ const [stdout, stderr, shown] = [
3171
+ logStream(process.stdout, has(args, "raw")),
3172
+ logStream(process.stderr, has(args, "raw")),
3173
+ logText(args),
3174
+ ];
2995
3175
  let runId = id;
2996
3176
  try {
2997
3177
  await api.run(id);
@@ -3019,25 +3199,27 @@ async function jobCommand(argv, env, out) {
3019
3199
  for (;;) {
3020
3200
  const page = await api.logs(runId, { cursor, limitBytes: 65536 });
3021
3201
  if (out.json)
3022
- out.write(JSON.stringify(page));
3202
+ out.write(shown(JSON.stringify(page)));
3023
3203
  else
3024
3204
  for (const chunk of page.chunks)
3025
- (chunk.stream === "stderr" ? process.stderr : process.stdout).write(chunk.text);
3205
+ (chunk.stream === "stderr" ? stderr : stdout)(chunk.text);
3026
3206
  cursor = page.nextCursor;
3027
3207
  if (page.complete) {
3208
+ if (!follow && out.json)
3209
+ return 0;
3210
+ const run = await api.run(runId);
3028
3211
  if (!out.json) {
3029
- const run = await api.run(runId);
3030
3212
  if (page.truncated)
3031
3213
  process.stderr.write("[the run printed more than it keeps; this is its last part]\n");
3032
3214
  process.stderr.write(`[${run.state}${run.exitCode === null ? "" : `, exit code ${run.exitCode}`}${run.reason ? `, ${run.reason}` : ""}]\n`);
3033
3215
  }
3034
- return 0;
3216
+ return follow ? (run.exitCode ?? (run.state === "succeeded" ? 0 : 1)) : 0;
3035
3217
  }
3036
3218
  if (page.chunks.length)
3037
3219
  continue;
3038
3220
  if (!follow) {
3039
3221
  if (!out.json)
3040
- process.stderr.write(`[${runId} is still going; follow it: ${me} job logs ${runId} -f]\n`);
3222
+ process.stderr.write(`[${runId} is still going; follow it: ${me} jobs logs ${runId} -f]\n`);
3041
3223
  return 0;
3042
3224
  }
3043
3225
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -3051,6 +3233,27 @@ async function jobCommand(argv, env, out) {
3051
3233
  print(`${job.name} ${job.state}.`, job);
3052
3234
  return 0;
3053
3235
  }
3236
+ case "trigger": {
3237
+ const id = need(rest[0], "a job id");
3238
+ const job = await api.trigger(id);
3239
+ print(`${job.name} runs now${job.state === "paused" ? ", once; it stays paused" : ""}. Follow it: ${me} jobs logs ${job.id} -f`, job);
3240
+ return 0;
3241
+ }
3242
+ case "reschedule": {
3243
+ const args = parse(rest, [], ["cron", "timezone", "at"], `${me} jobs reschedule <id> --cron "0 3 * * *" [--timezone Europe/Berlin]`);
3244
+ const id = need(args.positional[0], "a job id");
3245
+ const cron = flag(args, "cron");
3246
+ const at = flag(args, "at");
3247
+ if ((cron === undefined) === (at === undefined))
3248
+ throw usage('Give --cron "<five fields>" for a schedule, or --at <time|now> for one run.');
3249
+ if (at !== undefined && flag(args, "timezone") !== undefined)
3250
+ throw usage("--timezone goes with --cron; --at takes a time with its own offset.");
3251
+ const job = await api.reschedule(id, cron !== undefined
3252
+ ? { cron, ...(flag(args, "timezone") ? { timezone: flag(args, "timezone") } : {}) }
3253
+ : { at: atTime(at) });
3254
+ print(`${job.name}: ${scheduleText(job)}. Next run ${when(job.nextRunAt)}.`, job);
3255
+ return 0;
3256
+ }
3054
3257
  }
3055
3258
  throw unknown("job command", verb, [
3056
3259
  "create",
@@ -3060,12 +3263,15 @@ async function jobCommand(argv, env, out) {
3060
3263
  "logs",
3061
3264
  "pause",
3062
3265
  "resume",
3266
+ "trigger",
3267
+ "reschedule",
3063
3268
  "cancel",
3064
3269
  ]);
3065
3270
  }
3066
- const VOLUME_HELP = `runtime volume <command>
3271
+ const VOLUME_HELP = `runtime volumes <command>
3067
3272
 
3068
- create --size-mib <N> [--name n] [--shared] A persistent disk; prints its id
3273
+ create --size-mib <N> [--name n] [--label k=v]... [--shared]
3274
+ A persistent disk; prints its id
3069
3275
  ls Your volumes
3070
3276
  get <id> One volume, its backups and where it is attached
3071
3277
  resize <id> --size-mib <N> Grow a detached volume on its current host
@@ -3080,7 +3286,7 @@ const VOLUME_HELP = `runtime volume <command>
3080
3286
  backup-rm <backupId> Delete a backup
3081
3287
  backup-policy <id> [--daily on|off] [--retention-days N]
3082
3288
  Daily backups on or off, and how long each is kept
3083
- Attach one: runtime sandbox create --volume <id>:/data
3289
+ Attach one: runtime sandboxes create --volume <id>:/data
3084
3290
  Volumes are backed up off their host daily unless you turn it off.
3085
3291
  Shared commands work where shared disks are enabled.
3086
3292
  `;
@@ -3094,10 +3300,11 @@ async function volume(argv, env, out) {
3094
3300
  const api = (await client(env)).volumes;
3095
3301
  switch (verb) {
3096
3302
  case "create": {
3097
- const args = parse(rest, ["shared"], ["size-mib", "name"]);
3303
+ const args = parse(rest, ["shared"], ["size-mib", "name", "label"]);
3098
3304
  const created = await api.create({
3099
3305
  sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
3100
3306
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
3307
+ ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
3101
3308
  ...(has(args, "shared") ? { shared: true } : {}),
3102
3309
  });
3103
3310
  print(created.id, created);
@@ -3118,7 +3325,7 @@ async function volume(argv, env, out) {
3118
3325
  v.attachments.map((a) => `${a.sandboxId}:${a.path}`).join(", ") || "-",
3119
3326
  ]),
3120
3327
  ])
3121
- : "No volumes. Create one: runtime volume create --size-mib 10240", all);
3328
+ : "No volumes. Create one: runtime volumes create --size-mib 10240", all);
3122
3329
  return 0;
3123
3330
  }
3124
3331
  case "get": {
@@ -3130,7 +3337,7 @@ async function volume(argv, env, out) {
3130
3337
  const args = parse(rest, [], ["size-mib"]);
3131
3338
  const id = need(args.positional[0], "a volume id");
3132
3339
  if (args.positional.length !== 1 || args.rest)
3133
- throw usage(`Use ${me} volume resize <id> --size-mib <N>.`);
3340
+ throw usage(`Use ${me} volumes resize <id> --size-mib <N>.`);
3134
3341
  const sizeMiB = integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>"));
3135
3342
  const resized = await api.resize(id, { sizeMiB });
3136
3343
  print(describeRecord(resized, table), resized);
@@ -3142,7 +3349,7 @@ async function volume(argv, env, out) {
3142
3349
  const sandboxId = need(args.positional[1], "a sandbox id");
3143
3350
  const path = need(flag(args, "path"), "--path /data");
3144
3351
  if (args.positional.length !== 2 || args.rest)
3145
- throw usage(`Use ${me} volume attach <id> <sandboxId> --path /data.`);
3352
+ throw usage(`Use ${me} volumes attach <id> <sandboxId> --path /data.`);
3146
3353
  const attachment = await api.attach(id, { sandboxId, path });
3147
3354
  print(describeRecord(attachment, table), attachment);
3148
3355
  return attachment.error ? 1 : 0;
@@ -3153,7 +3360,7 @@ async function volume(argv, env, out) {
3153
3360
  const id = need(args.positional[0], "a volume id");
3154
3361
  const attachmentId = need(args.positional[1], "an attachment id");
3155
3362
  if (args.positional.length !== 2 || args.rest)
3156
- throw usage(`Use ${me} volume ${verb} <id> <attachmentId>.`);
3363
+ throw usage(`Use ${me} volumes ${verb} <id> <attachmentId>.`);
3157
3364
  const attachment = verb === "detach"
3158
3365
  ? await api.detach(id, attachmentId)
3159
3366
  : await api.getAttachment(id, attachmentId);
@@ -3162,7 +3369,8 @@ async function volume(argv, env, out) {
3162
3369
  }
3163
3370
  case "rm":
3164
3371
  case "delete": {
3165
- const found = await api.delete(need(rest[0], "a volume id"));
3372
+ const id = need(rest[0], "a volume id");
3373
+ const found = await withVolumeRelease((options) => api.delete(id, options), out);
3166
3374
  print(`${found.id} ${found.state}`, found);
3167
3375
  return 0;
3168
3376
  }
@@ -3172,7 +3380,7 @@ async function volume(argv, env, out) {
3172
3380
  const days = integer(args, "retention-days");
3173
3381
  const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
3174
3382
  if (!out.json && made.state === "pending")
3175
- out.error(`Copying it off the host; check with: ${me} volume backups ${id}`);
3383
+ out.error(`Copying it off the host; check with: ${me} volumes backups ${id}`);
3176
3384
  print(made.id, made);
3177
3385
  return 0;
3178
3386
  }
@@ -3200,7 +3408,7 @@ async function volume(argv, env, out) {
3200
3408
  const args = parse(rest, [], ["name"]);
3201
3409
  const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
3202
3410
  if (!out.json && created.state === "creating")
3203
- out.error(`Restoring; check with: ${me} volume get ${created.id}`);
3411
+ out.error(`Restoring; check with: ${me} volumes get ${created.id}`);
3204
3412
  print(created.id, created);
3205
3413
  return 0;
3206
3414
  }
@@ -3254,17 +3462,52 @@ const CREATE_FLAGS = [
3254
3462
  "get-or-create",
3255
3463
  ];
3256
3464
  /** Says on standard error, once for each reason, why a create has not
3257
- * answered yet: it is waiting for a trial slot or room, which can take up to
3465
+ * answered yet: it is waiting for a slot without credit or room, which can take up to
3258
3466
  * two minutes, and silence reads as a hang. */
3259
3467
  function waiting(out) {
3260
3468
  const said = new Set();
3261
3469
  return (refusal) => {
3470
+ // The CLI's volume wait owns this refusal, including its deadline.
3471
+ if (refusal.code === "volume_releasing")
3472
+ throw refusal;
3262
3473
  if (said.has(refusal.code))
3263
3474
  return;
3264
3475
  said.add(refusal.code);
3265
3476
  out.error(`${refusal.message} Waiting for room, up to 2 minutes; Ctrl-C stops.`);
3266
3477
  };
3267
3478
  }
3479
+ /** A stopping sandbox can take a minute to let go of its volume. Retry only
3480
+ * that refusal, with the same key so a create cannot make two sandboxes. */
3481
+ async function withVolumeRelease(operation, out) {
3482
+ const idempotencyKey = crypto.randomUUID();
3483
+ let until;
3484
+ for (;;) {
3485
+ try {
3486
+ return await operation({
3487
+ idempotencyKey,
3488
+ ...(until === undefined
3489
+ ? {}
3490
+ : { signal: AbortSignal.timeout(Math.max(1, Math.ceil(until - performance.now()))) }),
3491
+ });
3492
+ }
3493
+ catch (error) {
3494
+ if (!(error instanceof RuntimeError) ||
3495
+ error.code !== "volume_releasing" ||
3496
+ error.status !== 409)
3497
+ throw error;
3498
+ if (until === undefined) {
3499
+ until = performance.now() + 90_000;
3500
+ out.error("Waiting for the volume to be let go, up to 90 seconds; Ctrl-C stops.");
3501
+ }
3502
+ const left = until - performance.now();
3503
+ if (left <= 0)
3504
+ throw error;
3505
+ await new Promise((resolve) => setTimeout(resolve, Math.min(1000, left)));
3506
+ if (performance.now() >= until)
3507
+ throw error;
3508
+ }
3509
+ }
3510
+ }
3268
3511
  /** Options `sandbox create` and `run` take with a value. */
3269
3512
  const CREATE_VALUES = [
3270
3513
  "name",
@@ -3305,6 +3548,9 @@ function usdMicros(args, name) {
3305
3548
  }
3306
3549
  /** A create body from the command line's flags. */
3307
3550
  function createInput(args) {
3551
+ for (const name of ["image", "snapshot", "volume", "name", "region", "connect"])
3552
+ if (args.flags.get(name)?.some((value) => !value.trim()))
3553
+ throw usage(`--${name} needs a non-empty value.`);
3308
3554
  const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
3309
3555
  return {
3310
3556
  ...(flag(args, "image") ? { image: flag(args, "image") } : {}),
@@ -3339,7 +3585,7 @@ function createInput(args) {
3339
3585
  ? { timeoutSeconds: integer(args, "timeout") }
3340
3586
  : {}),
3341
3587
  ...(flag(args, "on-timeout")
3342
- ? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
3588
+ ? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop", "delete"]) }
3343
3589
  : {}),
3344
3590
  ...(integer(args, "idle-pause") !== undefined
3345
3591
  ? { idlePauseSeconds: integer(args, "idle-pause") }
@@ -3401,6 +3647,9 @@ ownInterrupt = true) {
3401
3647
  // (Sandbox.exec); `--json` answers the result and prints no such line.
3402
3648
  else if (result.timedOut)
3403
3649
  out.error(`runtime: timed out after ${timeout ? `${timeout} s` : "24 h"}; the command's process group was killed.`);
3650
+ const memory = outOfMemoryLines(result.outOfMemory);
3651
+ if (memory && !out.json)
3652
+ out.error(memory);
3404
3653
  // Never drop output silently: say so, and do not exit 0.
3405
3654
  const lost = extras.lostOutput(result, sbx.id);
3406
3655
  if (lost && !out.json)