withruntime 0.33.3 → 0.34.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.
Files changed (43) hide show
  1. package/E2B.md +7 -0
  2. package/dist/agents-cli.d.ts +1 -1
  3. package/dist/agents-cli.js +171 -7
  4. package/dist/agents-deploy.d.ts +13 -3
  5. package/dist/agents-deploy.js +68 -9
  6. package/dist/apps-cli.d.ts +1 -1
  7. package/dist/apps-cli.js +41 -9
  8. package/dist/browsers/index.d.ts +9 -0
  9. package/dist/browsers/index.js +10 -0
  10. package/dist/browsers-cli.d.ts +13 -0
  11. package/dist/browsers-cli.js +160 -0
  12. package/dist/ci-cli.d.ts +1 -1
  13. package/dist/ci-cli.js +16 -3
  14. package/dist/cli.d.ts +4 -0
  15. package/dist/cli.js +94 -10
  16. package/dist/desktops/index.d.ts +9 -0
  17. package/dist/desktops/index.js +10 -0
  18. package/dist/e2b/code-interpreter.d.ts +3 -12
  19. package/dist/e2b/code-interpreter.js +3 -6
  20. package/dist/e2b/index.d.ts +7 -1
  21. package/dist/e2b/index.js +9 -2
  22. package/dist/index.d.ts +5 -3
  23. package/dist/products/agents.d.ts +162 -8
  24. package/dist/products/agents.js +108 -3
  25. package/dist/products/apps.d.ts +46 -5
  26. package/dist/products/browsers.d.ts +26 -0
  27. package/dist/products/browsers.js +11 -0
  28. package/dist/products/ci.d.ts +10 -4
  29. package/dist/products/ci.js +8 -0
  30. package/dist/products/desktops.d.ts +71 -0
  31. package/dist/products/desktops.js +27 -0
  32. package/dist/products/index.d.ts +4 -0
  33. package/dist/products/index.js +5 -0
  34. package/dist/products/network.d.ts +4 -2
  35. package/dist/products/secrets.d.ts +10 -2
  36. package/dist/products/secrets.js +27 -7
  37. package/dist/products/uptime.d.ts +97 -0
  38. package/dist/products/uptime.js +42 -0
  39. package/dist/products/volumes.d.ts +41 -9
  40. package/dist/products/volumes.js +5 -2
  41. package/dist/transport.d.ts +4 -1
  42. package/dist/transport.js +2 -2
  43. package/package.json +12 -1
package/dist/apps-cli.js CHANGED
@@ -13,11 +13,17 @@ export const APP_HELP = `runtime apps <command>
13
13
  create [<name>] --vcpu <1-16> --memory <128-65536> --max-copies <1-1000>
14
14
  [--min-copies 0] [--concurrency 100] [--spares 2] [--port 8080]
15
15
  [--health-path /healthz] [--health-fail 60] [--stop-grace 30] [--cdn on|off]
16
- [--volume <volumeId>:<path>] [--label key=value]...
16
+ [--release at-once|staged] [--soak 300] [--volume <volumeId>:<path>] [--label key=value]...
17
17
  Make an App, waiting for its first deploy; prints its id.
18
18
  --health-fail: seconds a live copy may fail its health
19
19
  check before it is replaced. --stop-grace: seconds a
20
- stopped copy has after SIGTERM. --volume mounts one
20
+ stopped copy has after SIGTERM. --release staged: a new
21
+ version goes live on one copy, then about 10%, 50% and
22
+ all of them, each stage running --soak seconds (60 to
23
+ 3600) and judged on its visitors' errors and speed
24
+ before the next; a stage that does worse goes back and
25
+ the deploy fails, the live version never stopping.
26
+ --volume mounts one
21
27
  volume of yours at a path under /data, /mnt, /state,
22
28
  /storage or /volumes, in the App's one copy: it needs
23
29
  --max-copies 1 (--min-copies is then 1, --spares 0), and
@@ -27,7 +33,7 @@ export const APP_HELP = `runtime apps <command>
27
33
  update <app> [--name <name>] [--vcpu n] [--memory n] [--min-copies n] [--max-copies n]
28
34
  [--concurrency n] [--spares n] [--port n] [--health-path /path] [--health-fail n]
29
35
  [--stop-grace n] [--volume <volumeId>:<path> | none] [--cdn on|off]
30
- [--label key=value]...
36
+ [--release at-once|staged] [--soak n] [--label key=value]...
31
37
  Change its settings; --volume none takes the volume
32
38
  away, only before its first deploy;
33
39
  --cdn off sends visitors straight to Runtime's routers,
@@ -224,6 +230,19 @@ function onOff(value, kit) {
224
230
  return false;
225
231
  throw kit.usage(`--cdn takes on or off, not ${value}.`);
226
232
  }
233
+ /** --release at-once|staged and --soak <seconds>: how an App's new version
234
+ * goes live. */
235
+ function releases(args, kit) {
236
+ const given = kit.flag(args, "release");
237
+ const soak = kit.integer(args, "soak");
238
+ const stages = given?.replace("-", "_");
239
+ if (stages !== undefined && stages !== "at_once" && stages !== "staged")
240
+ throw kit.usage(`--release takes at-once or staged, not ${given}.`);
241
+ return {
242
+ ...(stages === undefined ? {} : { releaseStages: stages }),
243
+ ...(soak === undefined ? {} : { releaseSoakSeconds: soak }),
244
+ };
245
+ }
227
246
  /** --volume <volumeId>:<path>, or none (an update's: no volume). */
