withruntime 0.6.0 → 0.7.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 (47) hide show
  1. package/E2B.md +32 -32
  2. package/README.md +8 -5
  3. package/dist/ai/index.d.ts +21 -5
  4. package/dist/ai/index.js +14 -21
  5. package/dist/ai-harness/index.d.ts +194 -0
  6. package/dist/ai-harness/index.js +517 -0
  7. package/dist/billing-cli.d.ts +21 -0
  8. package/dist/billing-cli.js +158 -0
  9. package/dist/cli-extras.d.ts +15 -0
  10. package/dist/cli-extras.js +60 -4
  11. package/dist/cli-name.d.ts +6 -0
  12. package/dist/cli-name.js +8 -0
  13. package/dist/cli.d.ts +17 -1
  14. package/dist/cli.js +537 -130
  15. package/dist/client.d.ts +35 -6
  16. package/dist/client.js +23 -0
  17. package/dist/credentials.js +1 -2
  18. package/dist/daytona/filesystem.d.ts +2 -0
  19. package/dist/daytona/filesystem.js +9 -7
  20. package/dist/e2b/filesystem.js +3 -2
  21. package/dist/e2b/sandbox.d.ts +9 -6
  22. package/dist/e2b/sandbox.js +31 -7
  23. package/dist/errors.d.ts +5 -0
  24. package/dist/errors.js +7 -0
  25. package/dist/index.d.ts +8 -6
  26. package/dist/index.js +1 -0
  27. package/dist/keys.js +1 -1
  28. package/dist/login.d.ts +2 -6
  29. package/dist/login.js +3 -8
  30. package/dist/network-cli.d.ts +1 -1
  31. package/dist/network-cli.js +64 -2
  32. package/dist/products/billing.d.ts +35 -0
  33. package/dist/products/billing.js +13 -0
  34. package/dist/products/index.d.ts +4 -1
  35. package/dist/products/index.js +4 -1
  36. package/dist/products/network-products.d.ts +45 -0
  37. package/dist/products/network-products.js +27 -0
  38. package/dist/products/previews.d.ts +1 -2
  39. package/dist/products/secrets.d.ts +14 -0
  40. package/dist/sandbox.d.ts +17 -3
  41. package/dist/sandbox.js +233 -62
  42. package/dist/transport.d.ts +17 -1
  43. package/dist/transport.js +124 -7
  44. package/dist/types.d.ts +0 -2
  45. package/dist/wallet.d.ts +30 -0
  46. package/dist/wallet.js +48 -0
  47. package/package.json +44 -17
package/dist/cli.js CHANGED
@@ -3,15 +3,14 @@ import { realpathSync } from "node:fs";
3
3
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { connectionStore, resolveCredential } from "./credentials.js";
7
- import { authenticationCommand, me, named } from "./login.js";
8
- import { createKey, dailyLimitMicros } from "./keys.js";
6
+ import { connectionOrigins, connectionStore, resolveCredential } from "./credentials.js";
7
+ import { me, named } from "./cli-name.js";
9
8
  import { Runtime } from "./client.js";
9
+ import { Sandbox as SandboxHandle } from "./sandbox.js";
10
10
  import { RuntimeError } from "./errors.js";
11
11
  import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
12
+ import { billingCommand } from "./billing-cli.js";
12
13
  import { envFetch } from "./proxy.js";
13
- import { identityToken } from "./identity.js";
14
- import { portForwardCommand, resolveSandbox, sshCommand } from "./ssh.js";
15
14
  import { VERSION } from "./transport.js";
16
15
  const HELP = `Runtime Cloud CLI ${VERSION}
17
16
 
@@ -30,22 +29,25 @@ Account
30
29
  account [ls] The accounts this machine is connected to
31
30
  account switch <name|id> Use another of them for every command after
32
31
  account add Connect another account (choose it in the browser)
32
+ account close --confirm "<account name>" Close the account for good (owner, key for every product)
33
33
  audit [--action member.] [--limit 50] [--before <next>]
34
34
  The account's audit log (owner or admin keys)
35
35
  sso Single sign-on and SCIM directory sync (owner or
36
36
  admin keys); an owner changes it on the website
37
- usage Balance, trial time and charges
37
+ usage [--csv] Balance, trial time and charges; --csv, each resource
38
38
  limits Whether this key is read-only, and its daily spending limit
39
39
  secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
40
- Store a secret sandboxes use without seeing it; the
41
- value is read from standard input, never an argument
40
+ [--allow '[GET,HEAD] /path/*']... Store a secret sandboxes use without seeing it; the
41
+ value is read from standard input, never an argument.
42
+ --allow (paid): only these methods and paths get it
42
43
  secrets ls Names, hosts and placeholders, never values
43
44
  secrets rm <NAME> Delete a secret
44
45
  keys create [--name <name>] [--read-only] [--daily-limit <usd>]
45
46
  A new API key for CI, approved by an owner in the browser
46
47
  referrals Your referral link: you both get up to $500
47
48
  compare --from <provider> [--days 30] What your usage would cost at e2b, daytona, vercel,
48
- modal, cloudflare, fly and others, and what you save
49
+ modal, cloudflare, fly, lambda-microvms, freestyle,
50
+ prime and others, and what you save
49
51
  switch [--from <provider>] Moving from a rival: record it before your first top-up,
50
52
  and that top-up is matched, up to $100
51
53
  ls Everything you run, every product
@@ -84,6 +86,7 @@ Products
84
86
  port A public TCP port to a sandbox: databases, game servers
85
87
  address A dedicated outbound address, for allow-lists
86
88
  tunnel A WireGuard tunnel from your own network into your sandboxes
89
+ network Account-wide network settings: send outbound traffic through your own proxy
87
90
  Each has its own help: \`runtime sandbox help\`, \`runtime image help\`...
88
91
 
89
92
  Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
@@ -97,6 +100,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
97
100
  ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
98
101
  port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
99
102
  create [--name n] [--label k=v]... [--vcpu 2] [--memory 4096] [--disk 4096]
103
+ [--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
100
104
  [--timeout <seconds>] [--on-timeout pause|stop] [--trial|--paid] [--no-wait]
101
105
  [--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
102
106
  [--image <name:tag|imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
@@ -137,6 +141,8 @@ const SANDBOX_HELP = `runtime sandbox <command>
137
141
  Start them; prints each URL and the header to send
138
142
  mcp <id> [status] | mcp <id> stop Their state and fresh URLs; stop them
139
143
  preview <id> <port> [--public] [--ttl <s>] Share a port at an HTTPS address
144
+ preview rotate <id> <port> Refuse every token given out for a private port so
145
+ far; prints the new token
140
146
  previews <id> Every shared port
141
147
  unshare <id> <port> Stop sharing a port
