withruntime 0.3.1 → 0.5.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 (105) hide show
  1. package/DAYTONA.md +129 -0
  2. package/E2B.md +146 -0
  3. package/README.md +84 -12
  4. package/VERCEL.md +127 -0
  5. package/dist/ai/index.d.ts +33 -0
  6. package/dist/ai/index.js +38 -0
  7. package/dist/claude-agent-sdk/index.d.ts +7 -0
  8. package/dist/claude-agent-sdk/index.js +65 -0
  9. package/dist/cli.d.ts +6 -0
  10. package/dist/cli.js +949 -105
  11. package/dist/client.d.ts +25 -2
  12. package/dist/client.js +14 -1
  13. package/dist/credentials.d.ts +10 -0
  14. package/dist/credentials.js +56 -1
  15. package/dist/daytona/client.d.ts +44 -0
  16. package/dist/daytona/client.js +40 -0
  17. package/dist/daytona/context.d.ts +26 -0
  18. package/dist/daytona/context.js +22 -0
  19. package/dist/daytona/daytona.d.ts +185 -0
  20. package/dist/daytona/daytona.js +416 -0
  21. package/dist/daytona/errors.d.ts +106 -0
  22. package/dist/daytona/errors.js +182 -0
  23. package/dist/daytona/filesystem.d.ts +84 -0
  24. package/dist/daytona/filesystem.js +206 -0
  25. package/dist/daytona/git.d.ts +33 -0
  26. package/dist/daytona/git.js +128 -0
  27. package/dist/daytona/image.d.ts +45 -0
  28. package/dist/daytona/image.js +140 -0
  29. package/dist/daytona/index.d.ts +50 -0
  30. package/dist/daytona/index.js +50 -0
  31. package/dist/daytona/process.d.ts +86 -0
  32. package/dist/daytona/process.js +339 -0
  33. package/dist/daytona/sandbox.d.ts +196 -0
  34. package/dist/daytona/sandbox.js +448 -0
  35. package/dist/e2b/client.d.ts +58 -0
  36. package/dist/e2b/client.js +66 -0
  37. package/dist/e2b/code-interpreter.d.ts +144 -0
  38. package/dist/e2b/code-interpreter.js +244 -0
  39. package/dist/e2b/commands.d.ts +100 -0
  40. package/dist/e2b/commands.js +268 -0
  41. package/dist/e2b/errors.d.ts +72 -0
  42. package/dist/e2b/errors.js +151 -0
  43. package/dist/e2b/filesystem.d.ts +82 -0
  44. package/dist/e2b/filesystem.js +143 -0
  45. package/dist/e2b/index.d.ts +28 -0
  46. package/dist/e2b/index.js +61 -0
  47. package/dist/e2b/sandbox.d.ts +203 -0
  48. package/dist/e2b/sandbox.js +490 -0
  49. package/dist/e2b/unsupported.d.ts +19 -0
  50. package/dist/e2b/unsupported.js +27 -0
  51. package/dist/errors.d.ts +10 -4
  52. package/dist/errors.js +26 -6
  53. package/dist/image-context.d.ts +39 -0
  54. package/dist/image-context.js +176 -0
  55. package/dist/index.d.ts +19 -2
  56. package/dist/index.js +17 -2
  57. package/dist/keys.d.ts +43 -0
  58. package/dist/keys.js +196 -0
  59. package/dist/login.js +27 -6
  60. package/dist/mcp.d.ts +2 -2
  61. package/dist/mcp.js +4 -3
  62. package/dist/openai-agents/index.d.ts +132 -0
  63. package/dist/openai-agents/index.js +791 -0
  64. package/dist/products/audit.d.ts +48 -0
  65. package/dist/products/audit.js +10 -0
  66. package/dist/products/images.d.ts +113 -7
  67. package/dist/products/images.js +186 -27
  68. package/dist/products/index.d.ts +11 -0
  69. package/dist/products/index.js +17 -1
  70. package/dist/products/observability.d.ts +186 -0
  71. package/dist/products/observability.js +117 -0
  72. package/dist/products/secrets.d.ts +40 -0
  73. package/dist/products/secrets.js +24 -0
  74. package/dist/products/switching.d.ts +89 -0
  75. package/dist/products/switching.js +25 -0
  76. package/dist/proxy.d.ts +30 -0
  77. package/dist/proxy.js +178 -0
  78. package/dist/sandbox.d.ts +30 -2
  79. package/dist/sandbox.js +101 -1
  80. package/dist/ssh.d.ts +21 -0
  81. package/dist/ssh.js +312 -0
  82. package/dist/tar.d.ts +1 -0
  83. package/dist/tar.js +4 -4
  84. package/dist/tools/index.d.ts +62 -0
  85. package/dist/tools/index.js +180 -0
  86. package/dist/transport.d.ts +12 -2
  87. package/dist/transport.js +76 -16
  88. package/dist/tunnel.d.ts +67 -0
  89. package/dist/tunnel.js +311 -0
  90. package/dist/types.d.ts +57 -1
  91. package/dist/vercel/client.d.ts +40 -0
  92. package/dist/vercel/client.js +39 -0
  93. package/dist/vercel/command.d.ts +70 -0
  94. package/dist/vercel/command.js +148 -0
  95. package/dist/vercel/errors.d.ts +53 -0
  96. package/dist/vercel/errors.js +113 -0
  97. package/dist/vercel/filesystem.d.ts +150 -0
  98. package/dist/vercel/filesystem.js +248 -0
  99. package/dist/vercel/index.d.ts +30 -0
  100. package/dist/vercel/index.js +38 -0
  101. package/dist/vercel/sandbox.d.ts +317 -0
  102. package/dist/vercel/sandbox.js +726 -0
  103. package/dist/vercel/snapshot.d.ts +93 -0
  104. package/dist/vercel/snapshot.js +123 -0
  105. package/package.json +69 -5
package/dist/cli.js CHANGED
@@ -1,19 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from "node:fs";
3
- import { readdir, readFile, stat, writeFile } from "node:fs/promises";
4
- import { dirname, join, relative, resolve, sep } from "node:path";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { resolveCredential } from "./credentials.js";
6
+ import { connectionStore, resolveCredential } from "./credentials.js";
7
7
  import { authenticationCommand, me, named } from "./login.js";
8
+ import { createKey, dailyLimitMicros } from "./keys.js";
8
9
  import { Runtime } from "./client.js";
9
10
  import { RuntimeError } from "./errors.js";
11
+ import { envFetch } from "./proxy.js";
12
+ import { portForwardCommand, sshCommand } from "./ssh.js";
10
13
  import { VERSION } from "./transport.js";
11
14
  const HELP = `Runtime Cloud CLI ${VERSION}
12
15
 
13
16
  Usage: runtime <product> <command> [options] runtime <command> [options]
14
17
 
15
18
  Start
16
- run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
19
+ sandbox run [--keep] [create options] -- <command>
20
+ A fresh sandbox runs one command, then stops
17
21
 
18
22
  Account
19
23
  login [--agent-name <name>] [--no-browser] [--wait]
@@ -21,9 +25,25 @@ Account
21
25
  login --with-key Or paste a key from withruntime.com/account/keys
22
26
  logout Revoke this machine's connection
23
27
  whoami The organization and agent you act as
28
+ account [ls] The accounts this machine is connected to
29
+ account switch <name|id> Use another of them for every command after
30
+ account add Connect another account (choose it in the browser)
31
+ audit [--action member.] [--limit 50] [--before <next>]
32
+ The account's audit log (owner or admin keys)
24
33
  usage Balance, trial time and charges
25
34
  limits Whether this key is read-only, and its daily spending limit
35
+ secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
36
+ Store a secret sandboxes use without seeing it; the
37
+ value is read from standard input, never an argument
38
+ secrets ls Names, hosts and placeholders, never values
39
+ secrets rm <NAME> Delete a secret
40
+ keys create [--name <name>] [--read-only] [--daily-limit <usd>]
41
+ A new API key for CI, approved by an owner in the browser
26
42
  referrals Your referral link: you both get up to $500
43
+ compare --from <provider> [--days 30] What your usage would cost at e2b, daytona, vercel,
44
+ modal, cloudflare, fly and others, and what you save
45
+ switch [--from <provider>] Moving from a rival: record it before your first top-up,
46
+ and that top-up is matched, up to $100
27
47
  ls Everything you run, every product
28
48
  feedback "<text>" [--kind bug|missing_feature|competitor_gap|migration_blocker|docs|pricing|praise|other]
29
49
  [--detail <text>] [--competitor <name>] [--request-id <req_...>]
@@ -35,21 +55,42 @@ Account
35
55
  docs [topic] Print a docs page (start, cli, api, python...)
36
56
  mcp Serve MCP on stdio for agents that need it
37
57
 
58
+ Connect
59
+ sandbox ssh <id|name> [-- <command...>] Log in over SSH; \`runtime sandbox ssh help\` for editors
60
+ sandbox ssh config [--install] Make \`ssh <id|name>.runtime\` work (VS Code, JetBrains)
61
+ sandbox port-forward <id|name> <port|local:remote>...
62
+ Forward local ports to ports in a sandbox
63
+
64
+ Observability
65
+ sandbox metrics <id> [--range 1h] CPU and memory over time (15m 1h 6h 24h 7d 30d)
66
+ events [--sandbox <id>] [--type <type>] Lifecycle events, newest first
67
+ webhooks ls | create <url> [--events a,b] | test <id> | deliveries <id>
68
+ | update <id> [--url u] [--events a,b] [--enable|--disable]
69
+ | rotate-secret <id> [--keep <seconds>] | retry <deliveryId> | rm <id>
70
+ Signed lifecycle events POSTed to your URL
71
+ otel ls | create <endpoint> [--header K=V]... [--signals logs,metrics] | flush <id> | rm <id>
72
+ Events and metrics to an OpenTelemetry endpoint
73
+
38
74
  Products
39
75
  sandbox Linux microVMs: exec, shell, files, code, previews, network, desktop
40
76
  snapshot Saved sandboxes, disk and memory, to start or fork from
41
- image Custom images from a Dockerfile, a public image or a package list
77
+ image Custom images from a Dockerfile, any public or private image, or a package list
42
78
  volume Persistent disks to attach to sandboxes
43
79
  Each has its own help: \`runtime sandbox help\`, \`runtime image help\`...
44
80
 
45
81
  Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
82
+ Behind a proxy: HTTPS_PROXY and NO_PROXY are read, on Node and Bun alike.
46
83
  `;
