withruntime 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DAYTONA.md +25 -25
- package/E2B.md +1 -1
- package/LICENSE +55 -0
- package/README.md +3 -2
- package/dist/cli-extras.d.ts +24 -0
- package/dist/cli-extras.js +205 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +430 -37
- package/dist/daytona/context.d.ts +5 -0
- package/dist/daytona/context.js +3 -0
- package/dist/daytona/filesystem.js +2 -2
- package/dist/daytona/process.js +3 -1
- package/dist/e2b/commands.js +6 -2
- package/dist/e2b/filesystem.d.ts +26 -1
- package/dist/e2b/filesystem.js +30 -2
- package/dist/e2b/index.d.ts +1 -1
- package/dist/e2b/index.js +1 -1
- package/dist/errors.js +2 -0
- package/dist/identity.d.ts +27 -0
- package/dist/identity.js +56 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/mcp.js +16 -3
- package/dist/network-cli.d.ts +15 -0
- package/dist/network-cli.js +323 -0
- package/dist/products/desktop.d.ts +41 -4
- package/dist/products/desktop.js +73 -3
- package/dist/products/index.d.ts +12 -0
- package/dist/products/index.js +12 -0
- package/dist/products/interpreter.d.ts +5 -3
- package/dist/products/interpreter.js +2 -1
- package/dist/products/mcp.d.ts +79 -0
- package/dist/products/mcp.js +37 -0
- package/dist/products/mounts.d.ts +49 -0
- package/dist/products/mounts.js +21 -0
- package/dist/products/network-products.d.ts +167 -0
- package/dist/products/network-products.js +82 -0
- package/dist/products/network.d.ts +1 -1
- package/dist/products/sso.d.ts +44 -0
- package/dist/products/sso.js +8 -0
- package/dist/products/switching.d.ts +7 -0
- package/dist/products/volumes.d.ts +68 -5
- package/dist/products/volumes.js +52 -4
- package/dist/products/watch.d.ts +91 -0
- package/dist/products/watch.js +135 -0
- package/dist/sandbox.d.ts +32 -2
- package/dist/sandbox.js +93 -16
- package/dist/snapshots.d.ts +11 -1
- package/dist/ssh.js +13 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +4 -1
- package/dist/tunnel.d.ts +4 -1
- package/dist/tunnel.js +8 -3
- package/dist/wireguard.d.ts +7 -0
- package/dist/wireguard.js +13 -0
- package/package.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { connectionStore, resolveCredential } from "./credentials.js";
|
|
@@ -8,8 +8,10 @@ import { authenticationCommand, me, named } from "./login.js";
|
|
|
8
8
|
import { createKey, dailyLimitMicros } from "./keys.js";
|
|
9
9
|
import { Runtime } from "./client.js";
|
|
10
10
|
import { RuntimeError } from "./errors.js";
|
|
11
|
+
import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
|
|
11
12
|
import { envFetch } from "./proxy.js";
|
|
12
|
-
import {
|
|
13
|
+
import { identityToken } from "./identity.js";
|
|
14
|
+
import { portForwardCommand, resolveSandbox, sshCommand } from "./ssh.js";
|
|
13
15
|
import { VERSION } from "./transport.js";
|
|
14
16
|
const HELP = `Runtime Cloud CLI ${VERSION}
|
|
15
17
|
|
|
@@ -30,6 +32,8 @@ Account
|
|
|
30
32
|
account add Connect another account (choose it in the browser)
|
|
31
33
|
audit [--action member.] [--limit 50] [--before <next>]
|
|
32
34
|
The account's audit log (owner or admin keys)
|
|
35
|
+
sso Single sign-on and SCIM directory sync (owner or
|
|
36
|
+
admin keys); an owner changes it on the website
|
|
33
37
|
usage Balance, trial time and charges
|
|
34
38
|
limits Whether this key is read-only, and its daily spending limit
|
|
35
39
|
secrets set <NAME> --host <host>... [--header <name> [--format '<text {value}>']]
|
|
@@ -76,6 +80,10 @@ Products
|
|
|
76
80
|
snapshot Saved sandboxes, disk and memory, to start or fork from
|
|
77
81
|
image Custom images from a Dockerfile, any public or private image, or a package list
|
|
78
82
|
volume Persistent disks to attach to sandboxes
|
|
83
|
+
domain Your own hostname for a sandbox's port, with HTTPS
|
|
84
|
+
port A public TCP port to a sandbox: databases, game servers
|
|
85
|
+
address A dedicated outbound address, for allow-lists
|
|
86
|
+
tunnel A WireGuard tunnel from your own network into your sandboxes
|
|
79
87
|
Each has its own help: \`runtime sandbox help\`, \`runtime image help\`...
|
|
80
88
|
|
|
81
89
|
Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
|
|
@@ -83,6 +91,8 @@ Behind a proxy: HTTPS_PROXY and NO_PROXY are read, on Node and Bun alike.
|
|
|
83
91
|
`;
|
|
84
92
|
const SANDBOX_HELP = `runtime sandbox <command>
|
|
85
93
|
|
|
94
|
+
Every <id> below can also be the sandbox's name.
|
|
95
|
+
|
|
86
96
|
run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
|
|
87
97
|
ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
|
|
88
98
|
port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
|
|
@@ -98,7 +108,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
98
108
|
Run and stream output; exits with its code
|
|
99
109
|
spawn <id> [--pty] -- <command...> Start in the background; prints a process id
|
|
100
110
|
ps <id> Running and recent processes
|
|
101
|
-
logs <id> <processId> [-f] A process's output
|
|
111
|
+
logs <id> <processId> [-f] A process's output so far; -f follows it to the end
|
|
102
112
|
kill <id> <processId> [--signal SIGTERM]
|
|
103
113
|
shell <id> [--command "bash -l"] An interactive terminal
|
|
104
114
|
cp <src> <dst> Copy files or directories; name a sandbox
|
|
@@ -111,19 +121,35 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
111
121
|
[--persistent on|off] [--max-total-cost <usd>|none]
|
|
112
122
|
Change its settings; persistent keeps it running
|
|
113
123
|
snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
|
|
124
|
+
mount <id> <s3|r2|gcs>://<bucket>[/prefix] <path> [--secret NAME] [--region r]
|
|
125
|
+
[--endpoint https://...] [--account-id id] [--read-only]
|
|
126
|
+
Your bucket as a directory; the proxy signs, the sandbox never sees the key
|
|
127
|
+
mounts <id> | unmount <id> <path> Its bucket mounts; unmount one
|
|
114
128
|
fork <id> [--count 3] [--name n] [--keep-snapshot] [--trial|--paid]
|
|
115
129
|
Copies of it as it is now, running; prints their ids
|
|
116
|
-
run-code <id> <file|-> [--lang python|javascript] [--context c] [--out-dir .]
|
|
130
|
+
run-code <id> <file|-> [--lang python|javascript|typescript|r|java|bash|go] [--context c] [--out-dir .]
|
|
117
131
|
Run a notebook-style cell; charts saved as PNG
|
|
132
|
+
watch <id> <path> [--recursive] [--include <glob>]... [--exclude <glob>]... [--events create,write,remove,rename,chmod]
|
|
133
|
+
Print file changes as they happen, until Ctrl-C
|
|
134
|
+
mcp catalog MCP servers a sandbox can run (licence, settings, hosts)
|
|
135
|
+
mcp <id> start <server>... [--secret SERVER.SETTING=SECRET]... [--env SERVER.NAME=value]...
|
|
136
|
+
[--option SERVER.NAME=value]... [--port 8765] [--replace]
|
|
137
|
+
Start them; prints each URL and the header to send
|
|
138
|
+
mcp <id> [status] | mcp <id> stop Their state and fresh URLs; stop them
|
|
118
139
|
preview <id> <port> [--public] [--ttl <s>] Share a port at an HTTPS address
|
|
119
140
|
previews <id> Every shared port
|
|
120
141
|
unshare <id> <port> Stop sharing a port
|
|
121
142
|
network <id> [--no-internet|--internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
122
143
|
Show its network rules, or replace them
|
|
144
|
+
identity-token --audience <aud> [--lifetime <seconds>]
|
|
145
|
+
Inside a sandbox: an OIDC token naming it, for AWS,
|
|
146
|
+
Google Cloud or your own API. Needs no API key
|
|
123
147
|
metrics <id> [--range 15m|1h|6h|24h|7d|30d]
|
|
124
148
|
CPU and memory over time, and the latest reading
|
|
125
149
|
desktop <id> start | stop | screenshot [file.png] | open <url> | click <x> <y>
|
|
126
150
|
| type <text> | press <keys> | windows
|
|
151
|
+
desktop <id> record start [--fps 10] [--max-seconds 1800] [--max-mib 512] | record stop <recId>
|
|
152
|
+
| record ls | record fetch <recId> [file.mp4] | record rm <recId>
|
|
127
153
|
|
|
128
154
|
Examples
|
|
129
155
|
id=$(runtime sandbox create)
|
|
@@ -169,6 +195,17 @@ const pairs = (args, name) => Object.fromEntries((args.flags.get(name) ?? []).ma
|
|
|
169
195
|
throw usage(`--${name} takes KEY=VALUE.`);
|
|
170
196
|
return [pair.slice(0, at), pair.slice(at + 1)];
|
|
171
197
|
}));
|
|
198
|
+
/** What the commands in cli-extras.ts use of this file. */
|
|
199
|
+
const kit = {
|
|
200
|
+
parse: (argv, booleans) => parse(argv, booleans),
|
|
201
|
+
flag,
|
|
202
|
+
has,
|
|
203
|
+
integer,
|
|
204
|
+
pairs,
|
|
205
|
+
need: (value, what) => need(value, what),
|
|
206
|
+
table: (rows) => table(rows),
|
|
207
|
+
usage: (message) => usage(message),
|
|
208
|
+
};
|
|
172
209
|
/** The product being run, so a mistake points at its own help. */
|
|
173
210
|
let product = "";
|
|
174
211
|
function usage(message) {
|
|
@@ -206,6 +243,7 @@ const COMMANDS = [
|
|
|
206
243
|
"whoami",
|
|
207
244
|
"usage",
|
|
208
245
|
"limits",
|
|
246
|
+
"sso",
|
|
209
247
|
"secrets",
|
|
210
248
|
"keys",
|
|
211
249
|
"referrals",
|
|
@@ -220,6 +258,10 @@ const COMMANDS = [
|
|
|
220
258
|
"snapshot",
|
|
221
259
|
"image",
|
|
222
260
|
"volume",
|
|
261
|
+
"domain",
|
|
262
|
+
"port",
|
|
263
|
+
"address",
|
|
264
|
+
"tunnel",
|
|
223
265
|
"webhooks",
|
|
224
266
|
"otel",
|
|
225
267
|
"events",
|
|
@@ -240,6 +282,9 @@ const SANDBOX_VERBS = [
|
|
|
240
282
|
"wake",
|
|
241
283
|
"restart",
|
|
242
284
|
"snapshot",
|
|
285
|
+
"mount",
|
|
286
|
+
"mounts",
|
|
287
|
+
"unmount",
|
|
243
288
|
"fork",
|
|
244
289
|
"extend",
|
|
245
290
|
"update",
|
|
@@ -259,6 +304,9 @@ const SANDBOX_VERBS = [
|
|
|
259
304
|
"network",
|
|
260
305
|
"desktop",
|
|
261
306
|
"metrics",
|
|
307
|
+
"watch",
|
|
308
|
+
"mcp",
|
|
309
|
+
"identity-token",
|
|
262
310
|
];
|
|
263
311
|
function need(value, what) {
|
|
264
312
|
if (!value)
|
|
@@ -454,7 +502,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
454
502
|
}
|
|
455
503
|
if (command === "usage") {
|
|
456
504
|
const value = await (await client(env)).usage();
|
|
457
|
-
print(
|
|
505
|
+
print(usageSummary(value), value);
|
|
458
506
|
return 0;
|
|
459
507
|
}
|
|
460
508
|
if (command === "limits") {
|
|
@@ -486,8 +534,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
486
534
|
return otelCommand(rest, env, out);
|
|
487
535
|
if (command === "events") {
|
|
488
536
|
const args = parse(rest);
|
|
489
|
-
const
|
|
490
|
-
|
|
537
|
+
const rt = await client(env);
|
|
538
|
+
const sandbox = flag(args, "sandbox");
|
|
539
|
+
const page = await rt.events.list({
|
|
540
|
+
...(sandbox ? { resourceId: await sandboxIdOf(rt, sandbox) } : {}),
|
|
491
541
|
...(flag(args, "type") ? { type: flag(args, "type") } : {}),
|
|
492
542
|
...(integer(args, "limit") ? { limit: integer(args, "limit") } : {}),
|
|
493
543
|
});
|
|
@@ -501,6 +551,29 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
501
551
|
}
|
|
502
552
|
if (command === "keys")
|
|
503
553
|
return keys(rest, env, out);
|
|
554
|
+
if (command === "sso") {
|
|
555
|
+
const status = await (await client(env)).sso.get();
|
|
556
|
+
print([
|
|
557
|
+
status.connections.length
|
|
558
|
+
? table([
|
|
559
|
+
["domain", "provider", "verified", "required", "new people join as"],
|
|
560
|
+
...status.connections.map((c) => [
|
|
561
|
+
c.domain,
|
|
562
|
+
`${c.provider} ${c.protocol.toUpperCase()}`,
|
|
563
|
+
c.domainVerifiedAt
|
|
564
|
+
? c.domainVerifiedAt.slice(0, 10)
|
|
565
|
+
: `no: TXT ${c.verification.name}`,
|
|
566
|
+
c.requireSso ? "yes" : "no",
|
|
567
|
+
c.defaultRole,
|
|
568
|
+
]),
|
|
569
|
+
])
|
|
570
|
+
: "No single sign-on yet.",
|
|
571
|
+
"",
|
|
572
|
+
`Directory sync (SCIM): ${status.scim.tokens} token${status.scim.tokens === 1 ? "" : "s"}, ${status.scim.activeUsers} of ${status.scim.users} people active, ${status.scim.groups.length} groups.`,
|
|
573
|
+
`An owner changes it at ${status.manage}`,
|
|
574
|
+
].join("\n"), status);
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
504
577
|
if (command === "account" || command === "accounts")
|
|
505
578
|
return accountCommand(rest, env, out);
|
|
506
579
|
if (command === "audit") {
|
|
@@ -558,7 +631,11 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
558
631
|
if (command === "mcp") {
|
|
559
632
|
const { serveMcp } = await import("./mcp.js");
|
|
560
633
|
await serveMcp(env);
|
|
561
|
-
|
|
634
|
+
/* Its input closed or a signal came: leave once stdout has drained. An
|
|
635
|
+
idle keep-alive connection to the API would otherwise hold the process
|
|
636
|
+
for several seconds after the client asked it to go. */
|
|
637
|
+
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
638
|
+
process.exit(0);
|
|
562
639
|
}
|
|
563
640
|
if (command === "docs") {
|
|
564
641
|
const topic = rest[0] ?? "index";
|
|
@@ -652,8 +729,101 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
652
729
|
product = "volume";
|
|
653
730
|
return volume(rest, env, out);
|
|
654
731
|
}
|
|
732
|
+
const networkProduct = { domains: "domain", ports: "port", addresses: "address", tunnels: "tunnel" }[command] ??
|
|
733
|
+
command;
|
|
734
|
+
if (NETWORK_PRODUCTS.includes(networkProduct)) {
|
|
735
|
+
product = networkProduct;
|
|
736
|
+
return networkProductCommand(networkProduct, rest, parse(rest, ["ipv6"]), out, () => client(env));
|
|
737
|
+
}
|
|
655
738
|
throw unknown("command", command, COMMANDS);
|
|
656
739
|
}
|
|
740
|
+
/** `runtime usage` for a person: the balance, the trial and what each kind
|
|
741
|
+
* of resource was charged, in dollars. --json keeps every figure, and each
|
|
742
|
+
* resource with its rates and CPU time. */
|
|
743
|
+
function usageSummary(u) {
|
|
744
|
+
const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
|
|
745
|
+
? BigInt(value)
|
|
746
|
+
: 0n;
|
|
747
|
+
const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
748
|
+
const takenBack = micros(u.takenBack);
|
|
749
|
+
const rows = [
|
|
750
|
+
["available", dollars(micros(u.available))],
|
|
751
|
+
["credited", dollars(micros(u.credited))],
|
|
752
|
+
[
|
|
753
|
+
"spent",
|
|
754
|
+
`${dollars(micros(u.spent))}${takenBack > 0n ? `, ${dollars(takenBack)} of it refunded or disputed` : ""}`,
|
|
755
|
+
],
|
|
756
|
+
["held for running work", dollars(micros(u.held))],
|
|
757
|
+
];
|
|
758
|
+
if (micros(u.expired) > 0n)
|
|
759
|
+
rows.push(["expired or taken back", dollars(micros(u.expired))]);
|
|
760
|
+
if (u.trial)
|
|
761
|
+
rows.push([
|
|
762
|
+
"free trial",
|
|
763
|
+
`${hours(u.trial.availableMs)} of ${hours(u.trial.totalMs)} hours left${u.trial.reservedMs > 0 ? `, ${hours(u.trial.reservedMs)} held by running sandboxes` : ""}`,
|
|
764
|
+
]);
|
|
765
|
+
const kinds = new Map();
|
|
766
|
+
for (const resource of u.resources ?? []) {
|
|
767
|
+
const kind = typeof resource.kind === "string" ? resource.kind : "other";
|
|
768
|
+
const entry = kinds.get(kind) ?? { count: 0, charged: 0n, held: 0n };
|
|
769
|
+
entry.count++;
|
|
770
|
+
entry.charged += micros(resource.chargedMicros);
|
|
771
|
+
entry.held += micros(resource.heldMicros);
|
|
772
|
+
kinds.set(kind, entry);
|
|
773
|
+
}
|
|
774
|
+
const plural = (kind, n) => n === 1 ? kind : kind.endsWith("x") || kind.endsWith("s") ? `${kind}es` : `${kind}s`;
|
|
775
|
+
const byKind = [...kinds.entries()].sort((a, b) => b[1].count - a[1].count);
|
|
776
|
+
return [
|
|
777
|
+
table(rows),
|
|
778
|
+
"",
|
|
779
|
+
byKind.length
|
|
780
|
+
? table([
|
|
781
|
+
["RESOURCES", "COUNT", "CHARGED", "HELD"],
|
|
782
|
+
...byKind.map(([kind, e]) => [
|
|
783
|
+
plural(kind, e.count),
|
|
784
|
+
e.count.toLocaleString("en-US"),
|
|
785
|
+
dollars(e.charged),
|
|
786
|
+
dollars(e.held),
|
|
787
|
+
]),
|
|
788
|
+
])
|
|
789
|
+
: "No resources yet.",
|
|
790
|
+
"",
|
|
791
|
+
`Each resource, with its rates and CPU time: ${me} usage --json`,
|
|
792
|
+
].join("\n");
|
|
793
|
+
}
|
|
794
|
+
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
|
+
/** A sandbox's id from its id or its name, for the filters that take an id. */
|
|
796
|
+
async function sandboxIdOf(runtime, target) {
|
|
797
|
+
return SANDBOX_UUID.test(target) ? target : (await resolveSandbox(runtime, target)).id;
|
|
798
|
+
}
|
|
799
|
+
/** What a process has printed so far, in order, without waiting for more. */
|
|
800
|
+
async function outputSoFar(runtime, sandboxId, processId) {
|
|
801
|
+
const chunks = [];
|
|
802
|
+
let cursor = 0;
|
|
803
|
+
let truncated = false;
|
|
804
|
+
let latest = null;
|
|
805
|
+
// The guest keeps the latest 1 MiB; a read returns at most 192 KiB.
|
|
806
|
+
for (let page = 0; page < 16; page++) {
|
|
807
|
+
const read = await runtime.transport.json({
|
|
808
|
+
method: "GET",
|
|
809
|
+
path: `/v1/sandboxes/${encodeURIComponent(sandboxId)}/processes/${encodeURIComponent(processId)}/output`,
|
|
810
|
+
query: { cursor, maxBytes: 196_608 },
|
|
811
|
+
});
|
|
812
|
+
chunks.push(...read.chunks);
|
|
813
|
+
truncated ||= read.truncated;
|
|
814
|
+
latest = read.process ?? latest;
|
|
815
|
+
if (read.chunks.length === 0 || read.nextCursor <= cursor)
|
|
816
|
+
break;
|
|
817
|
+
cursor = read.nextCursor;
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
processId,
|
|
821
|
+
state: latest?.state ?? null,
|
|
822
|
+
exitCode: latest?.exitCode ?? null,
|
|
823
|
+
truncated,
|
|
824
|
+
chunks,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
657
827
|
/** Dollars from integer microdollars: cents from a dollar up, and four
|
|
658
828
|
* places below one, so a few minutes of use does not round to $0.01. */
|
|
659
829
|
function dollars(micros) {
|
|
@@ -684,7 +854,16 @@ const RIVAL_NAMES = {
|
|
|
684
854
|
cloudflare: "Cloudflare Sandbox",
|
|
685
855
|
fly: "Fly",
|
|
686
856
|
};
|
|
687
|
-
/**
|
|
857
|
+
/** A date as the CLI writes one: 23 September 2026. */
|
|
858
|
+
const longDate = (iso) => new Date(iso).toLocaleDateString("en-GB", {
|
|
859
|
+
day: "numeric",
|
|
860
|
+
month: "long",
|
|
861
|
+
year: "numeric",
|
|
862
|
+
timeZone: "UTC",
|
|
863
|
+
});
|
|
864
|
+
/** One line on the switching credit: how to claim it, or what it did. A
|
|
865
|
+
* switch already on record is named with its provider and date, so it never
|
|
866
|
+
* reads as if this command recorded it, or recorded the provider it names. */
|
|
688
867
|
function switchingLine(summary, provider) {
|
|
689
868
|
const cap = dollars(summary.maxMicros);
|
|
690
869
|
const sw = summary.switch;
|
|
@@ -694,21 +873,32 @@ function switchingLine(summary, provider) {
|
|
|
694
873
|
return `Switching from ${RIVAL_NAMES[provider]}? Run \`${me} switch --from ${provider}\` before your first top-up and we match that top-up, up to ${cap}.`;
|
|
695
874
|
}
|
|
696
875
|
const from = RIVAL_NAMES[sw.provider];
|
|
876
|
+
const recorded = `This organization's switch from ${from} was recorded on ${longDate(sw.recordedAt)}`;
|
|
877
|
+
const once = provider && provider !== sw.provider
|
|
878
|
+
? ` A switch is recorded once, so it stays ${from}, not ${RIVAL_NAMES[provider]}.`
|
|
879
|
+
: "";
|
|
697
880
|
switch (sw.status) {
|
|
698
881
|
case "pending":
|
|
699
|
-
return
|
|
882
|
+
return `${recorded}: its first top-up is matched, up to ${cap}, once the payment settles.${once}`;
|
|
700
883
|
case "paid":
|
|
701
|
-
return
|
|
884
|
+
return `${recorded} and matched its first top-up: ${dollars(sw.creditMicros)} of credit.${once}`;
|
|
702
885
|
case "superseded":
|
|
703
|
-
return
|
|
886
|
+
return `${recorded}; its first top-up was matched by a referral instead, which is never less than the switching credit.${once}`;
|
|
704
887
|
case "reversed":
|
|
705
|
-
return
|
|
888
|
+
return `${recorded}; the top-up it matched was refunded, so the credit was taken back.${once}`;
|
|
706
889
|
case "refused":
|
|
707
890
|
return sw.refusedReason === "program_off"
|
|
708
|
-
?
|
|
709
|
-
:
|
|
891
|
+
? `${recorded}; the switching credit was paused when its first top-up arrived.${once}`
|
|
892
|
+
: `${recorded}; its first top-up was not matched. Ask support if you think that is wrong.${once}`;
|
|
710
893
|
}
|
|
711
894
|
}
|
|
895
|
+
/** The sandboxes the free trial paid for, said beside what was priced. */
|
|
896
|
+
function trialNote(trial, all) {
|
|
897
|
+
if (!trial)
|
|
898
|
+
return "";
|
|
899
|
+
const which = trial === all ? (all === 1 ? "It" : "All of them") : `${trial} of them`;
|
|
900
|
+
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
|
+
}
|
|
712
902
|
async function switching(command, argv, env, out) {
|
|
713
903
|
const args = parse(argv);
|
|
714
904
|
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
@@ -749,20 +939,32 @@ async function switching(command, argv, env, out) {
|
|
|
749
939
|
: `${c.rival.name} would cost ${dollars(-saving)} less${percent}`;
|
|
750
940
|
const figures = `${dollars(c.runtimeMicros)} on Runtime; the same sandboxes on ${c.rival.name}: ${dollars(c.rivalMicros)}; ${verdict}`;
|
|
751
941
|
const month = c.perMonth ? BigInt(c.perMonth.savingMicros) : undefined;
|
|
942
|
+
const monthly = month === undefined
|
|
943
|
+
? ""
|
|
944
|
+
: month >= 0n
|
|
945
|
+
? `about ${dollars(month)} a month`
|
|
946
|
+
: `about ${dollars(-month)} a month more on Runtime`;
|
|
947
|
+
/* The month is projected from the days the usage covers, not from the
|
|
948
|
+
window asked for: say which, or a young account's 7 and 90 days read the
|
|
949
|
+
same with nothing to say why. */
|
|
950
|
+
const covered = c.perMonth?.fromDays ?? c.window.days;
|
|
951
|
+
const pace = month === undefined
|
|
952
|
+
? "."
|
|
953
|
+
: covered < c.window.days
|
|
954
|
+
? `. Your first sandbox in this window ran ${covered <= 1 ? "within the last day" : `${covered.toLocaleString("en-US")} days ago`}; at that pace, ${monthly}.`
|
|
955
|
+
: c.window.days === 30
|
|
956
|
+
? `, ${monthly}.`
|
|
957
|
+
: `. At that pace, ${monthly}.`;
|
|
752
958
|
const lines = c.basis === "usage"
|
|
753
959
|
? [
|
|
754
|
-
`Your last ${c.window.days} days: ${figures}${
|
|
755
|
-
|
|
756
|
-
: month >= 0n
|
|
757
|
-
? `, about ${dollars(month)} a month.`
|
|
758
|
-
: `, about ${dollars(-month)} a month more on Runtime.`}`,
|
|
759
|
-
`Priced: ${c.usage.sandboxes.toLocaleString("en-US")} sandbox${c.usage.sandboxes === 1 ? "" : "es"}, ${(c.usage.runSeconds / 3600).toLocaleString("en-US", { maximumFractionDigits: 2 })} hours running.${c.usage.unpricedSandboxes ? ` ${c.usage.unpricedSandboxes} left out: ${c.rival.name} publishes no price for their size.` : ""}`,
|
|
960
|
+
`Your last ${c.window.days} days: ${figures}${pace}`,
|
|
961
|
+
`Priced: ${c.usage.sandboxes.toLocaleString("en-US")} sandbox${c.usage.sandboxes === 1 ? "" : "es"}, ${(c.usage.runSeconds / 3600).toLocaleString("en-US", { maximumFractionDigits: 2 })} hours running.${trialNote(c.usage.trialSandboxes, c.usage.sandboxes)}${c.usage.unpricedSandboxes ? ` ${c.usage.unpricedSandboxes} left out: ${c.rival.name} publishes no price for their size.` : ""}`,
|
|
760
962
|
]
|
|
761
963
|
: [
|
|
762
964
|
`No settled sandboxes in your last ${c.window.days} days yet, so this prices an example: ${c.usage.sandboxes.toLocaleString("en-US")} runs of ${(c.usage.runSeconds / c.usage.sandboxes).toLocaleString("en-US")} seconds using ${(c.usage.activeCpuSeconds / c.usage.sandboxes).toLocaleString("en-US")} CPU-seconds each.`,
|
|
763
965
|
`The example: ${figures}.`,
|
|
764
966
|
];
|
|
765
|
-
lines.push(`${c.rival.name}'s published rates, checked ${
|
|
967
|
+
lines.push(`${c.rival.name}'s published rates, checked ${longDate(`${c.rival.checked}T12:00:00Z`)}: ${c.rival.rates.join("; ")}. Compute only: storage, network, plan fees and free allowances are left out.`);
|
|
766
968
|
const line = switchingLine(c.switching, Object.hasOwn(SWITCH_FROM, from) ? SWITCH_FROM[from] : undefined);
|
|
767
969
|
if (line)
|
|
768
970
|
lines.push("", line);
|
|
@@ -806,8 +1008,22 @@ async function sandbox(argv, env, out) {
|
|
|
806
1008
|
out.write(out.json ? JSON.stringify({ usage: named(SANDBOX_HELP) }) : named(SANDBOX_HELP));
|
|
807
1009
|
return 0;
|
|
808
1010
|
}
|
|
1011
|
+
// Inside a sandbox, with no API key: the sandbox's own request token asks.
|
|
1012
|
+
if (verb === "identity-token") {
|
|
1013
|
+
const args = parse(rest);
|
|
1014
|
+
const audience = need(flag(args, "audience"), "--audience, such as sts.amazonaws.com");
|
|
1015
|
+
const lifetime = flag(args, "lifetime");
|
|
1016
|
+
const token = await identityToken({
|
|
1017
|
+
audience,
|
|
1018
|
+
...(lifetime === undefined ? {} : { lifetimeSeconds: Number(lifetime) }),
|
|
1019
|
+
});
|
|
1020
|
+
print(token.token, token);
|
|
1021
|
+
return 0;
|
|
1022
|
+
}
|
|
809
1023
|
const runtime = await client(env);
|
|
810
|
-
|
|
1024
|
+
/* By id or by name, for every command here, as ssh and port-forward
|
|
1025
|
+
always took one. */
|
|
1026
|
+
const get = (id) => resolveSandbox(runtime, need(id, "a sandbox id or name"));
|
|
811
1027
|
switch (verb) {
|
|
812
1028
|
case "create": {
|
|
813
1029
|
const args = parse(rest, CREATE_FLAGS);
|
|
@@ -868,6 +1084,50 @@ async function sandbox(argv, env, out) {
|
|
|
868
1084
|
print(`${sbx.id} ${sbx.state}`, sbx.info);
|
|
869
1085
|
return 0;
|
|
870
1086
|
}
|
|
1087
|
+
case "mount": {
|
|
1088
|
+
const args = parse(rest, ["read-only"]);
|
|
1089
|
+
const sbx = await get(args.positional[0]);
|
|
1090
|
+
const source = need(args.positional[1], "a bucket, as s3://bucket, r2://bucket or gcs://bucket");
|
|
1091
|
+
const found = /^(s3|r2|gcs):\/\/([^/]+)(?:\/(.+))?$/.exec(source);
|
|
1092
|
+
if (!found)
|
|
1093
|
+
throw usage("The bucket is s3://bucket[/prefix], r2://bucket or gcs://bucket.");
|
|
1094
|
+
const mounted = await sbx.mounts.add({
|
|
1095
|
+
provider: found[1],
|
|
1096
|
+
bucket: found[2],
|
|
1097
|
+
path: need(args.positional[2], "a path to mount it at, such as /data"),
|
|
1098
|
+
...(found[3] ? { prefix: found[3] } : {}),
|
|
1099
|
+
...(flag(args, "secret") ? { secret: flag(args, "secret") } : {}),
|
|
1100
|
+
...(flag(args, "region") ? { region: flag(args, "region") } : {}),
|
|
1101
|
+
...(flag(args, "endpoint") ? { endpoint: flag(args, "endpoint") } : {}),
|
|
1102
|
+
...(flag(args, "account-id") ? { accountId: flag(args, "account-id") } : {}),
|
|
1103
|
+
...(has(args, "read-only") ? { readOnly: true } : {}),
|
|
1104
|
+
});
|
|
1105
|
+
print(`${mounted.path} ${mounted.provider}://${mounted.bucket}`, mounted);
|
|
1106
|
+
return 0;
|
|
1107
|
+
}
|
|
1108
|
+
case "mounts": {
|
|
1109
|
+
const sbx = await get(rest[0]);
|
|
1110
|
+
const all = await sbx.mounts.list();
|
|
1111
|
+
print(all.length
|
|
1112
|
+
? table([
|
|
1113
|
+
["PATH", "BUCKET", "ENDPOINT", "SECRET", "MOUNTED"],
|
|
1114
|
+
...all.map((m) => [
|
|
1115
|
+
m.path,
|
|
1116
|
+
`${m.provider}://${m.bucket}${m.prefix ? `/${m.prefix}` : ""}`,
|
|
1117
|
+
m.endpoint,
|
|
1118
|
+
m.secret ?? "-",
|
|
1119
|
+
m.mounted ? "yes" : "no",
|
|
1120
|
+
]),
|
|
1121
|
+
])
|
|
1122
|
+
: "No buckets mounted.", all);
|
|
1123
|
+
return 0;
|
|
1124
|
+
}
|
|
1125
|
+
case "unmount": {
|
|
1126
|
+
const sbx = await get(rest[0]);
|
|
1127
|
+
const gone = await sbx.mounts.remove(need(rest[1], "the mounted path"));
|
|
1128
|
+
print(`${gone.path} unmounted`, gone);
|
|
1129
|
+
return 0;
|
|
1130
|
+
}
|
|
871
1131
|
case "snapshot": {
|
|
872
1132
|
const args = parse(rest);
|
|
873
1133
|
const sbx = await get(args.positional[0]);
|
|
@@ -972,7 +1232,7 @@ async function sandbox(argv, env, out) {
|
|
|
972
1232
|
const args = parse(rest, ["f", "follow"]);
|
|
973
1233
|
const sbx = await get(args.positional[0]);
|
|
974
1234
|
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
975
|
-
if (has(args, "f") || has(args, "follow")
|
|
1235
|
+
if (has(args, "f") || has(args, "follow")) {
|
|
976
1236
|
for await (const event of proc.output())
|
|
977
1237
|
if (event.type === "stdout")
|
|
978
1238
|
process.stdout.write(event.data);
|
|
@@ -982,7 +1242,17 @@ async function sandbox(argv, env, out) {
|
|
|
982
1242
|
out.write(JSON.stringify(event));
|
|
983
1243
|
return 0;
|
|
984
1244
|
}
|
|
985
|
-
|
|
1245
|
+
/* Without -f, what it has printed so far, then back to the prompt, as
|
|
1246
|
+
`docker logs` does: a server's log is read, not followed forever. */
|
|
1247
|
+
const so = await outputSoFar(runtime, sbx.id, proc.id);
|
|
1248
|
+
if (out.json) {
|
|
1249
|
+
out.write(JSON.stringify(so));
|
|
1250
|
+
return 0;
|
|
1251
|
+
}
|
|
1252
|
+
for (const chunk of so.chunks)
|
|
1253
|
+
(chunk.stream === "stderr" ? process.stderr : process.stdout).write(chunk.text);
|
|
1254
|
+
if (so.state === "running")
|
|
1255
|
+
process.stderr.write(`[${proc.id} is still running; follow it: ${me} sandbox logs ${sbx.id} ${proc.id} -f]\n`);
|
|
986
1256
|
return 0;
|
|
987
1257
|
}
|
|
988
1258
|
case "kill": {
|
|
@@ -1011,7 +1281,7 @@ async function sandbox(argv, env, out) {
|
|
|
1011
1281
|
...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
|
|
1012
1282
|
});
|
|
1013
1283
|
print(entries
|
|
1014
|
-
.map((e) => `${e.type === "directory" ? "d" : "-"} ${String(e.size).padStart(10)} ${e.path}`)
|
|
1284
|
+
.map((e) => `${e.type === "directory" ? "d" : e.type === "symlink" ? "l" : e.type === "other" ? "?" : "-"} ${String(e.size).padStart(10)} ${e.path}`)
|
|
1015
1285
|
.join("\n") || "(empty)", entries);
|
|
1016
1286
|
return 0;
|
|
1017
1287
|
}
|
|
@@ -1081,6 +1351,27 @@ async function sandbox(argv, env, out) {
|
|
|
1081
1351
|
}
|
|
1082
1352
|
case "desktop":
|
|
1083
1353
|
return desktop(await get(rest[0]), rest.slice(1), out);
|
|
1354
|
+
case "watch":
|
|
1355
|
+
return (await import("./cli-extras.js")).watchCommand(await get(rest[0]), rest.slice(1), out, kit);
|
|
1356
|
+
case "mcp":
|
|
1357
|
+
if (rest[0] === "catalog" || rest[0] === undefined) {
|
|
1358
|
+
const catalog = await runtime.mcp.catalog();
|
|
1359
|
+
print(table([
|
|
1360
|
+
["ID", "LICENCE", "VERSION", "NEEDS", "WHAT"],
|
|
1361
|
+
...catalog.map((entry) => [
|
|
1362
|
+
entry.id,
|
|
1363
|
+
entry.license,
|
|
1364
|
+
entry.version,
|
|
1365
|
+
entry.env
|
|
1366
|
+
.filter((item) => item.required)
|
|
1367
|
+
.map((item) => `${item.name}${item.secret ? " (secret)" : ""}`)
|
|
1368
|
+
.join(", ") || "-",
|
|
1369
|
+
entry.description,
|
|
1370
|
+
]),
|
|
1371
|
+
]), catalog);
|
|
1372
|
+
return 0;
|
|
1373
|
+
}
|
|
1374
|
+
return (await import("./cli-extras.js")).mcpCommand(await get(rest[0]), rest.slice(1), out, kit);
|
|
1084
1375
|
case "metrics": {
|
|
1085
1376
|
const args = parse(rest);
|
|
1086
1377
|
const sbx = await get(args.positional[0]);
|
|
@@ -1216,8 +1507,20 @@ async function runCode(sbx, argv, out) {
|
|
|
1216
1507
|
const args = parse(argv);
|
|
1217
1508
|
const source = need(args.positional[0], "a file, or - to read the code from standard input");
|
|
1218
1509
|
const code = await readInput(source);
|
|
1510
|
+
const byExtension = {
|
|
1511
|
+
js: "javascript",
|
|
1512
|
+
mjs: "javascript",
|
|
1513
|
+
ts: "typescript",
|
|
1514
|
+
r: "r",
|
|
1515
|
+
java: "java",
|
|
1516
|
+
jsh: "java",
|
|
1517
|
+
sh: "bash",
|
|
1518
|
+
bash: "bash",
|
|
1519
|
+
go: "go",
|
|
1520
|
+
};
|
|
1219
1521
|
const language = (flag(args, "lang") ??
|
|
1220
|
-
(
|
|
1522
|
+
byExtension[source.split(".").pop()?.toLowerCase() ?? ""] ??
|
|
1523
|
+
"python");
|
|
1221
1524
|
const timeout = integer(args, "timeout");
|
|
1222
1525
|
const execution = await sbx.interpreter.run(code, {
|
|
1223
1526
|
language,
|
|
@@ -1232,6 +1535,8 @@ async function runCode(sbx, argv, out) {
|
|
|
1232
1535
|
});
|
|
1233
1536
|
const saved = [];
|
|
1234
1537
|
const outDir = flag(args, "out-dir") ?? ".";
|
|
1538
|
+
if (execution.results.some((r) => r.data["image/png"] || r.refs["image/png"]))
|
|
1539
|
+
await mkdir(outDir, { recursive: true });
|
|
1235
1540
|
for (const [index, result] of execution.results.entries()) {
|
|
1236
1541
|
const inline = result.data["image/png"];
|
|
1237
1542
|
const ref = result.refs["image/png"];
|
|
@@ -1296,6 +1601,8 @@ async function desktop(sbx, argv, out) {
|
|
|
1296
1601
|
case "press":
|
|
1297
1602
|
print("Pressed.", await d.press(need(rest[0], "keys, e.g. ctrl+l")));
|
|
1298
1603
|
return 0;
|
|
1604
|
+
case "record":
|
|
1605
|
+
return (await import("./cli-extras.js")).recordCommand(sbx, rest, out, kit);
|
|
1299
1606
|
case "windows": {
|
|
1300
1607
|
const windows = await d.windows();
|
|
1301
1608
|
print(windows.length
|
|
@@ -1304,7 +1611,7 @@ async function desktop(sbx, argv, out) {
|
|
|
1304
1611
|
return 0;
|
|
1305
1612
|
}
|
|
1306
1613
|
}
|
|
1307
|
-
throw usage(`${me} sandbox desktop <id> start|stop|screenshot|open|click|type|press|windows`);
|
|
1614
|
+
throw usage(`${me} sandbox desktop <id> start|stop|screenshot|open|click|type|press|windows|record`);
|
|
1308
1615
|
}
|
|
1309
1616
|
const KEYS_HELP = `runtime keys <command>
|
|
1310
1617
|
|
|
@@ -1463,7 +1770,9 @@ async function snapshot(argv, env, out) {
|
|
|
1463
1770
|
case "list": {
|
|
1464
1771
|
const args = parse(rest);
|
|
1465
1772
|
const all = await (await api.list({
|
|
1466
|
-
...(flag(args, "sandbox")
|
|
1773
|
+
...(flag(args, "sandbox")
|
|
1774
|
+
? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
|
|
1775
|
+
: {}),
|
|
1467
1776
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1468
1777
|
})).toArray(1000);
|
|
1469
1778
|
print(all.length
|
|
@@ -1724,10 +2033,17 @@ const VOLUME_HELP = `runtime volume <command>
|
|
|
1724
2033
|
|
|
1725
2034
|
create --size-mib <N> [--name n] A persistent ext4 disk; prints its id
|
|
1726
2035
|
ls Your volumes
|
|
1727
|
-
get <id> One volume and where it is attached
|
|
1728
|
-
rm <id> Delete it and everything on it
|
|
2036
|
+
get <id> One volume, its backups and where it is attached
|
|
2037
|
+
rm <id> Delete it and everything on it (its backups stay)
|
|
2038
|
+
backup <id> [--retention-days N] [--no-wait]
|
|
2039
|
+
Back it up now, off its host; prints the backup id
|
|
2040
|
+
backups [<id>] Backups, newest first (of one volume, if given)
|
|
2041
|
+
restore <backupId> [--name n] A new volume from a backup, on any host
|
|
2042
|
+
backup-rm <backupId> Delete a backup
|
|
2043
|
+
backup-policy <id> [--daily on|off] [--retention-days N]
|
|
2044
|
+
Daily backups on or off, and how long each is kept
|
|
1729
2045
|
Attach one: runtime sandbox create --volume <id>:/data
|
|
1730
|
-
|
|
2046
|
+
Volumes are backed up off their host daily unless you turn it off.
|
|
1731
2047
|
`;
|
|
1732
2048
|
async function volume(argv, env, out) {
|
|
1733
2049
|
const [verb, ...rest] = argv;
|
|
@@ -1744,8 +2060,6 @@ async function volume(argv, env, out) {
|
|
|
1744
2060
|
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
1745
2061
|
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1746
2062
|
});
|
|
1747
|
-
if (!out.json)
|
|
1748
|
-
out.error("Not backed up off its server.");
|
|
1749
2063
|
print(created.id, created);
|
|
1750
2064
|
return 0;
|
|
1751
2065
|
}
|
|
@@ -1754,12 +2068,13 @@ async function volume(argv, env, out) {
|
|
|
1754
2068
|
const all = await (await api.list()).toArray(1000);
|
|
1755
2069
|
print(all.length
|
|
1756
2070
|
? table([
|
|
1757
|
-
["ID", "NAME", "STATE", "MIB", "ATTACHED"],
|
|
2071
|
+
["ID", "NAME", "STATE", "MIB", "BACKED UP", "ATTACHED"],
|
|
1758
2072
|
...all.map((v) => [
|
|
1759
2073
|
v.id,
|
|
1760
2074
|
v.name ?? "-",
|
|
1761
2075
|
v.state,
|
|
1762
2076
|
String(v.sizeMiB),
|
|
2077
|
+
v.backedUp ? (v.backups?.lastReadyAt ?? "yes") : "no",
|
|
1763
2078
|
v.attachments.map((a) => `${a.sandboxId}:${a.path}`).join(", ") || "-",
|
|
1764
2079
|
]),
|
|
1765
2080
|
])
|
|
@@ -1777,8 +2092,78 @@ async function volume(argv, env, out) {
|
|
|
1777
2092
|
print(`${found.id} ${found.state}`, found);
|
|
1778
2093
|
return 0;
|
|
1779
2094
|
}
|
|
2095
|
+
case "backup": {
|
|
2096
|
+
const args = parse(rest, ["no-wait"]);
|
|
2097
|
+
const id = need(args.positional[0], "a volume id");
|
|
2098
|
+
const days = integer(args, "retention-days");
|
|
2099
|
+
const made = await api.backup(id, days === undefined ? {} : { retentionDays: days }, has(args, "no-wait") ? { wait: 0 } : {});
|
|
2100
|
+
if (!out.json && made.state === "pending")
|
|
2101
|
+
out.error(`Copying it off the host; check with: ${me} volume backups ${id}`);
|
|
2102
|
+
print(made.id, made);
|
|
2103
|
+
return 0;
|
|
2104
|
+
}
|
|
2105
|
+
case "backups": {
|
|
2106
|
+
const volumeId = rest[0];
|
|
2107
|
+
const all = await (await api.backups(volumeId ? { volumeId } : {})).toArray(1000);
|
|
2108
|
+
print(all.length
|
|
2109
|
+
? table([
|
|
2110
|
+
["ID", "VOLUME", "STATE", "TRIGGER", "MIB", "STORED", "TAKEN", "EXPIRES"],
|
|
2111
|
+
...all.map((b) => [
|
|
2112
|
+
b.id,
|
|
2113
|
+
b.volumeId,
|
|
2114
|
+
b.state,
|
|
2115
|
+
b.trigger,
|
|
2116
|
+
String(b.sizeMiB),
|
|
2117
|
+
b.storedBytes === null ? "-" : `${Math.ceil(b.storedBytes / 1048576)} MiB`,
|
|
2118
|
+
b.createdAt,
|
|
2119
|
+
b.expiresAt,
|
|
2120
|
+
]),
|
|
2121
|
+
])
|
|
2122
|
+
: "No backups yet.", all);
|
|
2123
|
+
return 0;
|
|
2124
|
+
}
|
|
2125
|
+
case "restore": {
|
|
2126
|
+
const args = parse(rest);
|
|
2127
|
+
const created = await api.restore(need(args.positional[0], "a backup id"), flag(args, "name") ? { name: flag(args, "name") } : {});
|
|
2128
|
+
if (!out.json && created.state === "creating")
|
|
2129
|
+
out.error(`Restoring; check with: ${me} volume get ${created.id}`);
|
|
2130
|
+
print(created.id, created);
|
|
2131
|
+
return 0;
|
|
2132
|
+
}
|
|
2133
|
+
case "backup-rm":
|
|
2134
|
+
case "backup-delete": {
|
|
2135
|
+
const found = await api.deleteBackup(need(rest[0], "a backup id"));
|
|
2136
|
+
print(`${found.id} ${found.state}`, found);
|
|
2137
|
+
return 0;
|
|
2138
|
+
}
|
|
2139
|
+
case "backup-policy": {
|
|
2140
|
+
const args = parse(rest);
|
|
2141
|
+
const id = need(args.positional[0], "a volume id");
|
|
2142
|
+
const daily = flag(args, "daily");
|
|
2143
|
+
if (daily !== undefined && !["on", "off", "true", "false"].includes(daily))
|
|
2144
|
+
throw usage("--daily takes on or off.");
|
|
2145
|
+
const days = integer(args, "retention-days");
|
|
2146
|
+
if (daily === undefined && days === undefined)
|
|
2147
|
+
throw usage("Give --daily on|off, --retention-days N or both.");
|
|
2148
|
+
const updated = await api.setBackupPolicy(id, {
|
|
2149
|
+
...(daily === undefined ? {} : { daily: daily === "on" || daily === "true" }),
|
|
2150
|
+
...(days === undefined ? {} : { retentionDays: days }),
|
|
2151
|
+
});
|
|
2152
|
+
print(`${updated.id} daily backups ${updated.backups.daily ? "on" : "off"}, kept ${updated.backups.retentionDays} days`, updated);
|
|
2153
|
+
return 0;
|
|
2154
|
+
}
|
|
1780
2155
|
}
|
|
1781
|
-
throw unknown("volume command", verb, [
|
|
2156
|
+
throw unknown("volume command", verb, [
|
|
2157
|
+
"create",
|
|
2158
|
+
"ls",
|
|
2159
|
+
"get",
|
|
2160
|
+
"rm",
|
|
2161
|
+
"backup",
|
|
2162
|
+
"backups",
|
|
2163
|
+
"restore",
|
|
2164
|
+
"backup-rm",
|
|
2165
|
+
"backup-policy",
|
|
2166
|
+
]);
|
|
1782
2167
|
}
|
|
1783
2168
|
/** Flags `sandbox create` and `run` take without a value. */
|
|
1784
2169
|
const CREATE_FLAGS = [
|
|
@@ -1832,7 +2217,10 @@ function createInput(args) {
|
|
|
1832
2217
|
* (124 on a timeout), so a script's `set -e` behaves. */
|
|
1833
2218
|
async function execute(sbx, command, args, out, timeout) {
|
|
1834
2219
|
const target = command.length === 1 ? command[0] : command;
|
|
2220
|
+
const extras = await import("./cli-extras.js");
|
|
2221
|
+
const stdin = await extras.pipedStdin();
|
|
1835
2222
|
const options = {
|
|
2223
|
+
...(stdin ? { stdin } : {}),
|
|
1836
2224
|
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
1837
2225
|
...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
|
|
1838
2226
|
...(timeout ? { timeoutMs: timeout * 1000 } : {}),
|
|
@@ -1848,7 +2236,12 @@ async function execute(sbx, command, args, out, timeout) {
|
|
|
1848
2236
|
out.write(JSON.stringify(result));
|
|
1849
2237
|
else if (result.timedOut)
|
|
1850
2238
|
out.error(`runtime: timed out after ${timeout ?? 60} s; the command's process group was killed.`);
|
|
1851
|
-
|
|
2239
|
+
// Never drop output silently: say so, and do not exit 0.
|
|
2240
|
+
const lost = extras.lostOutput(result, sbx.id);
|
|
2241
|
+
if (lost && !out.json)
|
|
2242
|
+
out.error(lost);
|
|
2243
|
+
const code = result.timedOut ? 124 : Math.min(255, Math.max(0, result.exitCode ?? 1));
|
|
2244
|
+
return lost && code === 0 ? 1 : code;
|
|
1852
2245
|
}
|
|
1853
2246
|
async function shell(sbx, command) {
|
|
1854
2247
|
const stdin = process.stdin;
|