withruntime 0.4.0 → 0.5.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.
- package/DAYTONA.md +129 -0
- package/E2B.md +3 -3
- package/README.md +47 -9
- package/VERCEL.md +127 -0
- package/dist/ai/index.d.ts +33 -0
- package/dist/ai/index.js +38 -0
- package/dist/claude-agent-sdk/index.d.ts +7 -0
- package/dist/claude-agent-sdk/index.js +65 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +959 -102
- package/dist/client.d.ts +25 -2
- package/dist/client.js +14 -1
- package/dist/credentials.d.ts +10 -0
- package/dist/credentials.js +56 -1
- package/dist/daytona/client.d.ts +44 -0
- package/dist/daytona/client.js +40 -0
- package/dist/daytona/context.d.ts +31 -0
- package/dist/daytona/context.js +25 -0
- package/dist/daytona/daytona.d.ts +185 -0
- package/dist/daytona/daytona.js +416 -0
- package/dist/daytona/errors.d.ts +106 -0
- package/dist/daytona/errors.js +182 -0
- package/dist/daytona/filesystem.d.ts +84 -0
- package/dist/daytona/filesystem.js +206 -0
- package/dist/daytona/git.d.ts +33 -0
- package/dist/daytona/git.js +128 -0
- package/dist/daytona/image.d.ts +45 -0
- package/dist/daytona/image.js +140 -0
- package/dist/daytona/index.d.ts +50 -0
- package/dist/daytona/index.js +50 -0
- package/dist/daytona/process.d.ts +86 -0
- package/dist/daytona/process.js +341 -0
- package/dist/daytona/sandbox.d.ts +196 -0
- package/dist/daytona/sandbox.js +448 -0
- package/dist/e2b/commands.js +6 -2
- package/dist/e2b/sandbox.d.ts +22 -2
- package/dist/e2b/sandbox.js +39 -7
- package/dist/errors.d.ts +10 -4
- package/dist/errors.js +26 -6
- package/dist/image-context.d.ts +39 -0
- package/dist/image-context.js +176 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.js +17 -2
- package/dist/keys.d.ts +43 -0
- package/dist/keys.js +196 -0
- package/dist/login.js +14 -2
- package/dist/openai-agents/index.d.ts +132 -0
- package/dist/openai-agents/index.js +791 -0
- package/dist/products/audit.d.ts +48 -0
- package/dist/products/audit.js +10 -0
- package/dist/products/images.d.ts +113 -7
- package/dist/products/images.js +186 -27
- package/dist/products/index.d.ts +11 -0
- package/dist/products/index.js +17 -1
- package/dist/products/observability.d.ts +186 -0
- package/dist/products/observability.js +117 -0
- package/dist/products/secrets.d.ts +40 -0
- package/dist/products/secrets.js +24 -0
- package/dist/products/switching.d.ts +96 -0
- package/dist/products/switching.js +25 -0
- package/dist/sandbox.d.ts +26 -1
- package/dist/sandbox.js +101 -1
- package/dist/ssh.d.ts +21 -0
- package/dist/ssh.js +324 -0
- package/dist/tar.d.ts +1 -0
- package/dist/tar.js +4 -4
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.js +180 -0
- package/dist/transport.d.ts +12 -2
- package/dist/transport.js +31 -4
- package/dist/tunnel.d.ts +70 -0
- package/dist/tunnel.js +316 -0
- package/dist/types.d.ts +57 -1
- package/dist/vercel/client.d.ts +40 -0
- package/dist/vercel/client.js +39 -0
- package/dist/vercel/command.d.ts +70 -0
- package/dist/vercel/command.js +148 -0
- package/dist/vercel/errors.d.ts +53 -0
- package/dist/vercel/errors.js +113 -0
- package/dist/vercel/filesystem.d.ts +150 -0
- package/dist/vercel/filesystem.js +248 -0
- package/dist/vercel/index.d.ts +30 -0
- package/dist/vercel/index.js +38 -0
- package/dist/vercel/sandbox.d.ts +317 -0
- package/dist/vercel/sandbox.js +726 -0
- package/dist/vercel/snapshot.d.ts +93 -0
- package/dist/vercel/snapshot.js +123 -0
- package/package.json +56 -4
package/dist/cli.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join,
|
|
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";
|
|
10
11
|
import { envFetch } from "./proxy.js";
|
|
12
|
+
import { portForwardCommand, sshCommand } from "./ssh.js";
|
|
11
13
|
import { VERSION } from "./transport.js";
|
|
12
14
|
const HELP = `Runtime Cloud CLI ${VERSION}
|
|
13
15
|
|
|
14
16
|
Usage: runtime <product> <command> [options] runtime <command> [options]
|
|
15
17
|
|
|
16
18
|
Start
|
|
17
|
-
run [--keep] [create options] -- <command>
|
|
19
|
+
sandbox run [--keep] [create options] -- <command>
|
|
20
|
+
A fresh sandbox runs one command, then stops
|
|
18
21
|
|
|
19
22
|
Account
|
|
20
23
|
login [--agent-name <name>] [--no-browser] [--wait]
|
|
@@ -22,9 +25,25 @@ Account
|
|
|
22
25
|
login --with-key Or paste a key from withruntime.com/account/keys
|
|
23
26
|
logout Revoke this machine's connection
|
|
24
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)
|
|
25
33
|
usage Balance, trial time and charges
|
|
26
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
|
|
27
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
|
|
28
47
|
ls Everything you run, every product
|
|
29
48
|
feedback "<text>" [--kind bug|missing_feature|competitor_gap|migration_blocker|docs|pricing|praise|other]
|
|
30
49
|
[--detail <text>] [--competitor <name>] [--request-id <req_...>]
|
|
@@ -36,10 +55,26 @@ Account
|
|
|
36
55
|
docs [topic] Print a docs page (start, cli, api, python...)
|
|
37
56
|
mcp Serve MCP on stdio for agents that need it
|
|
38
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
|
+
|
|
39
74
|
Products
|
|
40
75
|
sandbox Linux microVMs: exec, shell, files, code, previews, network, desktop
|
|
41
76
|
snapshot Saved sandboxes, disk and memory, to start or fork from
|
|
42
|
-
image Custom images from a Dockerfile,
|
|
77
|
+
image Custom images from a Dockerfile, any public or private image, or a package list
|
|
43
78
|
volume Persistent disks to attach to sandboxes
|
|
44
79
|
Each has its own help: \`runtime sandbox help\`, \`runtime image help\`...
|
|
45
80
|
|
|
@@ -48,10 +83,14 @@ Behind a proxy: HTTPS_PROXY and NO_PROXY are read, on Node and Bun alike.
|
|
|
48
83
|
`;
|
|
49
84
|
const SANDBOX_HELP = `runtime sandbox <command>
|
|
50
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
|
|
51
89
|
create [--name n] [--label k=v]... [--vcpu 2] [--memory 4096] [--disk 4096]
|
|
52
90
|
[--timeout <seconds>] [--on-timeout pause|stop] [--trial|--paid] [--no-wait]
|
|
53
91
|
[--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
54
|
-
[--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]
|
|
55
94
|
Create one; prints its id
|
|
56
95
|
ls [--all] [--label k=v]... [--state running] List (live ones unless --all)
|
|
57
96
|
get <id> One sandbox, in full
|
|
@@ -68,6 +107,9 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
68
107
|
files <id> [path] [--depth 2] [--glob '**/*.py']
|
|
69
108
|
stop <id> | pause <id> | wake <id> | restart <id>
|
|
70
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
|
|
71
113
|
snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
|
|
72
114
|
fork <id> [--count 3] [--name n] [--keep-snapshot] [--trial|--paid]
|
|
73
115
|
Copies of it as it is now, running; prints their ids
|
|
@@ -78,6 +120,8 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
78
120
|
unshare <id> <port> Stop sharing a port
|
|
79
121
|
network <id> [--no-internet|--internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
80
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
|
|
81
125
|
desktop <id> start | stop | screenshot [file.png] | open <url> | click <x> <y>
|
|
82
126
|
| type <text> | press <keys> | windows
|
|
83
127
|
|
|
@@ -162,7 +206,11 @@ const COMMANDS = [
|
|
|
162
206
|
"whoami",
|
|
163
207
|
"usage",
|
|
164
208
|
"limits",
|
|
209
|
+
"secrets",
|
|
210
|
+
"keys",
|
|
165
211
|
"referrals",
|
|
212
|
+
"compare",
|
|
213
|
+
"switch",
|
|
166
214
|
"ls",
|
|
167
215
|
"feedback",
|
|
168
216
|
"support",
|
|
@@ -172,10 +220,18 @@ const COMMANDS = [
|
|
|
172
220
|
"snapshot",
|
|
173
221
|
"image",
|
|
174
222
|
"volume",
|
|
223
|
+
"webhooks",
|
|
224
|
+
"otel",
|
|
225
|
+
"events",
|
|
175
226
|
"help",
|
|
176
227
|
"version",
|
|
177
228
|
];
|
|
229
|
+
/** Sandbox commands that once sat at the top level. */
|
|
230
|
+
const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
|
|
178
231
|
const SANDBOX_VERBS = [
|
|
232
|
+
"run",
|
|
233
|
+
"ssh",
|
|
234
|
+
"port-forward",
|
|
179
235
|
"create",
|
|
180
236
|
"ls",
|
|
181
237
|
"get",
|
|
@@ -186,6 +242,7 @@ const SANDBOX_VERBS = [
|
|
|
186
242
|
"snapshot",
|
|
187
243
|
"fork",
|
|
188
244
|
"extend",
|
|
245
|
+
"update",
|
|
189
246
|
"exec",
|
|
190
247
|
"spawn",
|
|
191
248
|
"ps",
|
|
@@ -201,6 +258,7 @@ const SANDBOX_VERBS = [
|
|
|
201
258
|
"unshare",
|
|
202
259
|
"network",
|
|
203
260
|
"desktop",
|
|
261
|
+
"metrics",
|
|
204
262
|
];
|
|
205
263
|
function need(value, what) {
|
|
206
264
|
if (!value)
|
|
@@ -261,6 +319,14 @@ function describe(info) {
|
|
|
261
319
|
["funding", info.funding],
|
|
262
320
|
["region", info.region],
|
|
263
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"],
|
|
264
330
|
[
|
|
265
331
|
"labels",
|
|
266
332
|
Object.entries(info.labels ?? {})
|
|
@@ -276,7 +342,16 @@ function remote(spec) {
|
|
|
276
342
|
return match ? { id: match[1], path: match[2] } : undefined;
|
|
277
343
|
}
|
|
278
344
|
export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
279
|
-
const [
|
|
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;
|
|
280
355
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
281
356
|
product = "";
|
|
282
357
|
if (!command) {
|
|
@@ -289,7 +364,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
289
364
|
? JSON.stringify({ connected, usage: HELP })
|
|
290
365
|
: `Runtime Cloud ${VERSION}. ${start}
|
|
291
366
|
|
|
292
|
-
${me} run -- python3 -c 'print(6*7)'
|
|
367
|
+
${me} sandbox run -- python3 -c 'print(6*7)'
|
|
368
|
+
A fresh sandbox runs one command, then stops
|
|
293
369
|
${me} sandbox create A sandbox that stays; prints its id
|
|
294
370
|
${me} sandbox exec <id> -- <command> Run in it
|
|
295
371
|
${me} help Everything else
|
|
@@ -304,7 +380,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
304
380
|
const args = parse(rest, [...CREATE_FLAGS, "keep"]);
|
|
305
381
|
const commandLine = args.rest ?? args.positional;
|
|
306
382
|
if (!commandLine.length)
|
|
307
|
-
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)'`);
|
|
308
384
|
const runtime = await client(env);
|
|
309
385
|
const timeout = integer(args, "timeout");
|
|
310
386
|
const { timeoutSeconds: _lease, ...input } = createInput(args);
|
|
@@ -402,6 +478,60 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
402
478
|
]), l);
|
|
403
479
|
return 0;
|
|
404
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
|
+
}
|
|
405
535
|
if (command === "referrals") {
|
|
406
536
|
const r = await (await client(env)).referrals.get();
|
|
407
537
|
const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
@@ -423,6 +553,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
423
553
|
].join("\n"), r);
|
|
424
554
|
return 0;
|
|
425
555
|
}
|
|
556
|
+
if (command === "compare" || command === "switch")
|
|
557
|
+
return switching(command, rest, env, out);
|
|
426
558
|
if (command === "mcp") {
|
|
427
559
|
const { serveMcp } = await import("./mcp.js");
|
|
428
560
|
await serveMcp(env);
|
|
@@ -496,6 +628,14 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
496
628
|
: "Nothing yet. Start with: runtime sandbox create", items);
|
|
497
629
|
return 0;
|
|
498
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
|
+
}
|
|
499
639
|
if (command === "sandbox" || command === "sandboxes" || command === "sbx") {
|
|
500
640
|
product = "sandbox";
|
|
501
641
|
return sandbox(rest, env, out);
|
|
@@ -506,7 +646,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
506
646
|
}
|
|
507
647
|
if (command === "image" || command === "images") {
|
|
508
648
|
product = "image";
|
|
509
|
-
return
|
|
649
|
+
return imageCommand(rest, env, out);
|
|
510
650
|
}
|
|
511
651
|
if (command === "volume" || command === "volumes") {
|
|
512
652
|
product = "volume";
|
|
@@ -514,6 +654,141 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
514
654
|
}
|
|
515
655
|
throw unknown("command", command, COMMANDS);
|
|
516
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
|
+
/** A date as the CLI writes one: 23 September 2026. */
|
|
688
|
+
const longDate = (iso) => new Date(iso).toLocaleDateString("en-GB", {
|
|
689
|
+
day: "numeric",
|
|
690
|
+
month: "long",
|
|
691
|
+
year: "numeric",
|
|
692
|
+
timeZone: "UTC",
|
|
693
|
+
});
|
|
694
|
+
/** One line on the switching credit: how to claim it, or what it did. A
|
|
695
|
+
* switch already on record is named with its provider and date, so it never
|
|
696
|
+
* reads as if this command recorded it, or recorded the provider it names. */
|
|
697
|
+
function switchingLine(summary, provider) {
|
|
698
|
+
const cap = dollars(summary.maxMicros);
|
|
699
|
+
const sw = summary.switch;
|
|
700
|
+
if (!sw) {
|
|
701
|
+
if (!summary.eligible || !provider)
|
|
702
|
+
return undefined;
|
|
703
|
+
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}.`;
|
|
704
|
+
}
|
|
705
|
+
const from = RIVAL_NAMES[sw.provider];
|
|
706
|
+
const recorded = `This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}`;
|
|
707
|
+
const once = provider && provider !== sw.provider
|
|
708
|
+
? ` A switch is recorded once, so it stays ${from}, not ${RIVAL_NAMES[provider]}.`
|
|
709
|
+
: "";
|
|
710
|
+
switch (sw.status) {
|
|
711
|
+
case "pending":
|
|
712
|
+
return `${recorded}: its first top-up is matched, up to ${cap}, once the payment settles.${once}`;
|
|
713
|
+
case "paid":
|
|
714
|
+
return `${recorded} and matched its first top-up: ${dollars(sw.creditMicros)} of credit.${once}`;
|
|
715
|
+
case "superseded":
|
|
716
|
+
return `${recorded}; its first top-up was matched by a referral instead, which is never less than the switching credit.${once}`;
|
|
717
|
+
case "reversed":
|
|
718
|
+
return `${recorded}; the top-up it matched was refunded, so the credit was taken back.${once}`;
|
|
719
|
+
case "refused":
|
|
720
|
+
return sw.refusedReason === "program_off"
|
|
721
|
+
? `${recorded}; the switching credit was paused when its first top-up arrived.${once}`
|
|
722
|
+
: `${recorded}; its first top-up was not matched. Ask support if you think that is wrong.${once}`;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/** The sandboxes the free trial paid for, said beside what was priced. */
|
|
726
|
+
function trialNote(trial, all) {
|
|
727
|
+
if (!trial)
|
|
728
|
+
return "";
|
|
729
|
+
const which = trial === all ? (all === 1 ? "It" : "All of them") : `${trial} of them`;
|
|
730
|
+
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.`;
|
|
731
|
+
}
|
|
732
|
+
async function switching(command, argv, env, out) {
|
|
733
|
+
const args = parse(argv);
|
|
734
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
735
|
+
const known = new Set(command === "compare" ? ["from", "days"] : ["from"]);
|
|
736
|
+
for (const name of args.flags.keys())
|
|
737
|
+
if (!known.has(name))
|
|
738
|
+
throw usage(`Unknown option --${name}.`);
|
|
739
|
+
if (args.positional.length || args.rest)
|
|
740
|
+
throw usage(`Unexpected ${args.positional[0] ?? "--"}.`);
|
|
741
|
+
const from = flag(args, "from")?.trim().toLowerCase();
|
|
742
|
+
const api = (await client(env)).switching;
|
|
743
|
+
if (command === "switch") {
|
|
744
|
+
if (!from) {
|
|
745
|
+
const summary = await api.get();
|
|
746
|
+
print(switchingLine(summary) ??
|
|
747
|
+
(summary.eligible
|
|
748
|
+
? `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)}.`
|
|
749
|
+
: "The switching credit matches a first top-up, and this organization has made one already."), summary);
|
|
750
|
+
return 0;
|
|
751
|
+
}
|
|
752
|
+
const provider = SWITCH_FROM[from];
|
|
753
|
+
if (!provider || from === "fly-machines")
|
|
754
|
+
throw unknown("provider", from, Object.keys(RIVAL_NAMES));
|
|
755
|
+
const summary = await api.record({ provider });
|
|
756
|
+
print(`Recorded: switching from ${RIVAL_NAMES[provider]}. ${switchingLine(summary) ?? ""}`.trim(), summary);
|
|
757
|
+
return 0;
|
|
758
|
+
}
|
|
759
|
+
if (!from)
|
|
760
|
+
throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines...).`);
|
|
761
|
+
/* The server holds the list of rivals it has rates for, and names them all
|
|
762
|
+
when it refuses one, so a rival added there needs no new CLI. */
|
|
763
|
+
const days = integer(args, "days");
|
|
764
|
+
const c = await api.compare({ provider: from, days });
|
|
765
|
+
const saving = BigInt(c.savingMicros);
|
|
766
|
+
const percent = c.savingPercent === null ? "" : ` (${Math.abs(c.savingPercent)}%)`;
|
|
767
|
+
const verdict = saving >= 0n
|
|
768
|
+
? `you save ${dollars(saving)}${percent}`
|
|
769
|
+
: `${c.rival.name} would cost ${dollars(-saving)} less${percent}`;
|
|
770
|
+
const figures = `${dollars(c.runtimeMicros)} on Runtime; the same sandboxes on ${c.rival.name}: ${dollars(c.rivalMicros)}; ${verdict}`;
|
|
771
|
+
const month = c.perMonth ? BigInt(c.perMonth.savingMicros) : undefined;
|
|
772
|
+
const lines = c.basis === "usage"
|
|
773
|
+
? [
|
|
774
|
+
`Your last ${c.window.days} days: ${figures}${month === undefined
|
|
775
|
+
? "."
|
|
776
|
+
: month >= 0n
|
|
777
|
+
? `, about ${dollars(month)} a month.`
|
|
778
|
+
: `, about ${dollars(-month)} a month more on Runtime.`}`,
|
|
779
|
+
`Priced: ${c.usage.sandboxes.toLocaleString("en-US")} sandbox${c.usage.sandboxes === 1 ? "" : "es"}, ${(c.usage.runSeconds / 3600).toLocaleString("en-US", { maximumFractionDigits: 2 })} hours running.${trialNote(c.usage.trialSandboxes, c.usage.sandboxes)}${c.usage.unpricedSandboxes ? ` ${c.usage.unpricedSandboxes} left out: ${c.rival.name} publishes no price for their size.` : ""}`,
|
|
780
|
+
]
|
|
781
|
+
: [
|
|
782
|
+
`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.`,
|
|
783
|
+
`The example: ${figures}.`,
|
|
784
|
+
];
|
|
785
|
+
lines.push(`${c.rival.name}'s published rates, checked ${longDate(`${c.rival.checked}T12:00:00Z`)}: ${c.rival.rates.join("; ")}. Compute only: storage, network, plan fees and free allowances are left out.`);
|
|
786
|
+
const line = switchingLine(c.switching, Object.hasOwn(SWITCH_FROM, from) ? SWITCH_FROM[from] : undefined);
|
|
787
|
+
if (line)
|
|
788
|
+
lines.push("", line);
|
|
789
|
+
print(lines.join("\n"), c);
|
|
790
|
+
return 0;
|
|
791
|
+
}
|
|
517
792
|
async function support(argv, env, out) {
|
|
518
793
|
const args = parse(argv);
|
|
519
794
|
const api = (await client(env)).support;
|
|
@@ -639,6 +914,40 @@ async function sandbox(argv, env, out) {
|
|
|
639
914
|
print(forks.map((f) => f.id).join("\n"), forks.map((f) => f.info));
|
|
640
915
|
return 0;
|
|
641
916
|
}
|
|
917
|
+
case "update": {
|
|
918
|
+
const args = parse(rest);
|
|
919
|
+
const sbx = await get(args.positional[0]);
|
|
920
|
+
const onOff = (name) => {
|
|
921
|
+
const value = flag(args, name);
|
|
922
|
+
if (value === undefined)
|
|
923
|
+
return undefined;
|
|
924
|
+
if (value !== "on" && value !== "off")
|
|
925
|
+
throw usage(`--${name} takes on or off.`);
|
|
926
|
+
return value === "on";
|
|
927
|
+
};
|
|
928
|
+
const cost = flag(args, "max-total-cost");
|
|
929
|
+
if (cost !== undefined && cost !== "none" && !/^\d+(\.\d{1,6})?$/.test(cost))
|
|
930
|
+
throw usage("--max-total-cost takes dollars, such as 25 or 2.50, or none.");
|
|
931
|
+
const settings = {
|
|
932
|
+
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
933
|
+
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
934
|
+
...(integer(args, "idle-pause") !== undefined
|
|
935
|
+
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
936
|
+
: {}),
|
|
937
|
+
...(onOff("auto-wake") === undefined ? {} : { autoWake: onOff("auto-wake") }),
|
|
938
|
+
...(onOff("persistent") === undefined ? {} : { persistent: onOff("persistent") }),
|
|
939
|
+
...(cost === undefined
|
|
940
|
+
? {}
|
|
941
|
+
: {
|
|
942
|
+
maxTotalCostMicros: cost === "none" ? null : Math.round(Number(cost) * 1e6),
|
|
943
|
+
}),
|
|
944
|
+
};
|
|
945
|
+
if (!Object.keys(settings).length)
|
|
946
|
+
throw usage("Say what to change: --name, --label, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
|
|
947
|
+
await sbx.update(settings);
|
|
948
|
+
print(describe(sbx.info), sbx.info);
|
|
949
|
+
return 0;
|
|
950
|
+
}
|
|
642
951
|
case "extend": {
|
|
643
952
|
const sbx = await get(rest[0]);
|
|
644
953
|
const seconds = Number(need(rest[1], "seconds, e.g. runtime sandbox extend <id> 600"));
|
|
@@ -792,6 +1101,16 @@ async function sandbox(argv, env, out) {
|
|
|
792
1101
|
}
|
|
793
1102
|
case "desktop":
|
|
794
1103
|
return desktop(await get(rest[0]), rest.slice(1), out);
|
|
1104
|
+
case "metrics": {
|
|
1105
|
+
const args = parse(rest);
|
|
1106
|
+
const sbx = await get(args.positional[0]);
|
|
1107
|
+
const range = (flag(args, "range") ?? "1h");
|
|
1108
|
+
if (!["15m", "1h", "6h", "24h", "7d", "30d"].includes(range))
|
|
1109
|
+
throw usage("--range is one of 15m, 1h, 6h, 24h, 7d, 30d.");
|
|
1110
|
+
const m = await sbx.metrics({ range });
|
|
1111
|
+
print(metricsText(m), m);
|
|
1112
|
+
return 0;
|
|
1113
|
+
}
|
|
795
1114
|
}
|
|
796
1115
|
throw unknown("sandbox command", verb, SANDBOX_VERBS);
|
|
797
1116
|
}
|
|
@@ -802,6 +1121,109 @@ function attachment(spec) {
|
|
|
802
1121
|
throw usage("--volume takes <volumeId>:/path, or <volumeId>:/path:snapshot for a copy.");
|
|
803
1122
|
return { volumeId, path, mode };
|
|
804
1123
|
}
|
|
1124
|
+
/** A secret's value, from standard input only, so it never sits in shell
|
|
1125
|
+
* history or a process list. Piped: everything, less one trailing newline.
|
|
1126
|
+
* A terminal: one line, not shown. */
|
|
1127
|
+
async function readSecret() {
|
|
1128
|
+
const stdin = process.stdin;
|
|
1129
|
+
if (!stdin.isTTY) {
|
|
1130
|
+
const chunks = [];
|
|
1131
|
+
for await (const chunk of stdin)
|
|
1132
|
+
chunks.push(chunk);
|
|
1133
|
+
return Buffer.concat(chunks)
|
|
1134
|
+
.toString("utf8")
|
|
1135
|
+
.replace(/\r?\n$/, "");
|
|
1136
|
+
}
|
|
1137
|
+
process.stderr.write("Value (not shown), then Enter: ");
|
|
1138
|
+
stdin.setRawMode(true);
|
|
1139
|
+
stdin.resume();
|
|
1140
|
+
try {
|
|
1141
|
+
return await new Promise((done, fail) => {
|
|
1142
|
+
let value = "";
|
|
1143
|
+
const onData = (chunk) => {
|
|
1144
|
+
for (const char of chunk.toString("utf8")) {
|
|
1145
|
+
if (char === "\r" || char === "\n") {
|
|
1146
|
+
stdin.off("data", onData);
|
|
1147
|
+
process.stderr.write("\n");
|
|
1148
|
+
return done(value);
|
|
1149
|
+
}
|
|
1150
|
+
if (char === "\u0003") {
|
|
1151
|
+
stdin.off("data", onData);
|
|
1152
|
+
return fail(usage("Cancelled."));
|
|
1153
|
+
}
|
|
1154
|
+
if (char === "\u007f" || char === "\b")
|
|
1155
|
+
value = value.slice(0, -1);
|
|
1156
|
+
else
|
|
1157
|
+
value += char;
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
stdin.on("data", onData);
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
finally {
|
|
1164
|
+
stdin.setRawMode(false);
|
|
1165
|
+
stdin.pause();
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
export async function secretsCommand(argv, env, out,
|
|
1169
|
+
/** Tests give the value here; the CLI reads standard input. */
|
|
1170
|
+
read = readSecret) {
|
|
1171
|
+
const [verb, ...rest] = argv;
|
|
1172
|
+
const args = parse(rest);
|
|
1173
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1174
|
+
const rows = (list) => list.length
|
|
1175
|
+
? table([
|
|
1176
|
+
["name", "hosts", "header", "placeholder"],
|
|
1177
|
+
...list.map((secret) => [
|
|
1178
|
+
secret.name,
|
|
1179
|
+
secret.hosts.join(","),
|
|
1180
|
+
secret.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
|
|
1181
|
+
secret.placeholder,
|
|
1182
|
+
]),
|
|
1183
|
+
])
|
|
1184
|
+
: "No secrets.";
|
|
1185
|
+
if (verb === "ls" || verb === "list" || verb === undefined) {
|
|
1186
|
+
const list = await (await client(env)).secrets.list();
|
|
1187
|
+
print(rows(list), list);
|
|
1188
|
+
return 0;
|
|
1189
|
+
}
|
|
1190
|
+
if (verb === "set") {
|
|
1191
|
+
const name = need(args.positional[0], "the secret's name, such as OPENAI_API_KEY");
|
|
1192
|
+
const hosts = args.flags.get("host") ?? [];
|
|
1193
|
+
if (!hosts.length)
|
|
1194
|
+
throw usage("Give --host once for each host the value may go to, such as --host api.openai.com.");
|
|
1195
|
+
if (args.flags.has("value"))
|
|
1196
|
+
throw usage('The value is read from standard input, never an argument: printf %s "$KEY" | runtime secrets set NAME --host ...');
|
|
1197
|
+
const value = await read();
|
|
1198
|
+
if (!value)
|
|
1199
|
+
throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
|
|
1200
|
+
const header = flag(args, "header");
|
|
1201
|
+
const format = flag(args, "format");
|
|
1202
|
+
const saved = await (await client(env)).secrets.set(name, {
|
|
1203
|
+
value,
|
|
1204
|
+
hosts,
|
|
1205
|
+
...(header ? { header } : {}),
|
|
1206
|
+
...(format ? { format } : {}),
|
|
1207
|
+
});
|
|
1208
|
+
print([
|
|
1209
|
+
`Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
|
|
1210
|
+
saved.header
|
|
1211
|
+
? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
|
|
1212
|
+
: `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the URL and headers.`,
|
|
1213
|
+
...(saved.enforced
|
|
1214
|
+
? []
|
|
1215
|
+
: ["A host is catching up; running sandboxes have it within a minute."]),
|
|
1216
|
+
].join("\n"), saved);
|
|
1217
|
+
return 0;
|
|
1218
|
+
}
|
|
1219
|
+
if (verb === "rm" || verb === "delete") {
|
|
1220
|
+
const name = need(args.positional[0], "the secret's name");
|
|
1221
|
+
const removed = await (await client(env)).secrets.delete(name);
|
|
1222
|
+
print(`Deleted ${name}.`, removed);
|
|
1223
|
+
return 0;
|
|
1224
|
+
}
|
|
1225
|
+
throw usage("secrets takes set, ls or rm. Run `runtime help`.");
|
|
1226
|
+
}
|
|
805
1227
|
async function readInput(source) {
|
|
806
1228
|
if (source !== "-")
|
|
807
1229
|
return readFile(source, "utf8");
|
|
@@ -904,6 +1326,141 @@ async function desktop(sbx, argv, out) {
|
|
|
904
1326
|
}
|
|
905
1327
|
throw usage(`${me} sandbox desktop <id> start|stop|screenshot|open|click|type|press|windows`);
|
|
906
1328
|
}
|
|
1329
|
+
const KEYS_HELP = `runtime keys <command>
|
|
1330
|
+
|
|
1331
|
+
create [--name <name>] [--read-only] [--daily-limit <usd>]
|
|
1332
|
+
A new API key, for a CI runner or any place with no browser. Prints a link
|
|
1333
|
+
and a code; an account owner approves the key in the browser, seeing its
|
|
1334
|
+
name, access and limit. The key is printed once, alone on stdout, and
|
|
1335
|
+
lasts until revoked. --json prints it as JSON, still alone on stdout.
|
|
1336
|
+
|
|
1337
|
+
runtime keys create --name ci --daily-limit 25 | gh secret set RUNTIME_API_KEY
|
|
1338
|
+
|
|
1339
|
+
List, limit and revoke keys at https://withruntime.com/account/keys.
|
|
1340
|
+
`;
|
|
1341
|
+
/* `runtime keys create`: a key made from the terminal and approved in the
|
|
1342
|
+
browser. Marc, 23 September 2026; ARCHITECTURE.md section 10, "Keys from the
|
|
1343
|
+
terminal". Only the options the keys page offers. No key can list, create or
|
|
1344
|
+
revoke keys through the API, so `ls` and `revoke` send you to the page. */
|
|
1345
|
+
/* `runtime account`: the accounts this machine is connected to, and which one
|
|
1346
|
+
every command uses (ARCHITECTURE.md section 3.9). Each is its own saved
|
|
1347
|
+
connection with its own key; the SDK itself stays key-based. */
|
|
1348
|
+
async function accountCommand(argv, env, out) {
|
|
1349
|
+
product = "account";
|
|
1350
|
+
const [verb = "ls", ...rest] = argv;
|
|
1351
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1352
|
+
const store = connectionStore(env);
|
|
1353
|
+
if (verb === "add") {
|
|
1354
|
+
const result = await authenticationCommand(["login", "--another-account", ...rest], env);
|
|
1355
|
+
if (result.pending)
|
|
1356
|
+
throw notApprovedYet(result);
|
|
1357
|
+
await nameSaved(env);
|
|
1358
|
+
print(`Connected${"orgId" in result && result.orgId ? ` to ${String(result.orgId)}` : ""}.`, result);
|
|
1359
|
+
return 0;
|
|
1360
|
+
}
|
|
1361
|
+
if (verb === "switch" || verb === "use") {
|
|
1362
|
+
const which = rest.join(" ").trim();
|
|
1363
|
+
if (!which)
|
|
1364
|
+
throw usage(`Name the account: ${me} account switch <name|id>.`);
|
|
1365
|
+
if (env.RUNTIME_API_KEY)
|
|
1366
|
+
throw usage("RUNTIME_API_KEY is set, and it names one account. Unset it to switch.");
|
|
1367
|
+
const chosen = await store.use(which);
|
|
1368
|
+
if (!chosen)
|
|
1369
|
+
throw usage(`No saved connection for "${which}". Run \`${me} account\` to list them, or \`${me} account add\` to connect it.`);
|
|
1370
|
+
print(`Now using ${chosen.orgName ?? chosen.orgId}.`, {
|
|
1371
|
+
orgId: chosen.orgId,
|
|
1372
|
+
orgName: chosen.orgName ?? null,
|
|
1373
|
+
});
|
|
1374
|
+
return 0;
|
|
1375
|
+
}
|
|
1376
|
+
if (verb !== "ls" && verb !== "list")
|
|
1377
|
+
throw unknown("account command", verb, ["ls", "switch", "add"]);
|
|
1378
|
+
await nameSaved(env).catch(() => undefined);
|
|
1379
|
+
const current = await store.read().catch(() => null);
|
|
1380
|
+
const saved = [
|
|
1381
|
+
...(current ? [{ ...current, current: true }] : []),
|
|
1382
|
+
...(await store.others()).map((one) => ({ ...one, current: false })),
|
|
1383
|
+
];
|
|
1384
|
+
const rows = saved.map((one) => ({
|
|
1385
|
+
orgId: one.orgId,
|
|
1386
|
+
orgName: one.orgName ?? null,
|
|
1387
|
+
agentName: one.agentName,
|
|
1388
|
+
current: one.current,
|
|
1389
|
+
}));
|
|
1390
|
+
print(rows.length
|
|
1391
|
+
? table([
|
|
1392
|
+
["", "account", "id", "agent"],
|
|
1393
|
+
...rows.map((row) => [
|
|
1394
|
+
row.current ? "*" : "",
|
|
1395
|
+
row.orgName ?? "-",
|
|
1396
|
+
row.orgId,
|
|
1397
|
+
row.agentName,
|
|
1398
|
+
]),
|
|
1399
|
+
])
|
|
1400
|
+
: `Not connected. Run \`${me} login\`.`, { accounts: rows, environmentKey: Boolean(env.RUNTIME_API_KEY) });
|
|
1401
|
+
return 0;
|
|
1402
|
+
}
|
|
1403
|
+
/** Fill in the saved connection's account name from the API, once. */
|
|
1404
|
+
async function nameSaved(env) {
|
|
1405
|
+
if (env.RUNTIME_API_KEY)
|
|
1406
|
+
return;
|
|
1407
|
+
const store = connectionStore(env);
|
|
1408
|
+
const saved = await store.read();
|
|
1409
|
+
if (!saved || saved.orgName)
|
|
1410
|
+
return;
|
|
1411
|
+
const who = await new Runtime({
|
|
1412
|
+
apiKey: saved.key,
|
|
1413
|
+
baseUrl: env.RUNTIME_API_URL,
|
|
1414
|
+
maxRetries: 1,
|
|
1415
|
+
}).me();
|
|
1416
|
+
if (who.orgName && who.orgId === saved.orgId) {
|
|
1417
|
+
const again = await store.read();
|
|
1418
|
+
if (again && again.key === saved.key)
|
|
1419
|
+
await store.save({ ...again, orgName: who.orgName });
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
async function keys(argv, env, out) {
|
|
1423
|
+
product = "keys";
|
|
1424
|
+
const [verb, ...rest] = argv;
|
|
1425
|
+
if (!verb || verb === "help" || verb === "--help" || verb === "-h") {
|
|
1426
|
+
out.write(out.json ? JSON.stringify({ usage: named(KEYS_HELP) }) : named(KEYS_HELP));
|
|
1427
|
+
return 0;
|
|
1428
|
+
}
|
|
1429
|
+
if (verb === "ls" || verb === "list" || verb === "revoke")
|
|
1430
|
+
throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
|
|
1431
|
+
if (verb !== "create")
|
|
1432
|
+
throw unknown("keys command", verb, ["create"]);
|
|
1433
|
+
const args = parse(rest, ["read-only"]);
|
|
1434
|
+
const known = new Set(["name", "read-only", "daily-limit"]);
|
|
1435
|
+
for (const name of args.flags.keys())
|
|
1436
|
+
if (!known.has(name))
|
|
1437
|
+
throw usage(`Unknown option --${name}.`);
|
|
1438
|
+
if (args.positional.length || args.rest)
|
|
1439
|
+
throw usage(`Unexpected ${args.positional[0] ?? "--"}.`);
|
|
1440
|
+
const readOnly = has(args, "read-only");
|
|
1441
|
+
const limit = flag(args, "daily-limit");
|
|
1442
|
+
if (readOnly && limit !== undefined)
|
|
1443
|
+
throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
|
|
1444
|
+
const created = await createKey({
|
|
1445
|
+
name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
|
|
1446
|
+
access: readOnly ? "read" : "full",
|
|
1447
|
+
dailyLimitMicros: limit === undefined ? null : dailyLimitMicros(limit),
|
|
1448
|
+
}, env, { notify: out.error });
|
|
1449
|
+
const { confirm, ...key } = created;
|
|
1450
|
+
out.write(out.json ? JSON.stringify(key) : key.key);
|
|
1451
|
+
const terms = [
|
|
1452
|
+
key.access === "read" ? "read only" : "full access",
|
|
1453
|
+
key.access === "read"
|
|
1454
|
+
? undefined
|
|
1455
|
+
: key.dailyLimitMicros === null
|
|
1456
|
+
? "no daily limit"
|
|
1457
|
+
: `daily limit $${(Number(BigInt(key.dailyLimitMicros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
|
|
1458
|
+
"until revoked",
|
|
1459
|
+
].filter(Boolean);
|
|
1460
|
+
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.`);
|
|
1461
|
+
await confirm();
|
|
1462
|
+
return 0;
|
|
1463
|
+
}
|
|
907
1464
|
const SNAPSHOT_HELP = `runtime snapshot <command>
|
|
908
1465
|
|
|
909
1466
|
ls [--sandbox <id>] [--name n] Your snapshots
|
|
@@ -960,18 +1517,34 @@ async function snapshot(argv, env, out) {
|
|
|
960
1517
|
}
|
|
961
1518
|
const IMAGE_HELP = `runtime image <command>
|
|
962
1519
|
|
|
963
|
-
build
|
|
964
|
-
[
|
|
1520
|
+
build [<folder>] [-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
|
|
1521
|
+
[-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
|
|
1522
|
+
[--start <command>] [--ready-port <port> | --ready-command <command>]
|
|
1523
|
+
[--max-image-mib 8192] [--timeout <seconds>]
|
|
965
1524
|
Build and stream the log; prints the image id when ready.
|
|
966
|
-
A Dockerfile's
|
|
967
|
-
|
|
1525
|
+
A Dockerfile's folder is the context: everything its
|
|
1526
|
+
.dockerignore leaves in (100 MiB compressed at most) is
|
|
1527
|
+
sent, and a rebuild sends only what changed.
|
|
968
1528
|
ls Your images
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1529
|
+
versions <name> Every version of a name, newest first
|
|
1530
|
+
get <image> One image: an id, name, name:tag or name@version
|
|
1531
|
+
logs <image> [--follow]
|
|
1532
|
+
Its build log
|
|
1533
|
+
tag <image> <tag> Point a tag of the image's name at this version
|
|
1534
|
+
untag <image> <tag> Take a tag off
|
|
1535
|
+
rm <image> Delete that version and its tags
|
|
1536
|
+
registry ls Registries you keep credentials for
|
|
1537
|
+
registry set <registry> --username <user>
|
|
1538
|
+
Store credentials to pull private images; the token or
|
|
1539
|
+
password is read from standard input and never shown again
|
|
1540
|
+
registry set <ecr registry> --access-key-id <id>
|
|
1541
|
+
Amazon ECR: the secret access key is read from standard input
|
|
1542
|
+
registry rm <registry>
|
|
1543
|
+
Use one: runtime sandbox create --image app:latest
|
|
973
1544
|
`;
|
|
974
|
-
async function
|
|
1545
|
+
export async function imageCommand(argv, env, out,
|
|
1546
|
+
/** Tests give the registry secret here; the CLI reads standard input. */
|
|
1547
|
+
read = readSecret) {
|
|
975
1548
|
const [verb, ...rest] = argv;
|
|
976
1549
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
977
1550
|
if (!verb || verb === "help" || verb === "--help") {
|
|
@@ -981,30 +1554,72 @@ async function image(argv, env, out) {
|
|
|
981
1554
|
const api = (await client(env)).images;
|
|
982
1555
|
switch (verb) {
|
|
983
1556
|
case "build": {
|
|
984
|
-
const args = parse(rest);
|
|
1557
|
+
const args = parse(rest, ["no-cache"]);
|
|
985
1558
|
const list = (name) => (args.flags.get(name) ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean);
|
|
986
1559
|
const recipe = { apt: list("apt"), pip: list("pip"), npm: list("npm") };
|
|
987
1560
|
const hasRecipe = recipe.apt.length + recipe.pip.length + recipe.npm.length > 0;
|
|
988
|
-
const
|
|
1561
|
+
const dockerfile = flag(args, "dockerfile") ?? flag(args, "file") ?? flag(args, "f");
|
|
1562
|
+
const folder = args.positional[0];
|
|
1563
|
+
const fromDockerfile = dockerfile !== undefined || folder !== undefined;
|
|
1564
|
+
const sources = [fromDockerfile || undefined, flag(args, "from"), hasRecipe || undefined];
|
|
989
1565
|
if (sources.filter((s) => s !== undefined).length !== 1)
|
|
990
|
-
throw usage("Give exactly one of
|
|
1566
|
+
throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
|
|
991
1567
|
const build = {
|
|
992
1568
|
...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
|
|
993
1569
|
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
994
1570
|
};
|
|
1571
|
+
// -t app, -t app:v2 -t app:latest: one name, its tags.
|
|
1572
|
+
let name = flag(args, "name");
|
|
1573
|
+
const tags = [];
|
|
1574
|
+
for (const tagged of [...(args.flags.get("t") ?? []), ...(args.flags.get("tag") ?? [])]) {
|
|
1575
|
+
const [repository, tag] = tagged.split(":");
|
|
1576
|
+
if (!repository)
|
|
1577
|
+
throw usage("-t takes name or name:tag.");
|
|
1578
|
+
if (name !== undefined && name !== repository)
|
|
1579
|
+
throw usage("Every -t names the same image; give one name and several tags.");
|
|
1580
|
+
name = repository;
|
|
1581
|
+
if (tag)
|
|
1582
|
+
tags.push(tag);
|
|
1583
|
+
}
|
|
1584
|
+
const start = {
|
|
1585
|
+
...(flag(args, "start") ? { command: flag(args, "start") } : {}),
|
|
1586
|
+
...(integer(args, "ready-port") ? { readyPort: integer(args, "ready-port") } : {}),
|
|
1587
|
+
...(flag(args, "ready-command") ? { readyCommand: flag(args, "ready-command") } : {}),
|
|
1588
|
+
...(integer(args, "ready-timeout")
|
|
1589
|
+
? { readyTimeoutSeconds: integer(args, "ready-timeout") }
|
|
1590
|
+
: {}),
|
|
1591
|
+
};
|
|
995
1592
|
const common = {
|
|
996
|
-
...(
|
|
1593
|
+
...(name ? { name } : {}),
|
|
1594
|
+
...(tags.length ? { tags } : {}),
|
|
997
1595
|
...(Object.keys(build).length ? { build } : {}),
|
|
1596
|
+
...(Object.keys(start).length ? { start } : {}),
|
|
1597
|
+
...(has(args, "no-cache") ? { cache: false } : {}),
|
|
998
1598
|
};
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1599
|
+
let input;
|
|
1600
|
+
if (fromDockerfile) {
|
|
1601
|
+
const context = resolve(folder ?? dirname(dockerfile));
|
|
1602
|
+
const file = resolve(context, dockerfile ?? "Dockerfile");
|
|
1603
|
+
const text = await readFile(file, "utf8").catch(() => {
|
|
1604
|
+
throw usage(`No Dockerfile at ${file}.`);
|
|
1605
|
+
});
|
|
1606
|
+
input = {
|
|
1607
|
+
...common,
|
|
1608
|
+
dockerfile: text,
|
|
1609
|
+
contextDir: context,
|
|
1610
|
+
...(flag(args, "target") ? { target: flag(args, "target") } : {}),
|
|
1611
|
+
...(args.flags.get("build-arg") ? { buildArgs: pairs(args, "build-arg") } : {}),
|
|
1612
|
+
};
|
|
1613
|
+
if (!out.json)
|
|
1614
|
+
out.error(`Sending the build context from ${context}`);
|
|
1615
|
+
}
|
|
1616
|
+
else if (flag(args, "from"))
|
|
1617
|
+
input = { ...common, image: flag(args, "from") };
|
|
1618
|
+
else
|
|
1619
|
+
input = {
|
|
1620
|
+
...common,
|
|
1621
|
+
recipe: Object.fromEntries(Object.entries(recipe).filter(([, v]) => v.length)),
|
|
1622
|
+
};
|
|
1008
1623
|
const built = await api.build(input, {
|
|
1009
1624
|
...(out.json ? {} : { onLog: (line) => out.error(line.text) }),
|
|
1010
1625
|
});
|
|
@@ -1012,29 +1627,43 @@ async function image(argv, env, out) {
|
|
|
1012
1627
|
return 0;
|
|
1013
1628
|
}
|
|
1014
1629
|
case "ls":
|
|
1015
|
-
case "list":
|
|
1016
|
-
|
|
1630
|
+
case "list":
|
|
1631
|
+
case "versions": {
|
|
1632
|
+
const name = verb === "versions" ? need(rest[0], "a name") : undefined;
|
|
1633
|
+
const all = await (await api.list(name ? { name } : {})).toArray(1000);
|
|
1017
1634
|
print(all.length
|
|
1018
1635
|
? table([
|
|
1019
|
-
["ID", "NAME", "STATE", "MIB", "CREATED"],
|
|
1636
|
+
["ID", "NAME", "VERSION", "TAGS", "STATE", "MIB", "CREATED"],
|
|
1020
1637
|
...all.map((i) => [
|
|
1021
1638
|
i.id,
|
|
1022
1639
|
i.name ?? "-",
|
|
1640
|
+
i.version === null || i.version === undefined ? "-" : String(i.version),
|
|
1641
|
+
i.tags?.length ? i.tags.join(",") : "-",
|
|
1023
1642
|
i.state,
|
|
1024
1643
|
String(i.sizeMiB ?? "-"),
|
|
1025
1644
|
i.createdAt,
|
|
1026
1645
|
]),
|
|
1027
1646
|
])
|
|
1028
|
-
:
|
|
1647
|
+
: name
|
|
1648
|
+
? `No versions of ${name}.`
|
|
1649
|
+
: "No images. Build one: runtime image build --pip pandas", all);
|
|
1029
1650
|
return 0;
|
|
1030
1651
|
}
|
|
1031
1652
|
case "get": {
|
|
1032
|
-
const found = await api.
|
|
1653
|
+
const found = await api.resolve(need(rest[0], "an image"));
|
|
1033
1654
|
print(JSON.stringify(found, null, 2), found);
|
|
1034
1655
|
return 0;
|
|
1035
1656
|
}
|
|
1036
1657
|
case "logs": {
|
|
1037
|
-
const
|
|
1658
|
+
const args = parse(rest, ["follow", "f"]);
|
|
1659
|
+
const id = (await api.resolve(need(args.positional[0], "an image"))).id;
|
|
1660
|
+
if (has(args, "follow") || has(args, "f")) {
|
|
1661
|
+
const lines = [];
|
|
1662
|
+
const done = await api.followLogs(id, (line) => out.json ? lines.push(line.text) : out.write(line.text));
|
|
1663
|
+
if (out.json)
|
|
1664
|
+
print("", { lines, state: done.state });
|
|
1665
|
+
return done.state === "failed" ? 1 : 0;
|
|
1666
|
+
}
|
|
1038
1667
|
let after = 0;
|
|
1039
1668
|
const lines = [];
|
|
1040
1669
|
for (;;) {
|
|
@@ -1047,76 +1676,69 @@ async function image(argv, env, out) {
|
|
|
1047
1676
|
print(lines.join("\n") || "(no log yet)", lines);
|
|
1048
1677
|
return 0;
|
|
1049
1678
|
}
|
|
1679
|
+
case "tag":
|
|
1680
|
+
case "untag": {
|
|
1681
|
+
const ref = need(rest[0], "an image");
|
|
1682
|
+
const tag = need(rest[1], "a tag");
|
|
1683
|
+
const found = verb === "tag" ? await api.tag(ref, tag) : await api.untag(ref, tag);
|
|
1684
|
+
print(`${found.name ?? found.id}@${found.version ?? "-"} tags: ${found.tags.join(", ") || "none"}`, found);
|
|
1685
|
+
return 0;
|
|
1686
|
+
}
|
|
1050
1687
|
case "rm":
|
|
1051
1688
|
case "delete": {
|
|
1052
|
-
const found = await api.delete(need(rest[0], "an image
|
|
1689
|
+
const found = await api.delete(need(rest[0], "an image"));
|
|
1053
1690
|
print(`${found.id} ${found.state}`, found);
|
|
1054
1691
|
return 0;
|
|
1055
1692
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
throw usage("The Dockerfile's context is over 256 files or 1 MiB. Build from --from, or install packages with --pip/--apt/--npm.");
|
|
1093
|
-
files.push({
|
|
1094
|
-
path: name,
|
|
1095
|
-
content: bytes.toString("base64"),
|
|
1096
|
-
encoding: "base64",
|
|
1097
|
-
mode: info.mode & 0o777,
|
|
1098
|
-
});
|
|
1099
|
-
};
|
|
1100
|
-
for (const source of sources) {
|
|
1101
|
-
const absolute = resolve(root, source);
|
|
1102
|
-
if (absolute !== root && !absolute.startsWith(root + sep))
|
|
1103
|
-
throw usage(`COPY ${source} is outside the Dockerfile's folder.`);
|
|
1104
|
-
if (/[*?[]/.test(source)) {
|
|
1105
|
-
const pattern = new RegExp(`^${source
|
|
1106
|
-
.split("/")
|
|
1107
|
-
.at(-1)
|
|
1108
|
-
.replace(/[.+^${}()|\\]/g, "\\$&")
|
|
1109
|
-
.replace(/\*/g, "[^/]*")
|
|
1110
|
-
.replace(/\?/g, "[^/]")}$`);
|
|
1111
|
-
const folder = dirname(absolute);
|
|
1112
|
-
for (const entry of await readdir(folder))
|
|
1113
|
-
if (pattern.test(entry))
|
|
1114
|
-
await add(join(folder, entry));
|
|
1693
|
+
case "registry":
|
|
1694
|
+
case "registries": {
|
|
1695
|
+
const [action, ...more] = rest;
|
|
1696
|
+
if (action === "ls" || action === "list" || action === undefined) {
|
|
1697
|
+
const all = await api.registries.list();
|
|
1698
|
+
print(all.length
|
|
1699
|
+
? table([
|
|
1700
|
+
["REGISTRY", "KIND", "USER", "SINCE"],
|
|
1701
|
+
...all.map((r) => [r.registry, r.kind, r.username, r.createdAt]),
|
|
1702
|
+
])
|
|
1703
|
+
: "No registry credentials. Store some: runtime image registry set ghcr.io --username you", all);
|
|
1704
|
+
return 0;
|
|
1705
|
+
}
|
|
1706
|
+
if (action === "set") {
|
|
1707
|
+
const args = parse(more);
|
|
1708
|
+
const registry = need(args.positional[0], "a registry, such as ghcr.io");
|
|
1709
|
+
const accessKeyId = flag(args, "access-key-id");
|
|
1710
|
+
if (accessKeyId === undefined && flag(args, "username") === undefined)
|
|
1711
|
+
throw usage("Give --username (or --access-key-id for Amazon ECR).");
|
|
1712
|
+
if (!out.json && process.stdin.isTTY)
|
|
1713
|
+
out.error(accessKeyId ? "The secret access key:" : "The token or password:");
|
|
1714
|
+
const secret = await read();
|
|
1715
|
+
if (!secret)
|
|
1716
|
+
throw usage("Nothing on standard input.");
|
|
1717
|
+
const saved = await api.registries.set(accessKeyId
|
|
1718
|
+
? { registry, accessKeyId, secretAccessKey: secret }
|
|
1719
|
+
: { registry, username: flag(args, "username"), password: secret });
|
|
1720
|
+
print(`Stored credentials for ${saved.registry} (${saved.username}).`, saved);
|
|
1721
|
+
return 0;
|
|
1722
|
+
}
|
|
1723
|
+
if (action === "rm" || action === "delete") {
|
|
1724
|
+
const gone = await api.registries.delete(need(more[0], "a registry"));
|
|
1725
|
+
print(gone.deleted ? `Forgot ${gone.registry}.` : `Nothing stored for ${gone.registry}.`, gone);
|
|
1726
|
+
return 0;
|
|
1727
|
+
}
|
|
1728
|
+
throw unknown("registry command", action, ["ls", "set", "rm"]);
|
|
1115
1729
|
}
|
|
1116
|
-
else
|
|
1117
|
-
await add(absolute);
|
|
1118
1730
|
}
|
|
1119
|
-
|
|
1731
|
+
throw unknown("image command", verb, [
|
|
1732
|
+
"build",
|
|
1733
|
+
"ls",
|
|
1734
|
+
"versions",
|
|
1735
|
+
"get",
|
|
1736
|
+
"logs",
|
|
1737
|
+
"tag",
|
|
1738
|
+
"untag",
|
|
1739
|
+
"rm",
|
|
1740
|
+
"registry",
|
|
1741
|
+
]);
|
|
1120
1742
|
}
|
|
1121
1743
|
const VOLUME_HELP = `runtime volume <command>
|
|
1122
1744
|
|
|
@@ -1179,7 +1801,15 @@ async function volume(argv, env, out) {
|
|
|
1179
1801
|
throw unknown("volume command", verb, ["create", "ls", "get", "rm"]);
|
|
1180
1802
|
}
|
|
1181
1803
|
/** Flags `sandbox create` and `run` take without a value. */
|
|
1182
|
-
const CREATE_FLAGS = [
|
|
1804
|
+
const CREATE_FLAGS = [
|
|
1805
|
+
"trial",
|
|
1806
|
+
"paid",
|
|
1807
|
+
"no-wait",
|
|
1808
|
+
"no-internet",
|
|
1809
|
+
"no-auto-wake",
|
|
1810
|
+
"persistent",
|
|
1811
|
+
"get-or-create",
|
|
1812
|
+
];
|
|
1183
1813
|
/** A create body from the command line's flags. */
|
|
1184
1814
|
function createInput(args) {
|
|
1185
1815
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
@@ -1204,6 +1834,12 @@ function createInput(args) {
|
|
|
1204
1834
|
...(flag(args, "on-timeout")
|
|
1205
1835
|
? { onLeaseEnd: flag(args, "on-timeout") }
|
|
1206
1836
|
: {}),
|
|
1837
|
+
...(integer(args, "idle-pause") !== undefined
|
|
1838
|
+
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
1839
|
+
: {}),
|
|
1840
|
+
...(has(args, "no-auto-wake") ? { autoWake: false } : {}),
|
|
1841
|
+
...(has(args, "persistent") ? { persistent: true } : {}),
|
|
1842
|
+
...(has(args, "get-or-create") ? { getOrCreate: true } : {}),
|
|
1207
1843
|
...(has(args, "trial")
|
|
1208
1844
|
? { funding: "trial" }
|
|
1209
1845
|
: has(args, "paid")
|
|
@@ -1296,3 +1932,224 @@ if (import.meta.main ||
|
|
|
1296
1932
|
process.exitCode = 1;
|
|
1297
1933
|
});
|
|
1298
1934
|
}
|
|
1935
|
+
// ------------------------------------------------------------------ observability
|
|
1936
|
+
/** A sparkline of up to 60 points, oldest first. */
|
|
1937
|
+
function spark(values) {
|
|
1938
|
+
const bars = "▁▂▃▄▅▆▇█";
|
|
1939
|
+
const known = values.filter((v) => v !== null);
|
|
1940
|
+
const top = Math.max(1, ...known);
|
|
1941
|
+
const step = Math.max(1, Math.ceil(values.length / 60));
|
|
1942
|
+
const out = [];
|
|
1943
|
+
for (let i = 0; i < values.length; i += step) {
|
|
1944
|
+
const slice = values.slice(i, i + step).filter((v) => v !== null);
|
|
1945
|
+
out.push(slice.length ? bars[Math.min(7, Math.round((Math.max(...slice) / top) * 7))] : " ");
|
|
1946
|
+
}
|
|
1947
|
+
return out.join("");
|
|
1948
|
+
}
|
|
1949
|
+
const mib = (bytes) => `${Math.round(bytes / 1_048_576).toLocaleString("en-US")} MiB`;
|
|
1950
|
+
function metricsText(m) {
|
|
1951
|
+
const cpu = m.points.map((p) => p.cpuPercent);
|
|
1952
|
+
const memory = m.points.map((p) => p.memoryBytes);
|
|
1953
|
+
const peak = Math.max(0, ...m.points.map((p) => p.cpuPeakPercent ?? p.cpuPercent ?? 0));
|
|
1954
|
+
const latest = m.latest;
|
|
1955
|
+
return [
|
|
1956
|
+
table([
|
|
1957
|
+
["state", m.state],
|
|
1958
|
+
[
|
|
1959
|
+
"cpu now",
|
|
1960
|
+
latest?.cpuPercent == null
|
|
1961
|
+
? "no reading in the last 15 minutes"
|
|
1962
|
+
: `${latest.cpuPercent}% of ${m.vcpu} vCPU (${latest.cpuCores} cores)`,
|
|
1963
|
+
],
|
|
1964
|
+
["memory now", latest ? `${mib(latest.memoryBytes)} of ${mib(m.memoryLimitBytes)}` : "-"],
|
|
1965
|
+
[`cpu, ${m.range}`, m.points.length ? `${spark(cpu)} peak ${peak}%` : "no readings"],
|
|
1966
|
+
[
|
|
1967
|
+
`memory, ${m.range}`,
|
|
1968
|
+
m.points.length
|
|
1969
|
+
? `${spark(memory)} peak ${mib(Math.max(...m.points.map((p) => p.memoryPeakBytes)))}`
|
|
1970
|
+
: "no readings",
|
|
1971
|
+
],
|
|
1972
|
+
]),
|
|
1973
|
+
].join("\n");
|
|
1974
|
+
}
|
|
1975
|
+
const WEBHOOK_VERBS = [
|
|
1976
|
+
"ls",
|
|
1977
|
+
"create",
|
|
1978
|
+
"get",
|
|
1979
|
+
"update",
|
|
1980
|
+
"test",
|
|
1981
|
+
"deliveries",
|
|
1982
|
+
"retry",
|
|
1983
|
+
"rotate-secret",
|
|
1984
|
+
"rm",
|
|
1985
|
+
];
|
|
1986
|
+
async function webhooksCommand(argv, env, out) {
|
|
1987
|
+
const [verb = "ls", ...rest] = argv;
|
|
1988
|
+
product = "webhooks";
|
|
1989
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1990
|
+
if (verb === "help" || verb === "--help") {
|
|
1991
|
+
out.write(HELP.split("\n")
|
|
1992
|
+
.filter((line) => /webhooks|^\s{11}\|/.test(line))
|
|
1993
|
+
.join("\n"));
|
|
1994
|
+
return 0;
|
|
1995
|
+
}
|
|
1996
|
+
const hooks = (await client(env)).webhooks;
|
|
1997
|
+
const args = parse(rest, ["enable", "disable"]);
|
|
1998
|
+
const id = () => need(args.positional[0], "a webhook id");
|
|
1999
|
+
const events = () => flag(args, "events")
|
|
2000
|
+
?.split(",")
|
|
2001
|
+
.map((e) => e.trim())
|
|
2002
|
+
.filter(Boolean);
|
|
2003
|
+
const row = (h) => [
|
|
2004
|
+
h.id,
|
|
2005
|
+
h.url,
|
|
2006
|
+
h.enabled ? (h.failingSince ? `failing since ${h.failingSince}` : "on") : "off",
|
|
2007
|
+
h.events.join(","),
|
|
2008
|
+
];
|
|
2009
|
+
switch (verb) {
|
|
2010
|
+
case "ls":
|
|
2011
|
+
case "list": {
|
|
2012
|
+
const page = await hooks.list();
|
|
2013
|
+
print(page.data.length
|
|
2014
|
+
? table(page.data.map(row))
|
|
2015
|
+
: "No webhooks. Add one: runtime webhooks create <url>", page);
|
|
2016
|
+
return 0;
|
|
2017
|
+
}
|
|
2018
|
+
case "get": {
|
|
2019
|
+
const h = await hooks.get(id());
|
|
2020
|
+
print(table([row(h)]), h);
|
|
2021
|
+
return 0;
|
|
2022
|
+
}
|
|
2023
|
+
case "create":
|
|
2024
|
+
case "add": {
|
|
2025
|
+
const h = await hooks.create({
|
|
2026
|
+
url: need(args.positional[0], "the https URL to send to"),
|
|
2027
|
+
...(events() ? { events: events() } : {}),
|
|
2028
|
+
...(flag(args, "description") ? { description: flag(args, "description") } : {}),
|
|
2029
|
+
});
|
|
2030
|
+
print(`${h.id}\nSigning secret (shown once, keep it): ${h.secret}\nVerify deliveries with verifyWebhook from the withruntime SDK.`, h);
|
|
2031
|
+
return 0;
|
|
2032
|
+
}
|
|
2033
|
+
case "update": {
|
|
2034
|
+
const h = await hooks.update(id(), {
|
|
2035
|
+
...(flag(args, "url") ? { url: flag(args, "url") } : {}),
|
|
2036
|
+
...(events() ? { events: events() } : {}),
|
|
2037
|
+
...(has(args, "enable") ? { enabled: true } : {}),
|
|
2038
|
+
...(has(args, "disable") ? { enabled: false } : {}),
|
|
2039
|
+
});
|
|
2040
|
+
print(table([row(h)]), h);
|
|
2041
|
+
return 0;
|
|
2042
|
+
}
|
|
2043
|
+
case "rotate-secret": {
|
|
2044
|
+
const keep = integer(args, "keep");
|
|
2045
|
+
const h = await hooks.rotateSecret(id(), keep === undefined ? {} : { keepPreviousSeconds: keep });
|
|
2046
|
+
print(`New signing secret (shown once): ${h.secret}${h.previousSecretExpiresAt ? `\nThe old one also signs until ${h.previousSecretExpiresAt}.` : ""}`, h);
|
|
2047
|
+
return 0;
|
|
2048
|
+
}
|
|
2049
|
+
case "test": {
|
|
2050
|
+
const d = await hooks.test(id());
|
|
2051
|
+
print(d.state === "succeeded"
|
|
2052
|
+
? `Delivered: your endpoint answered ${d.lastStatus} in ${d.lastDurationMs} ms.`
|
|
2053
|
+
: d.state === "pending"
|
|
2054
|
+
? "Sent; no answer yet. See it with: runtime webhooks deliveries " + id()
|
|
2055
|
+
: `Not delivered: ${d.lastError ?? d.state}${d.lastStatus ? ` (HTTP ${d.lastStatus})` : ""}.`, d);
|
|
2056
|
+
return d.state === "failed" ? 1 : 0;
|
|
2057
|
+
}
|
|
2058
|
+
case "deliveries": {
|
|
2059
|
+
const page = await hooks.deliveries(id(), {
|
|
2060
|
+
...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
|
|
2061
|
+
});
|
|
2062
|
+
print(page.data.length
|
|
2063
|
+
? table(page.data.map((d) => [
|
|
2064
|
+
d.id,
|
|
2065
|
+
d.createdAt,
|
|
2066
|
+
d.eventType,
|
|
2067
|
+
d.state,
|
|
2068
|
+
`${d.attempts}/${d.maxAttempts}`,
|
|
2069
|
+
d.lastStatus ? String(d.lastStatus) : (d.lastError ?? "-"),
|
|
2070
|
+
]))
|
|
2071
|
+
: "No deliveries yet.", page);
|
|
2072
|
+
return 0;
|
|
2073
|
+
}
|
|
2074
|
+
case "retry": {
|
|
2075
|
+
const d = await hooks.retry(need(args.positional[0], "a delivery id"));
|
|
2076
|
+
print(`Queued ${d.id} to send again.`, d);
|
|
2077
|
+
return 0;
|
|
2078
|
+
}
|
|
2079
|
+
case "rm":
|
|
2080
|
+
case "delete": {
|
|
2081
|
+
print("Deleted.", await hooks.delete(id()));
|
|
2082
|
+
return 0;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
throw unknown("webhooks command", verb, WEBHOOK_VERBS);
|
|
2086
|
+
}
|
|
2087
|
+
const OTEL_VERBS = ["ls", "create", "get", "update", "flush", "rm"];
|
|
2088
|
+
async function otelCommand(argv, env, out) {
|
|
2089
|
+
const [verb = "ls", ...rest] = argv;
|
|
2090
|
+
product = "otel";
|
|
2091
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
2092
|
+
const otel = (await client(env)).otel;
|
|
2093
|
+
const args = parse(rest, ["enable", "disable"]);
|
|
2094
|
+
const id = () => need(args.positional[0], "an export id");
|
|
2095
|
+
const signals = () => flag(args, "signals")
|
|
2096
|
+
?.split(",")
|
|
2097
|
+
.map((s) => s.trim());
|
|
2098
|
+
const row = (x) => [
|
|
2099
|
+
x.id,
|
|
2100
|
+
x.endpoint,
|
|
2101
|
+
x.enabled ? "on" : "off",
|
|
2102
|
+
x.signals.join(","),
|
|
2103
|
+
x.lastError
|
|
2104
|
+
? `last error: ${x.lastError}`
|
|
2105
|
+
: x.lastSuccessAt
|
|
2106
|
+
? `sent ${x.lastSuccessAt}`
|
|
2107
|
+
: "not sent yet",
|
|
2108
|
+
];
|
|
2109
|
+
switch (verb) {
|
|
2110
|
+
case "ls":
|
|
2111
|
+
case "list": {
|
|
2112
|
+
const page = await otel.list();
|
|
2113
|
+
print(page.data.length
|
|
2114
|
+
? table(page.data.map(row))
|
|
2115
|
+
: "No exports. Add one: runtime otel create <endpoint>", page);
|
|
2116
|
+
return 0;
|
|
2117
|
+
}
|
|
2118
|
+
case "get": {
|
|
2119
|
+
const x = await otel.get(id());
|
|
2120
|
+
print(table([row(x)]), x);
|
|
2121
|
+
return 0;
|
|
2122
|
+
}
|
|
2123
|
+
case "create":
|
|
2124
|
+
case "add": {
|
|
2125
|
+
const x = await otel.create({
|
|
2126
|
+
endpoint: need(args.positional[0], "the OTLP/HTTP endpoint"),
|
|
2127
|
+
...(args.flags.has("header") ? { headers: pairs(args, "header") } : {}),
|
|
2128
|
+
...(signals() ? { signals: signals() } : {}),
|
|
2129
|
+
});
|
|
2130
|
+
print(`${x.id}\nPushing ${x.signals.join(" and ")} to ${x.endpoint} about every 30 seconds.`, x);
|
|
2131
|
+
return 0;
|
|
2132
|
+
}
|
|
2133
|
+
case "update": {
|
|
2134
|
+
const x = await otel.update(id(), {
|
|
2135
|
+
...(flag(args, "endpoint") ? { endpoint: flag(args, "endpoint") } : {}),
|
|
2136
|
+
...(args.flags.has("header") ? { headers: pairs(args, "header") } : {}),
|
|
2137
|
+
...(signals() ? { signals: signals() } : {}),
|
|
2138
|
+
...(has(args, "enable") ? { enabled: true } : {}),
|
|
2139
|
+
...(has(args, "disable") ? { enabled: false } : {}),
|
|
2140
|
+
});
|
|
2141
|
+
print(table([row(x)]), x);
|
|
2142
|
+
return 0;
|
|
2143
|
+
}
|
|
2144
|
+
case "flush": {
|
|
2145
|
+
print("Pushing now; `runtime otel get <id>` shows the answer in a few seconds.", await otel.flush(id()));
|
|
2146
|
+
return 0;
|
|
2147
|
+
}
|
|
2148
|
+
case "rm":
|
|
2149
|
+
case "delete": {
|
|
2150
|
+
print("Deleted.", await otel.delete(id()));
|
|
2151
|
+
return 0;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
throw unknown("otel command", verb, OTEL_VERBS);
|
|
2155
|
+
}
|