228
247
  function volume(args, kit) {
229
248
  const given = kit.flag(args, "volume");
@@ -239,8 +258,8 @@ function volume(args, kit) {
239
258
  return { volume: { volumeId, path } };
240
259
  }
241
260
  /** The flags an App's settings are given with, as the API names them. A
242
- * Function's leave out --min-copies, --stop-grace and --volume, which its
243
- * parse does not take. */
261
+ * Function's leave out --min-copies, --stop-grace, --volume, --release and
262
+ * --soak, which its parse does not take. */
244
263
  function settings(args, kit, kind) {
245
264
  const { integer, flag } = kit;
246
265
  const number = (name, field) => integer(args, name) === undefined ? {} : { [field]: integer(args, name) };
@@ -256,7 +275,11 @@ function settings(args, kit, kind) {
256
275
  ...(flag(args, "cdn") === undefined ? {} : { cdn: onOff(flag(args, "cdn"), kit) }),
257
276
  ...number("health-fail", "healthFailSeconds"),
258
277
  ...(kind.minCopies
259
- ? { ...number("stop-grace", "stopGraceSeconds"), ...volume(args, kit) }
278
+ ? {
279
+ ...number("stop-grace", "stopGraceSeconds"),
280
+ ...volume(args, kit),
281
+ ...releases(args, kit),
282
+ }
260
283
  : {}),
261
284
  };
262
285
  }
@@ -273,11 +296,13 @@ const SETTINGS = [
273
296
  "health-fail",
274
297
  "stop-grace",
275
298
  "volume",
299
+ "release",
300
+ "soak",
276
301
  "label",
277
302
  ];
278
303
  const settingsOf = (kind) => kind.minCopies
279
304
  ? SETTINGS
280
- : SETTINGS.filter((name) => !["min-copies", "stop-grace", "volume"].includes(name));
305
+ : SETTINGS.filter((name) => !["min-copies", "stop-grace", "volume", "release", "soak"].includes(name));
281
306
  /** A version's copies replaced while live: how many, and why. */
282
307
  function replaced(list) {
283
308
  if (!list?.length)
@@ -418,9 +443,16 @@ async function copiesCommand(kind, argv, env, out, client, kit) {
418
443
  ...(healthPath === undefined ? {} : { healthPath }),
419
444
  wait,
420
445
  };
446
+ // A staged App's deploy walks up to four stages, each soaking.
447
+ const app = wait && kind.minCopies ? await api.get(id) : undefined;
448
+ const staged = app && "releaseStages" in app && app.releaseStages === "staged" ? app : undefined;
421
449
  if (wait && !out.json)
422
- out.error(`Deploying ${args.positional[0]}...`);
423
- const deploy = await api.deploy(id, input, { timeoutMs: 15 * 60_000 });
450
+ out.error(staged
451
+ ? `Deploying ${args.positional[0]} in stages, each soaking ${staged.releaseSoakSeconds} s...`
452
+ : `Deploying ${args.positional[0]}...`);
453
+ const deploy = await api.deploy(id, input, {
454
+ timeoutMs: 15 * 60_000 + (staged ? 4 * staged.releaseSoakSeconds * 1000 : 0),
455
+ });
424
456
  const failed = deployFailure(deploy);
425
457
  if (failed && !out.json)
426
458
  throw failed;
@@ -0,0 +1,9 @@
1
+ /** `withruntime/browsers`: Browsers alone. Chromium on its own sandbox, with a private CDP address and screenshots, at the sandbox rates. In a private beta.
2
+ *
3
+ * import { browsers } from "withruntime/browsers";
4
+ * const client = browsers(); // RUNTIME_API_KEY, or this machine's saved connection
5
+ */
6
+ import { Runtime, type RuntimeOptions } from "../client.js";
7
+ export type * from "../products/browsers.js";
8
+ /** `runtime.browsers` of a client made with `options`. */
9
+ export declare function browsers(options?: RuntimeOptions): Runtime["browsers"];
@@ -0,0 +1,10 @@
1
+ /** `withruntime/browsers`: Browsers alone. Chromium on its own sandbox, with a private CDP address and screenshots, at the sandbox rates. In a private beta.
2
+ *
3
+ * import { browsers } from "withruntime/browsers";
4
+ * const client = browsers(); // RUNTIME_API_KEY, or this machine's saved connection
5
+ */
6
+ import { Runtime } from "../client.js";
7
+ /** `runtime.browsers` of a client made with `options`. */
8
+ export function browsers(options) {
9
+ return new Runtime(options).browsers;
10
+ }
@@ -0,0 +1,13 @@
1
+ import type { CliKit } from "./cli.js";
2
+ import type { Runtime } from "./client.js";
3
+ type Out = {
4
+ json: boolean;
5
+ write: (text: string) => void;
6
+ error: (text: string) => void;
7
+ };
8
+ export declare const BROWSER_HELP = "runtime browsers <command>\n\n A browser is Chromium on its own sandbox, billed at the sandbox rates, with a\n private CDP address for Playwright, Puppeteer, browser-use or Stagehand.\n In a private beta: outside it, each command says how to ask to join.\n\n create [--memory <1024-16384>] [--vcpu <1-16>] [--visible] [--width <px>] [--height <px>]\n [--timeout <seconds>] [--name <name>] [--label key=value]...\n Start one; prints its id, then its cdpUrl\n ls Your browsers\n get <browser> One browser, with a fresh cdpUrl (an id or its name)\n screenshot <browser> [<url>] [--out <file.png>] [--full] [--jpeg] [--wait <ms>]\n Open url in a new tab and save its screenshot\n (without url: what it shows now)\n rm <browser> Delete it and its sandbox\n\nExamples\n runtime browsers create --name scraper\n runtime browsers screenshot scraper https://withruntime.com --out home.png\n runtime browsers rm scraper\n";
9
+ export declare const DESKTOP_HELP = "runtime desktops <command>\n\n A desktop is a Linux desktop on its own sandbox, billed at the sandbox rates,\n with a private live view and computer use: screenshot, click, type, open.\n In a private beta: outside it, each command says how to ask to join.\n\n create [--memory <1024-16384>] [--vcpu <1-16>] [--width <px>] [--height <px>]\n [--timeout <seconds>] [--name <name>] [--label key=value]...\n Start one; prints its id, then its live view\n ls Your desktops\n get <desktop> One desktop, with a fresh live view (an id or its name)\n screenshot <desktop> [--out <file.png>] Save its screen\n open <desktop> <url> Open url in its Chromium\n rm <desktop> Delete it and its sandbox\n\nExamples\n runtime desktops create --name qa\n runtime desktops open qa https://withruntime.com\n runtime desktops screenshot qa --out screen.png\n";
10
+ /** `runtime browsers` and `runtime desktops`: the five verbs, a screenshot
11
+ * saved to a file, and a desktop's open. */
12
+ export declare function machineProductCommand(which: "browsers" | "desktops", argv: string[], out: Out, client: () => Promise<Runtime>, kit: CliKit): Promise<number>;
13
+ export {};
@@ -0,0 +1,160 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { me, named } from "./cli-name.js";
3
+ import { browsers } from "./products/browsers.js";
4
+ import { desktops } from "./products/desktops.js";
5
+ export const BROWSER_HELP = `runtime browsers <command>
6
+
7
+ A browser is Chromium on its own sandbox, billed at the sandbox rates, with a
8
+ private CDP address for Playwright, Puppeteer, browser-use or Stagehand.
9
+ In a private beta: outside it, each command says how to ask to join.
10
+
11
+ create [--memory <1024-16384>] [--vcpu <1-16>] [--visible] [--width <px>] [--height <px>]
12
+ [--timeout <seconds>] [--name <name>] [--label key=value]...
13
+ Start one; prints its id, then its cdpUrl
14
+ ls Your browsers
15
+ get <browser> One browser, with a fresh cdpUrl (an id or its name)
16
+ screenshot <browser> [<url>] [--out <file.png>] [--full] [--jpeg] [--wait <ms>]
17
+ Open url in a new tab and save its screenshot
18
+ (without url: what it shows now)
19
+ rm <browser> Delete it and its sandbox
20
+
21
+ Examples
22
+ runtime browsers create --name scraper
23
+ runtime browsers screenshot scraper https://withruntime.com --out home.png
24
+ runtime browsers rm scraper
25
+ `;
26
+ export const DESKTOP_HELP = `runtime desktops <command>
27
+
28
+ A desktop is a Linux desktop on its own sandbox, billed at the sandbox rates,
29
+ with a private live view and computer use: screenshot, click, type, open.
30
+ In a private beta: outside it, each command says how to ask to join.
31
+
32
+ create [--memory <1024-16384>] [--vcpu <1-16>] [--width <px>] [--height <px>]
33
+ [--timeout <seconds>] [--name <name>] [--label key=value]...
34
+ Start one; prints its id, then its live view
35
+ ls Your desktops
36
+ get <desktop> One desktop, with a fresh live view (an id or its name)
37
+ screenshot <desktop> [--out <file.png>] Save its screen
38
+ open <desktop> <url> Open url in its Chromium
39
+ rm <desktop> Delete it and its sandbox
40
+
41
+ Examples
42
+ runtime desktops create --name qa
43
+ runtime desktops open qa https://withruntime.com
44
+ runtime desktops screenshot qa --out screen.png
45
+ `;
46
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
47
+ /** `runtime browsers` and `runtime desktops`: the five verbs, a screenshot
48
+ * saved to a file, and a desktop's open. */
49
+ export async function machineProductCommand(which, argv, out, client, kit) {
50
+ const { parse, flag, has, integer, need, pairs, table, usage } = kit;
51
+ const noun = which === "browsers" ? "browser" : "desktop";
52
+ const help = which === "browsers" ? BROWSER_HELP : DESKTOP_HELP;
53
+ const [verb, ...rest] = argv;
54
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
55
+ if (!verb || verb === "help" || verb === "--help") {
56
+ print(named(help), { usage: named(help) });
57
+ return 0;
58
+ }
59
+ const transport = (await client()).transport;
60
+ const api = which === "browsers" ? browsers(transport) : desktops(transport);
61
+ const idOf = async (target) => {
62
+ const given = need(target, `a ${noun}'s id or name`);
63
+ if (UUID.test(given))
64
+ return given;
65
+ const found = (await api.list({ name: given })).data;
66
+ if (found.length === 1)
67
+ return found[0].id;
68
+ throw usage(found.length
69
+ ? `${found.length} ${which} are named ${given}; give one's id (${me} ${which} ls).`
70
+ : `No ${noun} is named ${given}. Your ${which}: ${me} ${which} ls`);
71
+ };
72
+ const save = (file, bytes, value) => {
73
+ writeFileSync(file, bytes);
74
+ print(`Saved ${file} (${bytes.length} bytes).`, { ...value, file, base64: undefined });
75
+ };
76
+ switch (verb) {
77
+ case "create": {
78
+ const args = parse(rest, ["visible"], ["memory", "vcpu", "width", "height", "timeout", "name", "label"]);
79
+ const input = {
80
+ ...(integer(args, "memory") === undefined ? {} : { memoryMiB: integer(args, "memory") }),
81
+ ...(integer(args, "vcpu") === undefined ? {} : { vcpu: integer(args, "vcpu") }),
82
+ ...(integer(args, "width") === undefined ? {} : { width: integer(args, "width") }),
83
+ ...(integer(args, "height") === undefined ? {} : { height: integer(args, "height") }),
84
+ ...(integer(args, "timeout") === undefined
85
+ ? {}
86
+ : { timeoutSeconds: integer(args, "timeout") }),
87
+ ...(flag(args, "name") === undefined ? {} : { name: flag(args, "name") }),
88
+ ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
89
+ ...(which === "browsers" && has(args, "visible") ? { headless: false } : {}),
90
+ };
91
+ const made = (await api.create(input));
92
+ const reach = which === "browsers" ? made.cdpUrl : made.liveViewUrl;
93
+ print(`${String(made.id)}\n${typeof reach === "string" ? reach : ""}`, made);
94
+ return 0;
95
+ }
96
+ case "ls":
97
+ case "list": {
98
+ const all = (await (await api.list({ limit: 100 })).toArray(Infinity));
99
+ print(all.length
100
+ ? table([
101
+ ["ID", "NAME", "STATE", "SIZE", "SANDBOX"],
102
+ ...all.map((item) => [
103
+ String(item.id),
104
+ typeof item.name === "string" ? item.name : "-",
105
+ String(item.state),
106
+ `${String(item.vcpu)} vCPU, ${String(item.memoryMiB)} MiB`,
107
+ String(item.sandboxId),
108
+ ]),
109
+ ])
110
+ : `No ${which}. Start one: ${me} ${which} create`, all);
111
+ return 0;
112
+ }
113
+ case "get": {
114
+ const found = await api.get(await idOf(rest[0]));
115
+ print(JSON.stringify(found, null, 2), found);
116
+ return 0;
117
+ }
118
+ case "screenshot":
119
+ case "shot": {
120
+ const args = parse(rest, ["full", "jpeg"], ["out", "wait"]);
121
+ const id = await idOf(args.positional[0]);
122
+ const jpeg = has(args, "jpeg");
123
+ const file = flag(args, "out") ?? `${noun}-${id.slice(0, 8)}.${jpeg ? "jpg" : "png"}`;
124
+ if (which === "desktops") {
125
+ const bytes = await desktops(transport).screenshot(id, jpeg ? { format: "jpeg" } : {});
126
+ save(file, bytes, { id });
127
+ return 0;
128
+ }
129
+ const url = args.positional[1];
130
+ const shot = await browsers(transport).screenshot(id, {
131
+ ...(url ? { url } : {}),
132
+ ...(has(args, "full") ? { fullPage: true } : {}),
133
+ ...(jpeg ? { format: "jpeg" } : {}),
134
+ ...(integer(args, "wait") === undefined ? {} : { waitMs: integer(args, "wait") }),
135
+ });
136
+ if (!out.json)
137
+ out.error(`${shot.url} (${shot.status ?? "-"}): ${shot.title}`);
138
+ save(file, Buffer.from(shot.base64, "base64"), shot);
139
+ return 0;
140
+ }
141
+ case "open": {
142
+ if (which !== "desktops")
143
+ break;
144
+ const id = await idOf(rest[0]);
145
+ const url = need(rest[1], "a URL to open");
146
+ const done = await desktops(transport).act(id, { action: "open", url });
147
+ print(`Opened ${url}.`, done);
148
+ return 0;
149
+ }
150
+ case "rm":
151
+ case "delete": {
152
+ const gone = await api.delete(await idOf(rest[0]));
153
+ print(`${String(rest[0])} deleted.`, gone ?? { deleted: true });
154
+ return 0;
155
+ }
156
+ }
157
+ throw kit.unknown(`${which} command`, verb, which === "browsers"
158
+ ? ["create", "ls", "get", "screenshot", "rm"]
159
+ : ["create", "ls", "get", "screenshot", "open", "rm"]);
160
+ }
package/dist/ci-cli.d.ts CHANGED
@@ -5,6 +5,6 @@ type Out = {
5
5
  write: (text: string) => void;
6
6
  error: (text: string) => void;
7
7
  };
8
- export declare const CI_HELP = "runtime ci <command>\n\n CI/CD checks each push and pull request of a connected GitHub repository in\n sandboxes, as the steps its config file names. A fork's pull request runs\n nothing; a writer can open it again from a branch of the repository. In a\n private beta: outside it, each command says how to ask to join.\n\n import [dir] [--write] [--force] This folder's GitHub Actions workflows and CircleCI\n config as a runtime.yml: printed, or written with\n --write; what isn't carried over is noted at its top.\n Needs no key and sends nothing\n connect <owner/name> [--branch main] [--config runtime.yml] [--installation <id>]\n [--trust <app-slug>[bot]]... [--name <name>]\n Connect a repository; prints its id. Takes a full key.\n --trust runs that App's pushes as its own from the start\n repos [ls] [--all] Connected repositories, with the Apps each trusts;\n --all: disconnected ones too\n repos trust <repoId | owner/name> <app-slug>[bot]...\n Run that App's pushes as the repository's own code,\n keeping the Apps it trusts already\n repos untrust <repoId | owner/name> <app-slug>[bot]...\n Stop trusting those Apps, keeping the rest\n repos undo <repoId | owner/name> on|off When main's check fails, find the first commit that\n fails it and open a pull request reverting it, which\n lands once its check passes. Off until turned on;\n takes an owner's or admin's full key\n disconnect <repoId | owner/name> Stop checking it; its checks still going are cancelled\n checks [--repository <repoId>] [--branch <name>] [--commit <sha>] [--pr <number>]\n [--state <state>]... [--limit 20] Checks, newest first, with their jobs by state (or ls)\n check <checkId> One check and its jobs in plan order (or get)\n cancel <checkId> Cancel a queued or running check: its jobs end,\n its machines are let go, GitHub is told\n logs <jobId> A job's output, its last 256 KiB, and its sandboxes\n tests <repoId> [--quarantined] Its test files: time, recent outcomes, quarantine\n flakes [--repository <repoId>] [--all] Flaky tests: each quarantined (or still gating), its\n owner and 48-hour deadline; --all: ended ones too\n flake <flakeId> One flaky test: its proof, owner, and recent runs\n flake prove <flakeId> Its 100 proof runs again at main's next commit\n flake hand <flakeId> <login | org/team> Hand it to someone else\n flake delete <flakeId> --reason <why> Say why its test is deleted; it ends once main\n no longer holds the file\n releases [--repository <repoId>] [--commit <sha>] [--limit 50]\n A release's steps as they were taken, oldest\n first: migrations, deploys, servers, waits\n\n A failed job's sandbox may be kept paused: `runtime sandboxes wake <id>`, then\n `runtime sandboxes shell <id>` to look around where it failed. A key reads the\n checks of repositories its own agent connected; an account-wide key reads and\n cancels every check of the account (switch one at withruntime.com/account/keys).\n\nExamples\n runtime ci import --write\n runtime ci connect acme/web --config .runtime/ci.yml\n runtime ci repos trust acme/web acme-bot[bot]\n runtime ci repos undo acme/web on\n runtime ci checks --commit $(git rev-parse HEAD)\n runtime ci cancel <checkId>\n runtime ci logs <jobId>\n";
8
+ export declare const CI_HELP = "runtime ci <command>\n\n CI/CD checks each push and pull request of a connected GitHub repository in\n sandboxes, as the steps its config file names. A fork's pull request runs\n nothing; a writer can open it again from a branch of the repository. In a\n private beta: outside it, each command says how to ask to join.\n\n import [dir] [--write] [--force] This folder's GitHub Actions workflows and CircleCI\n config as a runtime.yml: printed, or written with\n --write; what isn't carried over is noted at its top.\n Needs no key and sends nothing\n connect <owner/name> [--branch main] [--config runtime.yml] [--installation <id>]\n [--trust <app-slug>[bot]]... [--name <name>]\n Connect a repository; prints its id. Takes a full key.\n --trust runs that App's pushes as its own from the start\n repos [ls] [--all] Connected repositories, with the Apps each trusts;\n --all: disconnected ones too\n repos trust <repoId | owner/name> <app-slug>[bot]...\n Run that App's pushes as the repository's own code,\n keeping the Apps it trusts already\n repos untrust <repoId | owner/name> <app-slug>[bot]...\n Stop trusting those Apps, keeping the rest\n repos undo <repoId | owner/name> on|off When main's check fails, find the first commit that\n fails it and open a pull request reverting it, which\n lands once its check passes. Off until turned on;\n takes an owner's or admin's full key\n disconnect <repoId | owner/name> Stop checking it; its checks still going are cancelled\n checks [--repository <repoId>] [--branch <name>] [--commit <sha>] [--pr <number>]\n [--state <state>]... [--limit 20] Checks, newest first, with their jobs by state (or ls)\n check <checkId> One check and its jobs in plan order (or get)\n cancel <checkId> Cancel a queued or running check: its jobs end,\n its machines are let go, GitHub is told\n rerun <checkId> Run a check again: a new check of its commit, a\n pull request's as it would land now\n logs <jobId> A job's output, its last 256 KiB, and its sandboxes\n tests <repoId> [--quarantined] Its test files: time, recent outcomes, quarantine\n flakes [--repository <repoId>] [--all] Flaky tests: each quarantined (or still gating), its\n owner and 48-hour deadline; --all: ended ones too\n flake <flakeId> One flaky test: its proof, owner, and recent runs\n flake prove <flakeId> Its 100 proof runs again at main's next commit\n flake hand <flakeId> <login | org/team> Hand it to someone else\n flake delete <flakeId> --reason <why> Say why its test is deleted; it ends once main\n no longer holds the file\n releases [--repository <repoId>] [--commit <sha>] [--limit 50]\n A release's steps as they were taken, oldest\n first: migrations, deploys, servers, waits\n\n A failed job's sandbox may be kept paused: `runtime sandboxes wake <id>`, then\n `runtime sandboxes shell <id>` to look around where it failed. A key reads the\n checks of repositories its own agent connected; an account-wide key reads and\n cancels every check of the account (switch one at withruntime.com/account/keys).\n\nExamples\n runtime ci import --write\n runtime ci connect acme/web --config .runtime/ci.yml\n runtime ci repos trust acme/web acme-bot[bot]\n runtime ci repos undo acme/web on\n runtime ci checks --commit $(git rev-parse HEAD)\n runtime ci cancel <checkId>\n runtime ci logs <jobId>\n";
9
9
  export declare function ciCommand(argv: string[], out: Out, client: () => Promise<Runtime>, kit: CliKit): Promise<number>;
10
10
  export {};
package/dist/ci-cli.js CHANGED
@@ -35,6 +35,8 @@ export const CI_HELP = `runtime ci <command>
35
35
  check <checkId> One check and its jobs in plan order (or get)
36
36
  cancel <checkId> Cancel a queued or running check: its jobs end,
37
37
  its machines are let go, GitHub is told
38
+ rerun <checkId> Run a check again: a new check of its commit, a
39
+ pull request's as it would land now
38
40
  logs <jobId> A job's output, its last 256 KiB, and its sandboxes
39
41
  tests <repoId> [--quarantined] Its test files: time, recent outcomes, quarantine
40
42
  flakes [--repository <repoId>] [--all] Flaky tests: each quarantined (or still gating), its
@@ -310,6 +312,16 @@ export async function ciCommand(argv, out, client, kit) {
310
312
  print(`${check.repository} ${check.commit.slice(0, 12)} ${where(check)}: ${check.state}${check.reason ? ` (${check.reason})` : ""}`, check);
311
313
  return 0;
312
314
  }
315
+ case "rerun": {
316
+ const check = await api.checks.rerun(need(rest[0], "a check's id (runtime ci ls)"));
317
+ // A key that may run the check again but not read it is told the new one's id alone.
318
+ if (!("commit" in check)) {
319
+ print(`${check.id}: ${check.state}`, check);
320
+ return 0;
321
+ }
322
+ print(`${check.id} ${check.repository} ${check.commit.slice(0, 12)} ${where(check)}: ${check.state}${check.reason ? ` (${check.reason})` : ""}`, check);
323
+ return 0;
324
+ }
313
325
  case "logs":
314
326
  case "job": {
315
327
  const job = await api.jobs.get(need(rest[0], "a job's id (runtime ci get <checkId>)"));
@@ -440,6 +452,7 @@ export async function ciCommand(argv, out, client, kit) {
440
452
  "ls",
441
453
  "get",
442
454
  "cancel",
455
+ "rerun",
443
456
  "logs",
444
457
  "tests",
445
458
  "flakes",
@@ -447,15 +460,15 @@ export async function ciCommand(argv, out, client, kit) {
447
460
  "releases",
448
461
  ]);
449
462
  }
450
- /** A review's verdict in words: each finding with its place and fix, then
451
- * the notes, then who reviewed. */
463
+ /** An agent step's verdict in words: each finding with its place and fix,
464
+ * then the notes, then who judged. */
452
465
  function reviewed(r) {
453
466
  const at = (f) => (f.line > 0 ? `${f.file}:${f.line}` : f.file);
454
467
  return [
455
468
  `\n${r.verdict === "pass" ? "Passed review" : `Blocked: ${r.findings.length} finding(s)`}. ${r.summary}`,
456
469
  ...r.findings.map((f) => ` ${f.kind} at ${at(f)}: ${f.what}\n Fix: ${f.fix}`),
457
470
  ...r.notes.map((n) => ` note at ${at(n)}: ${n.what}`),
458
- `Reviewed by agent ${r.agent} (${r.model}) on ${r.account}${r.cut ? ", diff cut short" : ""}: session ${r.session}`,
471
+ `Judged by agent ${r.agent}${r.model ? ` (${r.model})` : ""}${r.account ? ` on ${r.account}` : ""}${r.cut ? ", diff cut short" : ""}: session ${r.session}`,
459
472
  "",
460
473
  ].join("\n");
461
474
  }
package/dist/cli.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { type Sandbox } from "./sandbox.js";
3
3
  import { RuntimeError } from "./errors.js";
4
4
  import type { OutOfMemory, Usage } from "./types.js";
5
+ import type { UsageExportPage } from "./products/observability.js";
5
6
  import type { SwitchProvider } from "./products/switching.js";
6
7
  import type { SecretRule, SecretScope } from "./products/secrets.js";
7
8
  type Out = {
@@ -38,6 +39,9 @@ declare const kit: {
38
39
  };
39
40
  export type CliKit = typeof kit;
40
41
  export declare function run(argv: string[], env?: NodeJS.ProcessEnv, out?: Out): Promise<number>;
42
+ /** `usage export`'s last word, on stderr beside the CSV: how much of the
43
+ * range can still change. */
44
+ export declare function finalLine(page: Pick<UsageExportPage, "until" | "finalThrough">): string;
41
45
  /** `runtime usage` for a person: the balance, the free usage and what each kind
42
46
  * of resource was charged, in dollars. --json keeps every figure, and each
43
47
  * resource with its rates and CPU time. */
package/dist/cli.js CHANGED
@@ -69,7 +69,9 @@ Account
69
69
  (job create --secret NAME); with --host, both
70
70
  secrets scope <NAME> --only-label k=v... | --only-sandbox <id>... | --only-app <id>... | --all-sandboxes
71
71
  Change which sandboxes get it; no value needed
72
- secrets ls | get <NAME> Names, hosts, placeholders and jobs versions, never values
72
+ secrets ls [--limit N] [--after NAME] | get <NAME>
73
+ Names, hosts, placeholders and jobs versions, never values;
74
+ by name, a page of N after NAME
73
75
  secrets rotate <NAME> A new version of the jobs copy, from standard input
74
76
  secrets reveal <NAME> [--version N] Print the jobs copy's value. The sandboxes' copy is
75
77
  never readable, by anyone
@@ -136,6 +138,8 @@ Products
136
138
  builds A connected repository's commits built into what an App deploys (beta)
137
139
  ci CI/CD: each push and pull request of a repository checked in sandboxes (beta)
138
140
  containers One OCI image's process on its own microVM, at the sandbox rates (beta)
141
+ browsers Chromium on its own sandbox: a CDP address and screenshots (beta)
142
+ desktops A Linux desktop on its own sandbox: live view, computer use (beta)
139
143
  agents Claude Code, Codex, Gemini CLI or your own agent in a sandbox per session (beta)
140
144
  domains Your own hostname for a sandbox's port, with HTTPS
141
145
  ports A public TCP port to a sandbox: databases, game servers
@@ -476,6 +480,8 @@ const COMMANDS = [
476
480
  "builds",
477
481
  "ci",
478
482
  "containers",
483
+ "browsers",
484
+ "desktops",
479
485
  "agents",
480
486
  "domains",
481
487
  "ports",
@@ -500,6 +506,8 @@ const OWN_HELP = [
500
506
  "builds",
501
507
  "ci",
502
508
  "containers",
509
+ "browsers",
510
+ "desktops",
503
511
  "agents",
504
512
  "keys",
505
513
  "reservations",
@@ -872,6 +880,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
872
880
  const rt = await client(env);
873
881
  const cursors = new Set();
874
882
  let cursor;
883
+ // The export is final only as far as its earliest page's finalThrough.
884
+ let finalThrough;
875
885
  for (;;) {
876
886
  const page = await rt.usageExport({
877
887
  since,
@@ -891,8 +901,19 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
891
901
  else if (page.csv)
892
902
  out.write(page.csv);
893
903
  await out.drain?.();
894
- if (page.nextCursor === null)
904
+ const at = page.finalThrough;
905
+ finalThrough =
906
+ finalThrough === undefined
907
+ ? at
908
+ : finalThrough === null || at === null
909
+ ? null
910
+ : at < finalThrough
911
+ ? at
912
+ : finalThrough;
913
+ if (page.nextCursor === null) {
914
+ out.error(finalLine({ until: page.until, finalThrough: finalThrough ?? null }));
895
915
  return 0;
916
+ }
896
917
  cursors.add(page.nextCursor);
897
918
  cursor = page.nextCursor;
898
919
  }
@@ -1351,6 +1372,10 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
1351
1372
  product = "containers";
1352
1373
  return (await import("./apps-cli.js")).containersCommand(rest, env, out, () => client(env), kit);
1353
1374
  }
1375
+ if (["browser", "browsers", "desktop", "desktops"].includes(command)) {
1376
+ product = command.endsWith("s") ? command : `${command}s`;
1377
+ return (await import("./browsers-cli.js")).machineProductCommand(product, rest, out, () => client(env), kit);
1378
+ }
1354
1379
  if (command === "agent" || command === "agents") {
1355
1380
  product = "agents";
1356
1381
  return (await import("./agents-cli.js")).agentsCommand(rest, env, out, () => client(env), kit);
@@ -1365,6 +1390,15 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
1365
1390
  }
1366
1391
  throw unknown("command", command, COMMANDS);
1367
1392
  }
1393
+ /** `usage export`'s last word, on stderr beside the CSV: how much of the
1394
+ * range can still change. */
1395
+ export function finalLine(page) {
1396
+ if (page.finalThrough === null)
1397
+ return "Charges in this range may still settle; export it again later for a final copy.";
1398
+ if (Date.parse(page.finalThrough) >= Date.parse(page.until))
1399
+ return "Final: no more charges will settle in this range.";
1400
+ return `Final up to ${page.finalThrough}; charges after that may still settle. Export again later for the rest.`;
1401
+ }
1368
1402
  /** `runtime usage` for a person: the balance, the free usage and what each kind
1369
1403
  * of resource was charged, in dollars. --json keeps every figure, and each
1370
1404
  * resource with its rates and CPU time. */
@@ -2689,6 +2723,8 @@ read = readSecret) {
2689
2723
  "only-app",
2690
2724
  "agent",
2691
2725
  "env",
2726
+ "limit",
2727
+ "after",
2692
2728
  ]);
2693
2729
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
2694
2730
  const rows = (list) => list.length
@@ -2706,11 +2742,19 @@ read = readSecret) {
2706
2742
  ])
2707
2743
  : "No secrets.";
2708
2744
  if (verb === "ls" || verb === "list" || verb === undefined) {
2745
+ const limit = integer(args, "limit");
2746
+ if (limit !== undefined && (limit < 1 || limit > 100))
2747
+ throw usage("--limit takes 1 to 100 secrets a page.");
2748
+ const after = flag(args, "after");
2709
2749
  const runtime = await client(env);
2710
- const all = await runtime.secrets.all();
2750
+ // A page of both copies by name, as the API pages each: an organization
2751
+ // keeps at most 50 secrets for sandboxes, so both are read whole.
2752
+ const every = (await runtime.secrets.all()).filter((s) => after === undefined || s.name > after);
2753
+ const all = limit === undefined ? every : every.slice(0, limit);
2754
+ const more = all.length < every.length ? all.at(-1).name : undefined;
2711
2755
  // --json keeps the sandboxes' list it always printed; the jobs copies
2712
2756
  // are in `runtime secrets get <NAME> --json`.
2713
- print(rows(all), all.flatMap((s) => (s.sandboxes ? [s.sandboxes] : [])));
2757
+ print(more ? `${rows(all)}\nMore: ${me} secrets ls --limit ${limit} --after ${more}` : rows(all), all.flatMap((s) => (s.sandboxes ? [s.sandboxes] : [])));
2714
2758
  return 0;
2715
2759
  }
2716
2760
  const jobsOnly = "Only a secret's copy for jobs has versions and can be read back";
@@ -4377,10 +4421,39 @@ const VOLUME_HELP = `runtime volumes <command>
4377
4421
  backup-rm <backupId> Delete a backup
4378
4422
  backup-policy <id> [--daily on|off] [--retention-days N]
4379
4423
  Daily backups on or off, and how long each is kept
4424
+ backup-policy <id> --every <minutes> | --cron "<m h dom mon dow>" [--keep hourly=24,daily=7,...]
4425
+ Back up on a schedule of its own (UTC), keeping
4426
+ last, hourly, daily, weekly, monthly, yearly counts
4427
+ backup-policy <id> --no-schedule Back to daily backups
4380
4428
  Attach one: runtime sandboxes create --volume <id>:/data
4381
- Volumes are backed up off their host daily unless you turn it off.
4429
+ Volumes are backed up off their host daily, or on their own schedule, unless you turn it off.
4382
4430
  Shared commands work where shared disks are enabled.
4383
4431
  `;
4432
+ const KEEP_TIERS = ["last", "hourly", "daily", "weekly", "monthly", "yearly"];
4433
+ /** `hourly=24,daily=7` as keep counts; the server checks the numbers. */
4434
+ function keepCounts(text) {
4435
+ const counts = {};
4436
+ for (const part of text.split(",")) {
4437
+ const [name, value, extra] = part.trim().split("=");
4438
+ const tier = KEEP_TIERS.find((t) => t === name);
4439
+ if (!tier || value === undefined || extra !== undefined || !/^\d+$/.test(value))
4440
+ throw usage(`--keep takes name=count pairs such as hourly=24,daily=7; names: ${KEEP_TIERS.join(", ")}.`);
4441
+ counts[tier] = Number(value);
4442
+ }
4443
+ return counts;
4444
+ }
4445
+ /** "daily backups on, kept 7 days", or the schedule, its keep counts and next run. */
4446
+ function backupPolicyWords(b) {
4447
+ if (!b.schedule)
4448
+ return `daily backups ${b.daily ? "on" : "off"}, kept ${b.retentionDays} days`;
4449
+ const when = "everyMinutes" in b.schedule
4450
+ ? `every ${b.schedule.everyMinutes} minutes`
4451
+ : `on cron "${b.schedule.cron.trim()}" (UTC)`;
4452
+ const kept = KEEP_TIERS.filter((t) => (b.keep?.[t] ?? 0) > 0)
4453
+ .map((t) => `${t} ${b.keep[t]}`)
4454
+ .join(", ");
4455
+ return `backups ${when}${b.daily ? "" : " (off)"}, keeping ${kept}${b.daily && b.nextAt ? `; next ${b.nextAt}` : ""}`;
4456
+ }
4384
4457
  async function volume(argv, env, out) {
4385
4458
  const [verb, ...rest] = argv;
4386
4459
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
@@ -4489,7 +4562,7 @@ async function volume(argv, env, out) {
4489
4562
  String(b.sizeMiB),
4490
4563
  b.storedBytes === null ? "-" : `${Math.ceil(b.storedBytes / 1048576)} MiB`,
4491
4564
  b.createdAt,
4492
- b.expiresAt,
4565
+ b.expiresAt ?? "by keep counts",
4493
4566
  ]),
4494
4567
  ])
4495
4568
  : "No backups yet.", all);
@@ -4510,19 +4583,30 @@ async function volume(argv, env, out) {
4510
4583
  return 0;
4511
4584
  }
4512
4585
  case "backup-policy": {
4513
- const args = parse(rest, [], ["daily", "retention-days"]);
4586
+ const args = parse(rest, ["no-schedule"], ["daily", "retention-days", "every", "cron", "keep"]);
4514
4587
  const id = need(args.positional[0], "a volume id");
4515
4588
  const daily = flag(args, "daily");
4516
4589
  if (daily !== undefined && !["on", "off", "true", "false"].includes(daily))
4517
4590
  throw usage("--daily takes on or off.");
4518
4591
  const days = integer(args, "retention-days");
4519
- if (daily === undefined && days === undefined)
4520
- throw usage("Give --daily on|off, --retention-days N or both.");
4592
+ const every = integer(args, "every");
4593
+ const cron = flag(args, "cron");
4594
+ const off = has(args, "no-schedule");
4595
+ if ([every !== undefined, cron !== undefined, off].filter(Boolean).length > 1)
4596
+ throw usage("Give one of --every, --cron and --no-schedule.");
4597
+ const keepText = flag(args, "keep");
4598
+ const keep = keepText === undefined ? undefined : keepCounts(keepText);
4599
+ if ([daily, days, every, cron, keep].every((x) => x === undefined) && !off)
4600
+ throw usage("Give --daily on|off, --retention-days N, --every, --cron, --keep or --no-schedule.");
4521
4601
  const updated = await api.setBackupPolicy(id, {
4522
4602
  ...(daily === undefined ? {} : { daily: daily === "on" || daily === "true" }),
4523
4603
  ...(days === undefined ? {} : { retentionDays: days }),
4604
+ ...(every === undefined ? {} : { schedule: { everyMinutes: every } }),
4605
+ ...(cron === undefined ? {} : { schedule: { cron } }),
4606
+ ...(off ? { schedule: null } : {}),
4607
+ ...(keep === undefined ? {} : { keep }),
4524
4608
  });
4525
- print(`${updated.id} daily backups ${updated.backups.daily ? "on" : "off"}, kept ${updated.backups.retentionDays} days`, updated);
4609
+ print(`${updated.id} ${backupPolicyWords(updated.backups)}`, updated);
4526
4610
  return 0;
4527
4611
  }
4528
4612
  }
@@ -0,0 +1,9 @@
1
+ /** `withruntime/desktops`: Desktops alone. A Linux desktop on its own sandbox, with a live view and computer use, at the sandbox rates. In a private beta.
2
+ *
3
+ * import { desktops } from "withruntime/desktops";
4
+ * const client = desktops(); // RUNTIME_API_KEY, or this machine's saved connection
5
+ */
6
+ import { Runtime, type RuntimeOptions } from "../client.js";
7
+ export type * from "../products/desktops.js";
8
+ /** `runtime.desktops` of a client made with `options`. */
9
+ export declare function desktops(options?: RuntimeOptions): Runtime["desktops"];
@@ -0,0 +1,10 @@
1
+ /** `withruntime/desktops`: Desktops alone. A Linux desktop on its own sandbox, with a live view and computer use, at the sandbox rates. In a private beta.
2
+ *
3
+ * import { desktops } from "withruntime/desktops";
4
+ * const client = desktops(); // RUNTIME_API_KEY, or this machine's saved connection
5
+ */
6
+ import { Runtime } from "../client.js";
7
+ /** `runtime.desktops` of a client made with `options`. */
8
+ export function desktops(options) {
9
+ return new Runtime(options).desktops;
10
+ }