withruntime 0.6.1 → 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 (46) hide show
  1. package/E2B.md +32 -32
  2. package/README.md +3 -3
  3. package/dist/ai/index.d.ts +21 -5
  4. package/dist/ai/index.js +14 -21
  5. package/dist/ai-harness/index.js +15 -2
  6. package/dist/billing-cli.d.ts +21 -0
  7. package/dist/billing-cli.js +158 -0
  8. package/dist/cli-extras.d.ts +15 -0
  9. package/dist/cli-extras.js +60 -4
  10. package/dist/cli-name.d.ts +6 -0
  11. package/dist/cli-name.js +8 -0
  12. package/dist/cli.d.ts +17 -1
  13. package/dist/cli.js +535 -130
  14. package/dist/client.d.ts +35 -6
  15. package/dist/client.js +23 -0
  16. package/dist/credentials.js +1 -2
  17. package/dist/daytona/filesystem.d.ts +2 -0
  18. package/dist/daytona/filesystem.js +9 -7
  19. package/dist/e2b/filesystem.js +3 -2
  20. package/dist/e2b/sandbox.d.ts +9 -6
  21. package/dist/e2b/sandbox.js +31 -7
  22. package/dist/errors.d.ts +5 -0
  23. package/dist/errors.js +7 -0
  24. package/dist/index.d.ts +8 -6
  25. package/dist/index.js +1 -0
  26. package/dist/keys.js +1 -1
  27. package/dist/login.d.ts +2 -6
  28. package/dist/login.js +3 -8
  29. package/dist/network-cli.d.ts +1 -1
  30. package/dist/network-cli.js +64 -2
  31. package/dist/products/billing.d.ts +35 -0
  32. package/dist/products/billing.js +13 -0
  33. package/dist/products/index.d.ts +4 -1
  34. package/dist/products/index.js +4 -1
  35. package/dist/products/network-products.d.ts +45 -0
  36. package/dist/products/network-products.js +27 -0
  37. package/dist/products/previews.d.ts +1 -2
  38. package/dist/products/secrets.d.ts +14 -0
  39. package/dist/sandbox.d.ts +17 -3
  40. package/dist/sandbox.js +233 -62
  41. package/dist/transport.d.ts +17 -1
  42. package/dist/transport.js +124 -7
  43. package/dist/types.d.ts +0 -2
  44. package/dist/wallet.d.ts +30 -0
  45. package/dist/wallet.js +48 -0
  46. package/package.json +2 -2
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,24 +280,28 @@ 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 = [
241
307
  "account",
@@ -244,6 +310,7 @@ const COMMANDS = [
244
310
  "logout",
245
311
  "whoami",
246
312
  "usage",
313
+ "billing",
247
314
  "limits",
248
315
  "sso",
249
316
  "secrets",
@@ -264,12 +331,38 @@ const COMMANDS = [
264
331
  "port",
265
332
  "address",
266
333
  "tunnel",
334
+ "network",
267
335
  "webhooks",
268
336
  "otel",
269
337
  "events",
270
338
  "help",
271
339
  "version",
272
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
+ ];
273
366
  /** Sandbox commands that once sat at the top level. */
274
367
  const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
275
368
  const SANDBOX_VERBS = [
@@ -315,6 +408,14 @@ function need(value, what) {
315
408
  throw usage(`Give ${what}.`);
316
409
  return value;
317
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
+ }
318
419
  /** The client, with the key from RUNTIME_API_KEY or this machine's saved
319
420
  * connection. With neither, the first command connects the machine first (a
320
421
  * browser approval, nothing to copy) and then does what was asked, so setup
@@ -341,6 +442,7 @@ async function client(env) {
341
442
  env.RUNTIME_NO_LOGIN)
342
443
  throw error;
343
444
  process.stderr.write("This machine is not connected to Runtime yet. Connecting it first.\n");
445
+ const { authenticationCommand } = await import("./login.js");
344
446
  const login = await authenticationCommand(["login"], env);
345
447
  if (login.pending)
346
448
  throw notApprovedYet(login);
@@ -383,7 +485,7 @@ function describe(info) {
383
485
  .map(([k, v]) => `${k}=${v}`)
384
486
  .join(", ") || "-",
385
487
  ],
386
- ["charged", `$${(info.chargedMicros / 1e6).toFixed(6)}`],
488
+ ["charged", dollars(BigInt(Math.round(Number(info.chargedMicros) || 0)))],
387
489
  ]);
388
490
  }
389
491
  /** Resolves `<id>:/path` into its parts, or undefined for a local path. */
@@ -402,6 +504,16 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
402
504
  SANDBOX_MOVED.includes(remaining[0] ?? "");
403
505
  const command = nested ? remaining[0] : first;
404
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);
405
517
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
406
518
  product = "";
407
519
  if (!command) {
@@ -427,14 +539,14 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
427
539
  return 0;
428
540
  }
429
541
  if (command === "run") {
430
- 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)'`);
431
543
  const commandLine = args.rest ?? args.positional;
432
544
  if (!commandLine.length)
433
545
  throw usage(`Give the command after --: ${me} sandbox run -- python3 -c 'print(6*7)'`);
434
546
  const runtime = await client(env);
435
547
  const timeout = integer(args, "timeout");
436
548
  const { timeoutSeconds: _lease, ...input } = createInput(args);
437
- const sbx = await runtime.sandboxes.create(input);
549
+ const sbx = await runtime.sandboxes.create(input, { onCapacityWait: waiting(out) });
438
550
  let cleanup;
439
551
  // SIGINT and normal completion share one stop. Keep an absolute deadline
440
552
  // across the stop retries and final state check, so cleanup cannot hang.
@@ -464,7 +576,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
464
576
  process.once("SIGINT", interrupted);
465
577
  let code = 1;
466
578
  try {
467
- code = await execute(sbx, commandLine, args, out, timeout);
579
+ code = await execute(sbx, commandLine, args, out, timeout, has(args, "keep"));
468
580
  }
469
581
  finally {
470
582
  try {
@@ -484,6 +596,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
484
596
  return 0;
485
597
  }
486
598
  if (command === "login" || command === "logout") {
599
+ const { authenticationCommand } = await import("./login.js");
487
600
  const result = await authenticationCommand([command, ...rest], env);
488
601
  print(result.pending
489
602
  ? `Not connected yet. When it is approved, run any command, or \`${me} login --wait\`; it uses this link.`
@@ -493,9 +606,26 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
493
606
  return 0;
494
607
  }
495
608
  if (command === "whoami") {
496
- 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";
497
625
  print(table([
498
- ["organization", who.orgId],
626
+ ["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
627
+ ...(who.role ? [["role", who.role]] : []),
628
+ ...(funding ? [["funding", funding]] : []),
499
629
  ["agent", who.principalId],
500
630
  ["key", who.credentialId ?? "-"],
501
631
  ["api", who.apiVersion],
@@ -503,7 +633,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
503
633
  return 0;
504
634
  }
505
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]`);
506
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
+ }
507
647
  print(usageSummary(value), value);
508
648
  return 0;
509
649
  }
@@ -535,7 +675,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
535
675
  if (command === "otel")
536
676
  return otelCommand(rest, env, out);
537
677
  if (command === "events") {
538
- const args = parse(rest);
678
+ const args = parse(rest, [], ["sandbox", "type", "limit"]);
539
679
  const rt = await client(env);
540
680
  const sandbox = flag(args, "sandbox");
541
681
  const page = await rt.events.list({
@@ -579,7 +719,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
579
719
  if (command === "account" || command === "accounts")
580
720
  return accountCommand(rest, env, out);
581
721
  if (command === "audit") {
582
- const args = parse(rest);
722
+ const args = parse(rest, [], ["action", "limit", "before"]);
583
723
  const known = new Set(["action", "limit", "before"]);
584
724
  for (const name of args.flags.keys())
585
725
  if (!known.has(name))
@@ -607,6 +747,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
607
747
  : "Nothing recorded yet.", page);
608
748
  return 0;
609
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
+ }
610
768
  if (command === "referrals") {
611
769
  const r = await (await client(env)).referrals.get();
612
770
  const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
@@ -648,7 +806,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
648
806
  return 0;
649
807
  }
650
808
  if (command === "feedback") {
651
- const args = parse(rest, ["list"]);
809
+ const args = parse(rest, ["list"], ["kind", "detail", "competitor", "request-id"]);
652
810
  if (has(args, "list")) {
653
811
  const { data } = await (await client(env)).feedback.list();
654
812
  print(data.length
@@ -709,11 +867,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
709
867
  }
710
868
  if (command === "ssh") {
711
869
  product = "ssh";
712
- return sshCommand(rest, env, out, () => client(env), me);
870
+ return (await import("./ssh.js")).sshCommand(rest, env, out, () => client(env), me);
713
871
  }
714
872
  if (command === "port-forward" || command === "forward") {
715
873
  product = "ssh";
716
- return portForwardCommand(rest, out, () => client(env), me);
874
+ return (await import("./ssh.js")).portForwardCommand(rest, out, () => client(env), me);
717
875
  }
718
876
  if (command === "sandbox" || command === "sandboxes" || command === "sbx") {
719
877
  product = "sandbox";
@@ -735,13 +893,62 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
735
893
  command;
736
894
  if (NETWORK_PRODUCTS.includes(networkProduct)) {
737
895
  product = networkProduct;
738
- 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));
739
897
  }
740
898
  throw unknown("command", command, COMMANDS);
741
899
  }
742
900
  /** `runtime usage` for a person: the balance, the trial and what each kind
743
901
  * of resource was charged, in dollars. --json keeps every figure, and each
744
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
+ }
745
952
  function usageSummary(u) {
746
953
  const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
747
954
  ? BigInt(value)
@@ -751,11 +958,14 @@ function usageSummary(u) {
751
958
  const rows = [
752
959
  ["available", dollars(micros(u.available))],
753
960
  ["credited", dollars(micros(u.credited))],
754
- [
755
- "spent",
756
- `${dollars(micros(u.spent))}${takenBack > 0n ? `, ${dollars(takenBack)} of it refunded or disputed` : ""}`,
757
- ],
758
- ["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))],
759
969
  ];
760
970
  if (micros(u.expired) > 0n)
761
971
  rows.push(["expired or taken back", dollars(micros(u.expired))]);
@@ -790,13 +1000,15 @@ function usageSummary(u) {
790
1000
  ])
791
1001
  : "No resources yet.",
792
1002
  "",
793
- `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`,
794
1004
  ].join("\n");
795
1005
  }
796
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;
797
1007
  /** A sandbox's id from its id or its name, for the filters that take an id. */
798
1008
  async function sandboxIdOf(runtime, target) {
799
- 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;
800
1012
  }
801
1013
  /** What a process has printed so far, in order, without waiting for more. */
802
1014
  async function outputSoFar(runtime, sandboxId, processId) {
@@ -902,7 +1114,7 @@ function trialNote(trial, all) {
902
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.`;
903
1115
  }
904
1116
  async function switching(command, argv, env, out) {
905
- const args = parse(argv);
1117
+ const args = parse(argv, [], command === "compare" ? ["from", "days"] : ["from"]);
906
1118
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
907
1119
  const known = new Set(command === "compare" ? ["from", "days"] : ["from"]);
908
1120
  for (const name of args.flags.keys())
@@ -929,7 +1141,7 @@ async function switching(command, argv, env, out) {
929
1141
  return 0;
930
1142
  }
931
1143
  if (!from)
932
- 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...).`);
933
1145
  /* The server holds the list of rivals it has rates for, and names them all
934
1146
  when it refuses one, so a rival added there needs no new CLI. */
935
1147
  const days = integer(args, "days");
@@ -944,7 +1156,7 @@ async function switching(command, argv, env, out) {
944
1156
  const monthly = month === undefined
945
1157
  ? ""
946
1158
  : month >= 0n
947
- ? `about ${dollars(month)} a month`
1159
+ ? `a saving of about ${dollars(month)} a month`
948
1160
  : `about ${dollars(-month)} a month more on Runtime`;
949
1161
  /* The month is projected from the days the usage covers, not from the
950
1162
  window asked for: say which, or a young account's 7 and 90 days read the
@@ -953,7 +1165,7 @@ async function switching(command, argv, env, out) {
953
1165
  const pace = month === undefined
954
1166
  ? "."
955
1167
  : covered < c.window.days
956
- ? `. 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}.`
957
1169
  : c.window.days === 30
958
1170
  ? `, ${monthly}.`
959
1171
  : `. At that pace, ${monthly}.`;
@@ -974,7 +1186,7 @@ async function switching(command, argv, env, out) {
974
1186
  return 0;
975
1187
  }
976
1188
  async function support(argv, env, out) {
977
- const args = parse(argv);
1189
+ const args = parse(argv, [], ["conversation"]);
978
1190
  const api = (await client(env)).support;
979
1191
  const conversationId = flag(args, "conversation");
980
1192
  const [first, second, third] = args.positional;
@@ -1012,9 +1224,10 @@ async function sandbox(argv, env, out) {
1012
1224
  }
1013
1225
  // Inside a sandbox, with no API key: the sandbox's own request token asks.
1014
1226
  if (verb === "identity-token") {
1015
- const args = parse(rest);
1227
+ const args = parse(rest, [], ["audience", "lifetime"]);
1016
1228
  const audience = need(flag(args, "audience"), "--audience, such as sts.amazonaws.com");
1017
1229
  const lifetime = flag(args, "lifetime");
1230
+ const { identityToken } = await import("./identity.js");
1018
1231
  const token = await identityToken({
1019
1232
  audience,
1020
1233
  ...(lifetime === undefined ? {} : { lifetimeSeconds: Number(lifetime) }),
@@ -1025,17 +1238,26 @@ async function sandbox(argv, env, out) {
1025
1238
  const runtime = await client(env);
1026
1239
  /* By id or by name, for every command here, as ssh and port-forward
1027
1240
  always took one. */
1028
- 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);
1029
1249
  switch (verb) {
1030
1250
  case "create": {
1031
- const args = parse(rest, CREATE_FLAGS);
1032
- 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
+ });
1033
1255
  print(sbx.id, sbx.info);
1034
1256
  return 0;
1035
1257
  }
1036
1258
  case "ls":
1037
1259
  case "list": {
1038
- const args = parse(rest, ["all"]);
1260
+ const args = parse(rest, ["all"], ["label", "state"]);
1039
1261
  const page = await runtime.sandboxes.list({
1040
1262
  ...(has(args, "all") ? { includeStopped: true } : {}),
1041
1263
  ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
@@ -1087,8 +1309,8 @@ async function sandbox(argv, env, out) {
1087
1309
  return 0;
1088
1310
  }
1089
1311
  case "mount": {
1090
- const args = parse(rest, ["read-only"]);
1091
- 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]);
1092
1314
  const source = need(args.positional[1], "a bucket, as s3://bucket, r2://bucket or gcs://bucket");
1093
1315
  const found = /^(s3|r2|gcs):\/\/([^/]+)(?:\/(.+))?$/.exec(source);
1094
1316
  if (!found)
@@ -1108,7 +1330,7 @@ async function sandbox(argv, env, out) {
1108
1330
  return 0;
1109
1331
  }
1110
1332
  case "mounts": {
1111
- const sbx = await get(rest[0]);
1333
+ const sbx = await use(rest[0]);
1112
1334
  const all = await sbx.mounts.list();
1113
1335
  print(all.length
1114
1336
  ? table([
@@ -1125,13 +1347,13 @@ async function sandbox(argv, env, out) {
1125
1347
  return 0;
1126
1348
  }
1127
1349
  case "unmount": {
1128
- const sbx = await get(rest[0]);
1350
+ const sbx = await use(rest[0]);
1129
1351
  const gone = await sbx.mounts.remove(need(rest[1], "the mounted path"));
1130
1352
  print(`${gone.path} unmounted`, gone);
1131
1353
  return 0;
1132
1354
  }
1133
1355
  case "snapshot": {
1134
- const args = parse(rest);
1356
+ const args = parse(rest, [], ["name", "retention"]);
1135
1357
  const sbx = await get(args.positional[0]);
1136
1358
  const snapshot = await sbx.snapshot({
1137
1359
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
@@ -1141,7 +1363,7 @@ async function sandbox(argv, env, out) {
1141
1363
  return 0;
1142
1364
  }
1143
1365
  case "fork": {
1144
- const args = parse(rest, ["keep-snapshot", "trial", "paid"]);
1366
+ const args = parse(rest, ["keep-snapshot", "trial", "paid"], ["count", "name"]);
1145
1367
  const sbx = await get(args.positional[0]);
1146
1368
  const forks = await sbx.fork({
1147
1369
  count: integer(args, "count") ?? 1,
@@ -1157,7 +1379,7 @@ async function sandbox(argv, env, out) {
1157
1379
  return 0;
1158
1380
  }
1159
1381
  case "update": {
1160
- const args = parse(rest);
1382
+ const args = parse(rest, [], ["name", "label", "idle-pause", "auto-wake", "persistent", "max-total-cost"]);
1161
1383
  const sbx = await get(args.positional[0]);
1162
1384
  const onOff = (name) => {
1163
1385
  const value = flag(args, name);
@@ -1198,19 +1420,19 @@ async function sandbox(argv, env, out) {
1198
1420
  return 0;
1199
1421
  }
1200
1422
  case "exec": {
1201
- const args = parse(rest);
1423
+ const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandbox exec <id> -- ls -la`);
1202
1424
  const command = args.rest ?? args.positional.slice(1);
1203
1425
  if (!command.length)
1204
1426
  throw usage("Give the command after --: runtime sandbox exec <id> -- ls -la");
1205
- const sbx = await get(args.positional[0]);
1427
+ const sbx = await use(args.positional[0]);
1206
1428
  return execute(sbx, command, args, out, integer(args, "timeout"));
1207
1429
  }
1208
1430
  case "spawn": {
1209
- const args = parse(rest, ["pty"]);
1431
+ const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandbox spawn <id> -- npm run dev`);
1210
1432
  const command = args.rest ?? [];
1211
1433
  if (!command.length)
1212
1434
  throw usage("Give the command after --: runtime sandbox spawn <id> -- npm run dev");
1213
- const sbx = await get(args.positional[0]);
1435
+ const sbx = await use(args.positional[0]);
1214
1436
  const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
1215
1437
  ...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
1216
1438
  ...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
@@ -1220,7 +1442,7 @@ async function sandbox(argv, env, out) {
1220
1442
  return 0;
1221
1443
  }
1222
1444
  case "ps": {
1223
- const sbx = await get(rest[0]);
1445
+ const sbx = await use(rest[0]);
1224
1446
  const list = await sbx.processes.list();
1225
1447
  print(list.length
1226
1448
  ? table([
@@ -1231,8 +1453,8 @@ async function sandbox(argv, env, out) {
1231
1453
  return 0;
1232
1454
  }
1233
1455
  case "logs": {
1234
- const args = parse(rest, ["f", "follow"]);
1235
- const sbx = await get(args.positional[0]);
1456
+ const args = parse(rest, ["f", "follow"], []);
1457
+ const sbx = await use(args.positional[0]);
1236
1458
  const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
1237
1459
  if (has(args, "f") || has(args, "follow")) {
1238
1460
  for await (const event of proc.output())
@@ -1258,26 +1480,28 @@ async function sandbox(argv, env, out) {
1258
1480
  return 0;
1259
1481
  }
1260
1482
  case "kill": {
1261
- const args = parse(rest);
1262
- const sbx = await get(args.positional[0]);
1483
+ const args = parse(rest, [], ["signal"]);
1484
+ const sbx = await use(args.positional[0]);
1263
1485
  const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
1264
1486
  await proc.kill((flag(args, "signal") ?? "SIGTERM"));
1265
1487
  print(`Signalled ${proc.id}.`, { processId: proc.id });
1266
1488
  return 0;
1267
1489
  }
1268
1490
  case "shell": {
1269
- const args = parse(rest);
1270
- const sbx = await get(args.positional[0]);
1491
+ const args = parse(rest, [], ["command"]);
1492
+ const sbx = await use(args.positional[0]);
1271
1493
  return shell(sbx, flag(args, "command"));
1272
1494
  }
1273
1495
  case "cat": {
1274
- const sbx = await get(rest[0]);
1275
- 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 });
1276
1500
  return 0;
1277
1501
  }
1278
1502
  case "files": {
1279
- const args = parse(rest);
1280
- const sbx = await get(args.positional[0]);
1503
+ const args = parse(rest, [], ["depth", "glob"]);
1504
+ const sbx = await use(args.positional[0]);
1281
1505
  const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
1282
1506
  ...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
1283
1507
  ...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
@@ -1288,36 +1512,51 @@ async function sandbox(argv, env, out) {
1288
1512
  return 0;
1289
1513
  }
1290
1514
  case "cp": {
1291
- const [from, to] = parse(rest).positional;
1515
+ const [from, to] = parse(rest, ["r", "recursive"], []).positional;
1292
1516
  const source = remote(need(from, "a source"));
1293
1517
  const target = remote(need(to, "a destination"));
1294
- if (source && !target)
1295
- 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
+ }
1296
1525
  else if (!source && target)
1297
- 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);
1298
1527
  else
1299
1528
  throw usage("Name exactly one side as <sandbox-id>:/path.");
1300
1529
  print(`Copied ${from} to ${to}.`, { from, to });
1301
1530
  return 0;
1302
1531
  }
1303
1532
  case "run-code":
1304
- return runCode(await get(rest[0]), rest.slice(1), out);
1533
+ return runCode(await use(rest[0]), rest.slice(1), out);
1305
1534
  case "preview": {
1306
- const args = parse(rest, ["public"]);
1307
- const sbx = await get(args.positional[0]);
1308
- 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]);
1309
1547
  const preview = await sbx.previews.create(port, {
1310
1548
  ...(has(args, "public") ? { visibility: "public" } : {}),
1311
1549
  ...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
1312
1550
  });
1313
- 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"
1314
1554
  ? preview.url
1315
- : `${preview.urlWithToken ?? preview.url}\nPrivate: send the header x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`) +
1316
- (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);
1317
1556
  return 0;
1318
1557
  }
1319
1558
  case "previews": {
1320
- const list = await (await get(rest[0])).previews.list();
1559
+ const list = await (await use(rest[0])).previews.list();
1321
1560
  print(list.length
1322
1561
  ? table([
1323
1562
  ["PORT", "VISIBILITY", "URL"],
@@ -1327,14 +1566,14 @@ async function sandbox(argv, env, out) {
1327
1566
  return 0;
1328
1567
  }
1329
1568
  case "unshare": {
1330
- const sbx = await get(rest[0]);
1331
- 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]);
1332
1571
  print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
1333
1572
  return 0;
1334
1573
  }
1335
1574
  case "network": {
1336
- const args = parse(rest, ["no-internet", "internet"]);
1337
- 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]);
1338
1577
  const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
1339
1578
  const policy = has(args, "no-internet") || has(args, "internet") || rules.length
1340
1579
  ? await sbx.network.set({
@@ -1344,7 +1583,10 @@ async function sandbox(argv, env, out) {
1344
1583
  : await sbx.network.get();
1345
1584
  print(table([
1346
1585
  ["internet", policy.internet ? "on" : "off"],
1347
- ["allow", policy.allow.join(", ") || "(the public web)"],
1586
+ [
1587
+ "allow",
1588
+ policy.allow.join(", ") || (policy.internet ? "(the public web)" : "(nothing)"),
1589
+ ],
1348
1590
  ["deny", policy.deny.join(", ") || "-"],
1349
1591
  ["connect", policy.connect.join(", ") || "-"],
1350
1592
  ["enforced", policy.enforced ? "yes" : "not yet; run the same command again"],
@@ -1352,9 +1594,9 @@ async function sandbox(argv, env, out) {
1352
1594
  return 0;
1353
1595
  }
1354
1596
  case "desktop":
1355
- return desktop(await get(rest[0]), rest.slice(1), out);
1597
+ return desktop(await use(rest[0]), rest.slice(1), out);
1356
1598
  case "watch":
1357
- 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);
1358
1600
  case "mcp":
1359
1601
  if (rest[0] === "catalog" || rest[0] === undefined) {
1360
1602
  const catalog = await runtime.mcp.catalog();
@@ -1373,10 +1615,10 @@ async function sandbox(argv, env, out) {
1373
1615
  ]), catalog);
1374
1616
  return 0;
1375
1617
  }
1376
- 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);
1377
1619
  case "metrics": {
1378
- const args = parse(rest);
1379
- const sbx = await get(args.positional[0]);
1620
+ const args = parse(rest, [], ["range"]);
1621
+ const sbx = await use(args.positional[0]);
1380
1622
  const range = (flag(args, "range") ?? "1h");
1381
1623
  if (!["15m", "1h", "6h", "24h", "7d", "30d"].includes(range))
1382
1624
  throw usage("--range is one of 15m, 1h, 6h, 24h, 7d, 30d.");
@@ -1438,20 +1680,49 @@ async function readSecret() {
1438
1680
  stdin.pause();
1439
1681
  }
1440
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
+ : "-";
1441
1711
  export async function secretsCommand(argv, env, out,
1442
1712
  /** Tests give the value here; the CLI reads standard input. */
1443
1713
  read = readSecret) {
1444
1714
  const [verb, ...rest] = argv;
1445
- const args = parse(rest);
1715
+ const args = parse(rest, [], ["host", "header", "format", "allow", "value"]);
1446
1716
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
1447
1717
  const rows = (list) => list.length
1448
1718
  ? table([
1449
- ["name", "hosts", "header", "placeholder"],
1719
+ ["name", "hosts", "header", "placeholder", "allow"],
1450
1720
  ...list.map((secret) => [
1451
1721
  secret.name,
1452
1722
  secret.hosts.join(","),
1453
1723
  secret.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
1454
1724
  secret.placeholder,
1725
+ ruleText(secret.rules),
1455
1726
  ]),
1456
1727
  ])
1457
1728
  : "No secrets.";
@@ -1472,17 +1743,21 @@ read = readSecret) {
1472
1743
  throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
1473
1744
  const header = flag(args, "header");
1474
1745
  const format = flag(args, "format");
1746
+ const allow = args.flags.get("allow");
1747
+ const rules = allow ? parseSecretRules(allow) : undefined;
1475
1748
  const saved = await (await client(env)).secrets.set(name, {
1476
1749
  value,
1477
1750
  hosts,
1478
1751
  ...(header ? { header } : {}),
1479
1752
  ...(format ? { format } : {}),
1753
+ ...(rules ? { rules } : {}),
1480
1754
  });
1481
1755
  print([
1482
1756
  `Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
1483
1757
  saved.header
1484
1758
  ? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
1485
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)}.`] : []),
1486
1761
  ...(saved.enforced
1487
1762
  ? []
1488
1763
  : ["A host is catching up; running sandboxes have it within a minute."]),
@@ -1506,7 +1781,7 @@ async function readInput(source) {
1506
1781
  return Buffer.concat(chunks).toString("utf8");
1507
1782
  }
1508
1783
  async function runCode(sbx, argv, out) {
1509
- const args = parse(argv);
1784
+ const args = parse(argv, [], ["lang", "context", "timeout", "out-dir"]);
1510
1785
  const source = need(args.positional[0], "a file, or - to read the code from standard input");
1511
1786
  const code = await readInput(source);
1512
1787
  const byExtension = {
@@ -1574,7 +1849,7 @@ async function desktop(sbx, argv, out) {
1574
1849
  const d = sbx.desktop;
1575
1850
  switch (action) {
1576
1851
  case "start": {
1577
- const args = parse(rest);
1852
+ const args = parse(rest, [], ["width", "height"]);
1578
1853
  const started = await d.start({
1579
1854
  ...(integer(args, "width") ? { width: integer(args, "width") } : {}),
1580
1855
  ...(integer(args, "height") ? { height: integer(args, "height") } : {}),
@@ -1640,6 +1915,7 @@ async function accountCommand(argv, env, out) {
1640
1915
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
1641
1916
  const store = connectionStore(env);
1642
1917
  if (verb === "add") {
1918
+ const { authenticationCommand } = await import("./login.js");
1643
1919
  const result = await authenticationCommand(["login", "--another-account", ...rest], env);
1644
1920
  if (result.pending)
1645
1921
  throw notApprovedYet(result);
@@ -1662,8 +1938,35 @@ async function accountCommand(argv, env, out) {
1662
1938
  });
1663
1939
  return 0;
1664
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
+ }
1665
1968
  if (verb !== "ls" && verb !== "list")
1666
- throw unknown("account command", verb, ["ls", "switch", "add"]);
1969
+ throw unknown("account command", verb, ["ls", "switch", "add", "close"]);
1667
1970
  await nameSaved(env).catch(() => undefined);
1668
1971
  const current = await store.read().catch(() => null);
1669
1972
  const saved = [
@@ -1719,7 +2022,7 @@ async function keys(argv, env, out) {
1719
2022
  throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
1720
2023
  if (verb !== "create")
1721
2024
  throw unknown("keys command", verb, ["create"]);
1722
- const args = parse(rest, ["read-only"]);
2025
+ const args = parse(rest, ["read-only"], ["name", "daily-limit"]);
1723
2026
  const known = new Set(["name", "read-only", "daily-limit"]);
1724
2027
  for (const name of args.flags.keys())
1725
2028
  if (!known.has(name))
@@ -1730,6 +2033,7 @@ async function keys(argv, env, out) {
1730
2033
  const limit = flag(args, "daily-limit");
1731
2034
  if (readOnly && limit !== undefined)
1732
2035
  throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
2036
+ const { createKey, dailyLimitMicros } = await import("./keys.js");
1733
2037
  const created = await createKey({
1734
2038
  name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
1735
2039
  access: readOnly ? "read" : "full",
@@ -1756,8 +2060,8 @@ const SNAPSHOT_HELP = `runtime snapshot <command>
1756
2060
  get <id> One snapshot
1757
2061
  extend <id> <days> Keep it longer
1758
2062
  rm <id> Delete it
1759
- Take one with \`runtime sandbox snapshot <sandboxId>\`; start from one with
1760
- \`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>\`.
1761
2065
  `;
1762
2066
  async function snapshot(argv, env, out) {
1763
2067
  const [verb, ...rest] = argv;
@@ -1770,7 +2074,7 @@ async function snapshot(argv, env, out) {
1770
2074
  switch (verb) {
1771
2075
  case "ls":
1772
2076
  case "list": {
1773
- const args = parse(rest);
2077
+ const args = parse(rest, [], ["sandbox", "name"]);
1774
2078
  const all = await (await api.list({
1775
2079
  ...(flag(args, "sandbox")
1776
2080
  ? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
@@ -1787,11 +2091,11 @@ async function snapshot(argv, env, out) {
1787
2091
  }
1788
2092
  case "get": {
1789
2093
  const found = await api.get(need(rest[0], "a snapshot id"));
1790
- print(JSON.stringify(found, null, 2), found);
2094
+ print((await import("./cli-extras.js")).describeSnapshot(found, table), found);
1791
2095
  return 0;
1792
2096
  }
1793
2097
  case "extend": {
1794
- 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"));
1795
2099
  const found = await api.extend(need(rest[0], "a snapshot id"), days);
1796
2100
  print(`${found.id} kept until ${found.expiresAt}`, found);
1797
2101
  return 0;
@@ -1811,8 +2115,10 @@ const IMAGE_HELP = `runtime image <command>
1811
2115
  build [<folder>] [-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
1812
2116
  [-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
1813
2117
  [--start <command>] [--ready-port <port> | --ready-command <command>]
1814
- [--max-image-mib 8192] [--timeout <seconds>]
2118
+ [--max-image-mib 8192] [--disk-mib <1024-32768>] [--timeout <seconds>]
1815
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.
1816
2122
  A Dockerfile's folder is the context: everything its
1817
2123
  .dockerignore leaves in (100 MiB compressed at most) is
1818
2124
  sent, and a rebuild sends only what changed.
@@ -1833,6 +2139,28 @@ const IMAGE_HELP = `runtime image <command>
1833
2139
  registry rm <registry>
1834
2140
  Use one: runtime sandbox create --image app:latest
1835
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
+ ];
1836
2164
  export async function imageCommand(argv, env, out,
1837
2165
  /** Tests give the registry secret here; the CLI reads standard input. */
1838
2166
  read = readSecret) {
@@ -1845,7 +2173,7 @@ read = readSecret) {
1845
2173
  const api = (await client(env)).images;
1846
2174
  switch (verb) {
1847
2175
  case "build": {
1848
- const args = parse(rest, ["no-cache"]);
2176
+ const args = parse(rest, ["no-cache"], IMAGE_BUILD_VALUES);
1849
2177
  const list = (name) => (args.flags.get(name) ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean);
1850
2178
  const recipe = { apt: list("apt"), pip: list("pip"), npm: list("npm") };
1851
2179
  const hasRecipe = recipe.apt.length + recipe.pip.length + recipe.npm.length > 0;
@@ -1857,6 +2185,7 @@ read = readSecret) {
1857
2185
  throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
1858
2186
  const build = {
1859
2187
  ...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
2188
+ ...(integer(args, "disk-mib") ? { diskMiB: integer(args, "disk-mib") } : {}),
1860
2189
  ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
1861
2190
  };
1862
2191
  // -t app, -t app:v2 -t app:latest: one name, its tags.
@@ -1946,7 +2275,7 @@ read = readSecret) {
1946
2275
  return 0;
1947
2276
  }
1948
2277
  case "logs": {
1949
- const args = parse(rest, ["follow", "f"]);
2278
+ const args = parse(rest, ["follow", "f"], []);
1950
2279
  const id = (await api.resolve(need(args.positional[0], "an image"))).id;
1951
2280
  if (has(args, "follow") || has(args, "f")) {
1952
2281
  const lines = [];
@@ -1995,7 +2324,7 @@ read = readSecret) {
1995
2324
  return 0;
1996
2325
  }
1997
2326
  if (action === "set") {
1998
- const args = parse(more);
2327
+ const args = parse(more, [], ["username", "access-key-id"]);
1999
2328
  const registry = need(args.positional[0], "a registry, such as ghcr.io");
2000
2329
  const accessKeyId = flag(args, "access-key-id");
2001
2330
  if (accessKeyId === undefined && flag(args, "username") === undefined)
@@ -2057,7 +2386,7 @@ async function volume(argv, env, out) {
2057
2386
  const api = (await client(env)).volumes;
2058
2387
  switch (verb) {
2059
2388
  case "create": {
2060
- const args = parse(rest);
2389
+ const args = parse(rest, [], ["size-mib", "name"]);
2061
2390
  const created = await api.create({
2062
2391
  sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
2063
2392
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
@@ -2095,7 +2424,7 @@ async function volume(argv, env, out) {
2095
2424
  return 0;
2096
2425
  }
2097
2426
  case "backup": {
2098
- const args = parse(rest, ["no-wait"]);
2427
+ const args = parse(rest, ["no-wait"], ["retention-days"]);
2099
2428
  const id = need(args.positional[0], "a volume id");
2100
2429
  const days = integer(args, "retention-days");
2101
2430
  const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
@@ -2125,7 +2454,7 @@ async function volume(argv, env, out) {
2125
2454
  return 0;
2126
2455
  }
2127
2456
  case "restore": {
2128
- const args = parse(rest);
2457
+ const args = parse(rest, [], ["name"]);
2129
2458
  const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
2130
2459
  if (!out.json && created.state === "creating")
2131
2460
  out.error(`Restoring; check with: ${me} volume get ${created.id}`);
@@ -2139,7 +2468,7 @@ async function volume(argv, env, out) {
2139
2468
  return 0;
2140
2469
  }
2141
2470
  case "backup-policy": {
2142
- const args = parse(rest);
2471
+ const args = parse(rest, [], ["daily", "retention-days"]);
2143
2472
  const id = need(args.positional[0], "a volume id");
2144
2473
  const daily = flag(args, "daily");
2145
2474
  if (daily !== undefined && !["on", "off", "true", "false"].includes(daily))
@@ -2177,6 +2506,49 @@ const CREATE_FLAGS = [
2177
2506
  "persistent",
2178
2507
  "get-or-create",
2179
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
+ }
2180
2552
  /** A create body from the command line's flags. */
2181
2553
  function createInput(args) {
2182
2554
  const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
@@ -2197,9 +2569,16 @@ function createInput(args) {
2197
2569
  ...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
2198
2570
  ...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
2199
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
+ : {}),
2200
2579
  ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
2201
2580
  ...(flag(args, "on-timeout")
2202
- ? { onLeaseEnd: flag(args, "on-timeout") }
2581
+ ? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
2203
2582
  : {}),
2204
2583
  ...(integer(args, "idle-pause") !== undefined
2205
2584
  ? { idlePauseSeconds: integer(args, "idle-pause") }
@@ -2217,7 +2596,9 @@ function createInput(args) {
2217
2596
  }
2218
2597
  /** Runs a command in a sandbox, streaming its output, and answers its exit code
2219
2598
  * (124 on a timeout), so a script's `set -e` behaves. */
2220
- 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) {
2221
2602
  const target = command.length === 1 ? command[0] : command;
2222
2603
  const extras = await import("./cli-extras.js");
2223
2604
  const stdin = await extras.pipedStdin();
@@ -2227,13 +2608,32 @@ async function execute(sbx, command, args, out, timeout) {
2227
2608
  ...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
2228
2609
  ...(timeout ? { timeoutMs: timeout * 1000 } : {}),
2229
2610
  };
2230
- const result = out.json
2231
- ? await sbx.exec(target, options)
2232
- : await sbx.exec(target, {
2233
- ...options,
2234
- onStdout: (t) => process.stdout.write(t),
2235
- onStderr: (t) => process.stderr.write(t),
2236
- });
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
+ }
2237
2637
  if (out.json)
2238
2638
  out.write(JSON.stringify(result));
2239
2639
  else if (result.timedOut)
@@ -2255,13 +2655,12 @@ async function shell(sbx, command) {
2255
2655
  });
2256
2656
  if (stdin.isTTY)
2257
2657
  stdin.setRawMode(true);
2658
+ const unhook = (await import("./cli-extras.js")).pipeShellInput(stdin, terminal);
2258
2659
  stdin.resume();
2259
- const onInput = (chunk) => terminal.write(new Uint8Array(chunk));
2260
2660
  const onResize = () => terminal.resize(size().cols, size().rows);
2261
- stdin.on("data", onInput);
2262
2661
  process.stdout.on("resize", onResize);
2263
2662
  const code = await terminal.exited;
2264
- stdin.off("data", onInput);
2663
+ unhook();
2265
2664
  process.stdout.off("resize", onResize);
2266
2665
  if (stdin.isTTY)
2267
2666
  stdin.setRawMode(false);
@@ -2291,7 +2690,13 @@ export function describeError(error, json) {
2291
2690
  details: error.details,
2292
2691
  },
2293
2692
  });
2294
- 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
+ : ""}`;
2295
2700
  }
2296
2701
  const message = error instanceof Error ? error.message : String(error);
2297
2702
  return json ? JSON.stringify({ error: { code: "cli_error", message } }) : `Error: ${message}`;
@@ -2369,7 +2774,7 @@ async function webhooksCommand(argv, env, out) {
2369
2774
  return 0;
2370
2775
  }
2371
2776
  const hooks = (await client(env)).webhooks;
2372
- const args = parse(rest, ["enable", "disable"]);
2777
+ const args = parse(rest, ["enable", "disable"], ["events", "description", "url", "keep", "limit"]);
2373
2778
  const id = () => need(args.positional[0], "a webhook id");
2374
2779
  const events = () => flag(args, "events")
2375
2780
  ?.split(",")
@@ -2465,7 +2870,7 @@ async function otelCommand(argv, env, out) {
2465
2870
  product = "otel";
2466
2871
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
2467
2872
  const otel = (await client(env)).otel;
2468
- const args = parse(rest, ["enable", "disable"]);
2873
+ const args = parse(rest, ["enable", "disable"], ["endpoint", "header", "signals"]);
2469
2874
  const id = () => need(args.positional[0], "an export id");
2470
2875
  const signals = () => flag(args, "signals")
2471
2876
  ?.split(",")