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.
- package/DAYTONA.md +129 -0
- package/E2B.md +146 -0
- package/README.md +84 -12
- 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 +949 -105
- 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 +26 -0
- package/dist/daytona/context.js +22 -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 +339 -0
- package/dist/daytona/sandbox.d.ts +196 -0
- package/dist/daytona/sandbox.js +448 -0
- package/dist/e2b/client.d.ts +58 -0
- package/dist/e2b/client.js +66 -0
- package/dist/e2b/code-interpreter.d.ts +144 -0
- package/dist/e2b/code-interpreter.js +244 -0
- package/dist/e2b/commands.d.ts +100 -0
- package/dist/e2b/commands.js +268 -0
- package/dist/e2b/errors.d.ts +72 -0
- package/dist/e2b/errors.js +151 -0
- package/dist/e2b/filesystem.d.ts +82 -0
- package/dist/e2b/filesystem.js +143 -0
- package/dist/e2b/index.d.ts +28 -0
- package/dist/e2b/index.js +61 -0
- package/dist/e2b/sandbox.d.ts +203 -0
- package/dist/e2b/sandbox.js +490 -0
- package/dist/e2b/unsupported.d.ts +19 -0
- package/dist/e2b/unsupported.js +27 -0
- 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 +27 -6
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +4 -3
- 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 +89 -0
- package/dist/products/switching.js +25 -0
- package/dist/proxy.d.ts +30 -0
- package/dist/proxy.js +178 -0
- package/dist/sandbox.d.ts +30 -2
- package/dist/sandbox.js +101 -1
- package/dist/ssh.d.ts +21 -0
- package/dist/ssh.js +312 -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 +76 -16
- package/dist/tunnel.d.ts +67 -0
- package/dist/tunnel.js +311 -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 +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 {
|
|
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";
|
|
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>
|
|
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,
|
|
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 [
|
|
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)'
|
|
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
|
|
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
|
|
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
|
|
957
|
-
[
|
|
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
|
|
960
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
...(
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
-
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
|
-
|
|
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 = [
|
|
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
|
+
}
|