withruntime 0.2.2 → 0.3.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/README.md +68 -4
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +1262 -0
- package/dist/client.d.ts +118 -0
- package/dist/client.js +160 -0
- package/dist/credentials.d.ts +44 -0
- package/dist/credentials.js +180 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.js +106 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +25 -0
- package/dist/login.d.ts +119 -0
- package/dist/login.js +367 -0
- package/dist/mcp.d.ts +15 -0
- package/dist/mcp.js +221 -0
- package/dist/page.d.ts +13 -0
- package/dist/page.js +39 -0
- package/dist/products/desktop.d.ts +103 -0
- package/dist/products/desktop.js +38 -0
- package/dist/products/images.d.ts +121 -0
- package/dist/products/images.js +64 -0
- package/dist/products/index.d.ts +29 -0
- package/dist/products/index.js +36 -0
- package/dist/products/interpreter.d.ts +88 -0
- package/dist/products/interpreter.js +81 -0
- package/dist/products/network.d.ts +47 -0
- package/dist/products/network.js +19 -0
- package/dist/products/previews.d.ts +51 -0
- package/dist/products/previews.js +30 -0
- package/dist/products/referrals.d.ts +36 -0
- package/dist/products/referrals.js +8 -0
- package/dist/products/volumes.d.ts +47 -0
- package/dist/products/volumes.js +27 -0
- package/dist/sandbox.d.ts +192 -0
- package/dist/sandbox.js +628 -0
- package/dist/snapshots.d.ts +50 -0
- package/dist/snapshots.js +55 -0
- package/dist/tar.d.ts +3 -0
- package/dist/tar.js +116 -0
- package/dist/transport.d.ts +65 -0
- package/dist/transport.js +261 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.js +1 -0
- package/package.json +53 -8
- package/cli.js +0 -7
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveCredential } from "./credentials.js";
|
|
7
|
+
import { authenticationCommand, me, named } from "./login.js";
|
|
8
|
+
import { Runtime } from "./client.js";
|
|
9
|
+
import { RuntimeError } from "./errors.js";
|
|
10
|
+
import { VERSION } from "./transport.js";
|
|
11
|
+
const HELP = `Runtime Cloud CLI ${VERSION}
|
|
12
|
+
|
|
13
|
+
Usage: runtime <product> <command> [options] runtime <command> [options]
|
|
14
|
+
|
|
15
|
+
Start
|
|
16
|
+
run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
|
|
17
|
+
|
|
18
|
+
Account
|
|
19
|
+
login [--agent-name <name>] [--no-browser] [--wait]
|
|
20
|
+
Connect this machine (no key to copy)
|
|
21
|
+
login --with-key Or paste a key from withruntime.com/account/keys
|
|
22
|
+
logout Revoke this machine's connection
|
|
23
|
+
whoami The organization and agent you act as
|
|
24
|
+
usage Balance, trial time and charges
|
|
25
|
+
referrals Your referral link: you both get up to $500
|
|
26
|
+
ls Everything you run, every product
|
|
27
|
+
feedback "<text>" [--kind bug|missing_feature|competitor_gap|migration_blocker|docs|pricing|praise|other]
|
|
28
|
+
[--detail <text>] [--competitor <name>] [--request-id <req_...>]
|
|
29
|
+
feedback --list What you reported, and what happened to it
|
|
30
|
+
support "<message>" [--conversation <id>] Ask Runtime support; read, approve, deny below
|
|
31
|
+
support read <conversationId>
|
|
32
|
+
support approve <actionId> <inputHash> --conversation <id>
|
|
33
|
+
support deny <actionId> --conversation <id>
|
|
34
|
+
docs [topic] Print a docs page (start, cli, api, python...)
|
|
35
|
+
mcp Serve MCP on stdio for agents that need it
|
|
36
|
+
|
|
37
|
+
Products
|
|
38
|
+
sandbox Linux microVMs: exec, shell, files, code, previews, network, desktop
|
|
39
|
+
snapshot Saved sandboxes, disk and memory, to start or fork from
|
|
40
|
+
image Custom images from a Dockerfile, a public image or a package list
|
|
41
|
+
volume Persistent disks to attach to sandboxes
|
|
42
|
+
Each has its own help: \`runtime sandbox help\`, \`runtime image help\`...
|
|
43
|
+
|
|
44
|
+
Every command takes --json. Keys: RUNTIME_API_KEY, or the connection from \`runtime login\`.
|
|
45
|
+
`;
|
|
46
|
+
const SANDBOX_HELP = `runtime sandbox <command>
|
|
47
|
+
|
|
48
|
+
create [--name n] [--label k=v]... [--vcpu 2] [--memory 4096] [--disk 4096]
|
|
49
|
+
[--timeout <seconds>] [--on-timeout pause|stop] [--trial|--paid] [--no-wait]
|
|
50
|
+
[--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
51
|
+
[--image <imageId> | --snapshot <snapshotId>] [--volume <volumeId>:/data[:snapshot]]...
|
|
52
|
+
Create one; prints its id
|
|
53
|
+
ls [--all] [--label k=v]... [--state running] List (live ones unless --all)
|
|
54
|
+
get <id> One sandbox, in full
|
|
55
|
+
exec <id> [--cwd /workspace] [--env K=V]... [--timeout <s>] -- <command...>
|
|
56
|
+
Run and stream output; exits with its code
|
|
57
|
+
spawn <id> [--pty] -- <command...> Start in the background; prints a process id
|
|
58
|
+
ps <id> Running and recent processes
|
|
59
|
+
logs <id> <processId> [-f] A process's output (follow with -f)
|
|
60
|
+
kill <id> <processId> [--signal SIGTERM]
|
|
61
|
+
shell <id> [--command "bash -l"] An interactive terminal
|
|
62
|
+
cp <src> <dst> Copy files or directories; name a sandbox
|
|
63
|
+
path as <id>:/path, e.g. cp ./app sbx:/workspace/app
|
|
64
|
+
cat <id> <path> Print a file
|
|
65
|
+
files <id> [path] [--depth 2] [--glob '**/*.py']
|
|
66
|
+
stop <id> | pause <id> | wake <id> | restart <id>
|
|
67
|
+
extend <id> <seconds> More time before the lease ends
|
|
68
|
+
snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
|
|
69
|
+
fork <id> [--count 3] [--name n] [--keep-snapshot]
|
|
70
|
+
Copies of it as it is now, running; prints their ids
|
|
71
|
+
run-code <id> <file|-> [--lang python|javascript] [--context c] [--out-dir .]
|
|
72
|
+
Run a notebook-style cell; charts saved as PNG
|
|
73
|
+
preview <id> <port> [--public] [--ttl <s>] Share a port at an HTTPS address
|
|
74
|
+
previews <id> Every shared port
|
|
75
|
+
unshare <id> <port> Stop sharing a port
|
|
76
|
+
network <id> [--no-internet|--internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
|
|
77
|
+
Show its network rules, or replace them
|
|
78
|
+
desktop <id> start | stop | screenshot [file.png] | open <url> | click <x> <y>
|
|
79
|
+
| type <text> | press <keys> | windows
|
|
80
|
+
|
|
81
|
+
Examples
|
|
82
|
+
id=$(runtime sandbox create)
|
|
83
|
+
runtime sandbox exec $id -- python3 -c 'print(6*7)'
|
|
84
|
+
runtime sandbox cp ./project $id:/workspace/project
|
|
85
|
+
runtime sandbox stop $id
|
|
86
|
+
`;
|
|
87
|
+
function parse(argv, booleans = []) {
|
|
88
|
+
const positional = [];
|
|
89
|
+
const flags = new Map();
|
|
90
|
+
let rest;
|
|
91
|
+
for (let i = 0; i < argv.length; i++) {
|
|
92
|
+
const arg = argv[i];
|
|
93
|
+
if (arg === "--") {
|
|
94
|
+
rest = argv.slice(i + 1);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
if (arg.startsWith("--") || (arg.startsWith("-") && arg.length === 2 && !/^-\d/.test(arg))) {
|
|
98
|
+
const [name, inline] = arg.replace(/^-+/, "").split(/=(.*)/s, 2);
|
|
99
|
+
const value = inline ?? (booleans.includes(name) ? "true" : argv[++i]);
|
|
100
|
+
if (value === undefined)
|
|
101
|
+
throw usage(`--${name} needs a value.`);
|
|
102
|
+
flags.set(name, [...(flags.get(name) ?? []), value]);
|
|
103
|
+
}
|
|
104
|
+
else
|
|
105
|
+
positional.push(arg);
|
|
106
|
+
}
|
|
107
|
+
return { positional, flags, rest };
|
|
108
|
+
}
|
|
109
|
+
const flag = (args, name) => args.flags.get(name)?.at(-1);
|
|
110
|
+
const has = (args, name) => args.flags.get(name)?.at(-1) === "true";
|
|
111
|
+
const integer = (args, name) => {
|
|
112
|
+
const value = flag(args, name);
|
|
113
|
+
if (value === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (!/^\d+$/.test(value))
|
|
116
|
+
throw usage(`--${name} must be a whole number.`);
|
|
117
|
+
return Number(value);
|
|
118
|
+
};
|
|
119
|
+
const pairs = (args, name) => Object.fromEntries((args.flags.get(name) ?? []).map((pair) => {
|
|
120
|
+
const at = pair.indexOf("=");
|
|
121
|
+
if (at < 1)
|
|
122
|
+
throw usage(`--${name} takes KEY=VALUE.`);
|
|
123
|
+
return [pair.slice(0, at), pair.slice(at + 1)];
|
|
124
|
+
}));
|
|
125
|
+
/** The product being run, so a mistake points at its own help. */
|
|
126
|
+
let product = "";
|
|
127
|
+
function usage(message) {
|
|
128
|
+
return new RuntimeError({
|
|
129
|
+
message,
|
|
130
|
+
code: "usage",
|
|
131
|
+
status: 0,
|
|
132
|
+
hint: product ? `Run \`${me} ${product} help\`.` : `Run \`${me} help\`.`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** "Unknown command X." and, when one is close, the command that was meant. */
|
|
136
|
+
function unknown(what, word, choices) {
|
|
137
|
+
const distance = (a, b) => {
|
|
138
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
139
|
+
for (let i = 1; i <= a.length; i++) {
|
|
140
|
+
let previous = row[0];
|
|
141
|
+
row[0] = i;
|
|
142
|
+
for (let j = 1; j <= b.length; j++) {
|
|
143
|
+
const next = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
144
|
+
previous = row[j];
|
|
145
|
+
row[j] = next;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return row[b.length];
|
|
149
|
+
};
|
|
150
|
+
const best = choices
|
|
151
|
+
.map((choice) => [choice, distance(word, choice)])
|
|
152
|
+
.sort((x, y) => x[1] - y[1])[0];
|
|
153
|
+
const meant = best && best[1] <= (word.length >= 4 ? 2 : 1) ? best[0] : undefined;
|
|
154
|
+
return usage(`Unknown ${what} ${word}.${meant ? ` Did you mean ${meant}?` : ""}`);
|
|
155
|
+
}
|
|
156
|
+
const COMMANDS = [
|
|
157
|
+
"login",
|
|
158
|
+
"logout",
|
|
159
|
+
"whoami",
|
|
160
|
+
"usage",
|
|
161
|
+
"referrals",
|
|
162
|
+
"ls",
|
|
163
|
+
"feedback",
|
|
164
|
+
"support",
|
|
165
|
+
"docs",
|
|
166
|
+
"mcp",
|
|
167
|
+
"sandbox",
|
|
168
|
+
"snapshot",
|
|
169
|
+
"image",
|
|
170
|
+
"volume",
|
|
171
|
+
"help",
|
|
172
|
+
"version",
|
|
173
|
+
];
|
|
174
|
+
const SANDBOX_VERBS = [
|
|
175
|
+
"create",
|
|
176
|
+
"ls",
|
|
177
|
+
"get",
|
|
178
|
+
"stop",
|
|
179
|
+
"pause",
|
|
180
|
+
"wake",
|
|
181
|
+
"restart",
|
|
182
|
+
"snapshot",
|
|
183
|
+
"fork",
|
|
184
|
+
"extend",
|
|
185
|
+
"exec",
|
|
186
|
+
"spawn",
|
|
187
|
+
"ps",
|
|
188
|
+
"logs",
|
|
189
|
+
"kill",
|
|
190
|
+
"shell",
|
|
191
|
+
"cat",
|
|
192
|
+
"files",
|
|
193
|
+
"cp",
|
|
194
|
+
"run-code",
|
|
195
|
+
"preview",
|
|
196
|
+
"previews",
|
|
197
|
+
"unshare",
|
|
198
|
+
"network",
|
|
199
|
+
"desktop",
|
|
200
|
+
];
|
|
201
|
+
function need(value, what) {
|
|
202
|
+
if (!value)
|
|
203
|
+
throw usage(`Give ${what}.`);
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
/** The client, with the key from RUNTIME_API_KEY or this machine's saved
|
|
207
|
+
* connection. With neither, the first command connects the machine first (a
|
|
208
|
+
* browser approval, nothing to copy) and then does what was asked, so setup
|
|
209
|
+
* is not a separate step. CI, or RUNTIME_NO_LOGIN, answers missing_api_key. */
|
|
210
|
+
/** The approval was asked for and not given yet: say where, and that running
|
|
211
|
+
* the same command again picks it up. */
|
|
212
|
+
function notApprovedYet(login) {
|
|
213
|
+
return new RuntimeError({
|
|
214
|
+
message: `Not connected yet. Open ${login.url}, check the code ${login.userCode} and choose Connect agent.`,
|
|
215
|
+
code: "connection_pending",
|
|
216
|
+
status: 0,
|
|
217
|
+
hint: "Once it is approved, run the same command again: it picks up this approval, with no new link.",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async function client(env) {
|
|
221
|
+
let apiKey;
|
|
222
|
+
try {
|
|
223
|
+
apiKey = await resolveCredential(env);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (!(error instanceof RuntimeError) ||
|
|
227
|
+
error.code !== "missing_api_key" ||
|
|
228
|
+
env.CI ||
|
|
229
|
+
env.RUNTIME_NO_LOGIN)
|
|
230
|
+
throw error;
|
|
231
|
+
process.stderr.write("This machine is not connected to Runtime yet. Connecting it first.\n");
|
|
232
|
+
const login = await authenticationCommand(["login"], env);
|
|
233
|
+
if (login.pending)
|
|
234
|
+
throw notApprovedYet(login);
|
|
235
|
+
apiKey = await resolveCredential(env);
|
|
236
|
+
}
|
|
237
|
+
return new Runtime({ apiKey, baseUrl: env.RUNTIME_API_URL, maxRetries: 4 });
|
|
238
|
+
}
|
|
239
|
+
function table(rows) {
|
|
240
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
|
|
241
|
+
return rows
|
|
242
|
+
.map((row) => row
|
|
243
|
+
.map((cell, column) => cell.padEnd(widths[column]))
|
|
244
|
+
.join(" ")
|
|
245
|
+
.trimEnd())
|
|
246
|
+
.join("\n");
|
|
247
|
+
}
|
|
248
|
+
function describe(info) {
|
|
249
|
+
return table([
|
|
250
|
+
["id", info.id],
|
|
251
|
+
["name", info.name ?? "-"],
|
|
252
|
+
["state", info.state],
|
|
253
|
+
[
|
|
254
|
+
"shape",
|
|
255
|
+
`${info.vcpu} vCPU, ${info.memoryMiB} MiB memory, ${info.diskMiB} MiB disk (${info.cpu})`,
|
|
256
|
+
],
|
|
257
|
+
["funding", info.funding],
|
|
258
|
+
["region", info.region],
|
|
259
|
+
["expires", info.expiresAt ?? "-"],
|
|
260
|
+
[
|
|
261
|
+
"labels",
|
|
262
|
+
Object.entries(info.labels ?? {})
|
|
263
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
264
|
+
.join(", ") || "-",
|
|
265
|
+
],
|
|
266
|
+
["charged", `$${(info.chargedMicros / 1e6).toFixed(6)}`],
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
/** Resolves `<id>:/path` into its parts, or undefined for a local path. */
|
|
270
|
+
function remote(spec) {
|
|
271
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9_.-]{0,127}):(\/.*)$/.exec(spec);
|
|
272
|
+
return match ? { id: match[1], path: match[2] } : undefined;
|
|
273
|
+
}
|
|
274
|
+
export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
275
|
+
const [command, ...rest] = argv.filter((arg) => arg !== "--json" || (argv.indexOf("--") !== -1 && argv.indexOf(arg) > argv.indexOf("--")));
|
|
276
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
277
|
+
product = "";
|
|
278
|
+
if (!command) {
|
|
279
|
+
const connected = Boolean(env.RUNTIME_API_KEY) ||
|
|
280
|
+
(await resolveCredential(env).then(() => true, () => false));
|
|
281
|
+
const start = connected
|
|
282
|
+
? "Connected."
|
|
283
|
+
: "Not connected yet: the first command below connects this machine (a browser approval).";
|
|
284
|
+
out.write(out.json
|
|
285
|
+
? JSON.stringify({ connected, usage: HELP })
|
|
286
|
+
: `Runtime Cloud ${VERSION}. ${start}
|
|
287
|
+
|
|
288
|
+
${me} run -- python3 -c 'print(6*7)' A fresh sandbox runs one command, then stops
|
|
289
|
+
${me} sandbox create A sandbox that stays; prints its id
|
|
290
|
+
${me} sandbox exec <id> -- <command> Run in it
|
|
291
|
+
${me} help Everything else
|
|
292
|
+
`);
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
296
|
+
out.write(out.json ? JSON.stringify({ usage: named(HELP) }) : named(HELP));
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
if (command === "run") {
|
|
300
|
+
const args = parse(rest, [...CREATE_FLAGS, "keep"]);
|
|
301
|
+
const commandLine = args.rest ?? args.positional;
|
|
302
|
+
if (!commandLine.length)
|
|
303
|
+
throw usage(`Give the command after --: ${me} run -- python3 -c 'print(6*7)'`);
|
|
304
|
+
const runtime = await client(env);
|
|
305
|
+
const timeout = integer(args, "timeout");
|
|
306
|
+
const { timeoutSeconds: _lease, ...input } = createInput(args);
|
|
307
|
+
const sbx = await runtime.sandboxes.create(input);
|
|
308
|
+
let cleanup;
|
|
309
|
+
// SIGINT and normal completion share one stop. Keep an absolute deadline
|
|
310
|
+
// across the stop retries and final state check, so cleanup cannot hang.
|
|
311
|
+
const stop = () => (cleanup ??= (async () => {
|
|
312
|
+
const signal = AbortSignal.timeout(30_000);
|
|
313
|
+
try {
|
|
314
|
+
await sbx.stop({ wait: false, signal });
|
|
315
|
+
if (sbx.state !== "stopped")
|
|
316
|
+
await sbx.waitFor("stopped", { timeoutSeconds: 25, signal });
|
|
317
|
+
if (sbx.state !== "stopped")
|
|
318
|
+
throw new Error(`Sandbox is still ${sbx.state}.`);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
out.error(describeError(new RuntimeError({
|
|
323
|
+
code: "cleanup_failed",
|
|
324
|
+
status: 0,
|
|
325
|
+
message: `Could not confirm that sandbox ${sbx.id} stopped. ${error instanceof Error ? error.message : String(error)}`,
|
|
326
|
+
hint: `Check it and retry cleanup with: ${me} sandbox stop ${sbx.id}`,
|
|
327
|
+
cause: error,
|
|
328
|
+
}), out.json));
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
})());
|
|
332
|
+
const interrupted = () => void stop().then(() => process.exit(130));
|
|
333
|
+
if (!has(args, "keep"))
|
|
334
|
+
process.once("SIGINT", interrupted);
|
|
335
|
+
let code = 1;
|
|
336
|
+
try {
|
|
337
|
+
code = await execute(sbx, commandLine, args, out, timeout);
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
try {
|
|
341
|
+
if (has(args, "keep"))
|
|
342
|
+
out.error(`Kept sandbox ${sbx.id}. Run more with: ${me} sandbox exec ${sbx.id} -- <command>`);
|
|
343
|
+
else if (!(await stop()) && code === 0)
|
|
344
|
+
code = 1;
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
process.off("SIGINT", interrupted);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return code;
|
|
351
|
+
}
|
|
352
|
+
if (command === "--version" || command === "version") {
|
|
353
|
+
print(VERSION, { version: VERSION });
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
if (command === "login" || command === "logout") {
|
|
357
|
+
const result = await authenticationCommand([command, ...rest], env);
|
|
358
|
+
print(result.pending
|
|
359
|
+
? `Not connected yet. When it is approved, run any command, or \`${me} login --wait\`; it uses this link.`
|
|
360
|
+
: command === "login"
|
|
361
|
+
? `Connected${"agentName" in result && result.agentName ? ` as ${String(result.agentName)}` : ""}.`
|
|
362
|
+
: "Disconnected.", result);
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
if (command === "whoami") {
|
|
366
|
+
const who = await (await client(env)).me();
|
|
367
|
+
print(table([
|
|
368
|
+
["organization", who.orgId],
|
|
369
|
+
["agent", who.principalId],
|
|
370
|
+
["key", who.credentialId ?? "-"],
|
|
371
|
+
["api", who.apiVersion],
|
|
372
|
+
]), who);
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
if (command === "usage") {
|
|
376
|
+
const value = await (await client(env)).usage();
|
|
377
|
+
print(JSON.stringify(value, null, 2), value);
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
if (command === "referrals") {
|
|
381
|
+
const r = await (await client(env)).referrals.get();
|
|
382
|
+
const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
383
|
+
print([
|
|
384
|
+
`Your link: ${r.link}`,
|
|
385
|
+
"",
|
|
386
|
+
`When a company signs up with it and makes its first top-up of ${usd(r.minPurchaseMicros)} or more,`,
|
|
387
|
+
`you each get credit equal to that top-up: at least ${usd(r.rewardMicros)}, at most ${usd(r.maxRewardMicros)}`,
|
|
388
|
+
`(up to ${usd(r.yearlyCapMicros)} a year for you).`,
|
|
389
|
+
...(r.enabled ? [] : ["The program is paused: new sign-ups are not counted right now."]),
|
|
390
|
+
"",
|
|
391
|
+
table([
|
|
392
|
+
["signed up", String(r.signedUp)],
|
|
393
|
+
["waiting for a first top-up", String(r.pending)],
|
|
394
|
+
["paid", String(r.paid)],
|
|
395
|
+
["earned", usd(r.earnedMicros)],
|
|
396
|
+
[`left this year (${r.year})`, usd(r.capRemainingMicros)],
|
|
397
|
+
]),
|
|
398
|
+
].join("\n"), r);
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
if (command === "mcp") {
|
|
402
|
+
const { serveMcp } = await import("./mcp.js");
|
|
403
|
+
await serveMcp(env);
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
if (command === "docs") {
|
|
407
|
+
const topic = rest[0] ?? "index";
|
|
408
|
+
const response = await fetch(`https://withruntime.com/docs/${topic === "index" ? "start" : encodeURIComponent(topic)}.md`, { redirect: "error" });
|
|
409
|
+
if (!response.ok)
|
|
410
|
+
throw usage(`No docs page named ${topic}. Try start, cli, api, javascript, python, mcp.`);
|
|
411
|
+
out.write(await response.text());
|
|
412
|
+
return 0;
|
|
413
|
+
}
|
|
414
|
+
if (command === "feedback") {
|
|
415
|
+
const args = parse(rest, ["list"]);
|
|
416
|
+
if (has(args, "list")) {
|
|
417
|
+
const { data } = await (await client(env)).feedback.list();
|
|
418
|
+
print(data.length
|
|
419
|
+
? table([
|
|
420
|
+
["ID", "KIND", "STATUS", "SUMMARY"],
|
|
421
|
+
...data.map((f) => [f.id, f.kind, f.status, f.summary]),
|
|
422
|
+
])
|
|
423
|
+
: 'Nothing reported yet. Tell us anything: runtime feedback "..."', data);
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
const summary = need(args.positional.join(" ").trim(), 'what to tell us, in quotes: runtime feedback "..."');
|
|
427
|
+
const kind = (flag(args, "kind") ?? "other");
|
|
428
|
+
const detail = flag(args, "detail");
|
|
429
|
+
const result = await (await client(env)).feedback.submit({
|
|
430
|
+
kind,
|
|
431
|
+
summary: summary.slice(0, 200),
|
|
432
|
+
...(detail || summary.length > 200 ? { detail: detail ?? summary } : {}),
|
|
433
|
+
...(flag(args, "competitor") ? { competitor: flag(args, "competitor") } : {}),
|
|
434
|
+
...(flag(args, "request-id") ? { requestId: flag(args, "request-id") } : {}),
|
|
435
|
+
});
|
|
436
|
+
print(`Thank you: filed ${result.id}${result.duplicate ? " (already filed)" : ""}. Follow it with \`${me} feedback --list\`.`, result);
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
if (command === "support")
|
|
440
|
+
return support(rest, env, out);
|
|
441
|
+
if (command === "ls") {
|
|
442
|
+
const runtime = await client(env);
|
|
443
|
+
// Every product in one list; a product this account cannot use is left out.
|
|
444
|
+
const quiet = (work) => work.catch(() => undefined);
|
|
445
|
+
const [sandboxes, snapshots, images, volumes] = await Promise.all([
|
|
446
|
+
runtime.sandboxes.list({ limit: 100 }),
|
|
447
|
+
quiet(runtime.snapshots.list({ limit: 100 })),
|
|
448
|
+
quiet(runtime.images.list({ limit: 100 })),
|
|
449
|
+
quiet(runtime.volumes.list({ limit: 100 })),
|
|
450
|
+
]);
|
|
451
|
+
const items = [
|
|
452
|
+
...sandboxes.data.map((s) => s.info),
|
|
453
|
+
...(snapshots?.data ?? []),
|
|
454
|
+
...(images?.data ?? []),
|
|
455
|
+
...(volumes?.data ?? []),
|
|
456
|
+
];
|
|
457
|
+
const rows = items.map((item) => [
|
|
458
|
+
item.id,
|
|
459
|
+
item.kind,
|
|
460
|
+
item.name ?? "-",
|
|
461
|
+
item.state ?? item.status ?? "-",
|
|
462
|
+
item.createdAt,
|
|
463
|
+
]);
|
|
464
|
+
print(rows.length
|
|
465
|
+
? table([["ID", "PRODUCT", "NAME", "STATE", "CREATED"], ...rows])
|
|
466
|
+
: "Nothing yet. Start with: runtime sandbox create", items);
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
if (command === "sandbox" || command === "sandboxes" || command === "sbx") {
|
|
470
|
+
product = "sandbox";
|
|
471
|
+
return sandbox(rest, env, out);
|
|
472
|
+
}
|
|
473
|
+
if (command === "snapshot" || command === "snapshots") {
|
|
474
|
+
product = "snapshot";
|
|
475
|
+
return snapshot(rest, env, out);
|
|
476
|
+
}
|
|
477
|
+
if (command === "image" || command === "images") {
|
|
478
|
+
product = "image";
|
|
479
|
+
return image(rest, env, out);
|
|
480
|
+
}
|
|
481
|
+
if (command === "volume" || command === "volumes") {
|
|
482
|
+
product = "volume";
|
|
483
|
+
return volume(rest, env, out);
|
|
484
|
+
}
|
|
485
|
+
throw unknown("command", command, COMMANDS);
|
|
486
|
+
}
|
|
487
|
+
async function support(argv, env, out) {
|
|
488
|
+
const args = parse(argv);
|
|
489
|
+
const api = (await client(env)).support;
|
|
490
|
+
const conversationId = flag(args, "conversation");
|
|
491
|
+
const [first, second, third] = args.positional;
|
|
492
|
+
let reply;
|
|
493
|
+
if (first === "read")
|
|
494
|
+
reply = await api.read(need(second, "the conversation id"));
|
|
495
|
+
else if (first === "approve")
|
|
496
|
+
reply = await api.message({
|
|
497
|
+
approveActionId: need(second, "the action id"),
|
|
498
|
+
approveInputHash: need(third, "the input hash"),
|
|
499
|
+
...(conversationId ? { conversationId } : {}),
|
|
500
|
+
});
|
|
501
|
+
else if (first === "deny")
|
|
502
|
+
reply = await api.message({
|
|
503
|
+
denyActionId: need(second, "the action id"),
|
|
504
|
+
...(conversationId ? { conversationId } : {}),
|
|
505
|
+
});
|
|
506
|
+
else
|
|
507
|
+
reply = await api.message({
|
|
508
|
+
message: need(args.positional.join(" ").trim(), 'a message: runtime support "..."'),
|
|
509
|
+
...(conversationId ? { conversationId } : {}),
|
|
510
|
+
});
|
|
511
|
+
const actions = (reply.pendingActions ?? []).map((a) => ` ${a.id} ${a.summary}\n approve: runtime support approve ${a.id} ${a.inputHash} --conversation ${reply.conversationId}`);
|
|
512
|
+
out.write(out.json
|
|
513
|
+
? JSON.stringify(reply)
|
|
514
|
+
: `${reply.reply ?? (reply.status === "working" ? "Support is working on it. Read again in a minute:" : "")}\n${reply.status === "working" ? ` runtime support read ${reply.conversationId}\n` : ""}${actions.length ? `Proposed actions (nothing runs until you approve):\n${actions.join("\n")}\n` : ""}[${reply.status}] conversation ${reply.conversationId}`);
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
async function sandbox(argv, env, out) {
|
|
518
|
+
const [verb, ...rest] = argv;
|
|
519
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
520
|
+
if (!verb || verb === "help" || verb === "--help") {
|
|
521
|
+
out.write(out.json ? JSON.stringify({ usage: named(SANDBOX_HELP) }) : named(SANDBOX_HELP));
|
|
522
|
+
return 0;
|
|
523
|
+
}
|
|
524
|
+
const runtime = await client(env);
|
|
525
|
+
const get = (id) => runtime.sandboxes.get(need(id, "a sandbox id"));
|
|
526
|
+
switch (verb) {
|
|
527
|
+
case "create": {
|
|
528
|
+
const args = parse(rest, CREATE_FLAGS);
|
|
529
|
+
const sbx = await runtime.sandboxes.create(createInput(args));
|
|
530
|
+
print(sbx.id, sbx.info);
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
case "ls":
|
|
534
|
+
case "list": {
|
|
535
|
+
const args = parse(rest, ["all"]);
|
|
536
|
+
const page = await runtime.sandboxes.list({
|
|
537
|
+
...(has(args, "all") ? { includeStopped: true } : {}),
|
|
538
|
+
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
539
|
+
...(args.flags.has("state")
|
|
540
|
+
? { state: args.flags.get("state") }
|
|
541
|
+
: {}),
|
|
542
|
+
});
|
|
543
|
+
const all = await page.toArray(1000);
|
|
544
|
+
// Stopped sandboxes are hidden by default; say so, or an agent that
|
|
545
|
+
// stopped everything reads "No sandboxes" as "never had any".
|
|
546
|
+
const hidden = out.json || args.flags.size
|
|
547
|
+
? null
|
|
548
|
+
: await runtime.sandboxes.list({ state: ["stopped"], limit: 50 });
|
|
549
|
+
const stopped = hidden?.data.length
|
|
550
|
+
? `${hidden.data.length}${hidden.hasMore ? "+" : ""} stopped; runtime sandbox ls --all shows them.`
|
|
551
|
+
: "";
|
|
552
|
+
const rows = all.map((s) => [
|
|
553
|
+
s.id,
|
|
554
|
+
s.info.name ?? "-",
|
|
555
|
+
s.state,
|
|
556
|
+
`${s.info.vcpu}/${s.info.memoryMiB}`,
|
|
557
|
+
s.info.funding,
|
|
558
|
+
s.info.expiresAt ?? "-",
|
|
559
|
+
]);
|
|
560
|
+
print([
|
|
561
|
+
rows.length
|
|
562
|
+
? table([["ID", "NAME", "STATE", "CPU/MIB", "FUNDING", "EXPIRES"], ...rows])
|
|
563
|
+
: stopped
|
|
564
|
+
? "No live sandboxes."
|
|
565
|
+
: "No sandboxes. Create one: runtime sandbox create",
|
|
566
|
+
stopped,
|
|
567
|
+
]
|
|
568
|
+
.filter(Boolean)
|
|
569
|
+
.join("\n"), all.map((s) => s.info));
|
|
570
|
+
return 0;
|
|
571
|
+
}
|
|
572
|
+
case "get": {
|
|
573
|
+
const sbx = await get(rest[0]);
|
|
574
|
+
print(describe(sbx.info), sbx.info);
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
case "stop":
|
|
578
|
+
case "pause":
|
|
579
|
+
case "wake":
|
|
580
|
+
case "restart": {
|
|
581
|
+
const sbx = await get(rest[0]);
|
|
582
|
+
await sbx[verb]();
|
|
583
|
+
print(`${sbx.id} ${sbx.state}`, sbx.info);
|
|
584
|
+
return 0;
|
|
585
|
+
}
|
|
586
|
+
case "snapshot": {
|
|
587
|
+
const args = parse(rest);
|
|
588
|
+
const sbx = await get(args.positional[0]);
|
|
589
|
+
const snapshot = await sbx.snapshot({
|
|
590
|
+
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
591
|
+
...(integer(args, "retention") ? { retentionDays: integer(args, "retention") } : {}),
|
|
592
|
+
});
|
|
593
|
+
print(snapshot.id, snapshot);
|
|
594
|
+
return 0;
|
|
595
|
+
}
|
|
596
|
+
case "fork": {
|
|
597
|
+
const args = parse(rest, ["keep-snapshot"]);
|
|
598
|
+
const sbx = await get(args.positional[0]);
|
|
599
|
+
const forks = await sbx.fork({
|
|
600
|
+
count: integer(args, "count") ?? 1,
|
|
601
|
+
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
602
|
+
...(has(args, "keep-snapshot") ? { keepSnapshot: true } : {}),
|
|
603
|
+
});
|
|
604
|
+
print(forks.map((f) => f.id).join("\n"), forks.map((f) => f.info));
|
|
605
|
+
return 0;
|
|
606
|
+
}
|
|
607
|
+
case "extend": {
|
|
608
|
+
const sbx = await get(rest[0]);
|
|
609
|
+
const seconds = Number(need(rest[1], "seconds, e.g. runtime sandbox extend <id> 600"));
|
|
610
|
+
await sbx.extend(seconds);
|
|
611
|
+
print(`${sbx.id} now ends at ${sbx.info.expiresAt}`, sbx.info);
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
case "exec": {
|
|
615
|
+
const args = parse(rest);
|
|
616
|
+
const command = args.rest ?? args.positional.slice(1);
|
|
617
|
+
if (!command.length)
|
|
618
|
+
throw usage("Give the command after --: runtime sandbox exec <id> -- ls -la");
|
|
619
|
+
const sbx = await get(args.positional[0]);
|
|
620
|
+
return execute(sbx, command, args, out, integer(args, "timeout"));
|
|
621
|
+
}
|
|
622
|
+
case "spawn": {
|
|
623
|
+
const args = parse(rest, ["pty"]);
|
|
624
|
+
const command = args.rest ?? [];
|
|
625
|
+
if (!command.length)
|
|
626
|
+
throw usage("Give the command after --: runtime sandbox spawn <id> -- npm run dev");
|
|
627
|
+
const sbx = await get(args.positional[0]);
|
|
628
|
+
const proc = await sbx.spawn(command.length === 1 ? command[0] : command, {
|
|
629
|
+
...(has(args, "pty") ? { pty: { cols: 120, rows: 40 } } : {}),
|
|
630
|
+
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
631
|
+
...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
|
|
632
|
+
});
|
|
633
|
+
print(proc.id, proc.info);
|
|
634
|
+
return 0;
|
|
635
|
+
}
|
|
636
|
+
case "ps": {
|
|
637
|
+
const sbx = await get(rest[0]);
|
|
638
|
+
const list = await sbx.processes.list();
|
|
639
|
+
print(list.length
|
|
640
|
+
? table([
|
|
641
|
+
["PROCESS", "STATE", "EXIT", "COMMAND"],
|
|
642
|
+
...list.map((p) => [p.id, p.state, String(p.exitCode ?? "-"), p.command]),
|
|
643
|
+
])
|
|
644
|
+
: "No processes.", list);
|
|
645
|
+
return 0;
|
|
646
|
+
}
|
|
647
|
+
case "logs": {
|
|
648
|
+
const args = parse(rest, ["f", "follow"]);
|
|
649
|
+
const sbx = await get(args.positional[0]);
|
|
650
|
+
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
651
|
+
if (has(args, "f") || has(args, "follow") || !out.json) {
|
|
652
|
+
for await (const event of proc.output())
|
|
653
|
+
if (event.type === "stdout")
|
|
654
|
+
process.stdout.write(event.data);
|
|
655
|
+
else if (event.type === "stderr")
|
|
656
|
+
process.stderr.write(event.data);
|
|
657
|
+
else if (out.json)
|
|
658
|
+
out.write(JSON.stringify(event));
|
|
659
|
+
return 0;
|
|
660
|
+
}
|
|
661
|
+
out.write(JSON.stringify(await proc.wait()));
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
case "kill": {
|
|
665
|
+
const args = parse(rest);
|
|
666
|
+
const sbx = await get(args.positional[0]);
|
|
667
|
+
const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
|
|
668
|
+
await proc.kill((flag(args, "signal") ?? "SIGTERM"));
|
|
669
|
+
print(`Signalled ${proc.id}.`, { processId: proc.id });
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
case "shell": {
|
|
673
|
+
const args = parse(rest);
|
|
674
|
+
const sbx = await get(args.positional[0]);
|
|
675
|
+
return shell(sbx, flag(args, "command"));
|
|
676
|
+
}
|
|
677
|
+
case "cat": {
|
|
678
|
+
const sbx = await get(rest[0]);
|
|
679
|
+
process.stdout.write(await sbx.files.read(need(rest[1], "a path")));
|
|
680
|
+
return 0;
|
|
681
|
+
}
|
|
682
|
+
case "files": {
|
|
683
|
+
const args = parse(rest);
|
|
684
|
+
const sbx = await get(args.positional[0]);
|
|
685
|
+
const entries = await sbx.files.list(args.positional[1] ?? "/workspace", {
|
|
686
|
+
...(integer(args, "depth") ? { depth: integer(args, "depth") } : {}),
|
|
687
|
+
...(flag(args, "glob") ? { glob: flag(args, "glob") } : {}),
|
|
688
|
+
});
|
|
689
|
+
print(entries
|
|
690
|
+
.map((e) => `${e.type === "directory" ? "d" : "-"} ${String(e.size).padStart(10)} ${e.path}`)
|
|
691
|
+
.join("\n") || "(empty)", entries);
|
|
692
|
+
return 0;
|
|
693
|
+
}
|
|
694
|
+
case "cp": {
|
|
695
|
+
const [from, to] = parse(rest).positional;
|
|
696
|
+
const source = remote(need(from, "a source"));
|
|
697
|
+
const target = remote(need(to, "a destination"));
|
|
698
|
+
if (source && !target)
|
|
699
|
+
await (await get(source.id)).files.download(source.path, to);
|
|
700
|
+
else if (!source && target)
|
|
701
|
+
await (await get(target.id)).files.upload(from, target.path);
|
|
702
|
+
else
|
|
703
|
+
throw usage("Name exactly one side as <sandbox-id>:/path.");
|
|
704
|
+
print(`Copied ${from} to ${to}.`, { from, to });
|
|
705
|
+
return 0;
|
|
706
|
+
}
|
|
707
|
+
case "run-code":
|
|
708
|
+
return runCode(await get(rest[0]), rest.slice(1), out);
|
|
709
|
+
case "preview": {
|
|
710
|
+
const args = parse(rest, ["public"]);
|
|
711
|
+
const sbx = await get(args.positional[0]);
|
|
712
|
+
const port = Number(need(args.positional[1], "a port, e.g. runtime sandbox preview <id> 3000"));
|
|
713
|
+
const preview = await sbx.previews.create(port, {
|
|
714
|
+
...(has(args, "public") ? { visibility: "public" } : {}),
|
|
715
|
+
...(integer(args, "ttl") ? { ttlSeconds: integer(args, "ttl") } : {}),
|
|
716
|
+
});
|
|
717
|
+
print(preview.visibility === "public"
|
|
718
|
+
? preview.url
|
|
719
|
+
: `${preview.urlWithToken ?? preview.url}\nPrivate: send the header x-runtime-preview-token: ${preview.token} (expires ${preview.tokenExpiresAt}).`, preview);
|
|
720
|
+
return 0;
|
|
721
|
+
}
|
|
722
|
+
case "previews": {
|
|
723
|
+
const list = await (await get(rest[0])).previews.list();
|
|
724
|
+
print(list.length
|
|
725
|
+
? table([
|
|
726
|
+
["PORT", "VISIBILITY", "URL"],
|
|
727
|
+
...list.map((p) => [String(p.port), p.visibility, p.url]),
|
|
728
|
+
])
|
|
729
|
+
: "No shared ports. Share one: runtime sandbox preview <id> 3000", list);
|
|
730
|
+
return 0;
|
|
731
|
+
}
|
|
732
|
+
case "unshare": {
|
|
733
|
+
const sbx = await get(rest[0]);
|
|
734
|
+
const port = Number(need(rest[1], "the port"));
|
|
735
|
+
print(`Port ${port} is no longer shared.`, await sbx.previews.delete(port));
|
|
736
|
+
return 0;
|
|
737
|
+
}
|
|
738
|
+
case "network": {
|
|
739
|
+
const args = parse(rest, ["no-internet", "internet"]);
|
|
740
|
+
const sbx = await get(args.positional[0]);
|
|
741
|
+
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
742
|
+
const policy = has(args, "no-internet") || has(args, "internet") || rules.length
|
|
743
|
+
? await sbx.network.set({
|
|
744
|
+
internet: !has(args, "no-internet"),
|
|
745
|
+
...Object.fromEntries(rules.map((name) => [name, args.flags.get(name)])),
|
|
746
|
+
})
|
|
747
|
+
: await sbx.network.get();
|
|
748
|
+
print(table([
|
|
749
|
+
["internet", policy.internet ? "on" : "off"],
|
|
750
|
+
["allow", policy.allow.join(", ") || "(the public web)"],
|
|
751
|
+
["deny", policy.deny.join(", ") || "-"],
|
|
752
|
+
["connect", policy.connect.join(", ") || "-"],
|
|
753
|
+
["enforced", policy.enforced ? "yes" : "not yet; run the same command again"],
|
|
754
|
+
]), policy);
|
|
755
|
+
return 0;
|
|
756
|
+
}
|
|
757
|
+
case "desktop":
|
|
758
|
+
return desktop(await get(rest[0]), rest.slice(1), out);
|
|
759
|
+
}
|
|
760
|
+
throw unknown("sandbox command", verb, SANDBOX_VERBS);
|
|
761
|
+
}
|
|
762
|
+
/** `<volumeId>:/path[:snapshot]`, as the create body wants it. */
|
|
763
|
+
function attachment(spec) {
|
|
764
|
+
const [volumeId, path, mode = "rw"] = spec.split(":");
|
|
765
|
+
if (!volumeId || !path?.startsWith("/") || (mode !== "rw" && mode !== "snapshot"))
|
|
766
|
+
throw usage("--volume takes <volumeId>:/path, or <volumeId>:/path:snapshot for a copy.");
|
|
767
|
+
return { volumeId, path, mode };
|
|
768
|
+
}
|
|
769
|
+
async function readInput(source) {
|
|
770
|
+
if (source !== "-")
|
|
771
|
+
return readFile(source, "utf8");
|
|
772
|
+
const chunks = [];
|
|
773
|
+
for await (const chunk of process.stdin)
|
|
774
|
+
chunks.push(chunk);
|
|
775
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
776
|
+
}
|
|
777
|
+
async function runCode(sbx, argv, out) {
|
|
778
|
+
const args = parse(argv);
|
|
779
|
+
const source = need(args.positional[0], "a file, or - to read the code from standard input");
|
|
780
|
+
const code = await readInput(source);
|
|
781
|
+
const language = (flag(args, "lang") ??
|
|
782
|
+
(/\.(m?js|ts)$/.test(source) ? "javascript" : "python"));
|
|
783
|
+
const timeout = integer(args, "timeout");
|
|
784
|
+
const execution = await sbx.interpreter.run(code, {
|
|
785
|
+
language,
|
|
786
|
+
...(flag(args, "context") ? { context: flag(args, "context") } : {}),
|
|
787
|
+
...(timeout ? { timeoutMs: timeout * 1000 } : {}),
|
|
788
|
+
...(out.json
|
|
789
|
+
? {}
|
|
790
|
+
: {
|
|
791
|
+
onStdout: (text) => process.stdout.write(text),
|
|
792
|
+
onStderr: (text) => process.stderr.write(text),
|
|
793
|
+
}),
|
|
794
|
+
});
|
|
795
|
+
const saved = [];
|
|
796
|
+
const outDir = flag(args, "out-dir") ?? ".";
|
|
797
|
+
for (const [index, result] of execution.results.entries()) {
|
|
798
|
+
const inline = result.data["image/png"];
|
|
799
|
+
const ref = result.refs["image/png"];
|
|
800
|
+
if (typeof inline !== "string" && !ref)
|
|
801
|
+
continue;
|
|
802
|
+
const bytes = typeof inline === "string"
|
|
803
|
+
? Buffer.from(inline, "base64")
|
|
804
|
+
: await sbx.interpreter.result(ref);
|
|
805
|
+
const file = join(outDir, `result-${execution.executionCount ?? 0}-${index + 1}.png`);
|
|
806
|
+
await writeFile(file, bytes);
|
|
807
|
+
saved.push(file);
|
|
808
|
+
}
|
|
809
|
+
if (out.json)
|
|
810
|
+
out.write(JSON.stringify({ ...execution, saved }));
|
|
811
|
+
else {
|
|
812
|
+
for (const result of execution.results) {
|
|
813
|
+
const text = result.data["text/plain"];
|
|
814
|
+
if (typeof text === "string" && !result.data["image/png"])
|
|
815
|
+
out.write(text);
|
|
816
|
+
}
|
|
817
|
+
for (const file of saved)
|
|
818
|
+
out.write(`Saved ${file}`);
|
|
819
|
+
if (execution.error)
|
|
820
|
+
out.error(execution.error.traceback || `${execution.error.name}: ${execution.error.value}`);
|
|
821
|
+
if (execution.status === "timeout")
|
|
822
|
+
out.error("runtime: the cell timed out.");
|
|
823
|
+
}
|
|
824
|
+
return execution.status === "timeout" ? 124 : execution.error ? 1 : 0;
|
|
825
|
+
}
|
|
826
|
+
async function desktop(sbx, argv, out) {
|
|
827
|
+
const [action, ...rest] = argv;
|
|
828
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
829
|
+
const d = sbx.desktop;
|
|
830
|
+
switch (action) {
|
|
831
|
+
case "start": {
|
|
832
|
+
const args = parse(rest);
|
|
833
|
+
const started = await d.start({
|
|
834
|
+
...(integer(args, "width") ? { width: integer(args, "width") } : {}),
|
|
835
|
+
...(integer(args, "height") ? { height: integer(args, "height") } : {}),
|
|
836
|
+
});
|
|
837
|
+
print(`Desktop ${started.size} on ${started.display}. Watch it: ${started.streamUrl}`, started);
|
|
838
|
+
return 0;
|
|
839
|
+
}
|
|
840
|
+
case "stop":
|
|
841
|
+
print("Desktop stopped.", await d.stop());
|
|
842
|
+
return 0;
|
|
843
|
+
case "screenshot": {
|
|
844
|
+
const file = rest[0] ?? "screenshot.png";
|
|
845
|
+
await writeFile(file, await d.screenshot());
|
|
846
|
+
print(`Saved ${file}`, { file });
|
|
847
|
+
return 0;
|
|
848
|
+
}
|
|
849
|
+
case "open":
|
|
850
|
+
print("Opened.", await d.open(need(rest[0], "a URL")));
|
|
851
|
+
return 0;
|
|
852
|
+
case "click":
|
|
853
|
+
print("Clicked.", await d.click(Number(need(rest[0], "x")), Number(need(rest[1], "y"))));
|
|
854
|
+
return 0;
|
|
855
|
+
case "type":
|
|
856
|
+
print("Typed.", await d.type(need(rest.join(" "), "the text")));
|
|
857
|
+
return 0;
|
|
858
|
+
case "press":
|
|
859
|
+
print("Pressed.", await d.press(need(rest[0], "keys, e.g. ctrl+l")));
|
|
860
|
+
return 0;
|
|
861
|
+
case "windows": {
|
|
862
|
+
const windows = await d.windows();
|
|
863
|
+
print(windows.length
|
|
864
|
+
? table([["WINDOW", "TITLE"], ...windows.map((w) => [w.id, w.title])])
|
|
865
|
+
: "No windows.", windows);
|
|
866
|
+
return 0;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
throw usage(`${me} sandbox desktop <id> start|stop|screenshot|open|click|type|press|windows`);
|
|
870
|
+
}
|
|
871
|
+
const SNAPSHOT_HELP = `runtime snapshot <command>
|
|
872
|
+
|
|
873
|
+
ls [--sandbox <id>] [--name n] Your snapshots
|
|
874
|
+
get <id> One snapshot
|
|
875
|
+
extend <id> <days> Keep it longer
|
|
876
|
+
rm <id> Delete it
|
|
877
|
+
Take one with \`runtime sandbox snapshot <sandboxId>\`; start from one with
|
|
878
|
+
\`runtime sandbox fork\` or the SDK's fork().
|
|
879
|
+
`;
|
|
880
|
+
async function snapshot(argv, env, out) {
|
|
881
|
+
const [verb, ...rest] = argv;
|
|
882
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
883
|
+
if (!verb || verb === "help" || verb === "--help") {
|
|
884
|
+
print(named(SNAPSHOT_HELP), { usage: named(SNAPSHOT_HELP) });
|
|
885
|
+
return 0;
|
|
886
|
+
}
|
|
887
|
+
const api = (await client(env)).snapshots;
|
|
888
|
+
switch (verb) {
|
|
889
|
+
case "ls":
|
|
890
|
+
case "list": {
|
|
891
|
+
const args = parse(rest);
|
|
892
|
+
const all = await (await api.list({
|
|
893
|
+
...(flag(args, "sandbox") ? { sandboxId: flag(args, "sandbox") } : {}),
|
|
894
|
+
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
895
|
+
})).toArray(1000);
|
|
896
|
+
print(all.length
|
|
897
|
+
? table([
|
|
898
|
+
["ID", "NAME", "STATUS", "FROM", "EXPIRES"],
|
|
899
|
+
...all.map((s) => [s.id, s.name ?? "-", s.status, s.sourceSandboxId, s.expiresAt]),
|
|
900
|
+
])
|
|
901
|
+
: "No snapshots. Take one: runtime sandbox snapshot <sandboxId>", all);
|
|
902
|
+
return 0;
|
|
903
|
+
}
|
|
904
|
+
case "get": {
|
|
905
|
+
const found = await api.get(need(rest[0], "a snapshot id"));
|
|
906
|
+
print(JSON.stringify(found, null, 2), found);
|
|
907
|
+
return 0;
|
|
908
|
+
}
|
|
909
|
+
case "extend": {
|
|
910
|
+
const days = integer(parse(rest.slice(1)), "days") ?? Number(need(rest[1], "days"));
|
|
911
|
+
const found = await api.extend(need(rest[0], "a snapshot id"), days);
|
|
912
|
+
print(`${found.id} kept until ${found.expiresAt}`, found);
|
|
913
|
+
return 0;
|
|
914
|
+
}
|
|
915
|
+
case "rm":
|
|
916
|
+
case "delete": {
|
|
917
|
+
const id = need(rest[0], "a snapshot id");
|
|
918
|
+
await api.delete(id);
|
|
919
|
+
print(`Deleted ${id}.`, { id, deleted: true });
|
|
920
|
+
return 0;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
throw unknown("snapshot command", verb, ["ls", "get", "extend", "rm"]);
|
|
924
|
+
}
|
|
925
|
+
const IMAGE_HELP = `runtime image <command>
|
|
926
|
+
|
|
927
|
+
build (--dockerfile <path> | --from <public image> | --pip <pkg>... --apt <pkg>... --npm <pkg>...)
|
|
928
|
+
[--name n] [--max-image-mib 4096] [--timeout <seconds>]
|
|
929
|
+
Build and stream the log; prints the image id when ready.
|
|
930
|
+
A Dockerfile's COPY and ADD sources are sent from its folder
|
|
931
|
+
(at most 256 files, 1 MiB in all).
|
|
932
|
+
ls Your images
|
|
933
|
+
get <id> One image
|
|
934
|
+
logs <id> Its build log
|
|
935
|
+
rm <id> Delete it
|
|
936
|
+
Use one: runtime sandbox create --image <id>
|
|
937
|
+
`;
|
|
938
|
+
async function image(argv, env, out) {
|
|
939
|
+
const [verb, ...rest] = argv;
|
|
940
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
941
|
+
if (!verb || verb === "help" || verb === "--help") {
|
|
942
|
+
print(named(IMAGE_HELP), { usage: named(IMAGE_HELP) });
|
|
943
|
+
return 0;
|
|
944
|
+
}
|
|
945
|
+
const api = (await client(env)).images;
|
|
946
|
+
switch (verb) {
|
|
947
|
+
case "build": {
|
|
948
|
+
const args = parse(rest);
|
|
949
|
+
const list = (name) => (args.flags.get(name) ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean);
|
|
950
|
+
const recipe = { apt: list("apt"), pip: list("pip"), npm: list("npm") };
|
|
951
|
+
const hasRecipe = recipe.apt.length + recipe.pip.length + recipe.npm.length > 0;
|
|
952
|
+
const sources = [flag(args, "dockerfile"), flag(args, "from"), hasRecipe || undefined];
|
|
953
|
+
if (sources.filter((s) => s !== undefined).length !== 1)
|
|
954
|
+
throw usage("Give exactly one of --dockerfile <path>, --from <image>, or --pip/--apt/--npm.");
|
|
955
|
+
const build = {
|
|
956
|
+
...(integer(args, "max-image-mib") ? { maxImageMiB: integer(args, "max-image-mib") } : {}),
|
|
957
|
+
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
958
|
+
};
|
|
959
|
+
const common = {
|
|
960
|
+
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
961
|
+
...(Object.keys(build).length ? { build } : {}),
|
|
962
|
+
};
|
|
963
|
+
const dockerfile = flag(args, "dockerfile");
|
|
964
|
+
const input = dockerfile
|
|
965
|
+
? { ...common, ...(await dockerContext(dockerfile)) }
|
|
966
|
+
: flag(args, "from")
|
|
967
|
+
? { ...common, image: flag(args, "from") }
|
|
968
|
+
: {
|
|
969
|
+
...common,
|
|
970
|
+
recipe: Object.fromEntries(Object.entries(recipe).filter(([, v]) => v.length)),
|
|
971
|
+
};
|
|
972
|
+
const built = await api.build(input, {
|
|
973
|
+
...(out.json ? {} : { onLog: (line) => out.error(line.text) }),
|
|
974
|
+
});
|
|
975
|
+
print(built.id, built);
|
|
976
|
+
return 0;
|
|
977
|
+
}
|
|
978
|
+
case "ls":
|
|
979
|
+
case "list": {
|
|
980
|
+
const all = await (await api.list()).toArray(1000);
|
|
981
|
+
print(all.length
|
|
982
|
+
? table([
|
|
983
|
+
["ID", "NAME", "STATE", "MIB", "CREATED"],
|
|
984
|
+
...all.map((i) => [
|
|
985
|
+
i.id,
|
|
986
|
+
i.name ?? "-",
|
|
987
|
+
i.state,
|
|
988
|
+
String(i.sizeMiB ?? "-"),
|
|
989
|
+
i.createdAt,
|
|
990
|
+
]),
|
|
991
|
+
])
|
|
992
|
+
: "No images. Build one: runtime image build --pip pandas", all);
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
995
|
+
case "get": {
|
|
996
|
+
const found = await api.get(need(rest[0], "an image id"));
|
|
997
|
+
print(JSON.stringify(found, null, 2), found);
|
|
998
|
+
return 0;
|
|
999
|
+
}
|
|
1000
|
+
case "logs": {
|
|
1001
|
+
const id = need(rest[0], "an image id");
|
|
1002
|
+
let after = 0;
|
|
1003
|
+
const lines = [];
|
|
1004
|
+
for (;;) {
|
|
1005
|
+
const page = await api.logs(id, after);
|
|
1006
|
+
lines.push(...page.lines.map((line) => line.text));
|
|
1007
|
+
if (!page.lines.length || page.nextAfter === after)
|
|
1008
|
+
break;
|
|
1009
|
+
after = page.nextAfter;
|
|
1010
|
+
}
|
|
1011
|
+
print(lines.join("\n") || "(no log yet)", lines);
|
|
1012
|
+
return 0;
|
|
1013
|
+
}
|
|
1014
|
+
case "rm":
|
|
1015
|
+
case "delete": {
|
|
1016
|
+
const found = await api.delete(need(rest[0], "an image id"));
|
|
1017
|
+
print(`${found.id} ${found.state}`, found);
|
|
1018
|
+
return 0;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
throw unknown("image command", verb, ["build", "ls", "get", "logs", "rm"]);
|
|
1022
|
+
}
|
|
1023
|
+
/** The Dockerfile and the files its COPY and ADD lines name, from its folder. */
|
|
1024
|
+
async function dockerContext(path) {
|
|
1025
|
+
const dockerfile = await readFile(path, "utf8");
|
|
1026
|
+
const root = dirname(resolve(path));
|
|
1027
|
+
const sources = new Set();
|
|
1028
|
+
for (const line of dockerfile.replace(/\\\r?\n/g, " ").split(/\r?\n/)) {
|
|
1029
|
+
const match = /^\s*(?:COPY|ADD)\s+(.+)$/i.exec(line);
|
|
1030
|
+
if (!match || /--from=/i.test(match[1]))
|
|
1031
|
+
continue;
|
|
1032
|
+
const body = match[1].trim();
|
|
1033
|
+
const words = body.startsWith("[")
|
|
1034
|
+
? JSON.parse(body)
|
|
1035
|
+
: body.split(/\s+/).filter((word) => !word.startsWith("--"));
|
|
1036
|
+
for (const word of words.slice(0, -1))
|
|
1037
|
+
if (!/^https?:\/\//.test(word))
|
|
1038
|
+
sources.add(word);
|
|
1039
|
+
}
|
|
1040
|
+
const files = [];
|
|
1041
|
+
let total = 0;
|
|
1042
|
+
const add = async (absolute) => {
|
|
1043
|
+
const info = await stat(absolute);
|
|
1044
|
+
if (info.isDirectory()) {
|
|
1045
|
+
for (const entry of await readdir(absolute))
|
|
1046
|
+
if (entry !== ".git")
|
|
1047
|
+
await add(join(absolute, entry));
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const name = relative(root, absolute).split(sep).join("/");
|
|
1051
|
+
if (files.some((file) => file.path === name))
|
|
1052
|
+
return;
|
|
1053
|
+
const bytes = await readFile(absolute);
|
|
1054
|
+
total += bytes.length;
|
|
1055
|
+
if (files.length >= 256 || total > 1024 * 1024)
|
|
1056
|
+
throw usage("The Dockerfile's context is over 256 files or 1 MiB. Build from --from, or install packages with --pip/--apt/--npm.");
|
|
1057
|
+
files.push({
|
|
1058
|
+
path: name,
|
|
1059
|
+
content: bytes.toString("base64"),
|
|
1060
|
+
encoding: "base64",
|
|
1061
|
+
mode: info.mode & 0o777,
|
|
1062
|
+
});
|
|
1063
|
+
};
|
|
1064
|
+
for (const source of sources) {
|
|
1065
|
+
const absolute = resolve(root, source);
|
|
1066
|
+
if (absolute !== root && !absolute.startsWith(root + sep))
|
|
1067
|
+
throw usage(`COPY ${source} is outside the Dockerfile's folder.`);
|
|
1068
|
+
if (/[*?[]/.test(source)) {
|
|
1069
|
+
const pattern = new RegExp(`^${source
|
|
1070
|
+
.split("/")
|
|
1071
|
+
.at(-1)
|
|
1072
|
+
.replace(/[.+^${}()|\\]/g, "\\$&")
|
|
1073
|
+
.replace(/\*/g, "[^/]*")
|
|
1074
|
+
.replace(/\?/g, "[^/]")}$`);
|
|
1075
|
+
const folder = dirname(absolute);
|
|
1076
|
+
for (const entry of await readdir(folder))
|
|
1077
|
+
if (pattern.test(entry))
|
|
1078
|
+
await add(join(folder, entry));
|
|
1079
|
+
}
|
|
1080
|
+
else
|
|
1081
|
+
await add(absolute);
|
|
1082
|
+
}
|
|
1083
|
+
return { dockerfile, files };
|
|
1084
|
+
}
|
|
1085
|
+
const VOLUME_HELP = `runtime volume <command>
|
|
1086
|
+
|
|
1087
|
+
create --size-mib <N> [--name n] A persistent ext4 disk; prints its id
|
|
1088
|
+
ls Your volumes
|
|
1089
|
+
get <id> One volume and where it is attached
|
|
1090
|
+
rm <id> Delete it and everything on it
|
|
1091
|
+
Attach one: runtime sandbox create --volume <id>:/data
|
|
1092
|
+
A volume lives on one server and is not backed up off it.
|
|
1093
|
+
`;
|
|
1094
|
+
async function volume(argv, env, out) {
|
|
1095
|
+
const [verb, ...rest] = argv;
|
|
1096
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
1097
|
+
if (!verb || verb === "help" || verb === "--help") {
|
|
1098
|
+
print(named(VOLUME_HELP), { usage: named(VOLUME_HELP) });
|
|
1099
|
+
return 0;
|
|
1100
|
+
}
|
|
1101
|
+
const api = (await client(env)).volumes;
|
|
1102
|
+
switch (verb) {
|
|
1103
|
+
case "create": {
|
|
1104
|
+
const args = parse(rest);
|
|
1105
|
+
const created = await api.create({
|
|
1106
|
+
sizeMiB: integer(args, "size-mib") ?? Number(need(undefined, "--size-mib <N>")),
|
|
1107
|
+
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1108
|
+
});
|
|
1109
|
+
if (!out.json)
|
|
1110
|
+
out.error("Not backed up off its server.");
|
|
1111
|
+
print(created.id, created);
|
|
1112
|
+
return 0;
|
|
1113
|
+
}
|
|
1114
|
+
case "ls":
|
|
1115
|
+
case "list": {
|
|
1116
|
+
const all = await (await api.list()).toArray(1000);
|
|
1117
|
+
print(all.length
|
|
1118
|
+
? table([
|
|
1119
|
+
["ID", "NAME", "STATE", "MIB", "ATTACHED"],
|
|
1120
|
+
...all.map((v) => [
|
|
1121
|
+
v.id,
|
|
1122
|
+
v.name ?? "-",
|
|
1123
|
+
v.state,
|
|
1124
|
+
String(v.sizeMiB),
|
|
1125
|
+
v.attachments.map((a) => `${a.sandboxId}:${a.path}`).join(", ") || "-",
|
|
1126
|
+
]),
|
|
1127
|
+
])
|
|
1128
|
+
: "No volumes. Create one: runtime volume create --size-mib 10240", all);
|
|
1129
|
+
return 0;
|
|
1130
|
+
}
|
|
1131
|
+
case "get": {
|
|
1132
|
+
const found = await api.get(need(rest[0], "a volume id"));
|
|
1133
|
+
print(JSON.stringify(found, null, 2), found);
|
|
1134
|
+
return 0;
|
|
1135
|
+
}
|
|
1136
|
+
case "rm":
|
|
1137
|
+
case "delete": {
|
|
1138
|
+
const found = await api.delete(need(rest[0], "a volume id"));
|
|
1139
|
+
print(`${found.id} ${found.state}`, found);
|
|
1140
|
+
return 0;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
throw unknown("volume command", verb, ["create", "ls", "get", "rm"]);
|
|
1144
|
+
}
|
|
1145
|
+
/** Flags `sandbox create` and `run` take without a value. */
|
|
1146
|
+
const CREATE_FLAGS = ["trial", "paid", "no-wait", "no-internet"];
|
|
1147
|
+
/** A create body from the command line's flags. */
|
|
1148
|
+
function createInput(args) {
|
|
1149
|
+
const rules = ["allow", "deny", "connect"].filter((name) => args.flags.has(name));
|
|
1150
|
+
return {
|
|
1151
|
+
...(flag(args, "image") ? { image: flag(args, "image") } : {}),
|
|
1152
|
+
...(flag(args, "snapshot") ? { snapshot: flag(args, "snapshot") } : {}),
|
|
1153
|
+
...(args.flags.has("volume") ? { volumes: args.flags.get("volume").map(attachment) } : {}),
|
|
1154
|
+
...(has(args, "no-internet") || rules.length
|
|
1155
|
+
? {
|
|
1156
|
+
network: {
|
|
1157
|
+
internet: !has(args, "no-internet"),
|
|
1158
|
+
...Object.fromEntries(rules.map((name) => [name, args.flags.get(name)])),
|
|
1159
|
+
},
|
|
1160
|
+
}
|
|
1161
|
+
: {}),
|
|
1162
|
+
...(flag(args, "name") ? { name: flag(args, "name") } : {}),
|
|
1163
|
+
...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
|
|
1164
|
+
...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
|
|
1165
|
+
...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
|
|
1166
|
+
...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
|
|
1167
|
+
...(integer(args, "timeout") ? { timeoutSeconds: integer(args, "timeout") } : {}),
|
|
1168
|
+
...(flag(args, "on-timeout")
|
|
1169
|
+
? { onLeaseEnd: flag(args, "on-timeout") }
|
|
1170
|
+
: {}),
|
|
1171
|
+
...(has(args, "trial")
|
|
1172
|
+
? { funding: "trial" }
|
|
1173
|
+
: has(args, "paid")
|
|
1174
|
+
? { funding: "paid" }
|
|
1175
|
+
: {}),
|
|
1176
|
+
...(has(args, "no-wait") ? { wait: false } : {}),
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
/** Runs a command in a sandbox, streaming its output, and answers its exit code
|
|
1180
|
+
* (124 on a timeout), so a script's `set -e` behaves. */
|
|
1181
|
+
async function execute(sbx, command, args, out, timeout) {
|
|
1182
|
+
const target = command.length === 1 ? command[0] : command;
|
|
1183
|
+
const options = {
|
|
1184
|
+
...(flag(args, "cwd") ? { cwd: flag(args, "cwd") } : {}),
|
|
1185
|
+
...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
|
|
1186
|
+
...(timeout ? { timeoutMs: timeout * 1000 } : {}),
|
|
1187
|
+
};
|
|
1188
|
+
const result = out.json
|
|
1189
|
+
? await sbx.exec(target, options)
|
|
1190
|
+
: await sbx.exec(target, {
|
|
1191
|
+
...options,
|
|
1192
|
+
onStdout: (t) => process.stdout.write(t),
|
|
1193
|
+
onStderr: (t) => process.stderr.write(t),
|
|
1194
|
+
});
|
|
1195
|
+
if (out.json)
|
|
1196
|
+
out.write(JSON.stringify(result));
|
|
1197
|
+
else if (result.timedOut)
|
|
1198
|
+
out.error(`runtime: timed out after ${timeout ?? 60} s; the command's process group was killed.`);
|
|
1199
|
+
return result.timedOut ? 124 : Math.min(255, Math.max(0, result.exitCode ?? 1));
|
|
1200
|
+
}
|
|
1201
|
+
async function shell(sbx, command) {
|
|
1202
|
+
const stdin = process.stdin;
|
|
1203
|
+
const size = () => ({ cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 });
|
|
1204
|
+
const terminal = await sbx.terminal({
|
|
1205
|
+
...size(),
|
|
1206
|
+
...(command ? { command } : {}),
|
|
1207
|
+
onData: (data) => process.stdout.write(data),
|
|
1208
|
+
});
|
|
1209
|
+
if (stdin.isTTY)
|
|
1210
|
+
stdin.setRawMode(true);
|
|
1211
|
+
stdin.resume();
|
|
1212
|
+
const onInput = (chunk) => terminal.write(new Uint8Array(chunk));
|
|
1213
|
+
const onResize = () => terminal.resize(size().cols, size().rows);
|
|
1214
|
+
stdin.on("data", onInput);
|
|
1215
|
+
process.stdout.on("resize", onResize);
|
|
1216
|
+
const code = await terminal.exited;
|
|
1217
|
+
stdin.off("data", onInput);
|
|
1218
|
+
process.stdout.off("resize", onResize);
|
|
1219
|
+
if (stdin.isTTY)
|
|
1220
|
+
stdin.setRawMode(false);
|
|
1221
|
+
stdin.pause();
|
|
1222
|
+
return code ?? 0;
|
|
1223
|
+
}
|
|
1224
|
+
function defaultOut(argv) {
|
|
1225
|
+
const json = argv.includes("--json") &&
|
|
1226
|
+
(argv.indexOf("--") === -1 || argv.indexOf("--json") < argv.indexOf("--"));
|
|
1227
|
+
return {
|
|
1228
|
+
json,
|
|
1229
|
+
write: (text) => process.stdout.write(text.endsWith("\n") ? text : `${text}\n`),
|
|
1230
|
+
error: (text) => process.stderr.write(`${text}\n`),
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
export function describeError(error, json) {
|
|
1234
|
+
if (error instanceof RuntimeError) {
|
|
1235
|
+
if (json)
|
|
1236
|
+
return JSON.stringify({
|
|
1237
|
+
error: {
|
|
1238
|
+
code: error.code,
|
|
1239
|
+
status: error.status,
|
|
1240
|
+
message: error.message,
|
|
1241
|
+
hint: error.hint,
|
|
1242
|
+
requestId: error.requestId,
|
|
1243
|
+
idempotencyKey: error.idempotencyKey,
|
|
1244
|
+
details: error.details,
|
|
1245
|
+
},
|
|
1246
|
+
});
|
|
1247
|
+
return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId ? `\nRequest: ${error.requestId} (report it: runtime feedback "..." --request-id ${error.requestId})` : ""}`;
|
|
1248
|
+
}
|
|
1249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1250
|
+
return json ? JSON.stringify({ error: { code: "cli_error", message } }) : `Error: ${message}`;
|
|
1251
|
+
}
|
|
1252
|
+
if (import.meta.main ||
|
|
1253
|
+
(process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))) {
|
|
1254
|
+
const argv = process.argv.slice(2);
|
|
1255
|
+
const out = defaultOut(argv);
|
|
1256
|
+
run(argv, process.env, out).then((code) => {
|
|
1257
|
+
process.exitCode = code;
|
|
1258
|
+
}, (error) => {
|
|
1259
|
+
out.error(describeError(error, out.json));
|
|
1260
|
+
process.exitCode = 1;
|
|
1261
|
+
});
|
|
1262
|
+
}
|