withruntime 0.5.1 → 0.6.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/LICENSE +55 -0
- package/README.md +8 -4
- package/dist/ai-harness/index.d.ts +194 -0
- package/dist/ai-harness/index.js +504 -0
- package/dist/cli-extras.d.ts +24 -0
- package/dist/cli-extras.js +205 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +403 -28
- package/dist/e2b/filesystem.d.ts +26 -1
- package/dist/e2b/filesystem.js +30 -2
- package/dist/e2b/index.d.ts +1 -1
- package/dist/e2b/index.js +1 -1
- package/dist/errors.js +2 -0
- package/dist/identity.d.ts +27 -0
- package/dist/identity.js +56 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/mcp.js +16 -3
- package/dist/network-cli.d.ts +15 -0
- package/dist/network-cli.js +323 -0
- package/dist/products/desktop.d.ts +41 -4
- package/dist/products/desktop.js +73 -3
- package/dist/products/index.d.ts +12 -0
- package/dist/products/index.js +12 -0
- package/dist/products/interpreter.d.ts +5 -3
- package/dist/products/interpreter.js +2 -1
- package/dist/products/mcp.d.ts +79 -0
- package/dist/products/mcp.js +37 -0
- package/dist/products/mounts.d.ts +49 -0
- package/dist/products/mounts.js +21 -0
- package/dist/products/network-products.d.ts +167 -0
- package/dist/products/network-products.js +82 -0
- package/dist/products/network.d.ts +1 -1
- package/dist/products/sso.d.ts +44 -0
- package/dist/products/sso.js +8 -0
- package/dist/products/volumes.d.ts +68 -5
- package/dist/products/volumes.js +52 -4
- package/dist/products/watch.d.ts +91 -0
- package/dist/products/watch.js +135 -0
- package/dist/sandbox.d.ts +32 -2
- package/dist/sandbox.js +93 -16
- package/dist/snapshots.d.ts +11 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +4 -1
- package/dist/wireguard.d.ts +7 -0
- package/dist/wireguard.js +13 -0
- package/package.json +49 -18
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { connectionStore, resolveCredential } from "./credentials.js";
|
|
@@ -8,8 +8,10 @@ import { authenticationCommand, me, named } from "./login.js";
|
|
|
8
8
|
import { createKey, dailyLimitMicros } from "./keys.js";
|
|
9
9
|
import { Runtime } from "./client.js";
|
|
10
10
|
import { RuntimeError } from "./errors.js";
|
|
11
|
+
import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
|
|
11
12
|
import { envFetch } from "./proxy.js";
|
|
12
|
-
import {
|
|
13
|
+
import { identityToken } from "./identity.js";
|
|
14
|
+
import { portForwardCommand, resolveSandbox, sshCommand } from "./ssh.js";
|
|
13
15
|
import { VERSION } from "./transport.js";
|
|
14
16
|
const HELP = `Runtime Cloud CLI ${VERSION}
|
|
15
17
|
|
|
@@ -30,6 +32,8 @@ Account
|
|
|
30
32
|
account add Connect another account (choose it in the browser)
|
|
31
33
|
audit [--action member.] [--limit 50] [--before <next>]
|
|
32
34
|
The account's audit log (owner or admin keys)
|
|
35
|
+
sso Single sign-on and SCIM directory sync (owner or
|
|
36
|
+
admin keys); an owner changes it on the website
|
|
33
37
|
usage Balance, trial time and charges
|
|
34
38
|
limits Whether this key is read-only, and its daily spending limit
|
|
35
39
|
secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
|
|
@@ -76,6 +80,10 @@ Products
|
|
|
76
80
|
snapshot Saved sandboxes, disk and memory, to start or fork from
|
|
77
81
|
image Custom images from a Dockerfile, any public or private image, or a package list
|
|
78
82
|
volume Persistent disks to attach to sandboxes
|
|
83
|
+
domain Your own hostname for a sandbox's port, with HTTPS
|
|
84
|
+
port A public TCP port to a sandbox: databases, game servers
|
|
85
|
+
address A dedicated outbound address, for allow-lists
|
|
86
|
+
tunnel A WireGuard tunnel from your own network into your sandboxes
|
|
79
87
|
Each has its own help: \`runtime sandbox help\`, \`runtime image help\`...
|
|
80
88
|
|
|
81
89
|
Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
|
|
@@ -83,6 +91,8 @@ Behind a proxy: HTTPS_PROXY and NO_PROXY are read, on Node and Bun alike.
|
|
|
83
91
|
`;
|
|
84
92
|
const SANDBOX_HELP = `runtime sandbox <command>
|
|
85
93
|
|
|
94
|
+
Every <id> below can also be the sandbox's name.
|
|
95
|
+
|
|
86
96
|
run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
|
|
87
97
|
ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
|
|
88
98
|
port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
|
|
@@ -98,7 +108,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
98
108
|
Run and stream output; exits with its code
|
|
99
109
|
spawn <id> [--pty] -- <command...> Start in the background; prints a process id
|
|
100
110
|
ps <id> Running and recent processes
|
|
101
|
-
logs <id> <processId> [-f] A process's output
|
|
111
|
+
logs <id> <processId> [-f] A process's output so far; -f follows it to the end
|
|
102
112
|
kill <id> <processId> [--signal SIGTERM]
|
|
103
113
|
shell <id> [--command "bash -l"] An interactive terminal
|
|
104
114
|
cp <src> <dst> Copy files or directories; name a sandbox
|
|
@@ -111,19 +121,35 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
111
121
|
[--persistent on|off] [--max-total-cost <usd>|none]
|
|
112
122
|
Change its settings; persistent keeps it running
|
|
113
123
|
snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
|
|
124
|
+
mount <id> <s3|r2|gcs>://<bucket>[/prefix] <path> [--secret NAME] [--region r]
|
|
125
|
+
[--endpoint https://...] [--account-id id] [--read-only]
|
|
126
|
+
Your bucket as a directory; the proxy signs, the sandbox never sees the key
|
|
127
|
+
mounts <id> | unmount <id> <path> Its bucket mounts; unmount one
|
|
114
128
|
fork <id> [--count 3] [--name n] [--keep-snapshot] [--trial|--paid]
|
|
115
129
|
Copies of it as it is now, running; prints their ids
|
|
116
|
-
run-code <id> <file|-> [--lang python|javascript] [--context c] [--out-dir .]
|
|
130
|
+
run-code <id> <file|-> [--lang python|javascript|typescript|r|java|bash|go] [--context c] [--out-dir .]
|
|
117
131
|
Run a notebook-style cell; charts saved as PNG
|
|
132
|
+
watch <id> <path> [--recursive] [--include <glob>]... [--exclude <glob>]... [--events create,write,remove,rename,chmod]
|
|
133
|
+
Print file changes as they happen, until Ctrl-C
|
|
134
|
+
mcp catalog MCP servers a sandbox can run (licence, settings, hosts)
|
|
135
|
+
mcp <id> start <server>... [--secret SERVER.SETTING=SECRET]... [--env SERVER.NAME=value]...
|
|
136
|
+
[--option SERVER.NAME=value]... [--port 8765] [--replace]
|
|
137
|
+
Start them; prints each URL and the header to send
|
|
138
|
+
mcp <id> [status] | mcp <id> stop Their state and fresh URLs; stop them
|
|
118
139
|
preview <id> <port> [--public] [--ttl <s>] Share a port at an HTTPS address
|
|
119
140
|
previews <id> Every shared port
|
|
120
141
|
unshare <id> <port> Stop sharing a port
|
|
121
142
|
network <id> [--no-internet|--internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
122
143
|
Show its network rules, or replace them
|
|
144
|
+
identity-token --audience <aud> [--lifetime <seconds>]
|
|
145
|
+
Inside a sandbox: an OIDC token naming it, for AWS,
|
|
146
|
+
Google Cloud or your own API. Needs no API key
|
|
123
147
|
metrics <id> [--range 15m|1h|6h|24h|7d|30d]
|
|
124
148
|
CPU and memory over time, and the latest reading
|
|
125
149
|
desktop <id> start | stop | screenshot [file.png] | open <url> | click <x> <y>
|
|
126
150
|
| type <text> | press <keys> | windows
|
|
151
|
+
desktop <id> record start [--fps 10] [--max-seconds 1800] [--max-mib 512] | record stop <recId>
|
|
152
|
+
| record ls | record fetch <recId> [file.mp4] | record rm <recId>
|
|
127
153
|
|
|
128
154
|
Examples
|
|
129
155
|
id=$(runtime sandbox create)
|
|
@@ -169,6 +195,17 @@ const pairs = (args, name) => Object.fromEntries((args.flags.get(name) ?? []).ma
|
|
|
169
195
|
throw usage(`--${name} takes KEY=VALUE.`);
|
|
170
196
|
return [pair.slice(0, at), pair.slice(at + 1)];
|
|
171
197
|
}));
|
|
198
|
+
/** What the commands in cli-extras.ts use of this file. */
|
|
199
|
+
const kit = {
|
|
200
|
+
parse: (argv, booleans) => parse(argv, booleans),
|
|
201
|
+
flag,
|
|
202
|
+
has,
|
|
203
|
+
integer,
|
|
204
|
+
pairs,
|
|
205
|
+
need: (value, what) => need(value, what),
|
|
206
|
+
table: (rows) => table(rows),
|
|
207
|
+
usage: (message) => usage(message),
|
|
208
|
+
};
|
|
172
209
|
/** The product being run, so a mistake points at its own help. */
|
|
173
210
|
let product = "";
|
|
174
211
|
function usage(message) {
|
|
@@ -201,11 +238,14 @@ function unknown(what, word, choices) {
|
|
|
201
238
|
return usage(`Unknown ${what} ${word}.${meant ? ` Did you mean ${meant}?` : ""}`);
|
|
202
239
|
}
|
|
203
240
|
const COMMANDS = [
|
|
241
|
+
"account",
|
|
242
|
+
"audit",
|
|
204
243
|
"login",
|
|
205
244
|
"logout",
|
|
206
245
|
"whoami",
|
|
207
246
|
"usage",
|
|
208
247
|
"limits",
|
|
248
|
+
"sso",
|
|
209
249
|
"secrets",
|
|
210
250
|
"keys",
|
|
211
251
|
"referrals",
|
|
@@ -220,6 +260,10 @@ const COMMANDS = [
|
|
|
220
260
|
"snapshot",
|
|
221
261
|
"image",
|
|
222
262
|
"volume",
|
|
263
|
+
"domain",
|
|
264
|
+
"port",
|
|
265
|
+
"address",
|
|
266
|
+
"tunnel",
|
|
223
267
|
"webhooks",
|
|
224
268
|
"otel",
|
|
225
269
|
"events",
|
|
@@ -240,6 +284,9 @@ const SANDBOX_VERBS = [
|
|
|
240
284
|
"wake",
|
|
241
285
|
"restart",
|
|
242
286
|
"snapshot",
|
|
287
|
+
"mount",
|
|
288
|
+
"mounts",
|
|
289
|
+
"unmount",
|
|
243
290
|
"fork",
|
|
244
291
|
"extend",
|
|
245
292
|
"update",
|
|
@@ -259,6 +306,9 @@ const SANDBOX_VERBS = [
|
|
|
259
306
|
"network",
|
|
260
307
|
"desktop",
|
|
261
308
|
"metrics",
|
|
309
|
+
"watch",
|
|
310
|
+
"mcp",
|
|
311
|
+
"identity-token",
|
|
262
312
|
];
|
|
263
313
|
function need(value, what) {
|
|
264
314
|
if (!value)
|
|
@@ -454,7 +504,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
454
504
|
}
|
|
455
505
|
if (command === "usage") {
|
|
456
506
|
const value = await (await client(env)).usage();
|
|
457
|
-
print(
|
|
507
|
+
print(usageSummary(value), value);
|
|
458
508
|
return 0;
|
|
459
509
|
}
|
|
460
510
|
if (command === "limits") {
|
|
@@ -486,8 +536,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
486
536
|
return otelCommand(rest, env, out);
|
|
487
537
|
if (command === "events") {
|
|
488
538
|
const args = parse(rest);
|
|
489
|
-
const
|
|
490
|
-
|
|
539
|
+
const rt = await client(env);
|
|
540
|
+
const sandbox = flag(args, "sandbox");
|
|
541
|
+
const page = await rt.events.list({
|
|
542
|
+
...(sandbox ? { resourceId: await sandboxIdOf(rt, sandbox) } : {}),
|
|
491
543
|
...(flag(args, "type") ? { type: flag(args, "type") } : {}),
|
|
492
544
|
...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
|
|
493
545
|
});
|
|
@@ -501,6 +553,29 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
501
553
|
}
|
|
502
554
|
if (command === "keys")
|
|
503
555
|
return keys(rest, env, out);
|
|
556
|
+
if (command === "sso") {
|
|
557
|
+
const status = await (await client(env)).sso.get();
|
|
558
|
+
print([
|
|
559
|
+
status.connections.length
|
|
560
|
+
? table([
|
|
561
|
+
["domain", "provider", "verified", "required", "new people join as"],
|
|
562
|
+
...status.connections.map((c) => [
|
|
563
|
+
c.domain,
|
|
564
|
+
`${c.provider} ${c.protocol.toUpperCase()}`,
|
|
565
|
+
c.domainVerifiedAt
|
|
566
|
+
? c.domainVerifiedAt.slice(0, 10)
|
|
567
|
+
: `no: TXT ${c.verification.name}`,
|
|
568
|
+
c.requireSso ? "yes" : "no",
|
|
569
|
+
c.defaultRole,
|
|
570
|
+
]),
|
|
571
|
+
])
|
|
572
|
+
: "No single sign-on yet.",
|
|
573
|
+
"",
|
|
574
|
+
`Directory sync (SCIM): ${status.scim.tokens} token${status.scim.tokens === 1 ? "" : "s"}, ${status.scim.activeUsers} of ${status.scim.users} people active, ${status.scim.groups.length} groups.`,
|
|
575
|
+
`An owner changes it at ${status.manage}`,
|
|
576
|
+
].join("\n"), status);
|
|
577
|
+
return 0;
|
|
578
|
+
}
|
|
504
579
|
if (command === "account" || command === "accounts")
|
|
505
580
|
return accountCommand(rest, env, out);
|
|
506
581
|
if (command === "audit") {
|
|
@@ -558,7 +633,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
558
633
|
if (command === "mcp") {
|
|
559
634
|
const { serveMcp } = await import("./mcp.js");
|
|
560
635
|
await serveMcp(env);
|
|
561
|
-
|
|
636
|
+
/* Its input closed or a signal came: leave once stdout has drained. An
|
|
637
|
+
idle keep-alive connection to the API would otherwise hold the process
|
|
638
|
+
for several seconds after the client asked it to go. */
|
|
639
|
+
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
640
|
+
process.exit(0);
|
|
562
641
|
}
|
|
563
642
|
if (command === "docs") {
|
|
564
643
|
const topic = rest[0] ?? "index";
|
|
@@ -652,8 +731,101 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
652
731
|
product = "volume";
|
|
653
732
|
return volume(rest, env, out);
|
|
654
733
|
}
|
|
734
|
+
const networkProduct = { domains: "domain", ports: "port", addresses: "address", tunnels: "tunnel" }[command] ??
|
|
735
|
+
command;
|
|
736
|
+
if (NETWORK_PRODUCTS.includes(networkProduct)) {
|
|
737
|
+
product = networkProduct;
|
|
738
|
+
return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"]), out, () => client(env));
|
|
739
|
+
}
|
|
655
740
|
throw unknown("command", command, COMMANDS);
|
|
656
741
|
}
|
|
742
|
+
/** `runtime usage` for a person: the balance, the trial and what each kind
|
|
743
|
+
* of resource was charged, in dollars. --json keeps every figure, and each
|
|
744
|
+
* resource with its rates and CPU time. */
|
|
745
|
+
function usageSummary(u) {
|
|
746
|
+
const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
|
|
747
|
+
? BigInt(value)
|
|
748
|
+
: 0n;
|
|
749
|
+
const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
750
|
+
const takenBack = micros(u.takenBack);
|
|
751
|
+
const rows = [
|
|
752
|
+
["available", dollars(micros(u.available))],
|
|
753
|
+
["credited", dollars(micros(u.credited))],
|
|
754
|
+
[
|
|
755
|
+
"spent",
|
|
756
|
+
`${dollars(micros(u.spent))}${takenBack > 0n ? `, ${dollars(takenBack)} of it refunded or disputed` : ""}`,
|
|
757
|
+
],
|
|
758
|
+
["held for running work", dollars(micros(u.held))],
|
|
759
|
+
];
|
|
760
|
+
if (micros(u.expired) > 0n)
|
|
761
|
+
rows.push(["expired or taken back", dollars(micros(u.expired))]);
|
|
762
|
+
if (u.trial)
|
|
763
|
+
rows.push([
|
|
764
|
+
"free trial",
|
|
765
|
+
`${hours(u.trial.availableMs)} of ${hours(u.trial.totalMs)} hours left${u.trial.reservedMs > 0 ? `, ${hours(u.trial.reservedMs)} held by running sandboxes` : ""}`,
|
|
766
|
+
]);
|
|
767
|
+
const kinds = new Map();
|
|
768
|
+
for (const resource of u.resources ?? []) {
|
|
769
|
+
const kind = typeof resource.kind === "string" ? resource.kind : "other";
|
|
770
|
+
const entry = kinds.get(kind) ?? { count: 0, charged: 0n, held: 0n };
|
|
771
|
+
entry.count++;
|
|
772
|
+
entry.charged += micros(resource.chargedMicros);
|
|
773
|
+
entry.held += micros(resource.heldMicros);
|
|
774
|
+
kinds.set(kind, entry);
|
|
775
|
+
}
|
|
776
|
+
const plural = (kind, n) => n === 1 ? kind : kind.endsWith("x") || kind.endsWith("s") ? `${kind}es` : `${kind}s`;
|
|
777
|
+
const byKind = [...kinds.entries()].sort((a, b) => b[1].count - a[1].count);
|
|
778
|
+
return [
|
|
779
|
+
table(rows),
|
|
780
|
+
"",
|
|
781
|
+
byKind.length
|
|
782
|
+
? table([
|
|
783
|
+
["RESOURCES", "COUNT", "CHARGED", "HELD"],
|
|
784
|
+
...byKind.map(([kind, e]) => [
|
|
785
|
+
plural(kind, e.count),
|
|
786
|
+
e.count.toLocaleString("en-US"),
|
|
787
|
+
dollars(e.charged),
|
|
788
|
+
dollars(e.held),
|
|
789
|
+
]),
|
|
790
|
+
])
|
|
791
|
+
: "No resources yet.",
|
|
792
|
+
"",
|
|
793
|
+
`Each resource, with its rates and CPU time: ${me} usage --json`,
|
|
794
|
+
].join("\n");
|
|
795
|
+
}
|
|
796
|
+
const SANDBOX_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
797
|
+
/** A sandbox's id from its id or its name, for the filters that take an id. */
|
|
798
|
+
async function sandboxIdOf(runtime, target) {
|
|
799
|
+
return SANDBOX_UUID.test(target) ? target : (await resolveSandbox(runtime, target)).id;
|
|
800
|
+
}
|
|
801
|
+
/** What a process has printed so far, in order, without waiting for more. */
|
|
802
|
+
async function outputSoFar(runtime, sandboxId, processId) {
|
|
803
|
+
const chunks = [];
|
|
804
|
+
let cursor = 0;
|
|
805
|
+
let truncated = false;
|
|
806
|
+
let latest = null;
|
|
807
|
+
// The guest keeps the latest 1 MiB; a read returns at most 192 KiB.
|
|
808
|
+
for (let page = 0; page < 16; page++) {
|
|
809
|
+
const read = await runtime.transport.json({
|
|
810
|
+
method: "GET",
|
|
811
|
+
path: `/v1/sandboxes/${encodeURIComponent(sandboxId)}/processes/${encodeURIComponent(processId)}/output`,
|
|
812
|
+
query: { cursor, maxBytes: 196_608 },
|
|
813
|
+
});
|
|
814
|
+
chunks.push(...read.chunks);
|
|
815
|
+
truncated ||= read.truncated;
|
|
816
|
+
latest = read.process ?? latest;
|
|
817
|
+
if (read.chunks.length === 0 || read.nextCursor <= cursor)
|
|
818
|
+
break;
|
|
819
|
+
cursor = read.nextCursor;
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
processId,
|
|
823
|
+
state: latest?.state ?? null,
|
|
824
|
+
exitCode: latest?.exitCode ?? null,
|
|
825
|
+
truncated,
|
|
826
|
+
chunks,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
657
829
|
/** Dollars from integer microdollars: cents from a dollar up, and four
|
|
658
830
|
* places below one, so a few minutes of use does not round to $0.01. */
|
|
659
831
|
function dollars(micros) {
|
|
@@ -769,13 +941,25 @@ async function switching(command, argv, env, out) {
|
|
|
769
941
|
: `${c.rival.name} would cost ${dollars(-saving)} less${percent}`;
|
|
770
942
|
const figures = `${dollars(c.runtimeMicros)} on Runtime; the same sandboxes on ${c.rival.name}: ${dollars(c.rivalMicros)}; ${verdict}`;
|
|
771
943
|
const month = c.perMonth ? BigInt(c.perMonth.savingMicros) : undefined;
|
|
944
|
+
const monthly = month === undefined
|
|
945
|
+
? ""
|
|
946
|
+
: month >= 0n
|
|
947
|
+
? `about ${dollars(month)} a month`
|
|
948
|
+
: `about ${dollars(-month)} a month more on Runtime`;
|
|
949
|
+
/* The month is projected from the days the usage covers, not from the
|
|
950
|
+
window asked for: say which, or a young account's 7 and 90 days read the
|
|
951
|
+
same with nothing to say why. */
|
|
952
|
+
const covered = c.perMonth?.fromDays ?? c.window.days;
|
|
953
|
+
const pace = month === undefined
|
|
954
|
+
? "."
|
|
955
|
+
: covered < c.window.days
|
|
956
|
+
? `. Your first sandbox in this window ran ${covered <= 1 ? "within the last day" : `${covered.toLocaleString("en-US")} days ago`}; at that pace, ${monthly}.`
|
|
957
|
+
: c.window.days === 30
|
|
958
|
+
? `, ${monthly}.`
|
|
959
|
+
: `. At that pace, ${monthly}.`;
|
|
772
960
|
const lines = c.basis === "usage"
|
|
773
961
|
? [
|
|
774
|
-
`Your last ${c.window.days} days: ${figures}${
|
|
775
|
-
? "."
|
|
776
|
-
: month >= 0n
|
|
777
|
-
? `, about ${dollars(month)} a month.`
|
|
778
|
-
: `, about ${dollars(-month)} a month more on Runtime.`}`,
|
|
962
|
+
`Your last ${c.window.days} days: ${figures}${pace}`,
|
|
779
963
|
`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
964
|
]
|
|
781
965
|
: [
|
|
@@ -826,8 +1010,22 @@ async function sandbox(argv, env, out) {
|
|
|
826
1010
|
out.write(out.json ? JSON.stringify({ usage: named(SANDBOX_HELP) }) : named(SANDBOX_HELP));
|
|
827
1011
|
return 0;
|
|
828
1012
|
}
|
|
1013
|
+
// Inside a sandbox, with no API key: the sandbox's own request token asks.
|
|
1014
|
+
if (verb === "identity-token") {
|
|
1015
|
+
const args = parse(rest);
|
|
1016
|
+
const audience = need(flag(args, "audience"), "--audience, such as sts.amazonaws.com");
|
|
1017
|
+
const lifetime = flag(args, "lifetime");
|
|
1018
|
+
const token = await identityToken({
|
|
1019
|
+
audience,
|
|
1020
|
+
...(lifetime === undefined ? {} : { lifetimeSeconds: Number(lifetime) }),
|
|
1021
|
+
});
|
|
1022
|
+
print(token.token, token);
|
|
1023
|
+
return 0;
|
|
1024
|
+
}
|
|
829
1025
|
const runtime = await client(env);
|
|
830
|
-
|
|
1026
|
+
/* By id or by name, for every command here, as ssh and port-forward
|
|
1027
|
+
always took one. */
|
|
1028
|
+
const get = (id) => resolveSandbox(runtime, need(id, "a sandbox id or name"));
|
|
831
1029
|
switch (verb) {
|
|
832
1030
|
case "create": {
|
|
833
1031
|
const args = parse(rest, CREATE_FLAGS);
|
|
@@ -888,6 +1086,50 @@ async function sandbox(argv, env, out) {
|
|
|
888
1086
|
print(`${sbx.id} ${sbx.state}`, sbx.info);
|
|
889
1087
|
return 0;
|
|
890
1088
|
}
|
|
1089
|
+
case "mount": {
|
|
1090
|
+
const args = parse(rest, ["read-only"]);
|
|
1091
|
+
const sbx = await get(args.positional[0]);
|
|
1092
|
+
const source = need(args.positional[1], "a bucket, as s3://bucket, r2://bucket or gcs://bucket");
|
|
1093
|
+
const found = /^(s3|r2|gcs):\/\/([^/]+)(?:\/(.+))?$/.exec(source);
|
|
1094
|
+
if (!found)
|
|
1095
|
+
throw usage("The bucket is s3://bucket[/prefix], r2://bucket or gcs://bucket.");
|
|
1096
|
+
const mounted = await sbx.mounts.add({
|
|
1097
|
+
provider: found[1],
|
|
1098
|
+
bucket: found[2],
|
|
1099
|
+
path: need(args.positional[2], "a path to mount it at, such as /data"),
|
|
1100
|
+
...(found[3] ? { prefix: found[3] } : {}),
|
|
1101
|
+
...(flag(args, "secret") ? { secret: flag(args, "secret") } : {}),
|
|
1102
|
+
...(flag(args, "region") ? { region: flag(args, "region") } : {}),
|
|
1103
|
+
...(flag(args, "endpoint") ? { endpoint: flag(args, "endpoint") } : {}),
|
|
1104
|
+
...(flag(args, "account-id") ? { accountId: flag(args, "account-id") } : {}),
|
|
1105
|
+
...(has(args, "read-only") ? { readOnly: true } : {}),
|
|
1106
|
+
});
|
|
1107
|
+
print(`${mounted.path} ${mounted.provider}://${mounted.bucket}`, mounted);
|
|
1108
|
+
return 0;
|
|
1109
|
+
}
|
|
1110
|
+
case "mounts": {
|
|
1111
|
+
const sbx = await get(rest[0]);
|
|
1112
|
+
const all = await sbx.mounts.list();
|
|
1113
|
+
print(all.length
|
|
1114
|
+
? table([
|
|
1115
|
+
["PATH", "BUCKET", "ENDPOINT", "SECRET", "MOUNTED"],
|
|
1116
|
+
...all.map((m) => [
|
|
1117
|
+
m.path,
|
|
1118
|
+
`${m.provider}://${m.bucket}${m.prefix ? `/${m.prefix}` : ""}`,
|
|
1119
|
+
m.endpoint,
|
|
1120
|
+
m.secret ?? "-",
|
|
1121
|
+
m.mounted ? "yes" : "no",
|
|
1122
|
+
]),
|
|
1123
|
+
])
|
|
1124
|
+
: "No buckets mounted.", all);
|
|
1125
|
+
return 0;
|
|
1126
|
+
}
|
|
1127
|
+
case "unmount": {
|
|
1128
|
+
const sbx = await get(rest[0]);
|
|
1129
|
+
const gone = await sbx.mounts.remove(need(rest[1], "the mounted path"));
|
|
1130
|
+
print(`${gone.path} unmounted`, gone);
|
|
1131
|
+
return 0;
|
|
1132
|
+
}
|
|
891
1133
|
case "snapshot": {
|
|
892
1134
|
const args = parse(rest);
|
|
893
1135
|
const sbx = await get(args.positional[0]);
|
|
@@ -992,7 +1234,7 @@ async function sandbox(argv, env, out) {
|
|
|
992
1234
|
const args = parse(rest, ["f", "follow"]);
|
|
993
1235
|
const sbx = await get(args.positional[0]);
|
|
994
1236
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
995
|
-
if (has(args, "f") || has(args, "follow")
|
|
1237
|
+
if (has(args, "f") || has(args, "follow")) {
|
|
996
1238
|
for await (const event of proc.output())
|
|
997
1239
|
if (event.type === "stdout")
|
|
998
1240
|
process.stdout.write(event.data);
|
|
@@ -1002,7 +1244,17 @@ async function sandbox(argv, env, out) {
|
|
|
1002
1244
|
out.write(JSON.stringify(event));
|
|
1003
1245
|
return 0;
|
|
1004
1246
|
}
|
|
1005
|
-
|
|
1247
|
+
/* Without -f, what it has printed so far, then back to the prompt, as
|
|
1248
|
+
`docker logs` does: a server's log is read, not followed forever. */
|
|
1249
|
+
const so = await outputSoFar(runtime, sbx.id, proc.id);
|
|
1250
|
+
if (out.json) {
|
|
1251
|
+
out.write(JSON.stringify(so));
|
|
1252
|
+
return 0;
|
|
1253
|
+
}
|
|
1254
|
+
for (const chunk of so.chunks)
|
|
1255
|
+
(chunk.stream === "stderr" ? process.stderr : process.stdout).write(chunk.text);
|
|
1256
|
+
if (so.state === "running")
|
|
1257
|
+
process.stderr.write(`[${proc.id} is still running; follow it: ${me} sandbox logs ${sbx.id} ${proc.id} -f]\n`);
|
|
1006
1258
|
return 0;
|
|
1007
1259
|
}
|
|
1008
1260
|
case "kill": {
|
|
@@ -1031,7 +1283,7 @@ async function sandbox(argv, env, out) {
|
|
|
1031
1283
|
...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
|
|
1032
1284
|
});
|
|
1033
1285
|
print(entries
|
|
1034
|
-
.map((e) => `${e.type === "directory" ? "d" : "-"} ${String(e.size).padStart(10)} ${e.path}`)
|
|
1286
|
+
.map((e) => `${e.type === "directory" ? "d" : e.type === "symlink" ? "l" : e.type === "other" ? "?" : "-"} ${String(e.size).padStart(10)} ${e.path}`)
|
|
1035
1287
|
.join("\n") || "(empty)", entries);
|
|
1036
1288
|
return 0;
|
|
1037
1289
|
}
|
|
@@ -1101,6 +1353,27 @@ async function sandbox(argv, env, out) {
|
|
|
1101
1353
|
}
|
|
1102
1354
|
case "desktop":
|
|
1103
1355
|
return desktop(await get(rest[0]), rest.slice(1), out);
|
|
1356
|
+
case "watch":
|
|
1357
|
+
return (await import("./cli-extras.js")).watchCommand(await get(rest[0]), rest.slice(1), out, kit);
|
|
1358
|
+
case "mcp":
|
|
1359
|
+
if (rest[0] === "catalog" || rest[0] === undefined) {
|
|
1360
|
+
const catalog = await runtime.mcp.catalog();
|
|
1361
|
+
print(table([
|
|
1362
|
+
["ID", "LICENCE", "VERSION", "NEEDS", "WHAT"],
|
|
1363
|
+
...catalog.map((entry) => [
|
|
1364
|
+
entry.id,
|
|
1365
|
+
entry.license,
|
|
1366
|
+
entry.version,
|
|
1367
|
+
entry.env
|
|
1368
|
+
.filter((item) => item.required)
|
|
1369
|
+
.map((item) => `${item.name}${item.secret ? " (secret)" : ""}`)
|
|
1370
|
+
.join(", ") || "-",
|
|
1371
|
+
entry.description,
|
|
1372
|
+
]),
|
|
1373
|
+
]), catalog);
|
|
1374
|
+
return 0;
|
|
1375
|
+
}
|
|
1376
|
+
return (await import("./cli-extras.js")).mcpCommand(await get(rest[0]), rest.slice(1), out, kit);
|
|
1104
1377
|
case "metrics": {
|
|
1105
1378
|
const args = parse(rest);
|
|
1106
1379
|
const sbx = await get(args.positional[0]);
|
|
@@ -1236,8 +1509,20 @@ async function runCode(sbx, argv, out) {
|
|
|
1236
1509
|
const args = parse(argv);
|
|
1237
1510
|
const source = need(args.positional[0], "a file, or - to read the code from standard input");
|
|
1238
1511
|
const code = await readInput(source);
|
|
1512
|
+
const byExtension = {
|
|
1513
|
+
js: "javascript",
|
|
1514
|
+
mjs: "javascript",
|
|
1515
|
+
ts: "typescript",
|
|
1516
|
+
r: "r",
|
|
1517
|
+
java: "java",
|
|
1518
|
+
jsh: "java",
|
|
1519
|
+
sh: "bash",
|
|
1520
|
+
bash: "bash",
|
|
1521
|
+
go: "go",
|
|
1522
|
+
};
|
|
1239
1523
|
const language = (flag(args, "lang") ??
|
|
1240
|
-
(
|
|
1524
|
+
byExtension[source.split(".").pop()?.toLowerCase() ?? ""] ??
|
|
1525
|
+
"python");
|
|
1241
1526
|
const timeout = integer(args, "timeout");
|
|
1242
1527
|
const execution = await sbx.interpreter.run(code, {
|
|
1243
1528
|
language,
|
|
@@ -1252,6 +1537,8 @@ async function runCode(sbx, argv, out) {
|
|
|
1252
1537
|
});
|
|
1253
1538
|
const saved = [];
|
|
1254
1539
|
const outDir = flag(args, "out-dir") ?? ".";
|
|
1540
|
+
if (execution.results.some((r) => r.data["image/png"] || r.refs["image/png"]))
|
|
1541
|
+
await mkdir(outDir, { recursive: true });
|
|
1255
1542
|
for (const [index, result] of execution.results.entries()) {
|
|
1256
1543
|
const inline = result.data["image/png"];
|
|
1257
1544
|
const ref = result.refs["image/png"];
|
|
@@ -1316,6 +1603,8 @@ async function desktop(sbx, argv, out) {
|
|
|
1316
1603
|
case "press":
|
|
1317
1604
|
print("Pressed.", await d.press(need(rest[0], "keys, e.g. ctrl+l")));
|
|
1318
1605
|
return 0;
|
|
1606
|
+
case "record":
|
|
1607
|
+
return (await import("./cli-extras.js")).recordCommand(sbx, rest, out, kit);
|
|
1319
1608
|
case "windows": {
|
|
1320
1609
|
const windows = await d.windows();
|
|
1321
1610
|
print(windows.length
|
|
@@ -1324,7 +1613,7 @@ async function desktop(sbx, argv, out) {
|
|
|
1324
1613
|
return 0;
|
|
1325
1614
|
}
|
|
1326
1615
|
}
|
|
1327
|
-
throw usage(`${me} sandbox desktop <id> start|stop|screenshot|open|click|type|press|windows`);
|
|
1616
|
+
throw usage(`${me} sandbox desktop <id> start|stop|screenshot|open|click|type|press|windows|record`);
|
|
1328
1617
|
}
|
|
1329
1618
|
const KEYS_HELP = `runtime keys <command>
|
|
1330
1619
|
|
|
@@ -1483,7 +1772,9 @@ async function snapshot(argv, env, out) {
|
|
|
1483
1772
|
case "list": {
|
|
1484
1773
|
const args = parse(rest);
|
|
1485
1774
|
const all = await (await api.list({
|
|
1486
|
-
...(flag(args, "sandbox")
|
|
1775
|
+
...(flag(args, "sandbox")
|
|
1776
|
+
? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
|
|
1777
|
+
: {}),
|
|
1487
1778
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1488
1779
|
})).toArray(1000);
|
|
1489
1780
|
print(all.length
|
|
@@ -1744,10 +2035,17 @@ const VOLUME_HELP = `runtime volume <command>
|
|
|
1744
2035
|
|
|
1745
2036
|
create --size-mib <N> [--name n] A persistent ext4 disk; prints its id
|
|
1746
2037
|
ls Your volumes
|
|
1747
|
-
get <id> One volume and where it is attached
|
|
1748
|
-
rm <id> Delete it and everything on it
|
|
2038
|
+
get <id> One volume, its backups and where it is attached
|
|
2039
|
+
rm <id> Delete it and everything on it (its backups stay)
|
|
2040
|
+
backup <id> [--retention-days N] [--no-wait]
|
|
2041
|
+
Back it up now, off its host; prints the backup id
|
|
2042
|
+
backups [<id>] Backups, newest first (of one volume, if given)
|
|
2043
|
+
restore <backupId> [--name n] A new volume from a backup, on any host
|
|
2044
|
+
backup-rm <backupId> Delete a backup
|
|
2045
|
+
backup-policy <id> [--daily on|off] [--retention-days N]
|
|
2046
|
+
Daily backups on or off, and how long each is kept
|
|
1749
2047
|
Attach one: runtime sandbox create --volume <id>:/data
|
|
1750
|
-
|
|
2048
|
+
Volumes are backed up off their host daily unless you turn it off.
|
|
1751
2049
|
`;
|
|
1752
2050
|
async function volume(argv, env, out) {
|
|
1753
2051
|
const [verb, ...rest] = argv;
|
|
@@ -1764,8 +2062,6 @@ async function volume(argv, env, out) {
|
|
|
1764
2062
|
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
1765
2063
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1766
2064
|
});
|
|
1767
|
-
if (!out.json)
|
|
1768
|
-
out.error("Not backed up off its server.");
|
|
1769
2065
|
print(created.id, created);
|
|
1770
2066
|
return 0;
|
|
1771
2067
|
}
|
|
@@ -1774,12 +2070,13 @@ async function volume(argv, env, out) {
|
|
|
1774
2070
|
const all = await (await api.list()).toArray(1000);
|
|
1775
2071
|
print(all.length
|
|
1776
2072
|
? table([
|
|
1777
|
-
["ID", "NAME", "STATE", "MIB", "ATTACHED"],
|
|
2073
|
+
["ID", "NAME", "STATE", "MIB", "BACKED UP", "ATTACHED"],
|
|
1778
2074
|
...all.map((v) => [
|
|
1779
2075
|
v.id,
|
|
1780
2076
|
v.name ?? "-",
|
|
1781
2077
|
v.state,
|
|
1782
2078
|
String(v.sizeMiB),
|
|
2079
|
+
v.backedUp ? (v.backups?.lastReadyAt ?? "yes") : "no",
|
|
1783
2080
|
v.attachments.map((a) => `${a.sandboxId}:${a.path}`).join(", ") || "-",
|
|
1784
2081
|
]),
|
|
1785
2082
|
])
|
|
@@ -1797,8 +2094,78 @@ async function volume(argv, env, out) {
|
|
|
1797
2094
|
print(`${found.id} ${found.state}`, found);
|
|
1798
2095
|
return 0;
|
|
1799
2096
|
}
|
|
2097
|
+
case "backup": {
|
|
2098
|
+
const args = parse(rest, ["no-wait"]);
|
|
2099
|
+
const id = need(args.positional[0], "a volume id");
|
|
2100
|
+
const days = integer(args, "retention-days");
|
|
2101
|
+
const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
|
|
2102
|
+
if (!out.json && made.state === "pending")
|
|
2103
|
+
out.error(`Copying it off the host; check with: ${me} volume backups ${id}`);
|
|
2104
|
+
print(made.id, made);
|
|
2105
|
+
return 0;
|
|
2106
|
+
}
|
|
2107
|
+
case "backups": {
|
|
2108
|
+
const volumeId = rest[0];
|
|
2109
|
+
const all = await (await api.backups(volumeId ? { volumeId } : {})).toArray(1000);
|
|
2110
|
+
print(all.length
|
|
2111
|
+
? table([
|
|
2112
|
+
["ID", "VOLUME", "STATE", "TRIGGER", "MIB", "STORED", "TAKEN", "EXPIRES"],
|
|
2113
|
+
...all.map((b) => [
|
|
2114
|
+
b.id,
|
|
2115
|
+
b.volumeId,
|
|
2116
|
+
b.state,
|
|
2117
|
+
b.trigger,
|
|
2118
|
+
String(b.sizeMiB),
|
|
2119
|
+
b.storedBytes === null ? "-" : `${Math.ceil(b.storedBytes / 1048576)} MiB`,
|
|
2120
|
+
b.createdAt,
|
|
2121
|
+
b.expiresAt,
|
|
2122
|
+
]),
|
|
2123
|
+
])
|
|
2124
|
+
: "No backups yet.", all);
|
|
2125
|
+
return 0;
|
|
2126
|
+
}
|
|
2127
|
+
case "restore": {
|
|
2128
|
+
const args = parse(rest);
|
|
2129
|
+
const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
|
|
2130
|
+
if (!out.json && created.state === "creating")
|
|
2131
|
+
out.error(`Restoring; check with: ${me} volume get ${created.id}`);
|
|
2132
|
+
print(created.id, created);
|
|
2133
|
+
return 0;
|
|
2134
|
+
}
|
|
2135
|
+
case "backup-rm":
|
|
2136
|
+
case "backup-delete": {
|
|
2137
|
+
const found = await api.deleteBackup(need(rest[0], "a backup id"));
|
|
2138
|
+
print(`${found.id} ${found.state}`, found);
|
|
2139
|
+
return 0;
|
|
2140
|
+
}
|
|
2141
|
+
case "backup-policy": {
|
|
2142
|
+
const args = parse(rest);
|
|
2143
|
+
const id = need(args.positional[0], "a volume id");
|
|
2144
|
+
const daily = flag(args, "daily");
|
|
2145
|
+
if (daily !== undefined && !["on", "off", "true", "false"].includes(daily))
|
|
2146
|
+
throw usage("--daily takes on or off.");
|
|
2147
|
+
const days = integer(args, "retention-days");
|
|
2148
|
+
if (daily === undefined && days === undefined)
|
|
2149
|
+
throw usage("Give --daily on|off, --retention-days N or both.");
|
|
2150
|
+
const updated = await api.setBackupPolicy(id, {
|
|
2151
|
+
...(daily === undefined ? {} : { daily: daily === "on" || daily === "true" }),
|
|
2152
|
+
...(days === undefined ? {} : { retentionDays: days }),
|
|
2153
|
+
});
|
|
2154
|
+
print(`${updated.id} daily backups ${updated.backups.daily ? "on" : "off"}, kept ${updated.backups.retentionDays} days`, updated);
|
|
2155
|
+
return 0;
|
|
2156
|
+
}
|
|
1800
2157
|
}
|
|
1801
|
-
throw unknown("volume command", verb, [
|
|
2158
|
+
throw unknown("volume command", verb, [
|
|
2159
|
+
"create",
|
|
2160
|
+
"ls",
|
|
2161
|
+
"get",
|
|
2162
|
+
"rm",
|
|
2163
|
+
"backup",
|
|
2164
|
+
"backups",
|
|
2165
|
+
"restore",
|
|
2166
|
+
"backup-rm",
|
|
2167
|
+
"backup-policy",
|
|
2168
|
+
]);
|
|
1802
2169
|
}
|
|
1803
2170
|
/** Flags `sandbox create` and `run` take without a value. */
|
|
1804
2171
|
const CREATE_FLAGS = [
|
|
@@ -1852,7 +2219,10 @@ function createInput(args) {
|
|
|
1852
2219
|
* (124 on a timeout), so a script's `set -e` behaves. */
|
|
1853
2220
|
async function execute(sbx, command, args, out, timeout) {
|
|
1854
2221
|
const target = command.length === 1 ? command[0] : command;
|
|
2222
|
+
const extras = await import("./cli-extras.js");
|
|
2223
|
+
const stdin = await extras.pipedStdin();
|
|
1855
2224
|
const options = {
|
|
2225
|
+
...(stdin ? { stdin } : {}),
|
|
1856
2226
|
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
1857
2227
|
...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
|
|
1858
2228
|
...(timeout ? { timeoutMs: timeout * 1000 } : {}),
|
|
@@ -1868,7 +2238,12 @@ async function execute(sbx, command, args, out, timeout) {
|
|
|
1868
2238
|
out.write(JSON.stringify(result));
|
|
1869
2239
|
else if (result.timedOut)
|
|
1870
2240
|
out.error(`runtime: timed out after ${timeout ?? 60} s; the command's process group was killed.`);
|
|
1871
|
-
|
|
2241
|
+
// Never drop output silently: say so, and do not exit 0.
|
|
2242
|
+
const lost = extras.lostOutput(result, sbx.id);
|
|
2243
|
+
if (lost && !out.json)
|
|
2244
|
+
out.error(lost);
|
|
2245
|
+
const code = result.timedOut ? 124 : Math.min(255, Math.max(0, result.exitCode ?? 1));
|
|
2246
|
+
return lost && code === 0 ? 1 : code;
|
|
1872
2247
|
}
|
|
1873
2248
|
async function shell(sbx, command) {
|
|
1874
2249
|
const stdin = process.stdin;
|