47
84
  const SANDBOX_HELP = `runtime sandbox <command>
48
85
 
86
+ run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
87
+ ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
88
+ port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
49
89
  create [--name n] [--label k=v]... [--vcpu 2] [--memory 4096] [--disk 4096]
50
90
  [--timeout <seconds>] [--on-timeout pause|stop] [--trial|--paid] [--no-wait]
51
91
  [--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
52
- [--image <imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
92
+ [--image <name:tag|imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
93
+ [--idle-pause <seconds>] [--no-auto-wake] [--persistent] [--get-or-create]
53
94
  Create one; prints its id
54
95
  ls [--all] [--label k=v]... [--state running] List (live ones unless --all)
55
96
  get <id> One sandbox, in full
@@ -66,8 +107,11 @@ const SANDBOX_HELP = `runtime sandbox <command>
66
107
  files <id> [path] [--depth 2] [--glob '**/*.py']
67
108
  stop <id> | pause <id> | wake <id> | restart <id>
68
109
  extend <id> <seconds> More time before the lease ends
110
+ update <id> [--name n] [--label k=v]... [--idle-pause <seconds>] [--auto-wake on|off]
111
+ [--persistent on|off] [--max-total-cost <usd>|none]
112
+ Change its settings; persistent keeps it running
69
113
  snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
70
- fork <id> [--count 3] [--name n] [--keep-snapshot]
114
+ fork <id> [--count 3] [--name n] [--keep-snapshot] [--trial|--paid]
71
115
  Copies of it as it is now, running; prints their ids
72
116
  run-code <id> <file|-> [--lang python|javascript] [--context c] [--out-dir .]
73
117
  Run a notebook-style cell; charts saved as PNG
@@ -76,6 +120,8 @@ const SANDBOX_HELP = `runtime sandbox <command>
76
120
  unshare <id> <port> Stop sharing a port
77
121
  network <id> [--no-internet|--internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
78
122
  Show its network rules, or replace them
123
+ metrics <id> [--range 15m|1h|6h|24h|7d|30d]
124
+ CPU and memory over time, and the latest reading
79
125
  desktop <id> start | stop | screenshot [file.png] | open <url> | click <x> <y>
80
126
  | type <text> | press <keys> | windows
81
127
 
@@ -160,7 +206,11 @@ const COMMANDS = [
160
206
  "whoami",
161
207
  "usage",
162
208
  "limits",
209
+ "secrets",
210
+ "keys",
163
211
  "referrals",
212
+ "compare",
213
+ "switch",
164
214
  "ls",
165
215
  "feedback",
166
216
  "support",
@@ -170,10 +220,18 @@ const COMMANDS = [
170
220
  "snapshot",
171
221
  "image",
172
222
  "volume",
223
+ "webhooks",
224
+ "otel",
225
+ "events",
173
226
  "help",
174
227
  "version",
175
228
  ];
229
+ /** Sandbox commands that once sat at the top level. */
230
+ const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
176
231
  const SANDBOX_VERBS = [
232
+ "run",
233
+ "ssh",
234
+ "port-forward",
177
235
  "create",
178
236
  "ls",
179
237
  "get",
@@ -184,6 +242,7 @@ const SANDBOX_VERBS = [
184
242
  "snapshot",
185
243
  "fork",
186
244
  "extend",
245
+ "update",
187
246
  "exec",
188
247
  "spawn",
189
248
  "ps",
@@ -199,6 +258,7 @@ const SANDBOX_VERBS = [
199
258
  "unshare",
200
259
  "network",
201
260
  "desktop",
261
+ "metrics",
202
262
  ];
203
263
  function need(value, what) {
204
264
  if (!value)
@@ -259,6 +319,14 @@ function describe(info) {
259
319
  ["funding", info.funding],
260
320
  ["region", info.region],
261
321
  ["expires", info.expiresAt ?? "-"],
322
+ ["auto wake", info.autoWake === false ? "off" : "on"],
323
+ [
324
+ "idle pause",
325
+ info.idlePauseSeconds
326
+ ? `${info.idlePauseSeconds} s${info.idlePauseUnusedOnly ? " while unused" : " idle"}`
327
+ : "off",
328
+ ],
329
+ ["persistent", info.persistent ? "yes" : "no"],
262
330
  [
263
331
  "labels",
264
332
  Object.entries(info.labels ?? {})
@@ -274,7 +342,16 @@ function remote(spec) {
274
342
  return match ? { id: match[1], path: match[2] } : undefined;
275
343
  }
276
344
  export async function run(argv, env = process.env, out = defaultOut(argv)) {
277
- const [command, ...rest] = argv.filter((arg) => arg !== "--json" || (argv.indexOf("--") !== -1 && argv.indexOf(arg) > argv.indexOf("--")));
345
+ const [first, ...remaining] = argv.filter((arg) => arg !== "--json" || (argv.indexOf("--") !== -1 && argv.indexOf(arg) > argv.indexOf("--")));
346
+ // Sandbox is one product of several, so its commands live under it
347
+ // (AGENTS.md, 23 September 2026): `runtime sandbox run`, `sandbox ssh` and
348
+ // `sandbox port-forward`. The old top-level forms were removed, not aliased.
349
+ if (first && SANDBOX_MOVED.includes(first))
350
+ throw usage(`\`${me} ${first}\` is now \`${me} sandbox ${first === "forward" ? "port-forward" : first}\`.`);
351
+ const nested = (first === "sandbox" || first === "sandboxes" || first === "sbx") &&
352
+ SANDBOX_MOVED.includes(remaining[0] ?? "");
353
+ const command = nested ? remaining[0] : first;
354
+ const rest = nested ? remaining.slice(1) : remaining;
278
355
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
279
356
  product = "";
