withruntime 0.2.2 → 0.3.1

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