withruntime 0.6.1 → 0.7.1

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 (49) 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 +543 -132
  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/commands.js +11 -2
  20. package/dist/e2b/errors.d.ts +5 -0
  21. package/dist/e2b/errors.js +3 -0
  22. package/dist/e2b/filesystem.js +3 -2
  23. package/dist/e2b/sandbox.d.ts +9 -6
  24. package/dist/e2b/sandbox.js +31 -7
  25. package/dist/errors.d.ts +5 -0
  26. package/dist/errors.js +7 -0
  27. package/dist/index.d.ts +8 -6
  28. package/dist/index.js +1 -0
  29. package/dist/keys.js +1 -1
  30. package/dist/login.d.ts +2 -6
  31. package/dist/login.js +3 -8
  32. package/dist/network-cli.d.ts +1 -1
  33. package/dist/network-cli.js +64 -2
  34. package/dist/products/billing.d.ts +35 -0
  35. package/dist/products/billing.js +13 -0
  36. package/dist/products/index.d.ts +4 -1
  37. package/dist/products/index.js +4 -1
  38. package/dist/products/network-products.d.ts +45 -0
  39. package/dist/products/network-products.js +27 -0
  40. package/dist/products/previews.d.ts +1 -2
  41. package/dist/products/secrets.d.ts +14 -0
  42. package/dist/sandbox.d.ts +17 -3
  43. package/dist/sandbox.js +233 -62
  44. package/dist/transport.d.ts +17 -1
  45. package/dist/transport.js +124 -7
  46. package/dist/types.d.ts +0 -2
  47. package/dist/wallet.d.ts +30 -0
  48. package/dist/wallet.js +48 -0
  49. package/package.json +14 -3
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,18 +596,42 @@ 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);
601
+ // Which account it connected to, so a person with several can tell. The
602
+ // connection stands whether or not this read answers.
603
+ const who = command === "login" && "connected" in result && result.connected
604
+ ? await (await client(env)).me().catch(() => undefined)
605
+ : undefined;
606
+ const account = who ? (who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId) : undefined;
488
607
  print(result.pending
489
608
  ? `Not connected yet. When it is approved, run any command, or \`${me} login --wait\`; it uses this link.`
490
609
  : command === "login"
491
- ? `Connected${"agentName" in result && result.agentName ? ` as ${String(result.agentName)}` : ""}.`
492
- : "Disconnected.", result);
610
+ ? `Connected${"agentName" in result && result.agentName ? ` as ${String(result.agentName)}` : ""}${account ? ` to ${account}` : ""}.`
611
+ : "Disconnected.", who ? { ...result, orgId: who.orgId, orgName: who.orgName ?? null } : result);
493
612
  return 0;
494
613
  }