280
357
  if (!command) {
@@ -287,7 +364,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
287
364
  ? JSON.stringify({ connected, usage: HELP })
288
365
  : `Runtime Cloud ${VERSION}. ${start}
289
366
 
290
- ${me} run -- python3 -c 'print(6*7)' A fresh sandbox runs one command, then stops
367
+ ${me} sandbox run -- python3 -c 'print(6*7)'
368
+ A fresh sandbox runs one command, then stops
291
369
  ${me} sandbox create A sandbox that stays; prints its id
292
370
  ${me} sandbox exec <id> -- <command> Run in it
293
371
  ${me} help Everything else
@@ -302,7 +380,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
302
380
  const args = parse(rest, [...CREATE_FLAGS, "keep"]);
303
381
  const commandLine = args.rest ?? args.positional;
304
382
  if (!commandLine.length)
305
- throw usage(`Give the command after --: ${me} run -- python3 -c 'print(6*7)'`);
383
+ throw usage(`Give the command after --: ${me} sandbox run -- python3 -c 'print(6*7)'`);
306
384
  const runtime = await client(env);
307
385
  const timeout = integer(args, "timeout");
308
386
  const { timeoutSeconds: _lease, ...input } = createInput(args);
@@ -400,6 +478,60 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
400
478
  ]), l);
401
479
  return 0;
402
480
  }
481
+ if (command === "secrets")
482
+ return secretsCommand(rest, env, out);
483
+ if (command === "webhooks" || command === "webhook")
484
+ return webhooksCommand(rest, env, out);
485
+ if (command === "otel")
486
+ return otelCommand(rest, env, out);
487
+ if (command === "events") {
488
+ const args = parse(rest);
489
+ const page = await (await client(env)).events.list({
490
+ ...(flag(args, "sandbox") ? { resourceId: flag(args, "sandbox") } : {}),
491
+ ...(flag(args, "type") ? { type: flag(args, "type") } : {}),
492
+ ...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
493
+ });
494
+ print(page.data.length
495
+ ? table(page.data.map((e) => {
496
+ const subject = (Object.values(e.data)[0] ?? {});
497
+ return [e.createdAt, e.type, e.resourceId ?? "-", subject.stopReason ?? ""];
498
+ }))
499
+ : "No events in the last 14 days.", page);
500
+ return 0;
501
+ }
502
+ if (command === "keys")
503
+ return keys(rest, env, out);
504
+ if (command === "account" || command === "accounts")
505
+ return accountCommand(rest, env, out);
506
+ if (command === "audit") {
507
+ const args = parse(rest);
508
+ const known = new Set(["action", "limit", "before"]);
509
+ for (const name of args.flags.keys())
510
+ if (!known.has(name))
511
+ throw usage(`Unknown option --${name}.`);
512
+ const page = await (await client(env)).audit.list({
513
+ ...(flag(args, "action") ? { action: flag(args, "action") } : {}),
514
+ ...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
515
+ ...(flag(args, "before") ? { before: flag(args, "before") } : {}),
516
+ });
517
+ print(page.events.length
518
+ ? [
519
+ table([
520
+ ["when (UTC)", "action", "who", "from"],
521
+ ...page.events.map((event) => [
522
+ event.at.slice(0, 19).replace("T", " "),
523
+ event.action,
524
+ event.actor.kind === "runtime"
525
+ ? "Runtime"
526
+ : `${event.actor.name ?? event.actor.kind}${event.actor.person ? ` (${event.actor.person})` : ""}`,
527
+ event.ip ?? event.via,
528
+ ]),
529
+ ]),
530
+ ...(page.next ? ["", `Older: ${me} audit --before ${page.next}`] : []),
531
+ ].join("\n")
532
+ : "Nothing recorded yet.", page);
533
+ return 0;
534
+ }
403
535
  if (command === "referrals") {
404
536
  const r = await (await client(env)).referrals.get();
405
537
  const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
@@ -421,6 +553,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
421
553
  ].join("\n"), r);
422
554
  return 0;
423
555
  }
556
+ if (command === "compare" || command === "switch")
557
+ return switching(command, rest, env, out);
424
558
  if (command === "mcp") {
425
559
  const { serveMcp } = await import("./mcp.js");
426
560
  await serveMcp(env);
@@ -428,7 +562,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
428
562
  }
429
563
  if (command === "docs") {
430
564
  const topic = rest[0] ?? "index";
431
- const response = await fetch(`https://withruntime.com/docs/${topic === "index" ? "start" : encodeURIComponent(topic)}.md`, { redirect: "error" });
565
+ const response = await envFetch(`https://withruntime.com/docs/${topic === "index" ? "start" : encodeURIComponent(topic)}.md`, { redirect: "error" });
432
566
  if (!response.ok)
433
567
  throw usage(`No docs page named ${topic}. Try start, cli, api, javascript, python, mcp.`);
434
568
  out.write(await response.text());
@@ -494,6 +628,14 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
494
628
  : "Nothing yet. Start with: runtime sandbox create", items);
495
629
  return 0;
496
630
  }
631
+ if (command === "ssh") {
632
+ product = "ssh";
633
+ return sshCommand(rest, env, out, () => client(env), me);
634
+ }
635
+ if (command === "port-forward" || command === "forward") {
636
+ product = "ssh";
637
+ return portForwardCommand(rest, out, () => client(env), me);
638
+ }
497
639
  if (command === "sandbox" || command === "sandboxes" || command === "sbx") {
498
640
  product = "sandbox";
499
641
  return sandbox(rest, env, out);
@@ -504,7 +646,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
504
646
  }
505
647
  if (command === "image" || command === "images") {
506
648
  product = "image";
507
- return image(rest, env, out);
649
+ return imageCommand(rest, env, out);
508
650
  }
509
651
  if (command === "volume" || command === "volumes") {
510
652
  product = "volume";
@@ -512,6 +654,121 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
512
654
  }
513
655
  throw unknown("command", command, COMMANDS);
514
656
  }
