withruntime 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +535 -130
- 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/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 +2 -2
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,6 +596,7 @@ 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);
|
|
488
601
|
print(result.pending
|
|
489
602
|
? `Not connected yet. When it is approved, run any command, or \`${me} login --wait\`; it uses this link.`
|
|
@@ -493,9 +606,26 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
493
606
|
return 0;
|
|
494
607
|
}
|
|
495
608
|
if (command === "whoami") {
|
|
496
|
-
const
|
|
609
|
+
const rt = await client(env);
|
|
610
|
+
// The usage call only adds the funding line: a key that cannot read it
|
|
611
|
+
// still gets its answer.
|
|
612
|
+
const [who, spend] = await Promise.all([rt.me(), rt.usage().catch(() => undefined)]);
|
|
613
|
+
const trialLeft = spend?.trial ? spend.trial.availableMs / 3_600_000 : 0;
|
|
614
|
+
const credit = spend ? BigInt(spend.available) : 0n;
|
|
615
|
+
const funding = !spend
|
|
616
|
+
? undefined
|
|
617
|
+
: [
|
|
618
|
+
...(trialLeft > 0
|
|
619
|
+
? [
|
|
620
|
+
`free trial, ${trialLeft.toLocaleString("en-US", { maximumFractionDigits: 1 })} hours left`,
|
|
621
|
+
]
|
|
622
|
+
: []),
|
|
623
|
+
...(credit > 0n ? [`${dollars(credit)} of credit`] : []),
|
|
624
|
+
].join("; ") || "no trial time or credit left: runtime billing";
|
|
497
625
|
print(table([
|
|
498
|
-
["organization", who.orgId],
|
|
626
|
+
["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
|
|
627
|
+
...(who.role ? [["role", who.role]] : []),
|
|
628
|
+
...(funding ? [["funding", funding]] : []),
|
|
499
629
|
["agent", who.principalId],
|
|
500
630
|
["key", who.credentialId ?? "-"],
|
|
501
631
|
["api", who.apiVersion],
|
|
@@ -503,7 +633,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
503
633
|
return 0;
|
|
504
634
|
}
|
|
505
635
|
if (command === "usage") {
|
|
636
|
+
const csv = rest.includes("--csv");
|
|
637
|
+
const unknown = rest.find((arg) => arg !== "--csv");
|
|
638
|
+
if (unknown)
|
|
639
|
+
throw usage(`Unknown option ${unknown}: ${me} usage [--csv | --json]`);
|
|
506
640
|
const value = await (await client(env)).usage();
|
|
641
|
+
if (csv) {
|
|
642
|
+
out.write(usageCsv(value));
|
|
643
|
+
if ((value.resources ?? []).length >= 100)
|
|
644
|
+
out.error("These are the newest 100 resources; older ones are not in this export.");
|
|
645
|
+
return 0;
|
|
646
|
+
}
|
|
507
647
|
print(usageSummary(value), value);
|
|
508
648
|
return 0;
|
|
509
649
|
}
|
|
@@ -535,7 +675,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
535
675
|
if (command === "otel")
|
|
536
676
|
return otelCommand(rest, env, out);
|
|
537
677
|
if (command === "events") {
|
|
538
|
-
const args = parse(rest);
|
|
678
|
+
const args = parse(rest, [], ["sandbox", "type", "limit"]);
|
|
539
679
|
const rt = await client(env);
|
|
540
680
|
const sandbox = flag(args, "sandbox");
|
|
541
681
|
const page = await rt.events.list({
|
|
@@ -579,7 +719,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
579
719
|
if (command === "account" || command === "accounts")
|
|
580
720
|
return accountCommand(rest, env, out);
|
|
581
721
|
if (command === "audit") {
|
|
582
|
-
const args = parse(rest);
|
|
722
|
+
const args = parse(rest, [], ["action", "limit", "before"]);
|
|
583
723
|
const known = new Set(["action", "limit", "before"]);
|
|
584
724
|
for (const name of args.flags.keys())
|
|
585
725
|
if (!known.has(name))
|
|
@@ -607,6 +747,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
607
747
|
: "Nothing recorded yet.", page);
|
|
608
748
|
return 0;
|
|
609
749
|
}
|
|
750
|
+
if (command === "billing") {
|
|
751
|
+
product = "billing";
|
|
752
|
+
const origins = connectionOrigins(env);
|
|
753
|
+
const { claimWalletAccount, openWalletAccount } = await import("./wallet.js");
|
|
754
|
+
return billingCommand(rest, parse(rest, ["wait", "new-account", "accept-terms"], []), out, () => client(env), undefined, {
|
|
755
|
+
open: (input) => openWalletAccount(input, { authUrl: origins.auth }),
|
|
756
|
+
claim: (code) => claimWalletAccount(code, { authUrl: origins.auth }),
|
|
757
|
+
save: (key, orgId) => connectionStore(env).save({
|
|
758
|
+
version: 1,
|
|
759
|
+
apiOrigin: origins.api,
|
|
760
|
+
authOrigin: origins.auth,
|
|
761
|
+
key,
|
|
762
|
+
connectionId: "pasted",
|
|
763
|
+
orgId,
|
|
764
|
+
agentName: "Wallet agent",
|
|
765
|
+
}),
|
|
766
|
+
});
|
|
767
|
+
}
|
|
610
768
|
if (command === "referrals") {
|
|
611
769
|
const r = await (await client(env)).referrals.get();
|
|
612
770
|
const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
@@ -648,7 +806,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
648
806
|
return 0;
|
|
649
807
|
}
|
|
650
808
|
if (command === "feedback") {
|
|
651
|
-
const args = parse(rest, ["list"]);
|
|
809
|
+
const args = parse(rest, ["list"], ["kind", "detail", "competitor", "request-id"]);
|
|
652
810
|
if (has(args, "list")) {
|
|
653
811
|
const { data } = await (await client(env)).feedback.list();
|
|
654
812
|
print(data.length
|
|
@@ -709,11 +867,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
709
867
|
}
|
|
710
868
|
if (command === "ssh") {
|
|
711
869
|
product = "ssh";
|
|
712
|
-
return sshCommand(rest, env, out, () => client(env), me);
|
|
870
|
+
return (await import("./ssh.js")).sshCommand(rest, env, out, () => client(env), me);
|
|
713
871
|
}
|
|
714
872
|
if (command === "port-forward" || command === "forward") {
|
|
715
873
|
product = "ssh";
|
|
716
|
-
return portForwardCommand(rest, out, () => client(env), me);
|
|
874
|
+
return (await import("./ssh.js")).portForwardCommand(rest, out, () => client(env), me);
|
|
717
875
|
}
|
|
718
876
|
if (command === "sandbox" || command === "sandboxes" || command === "sbx") {
|
|
719
877
|
product = "sandbox";
|
|
@@ -735,13 +893,62 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
735
893
|
command;
|
|
736
894
|
if (NETWORK_PRODUCTS.includes(networkProduct)) {
|
|
737
895
|
product = networkProduct;
|
|
738
|
-
return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"]), out, () => client(env));
|
|
896
|
+
return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"], ["sandbox", "subnet", "public-key", "route", "out", "host", "secret"]), out, () => client(env));
|
|
739
897
|
}
|
|
740
898
|
throw unknown("command", command, COMMANDS);
|
|
741
899
|
}
|
|
742
900
|
/** `runtime usage` for a person: the balance, the trial and what each kind
|
|
743
901
|
* of resource was charged, in dollars. --json keeps every figure, and each
|
|
744
902
|
* resource with its rates and CPU time. */
|
|
903
|
+
/** One row per resource, for a spreadsheet: when it ran, how big it was, how
|
|
904
|
+
* long it ran, the CPU it used and what it cost. Money is exact dollars to the
|
|
905
|
+
* microdollar (the API's integer microdollars moved six places), so a column
|
|
906
|
+
* sums to the ledger to the last digit. */
|
|
907
|
+
export function usageCsv(u) {
|
|
908
|
+
const cell = (value) => {
|
|
909
|
+
const text = typeof value === "string" || typeof value === "number" || typeof value === "bigint"
|
|
910
|
+
? String(value)
|
|
911
|
+
: "";
|
|
912
|
+
return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
913
|
+
};
|
|
914
|
+
const exact = (value) => {
|
|
915
|
+
const micros = BigInt(typeof value === "number" || typeof value === "string" ? value : 0);
|
|
916
|
+
const sign = micros < 0n ? "-" : "";
|
|
917
|
+
const size = micros < 0n ? -micros : micros;
|
|
918
|
+
return `${sign}${size / 1000000n}.${(size % 1000000n).toString().padStart(6, "0")}`;
|
|
919
|
+
};
|
|
920
|
+
const head = [
|
|
921
|
+
"resource_id",
|
|
922
|
+
"name",
|
|
923
|
+
"kind",
|
|
924
|
+
"state",
|
|
925
|
+
"created_at",
|
|
926
|
+
"vcpu",
|
|
927
|
+
"memory_mib",
|
|
928
|
+
"running_seconds",
|
|
929
|
+
"active_cpu_seconds",
|
|
930
|
+
"memory_gib_seconds",
|
|
931
|
+
"charged_usd",
|
|
932
|
+
"held_usd",
|
|
933
|
+
];
|
|
934
|
+
const rows = (u.resources ?? []).map((r) => [
|
|
935
|
+
r.resourceId,
|
|
936
|
+
r.name,
|
|
937
|
+
r.kind,
|
|
938
|
+
r.state ?? r.status,
|
|
939
|
+
r.createdAt,
|
|
940
|
+
r.vcpu,
|
|
941
|
+
r.memoryMiB,
|
|
942
|
+
r.runningSeconds,
|
|
943
|
+
r.activeCpuSeconds,
|
|
944
|
+
r.memoryGiBSeconds,
|
|
945
|
+
exact(r.chargedMicros),
|
|
946
|
+
exact(r.heldMicros),
|
|
947
|
+
]
|
|
948
|
+
.map(cell)
|
|
949
|
+
.join(","));
|
|
950
|
+
return [head.join(","), ...rows].join("\n");
|
|
951
|
+
}
|
|
745
952
|
function usageSummary(u) {
|
|
746
953
|
const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
|
|
747
954
|
? BigInt(value)
|
|
@@ -751,11 +958,14 @@ function usageSummary(u) {
|
|
|
751
958
|
const rows = [
|
|
752
959
|
["available", dollars(micros(u.available))],
|
|
753
960
|
["credited", dollars(micros(u.credited))],
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
],
|
|
758
|
-
|
|
961
|
+
// spent includes what refunds and disputes took back: say that part on
|
|
962
|
+
// its own line, so "spent" reads as what was used.
|
|
963
|
+
["used", dollars(micros(u.spent) - takenBack)],
|
|
964
|
+
...(takenBack > 0n ? [["returned by refunds and disputes", dollars(takenBack)]] : []),
|
|
965
|
+
// Holds cover running sandboxes and the hour ahead of stored snapshots,
|
|
966
|
+
// images and volumes (pricing guide), so an account with nothing running
|
|
967
|
+
// can still show one.
|
|
968
|
+
["held for running sandboxes and this hour's storage", dollars(micros(u.held))],
|
|
759
969
|
];
|
|
760
970
|
if (micros(u.expired) > 0n)
|
|
761
971
|
rows.push(["expired or taken back", dollars(micros(u.expired))]);
|
|
@@ -790,13 +1000,15 @@ function usageSummary(u) {
|
|
|
790
1000
|
])
|
|
791
1001
|
: "No resources yet.",
|
|
792
1002
|
"",
|
|
793
|
-
`Each resource, with its rates and CPU time: ${me} usage --json`,
|
|
1003
|
+
`Each resource, with its rates and CPU time: ${me} usage --json, or --csv for a spreadsheet`,
|
|
794
1004
|
].join("\n");
|
|
795
1005
|
}
|
|
796
1006
|
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
1007
|
/** A sandbox's id from its id or its name, for the filters that take an id. */
|
|
798
1008
|
async function sandboxIdOf(runtime, target) {
|
|
799
|
-
return SANDBOX_UUID.test(target)
|
|
1009
|
+
return SANDBOX_UUID.test(target)
|
|
1010
|
+
? target
|
|
1011
|
+
: (await (await import("./ssh.js")).resolveSandbox(runtime, target)).id;
|
|
800
1012
|
}
|
|
801
1013
|
/** What a process has printed so far, in order, without waiting for more. */
|
|
802
1014
|
async function outputSoFar(runtime, sandboxId, processId) {
|
|
@@ -902,7 +1114,7 @@ function trialNote(trial, all) {
|
|
|
902
1114
|
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
1115
|
}
|
|
904
1116
|
async function switching(command, argv, env, out) {
|
|
905
|
-
const args = parse(argv);
|
|
1117
|
+
const args = parse(argv, [], command === "compare" ? ["from", "days"] : ["from"]);
|
|
906
1118
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
907
1119
|
const known = new Set(command === "compare" ? ["from", "days"] : ["from"]);
|
|
908
1120
|
for (const name of args.flags.keys())
|
|
@@ -929,7 +1141,7 @@ async function switching(command, argv, env, out) {
|
|
|
929
1141
|
return 0;
|
|
930
1142
|
}
|
|
931
1143
|
if (!from)
|
|
932
|
-
throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines...).`);
|
|
1144
|
+
throw usage(`Name the rival: ${me} compare --from e2b (or daytona, vercel, modal, cloudflare, fly, fly-machines, lambda-microvms, freestyle, prime...).`);
|
|
933
1145
|
/* The server holds the list of rivals it has rates for, and names them all
|
|
934
1146
|
when it refuses one, so a rival added there needs no new CLI. */
|
|
935
1147
|
const days = integer(args, "days");
|
|
@@ -944,7 +1156,7 @@ async function switching(command, argv, env, out) {
|
|
|
944
1156
|
const monthly = month === undefined
|
|
945
1157
|
? ""
|
|
946
1158
|
: month >= 0n
|
|
947
|
-
? `about ${dollars(month)} a month`
|
|
1159
|
+
? `a saving of about ${dollars(month)} a month`
|
|
948
1160
|
: `about ${dollars(-month)} a month more on Runtime`;
|
|
949
1161
|
/* The month is projected from the days the usage covers, not from the
|
|
950
1162
|
window asked for: say which, or a young account's 7 and 90 days read the
|
|
@@ -953,7 +1165,7 @@ async function switching(command, argv, env, out) {
|
|
|
953
1165
|
const pace = month === undefined
|
|
954
1166
|
? "."
|
|
955
1167
|
: covered < c.window.days
|
|
956
|
-
? `. Your
|
|
1168
|
+
? `. Your usage so far covers ${covered <= 1 ? "less than a day" : `${covered.toLocaleString("en-US")} days`}; at that pace, ${monthly}.`
|
|
957
1169
|
: c.window.days === 30
|
|
958
1170
|
? `, ${monthly}.`
|
|
959
1171
|
: `. At that pace, ${monthly}.`;
|
|
@@ -974,7 +1186,7 @@ async function switching(command, argv, env, out) {
|
|
|
974
1186
|
return 0;
|
|
975
1187
|
}
|
|
976
1188
|
async function support(argv, env, out) {
|
|
977
|
-
const args = parse(argv);
|
|
1189
|
+
const args = parse(argv, [], ["conversation"]);
|
|
978
1190
|
const api = (await client(env)).support;
|
|
979
1191
|
const conversationId = flag(args, "conversation");
|
|
980
1192
|
const [first, second, third] = args.positional;
|
|
@@ -1012,9 +1224,10 @@ async function sandbox(argv, env, out) {
|
|
|
1012
1224
|
}
|
|
1013
1225
|
// Inside a sandbox, with no API key: the sandbox's own request token asks.
|
|
1014
1226
|
if (verb === "identity-token") {
|
|
1015
|
-
const args = parse(rest);
|
|
1227
|
+
const args = parse(rest, [], ["audience", "lifetime"]);
|
|
1016
1228
|
const audience = need(flag(args, "audience"), "--audience, such as sts.amazonaws.com");
|
|
1017
1229
|
const lifetime = flag(args, "lifetime");
|
|
1230
|
+
const { identityToken } = await import("./identity.js");
|
|
1018
1231
|
const token = await identityToken({
|
|
1019
1232
|
audience,
|
|
1020
1233
|
...(lifetime === undefined ? {} : { lifetimeSeconds: Number(lifetime) }),
|
|
@@ -1025,17 +1238,26 @@ async function sandbox(argv, env, out) {
|
|
|
1025
1238
|
const runtime = await client(env);
|
|
1026
1239
|
/* By id or by name, for every command here, as ssh and port-forward
|
|
1027
1240
|
always took one. */
|
|
1028
|
-
const get = (id) => resolveSandbox(runtime, need(id, "a sandbox id or name"));
|
|
1241
|
+
const get = async (id) => (await import("./ssh.js")).resolveSandbox(runtime, need(id, "a sandbox id or name"));
|
|
1242
|
+
/* For a command that needs only the id: an id is used as it is, with no
|
|
1243
|
+
lookup first, so `exec` is one request and not two (about 110 ms less
|
|
1244
|
+
from Colorado to Virginia). A sandbox that is gone or stopped is refused
|
|
1245
|
+
by the command itself, with the same words. */
|
|
1246
|
+
const use = async (id) => SANDBOX_UUID.test(id ?? "")
|
|
1247
|
+
? new SandboxHandle(runtime.transport, { id })
|
|
1248
|
+
: get(id);
|
|
1029
1249
|
switch (verb) {
|
|
1030
1250
|
case "create": {
|
|
1031
|
-
const args = parse(rest, CREATE_FLAGS);
|
|
1032
|
-
const sbx = await runtime.sandboxes.create(createInput(args)
|
|
1251
|
+
const args = parse(rest, CREATE_FLAGS, CREATE_VALUES);
|
|
1252
|
+
const sbx = await runtime.sandboxes.create(createInput(args), {
|
|
1253
|
+
onCapacityWait: waiting(out),
|
|
1254
|
+
});
|
|
1033
1255
|
print(sbx.id, sbx.info);
|
|
1034
1256
|
return 0;
|
|
1035
1257
|
}
|
|
1036
1258
|
case "ls":
|
|
1037
1259
|
case "list": {
|
|
1038
|
-
const args = parse(rest, ["all"]);
|
|
1260
|
+
const args = parse(rest, ["all"], ["label", "state"]);
|
|
1039
1261
|
const page = await runtime.sandboxes.list({
|
|
1040
1262
|
...(has(args, "all") ? { includeStopped: true } : {}),
|
|
1041
1263
|
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
@@ -1087,8 +1309,8 @@ async function sandbox(argv, env, out) {
|
|
|
1087
1309
|
return 0;
|
|
1088
1310
|
}
|
|
1089
1311
|
case "mount": {
|
|
1090
|
-
const args = parse(rest, ["read-only"]);
|
|
1091
|
-
const sbx = await
|
|
1312
|
+
const args = parse(rest, ["read-only"], ["secret", "region", "endpoint", "account-id"]);
|
|
1313
|
+
const sbx = await use(args.positional[0]);
|
|
1092
1314
|
const source = need(args.positional[1], "a bucket, as s3://bucket, r2://bucket or gcs://bucket");
|
|
1093
1315
|
const found = /^(s3|r2|gcs):\/\/([^/]+)(?:\/(.+))?$/.exec(source);
|
|
1094
1316
|
if (!found)
|
|
@@ -1108,7 +1330,7 @@ async function sandbox(argv, env, out) {
|
|
|
1108
1330
|
return 0;
|
|
1109
1331
|
}
|
|
1110
1332
|
case "mounts": {
|
|
1111
|
-
const sbx = await
|
|
1333
|
+
const sbx = await use(rest[0]);
|
|
1112
1334
|
const all = await sbx.mounts.list();
|
|
1113
1335
|
print(all.length
|
|
1114
1336
|
? table([
|
|
@@ -1125,13 +1347,13 @@ async function sandbox(argv, env, out) {
|
|
|
1125
1347
|
return 0;
|
|
1126
1348
|
}
|
|
1127
1349
|
case "unmount": {
|
|
1128
|
-
const sbx = await
|
|
1350
|
+
const sbx = await use(rest[0]);
|
|
1129
1351
|
const gone = await sbx.mounts.remove(need(rest[1], "the mounted path"));
|
|
1130
1352
|
print(`${gone.path} unmounted`, gone);
|
|
1131
1353
|
return 0;
|
|
1132
1354
|
}
|
|
1133
1355
|
case "snapshot": {
|
|
1134
|
-
const args = parse(rest);
|
|
1356
|
+
const args = parse(rest, [], ["name", "retention"]);
|
|
1135
1357
|
const sbx = await get(args.positional[0]);
|
|
1136
1358
|
const snapshot = await sbx.snapshot({
|
|
1137
1359
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
@@ -1141,7 +1363,7 @@ async function sandbox(argv, env, out) {
|
|
|
1141
1363
|
return 0;
|
|
1142
1364
|
}
|
|
1143
1365
|
case "fork": {
|
|
1144
|
-
const args = parse(rest, ["keep-snapshot", "trial", "paid"]);
|
|
1366
|
+
const args = parse(rest, ["keep-snapshot", "trial", "paid"], ["count", "name"]);
|
|
1145
1367
|
const sbx = await get(args.positional[0]);
|
|
1146
1368
|
const forks = await sbx.fork({
|
|
1147
1369
|
count: integer(args, "count") ?? 1,
|
|
@@ -1157,7 +1379,7 @@ async function sandbox(argv, env, out) {
|
|
|
1157
1379
|
return 0;
|
|
1158
1380
|
}
|
|
1159
1381
|
case "update": {
|
|
1160
|
-
const args = parse(rest);
|
|
1382
|
+
const args = parse(rest, [], ["name", "label", "idle-pause", "auto-wake", "persistent", "max-total-cost"]);
|
|
1161
1383
|
const sbx = await get(args.positional[0]);
|
|
1162
1384
|
const onOff = (name) => {
|
|
1163
1385
|
const value = flag(args, name);
|
|
@@ -1198,19 +1420,19 @@ async function sandbox(argv, env, out) {
|
|
|
1198
1420
|
return 0;
|
|
1199
1421
|
}
|
|
1200
1422
|
case "exec": {
|
|
1201
|
-
const args = parse(rest);
|
|
1423
|
+
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandbox exec <id> -- ls -la`);
|
|
1202
1424
|
const command = args.rest ?? args.positional.slice(1);
|
|
1203
1425
|
if (!command.length)
|
|
1204
1426
|
throw usage("Give the command after --: runtime sandbox exec <id> -- ls -la");
|
|
1205
|
-
const sbx = await
|
|
1427
|
+
const sbx = await use(args.positional[0]);
|
|
1206
1428
|
return execute(sbx, command, args, out, integer(args, "timeout"));
|
|
1207
1429
|
}
|
|
1208
1430
|
case "spawn": {
|
|
1209
|
-
const args = parse(rest, ["pty"]);
|
|
1431
|
+
const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandbox spawn <id> -- npm run dev`);
|
|
1210
1432
|
const command = args.rest ?? [];
|
|
1211
1433
|
if (!command.length)
|
|
1212
1434
|
throw usage("Give the command after --: runtime sandbox spawn <id> -- npm run dev");
|
|
1213
|
-
const sbx = await
|
|
1435
|
+
const sbx = await use(args.positional[0]);
|
|
1214
1436
|
const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
|
|
1215
1437
|
...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
|
|
1216
1438
|
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
@@ -1220,7 +1442,7 @@ async function sandbox(argv, env, out) {
|
|
|
1220
1442
|
return 0;
|
|
1221
1443
|
}
|
|
1222
1444
|
case "ps": {
|
|
1223
|
-
const sbx = await
|
|
1445
|
+
const sbx = await use(rest[0]);
|
|
1224
1446
|
const list = await sbx.processes.list();
|
|
1225
1447
|
print(list.length
|
|
1226
1448
|
? table([
|
|
@@ -1231,8 +1453,8 @@ async function sandbox(argv, env, out) {
|
|
|
1231
1453
|
return 0;
|
|
1232
1454
|
}
|
|
1233
1455
|
case "logs": {
|
|
1234
|
-
const args = parse(rest, ["f", "follow"]);
|
|
1235
|
-
const sbx = await
|
|
1456
|
+
const args = parse(rest, ["f", "follow"], []);
|
|
1457
|
+
const sbx = await use(args.positional[0]);
|
|
1236
1458
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
1237
1459
|
if (has(args, "f") || has(args, "follow")) {
|
|
1238
1460
|
for await (const event of proc.output())
|
|
@@ -1258,26 +1480,28 @@ async function sandbox(argv, env, out) {
|
|
|
1258
1480
|
return 0;
|
|
1259
1481
|
}
|
|
1260
1482
|
case "kill": {
|
|
1261
|
-
const args = parse(rest);
|
|
1262
|
-
const sbx = await
|
|
1483
|
+
const args = parse(rest, [], ["signal"]);
|
|
1484
|
+
const sbx = await use(args.positional[0]);
|
|
1263
1485
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
1264
1486
|
await proc.kill((flag(args, "signal") ?? "SIGTERM"));
|
|
1265
1487
|
print(`Signalled ${proc.id}.`, { processId: proc.id });
|
|
1266
1488
|
return 0;
|
|
1267
1489
|
}
|
|
1268
1490
|
case "shell": {
|
|
1269
|
-
const args = parse(rest);
|
|
1270
|
-
const sbx = await
|
|
1491
|
+
const args = parse(rest, [], ["command"]);
|
|
1492
|
+
const sbx = await use(args.positional[0]);
|
|
1271
1493
|
return shell(sbx, flag(args, "command"));
|
|
1272
1494
|
}
|
|
1273
1495
|
case "cat": {
|
|
1274
|
-
const sbx = await
|
|
1275
|
-
|
|
1496
|
+
const sbx = await use(rest[0]);
|
|
1497
|
+
// Streamed, any size; a stream that ends short throws, so exit is 1.
|
|
1498
|
+
const { Writable } = await import("node:stream");
|
|
1499
|
+
await (await sbx.files.readStream(need(rest[1], "a path"))).pipeTo(Writable.toWeb(process.stdout), { preventClose: true, preventAbort: true });
|
|
1276
1500
|
return 0;
|
|
1277
1501
|
}
|
|
1278
1502
|
case "files": {
|
|
1279
|
-
const args = parse(rest);
|
|
1280
|
-
const sbx = await
|
|
1503
|
+
const args = parse(rest, [], ["depth", "glob"]);
|
|
1504
|
+
const sbx = await use(args.positional[0]);
|
|
1281
1505
|
const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
|
|
1282
1506
|
...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
|
|
1283
1507
|
...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
|
|
@@ -1288,36 +1512,51 @@ async function sandbox(argv, env, out) {
|
|
|
1288
1512
|
return 0;
|
|
1289
1513
|
}
|
|
1290
1514
|
case "cp": {
|
|
1291
|
-
const [from, to] = parse(rest).positional;
|
|
1515
|
+
const [from, to] = parse(rest, ["r", "recursive"], []).positional;
|
|
1292
1516
|
const source = remote(need(from, "a source"));
|
|
1293
1517
|
const target = remote(need(to, "a destination"));
|
|
1294
|
-
|
|
1295
|
-
|
|
1518
|
+
// A destination ending in / (or a local directory) takes the source's
|
|
1519
|
+
// own name inside it, as cp does.
|
|
1520
|
+
const inside = (dir, path) => `${dir.replace(/\/+$/, "")}/${path.replace(/\/+$/, "").split("/").pop()}`;
|
|
1521
|
+
if (source && !target) {
|
|
1522
|
+
const local = await import("node:fs/promises").then((fs) => fs.stat(to).catch(() => null));
|
|
1523
|
+
await (await use(source.id)).files.download(source.path, to.endsWith("/") || local?.isDirectory() ? inside(to, source.path) : to);
|
|
1524
|
+
}
|
|
1296
1525
|
else if (!source && target)
|
|
1297
|
-
await (await
|
|
1526
|
+
await (await use(target.id)).files.upload(from, target.path.endsWith("/") ? inside(target.path, from) : target.path);
|
|
1298
1527
|
else
|
|
1299
1528
|
throw usage("Name exactly one side as <sandbox-id>:/path.");
|
|
1300
1529
|
print(`Copied ${from} to ${to}.`, { from, to });
|
|
1301
1530
|
return 0;
|
|
1302
1531
|
}
|
|
1303
1532
|
case "run-code":
|
|
1304
|
-
return runCode(await
|
|
1533
|
+
return runCode(await use(rest[0]), rest.slice(1), out);
|
|
1305
1534
|
case "preview": {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1535
|
+
if (rest[0] === "rotate") {
|
|
1536
|
+
const port = portOf(rest[2], "runtime sandbox preview rotate <id> 3000");
|
|
1537
|
+
const sbx = await use(rest[1]);
|
|
1538
|
+
const preview = await sbx.previews.rotate(port);
|
|
1539
|
+
print(preview.visibility === "public"
|
|
1540
|
+
? `Port ${port} is public: it takes no token, so there was none to refuse.`
|
|
1541
|
+
: `${preview.urlWithToken ?? preview.url}\nEvery earlier token for port ${port} is refused. New header: x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`, preview);
|
|
1542
|
+
return 0;
|
|
1543
|
+
}
|
|
1544
|
+
const args = parse(rest, ["public"], ["ttl"]);
|
|
1545
|
+
const port = portOf(args.positional[1], "runtime sandbox preview <id> 3000");
|
|
1546
|
+
const sbx = await use(args.positional[0]);
|
|
1309
1547
|
const preview = await sbx.previews.create(port, {
|
|
1310
1548
|
...(has(args, "public") ? { visibility: "public" } : {}),
|
|
1311
1549
|
...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
|
|
1312
1550
|
});
|
|
1313
|
-
|
|
1551
|
+
// The link and how to use it, nothing more: the API's `hint` (which
|
|
1552
|
+
// carries a referral line) stays in --json, not in the command's words.
|
|
1553
|
+
print(preview.visibility === "public"
|
|
1314
1554
|
? 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);
|
|
1555
|
+
: `${preview.urlWithToken ?? preview.url}\nPrivate: send the header x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`, preview);
|
|
1317
1556
|
return 0;
|
|
1318
1557
|
}
|
|
1319
1558
|
case "previews": {
|
|
1320
|
-
const list = await (await
|
|
1559
|
+
const list = await (await use(rest[0])).previews.list();
|
|
1321
1560
|
print(list.length
|
|
1322
1561
|
? table([
|
|
1323
1562
|
["PORT", "VISIBILITY", "URL"],
|
|
@@ -1327,14 +1566,14 @@ async function sandbox(argv, env, out) {
|
|
|
1327
1566
|
return 0;
|
|
1328
1567
|
}
|
|
1329
1568
|
case "unshare": {
|
|
1330
|
-
const
|
|
1331
|
-
const
|
|
1569
|
+
const port = portOf(rest[1], "runtime sandbox unshare <id> 3000");
|
|
1570
|
+
const sbx = await use(rest[0]);
|
|
1332
1571
|
print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
|
|
1333
1572
|
return 0;
|
|
1334
1573
|
}
|
|
1335
1574
|
case "network": {
|
|
1336
|
-
const args = parse(rest, ["no-internet", "internet"]);
|
|
1337
|
-
const sbx = await
|
|
1575
|
+
const args = parse(rest, ["no-internet", "internet"], ["allow", "deny", "connect"]);
|
|
1576
|
+
const sbx = await use(args.positional[0]);
|
|
1338
1577
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
1339
1578
|
const policy = has(args, "no-internet") || has(args, "internet") || rules.length
|
|
1340
1579
|
? await sbx.network.set({
|
|
@@ -1344,7 +1583,10 @@ async function sandbox(argv, env, out) {
|
|
|
1344
1583
|
: await sbx.network.get();
|
|
1345
1584
|
print(table([
|
|
1346
1585
|
["internet", policy.internet ? "on" : "off"],
|
|
1347
|
-
[
|
|
1586
|
+
[
|
|
1587
|
+
"allow",
|
|
1588
|
+
policy.allow.join(", ") || (policy.internet ? "(the public web)" : "(nothing)"),
|
|
1589
|
+
],
|
|
1348
1590
|
["deny", policy.deny.join(", ") || "-"],
|
|
1349
1591
|
["connect", policy.connect.join(", ") || "-"],
|
|
1350
1592
|
["enforced", policy.enforced ? "yes" : "not yet; run the same command again"],
|
|
@@ -1352,9 +1594,9 @@ async function sandbox(argv, env, out) {
|
|
|
1352
1594
|
return 0;
|
|
1353
1595
|
}
|
|
1354
1596
|
case "desktop":
|
|
1355
|
-
return desktop(await
|
|
1597
|
+
return desktop(await use(rest[0]), rest.slice(1), out);
|
|
1356
1598
|
case "watch":
|
|
1357
|
-
return (await import("./cli-extras.js")).watchCommand(await
|
|
1599
|
+
return (await import("./cli-extras.js")).watchCommand(await use(rest[0]), rest.slice(1), out, kit);
|
|
1358
1600
|
case "mcp":
|
|
1359
1601
|
if (rest[0] === "catalog" || rest[0] === undefined) {
|
|
1360
1602
|
const catalog = await runtime.mcp.catalog();
|
|
@@ -1373,10 +1615,10 @@ async function sandbox(argv, env, out) {
|
|
|
1373
1615
|
]), catalog);
|
|
1374
1616
|
return 0;
|
|
1375
1617
|
}
|
|
1376
|
-
return (await import("./cli-extras.js")).mcpCommand(await
|
|
1618
|
+
return (await import("./cli-extras.js")).mcpCommand(await use(rest[0]), rest.slice(1), out, kit);
|
|
1377
1619
|
case "metrics": {
|
|
1378
|
-
const args = parse(rest);
|
|
1379
|
-
const sbx = await
|
|
1620
|
+
const args = parse(rest, [], ["range"]);
|
|
1621
|
+
const sbx = await use(args.positional[0]);
|
|
1380
1622
|
const range = (flag(args, "range") ?? "1h");
|
|
1381
1623
|
if (!["15m", "1h", "6h", "24h", "7d", "30d"].includes(range))
|
|
1382
1624
|
throw usage("--range is one of 15m, 1h, 6h, 24h, 7d, 30d.");
|
|
@@ -1438,20 +1680,49 @@ async function readSecret() {
|
|
|
1438
1680
|
stdin.pause();
|
|
1439
1681
|
}
|
|
1440
1682
|
}
|
|
1683
|
+
/** `--allow` values as secret rules: each is `[METHOD[,METHOD]...] PATH`, and
|
|
1684
|
+
* rules naming the same methods share one rule's paths. Only the shape is read
|
|
1685
|
+
* here; the API holds every rule to the same check the host makes
|
|
1686
|
+
* (ARCHITECTURE.md section 10, "Per-path rules on an egress secret") and says
|
|
1687
|
+
* which one it refused. */
|
|
1688
|
+
export function parseSecretRules(values) {
|
|
1689
|
+
const byMethods = new Map();
|
|
1690
|
+
for (const value of values) {
|
|
1691
|
+
const words = value.trim().split(/\s+/);
|
|
1692
|
+
const path = words.at(-1);
|
|
1693
|
+
if (!path || words.length > 2 || !path.startsWith("/"))
|
|
1694
|
+
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)}.`);
|
|
1695
|
+
const methods = words.length === 2
|
|
1696
|
+
? [...new Set(words[0].toUpperCase().split(",").filter(Boolean))].sort()
|
|
1697
|
+
: undefined;
|
|
1698
|
+
const key = methods?.join(",") ?? "";
|
|
1699
|
+
const rule = byMethods.get(key) ?? { ...(methods ? { methods } : {}), paths: [] };
|
|
1700
|
+
if (!rule.paths.includes(path))
|
|
1701
|
+
rule.paths.push(path);
|
|
1702
|
+
byMethods.set(key, rule);
|
|
1703
|
+
}
|
|
1704
|
+
return [...byMethods.values()];
|
|
1705
|
+
}
|
|
1706
|
+
const ruleText = (rules) => rules?.length
|
|
1707
|
+
? rules
|
|
1708
|
+
.map((rule) => `${rule.methods ? `${rule.methods.join(",")} ` : ""}${rule.paths.join(" ")}`)
|
|
1709
|
+
.join("; ")
|
|
1710
|
+
: "-";
|
|
1441
1711
|
export async function secretsCommand(argv, env, out,
|
|
1442
1712
|
/** Tests give the value here; the CLI reads standard input. */
|
|
1443
1713
|
read = readSecret) {
|
|
1444
1714
|
const [verb, ...rest] = argv;
|
|
1445
|
-
const args = parse(rest);
|
|
1715
|
+
const args = parse(rest, [], ["host", "header", "format", "allow", "value"]);
|
|
1446
1716
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1447
1717
|
const rows = (list) => list.length
|
|
1448
1718
|
? table([
|
|
1449
|
-
["name", "hosts", "header", "placeholder"],
|
|
1719
|
+
["name", "hosts", "header", "placeholder", "allow"],
|
|
1450
1720
|
...list.map((secret) => [
|
|
1451
1721
|
secret.name,
|
|
1452
1722
|
secret.hosts.join(","),
|
|
1453
1723
|
secret.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
|
|
1454
1724
|
secret.placeholder,
|
|
1725
|
+
ruleText(secret.rules),
|
|
1455
1726
|
]),
|
|
1456
1727
|
])
|
|
1457
1728
|
: "No secrets.";
|
|
@@ -1472,17 +1743,21 @@ read = readSecret) {
|
|
|
1472
1743
|
throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
|
|
1473
1744
|
const header = flag(args, "header");
|
|
1474
1745
|
const format = flag(args, "format");
|
|
1746
|
+
const allow = args.flags.get("allow");
|
|
1747
|
+
const rules = allow ? parseSecretRules(allow) : undefined;
|
|
1475
1748
|
const saved = await (await client(env)).secrets.set(name, {
|
|
1476
1749
|
value,
|
|
1477
1750
|
hosts,
|
|
1478
1751
|
...(header ? { header } : {}),
|
|
1479
1752
|
...(format ? { format } : {}),
|
|
1753
|
+
...(rules ? { rules } : {}),
|
|
1480
1754
|
});
|
|
1481
1755
|
print([
|
|
1482
1756
|
`Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
|
|
1483
1757
|
saved.header
|
|
1484
1758
|
? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
|
|
1485
1759
|
: `Over HTTPS to ${saved.hosts.join(", ")}, the placeholder becomes the value in the URL and headers.`,
|
|
1760
|
+
...(saved.rules?.length ? [`Only these requests get it: ${ruleText(saved.rules)}.`] : []),
|
|
1486
1761
|
...(saved.enforced
|
|
1487
1762
|
? []
|
|
1488
1763
|
: ["A host is catching up; running sandboxes have it within a minute."]),
|
|
@@ -1506,7 +1781,7 @@ async function readInput(source) {
|
|
|
1506
1781
|
return Buffer.concat(chunks).toString("utf8");
|
|
1507
1782
|
}
|
|
1508
1783
|
async function runCode(sbx, argv, out) {
|
|
1509
|
-
const args = parse(argv);
|
|
1784
|
+
const args = parse(argv, [], ["lang", "context", "timeout", "out-dir"]);
|
|
1510
1785
|
const source = need(args.positional[0], "a file, or - to read the code from standard input");
|
|
1511
1786
|
const code = await readInput(source);
|
|
1512
1787
|
const byExtension = {
|
|
@@ -1574,7 +1849,7 @@ async function desktop(sbx, argv, out) {
|
|
|
1574
1849
|
const d = sbx.desktop;
|
|
1575
1850
|
switch (action) {
|
|
1576
1851
|
case "start": {
|
|
1577
|
-
const args = parse(rest);
|
|
1852
|
+
const args = parse(rest, [], ["width", "height"]);
|
|
1578
1853
|
const started = await d.start({
|
|
1579
1854
|
...(integer(args, "width") ? { width: integer(args, "width") } : {}),
|
|
1580
1855
|
...(integer(args, "height") ? { height: integer(args, "height") } : {}),
|
|
@@ -1640,6 +1915,7 @@ async function accountCommand(argv, env, out) {
|
|
|
1640
1915
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1641
1916
|
const store = connectionStore(env);
|
|
1642
1917
|
if (verb === "add") {
|
|
1918
|
+
const { authenticationCommand } = await import("./login.js");
|
|
1643
1919
|
const result = await authenticationCommand(["login", "--another-account", ...rest], env);
|
|
1644
1920
|
if (result.pending)
|
|
1645
1921
|
throw notApprovedYet(result);
|
|
@@ -1662,8 +1938,35 @@ async function accountCommand(argv, env, out) {
|
|
|
1662
1938
|
});
|
|
1663
1939
|
return 0;
|
|
1664
1940
|
}
|
|
1941
|
+
if (verb === "close") {
|
|
1942
|
+
/* ARCHITECTURE.md section 3.1: an owner's key for every product, and the
|
|
1943
|
+
account's name typed exactly. Nothing is asked twice: --confirm is the
|
|
1944
|
+
confirmation, as the website's typed name is. */
|
|
1945
|
+
const args = parse(rest, [], ["confirm"]);
|
|
1946
|
+
for (const name of args.flags.keys())
|
|
1947
|
+
if (name !== "confirm")
|
|
1948
|
+
throw usage(`Unknown option --${name}.`);
|
|
1949
|
+
const confirm = flag(args, "confirm");
|
|
1950
|
+
const runtime = await client(env);
|
|
1951
|
+
if (!confirm) {
|
|
1952
|
+
const who = await runtime.me();
|
|
1953
|
+
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>"}".`);
|
|
1954
|
+
}
|
|
1955
|
+
const closed = await runtime.account.close({ confirm });
|
|
1956
|
+
// This machine's key died with the account.
|
|
1957
|
+
if (!env.RUNTIME_API_KEY)
|
|
1958
|
+
await store.remove().catch(() => undefined);
|
|
1959
|
+
print([
|
|
1960
|
+
`Closed ${closed.name}. Every key has stopped working.`,
|
|
1961
|
+
closed.released
|
|
1962
|
+
? "Nothing was left running or stored."
|
|
1963
|
+
: "What was still running is stopping, and everything stored is being deleted.",
|
|
1964
|
+
"Credit bought in the last 15 days and not spent is refunded on request: write to support@withruntime.com.",
|
|
1965
|
+
].join("\n"), closed);
|
|
1966
|
+
return 0;
|
|
1967
|
+
}
|
|
1665
1968
|
if (verb !== "ls" && verb !== "list")
|
|
1666
|
-
throw unknown("account command", verb, ["ls", "switch", "add"]);
|
|
1969
|
+
throw unknown("account command", verb, ["ls", "switch", "add", "close"]);
|
|
1667
1970
|
await nameSaved(env).catch(() => undefined);
|
|
1668
1971
|
const current = await store.read().catch(() => null);
|
|
1669
1972
|
const saved = [
|
|
@@ -1719,7 +2022,7 @@ async function keys(argv, env, out) {
|
|
|
1719
2022
|
throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
|
|
1720
2023
|
if (verb !== "create")
|
|
1721
2024
|
throw unknown("keys command", verb, ["create"]);
|
|
1722
|
-
const args = parse(rest, ["read-only"]);
|
|
2025
|
+
const args = parse(rest, ["read-only"], ["name", "daily-limit"]);
|
|
1723
2026
|
const known = new Set(["name", "read-only", "daily-limit"]);
|
|
1724
2027
|
for (const name of args.flags.keys())
|
|
1725
2028
|
if (!known.has(name))
|
|
@@ -1730,6 +2033,7 @@ async function keys(argv, env, out) {
|
|
|
1730
2033
|
const limit = flag(args, "daily-limit");
|
|
1731
2034
|
if (readOnly && limit !== undefined)
|
|
1732
2035
|
throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
|
|
2036
|
+
const { createKey, dailyLimitMicros } = await import("./keys.js");
|
|
1733
2037
|
const created = await createKey({
|
|
1734
2038
|
name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
|
|
1735
2039
|
access: readOnly ? "read" : "full",
|
|
@@ -1756,8 +2060,8 @@ const SNAPSHOT_HELP = `runtime snapshot <command>
|
|
|
1756
2060
|
get <id> One snapshot
|
|
1757
2061
|
extend <id> <days> Keep it longer
|
|
1758
2062
|
rm <id> Delete it
|
|
1759
|
-
Take one with \`runtime sandbox snapshot <sandboxId>\`; start from
|
|
1760
|
-
\`runtime sandbox
|
|
2063
|
+
Take one with \`runtime sandbox snapshot <sandboxId>\`; start a sandbox from it
|
|
2064
|
+
with \`runtime sandbox create --snapshot <snapshotId>\`.
|
|
1761
2065
|
`;
|
|
1762
2066
|
async function snapshot(argv, env, out) {
|
|
1763
2067
|
const [verb, ...rest] = argv;
|
|
@@ -1770,7 +2074,7 @@ async function snapshot(argv, env, out) {
|
|
|
1770
2074
|
switch (verb) {
|
|
1771
2075
|
case "ls":
|
|
1772
2076
|
case "list": {
|
|
1773
|
-
const args = parse(rest);
|
|
2077
|
+
const args = parse(rest, [], ["sandbox", "name"]);
|
|
1774
2078
|
const all = await (await api.list({
|
|
1775
2079
|
...(flag(args, "sandbox")
|
|
1776
2080
|
? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
|
|
@@ -1787,11 +2091,11 @@ async function snapshot(argv, env, out) {
|
|
|
1787
2091
|
}
|
|
1788
2092
|
case "get": {
|
|
1789
2093
|
const found = await api.get(need(rest[0], "a snapshot id"));
|
|
1790
|
-
print(
|
|
2094
|
+
print((await import("./cli-extras.js")).describeSnapshot(found, table), found);
|
|
1791
2095
|
return 0;
|
|
1792
2096
|
}
|
|
1793
2097
|
case "extend": {
|
|
1794
|
-
const days = integer(parse(rest.slice(1)), "days") ?? Number(need(rest[1], "days"));
|
|
2098
|
+
const days = integer(parse(rest.slice(1), [], ["days"]), "days") ?? Number(need(rest[1], "days"));
|
|
1795
2099
|
const found = await api.extend(need(rest[0], "a snapshot id"), days);
|
|
1796
2100
|
print(`${found.id} kept until ${found.expiresAt}`, found);
|
|
1797
2101
|
return 0;
|
|
@@ -1811,8 +2115,10 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
1811
2115
|
build [<folder>] [-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
|
|
1812
2116
|
[-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
|
|
1813
2117
|
[--start <command>] [--ready-port <port> | --ready-command <command>]
|
|
1814
|
-
[--max-image-mib 8192] [--timeout <seconds>]
|
|
2118
|
+
[--max-image-mib 8192] [--disk-mib <1024-32768>] [--timeout <seconds>]
|
|
1815
2119
|
Build and stream the log; prints the image id when ready.
|
|
2120
|
+
--disk-mib sizes the build's scratch disk, where earlier
|
|
2121
|
+
stages and the context live.
|
|
1816
2122
|
A Dockerfile's folder is the context: everything its
|
|
1817
2123
|
.dockerignore leaves in (100 MiB compressed at most) is
|
|
1818
2124
|
sent, and a rebuild sends only what changed.
|
|
@@ -1833,6 +2139,28 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
1833
2139
|
registry rm <registry>
|
|
1834
2140
|
Use one: runtime sandbox create --image app:latest
|
|
1835
2141
|
`;
|
|
2142
|
+
/** Options `image build` takes with a value. */
|
|
2143
|
+
const IMAGE_BUILD_VALUES = [
|
|
2144
|
+
"f",
|
|
2145
|
+
"file",
|
|
2146
|
+
"dockerfile",
|
|
2147
|
+
"from",
|
|
2148
|
+
"pip",
|
|
2149
|
+
"apt",
|
|
2150
|
+
"npm",
|
|
2151
|
+
"t",
|
|
2152
|
+
"tag",
|
|
2153
|
+
"name",
|
|
2154
|
+
"target",
|
|
2155
|
+
"build-arg",
|
|
2156
|
+
"start",
|
|
2157
|
+
"ready-port",
|
|
2158
|
+
"ready-command",
|
|
2159
|
+
"ready-timeout",
|
|
2160
|
+
"max-image-mib",
|
|
2161
|
+
"disk-mib",
|
|
2162
|
+
"timeout",
|
|
2163
|
+
];
|
|
1836
2164
|
export async function imageCommand(argv, env, out,
|
|
1837
2165
|
/** Tests give the registry secret here; the CLI reads standard input. */
|
|
1838
2166
|
read = readSecret) {
|
|
@@ -1845,7 +2173,7 @@ read = readSecret) {
|
|
|
1845
2173
|
const api = (await client(env)).images;
|
|
1846
2174
|
switch (verb) {
|
|
1847
2175
|
case "build": {
|
|
1848
|
-
const args = parse(rest, ["no-cache"]);
|
|
2176
|
+
const args = parse(rest, ["no-cache"], IMAGE_BUILD_VALUES);
|
|
1849
2177
|
const list = (name) => (args.flags.get(name) ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean);
|
|
1850
2178
|
const recipe = { apt: list("apt"), pip: list("pip"), npm: list("npm") };
|
|
1851
2179
|
const hasRecipe = recipe.apt.length + recipe.pip.length + recipe.npm.length > 0;
|
|
@@ -1857,6 +2185,7 @@ read = readSecret) {
|
|
|
1857
2185
|
throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
|
|
1858
2186
|
const build = {
|
|
1859
2187
|
...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
|
|
2188
|
+
...(integer(args, "disk-mib") ? { diskMiB: integer(args, "disk-mib") } : {}),
|
|
1860
2189
|
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
1861
2190
|
};
|
|
1862
2191
|
// -t app, -t app:v2 -t app:latest: one name, its tags.
|
|
@@ -1946,7 +2275,7 @@ read = readSecret) {
|
|
|
1946
2275
|
return 0;
|
|
1947
2276
|
}
|
|
1948
2277
|
case "logs": {
|
|
1949
|
-
const args = parse(rest, ["follow", "f"]);
|
|
2278
|
+
const args = parse(rest, ["follow", "f"], []);
|
|
1950
2279
|
const id = (await api.resolve(need(args.positional[0], "an image"))).id;
|
|
1951
2280
|
if (has(args, "follow") || has(args, "f")) {
|
|
1952
2281
|
const lines = [];
|
|
@@ -1995,7 +2324,7 @@ read = readSecret) {
|
|
|
1995
2324
|
return 0;
|
|
1996
2325
|
}
|
|
1997
2326
|
if (action === "set") {
|
|
1998
|
-
const args = parse(more);
|
|
2327
|
+
const args = parse(more, [], ["username", "access-key-id"]);
|
|
1999
2328
|
const registry = need(args.positional[0], "a registry, such as ghcr.io");
|
|
2000
2329
|
const accessKeyId = flag(args, "access-key-id");
|
|
2001
2330
|
if (accessKeyId === undefined && flag(args, "username") === undefined)
|
|
@@ -2057,7 +2386,7 @@ async function volume(argv, env, out) {
|
|
|
2057
2386
|
const api = (await client(env)).volumes;
|
|
2058
2387
|
switch (verb) {
|
|
2059
2388
|
case "create": {
|
|
2060
|
-
const args = parse(rest);
|
|
2389
|
+
const args = parse(rest, [], ["size-mib", "name"]);
|
|
2061
2390
|
const created = await api.create({
|
|
2062
2391
|
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
2063
2392
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
@@ -2095,7 +2424,7 @@ async function volume(argv, env, out) {
|
|
|
2095
2424
|
return 0;
|
|
2096
2425
|
}
|
|
2097
2426
|
case "backup": {
|
|
2098
|
-
const args = parse(rest, ["no-wait"]);
|
|
2427
|
+
const args = parse(rest, ["no-wait"], ["retention-days"]);
|
|
2099
2428
|
const id = need(args.positional[0], "a volume id");
|
|
2100
2429
|
const days = integer(args, "retention-days");
|
|
2101
2430
|
const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
|
|
@@ -2125,7 +2454,7 @@ async function volume(argv, env, out) {
|
|
|
2125
2454
|
return 0;
|
|
2126
2455
|
}
|
|
2127
2456
|
case "restore": {
|
|
2128
|
-
const args = parse(rest);
|
|
2457
|
+
const args = parse(rest, [], ["name"]);
|
|
2129
2458
|
const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
|
|
2130
2459
|
if (!out.json && created.state === "creating")
|
|
2131
2460
|
out.error(`Restoring; check with: ${me} volume get ${created.id}`);
|
|
@@ -2139,7 +2468,7 @@ async function volume(argv, env, out) {
|
|
|
2139
2468
|
return 0;
|
|
2140
2469
|
}
|
|
2141
2470
|
case "backup-policy": {
|
|
2142
|
-
const args = parse(rest);
|
|
2471
|
+
const args = parse(rest, [], ["daily", "retention-days"]);
|
|
2143
2472
|
const id = need(args.positional[0], "a volume id");
|
|
2144
2473
|
const daily = flag(args, "daily");
|
|
2145
2474
|
if (daily !== undefined && !["on", "off", "true", "false"].includes(daily))
|
|
@@ -2177,6 +2506,49 @@ const CREATE_FLAGS = [
|
|
|
2177
2506
|
"persistent",
|
|
2178
2507
|
"get-or-create",
|
|
2179
2508
|
];
|
|
2509
|
+
/** Says on standard error, once for each reason, why a create has not
|
|
2510
|
+
* answered yet: it is waiting for a trial slot or room, which can take up to
|
|
2511
|
+
* two minutes, and silence reads as a hang. */
|
|
2512
|
+
function waiting(out) {
|
|
2513
|
+
const said = new Set();
|
|
2514
|
+
return (refusal) => {
|
|
2515
|
+
if (said.has(refusal.code))
|
|
2516
|
+
return;
|
|
2517
|
+
said.add(refusal.code);
|
|
2518
|
+
out.error(`${refusal.message} Waiting for room, up to 2 minutes; Ctrl-C stops.`);
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
/** Options `sandbox create` and `run` take with a value. */
|
|
2522
|
+
const CREATE_VALUES = [
|
|
2523
|
+
"name",
|
|
2524
|
+
"label",
|
|
2525
|
+
"region",
|
|
2526
|
+
"vcpu",
|
|
2527
|
+
"memory",
|
|
2528
|
+
"disk",
|
|
2529
|
+
"cpu",
|
|
2530
|
+
"cpu-floor",
|
|
2531
|
+
"timeout",
|
|
2532
|
+
"on-timeout",
|
|
2533
|
+
"idle-pause",
|
|
2534
|
+
"max-cost",
|
|
2535
|
+
"max-total-cost",
|
|
2536
|
+
"allow",
|
|
2537
|
+
"deny",
|
|
2538
|
+
"connect",
|
|
2539
|
+
"image",
|
|
2540
|
+
"snapshot",
|
|
2541
|
+
"volume",
|
|
2542
|
+
];
|
|
2543
|
+
/** Dollars from the command line as integer microdollars: 25, 2.50 or 0.000001. */
|
|
2544
|
+
function usdMicros(args, name) {
|
|
2545
|
+
const value = flag(args, name);
|
|
2546
|
+
if (value === undefined)
|
|
2547
|
+
return undefined;
|
|
2548
|
+
if (!/^\d+(\.\d{1,6})?$/.test(value))
|
|
2549
|
+
throw usage(`--${name} takes dollars, such as 25 or 2.50.`);
|
|
2550
|
+
return Math.round(Number(value) * 1e6);
|
|
2551
|
+
}
|
|
2180
2552
|
/** A create body from the command line's flags. */
|
|
2181
2553
|
function createInput(args) {
|
|
2182
2554
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
@@ -2197,9 +2569,16 @@ function createInput(args) {
|
|
|
2197
2569
|
...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
|
|
2198
2570
|
...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
|
|
2199
2571
|
...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
|
|
2572
|
+
...(flag(args, "cpu") ? { cpu: oneOf(args, "cpu", ["shared", "reserved"]) } : {}),
|
|
2573
|
+
...(integer(args, "cpu-floor") ? { cpuFloorMillis: integer(args, "cpu-floor") } : {}),
|
|
2574
|
+
...(flag(args, "region") ? { region: flag(args, "region") } : {}),
|
|
2575
|
+
...(usdMicros(args, "max-cost") ? { maxCostMicros: usdMicros(args, "max-cost") } : {}),
|
|
2576
|
+
...(usdMicros(args, "max-total-cost")
|
|
2577
|
+
? { maxTotalCostMicros: usdMicros(args, "max-total-cost") }
|
|
2578
|
+
: {}),
|
|
2200
2579
|
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
2201
2580
|
...(flag(args, "on-timeout")
|
|
2202
|
-
? { onLeaseEnd:
|
|
2581
|
+
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
|
|
2203
2582
|
: {}),
|
|
2204
2583
|
...(integer(args, "idle-pause") !== undefined
|
|
2205
2584
|
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
@@ -2217,7 +2596,9 @@ function createInput(args) {
|
|
|
2217
2596
|
}
|
|
2218
2597
|
/** Runs a command in a sandbox, streaming its output, and answers its exit code
|
|
2219
2598
|
* (124 on a timeout), so a script's `set -e` behaves. */
|
|
2220
|
-
async function execute(sbx, command, args, out, timeout
|
|
2599
|
+
async function execute(sbx, command, args, out, timeout,
|
|
2600
|
+
/** `run` stops its whole sandbox on Ctrl-C, so it handles the signal itself. */
|
|
2601
|
+
ownInterrupt = true) {
|
|
2221
2602
|
const target = command.length === 1 ? command[0] : command;
|
|
2222
2603
|
const extras = await import("./cli-extras.js");
|
|
2223
2604
|
const stdin = await extras.pipedStdin();
|
|
@@ -2227,13 +2608,32 @@ async function execute(sbx, command, args, out, timeout) {
|
|
|
2227
2608
|
...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
|
|
2228
2609
|
...(timeout ? { timeoutMs: timeout * 1000 } : {}),
|
|
2229
2610
|
};
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2611
|
+
// Ctrl-C stops the command in the sandbox too, and says so; without this
|
|
2612
|
+
// the CLI exited and the command ran on unseen.
|
|
2613
|
+
const interrupt = new AbortController();
|
|
2614
|
+
const onInterrupt = () => interrupt.abort(new Error("Interrupted"));
|
|
2615
|
+
if (ownInterrupt)
|
|
2616
|
+
process.once("SIGINT", onInterrupt);
|
|
2617
|
+
let result;
|
|
2618
|
+
try {
|
|
2619
|
+
result = out.json
|
|
2620
|
+
? await sbx.exec(target, { ...options, signal: interrupt.signal })
|
|
2621
|
+
: await sbx.exec(target, {
|
|
2622
|
+
...options,
|
|
2623
|
+
signal: interrupt.signal,
|
|
2624
|
+
onStdout: (t) => process.stdout.write(t),
|
|
2625
|
+
onStderr: (t) => process.stderr.write(t),
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
catch (error) {
|
|
2629
|
+
if (!interrupt.signal.aborted)
|
|
2630
|
+
throw error;
|
|
2631
|
+
out.error(describeError(error, out.json));
|
|
2632
|
+
return 130;
|
|
2633
|
+
}
|
|
2634
|
+
finally {
|
|
2635
|
+
process.off("SIGINT", onInterrupt);
|
|
2636
|
+
}
|
|
2237
2637
|
if (out.json)
|
|
2238
2638
|
out.write(JSON.stringify(result));
|
|
2239
2639
|
else if (result.timedOut)
|
|
@@ -2255,13 +2655,12 @@ async function shell(sbx, command) {
|
|
|
2255
2655
|
});
|
|
2256
2656
|
if (stdin.isTTY)
|
|
2257
2657
|
stdin.setRawMode(true);
|
|
2658
|
+
const unhook = (await import("./cli-extras.js")).pipeShellInput(stdin, terminal);
|
|
2258
2659
|
stdin.resume();
|
|
2259
|
-
const onInput = (chunk) => terminal.write(new Uint8Array(chunk));
|
|
2260
2660
|
const onResize = () => terminal.resize(size().cols, size().rows);
|
|
2261
|
-
stdin.on("data", onInput);
|
|
2262
2661
|
process.stdout.on("resize", onResize);
|
|
2263
2662
|
const code = await terminal.exited;
|
|
2264
|
-
|
|
2663
|
+
unhook();
|
|
2265
2664
|
process.stdout.off("resize", onResize);
|
|
2266
2665
|
if (stdin.isTTY)
|
|
2267
2666
|
stdin.setRawMode(false);
|
|
@@ -2291,7 +2690,13 @@ export function describeError(error, json) {
|
|
|
2291
2690
|
details: error.details,
|
|
2292
2691
|
},
|
|
2293
2692
|
});
|
|
2294
|
-
return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId
|
|
2693
|
+
return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId
|
|
2694
|
+
? // A refusal of what was sent is the caller's to fix; only a failure
|
|
2695
|
+
// of ours is worth a report.
|
|
2696
|
+
error.status >= 400 && error.status < 500
|
|
2697
|
+
? `\nRequest: ${error.requestId}`
|
|
2698
|
+
: `\nRequest: ${error.requestId} (report it: runtime feedback "..." --request-id ${error.requestId})`
|
|
2699
|
+
: ""}`;
|
|
2295
2700
|
}
|
|
2296
2701
|
const message = error instanceof Error ? error.message : String(error);
|
|
2297
2702
|
return json ? JSON.stringify({ error: { code: "cli_error", message } }) : `Error: ${message}`;
|
|
@@ -2369,7 +2774,7 @@ async function webhooksCommand(argv, env, out) {
|
|
|
2369
2774
|
return 0;
|
|
2370
2775
|
}
|
|
2371
2776
|
const hooks = (await client(env)).webhooks;
|
|
2372
|
-
const args = parse(rest, ["enable", "disable"]);
|
|
2777
|
+
const args = parse(rest, ["enable", "disable"], ["events", "description", "url", "keep", "limit"]);
|
|
2373
2778
|
const id = () => need(args.positional[0], "a webhook id");
|
|
2374
2779
|
const events = () => flag(args, "events")
|
|
2375
2780
|
?.split(",")
|
|
@@ -2465,7 +2870,7 @@ async function otelCommand(argv, env, out) {
|
|
|
2465
2870
|
product = "otel";
|
|
2466
2871
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
2467
2872
|
const otel = (await client(env)).otel;
|
|
2468
|
-
const args = parse(rest, ["enable", "disable"]);
|
|
2873
|
+
const args = parse(rest, ["enable", "disable"], ["endpoint", "header", "signals"]);
|
|
2469
2874
|
const id = () => need(args.positional[0], "an export id");
|
|
2470
2875
|
const signals = () => flag(args, "signals")
|
|
2471
2876
|
?.split(",")
|