495
614
  if (command === "whoami") {
496
- const who = await (await client(env)).me();
615
+ const rt = await client(env);
616
+ // The usage call only adds the funding line: a key that cannot read it
617
+ // still gets its answer.
618
+ const [who, spend] = await Promise.all([rt.me(), rt.usage().catch(() => undefined)]);
619
+ const trialLeft = spend?.trial ? spend.trial.availableMs / 3_600_000 : 0;
620
+ const credit = spend ? BigInt(spend.available) : 0n;
621
+ const funding = !spend
622
+ ? undefined
623
+ : [
624
+ ...(trialLeft > 0
625
+ ? [
626
+ `free trial, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left`,
627
+ ]
628
+ : []),
629
+ ...(credit > 0n ? [`${dollars(credit)} of credit`] : []),
630
+ ].join("; ") || "no trial time or credit left: runtime billing";
497
631
  print(table([
498
- ["organization", who.orgId],
632
+ ["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
633
+ ...(who.role ? [["role", who.role]] : []),
634
+ ...(funding ? [["funding", funding]] : []),
499
635
  ["agent", who.principalId],
500
636
  ["key", who.credentialId ?? "-"],
501
637
  ["api", who.apiVersion],
@@ -503,7 +639,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
503
639
  return 0;
504
640
  }
505
641
  if (command === "usage") {
642
+ const csv = rest.includes("--csv");
643
+ const unknown = rest.find((arg) => arg !== "--csv");
644
+ if (unknown)
645
+ throw usage(`Unknown option ${unknown}: ${me} usage [--csv | --json]`);
506
646
  const value = await (await client(env)).usage();
647
+ if (csv) {
648
+ out.write(usageCsv(value));
649
+ if ((value.resources ?? []).length >= 100)
650
+ out.error("These are the newest 100 resources; older ones are not in this export.");
651
+ return 0;
652
+ }
507
653
  print(usageSummary(value), value);
508
654
  return 0;
509
655
  }
@@ -535,7 +681,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
535
681
  if (command === "otel")
536
682
  return otelCommand(rest, env, out);
537
683
  if (command === "events") {
538
- const args = parse(rest);
684
+ const args = parse(rest, [], ["sandbox", "type", "limit"]);
539
685
  const rt = await client(env);
540
686
  const sandbox = flag(args, "sandbox");
541
687
  const page = await rt.events.list({
@@ -579,7 +725,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
579
725
  if (command === "account" || command === "accounts")
580
726
  return accountCommand(rest, env, out);
581
727
  if (command === "audit") {
582
- const args = parse(rest);
728
+ const args = parse(rest, [], ["action", "limit", "before"]);
583
729
  const known = new Set(["action", "limit", "before"]);
584
730
  for (const name of args.flags.keys())
585
731
  if (!known.has(name))
@@ -607,6 +753,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
607
753
  : "Nothing recorded yet.", page);
608
754
  return 0;
609
755
  }
756
+ if (command === "billing") {
757
+ product = "billing";
758
+ const origins = connectionOrigins(env);
759
+ const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
760
+ return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
761
+ open: (input) => openWalletAccount(input, { authUrl: origins.auth }),
762
+ claim: (code) => claimWalletAccount(code, { authUrl: origins.auth }),
763
+ save: (key, orgId) => connectionStore(env).save({
764
+ version: 1,
765
+ apiOrigin: origins.api,
766
+ authOrigin: origins.auth,
767
+ key,
768
+ connectionId: "pasted",
769
+ orgId,
770
+ agentName: "Wallet agent",
771
+ }),
772
+ });
773
+ }
610
774
  if (command === "referrals") {
611
775
  const r = await (await client(env)).referrals.get();
612
776
  const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
@@ -648,7 +812,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
648
812
  return 0;
649
813
  }
650
814
  if (command === "feedback") {
651
- const args = parse(rest, ["list"]);
815
+ const args = parse(rest, ["list"], ["kind", "detail", "competitor", "request-id"]);
652
816
  if (has(args, "list")) {
653
817
  const { data } = await (await client(env)).feedback.list();
654
818
  print(data.length
@@ -709,11 +873,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
709
873
  }
710
874
  if (command === "ssh") {
711
875
  product = "ssh";
712
- return sshCommand(rest, env, out, () => client(env), me);
876
+ return (await import("./ssh.js")).sshCommand(rest, env, out, () => client(env), me);
713
877
  }
714
878
  if (command === "port-forward" || command === "forward") {
715
879
  product = "ssh";
716
- return portForwardCommand(rest, out, () => client(env), me);
880
+ return (await import("./ssh.js")).portForwardCommand(rest, out, () => client(env), me);
717
881
  }
718
882
  if (command === "sandbox" || command === "sandboxes" || command === "sbx") {
719
883
  product = "sandbox";
@@ -735,13 +899,62 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
735
899
  command;
736
900
  if (NETWORK_PRODUCTS.includes(networkProduct)) {
737
901
  product = networkProduct;
738
- return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"]), out, () => client(env));
902
+ return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"], ["sandbox", "subnet", "public-key", "route", "out", "host", "secret"]), out, () => client(env));
739
903
  }
740
904
  throw unknown("command", command, COMMANDS);
741
905
  }
742
906
  /** `runtime usage` for a person: the balance, the trial and what each kind
743
907
  * of resource was charged, in dollars. --json keeps every figure, and each
744
908
  * resource with its rates and CPU time. */
909
+ /** One row per resource, for a spreadsheet: when it ran, how big it was, how
910
+ * long it ran, the CPU it used and what it cost. Money is exact dollars to the
911
+ * microdollar (the API's integer microdollars moved six places), so a column
912
+ * sums to the ledger to the last digit. */
913
+ export function usageCsv(u) {
914
+ const cell = (value) => {
915
+ const text = typeof value === "string" || typeof value === "number" || typeof value === "bigint"
916
+ ? String(value)
917
+ : "";
918
+ return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
919
+ };
920
+ const exact = (value) => {
921
+ const micros = BigInt(typeof value === "number" || typeof value === "string" ? value : 0);
922
+ const sign = micros < 0n ? "-" : "";
923
+ const size = micros < 0n ? -micros : micros;
924
+ return `${sign}${size / 1000000n}.${(size % 1000000n).toString().padStart(6, "0")}`;
925
+ };
926
+ const head = [
927
+ "resource_id",
928
+ "name",
929
+ "kind",
930
+ "state",
931
+ "created_at",
932
+ "vcpu",
933
+ "memory_mib",
934
+ "running_seconds",
935
+ "active_cpu_seconds",
936
+ "memory_gib_seconds",
937
+ "charged_usd",
938
+ "held_usd",
939
+ ];
940
+ const rows = (u.resources ?? []).map((r) => [
941
+ r.resourceId,
942
+ r.name,
943
+ r.kind,
944
+ r.state ?? r.status,
945
+ r.createdAt,
946
+ r.vcpu,
947
+ r.memoryMiB,
948
+ r.runningSeconds,
949
+ r.activeCpuSeconds,
950
+ r.memoryGiBSeconds,
951
+ exact(r.chargedMicros),
952
+ exact(r.heldMicros),
953
+ ]
954
+ .map(cell)
955
+ .join(","));
956
+ return [head.join(","), ...rows].join("\n");
957
+ }
745
958
  function usageSummary(u) {
746
959
  const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
747
960
  ? BigInt(value)
@@ -751,11 +964,14 @@ function usageSummary(u) {
751
964
  const rows = [
752
965
  ["available", dollars(micros(u.available))],
753
966
  ["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))],
967
+ // spent includes what refunds and disputes took back: say that part on
968
+ // its own line, so "spent" reads as what was used.
969
+ ["used", dollars(micros(u.spent) - takenBack)],
970
+ ...(takenBack > 0n ? [["returned by refunds and disputes", dollars(takenBack)]] : []),
971
+ // Holds cover running sandboxes and the hour ahead of stored snapshots,
972
+ // images and volumes (pricing guide), so an account with nothing running
973
+ // can still show one.
974
+ ["held for running sandboxes and this hour's storage", dollars(micros(u.held))],
759
975
  ];
760
976
  if (micros(u.expired) > 0n)
761
977
  rows.push(["expired or taken back", dollars(micros(u.expired))]);
@@ -790,13 +1006,15 @@ function usageSummary(u) {
790
1006
  ])
791
1007
  : "No resources yet.",
792
1008
  "",
793
- `Each resource, with its rates and CPU time: ${me} usage --json`,
1009
+ `Each resource, with its rates and CPU time: ${me} usage --json, or --csv for a spreadsheet`,
794
1010
  ].join("\n");
795
1011
  }
796
1012
  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
1013
  /** A sandbox's id from its id or its name, for the filters that take an id. */
798
1014
  async function sandboxIdOf(runtime, target) {
799
- return SANDBOX_UUID.test(target) ? target : (await resolveSandbox(runtime, target)).id;
1015
+ return SANDBOX_UUID.test(target)
1016
+ ? target
1017
+ : (await (await import("./ssh.js")).resolveSandbox(runtime, target)).id;
800
1018
  }
801
1019
  /** What a process has printed so far, in order, without waiting for more. */
802
1020
  async function outputSoFar(runtime, sandboxId, processId) {
@@ -902,7 +1120,7 @@ function trialNote(trial, all) {
902
1120
  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
1121
  }
904
1122
  async function switching(command, argv, env, out) {
905
- const args = parse(argv);
1123
+ const args = parse(argv, [], command === "compare" ? ["from", "days"] : ["from"]);
906
1124
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
907
1125
  const known = new Set(command === "compare" ? ["from", "days"] : ["from"]);
908
1126
  for (const name of args.flags.keys())
@@ -929,7 +1147,7 @@ async function switching(command, argv, env, out) {
929
1147
  return 0;
930
1148
  }
931
1149
  if (!from)
932
- throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines...).`);
1150
+ throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines, lambda-microvms, freestyle, prime...).`);
933
1151
  /* The server holds the list of rivals it has rates for, and names them all
934
1152
  when it refuses one, so a rival added there needs no new CLI. */
935
1153
  const days = integer(args, "days");
@@ -944,7 +1162,7 @@ async function switching(command, argv, env, out) {
944
1162
  const monthly = month === undefined
945
1163
  ? ""
946
1164
  : month >= 0n
947
- ? `about ${dollars(month)} a month`
1165
+ ? `a saving of about ${dollars(month)} a month`
948
1166
  : `about ${dollars(-month)} a month more on Runtime`;
949
1167
  /* The month is projected from the days the usage covers, not from the
950
1168
  window asked for: say which, or a young account's 7 and 90 days read the
@@ -953,7 +1171,7 @@ async function switching(command, argv, env, out) {
953
1171
  const pace = month === undefined
954
1172
  ? "."
955
1173
  : 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}.`
1174
+ ? `. Your usage so far covers ${covered <= 1 ? "less than a day" : `${covered.toLocaleString("en-US")} days`}; at that pace, ${monthly}.`
957
1175
  : c.window.days === 30
958
1176
  ? `, ${monthly}.`
959
1177
  : `. At that pace, ${monthly}.`;
@@ -974,7 +1192,7 @@ async function switching(command, argv, env, out) {
974
1192
  return 0;
975
1193
  }
976
1194
  async function support(argv, env, out) {
977
- const args = parse(argv);
1195
+ const args = parse(argv, [], ["conversation"]);
978
1196
  const api = (await client(env)).support;
979
1197
  const conversationId = flag(args, "conversation");
980
1198
  const [first, second, third] = args.positional;
@@ -1012,9 +1230,10 @@ async function sandbox(argv, env, out) {
1012
1230
  }
1013
1231
  // Inside a sandbox, with no API key: the sandbox's own request token asks.
1014
1232
  if (verb === "identity-token") {
1015
- const args = parse(rest);
1233
+ const args = parse(rest, [], ["audience", "lifetime"]);
1016
1234
  const audience = need(flag(args, "audience"), "--audience, such as sts.amazonaws.com");
1017
1235
  const lifetime = flag(args, "lifetime");
1236
+ const { identityToken } = await import("./identity.js");
1018
1237
  const token = await identityToken({
1019
1238
  audience,
1020
1239
  ...(lifetime === undefined ? {} : { lifetimeSeconds: Number(lifetime) }),
@@ -1025,17 +1244,26 @@ async function sandbox(argv, env, out) {
1025
1244
  const runtime = await client(env);
1026
1245
  /* By id or by name, for every command here, as ssh and port-forward
1027
1246
  always took one. */
1028
- const get = (id) => resolveSandbox(runtime, need(id, "a sandbox id or name"));
1247
+ const get = async (id) => (await import("./ssh.js")).resolveSandbox(runtime, need(id, "a sandbox id or name"));
1248
+ /* For a command that needs only the id: an id is used as it is, with no
1249
+ lookup first, so `exec` is one request and not two (about 110 ms less
1250
+ from Colorado to Virginia). A sandbox that is gone or stopped is refused
1251
+ by the command itself, with the same words. */
1252
+ const use = async (id) => SANDBOX_UUID.test(id ?? "")
1253
+ ? new SandboxHandle(runtime.transport, { id })
1254
+ : get(id);
1029
1255
  switch (verb) {
1030
1256
  case "create": {
1031
- const args = parse(rest, CREATE_FLAGS);
1032
- const sbx = await runtime.sandboxes.create(createInput(args));
1257
+ const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
1258
+ const sbx = await runtime.sandboxes.create(createInput(args), {
1259
+ onCapacityWait: waiting(out),
1260
+ });
1033
1261
  print(sbx.id, sbx.info);
1034
1262
  return 0;
1035
1263
  }
1036
1264
  case "ls":
1037
1265
  case "list": {
1038
- const args = parse(rest, ["all"]);
1266
+ const args = parse(rest, ["all"], ["label", "state"]);
1039
1267
  const page = await runtime.sandboxes.list({
1040
1268
  ...(has(args, "all") ? { includeStopped: true } : {}),
1041
1269
  ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
@@ -1087,8 +1315,8 @@ async function sandbox(argv, env, out) {
1087
1315
  return 0;
1088
1316
  }
1089
1317
  case "mount": {
1090
- const args = parse(rest, ["read-only"]);
1091
- const sbx = await get(args.positional[0]);
1318
+ const args = parse(rest, ["read-only"], ["secret", "region", "endpoint", "account-id"]);
1319
+ const sbx = await use(args.positional[0]);
1092
1320
  const source = need(args.positional[1], "a bucket, as s3://bucket, r2://bucket or gcs://bucket");
1093
1321
  const found = /^(s3|r2|gcs):\/\/([^/]+)(?:\/(.+))?$/.exec(source);
1094
1322
  if (!found)
@@ -1108,7 +1336,7 @@ async function sandbox(argv, env, out) {
1108
1336
  return 0;
1109
1337
  }
1110
1338
  case "mounts": {
1111
- const sbx = await get(rest[0]);
1339
+ const sbx = await use(rest[0]);
1112
1340
  const all = await sbx.mounts.list();
1113
1341
  print(all.length
1114
1342
  ? table([
@@ -1125,13 +1353,13 @@ async function sandbox(argv, env, out) {
1125
1353
  return 0;
1126
1354
  }
1127
1355
  case "unmount": {
1128
- const sbx = await get(rest[0]);
1356
+ const sbx = await use(rest[0]);
1129
1357
  const gone = await sbx.mounts.remove(need(rest[1], "the mounted path"));
1130
1358
  print(`${gone.path} unmounted`, gone);
1131
1359
  return 0;
1132
1360
  }
1133
1361
  case "snapshot": {
1134
- const args = parse(rest);
1362
+ const args = parse(rest, [], ["name", "retention"]);
1135
1363
  const sbx = await get(args.positional[0]);
1136
1364
  const snapshot = await sbx.snapshot({
1137
1365
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
@@ -1141,7 +1369,7 @@ async function sandbox(argv, env, out) {
1141
1369
  return 0;
1142
1370
  }
1143
1371
  case "fork": {
1144
- const args = parse(rest, ["keep-snapshot", "trial", "paid"]);
1372
+ const args = parse(rest, ["keep-snapshot", "trial", "paid"], ["count", "name"]);
1145
1373
  const sbx = await get(args.positional[0]);
1146
1374
  const forks = await sbx.fork({
1147
1375
  count: integer(args, "count") ?? 1,
@@ -1157,7 +1385,7 @@ async function sandbox(argv, env, out) {
1157
1385
  return 0;
1158
1386
  }
1159
1387
  case "update": {
1160
- const args = parse(rest);
1388
+ const args = parse(rest, [], ["name", "label", "idle-pause", "auto-wake", "persistent", "max-total-cost"]);
1161
1389
  const sbx = await get(args.positional[0]);
1162
1390
  const onOff = (name) => {
1163
1391
  const value = flag(args, name);
@@ -1198,19 +1426,19 @@ async function sandbox(argv, env, out) {
1198
1426
  return 0;
1199
1427
  }
1200
1428
  case "exec": {
1201
- const args = parse(rest);
1429
+ const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandbox exec <id> -- ls -la`);
1202
1430
  const command = args.rest ?? args.positional.slice(1);
1203
1431
  if (!command.length)
1204
1432
  throw usage("Give the command after --: runtime sandbox exec <id> -- ls -la");
1205
- const sbx = await get(args.positional[0]);
1433
+ const sbx = await use(args.positional[0]);
1206
1434
  return execute(sbx, command, args, out, integer(args, "timeout"));
1207
1435
  }
1208
1436
  case "spawn": {
1209
- const args = parse(rest, ["pty"]);
1437
+ const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandbox spawn <id> -- npm run dev`);
1210
1438
  const command = args.rest ?? [];
1211
1439
  if (!command.length)
1212
1440
  throw usage("Give the command after --: runtime sandbox spawn <id> -- npm run dev");
1213
- const sbx = await get(args.positional[0]);
1441
+ const sbx = await use(args.positional[0]);
1214
1442
  const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
1215
1443
  ...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
1216
1444
  ...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
@@ -1220,7 +1448,7 @@ async function sandbox(argv, env, out) {
1220
1448
  return 0;
1221
1449
  }
1222
1450
  case "ps": {
1223
- const sbx = await get(rest[0]);
1451
+ const sbx = await use(rest[0]);
1224
1452
  const list = await sbx.processes.list();
1225
1453
  print(list.length
1226
1454
  ? table([
@@ -1231,8 +1459,8 @@ async function sandbox(argv, env, out) {
1231
1459
  return 0;
1232
1460
  }
1233
1461
  case "logs": {
1234
- const args = parse(rest, ["f", "follow"]);
1235
- const sbx = await get(args.positional[0]);
1462
+ const args = parse(rest, ["f", "follow"], []);
1463
+ const sbx = await use(args.positional[0]);
1236
1464
  const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
1237
1465
  if (has(args, "f") || has(args, "follow")) {
1238
1466
  for await (const event of proc.output())
@@ -1258,26 +1486,28 @@ async function sandbox(argv, env, out) {
1258
1486
  return 0;
1259
1487
  }
1260
1488
  case "kill": {
1261
- const args = parse(rest);
1262
- const sbx = await get(args.positional[0]);
1489
+ const args = parse(rest, [], ["signal"]);
1490
+ const sbx = await use(args.positional[0]);
1263
1491
  const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
1264
1492
  await proc.kill((flag(args, "signal") ?? "SIGTERM"));
1265
1493
  print(`Signalled ${proc.id}.`, { processId: proc.id });
1266
1494
  return 0;
1267
1495
  }
1268
1496
  case "shell": {
1269
- const args = parse(rest);
1270
- const sbx = await get(args.positional[0]);
1497
+ const args = parse(rest, [], ["command"]);
1498
+ const sbx = await use(args.positional[0]);
1271
1499
  return shell(sbx, flag(args, "command"));
1272
1500
  }
1273
1501
  case "cat": {
1274
- const sbx = await get(rest[0]);
1275
- process.stdout.write(await sbx.files.read(need(rest[1], "a path")));
1502
+ const sbx = await use(rest[0]);
1503
+ // Streamed, any size; a stream that ends short throws, so exit is 1.
1504
+ const { Writable } = await import("node:stream");
1505
+ await (await sbx.files.readStream(need(rest[1], "a path"))).pipeTo(Writable.toWeb(process.stdout), { preventClose: true, preventAbort: true });
1276
1506
  return 0;
1277
1507
  }
1278
1508
  case "files": {
1279
- const args = parse(rest);
1280
- const sbx = await get(args.positional[0]);
1509
+ const args = parse(rest, [], ["depth", "glob"]);
1510
+ const sbx = await use(args.positional[0]);
1281
1511
  const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
1282
1512
  ...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
1283
1513
  ...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
@@ -1288,36 +1518,51 @@ async function sandbox(argv, env, out) {
1288
1518
  return 0;
1289
1519
  }
1290
1520
  case "cp": {
1291
- const [from, to] = parse(rest).positional;
1521
+ const [from, to] = parse(rest, ["r", "recursive"], []).positional;
1292
1522
  const source = remote(need(from, "a source"));
1293
1523
  const target = remote(need(to, "a destination"));
1294
- if (source && !target)
1295
- await (await get(source.id)).files.download(source.path, to);
1524
+ // A destination ending in / (or a local directory) takes the source's
1525
+ // own name inside it, as cp does.
1526
+ const inside = (dir, path) => `${dir.replace(/\/+$/, "")}/${path.replace(/\/+$/, "").split("/").pop()}`;
1527
+ if (source && !target) {
1528
+ const local = await import("node:fs/promises").then((fs) => fs.stat(to).catch(() => null));
1529
+ await (await use(source.id)).files.download(source.path, to.endsWith("/") || local?.isDirectory() ? inside(to, source.path) : to);
1530
+ }
1296
1531
  else if (!source && target)
1297
- await (await get(target.id)).files.upload(from, target.path);
1532
+ await (await use(target.id)).files.upload(from, target.path.endsWith("/") ? inside(target.path, from) : target.path);
1298
1533
  else
1299
1534
  throw usage("Name exactly one side as <sandbox-id>:/path.");
1300
1535
  print(`Copied ${from} to ${to}.`, { from, to });
1301
1536
  return 0;
1302
1537
  }
1303
1538
  case "run-code":
1304
- return runCode(await get(rest[0]), rest.slice(1), out);
1539
+ return runCode(await use(rest[0]), rest.slice(1), out);
1305
1540
  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"));
1541
+ if (rest[0] === "rotate") {
1542
+ const port = portOf(rest[2], "runtime sandbox preview rotate <id> 3000");
1543
+ const sbx = await use(rest[1]);
1544
+ const preview = await sbx.previews.rotate(port);
1545
+ print(preview.visibility === "public"
1546
+ ? `Port ${port} is public: it takes no token, so there was none to refuse.`
1547
+ : `${preview.urlWithToken ?? preview.url}\nEvery earlier token for port ${port} is refused. New header: x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`, preview);
1548
+ return 0;
1549
+ }
1550
+ const args = parse(rest, ["public"], ["ttl"]);
1551
+ const port = portOf(args.positional[1], "runtime sandbox preview <id> 3000");
1552
+ const sbx = await use(args.positional[0]);
1309
1553
  const preview = await sbx.previews.create(port, {
1310
1554
  ...(has(args, "public") ? { visibility: "public" } : {}),
1311
1555
  ...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
1312
1556
  });
1313
- print((preview.visibility === "public"
1557
+ // The link and how to use it, nothing more: the API's `hint` (which
1558
+ // carries a referral line) stays in --json, not in the command's words.
1559
+ print(preview.visibility === "public"
1314
1560
  ? 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);
1561
+ : `${preview.urlWithToken ?? preview.url}\nPrivate: send the header x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`, preview);
1317
1562
  return 0;
1318
1563
  }
1319
1564
  case "previews": {
1320
- const list = await (await get(rest[0])).previews.list();
1565
+ const list = await (await use(rest[0])).previews.list();
1321
1566
  print(list.length
1322
1567
  ? table([
1323
1568
  ["PORT", "VISIBILITY", "URL"],
@@ -1327,14 +1572,14 @@ async function sandbox(argv, env, out) {
1327
1572
  return 0;
1328
1573
  }
1329
1574
  case "unshare": {
1330
- const sbx = await get(rest[0]);
1331
- const port = Number(need(rest[1], "the port"));
1575
+ const port = portOf(rest[1], "runtime sandbox unshare <id> 3000");
1576
+ const sbx = await use(rest[0]);
1332
1577
  print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
1333
1578
  return 0;
1334
1579
  }
1335
1580
  case "network": {
1336
- const args = parse(rest, ["no-internet", "internet"]);
1337
- const sbx = await get(args.positional[0]);
1581
+ const args = parse(rest, ["no-internet", "internet"], ["allow", "deny", "connect"]);
1582
+ const sbx = await use(args.positional[0]);
1338
1583
  const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
1339
1584
  const policy = has(args, "no-internet") || has(args, "internet") || rules.length
1340
1585
  ? await sbx.network.set({
@@ -1344,7 +1589,10 @@ async function sandbox(argv, env, out) {
1344
1589
  : await sbx.network.get();
1345
1590
  print(table([
1346
1591
  ["internet", policy.internet ? "on" : "off"],
1347
- ["allow", policy.allow.join(", ") || "(the public web)"],
1592
+ [
1593
+ "allow",
1594
+ policy.allow.join(", ") || (policy.internet ? "(the public web)" : "(nothing)"),
1595
+ ],
1348
1596
  ["deny", policy.deny.join(", ") || "-"],
1349
1597
  ["connect", policy.connect.join(", ") || "-"],
1350
1598
  ["enforced", policy.enforced ? "yes" : "not yet; run the same command again"],
@@ -1352,9 +1600,9 @@ async function sandbox(argv, env, out) {
1352
1600
  return 0;
1353
1601
  }
1354
1602
  case "desktop":
1355
- return desktop(await get(rest[0]), rest.slice(1), out);
1603
+ return desktop(await use(rest[0]), rest.slice(1), out);
1356
1604
  case "watch":
1357
- return (await import("./cli-extras.js")).watchCommand(await get(rest[0]), rest.slice(1), out, kit);
1605
+ return (await import("./cli-extras.js")).watchCommand(await use(rest[0]), rest.slice(1), out, kit);
1358
1606
  case "mcp":
1359
1607
  if (rest[0] === "catalog" || rest[0] === undefined) {
1360
1608
  const catalog = await runtime.mcp.catalog();
@@ -1373,10 +1621,10 @@ async function sandbox(argv, env, out) {
1373
1621
  ]), catalog);
1374
1622
  return 0;
1375
1623
  }
1376
- return (await import("./cli-extras.js")).mcpCommand(await get(rest[0]), rest.slice(1), out, kit);
1624
+ return (await import("./cli-extras.js")).mcpCommand(await use(rest[0]), rest.slice(1), out, kit);
1377
1625
  case "metrics": {
1378
- const args = parse(rest);
1379
- const sbx = await get(args.positional[0]);
1626
+ const args = parse(rest, [], ["range"]);
1627
+ const sbx = await use(args.positional[0]);
1380
1628
  const range = (flag(args, "range") ?? "1h");
1381
1629
  if (!["15m", "1h", "6h", "24h", "7d", "30d"].includes(range))
1382
1630
  throw usage("--range is one of 15m, 1h, 6h, 24h, 7d, 30d.");
@@ -1438,20 +1686,49 @@ async function readSecret() {
1438
1686
  stdin.pause();
1439
1687
  }
1440
1688
  }
1689
+ /** `--allow` values as secret rules: each is `[METHOD[,METHOD]...] PATH`, and
1690
+ * rules naming the same methods share one rule's paths. Only the shape is read
1691
+ * here; the API holds every rule to the same check the host makes
1692
+ * (ARCHITECTURE.md section 10, "Per-path rules on an egress secret") and says
1693
+ * which one it refused. */
1694
+ export function parseSecretRules(values) {
1695
+ const byMethods = new Map();
1696
+ for (const value of values) {
1697
+ const words = value.trim().split(/\s+/);
1698
+ const path = words.at(-1);
1699
+ if (!path || words.length > 2 || !path.startsWith("/"))
1700
+ 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)}.`);
1701
+ const methods = words.length === 2
1702
+ ? [...new Set(words[0].toUpperCase().split(",").filter(Boolean))].sort()
1703
+ : undefined;
1704
+ const key = methods?.join(",") ?? "";
1705
+ const rule = byMethods.get(key) ?? { ...(methods ? { methods } : {}), paths: [] };
1706
+ if (!rule.paths.includes(path))
1707
+ rule.paths.push(path);
1708
+ byMethods.set(key, rule);
1709
+ }
1710
+ return [...byMethods.values()];
1711
+ }
1712
+ const ruleText = (rules) => rules?.length
1713
+ ? rules
1714
+ .map((rule) => `${rule.methods ? `${rule.methods.join(",")} ` : ""}${rule.paths.join(" ")}`)
1715
+ .join("; ")
1716
+ : "-";
1441
1717
  export async function secretsCommand(argv, env, out,
1442
1718
  /** Tests give the value here; the CLI reads standard input. */
1443
1719
  read = readSecret) {
1444
1720
  const [verb, ...rest] = argv;
1445
- const args = parse(rest);
1721
+ const args = parse(rest, [], ["host", "header", "format", "allow", "value"]);
1446
1722
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
1447
1723
  const rows = (list) => list.length
1448
1724
  ? table([
1449
- ["name", "hosts", "header", "placeholder"],
1725
+ ["name", "hosts", "header", "placeholder", "allow"],
1450
1726
  ...list.map((secret) => [
1451
1727
  secret.name,
1452
1728
  secret.hosts.join(","),
1453
1729
  secret.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
1454
1730
  secret.placeholder,
1731
+ ruleText(secret.rules),
1455
1732
  ]),
1456
1733
  ])
1457
1734
  : "No secrets.";
@@ -1472,17 +1749,21 @@ read = readSecret) {
1472
1749
  throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
1473
1750
  const header = flag(args, "header");
1474
1751
  const format = flag(args, "format");
1752
+ const allow = args.flags.get("allow");
1753
+ const rules = allow ? parseSecretRules(allow) : undefined;
1475
1754
  const saved = await (await client(env)).secrets.set(name, {
1476
1755
  value,
1477
1756
  hosts,
1478
1757
  ...(header ? { header } : {}),
1479
1758
  ...(format ? { format } : {}),
1759
+ ...(rules ? { rules } : {}),
1480
1760
  });
1481
1761
  print([
1482
1762
  `Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
1483
1763
  saved.header
1484
1764
  ? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
1485
1765
  : `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the URL and headers.`,
1766
+ ...(saved.rules?.length ? [`Only these requests get it: ${ruleText(saved.rules)}.`] : []),
1486
1767
  ...(saved.enforced
1487
1768
  ? []
1488
1769
  : ["A host is catching up; running sandboxes have it within a minute."]),
@@ -1506,7 +1787,7 @@ async function readInput(source) {
1506
1787
  return Buffer.concat(chunks).toString("utf8");
1507
1788
  }
1508
1789
  async function runCode(sbx, argv, out) {
1509
- const args = parse(argv);
1790
+ const args = parse(argv, [], ["lang", "context", "timeout", "out-dir"]);
1510
1791
  const source = need(args.positional[0], "a file, or - to read the code from standard input");
1511
1792
  const code = await readInput(source);
1512
1793
  const byExtension = {
@@ -1574,7 +1855,7 @@ async function desktop(sbx, argv, out) {
1574
1855
  const d = sbx.desktop;
1575
1856
  switch (action) {
1576
1857
  case "start": {
1577
- const args = parse(rest);
1858
+ const args = parse(rest, [], ["width", "height"]);
1578
1859
  const started = await d.start({
1579
1860
  ...(integer(args, "width") ? { width: integer(args, "width") } : {}),
1580
1861
  ...(integer(args, "height") ? { height: integer(args, "height") } : {}),
@@ -1640,6 +1921,7 @@ async function accountCommand(argv, env, out) {
1640
1921
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
1641
1922
  const store = connectionStore(env);
1642
1923
  if (verb === "add") {
1924
+ const { authenticationCommand } = await import("./login.js");
1643
1925
  const result = await authenticationCommand(["login", "--another-account", ...rest], env);
1644
1926
  if (result.pending)
1645
1927
  throw notApprovedYet(result);
@@ -1662,8 +1944,35 @@ async function accountCommand(argv, env, out) {
1662
1944
  });
1663
1945
  return 0;
1664
1946
  }
1947
+ if (verb === "close") {
1948
+ /* ARCHITECTURE.md section 3.1: an owner's key for every product, and the
1949
+ account's name typed exactly. Nothing is asked twice: --confirm is the
1950
+ confirmation, as the website's typed name is. */
1951
+ const args = parse(rest, [], ["confirm"]);
1952
+ for (const name of args.flags.keys())
1953
+ if (name !== "confirm")
1954
+ throw usage(`Unknown option --${name}.`);
1955
+ const confirm = flag(args, "confirm");
1956
+ const runtime = await client(env);
1957
+ if (!confirm) {
1958
+ const who = await runtime.me();
1959
+ 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>"}".`);
1960
+ }
1961
+ const closed = await runtime.account.close({ confirm });
1962
+ // This machine's key died with the account.
1963
+ if (!env.RUNTIME_API_KEY)
1964
+ await store.remove().catch(() => undefined);
1965
+ print([
1966
+ `Closed ${closed.name}. Every key has stopped working.`,
1967
+ closed.released
1968
+ ? "Nothing was left running or stored."
1969
+ : "What was still running is stopping, and everything stored is being deleted.",
1970
+ "Credit bought in the last 15 days and not spent is refunded on request: write to support@withruntime.com.",
1971
+ ].join("\n"), closed);
1972
+ return 0;
1973
+ }
1665
1974
  if (verb !== "ls" && verb !== "list")
1666
- throw unknown("account command", verb, ["ls", "switch", "add"]);
1975
+ throw unknown("account command", verb, ["ls", "switch", "add", "close"]);
1667
1976
  await nameSaved(env).catch(() => undefined);
1668
1977
  const current = await store.read().catch(() => null);
1669
1978
  const saved = [
@@ -1719,7 +2028,7 @@ async function keys(argv, env, out) {
1719
2028
  throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
1720
2029
  if (verb !== "create")
1721
2030
  throw unknown("keys command", verb, ["create"]);
1722
- const args = parse(rest, ["read-only"]);
2031
+ const args = parse(rest, ["read-only"], ["name", "daily-limit"]);
1723
2032
  const known = new Set(["name", "read-only", "daily-limit"]);
1724
2033
  for (const name of args.flags.keys())
1725
2034
  if (!known.has(name))
@@ -1730,6 +2039,7 @@ async function keys(argv, env, out) {
1730
2039
  const limit = flag(args, "daily-limit");
1731
2040
  if (readOnly && limit !== undefined)
1732
2041
  throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
2042
+ const { createKey, dailyLimitMicros } = await import("./keys.js");
1733
2043
  const created = await createKey({
1734
2044
  name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
1735
2045
  access: readOnly ? "read" : "full",
@@ -1756,8 +2066,8 @@ const SNAPSHOT_HELP = `runtime snapshot <command>
1756
2066
  get <id> One snapshot
1757
2067
  extend <id> <days> Keep it longer
1758
2068
  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().
2069
+ Take one with \`runtime sandbox snapshot <sandboxId>\`; start a sandbox from it
2070
+ with \`runtime sandbox create --snapshot <snapshotId>\`.
1761
2071
  `;
1762
2072
  async function snapshot(argv, env, out) {
1763
2073
  const [verb, ...rest] = argv;
@@ -1770,7 +2080,7 @@ async function snapshot(argv, env, out) {
1770
2080
  switch (verb) {
1771
2081
  case "ls":
1772
2082
  case "list": {
1773
- const args = parse(rest);
2083
+ const args = parse(rest, [], ["sandbox", "name"]);
1774
2084
  const all = await (await api.list({
1775
2085
  ...(flag(args, "sandbox")
1776
2086
  ? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
@@ -1787,11 +2097,11 @@ async function snapshot(argv, env, out) {
1787
2097
  }
1788
2098
  case "get": {
1789
2099
  const found = await api.get(need(rest[0], "a snapshot id"));
1790
- print(JSON.stringify(found, null, 2), found);
2100
+ print((await import("./cli-extras.js")).describeSnapshot(found, table), found);
1791
2101
  return 0;
1792
2102
  }
1793
2103
  case "extend": {
1794
- const days = integer(parse(rest.slice(1)), "days") ?? Number(need(rest[1], "days"));
2104
+ const days = integer(parse(rest.slice(1), [], ["days"]), "days") ?? Number(need(rest[1], "days"));
1795
2105
  const found = await api.extend(need(rest[0], "a snapshot id"), days);
1796
2106
  print(`${found.id} kept until ${found.expiresAt}`, found);
1797
2107
  return 0;
@@ -1811,8 +2121,10 @@ const IMAGE_HELP = `runtime image <command>
1811
2121
  build [<folder>] [-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
1812
2122
  [-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
1813
2123
  [--start <command>] [--ready-port <port> | --ready-command <command>]
1814
- [--max-image-mib 8192] [--timeout <seconds>]
2124
+ [--max-image-mib 8192] [--disk-mib <1024-32768>] [--timeout <seconds>]
1815
2125
  Build and stream the log; prints the image id when ready.
2126
+ --disk-mib sizes the build's scratch disk, where earlier
2127
+ stages and the context live.
1816
2128
  A Dockerfile's folder is the context: everything its
1817
2129
  .dockerignore leaves in (100 MiB compressed at most) is
1818
2130
  sent, and a rebuild sends only what changed.
@@ -1833,6 +2145,28 @@ const IMAGE_HELP = `runtime image <command>
1833
2145
  registry rm <registry>
1834
2146
  Use one: runtime sandbox create --image app:latest
1835
2147
  `;
2148
+ /** Options `image build` takes with a value. */
2149
+ const IMAGE_BUILD_VALUES = [
2150
+ "f",
2151
+ "file",
2152
+ "dockerfile",
2153
+ "from",
2154
+ "pip",
2155
+ "apt",
2156
+ "npm",
2157
+ "t",
2158
+ "tag",
2159
+ "name",
2160
+ "target",
2161
+ "build-arg",
2162
+ "start",
2163
+ "ready-port",
2164
+ "ready-command",
2165
+ "ready-timeout",
2166
+ "max-image-mib",
2167
+ "disk-mib",
2168
+ "timeout",
2169
+ ];
1836
2170
  export async function imageCommand(argv, env, out,
1837
2171
  /** Tests give the registry secret here; the CLI reads standard input. */
1838
2172
  read = readSecret) {
@@ -1845,7 +2179,7 @@ read = readSecret) {
1845
2179
  const api = (await client(env)).images;
1846
2180
  switch (verb) {
1847
2181
  case "build": {
1848
- const args = parse(rest, ["no-cache"]);
2182
+ const args = parse(rest, ["no-cache"], IMAGE_BUILD_VALUES);
1849
2183
  const list = (name) => (args.flags.get(name) ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean);
1850
2184
  const recipe = { apt: list("apt"), pip: list("pip"), npm: list("npm") };
1851
2185
  const hasRecipe = recipe.apt.length + recipe.pip.length + recipe.npm.length > 0;
@@ -1857,6 +2191,7 @@ read = readSecret) {
1857
2191
  throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
1858
2192
  const build = {
1859
2193
  ...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
2194
+ ...(integer(args, "disk-mib") ? { diskMiB: integer(args, "disk-mib") } : {}),
1860
2195
  ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
1861
2196
  };
1862
2197
  // -t app, -t app:v2 -t app:latest: one name, its tags.
@@ -1946,7 +2281,7 @@ read = readSecret) {
1946
2281
  return 0;
1947
2282
  }
1948
2283
  case "logs": {
1949
- const args = parse(rest, ["follow", "f"]);
2284
+ const args = parse(rest, ["follow", "f"], []);
1950
2285
  const id = (await api.resolve(need(args.positional[0], "an image"))).id;
1951
2286
  if (has(args, "follow") || has(args, "f")) {
1952
2287
  const lines = [];
@@ -1995,7 +2330,7 @@ read = readSecret) {
1995
2330
  return 0;
1996
2331
  }
1997
2332
  if (action === "set") {
1998
- const args = parse(more);
2333
+ const args = parse(more, [], ["username", "access-key-id"]);
1999
2334
  const registry = need(args.positional[0], "a registry, such as ghcr.io");
2000
2335
  const accessKeyId = flag(args, "access-key-id");
2001
2336
  if (accessKeyId === undefined && flag(args, "username") === undefined)
@@ -2057,7 +2392,7 @@ async function volume(argv, env, out) {
2057
2392
  const api = (await client(env)).volumes;
2058
2393
  switch (verb) {
2059
2394
  case "create": {
2060
- const args = parse(rest);
2395
+ const args = parse(rest, [], ["size-mib", "name"]);
2061
2396
  const created = await api.create({
2062
2397
  sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
2063
2398
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
@@ -2095,7 +2430,7 @@ async function volume(argv, env, out) {
2095
2430
  return 0;
2096
2431
  }
2097
2432
  case "backup": {
2098
- const args = parse(rest, ["no-wait"]);
2433
+ const args = parse(rest, ["no-wait"], ["retention-days"]);
2099
2434
  const id = need(args.positional[0], "a volume id");
2100
2435
  const days = integer(args, "retention-days");
2101
2436
  const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
@@ -2125,7 +2460,7 @@ async function volume(argv, env, out) {
2125
2460
  return 0;
2126
2461
  }
2127
2462
  case "restore": {
2128
- const args = parse(rest);
2463
+ const args = parse(rest, [], ["name"]);
2129
2464
  const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
2130
2465
  if (!out.json && created.state === "creating")
2131
2466
  out.error(`Restoring; check with: ${me} volume get ${created.id}`);
@@ -2139,7 +2474,7 @@ async function volume(argv, env, out) {
2139
2474
  return 0;
2140
2475
  }
2141
2476
  case "backup-policy": {
2142
- const args = parse(rest);
2477
+ const args = parse(rest, [], ["daily", "retention-days"]);
2143
2478
  const id = need(args.positional[0], "a volume id");
2144
2479
  const daily = flag(args, "daily");
2145
2480
  if (daily !== undefined && !["on", "off", "true", "false"].includes(daily))
@@ -2177,6 +2512,49 @@ const CREATE_FLAGS = [
2177
2512
  "persistent",
2178
2513
  "get-or-create",
2179
2514
  ];
2515
+ /** Says on standard error, once for each reason, why a create has not
2516
+ * answered yet: it is waiting for a trial slot or room, which can take up to
2517
+ * two minutes, and silence reads as a hang. */
2518
+ function waiting(out) {
2519
+ const said = new Set();
2520
+ return (refusal) => {
2521
+ if (said.has(refusal.code))
2522
+ return;
2523
+ said.add(refusal.code);
2524
+ out.error(`${refusal.message} Waiting for room, up to 2 minutes; Ctrl-C stops.`);
2525
+ };
2526
+ }
2527
+ /** Options `sandbox create` and `run` take with a value. */
2528
+ const CREATE_VALUES = [
2529
+ "name",
2530
+ "label",
2531
+ "region",
2532
+ "vcpu",
2533
+ "memory",
2534
+ "disk",
2535
+ "cpu",
2536
+ "cpu-floor",
2537
+ "timeout",
2538
+ "on-timeout",
2539
+ "idle-pause",
2540
+ "max-cost",
2541
+ "max-total-cost",
2542
+ "allow",
2543
+ "deny",
2544
+ "connect",
2545
+ "image",
2546
+ "snapshot",
2547
+ "volume",
2548
+ ];
2549
+ /** Dollars from the command line as integer microdollars: 25, 2.50 or 0.000001. */
2550
+ function usdMicros(args, name) {
2551
+ const value = flag(args, name);
2552
+ if (value === undefined)
2553
+ return undefined;
2554
+ if (!/^\d+(\.\d{1,6})?$/.test(value))
2555
+ throw usage(`--${name} takes dollars, such as 25 or 2.50.`);
2556
+ return Math.round(Number(value) * 1e6);
2557
+ }
2180
2558
  /** A create body from the command line's flags. */
2181
2559
  function createInput(args) {
2182
2560
  const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
@@ -2197,9 +2575,16 @@ function createInput(args) {
2197
2575
  ...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
2198
2576
  ...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
2199
2577
  ...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
2578
+ ...(flag(args, "cpu") ? { cpu: oneOf(args, "cpu", ["shared", "reserved"]) } : {}),
2579
+ ...(integer(args, "cpu-floor") ? { cpuFloorMillis: integer(args, "cpu-floor") } : {}),
2580
+ ...(flag(args, "region") ? { region: flag(args, "region") } : {}),
2581
+ ...(usdMicros(args, "max-cost") ? { maxCostMicros: usdMicros(args, "max-cost") } : {}),
2582
+ ...(usdMicros(args, "max-total-cost")
2583
+ ? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
2584
+ : {}),
2200
2585
  ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
2201
2586
  ...(flag(args, "on-timeout")
2202
- ? { onLeaseEnd: flag(args, "on-timeout") }
2587
+ ? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
2203
2588
  : {}),
2204
2589
  ...(integer(args, "idle-pause") !== undefined
2205
2590
  ? { idlePauseSeconds: integer(args, "idle-pause") }
@@ -2217,7 +2602,9 @@ function createInput(args) {
2217
2602
  }
2218
2603
  /** Runs a command in a sandbox, streaming its output, and answers its exit code
2219
2604
  * (124 on a timeout), so a script's `set -e` behaves. */
2220
- async function execute(sbx, command, args, out, timeout) {
2605
+ async function execute(sbx, command, args, out, timeout,
2606
+ /** `run` stops its whole sandbox on Ctrl-C, so it handles the signal itself. */
2607
+ ownInterrupt = true) {
2221
2608
  const target = command.length === 1 ? command[0] : command;
2222
2609
  const extras = await import("./cli-extras.js");
2223
2610
  const stdin = await extras.pipedStdin();
@@ -2227,13 +2614,32 @@ async function execute(sbx, command, args, out, timeout) {
2227
2614
  ...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
2228
2615
  ...(timeout ? { timeoutMs: timeout * 1000 } : {}),
2229
2616
  };
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
- });
2617
+ // Ctrl-C stops the command in the sandbox too, and says so; without this
2618
+ // the CLI exited and the command ran on unseen.
2619
+ const interrupt = new AbortController();
2620
+ const onInterrupt = () => interrupt.abort(new Error("Interrupted"));
2621
+ if (ownInterrupt)
2622
+ process.once("SIGINT", onInterrupt);
2623
+ let result;
2624
+ try {
2625
+ result = out.json
2626
+ ? await sbx.exec(target, { ...options, signal: interrupt.signal })
2627
+ : await sbx.exec(target, {
2628
+ ...options,
2629
+ signal: interrupt.signal,
2630
+ onStdout: (t) => process.stdout.write(t),
2631
+ onStderr: (t) => process.stderr.write(t),
2632
+ });
2633
+ }
2634
+ catch (error) {
2635
+ if (!interrupt.signal.aborted)
2636
+ throw error;
2637
+ out.error(describeError(error, out.json));
2638
+ return 130;
2639
+ }
2640
+ finally {
2641
+ process.off("SIGINT", onInterrupt);
2642
+ }
2237
2643
  if (out.json)
2238
2644
  out.write(JSON.stringify(result));
2239
2645
  else if (result.timedOut)
@@ -2255,13 +2661,12 @@ async function shell(sbx, command) {
2255
2661
  });
2256
2662
  if (stdin.isTTY)
2257
2663
  stdin.setRawMode(true);
2664
+ const unhook = (await import("./cli-extras.js")).pipeShellInput(stdin, terminal);
2258
2665
  stdin.resume();
2259
- const onInput = (chunk) => terminal.write(new Uint8Array(chunk));
2260
2666
  const onResize = () => terminal.resize(size().cols, size().rows);
2261
- stdin.on("data", onInput);
2262
2667
  process.stdout.on("resize", onResize);
2263
2668
  const code = await terminal.exited;
2264
- stdin.off("data", onInput);
2669
+ unhook();
2265
2670
  process.stdout.off("resize", onResize);
2266
2671
  if (stdin.isTTY)
2267
2672
  stdin.setRawMode(false);
@@ -2291,7 +2696,13 @@ export function describeError(error, json) {
2291
2696
  details: error.details,
2292
2697
  },
2293
2698
  });
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})` : ""}`;
2699
+ return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId
2700
+ ? // A refusal of what was sent is the caller's to fix; only a failure
2701
+ // of ours is worth a report.
2702
+ error.status >= 400 && error.status < 500
2703
+ ? `\nRequest: ${error.requestId}`
2704
+ : `\nRequest: ${error.requestId} (report it: runtime feedback "..." --request-id ${error.requestId})`
2705
+ : ""}`;
2295
2706
  }
2296
2707
  const message = error instanceof Error ? error.message : String(error);
2297
2708
  return json ? JSON.stringify({ error: { code: "cli_error", message } }) : `Error: ${message}`;
@@ -2369,7 +2780,7 @@ async function webhooksCommand(argv, env, out) {
2369
2780
  return 0;
2370
2781
  }
2371
2782
  const hooks = (await client(env)).webhooks;
2372
- const args = parse(rest, ["enable", "disable"]);
2783
+ const args = parse(rest, ["enable", "disable"], ["events", "description", "url", "keep", "limit"]);
2373
2784
  const id = () => need(args.positional[0], "a webhook id");
2374
2785
  const events = () => flag(args, "events")
2375
2786
  ?.split(",")
@@ -2465,7 +2876,7 @@ async function otelCommand(argv, env, out) {
2465
2876
  product = "otel";
2466
2877
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
2467
2878
  const otel = (await client(env)).otel;
2468
- const args = parse(rest, ["enable", "disable"]);
2879
+ const args = parse(rest, ["enable", "disable"], ["endpoint", "header", "signals"]);
2469
2880
  const id = () => need(args.positional[0], "an export id");
2470
2881
  const signals = () => flag(args, "signals")
2471
2882
  ?.split(",")