657
+ /** Dollars from integer microdollars: cents from a dollar up, and four
658
+ * places below one, so a few minutes of use does not round to $0.01. */
659
+ function dollars(micros) {
660
+ const value = BigInt(micros);
661
+ const sign = value < 0n ? "-" : "";
662
+ const size = value < 0n ? -value : value;
663
+ const places = size < 1000000n ? 4 : 2;
664
+ const unit = 10n ** BigInt(6 - places);
665
+ const rounded = (size + unit / 2n) / unit;
666
+ const whole = rounded / 10n ** BigInt(places);
667
+ const part = (rounded % 10n ** BigInt(places)).toString().padStart(places, "0");
668
+ return `${sign}$${whole.toLocaleString("en-US")}.${part}`;
669
+ }
670
+ const SWITCH_FROM = {
671
+ e2b: "e2b",
672
+ daytona: "daytona",
673
+ vercel: "vercel",
674
+ modal: "modal",
675
+ cloudflare: "cloudflare",
676
+ fly: "fly",
677
+ "fly-machines": "fly",
678
+ };
679
+ const RIVAL_NAMES = {
680
+ e2b: "E2B",
681
+ daytona: "Daytona",
682
+ vercel: "Vercel Sandbox",
683
+ modal: "Modal",
684
+ cloudflare: "Cloudflare Sandbox",
685
+ fly: "Fly",
686
+ };
687
+ /** One line on the switching credit: how to claim it, or what it did. */
688
+ function switchingLine(summary, provider) {
689
+ const cap = dollars(summary.maxMicros);
690
+ const sw = summary.switch;
691
+ if (!sw) {
692
+ if (!summary.eligible || !provider)
693
+ return undefined;
694
+ return `Switching from ${RIVAL_NAMES[provider]}? Run \`${me} switch --from ${provider}\` before your first top-up and we match that top-up, up to ${cap}.`;
695
+ }
696
+ const from = RIVAL_NAMES[sw.provider];
697
+ switch (sw.status) {
698
+ case "pending":
699
+ return `Your switch from ${from} is recorded: your first top-up is matched, up to ${cap}, once the payment settles.`;
700
+ case "paid":
701
+ return `Your switch from ${from} matched your first top-up: ${dollars(sw.creditMicros)} of credit.`;
702
+ case "superseded":
703
+ return `Your first top-up was matched by your referral instead, which is never less than the switching credit.`;
704
+ case "reversed":
705
+ return `The top-up your switching credit matched was refunded, so the credit was taken back.`;
706
+ case "refused":
707
+ return sw.refusedReason === "program_off"
708
+ ? "The switching credit was paused when your first top-up arrived."
709
+ : "Your first top-up was not matched. Ask support if you think that is wrong.";
710
+ }
711
+ }
712
+ async function switching(command, argv, env, out) {
713
+ const args = parse(argv);
714
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
715
+ const known = new Set(command === "compare" ? ["from", "days"] : ["from"]);
716
+ for (const name of args.flags.keys())
717
+ if (!known.has(name))
718
+ throw usage(`Unknown option --${name}.`);
719
+ if (args.positional.length || args.rest)
720
+ throw usage(`Unexpected ${args.positional[0] ?? "--"}.`);
721
+ const from = flag(args, "from")?.trim().toLowerCase();
722
+ const api = (await client(env)).switching;
723
+ if (command === "switch") {
724
+ if (!from) {
725
+ const summary = await api.get();
726
+ print(switchingLine(summary) ??
727
+ (summary.eligible
728
+ ? `Moving from a rival? Run \`${me} switch --from <provider>\` (${summary.providers.join(", ")}) before your first top-up, and that top-up is matched, up to ${dollars(summary.maxMicros)}.`
729
+ : "The switching credit matches a first top-up, and this organization has made one already."), summary);
730
+ return 0;
731
+ }
732
+ const provider = SWITCH_FROM[from];
733
+ if (!provider || from === "fly-machines")
734
+ throw unknown("provider", from, Object.keys(RIVAL_NAMES));
735
+ const summary = await api.record({ provider });
736
+ print(`Recorded: switching from ${RIVAL_NAMES[provider]}. ${switchingLine(summary) ?? ""}`.trim(), summary);
737
+ return 0;
738
+ }
739
+ if (!from)
740
+ throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines...).`);
741
+ /* The server holds the list of rivals it has rates for, and names them all
742
+ when it refuses one, so a rival added there needs no new CLI. */
743
+ const days = integer(args, "days");
744
+ const c = await api.compare({ provider: from, days });
745
+ const saving = BigInt(c.savingMicros);
746
+ const percent = c.savingPercent === null ? "" : ` (${Math.abs(c.savingPercent)}%)`;
747
+ const verdict = saving >= 0n
748
+ ? `you save ${dollars(saving)}${percent}`
749
+ : `${c.rival.name} would cost ${dollars(-saving)} less${percent}`;
750
+ const figures = `${dollars(c.runtimeMicros)} on Runtime; the same sandboxes on ${c.rival.name}: ${dollars(c.rivalMicros)}; ${verdict}`;
751
+ const month = c.perMonth ? BigInt(c.perMonth.savingMicros) : undefined;
752
+ const lines = c.basis === "usage"
753
+ ? [
754
+ `Your last ${c.window.days} days: ${figures}${month === undefined
755
+ ? "."
756
+ : month >= 0n
757
+ ? `, about ${dollars(month)} a month.`
758
+ : `, about ${dollars(-month)} a month more on Runtime.`}`,
759
+ `Priced: ${c.usage.sandboxes.toLocaleString("en-US")} sandbox${c.usage.sandboxes === 1 ? "" : "es"}, ${(c.usage.runSeconds / 3600).toLocaleString("en-US", { maximumFractionDigits: 2 })} hours running.${c.usage.unpricedSandboxes ? ` ${c.usage.unpricedSandboxes} left out: ${c.rival.name} publishes no price for their size.` : ""}`,
760
+ ]
761
+ : [
762
+ `No settled sandboxes in your last ${c.window.days} days yet, so this prices an example: ${c.usage.sandboxes.toLocaleString("en-US")} runs of ${(c.usage.runSeconds / c.usage.sandboxes).toLocaleString("en-US")} seconds using ${(c.usage.activeCpuSeconds / c.usage.sandboxes).toLocaleString("en-US")} CPU-seconds each.`,
763
+ `The example: ${figures}.`,
764
+ ];
765
+ lines.push(`${c.rival.name}'s published rates, checked ${new Date(`${c.rival.checked}T12:00:00Z`).toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" })}: ${c.rival.rates.join("; ")}. Compute only: storage, network, plan fees and free allowances are left out.`);
766
+ const line = switchingLine(c.switching, Object.hasOwn(SWITCH_FROM, from) ? SWITCH_FROM[from] : undefined);
767
+ if (line)
768
+ lines.push("", line);
769
+ print(lines.join("\n"), c);
770
+ return 0;
771
+ }
515
772
  async function support(argv, env, out) {
516
773
  const args = parse(argv);
517
774
  const api = (await client(env)).support;
@@ -622,16 +879,55 @@ async function sandbox(argv, env, out) {
622
879
  return 0;
623
880
  }
624
881
  case "fork": {
625
- const args = parse(rest, ["keep-snapshot"]);
882
+ const args = parse(rest, ["keep-snapshot", "trial", "paid"]);
626
883
  const sbx = await get(args.positional[0]);
627
884
  const forks = await sbx.fork({
628
885
  count: integer(args, "count") ?? 1,
629
886
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
630
887
  ...(has(args, "keep-snapshot") ? { keepSnapshot: true } : {}),
888
+ ...(has(args, "trial")
889
+ ? { funding: "trial" }
890
+ : has(args, "paid")
891
+ ? { funding: "paid" }
892
+ : {}),
631
893
  });
632
894
  print(forks.map((f) => f.id).join("\n"), forks.map((f) => f.info));
633
895
  return 0;
634
896
  }
897
+ case "update": {
898
+ const args = parse(rest);
899
+ const sbx = await get(args.positional[0]);
900
+ const onOff = (name) => {
901
+ const value = flag(args, name);
902
+ if (value === undefined)
903
+ return undefined;
904
+ if (value !== "on" && value !== "off")
905
+ throw usage(`--${name} takes on or off.`);
906
+ return value === "on";
907
+ };
908
+ const cost = flag(args, "max-total-cost");
909
+ if (cost !== undefined && cost !== "none" && !/^\d+(\.\d{1,6})?$/.test(cost))
910
+ throw usage("--max-total-cost takes dollars, such as 25 or 2.50, or none.");
911
+ const settings = {
912
+ ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
913
+ ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
914
+ ...(integer(args, "idle-pause") !== undefined
915
+ ? { idlePauseSeconds: integer(args, "idle-pause") }
916
+ : {}),
917
+ ...(onOff("auto-wake") === undefined ? {} : { autoWake: onOff("auto-wake") }),
918
+ ...(onOff("persistent") === undefined ? {} : { persistent: onOff("persistent") }),
919
+ ...(cost === undefined
920
+ ? {}
921
+ : {
922
+ maxTotalCostMicros: cost === "none" ? null : Math.round(Number(cost) * 1e6),
923
+ }),
924
+ };
925
+ if (!Object.keys(settings).length)
926
+ throw usage("Say what to change: --name, --label, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
927
+ await sbx.update(settings);
928
+ print(describe(sbx.info), sbx.info);
929
+ return 0;
930
+ }
635
931
  case "extend": {
636
932
  const sbx = await get(rest[0]);
637
933
  const seconds = Number(need(rest[1], "seconds, e.g. runtime sandbox extend <id> 600"));
@@ -785,6 +1081,16 @@ async function sandbox(argv, env, out) {
785
1081
  }
786
1082
  case "desktop":
787
1083
  return desktop(await get(rest[0]), rest.slice(1), out);
1084
+ case "metrics": {
1085
+ const args = parse(rest);
1086
+ const sbx = await get(args.positional[0]);
1087
+ const range = (flag(args, "range") ?? "1h");
1088
+ if (!["15m", "1h", "6h", "24h", "7d", "30d"].includes(range))
1089
+ throw usage("--range is one of 15m, 1h, 6h, 24h, 7d, 30d.");
1090
+ const m = await sbx.metrics({ range });
1091
+ print(metricsText(m), m);
1092
+ return 0;
1093
+ }
788
1094
  }
789
1095
  throw unknown("sandbox command", verb, SANDBOX_VERBS);
790
1096
  }
@@ -795,6 +1101,109 @@ function attachment(spec) {
795
1101
  throw usage("--volume takes <volumeId>:/path, or <volumeId>:/path:snapshot for a copy.");
796
1102
  return { volumeId, path, mode };
797
1103
  }
1104
+ /** A secret's value, from standard input only, so it never sits in shell
1105
+ * history or a process list. Piped: everything, less one trailing newline.
1106
+ * A terminal: one line, not shown. */
1107
+ async function readSecret() {
1108
+ const stdin = process.stdin;
1109
+ if (!stdin.isTTY) {
1110
+ const chunks = [];
1111
+ for await (const chunk of stdin)
1112
+ chunks.push(chunk);
1113
+ return Buffer.concat(chunks)
1114
+ .toString("utf8")
1115
+ .replace(/\r?\n$/, "");
1116
+ }
1117
+ process.stderr.write("Value (not shown), then Enter: ");
1118
+ stdin.setRawMode(true);
1119
+ stdin.resume();
1120
+ try {
1121
+ return await new Promise((done, fail) => {
1122
+ let value = "";
1123
+ const onData = (chunk) => {
1124
+ for (const char of chunk.toString("utf8")) {
1125
+ if (char === "\r" || char === "\n") {
1126
+ stdin.off("data", onData);
1127
+ process.stderr.write("\n");
1128
+ return done(value);
1129
+ }
1130
+ if (char === "\u0003") {
1131
+ stdin.off("data", onData);
1132
+ return fail(usage("Cancelled."));
1133
+ }
1134
+ if (char === "\u007f" || char === "\b")
1135
+ value = value.slice(0, -1);
1136
+ else
1137
+ value += char;
1138
+ }
1139
+ };
1140
+ stdin.on("data", onData);
1141
+ });
1142
+ }
1143
+ finally {
1144
+ stdin.setRawMode(false);
1145
+ stdin.pause();
1146
+ }
1147
+ }
1148
+ export async function secretsCommand(argv, env, out,
1149
+ /** Tests give the value here; the CLI reads standard input. */
1150
+ read = readSecret) {
1151
+ const [verb, ...rest] = argv;
1152
+ const args = parse(rest);
1153
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
1154
+ const rows = (list) => list.length
1155
+ ? table([
1156
+ ["name", "hosts", "header", "placeholder"],
1157
+ ...list.map((secret) => [
1158
+ secret.name,
1159
+ secret.hosts.join(","),
1160
+ secret.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
1161
+ secret.placeholder,
1162
+ ]),
1163
+ ])
1164
+ : "No secrets.";
1165
+ if (verb === "ls" || verb === "list" || verb === undefined) {
1166
+ const list = await (await client(env)).secrets.list();
1167
+ print(rows(list), list);
1168
+ return 0;
1169
+ }
1170
+ if (verb === "set") {
1171
+ const name = need(args.positional[0], "the secret's name, such as OPENAI_API_KEY");
1172
+ const hosts = args.flags.get("host") ?? [];
1173
+ if (!hosts.length)
1174
+ throw usage("Give --host once for each host the value may go to, such as --host api.openai.com.");
1175
+ if (args.flags.has("value"))
1176
+ throw usage('The value is read from standard input, never an argument: printf %s "$KEY" | runtime secrets set NAME --host ...');
1177
+ const value = await read();
1178
+ if (!value)
1179
+ throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
1180
+ const header = flag(args, "header");
1181
+ const format = flag(args, "format");
1182
+ const saved = await (await client(env)).secrets.set(name, {
1183
+ value,
1184
+ hosts,
1185
+ ...(header ? { header } : {}),
1186
+ ...(format ? { format } : {}),
1187
+ });
1188
+ print([
1189
+ `Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
1190
+ saved.header
1191
+ ? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
1192
+ : `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the URL and headers.`,
1193
+ ...(saved.enforced
1194
+ ? []
1195
+ : ["A host is catching up; running sandboxes have it within a minute."]),
1196
+ ].join("\n"), saved);
1197
+ return 0;
1198
+ }
1199
+ if (verb === "rm" || verb === "delete") {
1200
+ const name = need(args.positional[0], "the secret's name");
1201
+ const removed = await (await client(env)).secrets.delete(name);
1202
+ print(`Deleted ${name}.`, removed);
1203
+ return 0;
1204
+ }
1205
+ throw usage("secrets takes set, ls or rm. Run `runtime help`.");
1206
+ }
798
1207
  async function readInput(source) {
799
1208
  if (source !== "-")
800
1209
  return readFile(source, "utf8");
@@ -897,6 +1306,141 @@ async function desktop(sbx, argv, out) {
897
1306
  }
898
1307
  throw usage(`${me} sandbox desktop <id> start|stop|screenshot|open|click|type|press|windows`);
899
1308
  }
1309
+ const KEYS_HELP = `runtime keys <command>
1310
+
1311
+ create [--name <name>] [--read-only] [--daily-limit <usd>]
1312
+ A new API key, for a CI runner or any place with no browser. Prints a link
1313
+ and a code; an account owner approves the key in the browser, seeing its
1314
+ name, access and limit. The key is printed once, alone on stdout, and
1315
+ lasts until revoked. --json prints it as JSON, still alone on stdout.
1316
+
1317
+ runtime keys create --name ci --daily-limit 25 | gh secret set RUNTIME_API_KEY
1318
+
1319
+ List, limit and revoke keys at https://withruntime.com/account/keys.
1320
+ `;
1321
+ /* `runtime keys create`: a key made from the terminal and approved in the
1322
+ browser. Marc, 23 September 2026; ARCHITECTURE.md section 10, "Keys from the
1323
+ terminal". Only the options the keys page offers. No key can list, create or
1324
+ revoke keys through the API, so `ls` and `revoke` send you to the page. */
1325
+ /* `runtime account`: the accounts this machine is connected to, and which one
1326
+ every command uses (ARCHITECTURE.md section 3.9). Each is its own saved
1327
+ connection with its own key; the SDK itself stays key-based. */
1328
+ async function accountCommand(argv, env, out) {
1329
+ product = "account";
1330
+ const [verb = "ls", ...rest] = argv;
1331
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
1332
+ const store = connectionStore(env);
1333
+ if (verb === "add") {
1334
+ const result = await authenticationCommand(["login", "--another-account", ...rest], env);
1335
+ if (result.pending)
1336
+ throw notApprovedYet(result);
1337
+ await nameSaved(env);
1338
+ print(`Connected${"orgId" in result && result.orgId ? ` to ${String(result.orgId)}` : ""}.`, result);
1339
+ return 0;
1340
+ }
1341
+ if (verb === "switch" || verb === "use") {
1342
+ const which = rest.join(" ").trim();
1343
+ if (!which)
1344
+ throw usage(`Name the account: ${me} account switch <name|id>.`);
1345
+ if (env.RUNTIME_API_KEY)
1346
+ throw usage("RUNTIME_API_KEY is set, and it names one account. Unset it to switch.");
1347
+ const chosen = await store.use(which);
1348
+ if (!chosen)
1349
+ throw usage(`No saved connection for "${which}". Run \`${me} account\` to list them, or \`${me} account add\` to connect it.`);
1350
+ print(`Now using ${chosen.orgName ?? chosen.orgId}.`, {
1351
+ orgId: chosen.orgId,
1352
+ orgName: chosen.orgName ?? null,
1353
+ });
1354
+ return 0;
1355
+ }
1356
+ if (verb !== "ls" && verb !== "list")
1357
+ throw unknown("account command", verb, ["ls", "switch", "add"]);
1358
+ await nameSaved(env).catch(() => undefined);
1359
+ const current = await store.read().catch(() => null);
1360
+ const saved = [
1361
+ ...(current ? [{ ...current, current: true }] : []),
1362
+ ...(await store.others()).map((one) => ({ ...one, current: false })),
1363
+ ];
1364
+ const rows = saved.map((one) => ({
1365
+ orgId: one.orgId,
1366
+ orgName: one.orgName ?? null,
1367
+ agentName: one.agentName,
1368
+ current: one.current,
1369
+ }));
1370
+ print(rows.length
1371
+ ? table([
1372
+ ["", "account", "id", "agent"],
1373
+ ...rows.map((row) => [
1374
+ row.current ? "*" : "",
1375
+ row.orgName ?? "-",
1376
+ row.orgId,
1377
+ row.agentName,
1378
+ ]),
1379
+ ])
1380
+ : `Not connected. Run \`${me} login\`.`, { accounts: rows, environmentKey: Boolean(env.RUNTIME_API_KEY) });
1381
+ return 0;
1382
+ }
1383
+ /** Fill in the saved connection's account name from the API, once. */
1384
+ async function nameSaved(env) {
1385
+ if (env.RUNTIME_API_KEY)
1386
+ return;
1387
+ const store = connectionStore(env);
1388
+ const saved = await store.read();
1389
+ if (!saved || saved.orgName)
1390
+ return;
1391
+ const who = await new Runtime({
1392
+ apiKey: saved.key,
1393
+ baseUrl: env.RUNTIME_API_URL,
1394
+ maxRetries: 1,
1395
+ }).me();
1396
+ if (who.orgName && who.orgId === saved.orgId) {
1397
+ const again = await store.read();
1398
+ if (again && again.key === saved.key)
1399
+ await store.save({ ...again, orgName: who.orgName });
1400
+ }
1401
+ }
1402
+ async function keys(argv, env, out) {
1403
+ product = "keys";
1404
+ const [verb, ...rest] = argv;
1405
+ if (!verb || verb === "help" || verb === "--help" || verb === "-h") {
1406
+ out.write(out.json ? JSON.stringify({ usage: named(KEYS_HELP) }) : named(KEYS_HELP));
1407
+ return 0;
1408
+ }
1409
+ if (verb === "ls" || verb === "list" || verb === "revoke")
1410
+ throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
1411
+ if (verb !== "create")
1412
+ throw unknown("keys command", verb, ["create"]);
1413
+ const args = parse(rest, ["read-only"]);
1414
+ const known = new Set(["name", "read-only", "daily-limit"]);
1415
+ for (const name of args.flags.keys())
1416
+ if (!known.has(name))
1417
+ throw usage(`Unknown option --${name}.`);
1418
+ if (args.positional.length || args.rest)
1419
+ throw usage(`Unexpected ${args.positional[0] ?? "--"}.`);
1420
+ const readOnly = has(args, "read-only");
1421
+ const limit = flag(args, "daily-limit");
1422
+ if (readOnly && limit !== undefined)
1423
+ throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
1424
+ const created = await createKey({
1425
+ name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
1426
+ access: readOnly ? "read" : "full",
1427
+ dailyLimitMicros: limit === undefined ? null : dailyLimitMicros(limit),
1428
+ }, env, { notify: out.error });
1429
+ const { confirm, ...key } = created;
1430
+ out.write(out.json ? JSON.stringify(key) : key.key);
1431
+ const terms = [
1432
+ key.access === "read" ? "read only" : "full access",
1433
+ key.access === "read"
1434
+ ? undefined
1435
+ : key.dailyLimitMicros === null
1436
+ ? "no daily limit"
1437
+ : `daily limit $${(Number(BigInt(key.dailyLimitMicros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
1438
+ "until revoked",
1439
+ ].filter(Boolean);
1440
+ out.error(`Created key "${key.name}" (${terms.join(", ")}). Store it now: it is not shown again, and Runtime keeps only a hash of it.\nIn CI, give it to your jobs as RUNTIME_API_KEY from the secret store.`);
1441
+ await confirm();
1442
+ return 0;
1443
+ }
900
1444
  const SNAPSHOT_HELP = `runtime snapshot <command>
901
1445
 
902
1446
  ls [--sandbox <id>] [--name n] Your snapshots
@@ -953,18 +1497,34 @@ async function snapshot(argv, env, out) {
953
1497
  }
954
1498
  const IMAGE_HELP = `runtime image <command>
955
1499
 
956
- build (--dockerfile <path> | --from <public image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...)
957
- [--name n] [--max-image-mib 4096] [--timeout <seconds>]
1500
+ build [<folder>] [-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
1501
+ [-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
1502
+ [--start <command>] [--ready-port <port> | --ready-command <command>]
1503
+ [--max-image-mib 8192] [--timeout <seconds>]
958
1504
  Build and stream the log; prints the image id when ready.
959
- A Dockerfile's COPY and ADD sources are sent from its folder
960
- (at most 256 files, 1 MiB in all).
1505
+ A Dockerfile's folder is the context: everything its
1506
+ .dockerignore leaves in (100 MiB compressed at most) is
1507
+ sent, and a rebuild sends only what changed.
961
1508
  ls Your images
962
- get <id> One image
963
- logs <id> Its build log
964
- rm <id> Delete it
965
- Use one: runtime sandbox create --image <id>
1509
+ versions <name> Every version of a name, newest first
1510
+ get <image> One image: an id, name, name:tag or name@version
1511
+ logs <image> [--follow]
1512
+ Its build log
1513
+ tag <image> <tag> Point a tag of the image's name at this version
1514
+ untag <image> <tag> Take a tag off
1515
+ rm <image> Delete that version and its tags
1516
+ registry ls Registries you keep credentials for
1517
+ registry set <registry> --username <user>
1518
+ Store credentials to pull private images; the token or
1519
+ password is read from standard input and never shown again
1520
+ registry set <ecr registry> --access-key-id <id>
1521
+ Amazon ECR: the secret access key is read from standard input
1522
+ registry rm <registry>
1523
+ Use one: runtime sandbox create --image app:latest
966
1524
  `;
967
- async function image(argv, env, out) {
1525
+ export async function imageCommand(argv, env, out,
1526
+ /** Tests give the registry secret here; the CLI reads standard input. */
1527
+ read = readSecret) {
968
1528
  const [verb, ...rest] = argv;
969
1529
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
970
1530
  if (!verb || verb === "help" || verb === "--help") {
@@ -974,30 +1534,72 @@ async function image(argv, env, out) {
974
1534
  const api = (await client(env)).images;
975
1535
  switch (verb) {
976
1536
  case "build": {
977
- const args = parse(rest);
1537
+ const args = parse(rest, ["no-cache"]);
978
1538
  const list = (name) => (args.flags.get(name) ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean);
979
1539
  const recipe = { apt: list("apt"), pip: list("pip"), npm: list("npm") };
980
1540
  const hasRecipe = recipe.apt.length + recipe.pip.length + recipe.npm.length > 0;
981
- const sources = [flag(args, "dockerfile"), flag(args, "from"), hasRecipe || undefined];
1541
+ const dockerfile = flag(args, "dockerfile") ?? flag(args, "file") ?? flag(args, "f");
1542
+ const folder = args.positional[0];
1543
+ const fromDockerfile = dockerfile !== undefined || folder !== undefined;
1544
+ const sources = [fromDockerfile || undefined, flag(args, "from"), hasRecipe || undefined];
982
1545
  if (sources.filter((s) => s !== undefined).length !== 1)
983
- throw usage("Give exactly one of --dockerfile <path>, --from <image>, or --pip/--apt/--npm.");
1546
+ throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
984
1547
  const build = {
985
1548
  ...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
986
1549
  ...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
987
1550
  };
1551
+ // -t app, -t app:v2 -t app:latest: one name, its tags.
1552
+ let name = flag(args, "name");
1553
+ const tags = [];
1554
+ for (const tagged of [...(args.flags.get("t") ?? []), ...(args.flags.get("tag") ?? [])]) {
1555
+ const [repository, tag] = tagged.split(":");
1556
+ if (!repository)
1557
+ throw usage("-t takes name or name:tag.");
1558
+ if (name !== undefined && name !== repository)
1559
+ throw usage("Every -t names the same image; give one name and several tags.");
1560
+ name = repository;
1561
+ if (tag)
1562
+ tags.push(tag);
1563
+ }
1564
+ const start = {
1565
+ ...(flag(args, "start") ? { command: flag(args, "start") } : {}),
1566
+ ...(integer(args, "ready-port") ? { readyPort: integer(args, "ready-port") } : {}),
1567
+ ...(flag(args, "ready-command") ? { readyCommand: flag(args, "ready-command") } : {}),
1568
+ ...(integer(args, "ready-timeout")
1569
+ ? { readyTimeoutSeconds: integer(args, "ready-timeout") }
1570
+ : {}),
1571
+ };
988
1572
  const common = {
989
- ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
1573
+ ...(name ? { name } : {}),
1574
+ ...(tags.length ? { tags } : {}),
990
1575
  ...(Object.keys(build).length ? { build } : {}),
1576
+ ...(Object.keys(start).length ? { start } : {}),
1577
+ ...(has(args, "no-cache") ? { cache: false } : {}),
991
1578
  };
992
- const dockerfile = flag(args, "dockerfile");
993
- const input = dockerfile
994
- ? { ...common, ...(await dockerContext(dockerfile)) }
995
- : flag(args, "from")
996
- ? { ...common, image: flag(args, "from") }
997
- : {
998
- ...common,
999
- recipe: Object.fromEntries(Object.entries(recipe).filter(([, v]) => v.length)),
1000
- };
1579
+ let input;
1580
+ if (fromDockerfile) {
1581
+ const context = resolve(folder ?? dirname(dockerfile));
1582
+ const file = resolve(context, dockerfile ?? "Dockerfile");
1583
+ const text = await readFile(file, "utf8").catch(() => {
1584
+ throw usage(`No Dockerfile at ${file}.`);
1585
+ });
1586
+ input = {
1587
+ ...common,
1588
+ dockerfile: text,
1589
+ contextDir: context,
1590
+ ...(flag(args, "target") ? { target: flag(args, "target") } : {}),
1591
+ ...(args.flags.get("build-arg") ? { buildArgs: pairs(args, "build-arg") } : {}),
1592
+ };
1593
+ if (!out.json)
1594
+ out.error(`Sending the build context from ${context}`);
1595
+ }
1596
+ else if (flag(args, "from"))
1597
+ input = { ...common, image: flag(args, "from") };
1598
+ else
1599
+ input = {
1600
+ ...common,
1601
+ recipe: Object.fromEntries(Object.entries(recipe).filter(([, v]) => v.length)),
1602
+ };
1001
1603
  const built = await api.build(input, {
1002
1604
  ...(out.json ? {} : { onLog: (line) => out.error(line.text) }),
1003
1605
  });
@@ -1005,29 +1607,43 @@ async function image(argv, env, out) {
1005
1607
  return 0;
1006
1608
  }
1007
1609
  case "ls":
1008
- case "list": {
1009
- const all = await (await api.list()).toArray(1000);
1610
+ case "list":
1611
+ case "versions": {
1612
+ const name = verb === "versions" ? need(rest[0], "a name") : undefined;
1613
+ const all = await (await api.list(name ? { name } : {})).toArray(1000);
1010
1614
  print(all.length
1011
1615
  ? table([
1012
- ["ID", "NAME", "STATE", "MIB", "CREATED"],
1616
+ ["ID", "NAME", "VERSION", "TAGS", "STATE", "MIB", "CREATED"],
1013
1617
  ...all.map((i) => [
1014
1618
  i.id,
1015
1619
  i.name ?? "-",
1620
+ i.version === null || i.version === undefined ? "-" : String(i.version),
1621
+ i.tags?.length ? i.tags.join(",") : "-",
1016
1622
  i.state,
1017
1623
  String(i.sizeMiB ?? "-"),
1018
1624
  i.createdAt,
1019
1625
  ]),
1020
1626
  ])
1021
- : "No images. Build one: runtime image build --pip pandas", all);
1627
+ : name
1628
+ ? `No versions of ${name}.`
1629
+ : "No images. Build one: runtime image build --pip pandas", all);
1022
1630
  return 0;
1023
1631
  }
1024
1632
  case "get": {
1025
- const found = await api.get(need(rest[0], "an image id"));
1633
+ const found = await api.resolve(need(rest[0], "an image"));
1026
1634
  print(JSON.stringify(found, null, 2), found);
1027
1635
  return 0;
1028
1636
  }
1029
1637
  case "logs": {
1030
- const id = need(rest[0], "an image id");
1638
+ const args = parse(rest, ["follow", "f"]);
1639
+ const id = (await api.resolve(need(args.positional[0], "an image"))).id;
1640
+ if (has(args, "follow") || has(args, "f")) {
1641
+ const lines = [];
1642
+ const done = await api.followLogs(id, (line) => out.json ? lines.push(line.text) : out.write(line.text));
1643
+ if (out.json)
1644
+ print("", { lines, state: done.state });
1645
+ return done.state === "failed" ? 1 : 0;
1646
+ }
1031
1647
  let after = 0;
1032
1648
  const lines = [];
1033
1649
  for (;;) {
@@ -1040,76 +1656,69 @@ async function image(argv, env, out) {
1040
1656
  print(lines.join("\n") || "(no log yet)", lines);
1041
1657
  return 0;
1042
1658
  }
1659
+ case "tag":
1660
+ case "untag": {
1661
+ const ref = need(rest[0], "an image");
1662
+ const tag = need(rest[1], "a tag");
1663
+ const found = verb === "tag" ? await api.tag(ref, tag) : await api.untag(ref, tag);
1664
+ print(`${found.name ?? found.id}@${found.version ?? "-"} tags: ${found.tags.join(", ") || "none"}`, found);
1665
+ return 0;
1666
+ }
1043
1667
  case "rm":
1044
1668
  case "delete": {
1045
- const found = await api.delete(need(rest[0], "an image id"));
1669
+ const found = await api.delete(need(rest[0], "an image"));
1046
1670
  print(`${found.id} ${found.state}`, found);
1047
1671
  return 0;
1048
1672
  }
1049
- }
1050
- throw unknown("image command", verb, ["build", "ls", "get", "logs", "rm"]);
1051
- }
1052
- /** The Dockerfile and the files its COPY and ADD lines name, from its folder. */
1053
- async function dockerContext(path) {
1054
- const dockerfile = await readFile(path, "utf8");
1055
- const root = dirname(resolve(path));
1056
- const sources = new Set();
1057
- for (const line of dockerfile.replace(/\\\r?\n/g, " ").split(/\r?\n/)) {
1058
- const match = /^\s*(?:COPY|ADD)\s+(.+)$/i.exec(line);
1059
- if (!match || /--from=/i.test(match[1]))
1060
- continue;
1061
- const body = match[1].trim();
1062
- const words = body.startsWith("[")
1063
- ? JSON.parse(body)
1064
- : body.split(/\s+/).filter((word) => !word.startsWith("--"));
1065
- for (const word of words.slice(0, -1))
1066
- if (!/^https?:\/\//.test(word))
1067
- sources.add(word);
1068
- }
1069
- const files = [];
1070
- let total = 0;
1071
- const add = async (absolute) => {
1072
- const info = await stat(absolute);
1073
- if (info.isDirectory()) {
1074
- for (const entry of await readdir(absolute))
1075
- if (entry !== ".git")
1076
- await add(join(absolute, entry));
1077
- return;
1078
- }
1079
- const name = relative(root, absolute).split(sep).join("/");
1080
- if (files.some((file) => file.path === name))
1081
- return;
1082
- const bytes = await readFile(absolute);
1083
- total += bytes.length;
1084
- if (files.length >= 256 || total > 1024 * 1024)
1085
- throw usage("The Dockerfile's context is over 256 files or 1 MiB. Build from --from, or install packages with --pip/--apt/--npm.");
1086
- files.push({
1087
- path: name,
1088
- content: bytes.toString("base64"),
1089
- encoding: "base64",
1090
- mode: info.mode & 0o777,
1091
- });
1092
- };
1093
- for (const source of sources) {
1094
- const absolute = resolve(root, source);
1095
- if (absolute !== root && !absolute.startsWith(root + sep))
1096
- throw usage(`COPY ${source} is outside the Dockerfile's folder.`);
1097
- if (/[*?[]/.test(source)) {
1098
- const pattern = new RegExp(`^${source
1099
- .split("/")
1100
- .at(-1)
1101
- .replace(/[.+^${}()|\\]/g, "\\$&")
1102
- .replace(/\*/g, "[^/]*")
1103
- .replace(/\?/g, "[^/]")}$`);
1104
- const folder = dirname(absolute);
1105
- for (const entry of await readdir(folder))
1106
- if (pattern.test(entry))
1107
- await add(join(folder, entry));
1673
+ case "registry":
1674
+ case "registries": {
1675
+ const [action, ...more] = rest;
1676
+ if (action === "ls" || action === "list" || action === undefined) {
1677
+ const all = await api.registries.list();
1678
+ print(all.length
1679
+ ? table([
1680
+ ["REGISTRY", "KIND", "USER", "SINCE"],
1681
+ ...all.map((r) => [r.registry, r.kind, r.username, r.createdAt]),
1682
+ ])
1683
+ : "No registry credentials. Store some: runtime image registry set ghcr.io --username you", all);
1684
+ return 0;
1685
+ }
1686
+ if (action === "set") {
1687
+ const args = parse(more);
1688
+ const registry = need(args.positional[0], "a registry, such as ghcr.io");
1689
+ const accessKeyId = flag(args, "access-key-id");
1690
+ if (accessKeyId === undefined && flag(args, "username") === undefined)
1691
+ throw usage("Give --username (or --access-key-id for Amazon ECR).");
1692
+ if (!out.json && process.stdin.isTTY)
1693
+ out.error(accessKeyId ? "The secret access key:" : "The token or password:");
1694
+ const secret = await read();
1695
+ if (!secret)
1696
+ throw usage("Nothing on standard input.");
1697
+ const saved = await api.registries.set(accessKeyId
1698
+ ? { registry, accessKeyId, secretAccessKey: secret }
1699
+ : { registry, username: flag(args, "username"), password: secret });
1700
+ print(`Stored credentials for ${saved.registry} (${saved.username}).`, saved);
1701
+ return 0;
1702
+ }
1703
+ if (action === "rm" || action === "delete") {
1704
+ const gone = await api.registries.delete(need(more[0], "a registry"));
1705
+ print(gone.deleted ? `Forgot ${gone.registry}.` : `Nothing stored for ${gone.registry}.`, gone);
1706
+ return 0;
1707
+ }
1708
+ throw unknown("registry command", action, ["ls", "set", "rm"]);
1108
1709
  }
1109
- else
1110
- await add(absolute);
1111
1710
  }
1112
- return { dockerfile, files };
1711
+ throw unknown("image command", verb, [
1712
+ "build",
1713
+ "ls",
1714
+ "versions",
1715
+ "get",
1716
+ "logs",
1717
+ "tag",
1718
+ "untag",
1719
+ "rm",
1720
+ "registry",
1721
+ ]);
1113
1722
  }
1114
1723
  const VOLUME_HELP = `runtime volume <command>
1115
1724
 
@@ -1172,7 +1781,15 @@ async function volume(argv, env, out) {
1172
1781
  throw unknown("volume command", verb, ["create", "ls", "get", "rm"]);
1173
1782
  }
1174
1783
  /** Flags `sandbox create` and `run` take without a value. */
1175
- const CREATE_FLAGS = ["trial", "paid", "no-wait", "no-internet"];
1784
+ const CREATE_FLAGS = [
1785
+ "trial",
1786
+ "paid",
1787
+ "no-wait",
1788
+ "no-internet",
1789
+ "no-auto-wake",
1790
+ "persistent",
1791
+ "get-or-create",
1792
+ ];
1176
1793
  /** A create body from the command line's flags. */
1177
1794
  function createInput(args) {
1178
1795
  const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
@@ -1197,6 +1814,12 @@ function createInput(args) {
1197
1814
  ...(flag(args, "on-timeout")
1198
1815
  ? { onLeaseEnd: flag(args, "on-timeout") }
1199
1816
  : {}),
1817
+ ...(integer(args, "idle-pause") !== undefined
1818
+ ? { idlePauseSeconds: integer(args, "idle-pause") }
1819
+ : {}),
1820
+ ...(has(args, "no-auto-wake") ? { autoWake: false } : {}),
1821
+ ...(has(args, "persistent") ? { persistent: true } : {}),
1822
+ ...(has(args, "get-or-create") ? { getOrCreate: true } : {}),
1200
1823
  ...(has(args, "trial")
1201
1824
  ? { funding: "trial" }
1202
1825
  : has(args, "paid")
@@ -1289,3 +1912,224 @@ if (import.meta.main ||
1289
1912
  process.exitCode = 1;
1290
1913
  });
1291
1914
  }
1915
+ // ------------------------------------------------------------------ observability
1916
+ /** A sparkline of up to 60 points, oldest first. */
1917
+ function spark(values) {
1918
+ const bars = "▁▂▃▄▅▆▇█";
1919
+ const known = values.filter((v) => v !== null);
1920
+ const top = Math.max(1, ...known);
1921
+ const step = Math.max(1, Math.ceil(values.length / 60));
1922
+ const out = [];
1923
+ for (let i = 0; i < values.length; i += step) {
1924
+ const slice = values.slice(i, i + step).filter((v) => v !== null);
1925
+ out.push(slice.length ? bars[Math.min(7, Math.round((Math.max(...slice) / top) * 7))] : " ");
1926
+ }
1927
+ return out.join("");
1928
+ }
1929
+ const mib = (bytes) => `${Math.round(bytes / 1_048_576).toLocaleString("en-US")} MiB`;
1930
+ function metricsText(m) {
1931
+ const cpu = m.points.map((p) => p.cpuPercent);
1932
+ const memory = m.points.map((p) => p.memoryBytes);
1933
+ const peak = Math.max(0, ...m.points.map((p) => p.cpuPeakPercent ?? p.cpuPercent ?? 0));
1934
+ const latest = m.latest;
1935
+ return [
1936
+ table([
1937
+ ["state", m.state],
1938
+ [
1939
+ "cpu now",
1940
+ latest?.cpuPercent == null
1941
+ ? "no reading in the last 15 minutes"
1942
+ : `${latest.cpuPercent}% of ${m.vcpu} vCPU (${latest.cpuCores} cores)`,
1943
+ ],
1944
+ ["memory now", latest ? `${mib(latest.memoryBytes)} of ${mib(m.memoryLimitBytes)}` : "-"],
1945
+ [`cpu, ${m.range}`, m.points.length ? `${spark(cpu)} peak ${peak}%` : "no readings"],
1946
+ [
1947
+ `memory, ${m.range}`,
1948
+ m.points.length
1949
+ ? `${spark(memory)} peak ${mib(Math.max(...m.points.map((p) => p.memoryPeakBytes)))}`
1950
+ : "no readings",
1951
+ ],
1952
+ ]),
1953
+ ].join("\n");
1954
+ }
1955
+ const WEBHOOK_VERBS = [
1956
+ "ls",
1957
+ "create",
1958
+ "get",
1959
+ "update",
1960
+ "test",
1961
+ "deliveries",
1962
+ "retry",
1963
+ "rotate-secret",
1964
+ "rm",
1965
+ ];
1966
+ async function webhooksCommand(argv, env, out) {
1967
+ const [verb = "ls", ...rest] = argv;
1968
+ product = "webhooks";
1969
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
1970
+ if (verb === "help" || verb === "--help") {
1971
+ out.write(HELP.split("\n")
1972
+ .filter((line) => /webhooks|^\s{11}\|/.test(line))
1973
+ .join("\n"));
1974
+ return 0;
1975
+ }
1976
+ const hooks = (await client(env)).webhooks;
1977
+ const args = parse(rest, ["enable", "disable"]);
1978
+ const id = () => need(args.positional[0], "a webhook id");
1979
+ const events = () => flag(args, "events")
1980
+ ?.split(",")
1981
+ .map((e) => e.trim())
1982
+ .filter(Boolean);
1983
+ const row = (h) => [
1984
+ h.id,
1985
+ h.url,
1986
+ h.enabled ? (h.failingSince ? `failing since ${h.failingSince}` : "on") : "off",
1987
+ h.events.join(","),
1988
+ ];
1989
+ switch (verb) {
1990
+ case "ls":
1991
+ case "list": {
1992
+ const page = await hooks.list();
1993
+ print(page.data.length
1994
+ ? table(page.data.map(row))
1995
+ : "No webhooks. Add one: runtime webhooks create <url>", page);
1996
+ return 0;
1997
+ }
1998
+ case "get": {
1999
+ const h = await hooks.get(id());
2000
+ print(table([row(h)]), h);
2001
+ return 0;
2002
+ }
2003
+ case "create":
2004
+ case "add": {
2005
+ const h = await hooks.create({
2006
+ url: need(args.positional[0], "the https URL to send to"),
2007
+ ...(events() ? { events: events() } : {}),
2008
+ ...(flag(args, "description") ? { description: flag(args, "description") } : {}),
2009
+ });
2010
+ print(`${h.id}\nSigning secret (shown once, keep it): ${h.secret}\nVerify deliveries with verifyWebhook from the withruntime SDK.`, h);
2011
+ return 0;
2012
+ }
2013
+ case "update": {
2014
+ const h = await hooks.update(id(), {
2015
+ ...(flag(args, "url") ? { url: flag(args, "url") } : {}),
2016
+ ...(events() ? { events: events() } : {}),
2017
+ ...(has(args, "enable") ? { enabled: true } : {}),
2018
+ ...(has(args, "disable") ? { enabled: false } : {}),
2019
+ });
2020
+ print(table([row(h)]), h);
2021
+ return 0;
2022
+ }
2023
+ case "rotate-secret": {
2024
+ const keep = integer(args, "keep");
2025
+ const h = await hooks.rotateSecret(id(), keep === undefined ? {} : { keepPreviousSeconds: keep });
2026
+ print(`New signing secret (shown once): ${h.secret}${h.previousSecretExpiresAt ? `\nThe old one also signs until ${h.previousSecretExpiresAt}.` : ""}`, h);
2027
+ return 0;
2028
+ }
2029
+ case "test": {
2030
+ const d = await hooks.test(id());
2031
+ print(d.state === "succeeded"
2032
+ ? `Delivered: your endpoint answered ${d.lastStatus} in ${d.lastDurationMs} ms.`
2033
+ : d.state === "pending"
2034
+ ? "Sent; no answer yet. See it with: runtime webhooks deliveries " + id()
2035
+ : `Not delivered: ${d.lastError ?? d.state}${d.lastStatus ? ` (HTTP ${d.lastStatus})` : ""}.`, d);
2036
+ return d.state === "failed" ? 1 : 0;
2037
+ }
2038
+ case "deliveries": {
2039
+ const page = await hooks.deliveries(id(), {
2040
+ ...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
2041
+ });
2042
+ print(page.data.length
2043
+ ? table(page.data.map((d) => [
2044
+ d.id,
2045
+ d.createdAt,
2046
+ d.eventType,
2047
+ d.state,
2048
+ `${d.attempts}/${d.maxAttempts}`,
2049
+ d.lastStatus ? String(d.lastStatus) : (d.lastError ?? "-"),
2050
+ ]))
2051
+ : "No deliveries yet.", page);
2052
+ return 0;
2053
+ }
2054
+ case "retry": {
2055
+ const d = await hooks.retry(need(args.positional[0], "a delivery id"));
2056
+ print(`Queued ${d.id} to send again.`, d);
2057
+ return 0;
2058
+ }
2059
+ case "rm":
2060
+ case "delete": {
2061
+ print("Deleted.", await hooks.delete(id()));
2062
+ return 0;
2063
+ }
2064
+ }
2065
+ throw unknown("webhooks command", verb, WEBHOOK_VERBS);
2066
+ }
2067
+ const OTEL_VERBS = ["ls", "create", "get", "update", "flush", "rm"];
2068
+ async function otelCommand(argv, env, out) {
2069
+ const [verb = "ls", ...rest] = argv;
2070
+ product = "otel";
2071
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
2072
+ const otel = (await client(env)).otel;
2073
+ const args = parse(rest, ["enable", "disable"]);
2074
+ const id = () => need(args.positional[0], "an export id");
2075
+ const signals = () => flag(args, "signals")
2076
+ ?.split(",")
2077
+ .map((s) => s.trim());
2078
+ const row = (x) => [
2079
+ x.id,
2080
+ x.endpoint,
2081
+ x.enabled ? "on" : "off",
2082
+ x.signals.join(","),
2083
+ x.lastError
2084
+ ? `last error: ${x.lastError}`
2085
+ : x.lastSuccessAt
2086
+ ? `sent ${x.lastSuccessAt}`
2087
+ : "not sent yet",
2088
+ ];
2089
+ switch (verb) {
2090
+ case "ls":
2091
+ case "list": {
2092
+ const page = await otel.list();
2093
+ print(page.data.length
2094
+ ? table(page.data.map(row))
2095
+ : "No exports. Add one: runtime otel create <endpoint>", page);
2096
+ return 0;
2097
+ }
2098
+ case "get": {
2099
+ const x = await otel.get(id());
2100
+ print(table([row(x)]), x);
2101
+ return 0;
2102
+ }
2103
+ case "create":
2104
+ case "add": {
2105
+ const x = await otel.create({
2106
+ endpoint: need(args.positional[0], "the OTLP/HTTP endpoint"),
2107
+ ...(args.flags.has("header") ? { headers: pairs(args, "header") } : {}),
2108
+ ...(signals() ? { signals: signals() } : {}),
2109
+ });
2110
+ print(`${x.id}\nPushing ${x.signals.join(" and ")} to ${x.endpoint} about every 30 seconds.`, x);
2111
+ return 0;
2112
+ }
2113
+ case "update": {
2114
+ const x = await otel.update(id(), {
2115
+ ...(flag(args, "endpoint") ? { endpoint: flag(args, "endpoint") } : {}),
2116
+ ...(args.flags.has("header") ? { headers: pairs(args, "header") } : {}),
2117
+ ...(signals() ? { signals: signals() } : {}),
2118
+ ...(has(args, "enable") ? { enabled: true } : {}),
2119
+ ...(has(args, "disable") ? { enabled: false } : {}),
2120
+ });
2121
+ print(table([row(x)]), x);
2122
+ return 0;
2123
+ }
2124
+ case "flush": {
2125
+ print("Pushing now; `runtime otel get <id>` shows the answer in a few seconds.", await otel.flush(id()));
2126
+ return 0;
2127
+ }
2128
+ case "rm":
2129
+ case "delete": {
2130
+ print("Deleted.", await otel.delete(id()));
2131
+ return 0;
2132
+ }
2133
+ }
2134
+ throw unknown("otel command", verb, OTEL_VERBS);
2135
+ }