142
148
  network <id> [--no-internet|--internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
@@ -157,9 +163,38 @@ Examples
157
163
  runtime sandbox cp ./project $id:/workspace/project
158
164
  runtime sandbox stop $id
159
165
  `;
160
- function parse(argv, booleans = []) {
166
+ /** A command's options. Reading one its parse() did not declare is a bug in
167
+ * this file, and says so, so a test that runs the command finds it. */
168
+ class Flags extends Map {
169
+ known;
170
+ constructor(known) {
171
+ super();
172
+ this.known = known;
173
+ }
174
+ get(name) {
175
+ this.#declared(name);
176
+ return super.get(name);
177
+ }
178
+ has(name) {
179
+ this.#declared(name);
180
+ return super.has(name);
181
+ }
182
+ #declared(name) {
183
+ if (!this.known.has(name))
184
+ throw new Error(`The CLI reads --${name}, which this command's parse() does not declare.`);
185
+ }
186
+ }
187
+ /** A command's arguments: `booleans` are options that take no value, `values`
188
+ * those that take one. Any other option is refused before anything is sent, so
189
+ * `--memory-mib 8192` for `--memory` is a usage error with the right name, not a
190
+ * sandbox created at the default size. Everything after `--` is `rest`. */
191
+ function parse(argv, booleans, values,
192
+ /** For a command that runs one: how to write it, so an option that belongs
193
+ * to the command (`python3 -c ...`) is pointed after the `--`. */
194
+ commandAfter) {
195
+ const known = new Set([...values, ...booleans]);
161
196
  const positional = [];
162
- const flags = new Map();
197
+ const flags = new Flags(known);
163
198
  let rest;
164
199
  for (let i = 0; i < argv.length; i++) {
165
200
  const arg = argv[i];
@@ -169,6 +204,10 @@ function parse(argv, booleans = []) {
169
204
  }
170
205
  if (arg.startsWith("--") || (arg.startsWith("-") && arg.length === 2 && !/^-\d/.test(arg))) {
171
206
  const [name, inline] = arg.replace(/^-+/, "").split(/=(.*)/s, 2);
207
+ if (!known.has(name))
208
+ throw commandAfter && !meantOption(name, known)
209
+ ? usage(`Unknown option ${arg.split("=")[0]}. If it belongs to the command, put the command after --: ${commandAfter}`)
210
+ : unknownOption(name, known);
172
211
  const value = inline ?? (booleans.includes(name) ? "true" : argv[++i]);
173
212
  if (value === undefined)
174
213
  throw usage(`--${name} needs a value.`);
@@ -179,6 +218,22 @@ function parse(argv, booleans = []) {
179
218
  }
180
219
  return { positional, flags, rest };
181
220
  }
221
+ /** The declared option a mistyped one most likely meant. --memory-mib for
222
+ * --memory, --timeout-seconds for --timeout: the SDK's spelling of an option is
223
+ * the CLI's with its unit added. */
224
+ function meantOption(name, known) {
225
+ return ([...known].find((option) => name.startsWith(`${option}-`) || option.startsWith(`${name}-`)) ??
226
+ closest(name, [...known]));
227
+ }
228
+ /** "Unknown option --x", the one that was likely meant, and the ones there are. */
229
+ function unknownOption(name, known) {
230
+ const dash = (option) => (option.length === 1 ? `-${option}` : `--${option}`);
231
+ const meant = meantOption(name, known);
232
+ const options = [...known].filter((option) => option.length > 1).map(dash);
233
+ return usage(`Unknown option ${dash(name)}.${meant ? ` Did you mean ${dash(meant)}?` : ""}${options.length
234
+ ? ` This command takes ${options.join(", ")}.`
235
+ : " This command takes no options."}`);
236
+ }
182
237
  const flag = (args, name) => args.flags.get(name)?.at(-1);
183
238
  const has = (args, name) => args.flags.get(name)?.at(-1) === "true";
184
239
  const integer = (args, name) => {
@@ -189,6 +244,13 @@ const integer = (args, name) => {
189
244
  throw usage(`--${name} must be a whole number.`);
190
245
  return Number(value);
191
246
  };
247
+ /** A flag that takes one of a few words, checked before anything is sent. */
248
+ function oneOf(args, name, choices) {
249
+ const value = flag(args, name);
250
+ if (!choices.includes(value))
251
+ throw usage(`--${name} takes ${choices.join(" or ")}.`);
252
+ return value;
253
+ }
192
254
  const pairs = (args, name) => Object.fromEntries((args.flags.get(name) ?? []).map((pair) => {
193
255
  const at = pair.indexOf("=");
194
256
  if (at < 1)
@@ -197,7 +259,7 @@ const pairs = (args, name) => Object.fromEntries((args.flags.get(name) ?? []).ma
197
259
  }));
198
260
  /** What the commands in cli-extras.ts use of this file. */
199
261
  const kit = {
200
- parse: (argv, booleans) => parse(argv, booleans),
262
+ parse: (argv, booleans, values) => parse(argv, booleans, values),
201
263
  flag,
202
264
  has,
203
265
  integer,
@@ -218,30 +280,37 @@ function usage(message) {
218
280
  }
219
281
  /** "Unknown command X." and, when one is close, the command that was meant. */
220
282
  function unknown(what, word, choices) {
283
+ const meant = closest(word, choices);
284
+ return usage(`Unknown ${what} ${word}.${meant ? ` Did you mean ${meant}?` : ""}`);
285
+ }
286
+ /** The choice a mistyped word most likely meant, or none when nothing is close:
287
+ * one edit away (a swap of two letters counts as one), or two for a word longer
288
+ * than five letters, so "exce" finds exec and "bogus" finds nothing. */
289
+ function closest(word, choices) {
221
290
  const distance = (a, b) => {
222
- const row = Array.from({ length: b.length + 1 }, (_, j) => j);
223
- for (let i = 1; i <= a.length; i++) {
224
- let previous = row[0];
225
- row[0] = i;
291
+ const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
292
+ for (let i = 1; i <= a.length; i++)
226
293
  for (let j = 1; j <= b.length; j++) {
227
- const next = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
228
- previous = row[j];
229
- row[j] = next;
294
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
295
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
296
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
297
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
230
298
  }
231
- }
232
- return row[b.length];
299
+ return d[a.length][b.length];
233
300
  };
234
301
  const best = choices
235
302
  .map((choice) => [choice, distance(word, choice)])
236
303
  .sort((x, y) => x[1] - y[1])[0];
237
- const meant = best && best[1] <= (word.length >= 4 ? 2 : 1) ? best[0] : undefined;
238
- return usage(`Unknown ${what} ${word}.${meant ? ` Did you mean ${meant}?` : ""}`);
304
+ return best && best[1] <= (word.length > 5 ? 2 : 1) ? best[0] : undefined;
239
305
  }
240
306
  const COMMANDS = [
307
+ "account",
308
+ "audit",
241
309
  "login",
242
310
  "logout",
243
311
  "whoami",
244
312
  "usage",
313
+ "billing",
245
314
  "limits",
246
315
  "sso",
247
316
  "secrets",
@@ -262,12 +331,38 @@ const COMMANDS = [
262
331
  "port",
263
332
  "address",
264
333
  "tunnel",
334
+ "network",
265
335
  "webhooks",
266
336
  "otel",
267
337
  "events",
268
338
  "help",
269
339
  "version",
270
340
  ];
341
+ /** Commands with a help of their own: `runtime <command> help`. */
342
+ const OWN_HELP = [
343
+ "sandbox",
344
+ "sandboxes",
345
+ "sbx",
346
+ "snapshot",
347
+ "snapshots",
348
+ "image",
349
+ "images",
350
+ "volume",
351
+ "volumes",
352
+ "keys",
353
+ "webhooks",
354
+ "webhook",
355
+ "billing",
356
+ "domain",
357
+ "domains",
358
+ "port",
359
+ "ports",
360
+ "address",
361
+ "addresses",
362
+ "tunnel",
363
+ "tunnels",
364
+ "network",
365
+ ];
271
366
  /** Sandbox commands that once sat at the top level. */
272
367
  const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
273
368
  const SANDBOX_VERBS = [
@@ -313,6 +408,14 @@ function need(value, what) {
313
408
  throw usage(`Give ${what}.`);
314
409
  return value;
315
410
  }
411
+ /** A port from the command line: a whole number from 1 to 65535, checked
412
+ * before any call so a typo is a usage error, not a 404 from the API. */
413
+ function portOf(value, example) {
414
+ const port = Number(need(value, `a port, e.g. ${example}`));
415
+ if (!Number.isInteger(port) || port < 1 || port > 65_535)
416
+ throw usage(`${value} is not a port: give a whole number from 1 to 65535, e.g. ${example}.`);
417
+ return port;
418
+ }
316
419
  /** The client, with the key from RUNTIME_API_KEY or this machine's saved
317
420
  * connection. With neither, the first command connects the machine first (a
318
421
  * browser approval, nothing to copy) and then does what was asked, so setup
@@ -339,6 +442,7 @@ async function client(env) {
339
442
  env.RUNTIME_NO_LOGIN)
340
443
  throw error;
341
444
  process.stderr.write("This machine is not connected to Runtime yet. Connecting it first.\n");
445
+ const { authenticationCommand } = await import("./login.js");
342
446
  const login = await authenticationCommand(["login"], env);
343
447
  if (login.pending)
344
448
  throw notApprovedYet(login);
@@ -381,7 +485,7 @@ function describe(info) {
381
485
  .map(([k, v]) => `${k}=${v}`)
382
486
  .join(", ") || "-",
383
487
  ],
384
- ["charged", `$${(info.chargedMicros / 1e6).toFixed(6)}`],
488
+ ["charged", dollars(BigInt(Math.round(Number(info.chargedMicros) || 0)))],
385
489
  ]);
386
490
  }
387
491
  /** Resolves `<id>:/path` into its parts, or undefined for a local path. */
@@ -400,6 +504,16 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
400
504
  SANDBOX_MOVED.includes(remaining[0] ?? "");
401
505
  const command = nested ? remaining[0] : first;
402
506
  const rest = nested ? remaining.slice(1) : remaining;
507
+ // `runtime sandbox create --help` is how an agent asks what a command takes:
508
+ // answer with the product's help, never by running the command.
509
+ const cut = remaining.indexOf("--");
510
+ if (first &&
511
+ (cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h"))
512
+ return run(nested && command === "ssh"
513
+ ? [first, "ssh", "help"]
514
+ : OWN_HELP.includes(first)
515
+ ? [first, "help"]
516
+ : ["help"], env, out);
403
517
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
404
518
  product = "";
405
519
  if (!command) {
@@ -425,14 +539,14 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
425
539
  return 0;
426
540
  }
427
541
  if (command === "run") {
428
- const args = parse(rest, [...CREATE_FLAGS, "keep"]);
542
+ const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandbox run -- python3 -c 'print(6*7)'`);
429
543
  const commandLine = args.rest ?? args.positional;
430
544
  if (!commandLine.length)
431
545
  throw usage(`Give the command after --: ${me} sandbox run -- python3 -c 'print(6*7)'`);
432
546
  const runtime = await client(env);
433
547
  const timeout = integer(args, "timeout");
434
548
  const { timeoutSeconds: _lease, ...input } = createInput(args);
435
- const sbx = await runtime.sandboxes.create(input);
549
+ const sbx = await runtime.sandboxes.create(input, { onCapacityWait: waiting(out) });
436
550
  let cleanup;
437
551
  // SIGINT and normal completion share one stop. Keep an absolute deadline
438
552
  // across the stop retries and final state check, so cleanup cannot hang.
@@ -462,7 +576,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
462
576
  process.once("SIGINT", interrupted);
463
577
  let code = 1;
464
578
  try {
465
- code = await execute(sbx, commandLine, args, out, timeout);
579
+ code = await execute(sbx, commandLine, args, out, timeout, has(args, "keep"));
466
580
  }
467
581
  finally {
468
582
  try {
@@ -482,6 +596,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
482
596
  return 0;
483
597
  }
484
598
  if (command === "login" || command === "logout") {
599
+ const { authenticationCommand } = await import("./login.js");
485
600
  const result = await authenticationCommand([command, ...rest], env);
486
601
  print(result.pending
487
602
  ? `Not connected yet. When it is approved, run any command, or \`${me} login --wait\`; it uses this link.`
@@ -491,9 +606,26 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
491
606
  return 0;
492
607
  }
493
608
  if (command === "whoami") {
494
- const who = await (await client(env)).me();
609
+ const rt = await client(env);
610
+ // The usage call only adds the funding line: a key that cannot read it
611
+ // still gets its answer.
612
+ const [who, spend] = await Promise.all([rt.me(), rt.usage().catch(() => undefined)]);
613
+ const trialLeft = spend?.trial ? spend.trial.availableMs / 3_600_000 : 0;
614
+ const credit = spend ? BigInt(spend.available) : 0n;
615
+ const funding = !spend
616
+ ? undefined
617
+ : [
618
+ ...(trialLeft > 0
619
+ ? [
620
+ `free trial, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left`,
621
+ ]
622
+ : []),
623
+ ...(credit > 0n ? [`${dollars(credit)} of credit`] : []),
624
+ ].join("; ") || "no trial time or credit left: runtime billing";
495
625
  print(table([
496
- ["organization", who.orgId],
626
+ ["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
627
+ ...(who.role ? [["role", who.role]] : []),
628
+ ...(funding ? [["funding", funding]] : []),
497
629
  ["agent", who.principalId],
498
630
  ["key", who.credentialId ?? "-"],
499
631
  ["api", who.apiVersion],
@@ -501,7 +633,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
501
633
  return 0;
502
634
  }
503
635
  if (command === "usage") {
636
+ const csv = rest.includes("--csv");
637
+ const unknown = rest.find((arg) => arg !== "--csv");
638
+ if (unknown)
639
+ throw usage(`Unknown option ${unknown}: ${me} usage [--csv | --json]`);
504
640
  const value = await (await client(env)).usage();
641
+ if (csv) {
642
+ out.write(usageCsv(value));
643
+ if ((value.resources ?? []).length >= 100)
644
+ out.error("These are the newest 100 resources; older ones are not in this export.");
645
+ return 0;
646
+ }
505
647
  print(usageSummary(value), value);
506
648
  return 0;
507
649
  }
@@ -533,7 +675,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
533
675
  if (command === "otel")
534
676
  return otelCommand(rest, env, out);
535
677
  if (command === "events") {
536
- const args = parse(rest);
678
+ const args = parse(rest, [], ["sandbox", "type", "limit"]);
537
679
  const rt = await client(env);
538
680
  const sandbox = flag(args, "sandbox");
539
681
  const page = await rt.events.list({
@@ -577,7 +719,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
577
719
  if (command === "account" || command === "accounts")
578
720
  return accountCommand(rest, env, out);
579
721
  if (command === "audit") {
580
- const args = parse(rest);
722
+ const args = parse(rest, [], ["action", "limit", "before"]);
581
723
  const known = new Set(["action", "limit", "before"]);
582
724
  for (const name of args.flags.keys())
583
725
  if (!known.has(name))
@@ -605,6 +747,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
605
747
  : "Nothing recorded yet.", page);
606
748
  return 0;
607
749
  }
750
+ if (command === "billing") {
751
+ product = "billing";
752
+ const origins = connectionOrigins(env);
753
+ const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
754
+ return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
755
+ open: (input) => openWalletAccount(input, { authUrl: origins.auth }),
756
+ claim: (code) => claimWalletAccount(code, { authUrl: origins.auth }),
757
+ save: (key, orgId) => connectionStore(env).save({
758
+ version: 1,
759
+ apiOrigin: origins.api,
760
+ authOrigin: origins.auth,
761
+ key,
762
+ connectionId: "pasted",
763
+ orgId,
764
+ agentName: "Wallet agent",
765
+ }),
766
+ });
767
+ }
608
768
  if (command === "referrals") {
609
769
  const r = await (await client(env)).referrals.get();
610
770
  const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
@@ -646,7 +806,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
646
806
  return 0;
647
807
  }
648
808
  if (command === "feedback") {
649
- const args = parse(rest, ["list"]);
809
+ const args = parse(rest, ["list"], ["kind", "detail", "competitor", "request-id"]);
650
810
  if (has(args, "list")) {
651
811
  const { data } = await (await client(env)).feedback.list();
652
812
  print(data.length
@@ -707,11 +867,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
707
867
  }
708
868
  if (command === "ssh") {
709
869
  product = "ssh";
710
- return sshCommand(rest, env, out, () => client(env), me);
870
+ return (await import("./ssh.js")).sshCommand(rest, env, out, () => client(env), me);
711
871
  }
712
872
  if (command === "port-forward" || command === "forward") {
713
873
  product = "ssh";
714
- return portForwardCommand(rest, out, () => client(env), me);
874
+ return (await import("./ssh.js")).portForwardCommand(rest, out, () => client(env), me);
715
875
  }
716
876
  if (command === "sandbox" || command === "sandboxes" || command === "sbx") {
717
877
  product = "sandbox";
@@ -733,13 +893,62 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
733
893
  command;
734
894
  if (NETWORK_PRODUCTS.includes(networkProduct)) {
735
895
  product = networkProduct;
736
- return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"]), out, () => client(env));
896
+ return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"], ["sandbox", "subnet", "public-key", "route", "out", "host", "secret"]), out, () => client(env));
737
897
  }
738
898
  throw unknown("command", command, COMMANDS);
739
899
  }
740
900
  /** `runtime usage` for a person: the balance, the trial and what each kind
741
901
  * of resource was charged, in dollars. --json keeps every figure, and each
742
902
  * resource with its rates and CPU time. */
903
+ /** One row per resource, for a spreadsheet: when it ran, how big it was, how
904
+ * long it ran, the CPU it used and what it cost. Money is exact dollars to the
905
+ * microdollar (the API's integer microdollars moved six places), so a column
906
+ * sums to the ledger to the last digit. */
907
+ export function usageCsv(u) {
908
+ const cell = (value) => {
909
+ const text = typeof value === "string" || typeof value === "number" || typeof value === "bigint"
910
+ ? String(value)
911
+ : "";
912
+ return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
913
+ };
914
+ const exact = (value) => {
915
+ const micros = BigInt(typeof value === "number" || typeof value === "string" ? value : 0);
916
+ const sign = micros < 0n ? "-" : "";
917
+ const size = micros < 0n ? -micros : micros;
918
+ return `${sign}${size / 1000000n}.${(size % 1000000n).toString().padStart(6, "0")}`;
919
+ };
920
+ const head = [
921
+ "resource_id",
922
+ "name",
923
+ "kind",
924
+ "state",
925
+ "created_at",
926
+ "vcpu",
927
+ "memory_mib",
928
+ "running_seconds",
929
+ "active_cpu_seconds",
930
+ "memory_gib_seconds",
931
+ "charged_usd",
932
+ "held_usd",
933
+ ];
934
+ const rows = (u.resources ?? []).map((r) => [
935
+ r.resourceId,
936
+ r.name,
937
+ r.kind,
938
+ r.state ?? r.status,
939
+ r.createdAt,
940
+ r.vcpu,
941
+ r.memoryMiB,
942
+ r.runningSeconds,
943
+ r.activeCpuSeconds,
944
+ r.memoryGiBSeconds,
945
+ exact(r.chargedMicros),
946
+ exact(r.heldMicros),
947
+ ]
948
+ .map(cell)
949
+ .join(","));
950
+ return [head.join(","), ...rows].join("\n");
951
+ }
743
952
  function usageSummary(u) {
744
953
  const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
745
954
  ? BigInt(value)
@@ -749,11 +958,14 @@ function usageSummary(u) {
749
958
  const rows = [
750
959
  ["available", dollars(micros(u.available))],
751
960
  ["credited", dollars(micros(u.credited))],
752
- [
753
- "spent",
754
- `${dollars(micros(u.spent))}${takenBack > 0n ? `, ${dollars(takenBack)} of it refunded or disputed` : ""}`,
755
- ],
756
- ["held for running work", dollars(micros(u.held))],
961
+ // spent includes what refunds and disputes took back: say that part on
962
+ // its own line, so "spent" reads as what was used.
963
+ ["used", dollars(micros(u.spent) - takenBack)],
964
+ ...(takenBack > 0n ? [["returned by refunds and disputes", dollars(takenBack)]] : []),
965
+ // Holds cover running sandboxes and the hour ahead of stored snapshots,
966
+ // images and volumes (pricing guide), so an account with nothing running
967
+ // can still show one.
968
+ ["held for running sandboxes and this hour's storage", dollars(micros(u.held))],
757
969
  ];
758
970
  if (micros(u.expired) > 0n)
759
971
  rows.push(["expired or taken back", dollars(micros(u.expired))]);
@@ -788,13 +1000,15 @@ function usageSummary(u) {
788
1000
  ])
789
1001
  : "No resources yet.",
790
1002
  "",
791
- `Each resource, with its rates and CPU time: ${me} usage --json`,
1003
+ `Each resource, with its rates and CPU time: ${me} usage --json, or --csv for a spreadsheet`,
792
1004
  ].join("\n");
793
1005
  }
794
1006
  const SANDBOX_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
795
1007
  /** A sandbox's id from its id or its name, for the filters that take an id. */
796
1008
  async function sandboxIdOf(runtime, target) {
797
- return SANDBOX_UUID.test(target) ? target : (await resolveSandbox(runtime, target)).id;
1009
+ return SANDBOX_UUID.test(target)
1010
+ ? target
1011
+ : (await (await import("./ssh.js")).resolveSandbox(runtime, target)).id;
798
1012
  }
799
1013
  /** What a process has printed so far, in order, without waiting for more. */
800
1014
  async function outputSoFar(runtime, sandboxId, processId) {
@@ -900,7 +1114,7 @@ function trialNote(trial, all) {
900
1114
  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.`;
901
1115
  }
902
1116
  async function switching(command, argv, env, out) {
903
- const args = parse(argv);
1117
+ const args = parse(argv, [], command === "compare" ? ["from", "days"] : ["from"]);
904
1118
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
905
1119
  const known = new Set(command === "compare" ? ["from", "days"] : ["from"]);
906
1120
  for (const name of args.flags.keys())
@@ -927,7 +1141,7 @@ async function switching(command, argv, env, out) {
927
1141
  return 0;
928
1142
  }
929
1143
  if (!from)
930
- throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines...).`);
1144
+ throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines, lambda-microvms, freestyle, prime...).`);
931
1145
  /* The server holds the list of rivals it has rates for, and names them all
932
1146
  when it refuses one, so a rival added there needs no new CLI. */
933
1147
  const days = integer(args, "days");
@@ -942,7 +1156,7 @@ async function switching(command, argv, env, out) {
942
1156
  const monthly = month === undefined
943
1157
  ? ""
944
1158
  : month >= 0n
945
- ? `about ${dollars(month)} a month`
1159
+ ? `a saving of about ${dollars(month)} a month`
946
1160
  : `about ${dollars(-month)} a month more on Runtime`;
947
1161
  /* The month is projected from the days the usage covers, not from the
948
1162
  window asked for: say which, or a young account's 7 and 90 days read the
@@ -951,7 +1165,7 @@ async function switching(command, argv, env, out) {
951
1165
  const pace = month === undefined
952
1166
  ? "."
953
1167
  : covered < c.window.days
954
- ? `. Your first sandbox in this window ran ${covered <= 1 ? "within the last day" : `${covered.toLocaleString("en-US")} days ago`}; at that pace, ${monthly}.`
1168
+ ? `. Your usage so far covers ${covered <= 1 ? "less than a day" : `${covered.toLocaleString("en-US")} days`}; at that pace, ${monthly}.`
955
1169
  : c.window.days === 30
956
1170
  ? `, ${monthly}.`
957
1171
  : `. At that pace, ${monthly}.`;
@@ -972,7 +1186,7 @@ async function switching(command, argv, env, out) {
972
1186
  return 0;
973
1187
  }
974
1188
  async function support(argv, env, out) {
975
- const args = parse(argv);
1189
+ const args = parse(argv, [], ["conversation"]);
976
1190
  const api = (await client(env)).support;
977
1191
  const conversationId = flag(args, "conversation");
978
1192
  const [first, second, third] = args.positional;
@@ -1010,9 +1224,10 @@ async function sandbox(argv, env, out) {
1010
1224
  }
1011
1225
  // Inside a sandbox, with no API key: the sandbox's own request token asks.
1012
1226
  if (verb === "identity-token") {
1013
- const args = parse(rest);
1227
+ const args = parse(rest, [], ["audience", "lifetime"]);
1014
1228
  const audience = need(flag(args, "audience"), "--audience, such as sts.amazonaws.com");
1015
1229
  const lifetime = flag(args, "lifetime");
1230
+ const { identityToken } = await import("./identity.js");
1016
1231
  const token = await identityToken({
1017
1232
  audience,
1018
1233
  ...(lifetime === undefined ? {} : { lifetimeSeconds: Number(lifetime) }),
@@ -1023,17 +1238,26 @@ async function sandbox(argv, env, out) {
1023
1238
  const runtime = await client(env);
1024
1239
  /* By id or by name, for every command here, as ssh and port-forward
1025
1240
  always took one. */
1026
- const get = (id) => resolveSandbox(runtime, need(id, "a sandbox id or name"));
1241
+ const get = async (id) => (await import("./ssh.js")).resolveSandbox(runtime, need(id, "a sandbox id or name"));
1242
+ /* For a command that needs only the id: an id is used as it is, with no
1243
+ lookup first, so `exec` is one request and not two (about 110 ms less
1244
+ from Colorado to Virginia). A sandbox that is gone or stopped is refused
1245
+ by the command itself, with the same words. */
1246
+ const use = async (id) => SANDBOX_UUID.test(id ?? "")
1247
+ ? new SandboxHandle(runtime.transport, { id })
1248
+ : get(id);
1027
1249
  switch (verb) {
1028
1250
  case "create": {
1029
- const args = parse(rest, CREATE_FLAGS);
1030
- const sbx = await runtime.sandboxes.create(createInput(args));
1251
+ const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
1252
+ const sbx = await runtime.sandboxes.create(createInput(args), {
1253
+ onCapacityWait: waiting(out),
1254
+ });
1031
1255
  print(sbx.id, sbx.info);
1032
1256
  return 0;
1033
1257
  }
1034
1258
  case "ls":
1035
1259
  case "list": {
1036
- const args = parse(rest, ["all"]);
1260
+ const args = parse(rest, ["all"], ["label", "state"]);
1037
1261
  const page = await runtime.sandboxes.list({
1038
1262
  ...(has(args, "all") ? { includeStopped: true } : {}),
1039
1263
  ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
@@ -1085,8 +1309,8 @@ async function sandbox(argv, env, out) {
1085
1309
  return 0;
1086
1310
  }
1087
1311
  case "mount": {
1088
- const args = parse(rest, ["read-only"]);
1089
- const sbx = await get(args.positional[0]);
1312
+ const args = parse(rest, ["read-only"], ["secret", "region", "endpoint", "account-id"]);
1313
+ const sbx = await use(args.positional[0]);
1090
1314
  const source = need(args.positional[1], "a bucket, as s3://bucket, r2://bucket or gcs://bucket");
1091
1315
  const found = /^(s3|r2|gcs):\/\/([^/]+)(?:\/(.+))?$/.exec(source);
1092
1316
  if (!found)
@@ -1106,7 +1330,7 @@ async function sandbox(argv, env, out) {
1106
1330
  return 0;
1107
1331
  }
1108
1332
  case "mounts": {
1109
- const sbx = await get(rest[0]);
1333
+ const sbx = await use(rest[0]);
1110
1334
  const all = await sbx.mounts.list();
1111
1335
  print(all.length
1112
1336
  ? table([
@@ -1123,13 +1347,13 @@ async function sandbox(argv, env, out) {
1123
1347
  return 0;
1124
1348
  }
1125
1349
  case "unmount": {
1126
- const sbx = await get(rest[0]);
1350
+ const sbx = await use(rest[0]);
1127
1351
  const gone = await sbx.mounts.remove(need(rest[1], "the mounted path"));
1128
1352
  print(`${gone.path} unmounted`, gone);
1129
1353
  return 0;
1130
1354
  }
1131
1355
  case "snapshot": {
1132
- const args = parse(rest);
1356
+ const args = parse(rest, [], ["name", "retention"]);
1133
1357
  const sbx = await get(args.positional[0]);
1134
1358
  const snapshot = await sbx.snapshot({
1135
1359
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
@@ -1139,7 +1363,7 @@ async function sandbox(argv, env, out) {
1139
1363
  return 0;
1140
1364
  }
1141
1365
  case "fork": {
1142
- const args = parse(rest, ["keep-snapshot", "trial", "paid"]);
1366
+ const args = parse(rest, ["keep-snapshot", "trial", "paid"], ["count", "name"]);
1143
1367
  const sbx = await get(args.positional[0]);
1144
1368
  const forks = await sbx.fork({
1145
1369
  count: integer(args, "count") ?? 1,
@@ -1155,7 +1379,7 @@ async function sandbox(argv, env, out) {
1155
1379
  return 0;
1156
1380
  }
1157
1381
  case "update": {
1158
- const args = parse(rest);
1382
+ const args = parse(rest, [], ["name", "label", "idle-pause", "auto-wake", "persistent", "max-total-cost"]);
1159
1383
  const sbx = await get(args.positional[0]);
1160
1384
  const onOff = (name) => {
1161
1385
  const value = flag(args, name);
@@ -1196,19 +1420,19 @@ async function sandbox(argv, env, out) {
1196
1420
  return 0;
1197
1421
  }
1198
1422
  case "exec": {
1199
- const args = parse(rest);
1423
+ const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandbox exec <id> -- ls -la`);
1200
1424
  const command = args.rest ?? args.positional.slice(1);
1201
1425
  if (!command.length)
1202
1426
  throw usage("Give the command after --: runtime sandbox exec <id> -- ls -la");
1203
- const sbx = await get(args.positional[0]);
1427
+ const sbx = await use(args.positional[0]);
1204
1428
  return execute(sbx, command, args, out, integer(args, "timeout"));
1205
1429
  }
1206
1430
  case "spawn": {
1207
- const args = parse(rest, ["pty"]);
1431
+ const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandbox spawn <id> -- npm run dev`);
1208
1432
  const command = args.rest ?? [];
1209
1433
  if (!command.length)
1210
1434
  throw usage("Give the command after --: runtime sandbox spawn <id> -- npm run dev");
1211
- const sbx = await get(args.positional[0]);
1435
+ const sbx = await use(args.positional[0]);
1212
1436
  const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
1213
1437
  ...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
1214
1438
  ...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
@@ -1218,7 +1442,7 @@ async function sandbox(argv, env, out) {
1218
1442
  return 0;
1219
1443
  }
1220
1444
  case "ps": {
1221
- const sbx = await get(rest[0]);
1445
+ const sbx = await use(rest[0]);
1222
1446
  const list = await sbx.processes.list();
1223
1447
  print(list.length
1224
1448
  ? table([
@@ -1229,8 +1453,8 @@ async function sandbox(argv, env, out) {
1229
1453
  return 0;
1230
1454
  }
1231
1455
  case "logs": {
1232
- const args = parse(rest, ["f", "follow"]);
1233
- const sbx = await get(args.positional[0]);
1456
+ const args = parse(rest, ["f", "follow"], []);
1457
+ const sbx = await use(args.positional[0]);
1234
1458
  const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
1235
1459
  if (has(args, "f") || has(args, "follow")) {
1236
1460
  for await (const event of proc.output())
@@ -1256,26 +1480,28 @@ async function sandbox(argv, env, out) {
1256
1480
  return 0;
1257
1481
  }
1258
1482
  case "kill": {
1259
- const args = parse(rest);
1260
- const sbx = await get(args.positional[0]);
1483
+ const args = parse(rest, [], ["signal"]);
1484
+ const sbx = await use(args.positional[0]);
1261
1485
  const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
1262
1486
  await proc.kill((flag(args, "signal") ?? "SIGTERM"));
1263
1487
  print(`Signalled ${proc.id}.`, { processId: proc.id });
1264
1488
  return 0;
1265
1489
  }
1266
1490
  case "shell": {
1267
- const args = parse(rest);
1268
- const sbx = await get(args.positional[0]);
1491
+ const args = parse(rest, [], ["command"]);
1492
+ const sbx = await use(args.positional[0]);
1269
1493
  return shell(sbx, flag(args, "command"));
1270
1494
  }
1271
1495
  case "cat": {
1272
- const sbx = await get(rest[0]);
1273
- process.stdout.write(await sbx.files.read(need(rest[1], "a path")));
1496
+ const sbx = await use(rest[0]);
1497
+ // Streamed, any size; a stream that ends short throws, so exit is 1.
1498
+ const { Writable } = await import("node:stream");
1499
+ await (await sbx.files.readStream(need(rest[1], "a path"))).pipeTo(Writable.toWeb(process.stdout), { preventClose: true, preventAbort: true });
1274
1500
  return 0;
1275
1501
  }
1276
1502
  case "files": {
1277
- const args = parse(rest);
1278
- const sbx = await get(args.positional[0]);
1503
+ const args = parse(rest, [], ["depth", "glob"]);
1504
+ const sbx = await use(args.positional[0]);
1279
1505
  const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
1280
1506
  ...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
1281
1507
  ...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
@@ -1286,36 +1512,51 @@ async function sandbox(argv, env, out) {
1286
1512
  return 0;
1287
1513
  }
1288
1514
  case "cp": {
1289
- const [from, to] = parse(rest).positional;
1515
+ const [from, to] = parse(rest, ["r", "recursive"], []).positional;
1290
1516
  const source = remote(need(from, "a source"));
1291
1517
  const target = remote(need(to, "a destination"));
1292
- if (source && !target)
1293
- await (await get(source.id)).files.download(source.path, to);
1518
+ // A destination ending in / (or a local directory) takes the source's
1519
+ // own name inside it, as cp does.
1520
+ const inside = (dir, path) => `${dir.replace(/\/+$/, "")}/${path.replace(/\/+$/, "").split("/").pop()}`;
1521
+ if (source && !target) {
1522
+ const local = await import("node:fs/promises").then((fs) => fs.stat(to).catch(() => null));
1523
+ await (await use(source.id)).files.download(source.path, to.endsWith("/") || local?.isDirectory() ? inside(to, source.path) : to);
1524
+ }
1294
1525
  else if (!source && target)
1295
- await (await get(target.id)).files.upload(from, target.path);
1526
+ await (await use(target.id)).files.upload(from, target.path.endsWith("/") ? inside(target.path, from) : target.path);
1296
1527
  else
1297
1528
  throw usage("Name exactly one side as <sandbox-id>:/path.");
1298
1529
  print(`Copied ${from} to ${to}.`, { from, to });
1299
1530
  return 0;
1300
1531
  }
1301
1532
  case "run-code":
1302
- return runCode(await get(rest[0]), rest.slice(1), out);
1533
+ return runCode(await use(rest[0]), rest.slice(1), out);
1303
1534
  case "preview": {
1304
- const args = parse(rest, ["public"]);
1305
- const sbx = await get(args.positional[0]);
1306
- const port = Number(need(args.positional[1], "a port, e.g. runtime sandbox preview <id> 3000"));
1535
+ if (rest[0] === "rotate") {
1536
+ const port = portOf(rest[2], "runtime sandbox preview rotate <id> 3000");
1537
+ const sbx = await use(rest[1]);
1538
+ const preview = await sbx.previews.rotate(port);
1539
+ print(preview.visibility === "public"
1540
+ ? `Port ${port} is public: it takes no token, so there was none to refuse.`
1541
+ : `${preview.urlWithToken ?? preview.url}\nEvery earlier token for port ${port} is refused. New header: x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`, preview);
1542
+ return 0;
1543
+ }
1544
+ const args = parse(rest, ["public"], ["ttl"]);
1545
+ const port = portOf(args.positional[1], "runtime sandbox preview <id> 3000");
1546
+ const sbx = await use(args.positional[0]);
1307
1547
  const preview = await sbx.previews.create(port, {
1308
1548
  ...(has(args, "public") ? { visibility: "public" } : {}),
1309
1549
  ...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
1310
1550
  });
1311
- print((preview.visibility === "public"
1551
+ // The link and how to use it, nothing more: the API's `hint` (which
1552
+ // carries a referral line) stays in --json, not in the command's words.
1553
+ print(preview.visibility === "public"
1312
1554
  ? preview.url
1313
- : `${preview.urlWithToken ?? preview.url}\nPrivate: send the header x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`) +
1314
- (preview.hint ? `\n${preview.hint}` : ""), preview);
1555
+ : `${preview.urlWithToken ?? preview.url}\nPrivate: send the header x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`, preview);
1315
1556
  return 0;
1316
1557
  }
1317
1558
  case "previews": {
1318
- const list = await (await get(rest[0])).previews.list();
1559
+ const list = await (await use(rest[0])).previews.list();
1319
1560
  print(list.length
1320
1561
  ? table([
1321
1562
  ["PORT", "VISIBILITY", "URL"],
@@ -1325,14 +1566,14 @@ async function sandbox(argv, env, out) {
1325
1566
  return 0;
1326
1567
  }
1327
1568
  case "unshare": {
1328
- const sbx = await get(rest[0]);
1329
- const port = Number(need(rest[1], "the port"));
1569
+ const port = portOf(rest[1], "runtime sandbox unshare <id> 3000");
1570
+ const sbx = await use(rest[0]);
1330
1571
  print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
1331
1572
  return 0;
1332
1573
  }
1333
1574
  case "network": {
1334
- const args = parse(rest, ["no-internet", "internet"]);
1335
- const sbx = await get(args.positional[0]);
1575
+ const args = parse(rest, ["no-internet", "internet"], ["allow", "deny", "connect"]);
1576
+ const sbx = await use(args.positional[0]);
1336
1577
  const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
1337
1578
  const policy = has(args, "no-internet") || has(args, "internet") || rules.length
1338
1579
  ? await sbx.network.set({
@@ -1342,7 +1583,10 @@ async function sandbox(argv, env, out) {
1342
1583
  : await sbx.network.get();
1343
1584
  print(table([
1344
1585
  ["internet", policy.internet ? "on" : "off"],
1345
- ["allow", policy.allow.join(", ") || "(the public web)"],
1586
+ [
1587
+ "allow",
1588
+ policy.allow.join(", ") || (policy.internet ? "(the public web)" : "(nothing)"),
1589
+ ],
1346
1590
  ["deny", policy.deny.join(", ") || "-"],
1347
1591
  ["connect", policy.connect.join(", ") || "-"],
1348
1592
  ["enforced", policy.enforced ? "yes" : "not yet; run the same command again"],
@@ -1350,9 +1594,9 @@ async function sandbox(argv, env, out) {
1350
1594
  return 0;
1351
1595
  }
1352
1596
  case "desktop":
1353
- return desktop(await get(rest[0]), rest.slice(1), out);
1597
+ return desktop(await use(rest[0]), rest.slice(1), out);
1354
1598
  case "watch":
1355
- return (await import("./cli-extras.js")).watchCommand(await get(rest[0]), rest.slice(1), out, kit);
1599
+ return (await import("./cli-extras.js")).watchCommand(await use(rest[0]), rest.slice(1), out, kit);
1356
1600
  case "mcp":
1357
1601
  if (rest[0] === "catalog" || rest[0] === undefined) {
1358
1602
  const catalog = await runtime.mcp.catalog();
@@ -1371,10 +1615,10 @@ async function sandbox(argv, env, out) {
1371
1615
  ]), catalog);
1372
1616
  return 0;
1373
1617
  }
1374
- return (await import("./cli-extras.js")).mcpCommand(await get(rest[0]), rest.slice(1), out, kit);
1618
+ return (await import("./cli-extras.js")).mcpCommand(await use(rest[0]), rest.slice(1), out, kit);
1375
1619
  case "metrics": {
1376
- const args = parse(rest);
1377
- const sbx = await get(args.positional[0]);
1620
+ const args = parse(rest, [], ["range"]);
1621
+ const sbx = await use(args.positional[0]);
1378
1622
  const range = (flag(args, "range") ?? "1h");
1379
1623
  if (!["15m", "1h", "6h", "24h", "7d", "30d"].includes(range))
1380
1624
  throw usage("--range is one of 15m, 1h, 6h, 24h, 7d, 30d.");
@@ -1436,20 +1680,49 @@ async function readSecret() {
1436
1680
  stdin.pause();
1437
1681
  }
1438
1682
  }
1683
+ /** `--allow` values as secret rules: each is `[METHOD[,METHOD]...] PATH`, and
1684
+ * rules naming the same methods share one rule's paths. Only the shape is read
1685
+ * here; the API holds every rule to the same check the host makes
1686
+ * (ARCHITECTURE.md section 10, "Per-path rules on an egress secret") and says
1687
+ * which one it refused. */
1688
+ export function parseSecretRules(values) {
1689
+ const byMethods = new Map();
1690
+ for (const value of values) {
1691
+ const words = value.trim().split(/\s+/);
1692
+ const path = words.at(-1);
1693
+ if (!path || words.length > 2 || !path.startsWith("/"))
1694
+ throw usage(`--allow takes optional methods and one path, such as --allow "GET,HEAD /repos/acme/*" or --allow /v1/chat/completions; got ${JSON.stringify(value)}.`);
1695
+ const methods = words.length === 2
1696
+ ? [...new Set(words[0].toUpperCase().split(",").filter(Boolean))].sort()
1697
+ : undefined;
1698
+ const key = methods?.join(",") ?? "";
1699
+ const rule = byMethods.get(key) ?? { ...(methods ? { methods } : {}), paths: [] };
1700
+ if (!rule.paths.includes(path))
1701
+ rule.paths.push(path);
1702
+ byMethods.set(key, rule);
1703
+ }
1704
+ return [...byMethods.values()];
1705
+ }
1706
+ const ruleText = (rules) => rules?.length
1707
+ ? rules
1708
+ .map((rule) => `${rule.methods ? `${rule.methods.join(",")} ` : ""}${rule.paths.join(" ")}`)
1709
+ .join("; ")
1710
+ : "-";
1439
1711
  export async function secretsCommand(argv, env, out,
1440
1712
  /** Tests give the value here; the CLI reads standard input. */
1441
1713
  read = readSecret) {
1442
1714
  const [verb, ...rest] = argv;
1443
- const args = parse(rest);
1715
+ const args = parse(rest, [], ["host", "header", "format", "allow", "value"]);
1444
1716
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
1445
1717
  const rows = (list) => list.length
1446
1718
  ? table([
1447
- ["name", "hosts", "header", "placeholder"],
1719
+ ["name", "hosts", "header", "placeholder", "allow"],
1448
1720
  ...list.map((secret) => [
1449
1721
  secret.name,
1450
1722
  secret.hosts.join(","),
1451
1723
  secret.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
1452
1724
  secret.placeholder,
1725
+ ruleText(secret.rules),
1453
1726
  ]),
1454
1727
  ])
1455
1728
  : "No secrets.";
@@ -1470,17 +1743,21 @@ read = readSecret) {
1470
1743
  throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
1471
1744
  const header = flag(args, "header");
1472
1745
  const format = flag(args, "format");
1746
+ const allow = args.flags.get("allow");
1747
+ const rules = allow ? parseSecretRules(allow) : undefined;
1473
1748
  const saved = await (await client(env)).secrets.set(name, {
1474
1749
  value,
1475
1750
  hosts,
1476
1751
  ...(header ? { header } : {}),
1477
1752
  ...(format ? { format } : {}),
1753
+ ...(rules ? { rules } : {}),
1478
1754
  });
1479
1755
  print([
1480
1756
  `Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
1481
1757
  saved.header
1482
1758
  ? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
1483
1759
  : `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the URL and headers.`,
1760
+ ...(saved.rules?.length ? [`Only these requests get it: ${ruleText(saved.rules)}.`] : []),
1484
1761
  ...(saved.enforced
1485
1762
  ? []
1486
1763
  : ["A host is catching up; running sandboxes have it within a minute."]),
@@ -1504,7 +1781,7 @@ async function readInput(source) {
1504
1781
  return Buffer.concat(chunks).toString("utf8");
1505
1782
  }
1506
1783
  async function runCode(sbx, argv, out) {
1507
- const args = parse(argv);
1784
+ const args = parse(argv, [], ["lang", "context", "timeout", "out-dir"]);
1508
1785
  const source = need(args.positional[0], "a file, or - to read the code from standard input");
1509
1786
  const code = await readInput(source);
1510
1787
  const byExtension = {
@@ -1572,7 +1849,7 @@ async function desktop(sbx, argv, out) {
1572
1849
  const d = sbx.desktop;
1573
1850
  switch (action) {
1574
1851
  case "start": {
1575
- const args = parse(rest);
1852
+ const args = parse(rest, [], ["width", "height"]);
1576
1853
  const started = await d.start({
1577
1854
  ...(integer(args, "width") ? { width: integer(args, "width") } : {}),
1578
1855
  ...(integer(args, "height") ? { height: integer(args, "height") } : {}),
@@ -1638,6 +1915,7 @@ async function accountCommand(argv, env, out) {
1638
1915
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
1639
1916
  const store = connectionStore(env);
1640
1917
  if (verb === "add") {
1918
+ const { authenticationCommand } = await import("./login.js");
1641
1919
  const result = await authenticationCommand(["login", "--another-account", ...rest], env);
1642
1920
  if (result.pending)
1643
1921
  throw notApprovedYet(result);
@@ -1660,8 +1938,35 @@ async function accountCommand(argv, env, out) {
1660
1938
  });
1661
1939
  return 0;
1662
1940
  }
1941
+ if (verb === "close") {
1942
+ /* ARCHITECTURE.md section 3.1: an owner's key for every product, and the
1943
+ account's name typed exactly. Nothing is asked twice: --confirm is the
1944
+ confirmation, as the website's typed name is. */
1945
+ const args = parse(rest, [], ["confirm"]);
1946
+ for (const name of args.flags.keys())
1947
+ if (name !== "confirm")
1948
+ throw usage(`Unknown option --${name}.`);
1949
+ const confirm = flag(args, "confirm");
1950
+ const runtime = await client(env);
1951
+ if (!confirm) {
1952
+ const who = await runtime.me();
1953
+ throw usage(`Closing stops and deletes everything in the account, ends every key and cannot be undone. To go ahead, type its name: ${me} account close --confirm "${who.orgName ?? "<account name>"}".`);
1954
+ }
1955
+ const closed = await runtime.account.close({ confirm });
1956
+ // This machine's key died with the account.
1957
+ if (!env.RUNTIME_API_KEY)
1958
+ await store.remove().catch(() => undefined);
1959
+ print([
1960
+ `Closed ${closed.name}. Every key has stopped working.`,
1961
+ closed.released
1962
+ ? "Nothing was left running or stored."
1963
+ : "What was still running is stopping, and everything stored is being deleted.",
1964
+ "Credit bought in the last 15 days and not spent is refunded on request: write to support@withruntime.com.",
1965
+ ].join("\n"), closed);
1966
+ return 0;
1967
+ }
1663
1968
  if (verb !== "ls" && verb !== "list")
1664
- throw unknown("account command", verb, ["ls", "switch", "add"]);
1969
+ throw unknown("account command", verb, ["ls", "switch", "add", "close"]);
1665
1970
  await nameSaved(env).catch(() => undefined);
1666
1971
  const current = await store.read().catch(() => null);
1667
1972
  const saved = [
@@ -1717,7 +2022,7 @@ async function keys(argv, env, out) {
1717
2022
  throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
1718
2023
  if (verb !== "create")
1719
2024
  throw unknown("keys command", verb, ["create"]);
1720
- const args = parse(rest, ["read-only"]);
2025
+ const args = parse(rest, ["read-only"], ["name", "daily-limit"]);
1721
2026
  const known = new Set(["name", "read-only", "daily-limit"]);
1722
2027
  for (const name of args.flags.keys())
1723
2028
  if (!known.has(name))
@@ -1728,6 +2033,7 @@ async function keys(argv, env, out) {
1728
2033
  const limit = flag(args, "daily-limit");
1729
2034
  if (readOnly && limit !== undefined)
1730
2035
  throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
2036
+ const { createKey, dailyLimitMicros } = await import("./keys.js");
1731
2037
  const created = await createKey({
1732
2038
  name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
1733
2039
  access: readOnly ? "read" : "full",
@@ -1754,8 +2060,8 @@ const SNAPSHOT_HELP = `runtime snapshot <command>
1754
2060
  get <id> One snapshot
1755
2061
  extend <id> <days> Keep it longer
1756
2062
  rm <id> Delete it
1757
- Take one with \`runtime sandbox snapshot <sandboxId>\`; start from one with
1758
- \`runtime sandbox fork\` or the SDK's fork().
2063
+ Take one with \`runtime sandbox snapshot <sandboxId>\`; start a sandbox from it
2064
+ with \`runtime sandbox create --snapshot <snapshotId>\`.
1759
2065
  `;
1760
2066
  async function snapshot(argv, env, out) {
1761
2067
  const [verb, ...rest] = argv;
@@ -1768,7 +2074,7 @@ async function snapshot(argv, env, out) {
1768
2074
  switch (verb) {
1769
2075
  case "ls":
1770
2076
  case "list": {
1771
- const args = parse(rest);
2077
+ const args = parse(rest, [], ["sandbox", "name"]);
1772
2078
  const all = await (await api.list({
1773
2079
  ...(flag(args, "sandbox")
1774
2080
  ? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
@@ -1785,11 +2091,11 @@ async function snapshot(argv, env, out) {
1785
2091
  }
1786
2092
  case "get": {
1787
2093
  const found = await api.get(need(rest[0], "a snapshot id"));
1788
- print(JSON.stringify(found, null, 2), found);
2094
+ print((await import("./cli-extras.js")).describeSnapshot(found, table), found);
1789
2095
  return 0;
1790
2096
  }
1791
2097
  case "extend": {
1792
- const days = integer(parse(rest.slice(1)), "days") ?? Number(need(rest[1], "days"));
2098
+ const days = integer(parse(rest.slice(1), [], ["days"]), "days") ?? Number(need(rest[1], "days"));
1793
2099
  const found = await api.extend(need(rest[0], "a snapshot id"), days);
1794
2100
  print(`${found.id} kept until ${found.expiresAt}`, found);
1795
2101
  return 0;
@@ -1809,8 +2115,10 @@ const IMAGE_HELP = `runtime image <command>
1809
2115
  build [<folder>] [-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
1810
2116
  [-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
1811
2117
  [--start <command>] [--ready-port <port> | --ready-command <command>]
1812
- [--max-image-mib 8192] [--timeout <seconds>]
2118
+ [--max-image-mib 8192] [--disk-mib <1024-32768>] [--timeout <seconds>]
1813
2119
  Build and stream the log; prints the image id when ready.
2120
+ --disk-mib sizes the build's scratch disk, where earlier
2121
+ stages and the context live.
1814
2122
  A Dockerfile's folder is the context: everything its
1815
2123
  .dockerignore leaves in (100 MiB compressed at most) is
1816
2124
  sent, and a rebuild sends only what changed.
@@ -1831,6 +2139,28 @@ const IMAGE_HELP = `runtime image <command>
1831
2139
  registry rm <registry>
1832
2140
  Use one: runtime sandbox create --image app:latest
1833
2141
  `;
2142
+ /** Options `image build` takes with a value. */
2143
+ const IMAGE_BUILD_VALUES = [
2144
+ "f",
2145
+ "file",
2146
+ "dockerfile",
2147
+ "from",
2148
+ "pip",
2149
+ "apt",
2150
+ "npm",
2151
+ "t",
2152
+ "tag",
2153
+ "name",
2154
+ "target",
2155
+ "build-arg",
2156
+ "start",
2157
+ "ready-port",
2158
+ "ready-command",
2159
+ "ready-timeout",
2160
+ "max-image-mib",
2161
+ "disk-mib",
2162
+ "timeout",
2163
+ ];
1834
2164
  export async function imageCommand(argv, env, out,
1835
2165
  /** Tests give the registry secret here; the CLI reads standard input. */
1836
2166
  read = readSecret) {
@@ -1843,7 +2173,7 @@ read = readSecret) {
1843
2173
  const api = (await client(env)).images;
1844
2174
  switch (verb) {
1845
2175
  case "build": {
1846
- const args = parse(rest, ["no-cache"]);
2176
+ const args = parse(rest, ["no-cache"], IMAGE_BUILD_VALUES);
1847
2177
  const list = (name) => (args.flags.get(name) ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean);
1848
2178
  const recipe = { apt: list("apt"), pip: list("pip"), npm: list("npm") };
1849
2179
  const hasRecipe = recipe.apt.length + recipe.pip.length + recipe.npm.length > 0;
@@ -1855,6 +2185,7 @@ read = readSecret) {
1855
2185
  throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
1856
2186
  const build = {
1857
2187
  ...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
2188
+ ...(integer(args, "disk-mib") ? { diskMiB: integer(args, "disk-mib") } : {}),
1858
2189
  ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
1859
2190
  };
1860
2191
  // -t app, -t app:v2 -t app:latest: one name, its tags.
@@ -1944,7 +2275,7 @@ read = readSecret) {
1944
2275
  return 0;
1945
2276
  }
1946
2277
  case "logs": {
1947
- const args = parse(rest, ["follow", "f"]);
2278
+ const args = parse(rest, ["follow", "f"], []);
1948
2279
  const id = (await api.resolve(need(args.positional[0], "an image"))).id;
1949
2280
  if (has(args, "follow") || has(args, "f")) {
1950
2281
  const lines = [];
@@ -1993,7 +2324,7 @@ read = readSecret) {
1993
2324
  return 0;
1994
2325
  }
1995
2326
  if (action === "set") {
1996
- const args = parse(more);
2327
+ const args = parse(more, [], ["username", "access-key-id"]);
1997
2328
  const registry = need(args.positional[0], "a registry, such as ghcr.io");
1998
2329
  const accessKeyId = flag(args, "access-key-id");
1999
2330
  if (accessKeyId === undefined && flag(args, "username") === undefined)
@@ -2055,7 +2386,7 @@ async function volume(argv, env, out) {
2055
2386
  const api = (await client(env)).volumes;
2056
2387
  switch (verb) {
2057
2388
  case "create": {
2058
- const args = parse(rest);
2389
+ const args = parse(rest, [], ["size-mib", "name"]);
2059
2390
  const created = await api.create({
2060
2391
  sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
2061
2392
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
@@ -2093,7 +2424,7 @@ async function volume(argv, env, out) {
2093
2424
  return 0;
2094
2425
  }
2095
2426
  case "backup": {
2096
- const args = parse(rest, ["no-wait"]);
2427
+ const args = parse(rest, ["no-wait"], ["retention-days"]);
2097
2428
  const id = need(args.positional[0], "a volume id");
2098
2429
  const days = integer(args, "retention-days");
2099
2430
  const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
@@ -2123,7 +2454,7 @@ async function volume(argv, env, out) {
2123
2454
  return 0;
2124
2455
  }
2125
2456
  case "restore": {
2126
- const args = parse(rest);
2457
+ const args = parse(rest, [], ["name"]);
2127
2458
  const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
2128
2459
  if (!out.json && created.state === "creating")
2129
2460
  out.error(`Restoring; check with: ${me} volume get ${created.id}`);
@@ -2137,7 +2468,7 @@ async function volume(argv, env, out) {
2137
2468
  return 0;
2138
2469
  }
2139
2470
  case "backup-policy": {
2140
- const args = parse(rest);
2471
+ const args = parse(rest, [], ["daily", "retention-days"]);
2141
2472
  const id = need(args.positional[0], "a volume id");
2142
2473
  const daily = flag(args, "daily");
2143
2474
  if (daily !== undefined && !["on", "off", "true", "false"].includes(daily))
@@ -2175,6 +2506,49 @@ const CREATE_FLAGS = [
2175
2506
  "persistent",
2176
2507
  "get-or-create",
2177
2508
  ];
2509
+ /** Says on standard error, once for each reason, why a create has not
2510
+ * answered yet: it is waiting for a trial slot or room, which can take up to
2511
+ * two minutes, and silence reads as a hang. */
2512
+ function waiting(out) {
2513
+ const said = new Set();
2514
+ return (refusal) => {
2515
+ if (said.has(refusal.code))
2516
+ return;
2517
+ said.add(refusal.code);
2518
+ out.error(`${refusal.message} Waiting for room, up to 2 minutes; Ctrl-C stops.`);
2519
+ };
2520
+ }
2521
+ /** Options `sandbox create` and `run` take with a value. */
2522
+ const CREATE_VALUES = [
2523
+ "name",
2524
+ "label",
2525
+ "region",
2526
+ "vcpu",
2527
+ "memory",
2528
+ "disk",
2529
+ "cpu",
2530
+ "cpu-floor",
2531
+ "timeout",
2532
+ "on-timeout",
2533
+ "idle-pause",
2534
+ "max-cost",
2535
+ "max-total-cost",
2536
+ "allow",
2537
+ "deny",
2538
+ "connect",
2539
+ "image",
2540
+ "snapshot",
2541
+ "volume",
2542
+ ];
2543
+ /** Dollars from the command line as integer microdollars: 25, 2.50 or 0.000001. */
2544
+ function usdMicros(args, name) {
2545
+ const value = flag(args, name);
2546
+ if (value === undefined)
2547
+ return undefined;
2548
+ if (!/^\d+(\.\d{1,6})?$/.test(value))
2549
+ throw usage(`--${name} takes dollars, such as 25 or 2.50.`);
2550
+ return Math.round(Number(value) * 1e6);
2551
+ }
2178
2552
  /** A create body from the command line's flags. */
2179
2553
  function createInput(args) {
2180
2554
  const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
@@ -2195,9 +2569,16 @@ function createInput(args) {
2195
2569
  ...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
2196
2570
  ...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
2197
2571
  ...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
2572
+ ...(flag(args, "cpu") ? { cpu: oneOf(args, "cpu", ["shared", "reserved"]) } : {}),
2573
+ ...(integer(args, "cpu-floor") ? { cpuFloorMillis: integer(args, "cpu-floor") } : {}),
2574
+ ...(flag(args, "region") ? { region: flag(args, "region") } : {}),
2575
+ ...(usdMicros(args, "max-cost") ? { maxCostMicros: usdMicros(args, "max-cost") } : {}),
2576
+ ...(usdMicros(args, "max-total-cost")
2577
+ ? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
2578
+ : {}),
2198
2579
  ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
2199
2580
  ...(flag(args, "on-timeout")
2200
- ? { onLeaseEnd: flag(args, "on-timeout") }
2581
+ ? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
2201
2582
  : {}),
2202
2583
  ...(integer(args, "idle-pause") !== undefined
2203
2584
  ? { idlePauseSeconds: integer(args, "idle-pause") }
@@ -2215,7 +2596,9 @@ function createInput(args) {
2215
2596
  }
2216
2597
  /** Runs a command in a sandbox, streaming its output, and answers its exit code
2217
2598
  * (124 on a timeout), so a script's `set -e` behaves. */
2218
- async function execute(sbx, command, args, out, timeout) {
2599
+ async function execute(sbx, command, args, out, timeout,
2600
+ /** `run` stops its whole sandbox on Ctrl-C, so it handles the signal itself. */
2601
+ ownInterrupt = true) {
2219
2602
  const target = command.length === 1 ? command[0] : command;
2220
2603
  const extras = await import("./cli-extras.js");
2221
2604
  const stdin = await extras.pipedStdin();
@@ -2225,13 +2608,32 @@ async function execute(sbx, command, args, out, timeout) {
2225
2608
  ...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
2226
2609
  ...(timeout ? { timeoutMs: timeout * 1000 } : {}),
2227
2610
  };
2228
- const result = out.json
2229
- ? await sbx.exec(target, options)
2230
- : await sbx.exec(target, {
2231
- ...options,
2232
- onStdout: (t) => process.stdout.write(t),
2233
- onStderr: (t) => process.stderr.write(t),
2234
- });
2611
+ // Ctrl-C stops the command in the sandbox too, and says so; without this
2612
+ // the CLI exited and the command ran on unseen.
2613
+ const interrupt = new AbortController();
2614
+ const onInterrupt = () => interrupt.abort(new Error("Interrupted"));
2615
+ if (ownInterrupt)
2616
+ process.once("SIGINT", onInterrupt);
2617
+ let result;
2618
+ try {
2619
+ result = out.json
2620
+ ? await sbx.exec(target, { ...options, signal: interrupt.signal })
2621
+ : await sbx.exec(target, {
2622
+ ...options,
2623
+ signal: interrupt.signal,
2624
+ onStdout: (t) => process.stdout.write(t),
2625
+ onStderr: (t) => process.stderr.write(t),
2626
+ });
2627
+ }
2628
+ catch (error) {
2629
+ if (!interrupt.signal.aborted)
2630
+ throw error;
2631
+ out.error(describeError(error, out.json));
2632
+ return 130;
2633
+ }
2634
+ finally {
2635
+ process.off("SIGINT", onInterrupt);
2636
+ }
2235
2637
  if (out.json)
2236
2638
  out.write(JSON.stringify(result));
2237
2639
  else if (result.timedOut)
@@ -2253,13 +2655,12 @@ async function shell(sbx, command) {
2253
2655
  });
2254
2656
  if (stdin.isTTY)
2255
2657
  stdin.setRawMode(true);
2658
+ const unhook = (await import("./cli-extras.js")).pipeShellInput(stdin, terminal);
2256
2659
  stdin.resume();
2257
- const onInput = (chunk) => terminal.write(new Uint8Array(chunk));
2258
2660
  const onResize = () => terminal.resize(size().cols, size().rows);
2259
- stdin.on("data", onInput);
2260
2661
  process.stdout.on("resize", onResize);
2261
2662
  const code = await terminal.exited;
2262
- stdin.off("data", onInput);
2663
+ unhook();
2263
2664
  process.stdout.off("resize", onResize);
2264
2665
  if (stdin.isTTY)
2265
2666
  stdin.setRawMode(false);
@@ -2289,7 +2690,13 @@ export function describeError(error, json) {
2289
2690
  details: error.details,
2290
2691
  },
2291
2692
  });
2292
- return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId ? `\nRequest: ${error.requestId} (report it: runtime feedback "..." --request-id ${error.requestId})` : ""}`;
2693
+ return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId
2694
+ ? // A refusal of what was sent is the caller's to fix; only a failure
2695
+ // of ours is worth a report.
2696
+ error.status >= 400 && error.status < 500
2697
+ ? `\nRequest: ${error.requestId}`
2698
+ : `\nRequest: ${error.requestId} (report it: runtime feedback "..." --request-id ${error.requestId})`
2699
+ : ""}`;
2293
2700
  }
2294
2701
  const message = error instanceof Error ? error.message : String(error);
2295
2702
  return json ? JSON.stringify({ error: { code: "cli_error", message } }) : `Error: ${message}`;
@@ -2367,7 +2774,7 @@ async function webhooksCommand(argv, env, out) {
2367
2774
  return 0;
2368
2775
  }
2369
2776
  const hooks = (await client(env)).webhooks;
2370
- const args = parse(rest, ["enable", "disable"]);
2777
+ const args = parse(rest, ["enable", "disable"], ["events", "description", "url", "keep", "limit"]);
2371
2778
  const id = () => need(args.positional[0], "a webhook id");
2372
2779
  const events = () => flag(args, "events")
2373
2780
  ?.split(",")
@@ -2463,7 +2870,7 @@ async function otelCommand(argv, env, out) {
2463
2870
  product = "otel";
2464
2871
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
2465
2872
  const otel = (await client(env)).otel;
2466
- const args = parse(rest, ["enable", "disable"]);
2873
+ const args = parse(rest, ["enable", "disable"], ["endpoint", "header", "signals"]);
2467
2874
  const id = () => need(args.positional[0], "an export id");
2468
2875
  const signals = () => flag(args, "signals")
2469
2876
  ?.split(",")