withruntime 0.6.0 → 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 +8 -5
- package/dist/ai/index.d.ts +21 -5
- package/dist/ai/index.js +14 -21
- package/dist/ai-harness/index.d.ts +194 -0
- package/dist/ai-harness/index.js +517 -0
- 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 +537 -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 +44 -17
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,30 +280,37 @@ 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 = [
|
|
307
|
+
"account",
|
|
308
|
+
"audit",
|
|
241
309
|
"login",
|
|
242
310
|
"logout",
|
|
243
311
|
"whoami",
|
|
244
312
|
"usage",
|
|
313
|
+
"billing",
|
|
245
314
|
"limits",
|
|
246
315
|
"sso",
|
|
247
316
|
"secrets",
|
|
@@ -262,12 +331,38 @@ const COMMANDS = [
|
|
|
262
331
|
"port",
|
|
263
332
|
"address",
|
|
264
333
|
"tunnel",
|
|
334
|
+
"network",
|
|
265
335
|
"webhooks",
|
|
266
336
|
"otel",
|
|
267
337
|
"events",
|
|
268
338
|
"help",
|
|
269
339
|
"version",
|
|
270
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
|
+
];
|
|
271
366
|
/** Sandbox commands that once sat at the top level. */
|
|
272
367
|
const SANDBOX_MOVED = ["run", "ssh", "port-forward", "forward"];
|
|
273
368
|
const SANDBOX_VERBS = [
|
|
@@ -313,6 +408,14 @@ function need(value, what) {
|
|
|
313
408
|
throw usage(`Give ${what}.`);
|
|
314
409
|
return value;
|
|
315
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
|
+
}
|
|
316
419
|
/** The client, with the key from RUNTIME_API_KEY or this machine's saved
|
|
317
420
|
* connection. With neither, the first command connects the machine first (a
|
|
318
421
|
* browser approval, nothing to copy) and then does what was asked, so setup
|
|
@@ -339,6 +442,7 @@ async function client(env) {
|
|
|
339
442
|
env.RUNTIME_NO_LOGIN)
|
|
340
443
|
throw error;
|
|
341
444
|
process.stderr.write("This machine is not connected to Runtime yet. Connecting it first.\n");
|
|
445
|
+
const { authenticationCommand } = await import("./login.js");
|
|
342
446
|
const login = await authenticationCommand(["login"], env);
|
|
343
447
|
if (login.pending)
|
|
344
448
|
throw notApprovedYet(login);
|
|
@@ -381,7 +485,7 @@ function describe(info) {
|
|
|
381
485
|
.map(([k, v]) => `${k}=${v}`)
|
|
382
486
|
.join(", ") || "-",
|
|
383
487
|
],
|
|
384
|
-
["charged",
|
|
488
|
+
["charged", dollars(BigInt(Math.round(Number(info.chargedMicros) || 0)))],
|
|
385
489
|
]);
|
|
386
490
|
}
|
|
387
491
|
/** Resolves `<id>:/path` into its parts, or undefined for a local path. */
|
|
@@ -400,6 +504,16 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
400
504
|
SANDBOX_MOVED.includes(remaining[0] ?? "");
|
|
401
505
|
const command = nested ? remaining[0] : first;
|
|
402
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);
|
|
403
517
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
404
518
|
product = "";
|
|
405
519
|
if (!command) {
|
|
@@ -425,14 +539,14 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
425
539
|
return 0;
|
|
426
540
|
}
|
|
427
541
|
if (command === "run") {
|
|
428
|
-
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)'`);
|
|
429
543
|
const commandLine = args.rest ?? args.positional;
|
|
430
544
|
if (!commandLine.length)
|
|
431
545
|
throw usage(`Give the command after --: ${me} sandbox run -- python3 -c 'print(6*7)'`);
|
|
432
546
|
const runtime = await client(env);
|
|
433
547
|
const timeout = integer(args, "timeout");
|
|
434
548
|
const { timeoutSeconds: _lease, ...input } = createInput(args);
|
|
435
|
-
const sbx = await runtime.sandboxes.create(input);
|
|
549
|
+
const sbx = await runtime.sandboxes.create(input, { onCapacityWait: waiting(out) });
|
|
436
550
|
let cleanup;
|
|
437
551
|
// SIGINT and normal completion share one stop. Keep an absolute deadline
|
|
438
552
|
// across the stop retries and final state check, so cleanup cannot hang.
|
|
@@ -462,7 +576,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
462
576
|
process.once("SIGINT", interrupted);
|
|
463
577
|
let code = 1;
|
|
464
578
|
try {
|
|
465
|
-
code = await execute(sbx, commandLine, args, out, timeout);
|
|
579
|
+
code = await execute(sbx, commandLine, args, out, timeout, has(args, "keep"));
|
|
466
580
|
}
|
|
467
581
|
finally {
|
|
468
582
|
try {
|
|
@@ -482,6 +596,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
482
596
|
return 0;
|
|
483
597
|
}
|
|
484
598
|
if (command === "login" || command === "logout") {
|
|
599
|
+
const { authenticationCommand } = await import("./login.js");
|
|
485
600
|
const result = await authenticationCommand([command, ...rest], env);
|
|
486
601
|
print(result.pending
|
|
487
602
|
? `Not connected yet. When it is approved, run any command, or \`${me} login --wait\`; it uses this link.`
|
|
@@ -491,9 +606,26 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
491
606
|
return 0;
|
|
492
607
|
}
|
|
493
608
|
if (command === "whoami") {
|
|
494
|
-
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";
|
|
495
625
|
print(table([
|
|
496
|
-
["organization", who.orgId],
|
|
626
|
+
["organization", who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId],
|
|
627
|
+
...(who.role ? [["role", who.role]] : []),
|
|
628
|
+
...(funding ? [["funding", funding]] : []),
|
|
497
629
|
["agent", who.principalId],
|
|
498
630
|
["key", who.credentialId ?? "-"],
|
|
499
631
|
["api", who.apiVersion],
|
|
@@ -501,7 +633,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
501
633
|
return 0;
|
|
502
634
|
}
|
|
503
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]`);
|
|
504
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
|
+
}
|
|
505
647
|
print(usageSummary(value), value);
|
|
506
648
|
return 0;
|
|
507
649
|
}
|
|
@@ -533,7 +675,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
533
675
|
if (command === "otel")
|
|
534
676
|
return otelCommand(rest, env, out);
|
|
535
677
|
if (command === "events") {
|
|
536
|
-
const args = parse(rest);
|
|
678
|
+
const args = parse(rest, [], ["sandbox", "type", "limit"]);
|
|
537
679
|
const rt = await client(env);
|
|
538
680
|
const sandbox = flag(args, "sandbox");
|
|
539
681
|
const page = await rt.events.list({
|
|
@@ -577,7 +719,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
577
719
|
if (command === "account" || command === "accounts")
|
|
578
720
|
return accountCommand(rest, env, out);
|
|
579
721
|
if (command === "audit") {
|
|
580
|
-
const args = parse(rest);
|
|
722
|
+
const args = parse(rest, [], ["action", "limit", "before"]);
|
|
581
723
|
const known = new Set(["action", "limit", "before"]);
|
|
582
724
|
for (const name of args.flags.keys())
|
|
583
725
|
if (!known.has(name))
|
|
@@ -605,6 +747,24 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
605
747
|
: "Nothing recorded yet.", page);
|
|
606
748
|
return 0;
|
|
607
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
|
+
}
|
|
608
768
|
if (command === "referrals") {
|
|
609
769
|
const r = await (await client(env)).referrals.get();
|
|
610
770
|
const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
@@ -646,7 +806,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
646
806
|
return 0;
|
|
647
807
|
}
|
|
648
808
|
if (command === "feedback") {
|
|
649
|
-
const args = parse(rest, ["list"]);
|
|
809
|
+
const args = parse(rest, ["list"], ["kind", "detail", "competitor", "request-id"]);
|
|
650
810
|
if (has(args, "list")) {
|
|
651
811
|
const { data } = await (await client(env)).feedback.list();
|
|
652
812
|
print(data.length
|
|
@@ -707,11 +867,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
707
867
|
}
|
|
708
868
|
if (command === "ssh") {
|
|
709
869
|
product = "ssh";
|
|
710
|
-
return sshCommand(rest, env, out, () => client(env), me);
|
|
870
|
+
return (await import("./ssh.js")).sshCommand(rest, env, out, () => client(env), me);
|
|
711
871
|
}
|
|
712
872
|
if (command === "port-forward" || command === "forward") {
|
|
713
873
|
product = "ssh";
|
|
714
|
-
return portForwardCommand(rest, out, () => client(env), me);
|
|
874
|
+
return (await import("./ssh.js")).portForwardCommand(rest, out, () => client(env), me);
|
|
715
875
|
}
|
|
716
876
|
if (command === "sandbox" || command === "sandboxes" || command === "sbx") {
|
|
717
877
|
product = "sandbox";
|
|
@@ -733,13 +893,62 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
733
893
|
command;
|
|
734
894
|
if (NETWORK_PRODUCTS.includes(networkProduct)) {
|
|
735
895
|
product = networkProduct;
|
|
736
|
-
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));
|
|
737
897
|
}
|
|
738
898
|
throw unknown("command", command, COMMANDS);
|
|
739
899
|
}
|
|
740
900
|
/** `runtime usage` for a person: the balance, the trial and what each kind
|
|
741
901
|
* of resource was charged, in dollars. --json keeps every figure, and each
|
|
742
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
|
+
}
|
|
743
952
|
function usageSummary(u) {
|
|
744
953
|
const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
|
|
745
954
|
? BigInt(value)
|
|
@@ -749,11 +958,14 @@ function usageSummary(u) {
|
|
|
749
958
|
const rows = [
|
|
750
959
|
["available", dollars(micros(u.available))],
|
|
751
960
|
["credited", dollars(micros(u.credited))],
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
],
|
|
756
|
-
|
|
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))],
|
|
757
969
|
];
|
|
758
970
|
if (micros(u.expired) > 0n)
|
|
759
971
|
rows.push(["expired or taken back", dollars(micros(u.expired))]);
|
|
@@ -788,13 +1000,15 @@ function usageSummary(u) {
|
|
|
788
1000
|
])
|
|
789
1001
|
: "No resources yet.",
|
|
790
1002
|
"",
|
|
791
|
-
`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`,
|
|
792
1004
|
].join("\n");
|
|
793
1005
|
}
|
|
794
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;
|
|
795
1007
|
/** A sandbox's id from its id or its name, for the filters that take an id. */
|
|
796
1008
|
async function sandboxIdOf(runtime, target) {
|
|
797
|
-
return SANDBOX_UUID.test(target)
|
|
1009
|
+
return SANDBOX_UUID.test(target)
|
|
1010
|
+
? target
|
|
1011
|
+
: (await (await import("./ssh.js")).resolveSandbox(runtime, target)).id;
|
|
798
1012
|
}
|
|
799
1013
|
/** What a process has printed so far, in order, without waiting for more. */
|
|
800
1014
|
async function outputSoFar(runtime, sandboxId, processId) {
|
|
@@ -900,7 +1114,7 @@ function trialNote(trial, all) {
|
|
|
900
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.`;
|
|
901
1115
|
}
|
|
902
1116
|
async function switching(command, argv, env, out) {
|
|
903
|
-
const args = parse(argv);
|
|
1117
|
+
const args = parse(argv, [], command === "compare" ? ["from", "days"] : ["from"]);
|
|
904
1118
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
905
1119
|
const known = new Set(command === "compare" ? ["from", "days"] : ["from"]);
|
|
906
1120
|
for (const name of args.flags.keys())
|
|
@@ -927,7 +1141,7 @@ async function switching(command, argv, env, out) {
|
|
|
927
1141
|
return 0;
|
|
928
1142
|
}
|
|
929
1143
|
if (!from)
|
|
930
|
-
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...).`);
|
|
931
1145
|
/* The server holds the list of rivals it has rates for, and names them all
|
|
932
1146
|
when it refuses one, so a rival added there needs no new CLI. */
|
|
933
1147
|
const days = integer(args, "days");
|
|
@@ -942,7 +1156,7 @@ async function switching(command, argv, env, out) {
|
|
|
942
1156
|
const monthly = month === undefined
|
|
943
1157
|
? ""
|
|
944
1158
|
: month >= 0n
|
|
945
|
-
? `about ${dollars(month)} a month`
|
|
1159
|
+
? `a saving of about ${dollars(month)} a month`
|
|
946
1160
|
: `about ${dollars(-month)} a month more on Runtime`;
|
|
947
1161
|
/* The month is projected from the days the usage covers, not from the
|
|
948
1162
|
window asked for: say which, or a young account's 7 and 90 days read the
|
|
@@ -951,7 +1165,7 @@ async function switching(command, argv, env, out) {
|
|
|
951
1165
|
const pace = month === undefined
|
|
952
1166
|
? "."
|
|
953
1167
|
: covered < c.window.days
|
|
954
|
-
? `. Your
|
|
1168
|
+
? `. Your usage so far covers ${covered <= 1 ? "less than a day" : `${covered.toLocaleString("en-US")} days`}; at that pace, ${monthly}.`
|
|
955
1169
|
: c.window.days === 30
|
|
956
1170
|
? `, ${monthly}.`
|
|
957
1171
|
: `. At that pace, ${monthly}.`;
|
|
@@ -972,7 +1186,7 @@ async function switching(command, argv, env, out) {
|
|
|
972
1186
|
return 0;
|
|
973
1187
|
}
|
|
974
1188
|
async function support(argv, env, out) {
|
|
975
|
-
const args = parse(argv);
|
|
1189
|
+
const args = parse(argv, [], ["conversation"]);
|
|
976
1190
|
const api = (await client(env)).support;
|
|
977
1191
|
const conversationId = flag(args, "conversation");
|
|
978
1192
|
const [first, second, third] = args.positional;
|
|
@@ -1010,9 +1224,10 @@ async function sandbox(argv, env, out) {
|
|
|
1010
1224
|
}
|
|
1011
1225
|
// Inside a sandbox, with no API key: the sandbox's own request token asks.
|
|
1012
1226
|
if (verb === "identity-token") {
|
|
1013
|
-
const args = parse(rest);
|
|
1227
|
+
const args = parse(rest, [], ["audience", "lifetime"]);
|
|
1014
1228
|
const audience = need(flag(args, "audience"), "--audience, such as sts.amazonaws.com");
|
|
1015
1229
|
const lifetime = flag(args, "lifetime");
|
|
1230
|
+
const { identityToken } = await import("./identity.js");
|
|
1016
1231
|
const token = await identityToken({
|
|
1017
1232
|
audience,
|
|
1018
1233
|
...(lifetime === undefined ? {} : { lifetimeSeconds: Number(lifetime) }),
|
|
@@ -1023,17 +1238,26 @@ async function sandbox(argv, env, out) {
|
|
|
1023
1238
|
const runtime = await client(env);
|
|
1024
1239
|
/* By id or by name, for every command here, as ssh and port-forward
|
|
1025
1240
|
always took one. */
|
|
1026
|
-
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);
|
|
1027
1249
|
switch (verb) {
|
|
1028
1250
|
case "create": {
|
|
1029
|
-
const args = parse(rest, CREATE_FLAGS);
|
|
1030
|
-
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
|
+
});
|
|
1031
1255
|
print(sbx.id, sbx.info);
|
|
1032
1256
|
return 0;
|
|
1033
1257
|
}
|
|
1034
1258
|
case "ls":
|
|
1035
1259
|
case "list": {
|
|
1036
|
-
const args = parse(rest, ["all"]);
|
|
1260
|
+
const args = parse(rest, ["all"], ["label", "state"]);
|
|
1037
1261
|
const page = await runtime.sandboxes.list({
|
|
1038
1262
|
...(has(args, "all") ? { includeStopped: true } : {}),
|
|
1039
1263
|
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
@@ -1085,8 +1309,8 @@ async function sandbox(argv, env, out) {
|
|
|
1085
1309
|
return 0;
|
|
1086
1310
|
}
|
|
1087
1311
|
case "mount": {
|
|
1088
|
-
const args = parse(rest, ["read-only"]);
|
|
1089
|
-
const sbx = await
|
|
1312
|
+
const args = parse(rest, ["read-only"], ["secret", "region", "endpoint", "account-id"]);
|
|
1313
|
+
const sbx = await use(args.positional[0]);
|
|
1090
1314
|
const source = need(args.positional[1], "a bucket, as s3://bucket, r2://bucket or gcs://bucket");
|
|
1091
1315
|
const found = /^(s3|r2|gcs):\/\/([^/]+)(?:\/(.+))?$/.exec(source);
|
|
1092
1316
|
if (!found)
|
|
@@ -1106,7 +1330,7 @@ async function sandbox(argv, env, out) {
|
|
|
1106
1330
|
return 0;
|
|
1107
1331
|
}
|
|
1108
1332
|
case "mounts": {
|
|
1109
|
-
const sbx = await
|
|
1333
|
+
const sbx = await use(rest[0]);
|
|
1110
1334
|
const all = await sbx.mounts.list();
|
|
1111
1335
|
print(all.length
|
|
1112
1336
|
? table([
|
|
@@ -1123,13 +1347,13 @@ async function sandbox(argv, env, out) {
|
|
|
1123
1347
|
return 0;
|
|
1124
1348
|
}
|
|
1125
1349
|
case "unmount": {
|
|
1126
|
-
const sbx = await
|
|
1350
|
+
const sbx = await use(rest[0]);
|
|
1127
1351
|
const gone = await sbx.mounts.remove(need(rest[1], "the mounted path"));
|
|
1128
1352
|
print(`${gone.path} unmounted`, gone);
|
|
1129
1353
|
return 0;
|
|
1130
1354
|
}
|
|
1131
1355
|
case "snapshot": {
|
|
1132
|
-
const args = parse(rest);
|
|
1356
|
+
const args = parse(rest, [], ["name", "retention"]);
|
|
1133
1357
|
const sbx = await get(args.positional[0]);
|
|
1134
1358
|
const snapshot = await sbx.snapshot({
|
|
1135
1359
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
@@ -1139,7 +1363,7 @@ async function sandbox(argv, env, out) {
|
|
|
1139
1363
|
return 0;
|
|
1140
1364
|
}
|
|
1141
1365
|
case "fork": {
|
|
1142
|
-
const args = parse(rest, ["keep-snapshot", "trial", "paid"]);
|
|
1366
|
+
const args = parse(rest, ["keep-snapshot", "trial", "paid"], ["count", "name"]);
|
|
1143
1367
|
const sbx = await get(args.positional[0]);
|
|
1144
1368
|
const forks = await sbx.fork({
|
|
1145
1369
|
count: integer(args, "count") ?? 1,
|
|
@@ -1155,7 +1379,7 @@ async function sandbox(argv, env, out) {
|
|
|
1155
1379
|
return 0;
|
|
1156
1380
|
}
|
|
1157
1381
|
case "update": {
|
|
1158
|
-
const args = parse(rest);
|
|
1382
|
+
const args = parse(rest, [], ["name", "label", "idle-pause", "auto-wake", "persistent", "max-total-cost"]);
|
|
1159
1383
|
const sbx = await get(args.positional[0]);
|
|
1160
1384
|
const onOff = (name) => {
|
|
1161
1385
|
const value = flag(args, name);
|
|
@@ -1196,19 +1420,19 @@ async function sandbox(argv, env, out) {
|
|
|
1196
1420
|
return 0;
|
|
1197
1421
|
}
|
|
1198
1422
|
case "exec": {
|
|
1199
|
-
const args = parse(rest);
|
|
1423
|
+
const args = parse(rest, [], ["cwd", "env", "timeout"], `${me} sandbox exec <id> -- ls -la`);
|
|
1200
1424
|
const command = args.rest ?? args.positional.slice(1);
|
|
1201
1425
|
if (!command.length)
|
|
1202
1426
|
throw usage("Give the command after --: runtime sandbox exec <id> -- ls -la");
|
|
1203
|
-
const sbx = await
|
|
1427
|
+
const sbx = await use(args.positional[0]);
|
|
1204
1428
|
return execute(sbx, command, args, out, integer(args, "timeout"));
|
|
1205
1429
|
}
|
|
1206
1430
|
case "spawn": {
|
|
1207
|
-
const args = parse(rest, ["pty"]);
|
|
1431
|
+
const args = parse(rest, ["pty"], ["cwd", "env"], `${me} sandbox spawn <id> -- npm run dev`);
|
|
1208
1432
|
const command = args.rest ?? [];
|
|
1209
1433
|
if (!command.length)
|
|
1210
1434
|
throw usage("Give the command after --: runtime sandbox spawn <id> -- npm run dev");
|
|
1211
|
-
const sbx = await
|
|
1435
|
+
const sbx = await use(args.positional[0]);
|
|
1212
1436
|
const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
|
|
1213
1437
|
...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
|
|
1214
1438
|
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
@@ -1218,7 +1442,7 @@ async function sandbox(argv, env, out) {
|
|
|
1218
1442
|
return 0;
|
|
1219
1443
|
}
|
|
1220
1444
|
case "ps": {
|
|
1221
|
-
const sbx = await
|
|
1445
|
+
const sbx = await use(rest[0]);
|
|
1222
1446
|
const list = await sbx.processes.list();
|
|
1223
1447
|
print(list.length
|
|
1224
1448
|
? table([
|
|
@@ -1229,8 +1453,8 @@ async function sandbox(argv, env, out) {
|
|
|
1229
1453
|
return 0;
|
|
1230
1454
|
}
|
|
1231
1455
|
case "logs": {
|
|
1232
|
-
const args = parse(rest, ["f", "follow"]);
|
|
1233
|
-
const sbx = await
|
|
1456
|
+
const args = parse(rest, ["f", "follow"], []);
|
|
1457
|
+
const sbx = await use(args.positional[0]);
|
|
1234
1458
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
1235
1459
|
if (has(args, "f") || has(args, "follow")) {
|
|
1236
1460
|
for await (const event of proc.output())
|
|
@@ -1256,26 +1480,28 @@ async function sandbox(argv, env, out) {
|
|
|
1256
1480
|
return 0;
|
|
1257
1481
|
}
|
|
1258
1482
|
case "kill": {
|
|
1259
|
-
const args = parse(rest);
|
|
1260
|
-
const sbx = await
|
|
1483
|
+
const args = parse(rest, [], ["signal"]);
|
|
1484
|
+
const sbx = await use(args.positional[0]);
|
|
1261
1485
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
1262
1486
|
await proc.kill((flag(args, "signal") ?? "SIGTERM"));
|
|
1263
1487
|
print(`Signalled ${proc.id}.`, { processId: proc.id });
|
|
1264
1488
|
return 0;
|
|
1265
1489
|
}
|
|
1266
1490
|
case "shell": {
|
|
1267
|
-
const args = parse(rest);
|
|
1268
|
-
const sbx = await
|
|
1491
|
+
const args = parse(rest, [], ["command"]);
|
|
1492
|
+
const sbx = await use(args.positional[0]);
|
|
1269
1493
|
return shell(sbx, flag(args, "command"));
|
|
1270
1494
|
}
|
|
1271
1495
|
case "cat": {
|
|
1272
|
-
const sbx = await
|
|
1273
|
-
|
|
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 });
|
|
1274
1500
|
return 0;
|
|
1275
1501
|
}
|
|
1276
1502
|
case "files": {
|
|
1277
|
-
const args = parse(rest);
|
|
1278
|
-
const sbx = await
|
|
1503
|
+
const args = parse(rest, [], ["depth", "glob"]);
|
|
1504
|
+
const sbx = await use(args.positional[0]);
|
|
1279
1505
|
const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
|
|
1280
1506
|
...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
|
|
1281
1507
|
...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
|
|
@@ -1286,36 +1512,51 @@ async function sandbox(argv, env, out) {
|
|
|
1286
1512
|
return 0;
|
|
1287
1513
|
}
|
|
1288
1514
|
case "cp": {
|
|
1289
|
-
const [from, to] = parse(rest).positional;
|
|
1515
|
+
const [from, to] = parse(rest, ["r", "recursive"], []).positional;
|
|
1290
1516
|
const source = remote(need(from, "a source"));
|
|
1291
1517
|
const target = remote(need(to, "a destination"));
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
+
}
|
|
1294
1525
|
else if (!source && target)
|
|
1295
|
-
await (await
|
|
1526
|
+
await (await use(target.id)).files.upload(from, target.path.endsWith("/") ? inside(target.path, from) : target.path);
|
|
1296
1527
|
else
|
|
1297
1528
|
throw usage("Name exactly one side as <sandbox-id>:/path.");
|
|
1298
1529
|
print(`Copied ${from} to ${to}.`, { from, to });
|
|
1299
1530
|
return 0;
|
|
1300
1531
|
}
|
|
1301
1532
|
case "run-code":
|
|
1302
|
-
return runCode(await
|
|
1533
|
+
return runCode(await use(rest[0]), rest.slice(1), out);
|
|
1303
1534
|
case "preview": {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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]);
|
|
1307
1547
|
const preview = await sbx.previews.create(port, {
|
|
1308
1548
|
...(has(args, "public") ? { visibility: "public" } : {}),
|
|
1309
1549
|
...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
|
|
1310
1550
|
});
|
|
1311
|
-
|
|
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"
|
|
1312
1554
|
? preview.url
|
|
1313
|
-
: `${preview.urlWithToken ?? preview.url}\nPrivate: send the header x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt})
|
|
1314
|
-
(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);
|
|
1315
1556
|
return 0;
|
|
1316
1557
|
}
|
|
1317
1558
|
case "previews": {
|
|
1318
|
-
const list = await (await
|
|
1559
|
+
const list = await (await use(rest[0])).previews.list();
|
|
1319
1560
|
print(list.length
|
|
1320
1561
|
? table([
|
|
1321
1562
|
["PORT", "VISIBILITY", "URL"],
|
|
@@ -1325,14 +1566,14 @@ async function sandbox(argv, env, out) {
|
|
|
1325
1566
|
return 0;
|
|
1326
1567
|
}
|
|
1327
1568
|
case "unshare": {
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
1569
|
+
const port = portOf(rest[1], "runtime sandbox unshare <id> 3000");
|
|
1570
|
+
const sbx = await use(rest[0]);
|
|
1330
1571
|
print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
|
|
1331
1572
|
return 0;
|
|
1332
1573
|
}
|
|
1333
1574
|
case "network": {
|
|
1334
|
-
const args = parse(rest, ["no-internet", "internet"]);
|
|
1335
|
-
const sbx = await
|
|
1575
|
+
const args = parse(rest, ["no-internet", "internet"], ["allow", "deny", "connect"]);
|
|
1576
|
+
const sbx = await use(args.positional[0]);
|
|
1336
1577
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
1337
1578
|
const policy = has(args, "no-internet") || has(args, "internet") || rules.length
|
|
1338
1579
|
? await sbx.network.set({
|
|
@@ -1342,7 +1583,10 @@ async function sandbox(argv, env, out) {
|
|
|
1342
1583
|
: await sbx.network.get();
|
|
1343
1584
|
print(table([
|
|
1344
1585
|
["internet", policy.internet ? "on" : "off"],
|
|
1345
|
-
[
|
|
1586
|
+
[
|
|
1587
|
+
"allow",
|
|
1588
|
+
policy.allow.join(", ") || (policy.internet ? "(the public web)" : "(nothing)"),
|
|
1589
|
+
],
|
|
1346
1590
|
["deny", policy.deny.join(", ") || "-"],
|
|
1347
1591
|
["connect", policy.connect.join(", ") || "-"],
|
|
1348
1592
|
["enforced", policy.enforced ? "yes" : "not yet; run the same command again"],
|
|
@@ -1350,9 +1594,9 @@ async function sandbox(argv, env, out) {
|
|
|
1350
1594
|
return 0;
|
|
1351
1595
|
}
|
|
1352
1596
|
case "desktop":
|
|
1353
|
-
return desktop(await
|
|
1597
|
+
return desktop(await use(rest[0]), rest.slice(1), out);
|
|
1354
1598
|
case "watch":
|
|
1355
|
-
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);
|
|
1356
1600
|
case "mcp":
|
|
1357
1601
|
if (rest[0] === "catalog" || rest[0] === undefined) {
|
|
1358
1602
|
const catalog = await runtime.mcp.catalog();
|
|
@@ -1371,10 +1615,10 @@ async function sandbox(argv, env, out) {
|
|
|
1371
1615
|
]), catalog);
|
|
1372
1616
|
return 0;
|
|
1373
1617
|
}
|
|
1374
|
-
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);
|
|
1375
1619
|
case "metrics": {
|
|
1376
|
-
const args = parse(rest);
|
|
1377
|
-
const sbx = await
|
|
1620
|
+
const args = parse(rest, [], ["range"]);
|
|
1621
|
+
const sbx = await use(args.positional[0]);
|
|
1378
1622
|
const range = (flag(args, "range") ?? "1h");
|
|
1379
1623
|
if (!["15m", "1h", "6h", "24h", "7d", "30d"].includes(range))
|
|
1380
1624
|
throw usage("--range is one of 15m, 1h, 6h, 24h, 7d, 30d.");
|
|
@@ -1436,20 +1680,49 @@ async function readSecret() {
|
|
|
1436
1680
|
stdin.pause();
|
|
1437
1681
|
}
|
|
1438
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
|
+
: "-";
|
|
1439
1711
|
export async function secretsCommand(argv, env, out,
|
|
1440
1712
|
/** Tests give the value here; the CLI reads standard input. */
|
|
1441
1713
|
read = readSecret) {
|
|
1442
1714
|
const [verb, ...rest] = argv;
|
|
1443
|
-
const args = parse(rest);
|
|
1715
|
+
const args = parse(rest, [], ["host", "header", "format", "allow", "value"]);
|
|
1444
1716
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1445
1717
|
const rows = (list) => list.length
|
|
1446
1718
|
? table([
|
|
1447
|
-
["name", "hosts", "header", "placeholder"],
|
|
1719
|
+
["name", "hosts", "header", "placeholder", "allow"],
|
|
1448
1720
|
...list.map((secret) => [
|
|
1449
1721
|
secret.name,
|
|
1450
1722
|
secret.hosts.join(","),
|
|
1451
1723
|
secret.header ? `${secret.header}: ${secret.format ?? "{value}"}` : "-",
|
|
1452
1724
|
secret.placeholder,
|
|
1725
|
+
ruleText(secret.rules),
|
|
1453
1726
|
]),
|
|
1454
1727
|
])
|
|
1455
1728
|
: "No secrets.";
|
|
@@ -1470,17 +1743,21 @@ read = readSecret) {
|
|
|
1470
1743
|
throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
|
|
1471
1744
|
const header = flag(args, "header");
|
|
1472
1745
|
const format = flag(args, "format");
|
|
1746
|
+
const allow = args.flags.get("allow");
|
|
1747
|
+
const rules = allow ? parseSecretRules(allow) : undefined;
|
|
1473
1748
|
const saved = await (await client(env)).secrets.set(name, {
|
|
1474
1749
|
value,
|
|
1475
1750
|
hosts,
|
|
1476
1751
|
...(header ? { header } : {}),
|
|
1477
1752
|
...(format ? { format } : {}),
|
|
1753
|
+
...(rules ? { rules } : {}),
|
|
1478
1754
|
});
|
|
1479
1755
|
print([
|
|
1480
1756
|
`Stored ${saved.name}. Sandboxes see ${saved.name}=${saved.placeholder}.`,
|
|
1481
1757
|
saved.header
|
|
1482
1758
|
? `Requests to ${saved.hosts.join(", ")} get ${saved.header}: ${saved.format ?? "{value}"} over HTTPS.`
|
|
1483
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)}.`] : []),
|
|
1484
1761
|
...(saved.enforced
|
|
1485
1762
|
? []
|
|
1486
1763
|
: ["A host is catching up; running sandboxes have it within a minute."]),
|
|
@@ -1504,7 +1781,7 @@ async function readInput(source) {
|
|
|
1504
1781
|
return Buffer.concat(chunks).toString("utf8");
|
|
1505
1782
|
}
|
|
1506
1783
|
async function runCode(sbx, argv, out) {
|
|
1507
|
-
const args = parse(argv);
|
|
1784
|
+
const args = parse(argv, [], ["lang", "context", "timeout", "out-dir"]);
|
|
1508
1785
|
const source = need(args.positional[0], "a file, or - to read the code from standard input");
|
|
1509
1786
|
const code = await readInput(source);
|
|
1510
1787
|
const byExtension = {
|
|
@@ -1572,7 +1849,7 @@ async function desktop(sbx, argv, out) {
|
|
|
1572
1849
|
const d = sbx.desktop;
|
|
1573
1850
|
switch (action) {
|
|
1574
1851
|
case "start": {
|
|
1575
|
-
const args = parse(rest);
|
|
1852
|
+
const args = parse(rest, [], ["width", "height"]);
|
|
1576
1853
|
const started = await d.start({
|
|
1577
1854
|
...(integer(args, "width") ? { width: integer(args, "width") } : {}),
|
|
1578
1855
|
...(integer(args, "height") ? { height: integer(args, "height") } : {}),
|
|
@@ -1638,6 +1915,7 @@ async function accountCommand(argv, env, out) {
|
|
|
1638
1915
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1639
1916
|
const store = connectionStore(env);
|
|
1640
1917
|
if (verb === "add") {
|
|
1918
|
+
const { authenticationCommand } = await import("./login.js");
|
|
1641
1919
|
const result = await authenticationCommand(["login", "--another-account", ...rest], env);
|
|
1642
1920
|
if (result.pending)
|
|
1643
1921
|
throw notApprovedYet(result);
|
|
@@ -1660,8 +1938,35 @@ async function accountCommand(argv, env, out) {
|
|
|
1660
1938
|
});
|
|
1661
1939
|
return 0;
|
|
1662
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
|
+
}
|
|
1663
1968
|
if (verb !== "ls" && verb !== "list")
|
|
1664
|
-
throw unknown("account command", verb, ["ls", "switch", "add"]);
|
|
1969
|
+
throw unknown("account command", verb, ["ls", "switch", "add", "close"]);
|
|
1665
1970
|
await nameSaved(env).catch(() => undefined);
|
|
1666
1971
|
const current = await store.read().catch(() => null);
|
|
1667
1972
|
const saved = [
|
|
@@ -1717,7 +2022,7 @@ async function keys(argv, env, out) {
|
|
|
1717
2022
|
throw usage("List and revoke keys at https://withruntime.com/account/keys. No key can list or revoke keys.");
|
|
1718
2023
|
if (verb !== "create")
|
|
1719
2024
|
throw unknown("keys command", verb, ["create"]);
|
|
1720
|
-
const args = parse(rest, ["read-only"]);
|
|
2025
|
+
const args = parse(rest, ["read-only"], ["name", "daily-limit"]);
|
|
1721
2026
|
const known = new Set(["name", "read-only", "daily-limit"]);
|
|
1722
2027
|
for (const name of args.flags.keys())
|
|
1723
2028
|
if (!known.has(name))
|
|
@@ -1728,6 +2033,7 @@ async function keys(argv, env, out) {
|
|
|
1728
2033
|
const limit = flag(args, "daily-limit");
|
|
1729
2034
|
if (readOnly && limit !== undefined)
|
|
1730
2035
|
throw usage("A read-only key cannot spend, so it takes no --daily-limit.");
|
|
2036
|
+
const { createKey, dailyLimitMicros } = await import("./keys.js");
|
|
1731
2037
|
const created = await createKey({
|
|
1732
2038
|
name: flag(args, "name") ?? (readOnly ? "Read-only key" : "My API key"),
|
|
1733
2039
|
access: readOnly ? "read" : "full",
|
|
@@ -1754,8 +2060,8 @@ const SNAPSHOT_HELP = `runtime snapshot <command>
|
|
|
1754
2060
|
get <id> One snapshot
|
|
1755
2061
|
extend <id> <days> Keep it longer
|
|
1756
2062
|
rm <id> Delete it
|
|
1757
|
-
Take one with \`runtime sandbox snapshot <sandboxId>\`; start from
|
|
1758
|
-
\`runtime sandbox
|
|
2063
|
+
Take one with \`runtime sandbox snapshot <sandboxId>\`; start a sandbox from it
|
|
2064
|
+
with \`runtime sandbox create --snapshot <snapshotId>\`.
|
|
1759
2065
|
`;
|
|
1760
2066
|
async function snapshot(argv, env, out) {
|
|
1761
2067
|
const [verb, ...rest] = argv;
|
|
@@ -1768,7 +2074,7 @@ async function snapshot(argv, env, out) {
|
|
|
1768
2074
|
switch (verb) {
|
|
1769
2075
|
case "ls":
|
|
1770
2076
|
case "list": {
|
|
1771
|
-
const args = parse(rest);
|
|
2077
|
+
const args = parse(rest, [], ["sandbox", "name"]);
|
|
1772
2078
|
const all = await (await api.list({
|
|
1773
2079
|
...(flag(args, "sandbox")
|
|
1774
2080
|
? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
|
|
@@ -1785,11 +2091,11 @@ async function snapshot(argv, env, out) {
|
|
|
1785
2091
|
}
|
|
1786
2092
|
case "get": {
|
|
1787
2093
|
const found = await api.get(need(rest[0], "a snapshot id"));
|
|
1788
|
-
print(
|
|
2094
|
+
print((await import("./cli-extras.js")).describeSnapshot(found, table), found);
|
|
1789
2095
|
return 0;
|
|
1790
2096
|
}
|
|
1791
2097
|
case "extend": {
|
|
1792
|
-
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"));
|
|
1793
2099
|
const found = await api.extend(need(rest[0], "a snapshot id"), days);
|
|
1794
2100
|
print(`${found.id} kept until ${found.expiresAt}`, found);
|
|
1795
2101
|
return 0;
|
|
@@ -1809,8 +2115,10 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
1809
2115
|
build [<folder>] [-f <Dockerfile>] | --from <image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...
|
|
1810
2116
|
[-t name[:tag]]... [--target <stage>] [--build-arg K=V]... [--no-cache]
|
|
1811
2117
|
[--start <command>] [--ready-port <port> | --ready-command <command>]
|
|
1812
|
-
[--max-image-mib 8192] [--timeout <seconds>]
|
|
2118
|
+
[--max-image-mib 8192] [--disk-mib <1024-32768>] [--timeout <seconds>]
|
|
1813
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.
|
|
1814
2122
|
A Dockerfile's folder is the context: everything its
|
|
1815
2123
|
.dockerignore leaves in (100 MiB compressed at most) is
|
|
1816
2124
|
sent, and a rebuild sends only what changed.
|
|
@@ -1831,6 +2139,28 @@ const IMAGE_HELP = `runtime image <command>
|
|
|
1831
2139
|
registry rm <registry>
|
|
1832
2140
|
Use one: runtime sandbox create --image app:latest
|
|
1833
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
|
+
];
|
|
1834
2164
|
export async function imageCommand(argv, env, out,
|
|
1835
2165
|
/** Tests give the registry secret here; the CLI reads standard input. */
|
|
1836
2166
|
read = readSecret) {
|
|
@@ -1843,7 +2173,7 @@ read = readSecret) {
|
|
|
1843
2173
|
const api = (await client(env)).images;
|
|
1844
2174
|
switch (verb) {
|
|
1845
2175
|
case "build": {
|
|
1846
|
-
const args = parse(rest, ["no-cache"]);
|
|
2176
|
+
const args = parse(rest, ["no-cache"], IMAGE_BUILD_VALUES);
|
|
1847
2177
|
const list = (name) => (args.flags.get(name) ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean);
|
|
1848
2178
|
const recipe = { apt: list("apt"), pip: list("pip"), npm: list("npm") };
|
|
1849
2179
|
const hasRecipe = recipe.apt.length + recipe.pip.length + recipe.npm.length > 0;
|
|
@@ -1855,6 +2185,7 @@ read = readSecret) {
|
|
|
1855
2185
|
throw usage("Give exactly one of a folder with a Dockerfile (or -f <Dockerfile>), --from <image>, or --pip/--apt/--npm.");
|
|
1856
2186
|
const build = {
|
|
1857
2187
|
...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
|
|
2188
|
+
...(integer(args, "disk-mib") ? { diskMiB: integer(args, "disk-mib") } : {}),
|
|
1858
2189
|
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
1859
2190
|
};
|
|
1860
2191
|
// -t app, -t app:v2 -t app:latest: one name, its tags.
|
|
@@ -1944,7 +2275,7 @@ read = readSecret) {
|
|
|
1944
2275
|
return 0;
|
|
1945
2276
|
}
|
|
1946
2277
|
case "logs": {
|
|
1947
|
-
const args = parse(rest, ["follow", "f"]);
|
|
2278
|
+
const args = parse(rest, ["follow", "f"], []);
|
|
1948
2279
|
const id = (await api.resolve(need(args.positional[0], "an image"))).id;
|
|
1949
2280
|
if (has(args, "follow") || has(args, "f")) {
|
|
1950
2281
|
const lines = [];
|
|
@@ -1993,7 +2324,7 @@ read = readSecret) {
|
|
|
1993
2324
|
return 0;
|
|
1994
2325
|
}
|
|
1995
2326
|
if (action === "set") {
|
|
1996
|
-
const args = parse(more);
|
|
2327
|
+
const args = parse(more, [], ["username", "access-key-id"]);
|
|
1997
2328
|
const registry = need(args.positional[0], "a registry, such as ghcr.io");
|
|
1998
2329
|
const accessKeyId = flag(args, "access-key-id");
|
|
1999
2330
|
if (accessKeyId === undefined && flag(args, "username") === undefined)
|
|
@@ -2055,7 +2386,7 @@ async function volume(argv, env, out) {
|
|
|
2055
2386
|
const api = (await client(env)).volumes;
|
|
2056
2387
|
switch (verb) {
|
|
2057
2388
|
case "create": {
|
|
2058
|
-
const args = parse(rest);
|
|
2389
|
+
const args = parse(rest, [], ["size-mib", "name"]);
|
|
2059
2390
|
const created = await api.create({
|
|
2060
2391
|
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
2061
2392
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
@@ -2093,7 +2424,7 @@ async function volume(argv, env, out) {
|
|
|
2093
2424
|
return 0;
|
|
2094
2425
|
}
|
|
2095
2426
|
case "backup": {
|
|
2096
|
-
const args = parse(rest, ["no-wait"]);
|
|
2427
|
+
const args = parse(rest, ["no-wait"], ["retention-days"]);
|
|
2097
2428
|
const id = need(args.positional[0], "a volume id");
|
|
2098
2429
|
const days = integer(args, "retention-days");
|
|
2099
2430
|
const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
|
|
@@ -2123,7 +2454,7 @@ async function volume(argv, env, out) {
|
|
|
2123
2454
|
return 0;
|
|
2124
2455
|
}
|
|
2125
2456
|
case "restore": {
|
|
2126
|
-
const args = parse(rest);
|
|
2457
|
+
const args = parse(rest, [], ["name"]);
|
|
2127
2458
|
const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
|
|
2128
2459
|
if (!out.json && created.state === "creating")
|
|
2129
2460
|
out.error(`Restoring; check with: ${me} volume get ${created.id}`);
|
|
@@ -2137,7 +2468,7 @@ async function volume(argv, env, out) {
|
|
|
2137
2468
|
return 0;
|
|
2138
2469
|
}
|
|
2139
2470
|
case "backup-policy": {
|
|
2140
|
-
const args = parse(rest);
|
|
2471
|
+
const args = parse(rest, [], ["daily", "retention-days"]);
|
|
2141
2472
|
const id = need(args.positional[0], "a volume id");
|
|
2142
2473
|
const daily = flag(args, "daily");
|
|
2143
2474
|
if (daily !== undefined && !["on", "off", "true", "false"].includes(daily))
|
|
@@ -2175,6 +2506,49 @@ const CREATE_FLAGS = [
|
|
|
2175
2506
|
"persistent",
|
|
2176
2507
|
"get-or-create",
|
|
2177
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
|
+
}
|
|
2178
2552
|
/** A create body from the command line's flags. */
|
|
2179
2553
|
function createInput(args) {
|
|
2180
2554
|
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
@@ -2195,9 +2569,16 @@ function createInput(args) {
|
|
|
2195
2569
|
...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
|
|
2196
2570
|
...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
|
|
2197
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
|
+
: {}),
|
|
2198
2579
|
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
2199
2580
|
...(flag(args, "on-timeout")
|
|
2200
|
-
? { onLeaseEnd:
|
|
2581
|
+
? { onLeaseEnd: oneOf(args, "on-timeout", ["pause", "stop"]) }
|
|
2201
2582
|
: {}),
|
|
2202
2583
|
...(integer(args, "idle-pause") !== undefined
|
|
2203
2584
|
? { idlePauseSeconds: integer(args, "idle-pause") }
|
|
@@ -2215,7 +2596,9 @@ function createInput(args) {
|
|
|
2215
2596
|
}
|
|
2216
2597
|
/** Runs a command in a sandbox, streaming its output, and answers its exit code
|
|
2217
2598
|
* (124 on a timeout), so a script's `set -e` behaves. */
|
|
2218
|
-
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) {
|
|
2219
2602
|
const target = command.length === 1 ? command[0] : command;
|
|
2220
2603
|
const extras = await import("./cli-extras.js");
|
|
2221
2604
|
const stdin = await extras.pipedStdin();
|
|
@@ -2225,13 +2608,32 @@ async function execute(sbx, command, args, out, timeout) {
|
|
|
2225
2608
|
...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
|
|
2226
2609
|
...(timeout ? { timeoutMs: timeout * 1000 } : {}),
|
|
2227
2610
|
};
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
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
|
+
}
|
|
2235
2637
|
if (out.json)
|
|
2236
2638
|
out.write(JSON.stringify(result));
|
|
2237
2639
|
else if (result.timedOut)
|
|
@@ -2253,13 +2655,12 @@ async function shell(sbx, command) {
|
|
|
2253
2655
|
});
|
|
2254
2656
|
if (stdin.isTTY)
|
|
2255
2657
|
stdin.setRawMode(true);
|
|
2658
|
+
const unhook = (await import("./cli-extras.js")).pipeShellInput(stdin, terminal);
|
|
2256
2659
|
stdin.resume();
|
|
2257
|
-
const onInput = (chunk) => terminal.write(new Uint8Array(chunk));
|
|
2258
2660
|
const onResize = () => terminal.resize(size().cols, size().rows);
|
|
2259
|
-
stdin.on("data", onInput);
|
|
2260
2661
|
process.stdout.on("resize", onResize);
|
|
2261
2662
|
const code = await terminal.exited;
|
|
2262
|
-
|
|
2663
|
+
unhook();
|
|
2263
2664
|
process.stdout.off("resize", onResize);
|
|
2264
2665
|
if (stdin.isTTY)
|
|
2265
2666
|
stdin.setRawMode(false);
|
|
@@ -2289,7 +2690,13 @@ export function describeError(error, json) {
|
|
|
2289
2690
|
details: error.details,
|
|
2290
2691
|
},
|
|
2291
2692
|
});
|
|
2292
|
-
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
|
+
: ""}`;
|
|
2293
2700
|
}
|
|
2294
2701
|
const message = error instanceof Error ? error.message : String(error);
|
|
2295
2702
|
return json ? JSON.stringify({ error: { code: "cli_error", message } }) : `Error: ${message}`;
|
|
@@ -2367,7 +2774,7 @@ async function webhooksCommand(argv, env, out) {
|
|
|
2367
2774
|
return 0;
|
|
2368
2775
|
}
|
|
2369
2776
|
const hooks = (await client(env)).webhooks;
|
|
2370
|
-
const args = parse(rest, ["enable", "disable"]);
|
|
2777
|
+
const args = parse(rest, ["enable", "disable"], ["events", "description", "url", "keep", "limit"]);
|
|
2371
2778
|
const id = () => need(args.positional[0], "a webhook id");
|
|
2372
2779
|
const events = () => flag(args, "events")
|
|
2373
2780
|
?.split(",")
|
|
@@ -2463,7 +2870,7 @@ async function otelCommand(argv, env, out) {
|
|
|
2463
2870
|
product = "otel";
|
|
2464
2871
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
2465
2872
|
const otel = (await client(env)).otel;
|
|
2466
|
-
const args = parse(rest, ["enable", "disable"]);
|
|
2873
|
+
const args = parse(rest, ["enable", "disable"], ["endpoint", "header", "signals"]);
|
|
2467
2874
|
const id = () => need(args.positional[0], "an export id");
|
|
2468
2875
|
const signals = () => flag(args, "signals")
|
|
2469
2876
|
?.split(",")
|