withruntime 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/E2B.md +32 -32
- package/README.md +3 -3
- package/dist/ai/index.d.ts +21 -5
- package/dist/ai/index.js +14 -21
- package/dist/ai-harness/index.js +15 -2
- package/dist/billing-cli.d.ts +21 -0
- package/dist/billing-cli.js +158 -0
- package/dist/cli-extras.d.ts +15 -0
- package/dist/cli-extras.js +60 -4
- package/dist/cli-name.d.ts +6 -0
- package/dist/cli-name.js +8 -0
- package/dist/cli.d.ts +17 -1
- package/dist/cli.js +543 -132
- package/dist/client.d.ts +35 -6
- package/dist/client.js +23 -0
- package/dist/credentials.js +1 -2
- package/dist/daytona/filesystem.d.ts +2 -0
- package/dist/daytona/filesystem.js +9 -7
- package/dist/e2b/commands.js +11 -2
- package/dist/e2b/errors.d.ts +5 -0
- package/dist/e2b/errors.js +3 -0
- package/dist/e2b/filesystem.js +3 -2
- package/dist/e2b/sandbox.d.ts +9 -6
- package/dist/e2b/sandbox.js +31 -7
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +1 -0
- package/dist/keys.js +1 -1
- package/dist/login.d.ts +2 -6
- package/dist/login.js +3 -8
- package/dist/network-cli.d.ts +1 -1
- package/dist/network-cli.js +64 -2
- package/dist/products/billing.d.ts +35 -0
- package/dist/products/billing.js +13 -0
- package/dist/products/index.d.ts +4 -1
- package/dist/products/index.js +4 -1
- package/dist/products/network-products.d.ts +45 -0
- package/dist/products/network-products.js +27 -0
- package/dist/products/previews.d.ts +1 -2
- package/dist/products/secrets.d.ts +14 -0
- package/dist/sandbox.d.ts +17 -3
- package/dist/sandbox.js +233 -62
- package/dist/transport.d.ts +17 -1
- package/dist/transport.js +124 -7
- package/dist/types.d.ts +0 -2
- package/dist/wallet.d.ts +30 -0
- package/dist/wallet.js +48 -0
- package/package.json +14 -3
package/dist/cli.js
CHANGED
|
@@ -3,15 +3,14 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { connectionStore, resolveCredential } from "./credentials.js";
|
|
7
|
-
import {
|
|
8
|
-
import { createKey, dailyLimitMicros } from "./keys.js";
|
|
6
|
+
import { connectionOrigins, connectionStore, resolveCredential } from "./credentials.js";
|
|
7
|
+
import { me, named } from "./cli-name.js";
|
|
9
8
|
import { Runtime } from "./client.js";
|
|
9
|
+
import { Sandbox as SandboxHandle } from "./sandbox.js";
|
|
10
10
|
import { RuntimeError } from "./errors.js";
|
|
11
11
|
import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
|
|
12
|
+
import { billingCommand } from "./billing-cli.js";
|
|
12
13
|
import { envFetch } from "./proxy.js";
|
|
13
|
-
import { identityToken } from "./identity.js";
|
|
14
|
-
import { portForwardCommand, resolveSandbox, sshCommand } from "./ssh.js";
|
|
15
14
|
import { VERSION } from "./transport.js";
|
|
16
15
|
const HELP = `Runtime Cloud CLI ${VERSION}
|
|
17
16
|
|
|
@@ -30,22 +29,25 @@ Account
|
|
|
30
29
|
account [ls] The accounts this machine is connected to
|
|
31
30
|
account switch <name|id> Use another of them for every command after
|
|
32
31
|
account add Connect another account (choose it in the browser)
|
|
32
|
+
account close --confirm "<account name>" Close the account for good (owner, key for every product)
|
|
33
33
|
audit [--action member.] [--limit 50] [--before <next>]
|
|
34
34
|
The account's audit log (owner or admin keys)
|
|
35
35
|
sso Single sign-on and SCIM directory sync (owner or
|
|
36
36
|
admin keys); an owner changes it on the website
|
|
37
|
-
usage
|
|
37
|
+
usage [--csv] Balance, trial time and charges; --csv, each resource
|
|
38
38
|
limits Whether this key is read-only, and its daily spending limit
|
|
39
39
|
secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
|
|
40
|
-
|
|
41
|
-
value is read from standard input, never an argument
|
|
40
|
+
[--allow '[GET,HEAD] /path/*']... Store a secret sandboxes use without seeing it; the
|
|
41
|
+
value is read from standard input, never an argument.
|
|
42
|
+
--allow (paid): only these methods and paths get it
|
|
42
43
|
secrets ls Names, hosts and placeholders, never values
|
|
43
44
|
secrets rm <NAME> Delete a secret
|
|
44
45
|
keys create [--name <name>] [--read-only] [--daily-limit <usd>]
|
|
45
46
|
A new API key for CI, approved by an owner in the browser
|
|
46
47
|
referrals Your referral link: you both get up to $500
|
|
47
48
|
compare --from <provider> [--days 30] What your usage would cost at e2b, daytona, vercel,
|
|
48
|
-
modal, cloudflare, fly
|
|
49
|
+
modal, cloudflare, fly, lambda-microvms, freestyle,
|
|
50
|
+
prime and others, and what you save
|
|
49
51
|
switch [--from <provider>] Moving from a rival: record it before your first top-up,
|
|
50
52
|
and that top-up is matched, up to $100
|
|
51
53
|
ls Everything you run, every product
|
|
@@ -84,6 +86,7 @@ Products
|
|
|
84
86
|
port A public TCP port to a sandbox: databases, game servers
|
|
85
87
|
address A dedicated outbound address, for allow-lists
|
|
86
88
|
tunnel A WireGuard tunnel from your own network into your sandboxes
|
|
89
|
+
network Account-wide network settings: send outbound traffic through your own proxy
|
|
87
90
|
Each has its own help: \`runtime sandbox help\`, \`runtime image help\`...
|
|
88
91
|
|
|
89
92
|
Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
|
|
@@ -97,6 +100,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
97
100
|
ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
|
|
98
101
|
port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
|
|
99
102
|
create [--name n] [--label k=v]... [--vcpu 2] [--memory 4096] [--disk 4096]
|
|
103
|
+
[--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
|
|
100
104
|
[--timeout <seconds>] [--on-timeout pause|stop] [--trial|--paid] [--no-wait]
|
|
101
105
|
[--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
102
106
|
[--image <name:tag|imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
|
|
@@ -137,6 +141,8 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
137
141
|
Start them; prints each URL and the header to send
|
|
138
142
|
mcp <id> [status] | mcp <id> stop Their state and fresh URLs; stop them
|
|
139
143
|
preview <id> <port> [--public] [--ttl <s>] Share a port at an HTTPS address
|
|
144
|
+
preview rotate <id> <port> Refuse every token given out for a private port so
|
|
145
|
+
far; prints the new token
|
|
140
146
|
previews <id> Every shared port
|
|
141
147
|
unshare <id> <port> Stop sharing a port
|
|
142
148
|
network <id> [--no-internet|--internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
@@ -157,9 +163,38 @@ Examples
|
|
|
157
163
|
runtime sandbox cp ./project $id:/workspace/project
|
|
158
164
|
runtime sandbox stop $id
|
|
159
165
|
`;
|
|
160
|
-
|
|
166
|
+
/** A command's options. Reading one its parse() did not declare is a bug in
|
|
167
|
+
* this file, and says so, so a test that runs the command finds it. */
|
|
168
|
+
class Flags extends Map {
|
|
169
|
+
known;
|
|
170
|
+
constructor(known) {
|
|
171
|
+
super();
|
|
172
|
+
this.known = known;
|
|
173
|
+
}
|
|
174
|
+
get(name) {
|
|
175
|
+
this.#declared(name);
|
|
176
|
+
return super.get(name);
|
|
177
|
+
}
|
|
178
|
+
has(name) {
|
|
179
|
+
this.#declared(name);
|
|
180
|
+
return super.has(name);
|
|
181
|
+
}
|
|
182
|
+
#declared(name) {
|
|
183
|
+
if (!this.known.has(name))
|
|
184
|
+
throw new Error(`The CLI reads --${name}, which this command's parse() does not declare.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** A command's arguments: `booleans` are options that take no value, `values`
|
|
188
|
+
* those that take one. Any other option is refused before anything is sent, so
|
|
189
|
+
* `--memory-mib 8192` for `--memory` is a usage error with the right name, not a
|
|
190
|
+
* sandbox created at the default size. Everything after `--` is `rest`. */
|
|
191
|
+
function parse(argv, booleans, values,
|
|
192
|
+
/** For a command that runs one: how to write it, so an option that belongs
|
|
193
|
+
* to the command (`python3 -c ...`) is pointed after the `--`. */
|
|
194
|
+
commandAfter) {
|
|
195
|
+
const known = new Set([...values, ...booleans]);
|
|
161
196
|
const positional = [];
|
|
162
|
-
const flags = new
|
|
197
|
+
const flags = new Flags(known);
|
|
163
198
|
let rest;
|
|
164
199
|
for (let i = 0; i < argv.length; i++) {
|
|
165
200
|
const arg = argv[i];
|
|
@@ -169,6 +204,10 @@ function parse(argv, booleans = []) {
|
|
|
169
204
|
}
|
|
170
205
|
if (arg.startsWith("--") || (arg.startsWith("-") && arg.length === 2 && !/^-\d/.test(arg))) {
|
|
171
206
|
const [name, inline] = arg.replace(/^-+/, "").split(/=(.*)/s, 2);
|
|
207
|
+
if (!known.has(name))
|
|
208
|
+
throw commandAfter && !meantOption(name, known)
|
|
209
|
+
? usage(`Unknown option ${arg.split("=")[0]}. If it belongs to the command, put the command after --: ${commandAfter}`)
|
|
210
|
+
: unknownOption(name, known);
|
|
172
211
|
const value = inline ?? (booleans.includes(name) ? "true" : argv[++i]);
|
|
173
212
|
if (value === undefined)
|
|
174
213
|
throw usage(`--${name} needs a value.`);
|
|
@@ -179,6 +218,22 @@ function parse(argv, booleans = []) {
|
|
|
179
218
|
}
|
|
180
219
|
return { positional, flags, rest };
|
|
181
220
|
}
|
|
221
|
+
/** The declared option a mistyped one most likely meant. --memory-mib for
|
|
222
|
+
* --memory, --timeout-seconds for --timeout: the SDK's spelling of an option is
|
|
223
|
+
* the CLI's with its unit added. */
|
|
224
|
+
function meantOption(name, known) {
|
|
225
|
+
return ([...known].find((option) => name.startsWith(`${option}-`) || option.startsWith(`${name}-`)) ??
|
|
226
|
+
closest(name, [...known]));
|
|
227
|
+
}
|
|
228
|
+
/** "Unknown option --x", the one that was likely meant, and the ones there are. */
|
|
229
|
+
function unknownOption(name, known) {
|
|
230
|
+
const dash = (option) => (option.length === 1 ? `-${option}` : `--${option}`);
|
|
231
|
+
const meant = meantOption(name, known);
|
|
232
|
+
const options = [...known].filter((option) => option.length > 1).map(dash);
|
|
233
|
+
return usage(`Unknown option ${dash(name)}.${meant ? ` Did you mean ${dash(meant)}?` : ""}${options.length
|
|
234
|
+
? ` This command takes ${options.join(", ")}.`
|
|
235
|
+
: " This command takes no options."}`);
|
|
236
|
+
}
|
|
182
237
|
const flag = (args, name) => args.flags.get(name)?.at(-1);
|
|
183
238
|
const has = (args, name) => args.flags.get(name)?.at(-1) === "true";
|
|
184
239
|
const integer = (args, name) => {
|
|
@@ -189,6 +244,13 @@ const integer = (args, name) => {
|
|
|
189
244
|
throw usage(`--${name} must be a whole number.`);
|
|
190
245
|
return Number(value);
|
|
191
246
|
};
|
|
247
|
+
/** A flag that takes one of a few words, checked before anything is sent. */
|
|
248
|
+
function oneOf(args, name, choices) {
|
|
249
|
+
const value = flag(args, name);
|
|
250
|
+
if (!choices.includes(value))
|
|
251
|
+
throw usage(`--${name} takes ${choices.join(" or ")}.`);
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
192
254
|
const pairs = (args, name) => Object.fromEntries((args.flags.get(name) ?? []).map((pair) => {
|
|
193
255
|
const at = pair.indexOf("=");
|
|
194
256
|
if (at < 1)
|
|
@@ -197,7 +259,7 @@ const pairs = (args, name) => Object.fromEntries((args.flags.get(name) ?? []).ma
|
|
|
197
259
|
}));
|
|
198
260
|
/** What the commands in cli-extras.ts use of this file. */
|
|
199
261
|
const kit = {
|
|
200
|
-
parse: (argv, booleans) => parse(argv, booleans),
|
|
262
|
+
parse: (argv, booleans, values) => parse(argv, booleans, values),
|
|
201
263
|
flag,
|
|
202
264
|
has,
|
|
203
265
|
integer,
|
|
@@ -218,24 +280,28 @@ function usage(message) {
|
|
|
218
280
|
}
|
|
219
281
|
/** "Unknown command X." and, when one is close, the command that was meant. */
|
|
220
282
|
function unknown(what, word, choices) {
|
|
283
|
+
const meant = closest(word, choices);
|
|
284
|
+
return usage(`Unknown ${what} ${word}.${meant ? ` Did you mean ${meant}?` : ""}`);
|
|
285
|
+
}
|
|
286
|
+
/** The choice a mistyped word most likely meant, or none when nothing is close:
|
|
287
|
+
* one edit away (a swap of two letters counts as one), or two for a word longer
|
|
288
|
+
* than five letters, so "exce" finds exec and "bogus" finds nothing. */
|
|
289
|
+
function closest(word, choices) {
|
|
221
290
|
const distance = (a, b) => {
|
|
222
|
-
const
|
|
223
|
-
for (let i = 1; i <= a.length; i++)
|
|
224
|
-
let previous = row[0];
|
|
225
|
-
row[0] = i;
|
|
291
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
292
|
+
for (let i = 1; i <= a.length; i++)
|
|
226
293
|
for (let j = 1; j <= b.length; j++) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
294
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
295
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
296
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
|
|
297
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
230
298
|
}
|
|
231
|
-
|
|
232
|
-
return row[b.length];
|
|
299
|
+
return d[a.length][b.length];
|
|
233
300
|
};
|
|
234
301
|
const best = choices
|
|
235
302
|
.map((choice) => [choice, distance(word, choice)])
|
|
236
303
|
.sort((x, y) => x[1] - y[1])[0];
|
|
237
|
-
|
|
238
|
-
return usage(`Unknown ${what} ${word}.${meant ? ` Did you mean ${meant}?` : ""}`);
|
|
304
|
+
return best && best[1] <= (word.length > 5 ? 2 : 1) ? best[0] : undefined;
|
|
239
305
|
}
|
|
240
306
|
const COMMANDS = [
|
|
241
307
|
"account",
|
|
@@ -244,6 +310,7 @@ const COMMANDS = [
|
|
|
244
310
|
"logout",
|
|
245
311
|
"whoami",
|
|
246
312
|
"usage",
|
|
313
|
+
"billing",
|
|
247
314
|
"limits",
|
|
248
315
|
"sso",
|
|
249
316
|
"secrets",
|
|
@@ -264,12 +331,38 @@ const COMMANDS = [
|
|
|
264
331
|
"port",
|
|
265
332
|
"address",
|
|
266
333
|
"tunnel",
|
|
334
|
+
"network",
|
|
267
335
|
"webhooks",
|
|
268
336
|
"otel",
|
|
269
337
|
"events",
|
|
270
338
|
"help",
|
|
271
339
|
"version",
|
|
272
340
|
];
|
|
341
|
+
/** Commands with a help of their own: `runtime <command> help`. */
|
|
342
|
+
const OWN_HELP = [
|
|
343
|
+
"sandbox",
|
|
344
|
+
"sandboxes",
|
|
345
|
+
"sbx",
|
|
346
|
+
"snapshot",
|
|
347
|
+
"snapshots",
|
|
348
|
+
"image",
|
|
349
|
+
"images",
|
|
350
|
+
"volume",
|
|
351
|
+
"volumes",
|
|
352
|
+
"keys",
|
|
353
|
+
"webhooks",
|
|
354
|
+
"webhook",
|
|
355
|
+
"billing",
|
|
356
|
+
"domain",
|
|
357
|
+
"domains",
|
|
358
|
+
"port",
|
|
359
|
+
"ports",
|
|
360
|
+
"address",
|
|
361
|
+
"addresses",
|
|
362
|
+
"tunnel",
|
|
363
|
+
"tunnels",
|
|
364
|
+
"network",
|
|
365
|
+
];
|
|
273
366
|
/** Sandbox commands that once sat at the top level. */
|
|
274
367
|
const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
|
|
275
368
|
const SANDBOX_VERBS = [
|
|
@@ -315,6 +408,14 @@ function need(value, what) {
|
|
|
315
408
|
throw usage(`Give ${what}.`);
|
|
316
409
|
return value;
|
|
317
410
|
}
|
|
411
|
+
/** A port from the command line: a whole number from 1 to 65535, checked
|
|
412
|
+
* before any call so a typo is a usage error, not a 404 from the API. */
|
|
413
|
+
function portOf(value, example) {
|
|
414
|
+
const port = Number(need(value, `a port, e.g. ${example}`));
|
|
415
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
416
|
+
throw usage(`${value} is not a port: give a whole number from 1 to 65535, e.g. ${example}.`);
|
|
417
|
+
return port;
|
|
418
|
+
}
|
|
318
419
|
/** The client, with the key from RUNTIME_API_KEY or this machine's saved
|
|
319
420
|
* connection. With neither, the first command connects the machine first (a
|
|
320
421
|
* browser approval, nothing to copy) and then does what was asked, so setup
|
|
@@ -341,6 +442,7 @@ async function client(env) {
|
|
|
341
442
|
env.RUNTIME_NO_LOGIN)
|
|
342
443
|
throw error;
|
|
343
444
|
process.stderr.write("This machine is not connected to Runtime yet. Connecting it first.\n");
|
|
445
|
+
const { authenticationCommand } = await import("./login.js");
|
|
344
446
|
const login = await authenticationCommand(["login"], env);
|
|
345
447
|
if (login.pending)
|
|
346
448
|
throw notApprovedYet(login);
|
|
@@ -383,7 +485,7 @@ function describe(info) {
|
|
|
383
485
|
.map(([k, v]) => `${k}=${v}`)
|
|
384
486
|
.join(", ") || "-",
|
|
385
487
|
],
|
|
386
|
-
["charged",
|
|
488
|
+
["charged", dollars(BigInt(Math.round(Number(info.chargedMicros) || 0)))],
|
|
387
489
|
]);
|
|
388
490
|
}
|
|
389
491
|
/** Resolves `<id>:/path` into its parts, or undefined for a local path. */
|
|
@@ -402,6 +504,16 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
402
504
|
SANDBOX_MOVED.includes(remaining[0] ?? "");
|
|
403
505
|
const command = nested ? remaining[0] : first;
|
|
404
506
|
const rest = nested ? remaining.slice(1) : remaining;
|
|
507
|
+
// `runtime sandbox create --help` is how an agent asks what a command takes:
|
|
508
|
+
// answer with the product's help, never by running the command.
|
|
509
|
+
const cut = remaining.indexOf("--");
|
|
510
|
+
if (first &&
|
|
511
|
+
(cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h"))
|
|
512
|
+
return run(nested && command === "ssh"
|
|
513
|
+
? [first, "ssh", "help"]
|
|
514
|
+
: OWN_HELP.includes(first)
|
|
515
|
+
? [first, "help"]
|
|
516
|
+
: ["help"], env, out);
|
|
405
517
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
406
518
|
product = "";
|
|
407
519
|
if (!command) {
|
|
@@ -427,14 +539,14 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
427
539
|
return 0;
|
|
428
540
|
}
|
|
429
541
|
if (command === "run") {
|
|
430
|
-
const args = parse(rest, [...CREATE_FLAGS, "keep"]);
|
|
542
|
+
const args = parse(rest, [...CREATE_FLAGS, "keep"], [...CREATE_VALUES, "cwd", "env"], `${me} sandbox run -- python3 -c 'print(6*7)'`);
|
|
431
543
|
const commandLine = args.rest ?? args.positional;
|
|
432
544
|
if (!commandLine.length)
|
|
433
545
|
throw usage(`Give the command after --: ${me} sandbox run -- python3 -c 'print(6*7)'`);
|
|
434
546
|
const runtime = await client(env);
|
|
435
547
|
const timeout = integer(args, "timeout");
|
|
436
548
|
const { timeoutSeconds: _lease, ...input } = createInput(args);
|
|
437
|
-
const sbx = await runtime.sandboxes.create(input);
|
|
549
|
+
const sbx = await runtime.sandboxes.create(input, { onCapacityWait: waiting(out) });
|
|
438
550
|
let cleanup;
|
|
439
551
|
// SIGINT and normal completion share one stop. Keep an absolute deadline
|
|
440
552
|
// across the stop retries and final state check, so cleanup cannot hang.
|
|
@@ -464,7 +576,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
464
576
|
process.once("SIGINT", interrupted);
|
|
465
577
|
let code = 1;
|
|
466
578
|
try {
|
|
467
|
-
code = await execute(sbx, commandLine, args, out, timeout);
|
|
579
|
+
code = await execute(sbx, commandLine, args, out, timeout, has(args, "keep"));
|
|
468
580
|
}
|
|
469
581
|
finally {
|
|
470
582
|
try {
|
|
@@ -484,18 +596,42 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
484
596
|
return 0;
|
|
485
597
|
}
|
|
486
598
|
if (command === "login" || command === "logout") {
|
|
599
|
+
const { authenticationCommand } = await import("./login.js");
|
|
487
600
|
const result = await authenticationCommand([command, ...rest], env);
|
|
601
|
+
// Which account it connected to, so a person with several can tell. The
|
|
602
|
+
// connection stands whether or not this read answers.
|
|
603
|
+
const who = command === "login" && "connected" in result && result.connected
|
|
604
|
+
? await (await client(env)).me().catch(() => undefined)
|
|
605
|
+
: undefined;
|
|
606
|
+
const account = who ? (who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId) : undefined;
|
|
488
607
|
print(result.pending
|
|
489
608
|
? `Not connected yet. When it is approved, run any command, or \`${me} login --wait\`; it uses this link.`
|
|
490
609
|
: command === "login"
|
|
491
|
-
? `Connected${"agentName" in result && result.agentName ? ` as ${String(result.agentName)}` : ""}.`
|
|
492
|
-
: "Disconnected.", result);
|
|
610
|
+
? `Connected${"agentName" in result && result.agentName ? ` as ${String(result.agentName)}` : ""}${account ? ` to ${account}` : ""}.`
|
|
611
|
+
: "Disconnected.", who ? { ...result, orgId: who.orgId, orgName: who.orgName ?? null } : result);
|
|
493
612
|
return 0;
|
|
494
613
|
}
|
|
495
614
|
if (command === "whoami") {
|
|
496
|
-
const
|
|
615
|
+
const rt = await client(env);
|
|
616
|
+
// The usage call only adds the funding line: a key that cannot read it
|
|
617
|
+
// still gets its answer.
|
|
618
|
+
const [who, spend] = await Promise.all([rt.me(), rt.usage().catch(() => undefined)]);
|
|
619
|
+
const trialLeft = spend?.trial ? spend.trial.availableMs / 3_600_000 : 0;
|
|
620
|
+
const credit = spend ? BigInt(spend.available) : 0n;
|
|
621
|
+
const funding = !spend
|
|
622
|
+
? undefined
|
|
623
|
+
: [
|
|
624
|
+
...(trialLeft > 0
|
|
625
|
+
? [
|
|
626
|
+
`free trial, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left`,
|
|
627
|
+
]
|
|
628
|
+
: []),
|
|
629
|
+
...(credit > 0n ? [`${dollars(credit)} of credit`] : []),
|
|
630
|
+
].join("; ") || "no trial time or credit left: runtime billing";
|
|
497
631
|
print(table([
|
|
498
|
-
["organization", who.orgId],
|
|
632
|
+
["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
|
|
633
|
+
...(who.role ? [["role", who.role]] : []),
|
|
634
|
+
...(funding ? [["funding", funding]] : []),
|
|
499
635
|
["agent", who.principalId],
|
|
500
636
|
["key", who.credentialId ?? "-"],
|
|
501
637
|
["api", who.apiVersion],
|
|
@@ -503,7 +639,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
503
639
|
return 0;
|
|
504
640
|
}
|
|
505
641
|
if (command === "usage") {
|
|
642
|
+
const csv = rest.includes("--csv");
|
|
643
|
+
const unknown = rest.find((arg) => arg !== "--csv");
|
|
644
|
+
if (unknown)
|
|
645
|
+
throw usage(`Unknown option ${unknown}: ${me} usage [--csv | --json]`);
|
|
506
646
|
const value = await (await client(env)).usage();
|
|
647
|
+
if (csv) {
|
|
648
|
+
out.write(usageCsv(value));
|
|
649
|
+
if ((value.resources ?? []).length >= 100)
|
|
650
|
+
out.error("These are the newest 100 resources; older ones are not in this export.");
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
507
653
|
print(usageSummary(value), value);
|
|
508
654
|
return 0;
|
|
509
655
|
}
|
|
@@ -535,7 +681,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
535
681
|
if (command === "otel")
|
|
536
682
|
return otelCommand(rest, env, out);
|
|
537
683
|
if (command === "events") {
|
|
538
|
-
const args = parse(rest);
|
|
684
|
+
const args = parse(rest, [], ["sandbox", "type", "limit"]);
|
|
539
685
|
const rt = await client(env);
|
|
540
686
|
const sandbox = flag(args, "sandbox");
|
|
541
687
|
const page = await rt.events.list({
|
|
@@ -579,7 +725,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
579
725
|
if (command === "account" || command === "accounts")
|
|
580
726
|
return accountCommand(rest, env, out);
|
|
581
727
|
if (command === "audit") {
|
|
582
|
-
const args = parse(rest);
|
|
728
|
+
const args = parse(rest, [], ["action", "limit", "before"]);
|
|
583
729
|
const known = new Set(["action", "limit", "before"]);
|
|
584
730
|
for (const name of args.flags.keys())
|
|
585
731
|
if (!known.has(name))
|
|
@@ -607,6 +753,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
607
753
|
: "Nothing recorded yet.", page);
|
|
608
754
|
return 0;
|
|
609
755
|
}
|
|
756
|
+
if (command === "billing") {
|
|
757
|
+
product = "billing";
|
|
758
|
+
const origins = connectionOrigins(env);
|
|
759
|
+
const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
|
|
760
|
+
return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
|
|
761
|
+
open: (input) => openWalletAccount(input, { authUrl: origins.auth }),
|
|
762
|
+
claim: (code) => claimWalletAccount(code, { authUrl: origins.auth }),
|
|
763
|
+
save: (key, orgId) => connectionStore(env).save({
|
|
764
|
+
version: 1,
|
|
765
|
+
apiOrigin: origins.api,
|
|
766
|
+
authOrigin: origins.auth,
|
|
767
|
+
key,
|
|
768
|
+
connectionId: "pasted",
|
|
769
|
+
orgId,
|
|
770
|
+
agentName: "Wallet agent",
|
|
771
|
+
}),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
610
774
|
if (command === "referrals") {
|
|
611
775
|
const r = await (await client(env)).referrals.get();
|
|
612
776
|
const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
@@ -648,7 +812,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
648
812
|
return 0;
|
|
649
813
|
}
|
|
650
814
|
if (command === "feedback") {
|
|
651
|
-
const args = parse(rest, ["list"]);
|
|
815
|
+
const args = parse(rest, ["list"], ["kind", "detail", "competitor", "request-id"]);
|
|
652
816
|
if (has(args, "list")) {
|
|
653
817
|
const { data } = await (await client(env)).feedback.list();
|
|
654
818
|
print(data.length
|
|
@@ -709,11 +873,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
709
873
|
}
|
|
710
874
|
if (command === "ssh") {
|
|
711
875
|
product = "ssh";
|
|
712
|
-
return sshCommand(rest, env, out, () => client(env), me);
|
|
876
|
+
return (await import("./ssh.js")).sshCommand(rest, env, out, () => client(env), me);
|
|
713
877
|
}
|
|
714
878
|
if (command === "port-forward" || command === "forward") {
|
|
715
879
|
product = "ssh";
|
|
716
|
-
return portForwardCommand(rest, out, () => client(env), me);
|
|
880
|
+
return (await import("./ssh.js")).portForwardCommand(rest, out, () => client(env), me);
|
|
717
881
|
}
|
|
718
882
|
if (command === "sandbox" || command === "sandboxes" || command === "sbx") {
|
|
719
883
|
product = "sandbox";
|
|
@@ -735,13 +899,62 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
735
899
|
command;
|
|
736
900
|
if (NETWORK_PRODUCTS.includes(networkProduct)) {
|
|
737
901
|
product = networkProduct;
|
|
738
|
-
return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"]), out, () => client(env));
|
|
902
|
+
return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"], ["sandbox", "subnet", "public-key", "route", "out", "host", "secret"]), out, () => client(env));
|
|
739
903
|
}
|
|
740
904
|
throw unknown("command", command, COMMANDS);
|
|
741
905
|
}
|
|
742
906
|
/** `runtime usage` for a person: the balance, the trial and what each kind
|
|
743
907
|
* of resource was charged, in dollars. --json keeps every figure, and each
|
|
744
908
|
* resource with its rates and CPU time. */
|
|
909
|
+
/** One row per resource, for a spreadsheet: when it ran, how big it was, how
|
|
910
|
+
* long it ran, the CPU it used and what it cost. Money is exact dollars to the
|
|
911
|
+
* microdollar (the API's integer microdollars moved six places), so a column
|
|
912
|
+
* sums to the ledger to the last digit. */
|
|
913
|
+
export function usageCsv(u) {
|
|
914
|
+
const cell = (value) => {
|
|
915
|
+
const text = typeof value === "string" || typeof value === "number" || typeof value === "bigint"
|
|
916
|
+
? String(value)
|
|
917
|
+
: "";
|
|
918
|
+
return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
919
|
+
};
|
|
920
|
+
const exact = (value) => {
|
|
921
|
+
const micros = BigInt(typeof value === "number" || typeof value === "string" ? value : 0);
|
|
922
|
+
const sign = micros < 0n ? "-" : "";
|
|
923
|
+
const size = micros < 0n ? -micros : micros;
|
|
924
|
+
return `${sign}${size / 1000000n}.${(size % 1000000n).toString().padStart(6, "0")}`;
|
|
925
|
+
};
|
|
926
|
+
const head = [
|
|
927
|
+
"resource_id",
|
|
928
|
+
"name",
|
|
929
|
+
"kind",
|
|
930
|
+
"state",
|
|
931
|
+
"created_at",
|
|
932
|
+
"vcpu",
|
|
933
|
+
"memory_mib",
|
|
934
|
+
"running_seconds",
|
|
935
|
+
"active_cpu_seconds",
|
|
936
|
+
"memory_gib_seconds",
|
|
937
|
+
"charged_usd",
|
|
938
|
+
"held_usd",
|
|
939
|
+
];
|
|
940
|
+
const rows = (u.resources ?? []).map((r) => [
|
|
941
|
+
r.resourceId,
|
|
942
|
+
r.name,
|
|
943
|
+
r.kind,
|
|
944
|
+
r.state ?? r.status,
|
|
945
|
+
r.createdAt,
|
|
946
|
+
r.vcpu,
|
|
947
|
+
r.memoryMiB,
|
|
948
|
+
r.runningSeconds,
|
|
949
|
+
r.activeCpuSeconds,
|
|
950
|
+
r.memoryGiBSeconds,
|
|
951
|
+
exact(r.chargedMicros),
|
|
952
|
+
exact(r.heldMicros),
|
|
953
|
+
]
|
|
954
|
+
.map(cell)
|
|
955
|
+
.join(","));
|
|
956
|
+
return [head.join(","), ...rows].join("\n");
|
|
957
|
+
}
|
|
745
958
|
function usageSummary(u) {
|
|
746
959
|
const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
|
|
747
960
|
? BigInt(value)
|
|
@@ -751,11 +964,14 @@ function usageSummary(u) {
|
|
|
751
964
|
const rows = [
|
|
752
965
|
["available", dollars(micros(u.available))],
|
|
753
966
|
["credited", dollars(micros(u.credited))],
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
],
|
|
758
|
-
|
|
967
|
+
// spent includes what refunds and disputes took back: say that part on
|
|
968
|
+
// its own line, so "spent" reads as what was used.
|
|
969
|
+
["used", dollars(micros(u.spent) - takenBack)],
|
|
970
|
+
...(takenBack > 0n ? [["returned by refunds and disputes", dollars(takenBack)]] : []),
|
|
971
|
+
// Holds cover running sandboxes and the hour ahead of stored snapshots,
|
|
972
|
+
// images and volumes (pricing guide), so an account with nothing running
|
|
973
|
+
// can still show one.
|
|
974
|
+
["held for running sandboxes and this hour's storage", dollars(micros(u.held))],
|
|
759
975
|
];
|
|
760
976
|
if (micros(u.expired) > 0n)
|
|
761
977
|
rows.push(["expired or taken back", dollars(micros(u.expired))]);
|
|
@@ -790,13 +1006,15 @@ function usageSummary(u) {
|
|
|
790
1006
|
])
|
|
791
1007
|
: "No resources yet.",
|
|
792
1008
|
"",
|
|
793
|
-
`Each resource, with its rates and CPU time: ${me} usage --json`,
|
|
1009
|
+
`Each resource, with its rates and CPU time: ${me} usage --json, or --csv for a spreadsheet`,
|
|
794
1010
|
].join("\n");
|
|
795
1011
|
}
|
|
796
1012
|
const SANDBOX_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
797
1013
|
/** A sandbox's id from its id or its name, for the filters that take an id. */
|
|
798
1014
|
async function sandboxIdOf(runtime, target) {
|
|
799
|
-
return SANDBOX_UUID.test(target)
|
|
1015
|
+
return SANDBOX_UUID.test(target)
|
|
1016
|
+
? target
|
|
1017
|
+
: (await (await import("./ssh.js")).resolveSandbox(runtime, target)).id;
|
|
800
1018
|
}
|
|
801
1019
|
/** What a process has printed so far, in order, without waiting for more. */
|
|
802
1020
|
async function outputSoFar(runtime, sandboxId, processId) {
|
|
@@ -902,7 +1120,7 @@ function trialNote(trial, all) {
|
|
|
902
1120
|
return ` ${which} ran on the free trial, which charged nothing; Runtime's side prices ${trial === 1 ? "it" : "them"} at the standard rates, what the same work costs on paid credit.`;
|
|
903
1121
|
}
|
|
904
1122
|
async function switching(command, argv, env, out) {
|
|
905
|
-
const args = parse(argv);
|
|
1123
|
+
const args = parse(argv, [], command === "compare" ? ["from", "days"] : ["from"]);
|
|
906
1124
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
907
1125
|
const known = new Set(command === "compare" ? ["from", "days"] : ["from"]);
|
|
908
1126
|
for (const name of args.flags.keys())
|
|
@@ -929,7 +1147,7 @@ async function switching(command, argv, env, out) {
|
|
|
929
1147
|
return 0;
|
|
930
1148
|
}
|
|
931
1149
|
if (!from)
|
|
932
|
-
throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines...).`);
|
|
1150
|
+
throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines, lambda-microvms, freestyle, prime...).`);
|
|
933
1151
|
/* The server holds the list of rivals it has rates for, and names them all
|
|
934
1152
|
when it refuses one, so a rival added there needs no new CLI. */
|
|
935
1153
|
const days = integer(args, "days");
|
|
@@ -944,7 +1162,7 @@ async function switching(command, argv, env, out) {
|
|
|
944
1162
|
const monthly = month === undefined
|
|
945
1163
|
? ""
|
|
946
1164
|
: month >= 0n
|
|
947
|
-
? `about ${dollars(month)} a month`
|
|
1165
|
+
? `a saving of about ${dollars(month)} a month`
|
|
948
1166
|
: `about ${dollars(-month)} a month more on Runtime`;
|
|
949
1167
|
/* The month is projected from the days the usage covers, not from the
|
|
950
1168
|
window asked for: say which, or a young account's 7 and 90 days read the
|
|
@@ -953,7 +1171,7 @@ async function switching(command, argv, env, out) {
|
|
|
953
1171
|
const pace = month === undefined
|
|
954
1172
|
? "."
|
|
955
1173
|
: covered < c.window.days
|
|
956
|
-
? `. Your
|
|
1174
|
+
? `. Your usage so far covers ${covered <= 1 ? "less than a day" : `${covered.toLocaleString("en-US")} days`}; at that pace, ${monthly}.`
|
|
957
1175
|
: c.window.days === 30
|
|
958
1176
|
? `, ${monthly}.`
|
|
959
1177
|
: `. At that pace, ${monthly}.`;
|
|
@@ -974,7 +1192,7 @@ async function switching(command, argv, env, out) {
|
|
|
974
1192
|
return 0;
|
|
975
1193
|
}
|
|
976
1194
|
async function support(argv, env, out) {
|
|
977
|
-
const args = parse(argv);
|
|
1195
|
+
const args = parse(argv, [], ["conversation"]);
|
|
978
1196
|
const api = (await client(env)).support;
|
|
979
1197
|
const conversationId = flag(args, "conversation");
|
|
980
1198
|
const [first, second, third] = args.positional;
|
|
@@ -1012,9 +1230,10 @@ async function sandbox(argv, env, out) {
|
|
|
1012
1230
|
}
|
|
1013
1231
|
// Inside a sandbox, with no API key: the sandbox's own request token asks.
|
|
1014
1232
|
if (verb === "identity-token") {
|
|
1015
|
-
const args = parse(rest);
|
|
1233
|
+
const args = parse(rest, [], ["audience", "lifetime"]);
|
|
1016
1234
|
const audience = need(flag(args, "audience"), "--audience, such as sts.amazonaws.com");
|
|
1017
1235
|
const lifetime = flag(args, "lifetime");
|
|
1236
|
+
const { identityToken } = await import("./identity.js");
|
|
1018
1237
|
const token = await identityToken({
|
|
1019
1238
|
audience,
|
|
1020
1239
|
...(lifetime === undefined ? {} : { lifetimeSeconds: Number(lifetime) }),
|
|
@@ -1025,17 +1244,26 @@ async function sandbox(argv, env, out) {
|
|
|
1025
1244
|
const runtime = await client(env);
|
|
1026
1245
|
/* By id or by name, for every command here, as ssh and port-forward
|
|
1027
1246
|
always took one. */
|
|
1028
|
-
const get = (id) => resolveSandbox(runtime, need(id, "a sandbox id or name"));
|
|
1247
|
+
const get = async (id) => (await import("./ssh.js")).resolveSandbox(runtime, need(id, "a sandbox id or name"));
|
|
1248
|
+
/* For a command that needs only the id: an id is used as it is, with no
|
|
1249
|
+
lookup first, so `exec` is one request and not two (about 110 ms less
|
|
1250
|
+
from Colorado to Virginia). A sandbox that is gone or stopped is refused
|
|
1251
|
+
by the command itself, with the same words. */
|
|
1252
|
+
const use = async (id) => SANDBOX_UUID.test(id ?? "")
|
|
1253
|
+
? new SandboxHandle(runtime.transport, { id })
|
|
1254
|
+
: get(id);
|
|
1029
1255
|
switch (verb) {
|
|
1030
1256
|
case "create": {
|
|
1031
|
-
const args = parse(rest, CREATE_FLAGS);
|
|
1032
|
-
const sbx = await runtime.sandboxes.create(createInput(args)
|
|
1257
|
+
const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
|
|
1258
|
+
const sbx = await runtime.sandboxes.create(createInput(args), {
|
|
1259
|
+
onCapacityWait: waiting(out),
|
|
1260
|
+
});
|
|
1033
1261
|
print(sbx.id, sbx.info);
|
|
1034
1262
|
return 0;
|
|
1035
1263
|
}
|
|
1036
1264
|
case "ls":
|
|
1037
1265
|
case "list": {
|
|
1038
|
-
const args = parse(rest, ["all"]);
|
|
1266
|
+
const args = parse(rest, ["all"], ["label", "state"]);
|
|
1039
1267
|
const page = await runtime.sandboxes.list({
|
|
1040
1268
|
...(has(args, "all") ? { includeStopped: true } : {}),
|
|
1041
1269
|
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
@@ -1087,8 +1315,8 @@ async function sandbox(argv, env, out) {
|
|
|
1087
1315
|
return 0;
|
|
1088
1316
|
}
|
|
1089
1317
|
case "mount": {
|
|
1090
|
-
const args = parse(rest, ["read-only"]);
|
|
1091
|
-
const sbx = await
|
|
1318
|
+
const args = parse(rest, ["read-only"], ["secret", "region", "endpoint", "account-id"]);
|
|
1319
|
+
const sbx = await use(args.positional[0]);
|
|
1092
1320
|
const source = need(args.positional[1], "a bucket, as s3://bucket, r2://bucket or gcs://bucket");
|
|
1093
1321
|
const found = /^(s3|r2|gcs):\/\/([^/]+)(?:\/(.+))?$/.exec(source);
|
|
1094
1322
|
if (!found)
|
|
@@ -1108,7 +1336,7 @@ async function sandbox(argv, env, out) {
|
|
|
1108
1336
|
return 0;
|
|
1109
1337
|
}
|
|
1110
1338
|
case "mounts": {
|
|
1111
|
-
const sbx = await
|
|
1339
|
+
const sbx = await use(rest[0]);
|
|
1112
1340
|
const all = await sbx.mounts.list();
|
|
1113
1341
|
print(all.length
|
|
1114
1342
|
? table([
|
|
@@ -1125,13 +1353,13 @@ async function sandbox(argv, env, out) {
|
|
|
1125
1353
|
return 0;
|
|
1126
1354
|
}
|
|
1127
1355
|
case "unmount": {
|
|
1128
|
-
const sbx = await
|
|
1356
|
+
const sbx = await use(rest[0]);
|
|
1129
1357
|
const gone = await sbx.mounts.remove(need(rest[1], "the mounted path"));
|
|
1130
1358
|
print(`${gone.path} unmounted`, gone);
|
|
1131
1359
|
return 0;
|
|
1132
1360
|
}
|
|
1133
1361
|
case "snapshot": {
|
|
1134
|
-
const args = parse(rest);
|
|
1362
|
+
const args = parse(rest, [], ["name", "retention"]);
|
|
1135
1363
|
const sbx = await get(args.positional[0]);
|
|
1136
1364
|
const snapshot = await sbx.snapshot({
|
|
1137
1365
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
@@ -1141,7 +1369,7 @@ async function sandbox(argv, env, out) {
|
|
|
1141
1369
|
return 0;
|
|
1142
1370
|
}
|
|
1143
1371
|
case "fork": {
|
|
1144
|
-
const args = parse(rest, ["keep-snapshot", "trial", "paid"]);
|
|
1372
|
+
const args = parse(rest, ["keep-snapshot", "trial", "paid"], ["count", "name"]);
|
|
1145
1373
|
const sbx = await get(args.positional[0]);
|
|
1146
1374
|
const forks = await sbx.fork({
|
|
1147
1375
|
count: integer(args, "count") ?? 1,
|
|
@@ -1157,7 +1385,7 @@ async function sandbox(argv, env, out) {
|
|
|
1157
1385
|
return 0;
|
|
1158
1386
|
}
|
|
1159
1387
|
case "update": {
|
|
1160
|
-
const args = parse(rest);
|
|
1388
|
+
const args = parse(rest, [], ["name", "label", "idle-pause", "auto-wake", "persistent", "max-total-cost"]);
|
|
1161
1389
|
const sbx = await get(args.positional[0]);
|
|
1162
1390
|
const onOff = (name) => {
|
|
1163
1391
|
const value = flag(args, name);
|
|
@@ -1198,19 +1426,19 @@ async function sandbox(argv, env, out) {
|
|
|
1198
1426
|
return 0;
|
|
1199
1427
|
}
|
|
1200
1428
|
case "exec": {
|
|
1201
|
-
const args = parse(rest);
|
|
1429
|
+
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandbox exec <id> -- ls -la`);
|
|
1202
1430
|
const command = args.rest ?? args.positional.slice(1);
|
|
1203
1431
|
if (!command.length)
|
|
1204
1432
|
throw usage("Give the command after --: runtime sandbox exec <id> -- ls -la");
|
|
1205
|
-
const sbx = await
|
|
1433
|
+
const sbx = await use(args.positional[0]);
|
|
1206
1434
|
return execute(sbx, command, args, out, integer(args, "timeout"));
|
|
1207
1435
|
}
|
|
1208
1436
|
case "spawn": {
|
|
1209
|
-
const args = parse(rest, ["pty"]);
|
|
1437
|
+
const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandbox spawn <id> -- npm run dev`);
|
|
1210
1438
|
const command = args.rest ?? [];
|
|
1211
1439
|
if (!command.length)
|
|
1212
1440
|
throw usage("Give the command after --: runtime sandbox spawn <id> -- npm run dev");
|
|
1213
|
-
const sbx = await
|
|
1441
|
+
const sbx = await use(args.positional[0]);
|
|
1214
1442
|
const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
|
|
1215
1443
|
...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
|
|
1216
1444
|
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
@@ -1220,7 +1448,7 @@ async function sandbox(argv, env, out) {
|
|
|
1220
1448
|
return 0;
|
|
1221
1449
|
}
|
|
1222
1450
|
case "ps": {
|
|
1223
|
-
const sbx = await
|
|
1451
|
+
const sbx = await use(rest[0]);
|
|
1224
1452
|
const list = await sbx.processes.list();
|
|
1225
1453
|
print(list.length
|
|
1226
1454
|
? table([
|
|
@@ -1231,8 +1459,8 @@ async function sandbox(argv, env, out) {
|
|
|
1231
1459
|
return 0;
|
|
1232
1460
|
}
|
|
1233
1461
|
case "logs": {
|
|
1234
|
-
const args = parse(rest, ["f", "follow"]);
|
|
1235
|
-
const sbx = await
|
|
1462
|
+
const args = parse(rest, ["f", "follow"], []);
|
|
1463
|
+
const sbx = await use(args.positional[0]);
|
|
1236
1464
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
1237
1465
|
if (has(args, "f") || has(args, "follow")) {
|
|
1238
1466
|
for await (const event of proc.output())
|
|
@@ -1258,26 +1486,28 @@ async function sandbox(argv, env, out) {
|
|
|
1258
1486
|
return 0;
|
|
1259
1487
|
}
|
|
1260
1488
|
case "kill": {
|
|
1261
|
-
const args = parse(rest);
|
|
1262
|
-
const sbx = await
|
|
1489
|
+
const args = parse(rest, [], ["signal"]);
|
|
1490
|
+
const sbx = await use(args.positional[0]);
|
|
1263
1491
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
1264
1492
|
await proc.kill((flag(args, "signal") ?? "SIGTERM"));
|
|
1265
1493
|
print(`Signalled ${proc.id}.`, { processId: proc.id });
|
|
1266
1494
|
return 0;
|
|
1267
1495
|
}
|
|
1268
1496
|
case "shell": {
|
|
1269
|
-
const args = parse(rest);
|
|
1270
|
-
const sbx = await
|
|
1497
|
+
const args = parse(rest, [], ["command"]);
|
|
1498
|
+
const sbx = await use(args.positional[0]);
|
|
1271
1499
|
return shell(sbx, flag(args, "command"));
|
|
1272
1500
|
}
|
|
1273
1501
|
case "cat": {
|
|
1274
|
-
const sbx = await
|
|
1275
|
-
|
|
1502
|
+
const sbx = await use(rest[0]);
|
|
1503
|
+
// Streamed, any size; a stream that ends short throws, so exit is 1.
|
|
1504
|
+
const { Writable } = await import("node:stream");
|
|
1505
|
+
await (await sbx.files.readStream(need(rest[1], "a path"))).pipeTo(Writable.toWeb(process.stdout), { preventClose: true, preventAbort: true });
|
|
1276
1506
|
return 0;
|
|
1277
1507
|
}
|
|
1278
1508
|
case "files": {
|
|
1279
|
-
const args = parse(rest);
|
|
1280
|
-
const sbx = await
|
|
1509
|
+
const args = parse(rest, [], ["depth", "glob"]);
|
|
1510
|
+
const sbx = await use(args.positional[0]);
|
|
1281
1511
|
const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
|
|
1282
1512
|
...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
|
|
1283
1513
|
...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
|
|
@@ -1288,36 +1518,51 @@ async function sandbox(argv, env, out) {
|
|
|
1288
1518
|
return 0;
|
|
1289
1519
|
}
|
|
1290
1520
|
case "cp": {
|
|
1291
|
-
const [from, to] = parse(rest).positional;
|
|
1521
|
+
const [from, to] = parse(rest, ["r", "recursive"], []).positional;
|
|
1292
1522
|
const source = remote(need(from, "a source"));
|
|
1293
1523
|
const target = remote(need(to, "a destination"));
|
|
1294
|
-
|
|
1295
|
-
|
|
1524
|
+
// A destination ending in / (or a local directory) takes the source's
|
|
1525
|
+
// own name inside it, as cp does.
|
|
1526
|
+
const inside = (dir, path) => `${dir.replace(/\/+$/, "")}/${path.replace(/\/+$/, "").split("/").pop()}`;
|
|
1527
|
+
if (source && !target) {
|
|
1528
|
+
const local = await import("node:fs/promises").then((fs) => fs.stat(to).catch(() => null));
|
|
1529
|
+
await (await use(source.id)).files.download(source.path, to.endsWith("/") || local?.isDirectory() ? inside(to, source.path) : to);
|
|
1530
|
+
}
|
|
1296
1531
|
else if (!source && target)
|
|
1297
|
-
await (await
|
|
1532
|
+
await (await use(target.id)).files.upload(from, target.path.endsWith("/") ? inside(target.path, from) : target.path);
|
|
1298
1533
|
else
|
|
1299
1534
|
throw usage("Name exactly one side as <sandbox-id>:/path.");
|
|
1300
1535
|
print(`Copied ${from} to ${to}.`, { from, to });
|
|
1301
1536
|
return 0;
|
|
1302
1537
|
}
|
|
1303
1538
|
case "run-code":
|
|
1304
|
-
return runCode(await
|
|
1539
|
+
return runCode(await use(rest[0]), rest.slice(1), out);
|
|
1305
1540
|
case "preview": {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1541
|
+
if (rest[0] === "rotate") {
|
|
1542
|
+
const port = portOf(rest[2], "runtime sandbox preview rotate <id> 3000");
|
|
1543
|
+
const sbx = await use(rest[1]);
|
|
1544
|
+
const preview = await sbx.previews.rotate(port);
|
|
1545
|
+
print(preview.visibility === "public"
|
|
1546
|
+
? `Port ${port} is public: it takes no token, so there was none to refuse.`
|
|
1547
|
+
: `${preview.urlWithToken ?? preview.url}\nEvery earlier token for port ${port} is refused. New header: x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`, preview);
|
|
1548
|
+
return 0;
|
|
1549
|
+
}
|
|
1550
|
+
const args = parse(rest, ["public"], ["ttl"]);
|
|
1551
|
+
const port = portOf(args.positional[1], "runtime sandbox preview <id> 3000");
|
|
1552
|
+
const sbx = await use(args.positional[0]);
|
|
1309
1553
|
const preview = await sbx.previews.create(port, {
|
|
1310
1554
|
...(has(args, "public") ? { visibility: "public" } : {}),
|
|
1311
1555
|
...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
|
|
1312
1556
|
});
|
|
1313
|
-
|
|
1557
|
+
// The link and how to use it, nothing more: the API's `hint` (which
|
|
1558
|
+
// carries a referral line) stays in --json, not in the command's words.
|
|
1559
|
+
print(preview.visibility === "public"
|
|
1314
1560
|
? preview.url
|
|
1315
|
-
: `${preview.urlWithToken ?? preview.url}\nPrivate: send the header x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt})
|
|
1316
|
-
(preview.hint ? `\n${preview.hint}` : ""), preview);
|
|
1561
|
+
: `${preview.urlWithToken ?? preview.url}\nPrivate: send the header x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`, preview);
|
|
1317
1562
|
return 0;
|
|
1318
1563
|
}
|
|
1319
1564
|
case "previews": {
|
|
1320
|
-
const list = await (await
|
|
1565
|
+
const list = await (await use(rest[0])).previews.list();
|
|
1321
1566
|
print(list.length
|
|
1322
1567
|
? table([
|
|
1323
1568
|
["PORT", "VISIBILITY", "URL"],
|
|
@@ -1327,14 +1572,14 @@ async function sandbox(argv, env, out) {
|
|
|
1327
1572
|
return 0;
|
|
1328
1573
|
}
|
|
1329
1574
|
case "unshare": {
|
|
1330
|
-
const
|
|
1331
|
-
const
|
|
1575
|
+
const port = portOf(rest[1], "runtime sandbox unshare <id> 3000");
|
|
1576
|
+
const sbx = await use(rest[0]);
|
|
1332
1577
|
print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
|
|
1333
1578
|
return 0;
|
|
1334
1579
|
}
|
|
1335
1580
|
case "network": {
|
|
1336
|
-
const args = parse(rest, ["no-internet", "internet"]);
|
|
1337
|
-
const sbx = await
|
|
1581
|
+
const args = parse(rest, ["no-internet", "internet"], ["allow", "deny", "connect"]);
|
|
1582
|
+
const sbx = await use(args.positional[0]);
|
|
1338
1583
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
1339
1584
|
const policy = has(args, "no-internet") || has(args, "internet") || rules.length
|
|
1340
1585
|
? await sbx.network.set({
|
|
@@ -1344,7 +1589,10 @@ async function sandbox(argv, env, out) {
|
|
|
1344
1589
|
: await sbx.network.get();
|
|
1345
1590
|
print(table([
|
|
1346
1591
|
["internet", policy.internet ? "on" : "off"],
|
|
1347
|
-
[
|
|
1592
|
+
[
|
|
1593
|
+
"allow",
|
|
1594
|
+
policy.allow.join(", ") || (policy.internet ? "(the public web)" : "(nothing)"),
|
|
1595
|
+
],
|
|
1348
1596
|
["deny", policy.deny.join(", ") || "-"],
|
|
1349
1597
|
["connect", policy.connect.join(", ") || "-"],
|
|
1350
1598
|
["enforced", policy.enforced ? "yes" : "not yet; run the same command again"],
|
|
@@ -1352,9 +1600,9 @@ async function sandbox(argv, env, out) {
|
|
|
1352
1600
|
return 0;
|
|
1353
1601
|
}
|
|
1354
1602
|
case "desktop":
|
|
1355
|
-
return desktop(await
|
|
1603
|
+
return desktop(await use(rest[0]), rest.slice(1), out);
|
|
1356
1604
|
case "watch":
|
|
1357
|
-
return (await import("./cli-extras.js")).watchCommand(await
|
|
1605
|
+
return (await import("./cli-extras.js")).watchCommand(await use(rest[0]), rest.slice(1), out, kit);
|
|
1358
1606
|
case "mcp":
|
|
1359
1607
|
if (rest[0] === "catalog" || rest[0] === undefined) {
|
|
1360
1608
|
const catalog = await runtime.mcp.catalog();
|
|
@@ -1373,10 +1621,10 @@ async function sandbox(argv, env, out) {
|
|
|
1373
1621
|
]), catalog);
|
|
1374
1622
|
return 0;
|
|
1375
1623
|
}
|
|
1376
|
-
return (await import("./cli-extras.js")).mcpCommand(await
|
|
1624
|
+
return (await import("./cli-extras.js")).mcpCommand(await use(rest[0]), rest.slice(1), out, kit);
|
|
1377
1625
|
case "metrics": {
|
|
1378
|
-
const args = parse(rest);
|
|
1379
|
-
const sbx = await
|
|
1626
|
+
const args = parse(rest, [], ["range"]);
|
|
1627
|
+
const sbx = await use(args.positional[0]);
|
|
1380
1628
|
const range = (flag(args, "range") ?? "1h");
|
|
1381
1629
|
if (!["15m", "1h", "6h", "24h", "7d", "30d"].includes(range))
|
|
1382
1630
|
throw usage("--range is one of 15m, 1h, 6h, 24h, 7d, 30d.");
|
|
@@ -1438,20 +1686,49 @@ async function readSecret() {
|
|
|
1438
1686
|
stdin.pause();
|
|
1439
1687
|
}
|
|
1440
1688
|
}
|
|
1689
|
+
/** `--allow` values as secret rules: each is `[METHOD[,METHOD]...] PATH`, and
|
|
1690
|
+
* rules naming the same methods share one rule's paths. Only the shape is read
|
|
1691
|
+
* here; the API holds every rule to the same check the host makes
|
|
1692
|
+
* (ARCHITECTURE.md section 10, "Per-path rules on an egress secret") and says
|
|
1693
|
+
* which one it refused. */
|
|
1694
|
+
export function parseSecretRules(values) {
|
|
1695
|
+
const byMethods = new Map();
|
|
1696
|
+
for (const value of values) {
|
|
1697
|
+
const words = value.trim().split(/\s+/);
|
|
1698
|
+
const path = words.at(-1);
|
|
1699
|
+
if (!path || words.length > 2 || !path.startsWith("/"))
|
|
1700
|
+
throw usage(`--allow takes optional methods and one path, such as --allow "GET,HEAD /repos/acme/*" or --allow /v1/chat/completions; got ${JSON.stringify(value)}.`);
|
|
1701
|
+
const methods = words.length === 2
|
|
1702
|
+
? [...new Set(words[0].toUpperCase().split(",").filter(Boolean))].sort()
|
|
1703
|
+
: undefined;
|
|
1704
|
+
const key = methods?.join(",") ?? "";
|
|
1705
|
+
const rule = byMethods.get(key) ?? { ...(methods ? { methods } : {}), paths: [] };
|
|
1706
|
+
if (!rule.paths.includes(path))
|
|
1707
|
+
rule.paths.push(path);
|
|
1708
|
+
byMethods.set(key, rule);
|
|
1709
|
+
}
|
|
1710
|
+
return [...byMethods.values()];
|
|
1711
|
+
}
|
|
1712
|
+
const ruleText = (rules) => rules?.length
|
|
1713
|
+
? rules
|
|
1714
|
+
.map((rule) => `${rule.methods ? `${rule.methods.join(",")} ` : ""}${rule.paths.join(" ")}`)
|
|
1715
|
+
.join("; ")
|
|
1716
|
+
: "-";
|
|
1441
1717
|
export async function secretsCommand(argv, env, out,
|
|
1442
1718
|
/** Tests give the value here; the CLI reads standard input. */
|
|
1443
1719
|
read = readSecret) {
|
|
1444
1720
|
const [verb, ...rest] = argv;
|
|
1445
|
-
const args = parse(rest);
|
|
1721
|
+
const args = parse(rest, [], ["host", "header", "format", "allow", "value"]);
|
|
1446
1722
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1447
1723
|
const rows = (list) => list.length
|
|
1448
1724
|
? table([
|
|
1449
|
-
["name", "hosts", "header", "placeholder"],
|
|
1725
|
+
["name", "hosts", "header", "placeholder", "allow"],
|
|
1450
1726
|
...list.map((secret) => [
|
|
1451
1727
|
secret.name,
|
|
1452
1728
|
secret.hosts.join(","),
|
|
1453
1729
|
secret.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
|
|
1454
1730
|
secret.placeholder,
|
|
1731
|
+
ruleText(secret.rules),
|
|
1455
1732
|
]),
|
|
1456
1733
|
])
|
|
1457
1734
|
: "No secrets.";
|
|
@@ -1472,17 +1749,21 @@ read = readSecret) {
|
|
|
1472
1749
|
throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
|
|
1473
1750
|
const header = flag(args, "header");
|
|
1474
1751
|
const format = flag(args, "format");
|
|
1752
|
+
const allow = args.flags.get("allow");
|
|
1753
|
+
const rules = allow ? parseSecretRules(allow) : undefined;
|
|
1475
1754
|
const saved = await (await client(env)).secrets.set(name, {
|
|
1476
1755
|
value,
|
|
1477
1756
|
hosts,
|
|
1478
1757
|
...(header ? { header } : {}),
|
|
1479
1758
|
...(format ? { format } : {}),
|
|
1759
|
+
...(rules ? { rules } : {}),
|
|
1480
1760
|
});
|
|
1481
1761
|
print([
|
|
1482
1762
|
`Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
|
|
1483
1763
|
saved.header
|
|
1484
1764
|
? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
|
|
1485
1765
|
: `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the URL and headers.`,
|
|
1766
|
+
...(saved.rules?.length ? [`Only these requests get it: ${ruleText(saved.rules)}.`] : []),
|
|
1486
1767
|
...(saved.enforced
|
|
1487
1768
|
? []
|
|
1488
1769
|
: ["A host is catching up; running sandboxes have it within a minute."]),
|
|
@@ -1506,7 +1787,7 @@ async function readInput(source) {
|
|
|
1506
1787
|
return Buffer.concat(chunks).toString("utf8");
|
|
1507
1788
|
}
|
|
1508
1789
|
async function runCode(sbx, argv, out) {
|
|
1509
|
-
const args = parse(argv);
|
|
1790
|
+
const args = parse(argv, [], ["lang", "context", "timeout", "out-dir"]);
|
|
1510
1791
|
const source = need(args.positional[0], "a file, or - to read the code from standard input");
|
|
1511
1792
|
const code = await readInput(source);
|
|
1512
1793
|
const byExtension = {
|
|
@@ -1574,7 +1855,7 @@ async function desktop(sbx, argv, out) {
|
|
|
1574
1855
|
const d = sbx.desktop;
|
|
1575
1856
|
switch (action) {
|
|
1576
1857
|
case "start": {
|
|
1577
|
-
const args = parse(rest);
|
|
1858
|
+
const args = parse(rest, [], ["width", "height"]);
|
|
1578
1859
|
const started = await d.start({
|
|
1579
1860
|
...(integer(args, "width") ? { width: integer(args, "width") } : {}),
|
|
1580
1861
|
...(integer(args, "height") ? { height: integer(args, "height") } : {}),
|
|
@@ -1640,6 +1921,7 @@ async function accountCommand(argv, env, out) {
|
|
|
1640
1921
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1641
1922
|
const store = connectionStore(env);
|
|
1642
1923
|
if (verb === "add") {
|
|
1924
|
+
const { authenticationCommand } = await import("./login.js");
|
|
1643
1925
|
const result = await authenticationCommand(["login", "--another-account", ...rest], env);
|
|
1644
1926
|
if (result.pending)
|
|
1645
1927
|
throw notApprovedYet(result);
|
|
@@ -1662,8 +1944,35 @@ async function accountCommand(argv, env, out) {
|
|
|
1662
1944
|
});
|
|
1663
1945
|
return 0;
|
|
1664
1946
|
}
|
|
1947
|
+
if (verb === "close") {
|
|
1948
|
+
/* ARCHITECTURE.md section 3.1: an owner's key for every product, and the
|
|
1949
|
+
account's name typed exactly. Nothing is asked twice: --confirm is the
|
|
1950
|
+
confirmation, as the website's typed name is. */
|
|
1951
|
+
const args = parse(rest, [], ["confirm"]);
|
|
1952
|
+
for (const name of args.flags.keys())
|
|
1953
|
+
if (name !== "confirm")
|
|
1954
|
+
throw usage(`Unknown option --${name}.`);
|
|
1955
|
+
const confirm = flag(args, "confirm");
|
|
1956
|
+
const runtime = await client(env);
|
|
1957
|
+
if (!confirm) {
|
|
1958
|
+
const who = await runtime.me();
|
|
1959
|
+
throw usage(`Closing stops and deletes everything in the account, ends every key and cannot be undone. To go ahead, type its name: ${me} account close --confirm "${who.orgName ?? "<account name>"}".`);
|
|
1960
|
+
}
|
|
1961
|
+
const closed = await runtime.account.close({ confirm });
|
|
1962
|
+
// This machine's key died with the account.
|
|
1963
|
+
if (!env.RUNTIME_API_KEY)
|
|
1964
|
+
await store.remove().catch(() => undefined);
|
|
1965
|
+
print([
|
|
1966
|
+
`Closed ${closed.name}. Every key has stopped working.`,
|
|
1967
|
+
closed.released
|
|
1968
|
+
? "Nothing was left running or stored."
|
|
1969
|
+
: "What was still running is stopping, and everything stored is being deleted.",
|
|
1970
|
+
"Credit bought in the last 15 days and not spent is refunded on request: write to support@withruntime.com.",
|
|
1971
|
+
].join("\n"), closed);
|
|
1972
|
+
return 0;
|
|
1973
|
+
}
|
|
1665
1974
|
if (verb !== "ls" && verb !== "list")
|
|
1666
|
-
throw unknown("account command", verb, ["ls", "switch", "add"]);
|
|
1975
|
+
throw unknown("account command", verb, ["ls", "switch", "add", "close"]);
|
|
1667
1976
|
await nameSaved(env).catch(() => undefined);
|
|
1668
1977
|
const current = await store.read().catch(() => null);
|
|
1669
1978
|
const saved = [
|
|
@@ -1719,7 +2028,7 @@ async function keys(argv, env, out) {
|
|
|
1719
2028
|
throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
|
|
1720
2029
|
if (verb !== "create")
|
|
1721
2030
|
throw unknown("keys command", verb, ["create"]);
|
|
1722
|
-
const args = parse(rest, ["read-only"]);
|
|
2031
|
+
const args = parse(rest, ["read-only"], ["name", "daily-limit"]);
|
|
1723
2032
|
const known = new Set(["name", "read-only", "daily-limit"]);
|
|
1724
2033
|
for (const name of args.flags.keys())
|
|
1725
2034
|
if (!known.has(name))
|
|
@@ -1730,6 +2039,7 @@ async function keys(argv, env, out) {
|
|
|
1730
2039
|
const limit = flag(args, "daily-limit");
|
|
1731
2040
|
if (readOnly && limit !== undefined)
|
|
1732
2041
|
throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
|
|
2042
|
+
const { createKey, dailyLimitMicros } = await import("./keys.js");
|
|
1733
2043
|
const created = await createKey({
|
|
1734
2044
|
name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
|
|
1735
2045
|
access: readOnly ? "read" : "full",
|
|
@@ -1756,8 +2066,8 @@ const SNAPSHOT_HELP = `runtime snapshot <command>
|
|
|
1756
2066
|
get <id> One snapshot
|
|
1757
2067
|
extend <id> <days> Keep it longer
|
|
1758
2068
|
rm <id> Delete it
|
|
1759
|
-
Take one with \`runtime sandbox snapshot <sandboxId>\`; start from
|
|
1760
|
-
\`runtime sandbox
|
|
2069
|
+
Take one with \`runtime sandbox snapshot <sandboxId>\`; start a sandbox from it
|
|
2070
|
+
with \`runtime sandbox create --snapshot <snapshotId>\`.
|
|
1761
2071
|
`;
|
|
1762
2072
|
async function snapshot(argv, env, out) {
|
|
1763
2073
|
const [verb, ...rest] = argv;
|
|
@@ -1770,7 +2080,7 @@ async function snapshot(argv, env, out) {
|
|
|
1770
2080
|
switch (verb) {
|
|
1771
2081
|
case "ls":
|
|
1772
2082
|
case "list": {
|
|
1773
|
-
const args = parse(rest);
|
|
2083
|
+
const args = parse(rest, [], ["sandbox", "name"]);
|
|
1774
2084
|
const all = await (await api.list({
|
|
1775
2085
|
...(flag(args, "sandbox")
|
|
1776
2086
|
? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
|
|
@@ -1787,11 +2097,11 @@ async function snapshot(argv, env, out) {
|
|
|
1787
2097
|
}
|
|
1788
2098
|
case "get": {
|
|
1789
2099
|
const found = await api.get(need(rest[0], "a snapshot id"));
|
|
1790
|
-
print(
|
|
2100
|
+
print((await import("./cli-extras.js")).describeSnapshot(found, table), found);
|
|
1791
2101
|
return 0;
|
|
1792
2102
|
}
|
|
1793
2103
|
case "extend": {
|
|
1794
|
-
const days = integer(parse(rest.slice(1)), "days") ?? Number(need(rest[1], "days"));
|
|
2104
|
+
const days = integer(parse(rest.slice(1), [], ["days"]), "days") ?? Number(need(rest[1], "days"));
|
|
1795
2105
|
const found = await api.extend(need(rest[0], "a snapshot id"), days);
|
|
1796
2106
|
print(`${found.id} kept until ${found.expiresAt}`, found);
|
|
1797
2107
|
return 0;
|
|
@@ -1811,8 +2121,10 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
1811
2121
|
build [<folder>] [-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
|
|
1812
2122
|
[-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
|
|
1813
2123
|
[--start <command>] [--ready-port <port> | --ready-command <command>]
|
|
1814
|
-
[--max-image-mib 8192] [--timeout <seconds>]
|
|
2124
|
+
[--max-image-mib 8192] [--disk-mib <1024-32768>] [--timeout <seconds>]
|
|
1815
2125
|
Build and stream the log; prints the image id when ready.
|
|
2126
|
+
--disk-mib sizes the build's scratch disk, where earlier
|
|
2127
|
+
stages and the context live.
|
|
1816
2128
|
A Dockerfile's folder is the context: everything its
|
|
1817
2129
|
.dockerignore leaves in (100 MiB compressed at most) is
|
|
1818
2130
|
sent, and a rebuild sends only what changed.
|
|
@@ -1833,6 +2145,28 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
1833
2145
|
registry rm <registry>
|
|
1834
2146
|
Use one: runtime sandbox create --image app:latest
|
|
1835
2147
|
`;
|
|
2148
|
+
/** Options `image build` takes with a value. */
|
|
2149
|
+
const IMAGE_BUILD_VALUES = [
|
|
2150
|
+
"f",
|
|
2151
|
+
"file",
|
|
2152
|
+
"dockerfile",
|
|
2153
|
+
"from",
|
|
2154
|
+
"pip",
|
|
2155
|
+
"apt",
|
|
2156
|
+
"npm",
|
|
2157
|
+
"t",
|
|
2158
|
+
"tag",
|
|
2159
|
+
"name",
|
|
2160
|
+
"target",
|
|
2161
|
+
"build-arg",
|
|
2162
|
+
"start",
|
|
2163
|
+
"ready-port",
|
|
2164
|
+
"ready-command",
|
|
2165
|
+
"ready-timeout",
|
|
2166
|
+
"max-image-mib",
|
|
2167
|
+
"disk-mib",
|
|
2168
|
+
"timeout",
|
|
2169
|
+
];
|
|
1836
2170
|
export async function imageCommand(argv, env, out,
|
|
1837
2171
|
/** Tests give the registry secret here; the CLI reads standard input. */
|
|
1838
2172
|
read = readSecret) {
|
|
@@ -1845,7 +2179,7 @@ read = readSecret) {
|
|
|
1845
2179
|
const api = (await client(env)).images;
|
|
1846
2180
|
switch (verb) {
|
|
1847
2181
|
case "build": {
|
|
1848
|
-
const args = parse(rest, ["no-cache"]);
|
|
2182
|
+
const args = parse(rest, ["no-cache"], IMAGE_BUILD_VALUES);
|
|
1849
2183
|
const list = (name) => (args.flags.get(name) ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean);
|
|
1850
2184
|
const recipe = { apt: list("apt"), pip: list("pip"), npm: list("npm") };
|
|
1851
2185
|
const hasRecipe = recipe.apt.length + recipe.pip.length + recipe.npm.length > 0;
|
|
@@ -1857,6 +2191,7 @@ read = readSecret) {
|
|
|
1857
2191
|
throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
|
|
1858
2192
|
const build = {
|
|
1859
2193
|
...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
|
|
2194
|
+
...(integer(args, "disk-mib") ? { diskMiB: integer(args, "disk-mib") } : {}),
|
|
1860
2195
|
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
1861
2196
|
};
|
|
1862
2197
|
// -t app, -t app:v2 -t app:latest: one name, its tags.
|
|
@@ -1946,7 +2281,7 @@ read = readSecret) {
|
|
|
1946
2281
|
return 0;
|
|
1947
2282
|
}
|
|
1948
2283
|
case "logs": {
|
|
1949
|
-
const args = parse(rest, ["follow", "f"]);
|
|
2284
|
+
const args = parse(rest, ["follow", "f"], []);
|
|
1950
2285
|
const id = (await api.resolve(need(args.positional[0], "an image"))).id;
|
|
1951
2286
|
if (has(args, "follow") || has(args, "f")) {
|
|
1952
2287
|
const lines = [];
|
|
@@ -1995,7 +2330,7 @@ read = readSecret) {
|
|
|
1995
2330
|
return 0;
|
|
1996
2331
|
}
|
|
1997
2332
|
if (action === "set") {
|
|
1998
|
-
const args = parse(more);
|
|
2333
|
+
const args = parse(more, [], ["username", "access-key-id"]);
|
|
1999
2334
|
const registry = need(args.positional[0], "a registry, such as ghcr.io");
|
|
2000
2335
|
const accessKeyId = flag(args, "access-key-id");
|
|
2001
2336
|
if (accessKeyId === undefined && flag(args, "username") === undefined)
|
|
@@ -2057,7 +2392,7 @@ async function volume(argv, env, out) {
|
|
|
2057
2392
|
const api = (await client(env)).volumes;
|
|
2058
2393
|
switch (verb) {
|
|
2059
2394
|
case "create": {
|
|
2060
|
-
const args = parse(rest);
|
|
2395
|
+
const args = parse(rest, [], ["size-mib", "name"]);
|
|
2061
2396
|
const created = await api.create({
|
|
2062
2397
|
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
2063
2398
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
@@ -2095,7 +2430,7 @@ async function volume(argv, env, out) {
|
|
|
2095
2430
|
return 0;
|
|
2096
2431
|
}
|
|
2097
2432
|
case "backup": {
|
|
2098
|
-
const args = parse(rest, ["no-wait"]);
|
|
2433
|
+
const args = parse(rest, ["no-wait"], ["retention-days"]);
|
|
2099
2434
|
const id = need(args.positional[0], "a volume id");
|
|
2100
2435
|
const days = integer(args, "retention-days");
|
|
2101
2436
|
const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
|
|
@@ -2125,7 +2460,7 @@ async function volume(argv, env, out) {
|
|
|
2125
2460
|
return 0;
|
|
2126
2461
|
}
|
|
2127
2462
|
case "restore": {
|
|
2128
|
-
const args = parse(rest);
|
|
2463
|
+
const args = parse(rest, [], ["name"]);
|
|
2129
2464
|
const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
|
|
2130
2465
|
if (!out.json && created.state === "creating")
|
|
2131
2466
|
out.error(`Restoring; check with: ${me} volume get ${created.id}`);
|
|
@@ -2139,7 +2474,7 @@ async function volume(argv, env, out) {
|
|
|
2139
2474
|
return 0;
|
|
2140
2475
|
}
|
|
2141
2476
|
case "backup-policy": {
|
|
2142
|
-
const args = parse(rest);
|
|
2477
|
+
const args = parse(rest, [], ["daily", "retention-days"]);
|
|
2143
2478
|
const id = need(args.positional[0], "a volume id");
|
|
2144
2479
|
const daily = flag(args, "daily");
|
|
2145
2480
|
if (daily !== undefined && !["on", "off", "true", "false"].includes(daily))
|
|
@@ -2177,6 +2512,49 @@ const CREATE_FLAGS = [
|
|
|
2177
2512
|
"persistent",
|
|
2178
2513
|
"get-or-create",
|
|
2179
2514
|
];
|
|
2515
|
+
/** Says on standard error, once for each reason, why a create has not
|
|
2516
|
+
* answered yet: it is waiting for a trial slot or room, which can take up to
|
|
2517
|
+
* two minutes, and silence reads as a hang. */
|
|
2518
|
+
function waiting(out) {
|
|
2519
|
+
const said = new Set();
|
|
2520
|
+
return (refusal) => {
|
|
2521
|
+
if (said.has(refusal.code))
|
|
2522
|
+
return;
|
|
2523
|
+
said.add(refusal.code);
|
|
2524
|
+
out.error(`${refusal.message} Waiting for room, up to 2 minutes; Ctrl-C stops.`);
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
/** Options `sandbox create` and `run` take with a value. */
|
|
2528
|
+
const CREATE_VALUES = [
|
|
2529
|
+
"name",
|
|
2530
|
+
"label",
|
|
2531
|
+
"region",
|
|
2532
|
+
"vcpu",
|
|
2533
|
+
"memory",
|
|
2534
|
+
"disk",
|
|
2535
|
+
"cpu",
|
|
2536
|
+
"cpu-floor",
|
|
2537
|
+
"timeout",
|
|
2538
|
+
"on-timeout",
|
|
2539
|
+
"idle-pause",
|
|
2540
|
+
"max-cost",
|
|
2541
|
+
"max-total-cost",
|
|
2542
|
+
"allow",
|
|
2543
|
+
"deny",
|
|
2544
|
+
"connect",
|
|
2545
|
+
"image",
|
|
2546
|
+
"snapshot",
|
|
2547
|
+
"volume",
|
|
2548
|
+
];
|
|
2549
|
+
/** Dollars from the command line as integer microdollars: 25, 2.50 or 0.000001. */
|
|
2550
|
+
function usdMicros(args, name) {
|
|
2551
|
+
const value = flag(args, name);
|
|
2552
|
+
if (value === undefined)
|
|
2553
|
+
return undefined;
|
|
2554
|
+
if (!/^\d+(\.\d{1,6})?$/.test(value))
|
|
2555
|
+
throw usage(`--${name} takes dollars, such as 25 or 2.50.`);
|
|
2556
|
+
return Math.round(Number(value) * 1e6);
|
|
2557
|
+
}
|
|
2180
2558
|
/** A create body from the command line's flags. */
|
|
2181
2559
|
function createInput(args) {
|
|
2182
2560
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
@@ -2197,9 +2575,16 @@ function createInput(args) {
|
|
|
2197
2575
|
...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
|
|
2198
2576
|
...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
|
|
2199
2577
|
...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
|
|
2578
|
+
...(flag(args, "cpu") ? { cpu: oneOf(args, "cpu", ["shared", "reserved"]) } : {}),
|
|
2579
|
+
...(integer(args, "cpu-floor") ? { cpuFloorMillis: integer(args, "cpu-floor") } : {}),
|
|
2580
|
+
...(flag(args, "region") ? { region: flag(args, "region") } : {}),
|
|
2581
|
+
...(usdMicros(args, "max-cost") ? { maxCostMicros: usdMicros(args, "max-cost") } : {}),
|
|
2582
|
+
...(usdMicros(args, "max-total-cost")
|
|
2583
|
+
? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
|
|
2584
|
+
: {}),
|
|
2200
2585
|
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
2201
2586
|
...(flag(args, "on-timeout")
|
|
2202
|
-
? { onLeaseEnd:
|
|
2587
|
+
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
|
|
2203
2588
|
: {}),
|
|
2204
2589
|
...(integer(args, "idle-pause") !== undefined
|
|
2205
2590
|
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
@@ -2217,7 +2602,9 @@ function createInput(args) {
|
|
|
2217
2602
|
}
|
|
2218
2603
|
/** Runs a command in a sandbox, streaming its output, and answers its exit code
|
|
2219
2604
|
* (124 on a timeout), so a script's `set -e` behaves. */
|
|
2220
|
-
async function execute(sbx, command, args, out, timeout
|
|
2605
|
+
async function execute(sbx, command, args, out, timeout,
|
|
2606
|
+
/** `run` stops its whole sandbox on Ctrl-C, so it handles the signal itself. */
|
|
2607
|
+
ownInterrupt = true) {
|
|
2221
2608
|
const target = command.length === 1 ? command[0] : command;
|
|
2222
2609
|
const extras = await import("./cli-extras.js");
|
|
2223
2610
|
const stdin = await extras.pipedStdin();
|
|
@@ -2227,13 +2614,32 @@ async function execute(sbx, command, args, out, timeout) {
|
|
|
2227
2614
|
...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
|
|
2228
2615
|
...(timeout ? { timeoutMs: timeout * 1000 } : {}),
|
|
2229
2616
|
};
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2617
|
+
// Ctrl-C stops the command in the sandbox too, and says so; without this
|
|
2618
|
+
// the CLI exited and the command ran on unseen.
|
|
2619
|
+
const interrupt = new AbortController();
|
|
2620
|
+
const onInterrupt = () => interrupt.abort(new Error("Interrupted"));
|
|
2621
|
+
if (ownInterrupt)
|
|
2622
|
+
process.once("SIGINT", onInterrupt);
|
|
2623
|
+
let result;
|
|
2624
|
+
try {
|
|
2625
|
+
result = out.json
|
|
2626
|
+
? await sbx.exec(target, { ...options, signal: interrupt.signal })
|
|
2627
|
+
: await sbx.exec(target, {
|
|
2628
|
+
...options,
|
|
2629
|
+
signal: interrupt.signal,
|
|
2630
|
+
onStdout: (t) => process.stdout.write(t),
|
|
2631
|
+
onStderr: (t) => process.stderr.write(t),
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
catch (error) {
|
|
2635
|
+
if (!interrupt.signal.aborted)
|
|
2636
|
+
throw error;
|
|
2637
|
+
out.error(describeError(error, out.json));
|
|
2638
|
+
return 130;
|
|
2639
|
+
}
|
|
2640
|
+
finally {
|
|
2641
|
+
process.off("SIGINT", onInterrupt);
|
|
2642
|
+
}
|
|
2237
2643
|
if (out.json)
|
|
2238
2644
|
out.write(JSON.stringify(result));
|
|
2239
2645
|
else if (result.timedOut)
|
|
@@ -2255,13 +2661,12 @@ async function shell(sbx, command) {
|
|
|
2255
2661
|
});
|
|
2256
2662
|
if (stdin.isTTY)
|
|
2257
2663
|
stdin.setRawMode(true);
|
|
2664
|
+
const unhook = (await import("./cli-extras.js")).pipeShellInput(stdin, terminal);
|
|
2258
2665
|
stdin.resume();
|
|
2259
|
-
const onInput = (chunk) => terminal.write(new Uint8Array(chunk));
|
|
2260
2666
|
const onResize = () => terminal.resize(size().cols, size().rows);
|
|
2261
|
-
stdin.on("data", onInput);
|
|
2262
2667
|
process.stdout.on("resize", onResize);
|
|
2263
2668
|
const code = await terminal.exited;
|
|
2264
|
-
|
|
2669
|
+
unhook();
|
|
2265
2670
|
process.stdout.off("resize", onResize);
|
|
2266
2671
|
if (stdin.isTTY)
|
|
2267
2672
|
stdin.setRawMode(false);
|
|
@@ -2291,7 +2696,13 @@ export function describeError(error, json) {
|
|
|
2291
2696
|
details: error.details,
|
|
2292
2697
|
},
|
|
2293
2698
|
});
|
|
2294
|
-
return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId
|
|
2699
|
+
return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId
|
|
2700
|
+
? // A refusal of what was sent is the caller's to fix; only a failure
|
|
2701
|
+
// of ours is worth a report.
|
|
2702
|
+
error.status >= 400 && error.status < 500
|
|
2703
|
+
? `\nRequest: ${error.requestId}`
|
|
2704
|
+
: `\nRequest: ${error.requestId} (report it: runtime feedback "..." --request-id ${error.requestId})`
|
|
2705
|
+
: ""}`;
|
|
2295
2706
|
}
|
|
2296
2707
|
const message = error instanceof Error ? error.message : String(error);
|
|
2297
2708
|
return json ? JSON.stringify({ error: { code: "cli_error", message } }) : `Error: ${message}`;
|
|
@@ -2369,7 +2780,7 @@ async function webhooksCommand(argv, env, out) {
|
|
|
2369
2780
|
return 0;
|
|
2370
2781
|
}
|
|
2371
2782
|
const hooks = (await client(env)).webhooks;
|
|
2372
|
-
const args = parse(rest, ["enable", "disable"]);
|
|
2783
|
+
const args = parse(rest, ["enable", "disable"], ["events", "description", "url", "keep", "limit"]);
|
|
2373
2784
|
const id = () => need(args.positional[0], "a webhook id");
|
|
2374
2785
|
const events = () => flag(args, "events")
|
|
2375
2786
|
?.split(",")
|
|
@@ -2465,7 +2876,7 @@ async function otelCommand(argv, env, out) {
|
|
|
2465
2876
|
product = "otel";
|
|
2466
2877
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
2467
2878
|
const otel = (await client(env)).otel;
|
|
2468
|
-
const args = parse(rest, ["enable", "disable"]);
|
|
2879
|
+
const args = parse(rest, ["enable", "disable"], ["endpoint", "header", "signals"]);
|
|
2469
2880
|
const id = () => need(args.positional[0], "an export id");
|
|
2470
2881
|
const signals = () => flag(args, "signals")
|
|
2471
2882
|
?.split(",")
|