withruntime 0.23.0 → 0.33.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 (78) hide show
  1. package/COMPATIBILITY.md +66 -5
  2. package/E2B.md +1 -1
  3. package/README.md +36 -2
  4. package/README.src.md +35 -1
  5. package/compatibility-lock.json +3 -3
  6. package/dist/agents/browser.d.ts +12 -0
  7. package/dist/agents/browser.js +12 -0
  8. package/dist/agents/index.d.ts +7 -7
  9. package/dist/agents/index.js +7 -8
  10. package/dist/agents-cli.d.ts +1 -1
  11. package/dist/agents-cli.js +250 -3
  12. package/dist/agents-deploy.d.ts +186 -0
  13. package/dist/agents-deploy.js +752 -0
  14. package/dist/agents-file.d.ts +19 -0
  15. package/dist/agents-file.js +135 -0
  16. package/dist/apps/index.d.ts +10 -0
  17. package/dist/apps/index.js +11 -0
  18. package/dist/apps-cli.d.ts +3 -1
  19. package/dist/apps-cli.js +285 -38
  20. package/dist/blaxel/errors.js +2 -1
  21. package/dist/ci/index.d.ts +13 -0
  22. package/dist/ci/index.js +14 -0
  23. package/dist/ci-cli.d.ts +10 -0
  24. package/dist/ci-cli.js +402 -0
  25. package/dist/ci-import.d.ts +23 -0
  26. package/dist/ci-import.js +565 -0
  27. package/dist/cli.d.ts +18 -7
  28. package/dist/cli.js +775 -85
  29. package/dist/daytona/errors.js +2 -1
  30. package/dist/e2b/errors.js +2 -1
  31. package/dist/e2b/filesystem.d.ts +1 -0
  32. package/dist/e2b/filesystem.js +1 -1
  33. package/dist/e2b/sandbox.d.ts +11 -2
  34. package/dist/e2b/sandbox.js +22 -6
  35. package/dist/errors.d.ts +11 -4
  36. package/dist/errors.js +27 -4
  37. package/dist/fly/index.d.ts +102 -0
  38. package/dist/fly/index.js +930 -0
  39. package/dist/index.d.ts +7 -3
  40. package/dist/machine.d.ts +10 -6
  41. package/dist/machine.js +14 -6
  42. package/dist/modal/index.d.ts +34 -0
  43. package/dist/modal/index.js +128 -22
  44. package/dist/products/agents.d.ts +101 -3
  45. package/dist/products/agents.js +49 -0
  46. package/dist/products/apps.d.ts +206 -10
  47. package/dist/products/apps.js +172 -1
  48. package/dist/products/ci.d.ts +335 -0
  49. package/dist/products/ci.js +108 -0
  50. package/dist/products/index.d.ts +8 -0
  51. package/dist/products/index.js +15 -0
  52. package/dist/products/jobs.d.ts +105 -5
  53. package/dist/products/jobs.js +47 -3
  54. package/dist/products/network.d.ts +13 -0
  55. package/dist/products/observability.d.ts +1 -1
  56. package/dist/products/plans.d.ts +131 -0
  57. package/dist/products/plans.js +24 -0
  58. package/dist/products/reservations.d.ts +108 -0
  59. package/dist/products/reservations.js +41 -0
  60. package/dist/products/resources.d.ts +2 -1
  61. package/dist/products/secrets.d.ts +90 -6
  62. package/dist/products/secrets.js +8 -2
  63. package/dist/proxy.d.ts +3 -3
  64. package/dist/proxy.js +20 -0
  65. package/dist/sandbox.d.ts +29 -1
  66. package/dist/sandbox.js +56 -3
  67. package/dist/snapshots.d.ts +1 -1
  68. package/dist/switched-off.d.ts +5 -0
  69. package/dist/switched-off.js +8 -0
  70. package/dist/tar.d.ts +1 -1
  71. package/dist/tar.js +19 -1
  72. package/dist/transport.d.ts +1 -1
  73. package/dist/transport.js +48 -9
  74. package/dist/types.d.ts +35 -4
  75. package/dist/vercel/errors.d.ts +1 -1
  76. package/dist/vercel/errors.js +2 -1
  77. package/dist/vercel/snapshot.js +4 -2
  78. package/package.json +23 -6
package/dist/ci-cli.js ADDED
@@ -0,0 +1,402 @@
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { importCi } from "./ci-import.js";
4
+ import { me, named } from "./cli-name.js";
5
+ import { ci, } from "./products/ci.js";
6
+ export const CI_HELP = `runtime ci <command>
7
+
8
+ CI/CD checks each push and pull request of a connected GitHub repository in
9
+ sandboxes, as the steps its config file names. A fork's pull request waits
10
+ until a writer re-runs it on GitHub. In a private beta: outside it, each
11
+ command says how to ask to join.
12
+
13
+ import [dir] [--write] [--force] This folder's GitHub Actions workflows and CircleCI
14
+ config as a runtime.yml: printed, or written with
15
+ --write; what isn't carried over is noted at its top.
16
+ Needs no key and sends nothing
17
+ connect <owner/name> [--branch main] [--config runtime.yml] [--installation <id>]
18
+ [--trust <app-slug>[bot]]... [--name <name>]
19
+ Connect a repository; prints its id. Takes a full key.
20
+ --trust runs that App's pushes as its own from the start
21
+ repos [ls] [--all] Connected repositories, with the Apps each trusts;
22
+ --all: disconnected ones too
23
+ repos trust <repoId | owner/name> <app-slug>[bot]...
24
+ Run that App's pushes as the repository's own code,
25
+ keeping the Apps it trusts already
26
+ repos untrust <repoId | owner/name> <app-slug>[bot]...
27
+ Stop trusting those Apps, keeping the rest
28
+ disconnect <repoId | owner/name> Stop checking it; its checks still going are cancelled
29
+ ls [--repository <repoId>] [--branch <name>] [--commit <sha>] [--pr <number>]
30
+ [--state <state>]... [--limit 20] Checks, newest first, with their jobs by state
31
+ get <checkId> One check and its jobs in plan order
32
+ cancel <checkId> Cancel a queued or running check: its jobs end,
33
+ its machines are let go, GitHub is told
34
+ logs <jobId> A job's output, its last 256 KiB, and its sandboxes
35
+ tests <repoId> [--quarantined] Its test files: time, recent outcomes, quarantine
36
+ flakes [--repository <repoId>] [--all] Flaky tests: each quarantined (or still gating), its
37
+ owner and 48-hour deadline; --all: ended ones too
38
+ flake <flakeId> One flaky test: its proof, owner, and recent runs
39
+ flake prove <flakeId> Its 100 proof runs again at main's next commit
40
+ flake hand <flakeId> <login | org/team> Hand it to someone else
41
+ flake delete <flakeId> --reason <why> Say why its test is deleted; it ends once main
42
+ no longer holds the file
43
+
44
+ A failed job's sandbox may be kept paused: \`runtime sandboxes wake <id>\`, then
45
+ \`runtime sandboxes shell <id>\` to look around where it failed.
46
+
47
+ Examples
48
+ runtime ci import --write
49
+ runtime ci connect acme/web --config .runtime/ci.yml
50
+ runtime ci repos trust acme/web acme-bot[bot]
51
+ runtime ci ls --commit $(git rev-parse HEAD)
52
+ runtime ci logs <jobId>
53
+ `;
54
+ const when = (ms) => (ms === null ? "-" : new Date(ms).toISOString());
55
+ const took = (from, to) => from === null || to === null ? "-" : `${((to - from) / 1000).toFixed(1)}s`;
56
+ const counted = (c) => Object.entries(c)
57
+ .map(([state, n]) => `${n} ${state}`)
58
+ .join(", ") || "-";
59
+ /** Where a check's machines came from and how soon, in a line. */
60
+ const machined = (m) => {
61
+ if (!m)
62
+ return null;
63
+ const from = [
64
+ m.reused && `${m.reused} its pull request's own`,
65
+ m.pooled && `${m.pooled} pooled`,
66
+ m.forked && `${m.forked} forked in ${m.forks}`,
67
+ m.created && `${m.created} created`,
68
+ ].filter(Boolean);
69
+ const s = (ms) => (ms === null ? "-" : `${(ms / 1000).toFixed(2)}s`);
70
+ return `machines: ${from.join(", ") || "none"}; first in ${s(m.firstMs)}, first test at ${s(m.firstTestMs)}${m.lineMs ? `, ${s(m.lineMs)} in line` : ""}${m.kept ? `; ${m.kept} kept for the next push` : ""}`;
71
+ };
72
+ const where = (c) => c.pullRequest !== null ? `#${c.pullRequest}` : (c.branch ?? c.event);
73
+ const STATES = ["queued", "running", "passed", "failed", "cancelled"];
74
+ const ownerOf = (f) => f.owner === null
75
+ ? "nobody found"
76
+ : `@${f.owner.login}${f.owner.pullRequest === null ? "" : ` (#${f.owner.pullRequest})`}`;
77
+ /** `runtime ci import [dir] [--write] [--force]`: the folder's GitHub Actions
78
+ * workflows and CircleCI config as a `runtime.yml`, printed or written. */
79
+ async function importCommand(argv, out, kit) {
80
+ const args = kit.parse(argv, ["write", "force"], []);
81
+ const root = args.positional[0] ?? ".";
82
+ const files = [];
83
+ const workflows = join(root, ".github", "workflows");
84
+ if (existsSync(workflows))
85
+ for (const name of readdirSync(workflows).sort())
86
+ if (/\.ya?ml$/.test(name))
87
+ files.push({
88
+ path: `.github/workflows/${name}`,
89
+ text: readFileSync(join(workflows, name), "utf8"),
90
+ });
91
+ const circle = join(root, ".circleci", "config.yml");
92
+ if (existsSync(circle))
93
+ files.push({ path: ".circleci/config.yml", text: readFileSync(circle, "utf8") });
94
+ if (!files.length)
95
+ throw kit.usage(`${root} has neither .github/workflows/*.yml nor .circleci/config.yml to import.`);
96
+ const made = importCi(files);
97
+ if (!made.yaml) {
98
+ out.error(made.notes.join("\n"));
99
+ throw kit.usage("No job of these runs on a push or a pull request: nothing to write.");
100
+ }
101
+ if (!kit.has(args, "write")) {
102
+ out.write(out.json ? JSON.stringify({ ...made, read: files.map((f) => f.path) }) : made.yaml.trimEnd());
103
+ return 0;
104
+ }
105
+ const target = join(root, "runtime.yml");
106
+ if (existsSync(target) && !kit.has(args, "force"))
107
+ throw kit.usage(`${target} is there already: --force writes over it.`);
108
+ writeFileSync(target, made.yaml);
109
+ const notes = made.notes.length
110
+ ? `, ${made.notes.length} notes on what was not carried over at its top`
111
+ : "";
112
+ out.write(out.json
113
+ ? JSON.stringify({ ...made, read: files.map((f) => f.path), written: target })
114
+ : `${target}: ${made.steps.length} steps from ${files.length === 1 ? "1 file" : `${files.length} files`}${notes}. Push it, then \`${me} ci connect <owner/name>\`.`);
115
+ return 0;
116
+ }
117
+ export async function ciCommand(argv, out, client, kit) {
118
+ const { parse, flag, has, need, table, usage, unknown } = kit;
119
+ const [verb, ...rest] = argv;
120
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
121
+ if (!verb || verb === "help" || verb === "--help") {
122
+ print(named(CI_HELP), { usage: named(CI_HELP) });
123
+ return 0;
124
+ }
125
+ // Reads and writes this folder's files only: no key, nothing sent.
126
+ if (verb === "import")
127
+ return importCommand(rest, out, kit);
128
+ const api = ci((await client()).transport);
129
+ const whole = (value, what) => {
130
+ if (value === undefined)
131
+ return undefined;
132
+ if (!/^[1-9][0-9]{0,8}$/.test(value))
133
+ throw usage(`${what} takes a whole number: ${value}`);
134
+ return Number(value);
135
+ };
136
+ const apps = (words) => {
137
+ if (!words.length)
138
+ throw usage(`Name an App by its login, such as acme-bot[bot].`);
139
+ for (const word of words)
140
+ if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\[bot\]$/.test(word))
141
+ throw usage(`${word} is not an App's login: name it <app-slug>[bot], as GitHub shows its pushes (quote it in your shell: '${word.replace(/\[bot\]$/, "")}[bot]').`);
142
+ return words;
143
+ };
144
+ const trusted = (r) => r.trustedApps?.join(", ") || "none";
145
+ const disconnect = async (idOrName) => {
146
+ const found = await api.repositories.find(idOrName);
147
+ const repo = await api.repositories.disconnect(found?.id ?? idOrName);
148
+ print(`${repo.repository} disconnected; its checks that ended stay.`, repo);
149
+ return 0;
150
+ };
151
+ switch (verb) {
152
+ case "connect": {
153
+ const args = parse(rest, [], ["branch", "config", "installation", "trust", "name"]);
154
+ const repository = need(args.positional[0], "the repository, such as acme/web");
155
+ const installation = whole(flag(args, "installation"), "--installation");
156
+ const trust = args.flags.get("trust") ?? [];
157
+ const input = {
158
+ repository,
159
+ ...(flag(args, "branch") ? { productionBranch: flag(args, "branch") } : {}),
160
+ ...(flag(args, "config") ? { configPath: flag(args, "config") } : {}),
161
+ ...(installation === undefined ? {} : { installation }),
162
+ ...(trust.length ? { trustedApps: apps(trust) } : {}),
163
+ ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
164
+ };
165
+ const repo = await api.repositories.connect(input);
166
+ if (!out.json)
167
+ out.error(`Connected ${repo.repository}: each push and pull request is checked as ${repo.configPath} says; ${repo.productionBranch} is its production branch; it trusts ${trusted(repo)}. Its checks: ${me} ci ls --repository ${repo.id}`);
168
+ print(repo.id, repo);
169
+ return 0;
170
+ }
171
+ case "repos":
172
+ case "repositories": {
173
+ const [action, ...more] = rest;
174
+ if (action === "trust" || action === "untrust") {
175
+ const [which, ...logins] = more;
176
+ const id = need(which, "the repository's id or owner/name");
177
+ const listed = apps(logins);
178
+ const repo = action === "trust"
179
+ ? await api.repositories.trust(id, listed)
180
+ : await api.repositories.untrust(id, listed);
181
+ print(`${repo.repository} trusts ${trusted(repo)}.`, repo);
182
+ return 0;
183
+ }
184
+ if (action === "connect")
185
+ return ciCommand(["connect", ...more], out, client, kit);
186
+ if (action === "disconnect" || action === "rm")
187
+ return disconnect(need(more[0], "the repository's id or owner/name"));
188
+ const listing = action === "ls" || action === "list" ? more : rest;
189
+ if (listing.length && !listing[0].startsWith("-"))
190
+ throw unknown("ci repos command", listing[0], [
191
+ "ls",
192
+ "connect",
193
+ "trust",
194
+ "untrust",
195
+ "disconnect",
196
+ ]);
197
+ const args = parse(listing, ["all"], []);
198
+ const all = await (await api.repositories.list({
199
+ limit: 100,
200
+ ...(has(args, "all") ? {} : { state: ["connected"] }),
201
+ })).toArray(Infinity);
202
+ print(all.length
203
+ ? table([
204
+ ["ID", "REPOSITORY", "BRANCH", "CONFIG", "TRUSTED APPS", "STATE"],
205
+ ...all.map((r) => [
206
+ r.id,
207
+ r.repository,
208
+ r.productionBranch,
209
+ r.configPath,
210
+ trusted(r),
211
+ r.state,
212
+ ]),
213
+ ])
214
+ : `No repositories this key lists. Connect one: ${me} ci connect acme/web`, all);
215
+ return 0;
216
+ }
217
+ case "disconnect":
218
+ return disconnect(need(rest[0], "the repository's id or owner/name"));
219
+ case "ls":
220
+ case "list": {
221
+ const args = parse(rest, [], ["repository", "branch", "commit", "pr", "state", "limit"]);
222
+ const commit = flag(args, "commit");
223
+ if (commit !== undefined && !/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(commit))
224
+ throw usage(`--commit takes a full sha in lower case: ${me} ci ls --commit $(git rev-parse HEAD)`);
225
+ const states = args.flags.get("state");
226
+ for (const state of states ?? [])
227
+ if (!STATES.includes(state))
228
+ throw unknown("state", state, STATES);
229
+ const pullRequest = whole(flag(args, "pr"), "--pr");
230
+ const limit = whole(flag(args, "limit"), "--limit") ?? 20;
231
+ const page = await api.checks.list({
232
+ limit: Math.min(limit, 100),
233
+ ...(flag(args, "repository") ? { repositoryId: flag(args, "repository") } : {}),
234
+ ...(flag(args, "branch") ? { branch: flag(args, "branch") } : {}),
235
+ ...(commit ? { commit } : {}),
236
+ ...(pullRequest === undefined ? {} : { pullRequest }),
237
+ ...(states ? { state: states } : {}),
238
+ });
239
+ const all = await page.toArray(limit);
240
+ print(all.length
241
+ ? table([
242
+ ["ID", "STATE", "REPOSITORY", "WHERE", "COMMIT", "JOBS", "TOOK", "CREATED"],
243
+ ...all.map((c) => [
244
+ c.id,
245
+ c.waiting ? "waiting" : c.state,
246
+ c.repository,
247
+ where(c),
248
+ c.commit.slice(0, 12),
249
+ counted(c.counts),
250
+ took(c.startedAt, c.endedAt),
251
+ when(c.createdAt),
252
+ ]),
253
+ ])
254
+ : "No checks.", all);
255
+ return 0;
256
+ }
257
+ case "get": {
258
+ const check = await api.checks.get(need(rest[0], "a check's id"));
259
+ const job = (j) => [
260
+ j.id,
261
+ j.parts > 1 ? `${j.name} (${j.part}/${j.parts})` : j.name,
262
+ j.state,
263
+ j.ms === null ? "-" : `${(j.ms / 1000).toFixed(1)}s`,
264
+ j.exitCode === null ? "-" : String(j.exitCode),
265
+ j.sandboxIds.join(",") || "-",
266
+ ];
267
+ print([
268
+ `${check.repository} ${check.commit.slice(0, 12)} ${where(check)}: ${check.waiting ? "waiting for a writer to re-run it on GitHub" : check.state}${check.reason ? ` (${check.reason})` : ""}`,
269
+ ...(check.machines ? [machined(check.machines)] : []),
270
+ check.jobs.length
271
+ ? table([["JOB", "NAME", "STATE", "TOOK", "EXIT", "SANDBOXES"], ...check.jobs.map(job)])
272
+ : "No jobs yet.",
273
+ ].join("\n"), check);
274
+ return 0;
275
+ }
276
+ case "cancel": {
277
+ const check = await api.checks.cancel(need(rest[0], "a check's id (runtime ci ls)"));
278
+ print(`${check.repository} ${check.commit.slice(0, 12)} ${where(check)}: ${check.state}${check.reason ? ` (${check.reason})` : ""}`, check);
279
+ return 0;
280
+ }
281
+ case "logs":
282
+ case "job": {
283
+ const job = await api.jobs.get(need(rest[0], "a job's id (runtime ci get <checkId>)"));
284
+ if (out.json) {
285
+ out.write(JSON.stringify(job));
286
+ return 0;
287
+ }
288
+ out.write(job.log.endsWith("\n") || !job.log ? job.log : `${job.log}\n`);
289
+ if (job.review)
290
+ out.write(reviewed(job.review));
291
+ out.error(`[${job.name}${job.parts > 1 ? ` ${job.part}/${job.parts}` : ""} ${job.state}${job.exitCode === null ? "" : `, exit code ${job.exitCode}`}${job.reason ? `, ${job.reason}` : ""}]`);
292
+ if (job.state === "failed" && job.sandboxIds.length)
293
+ out.error(`Its sandbox may be kept paused, to look into: ${me} sandboxes wake ${job.sandboxIds[0]}, then ${me} sandboxes shell ${job.sandboxIds[0]}`);
294
+ return 0;
295
+ }
296
+ case "flakes": {
297
+ const args = parse(rest, ["all"], ["repository"]);
298
+ const repositoryId = flag(args, "repository");
299
+ const all = await (await api.flakes.list({
300
+ limit: 100,
301
+ ...(repositoryId ? { repositoryId } : {}),
302
+ ...(has(args, "all") ? {} : { state: ["quarantined", "gating"] }),
303
+ })).toArray(Infinity);
304
+ print(all.length
305
+ ? table([
306
+ ["ID", "FILE", "STATE", "OWNER", "DUE"],
307
+ ...all.map((f) => [
308
+ f.id,
309
+ f.file,
310
+ f.state,
311
+ ownerOf(f),
312
+ f.endedAt !== null ? "-" : `${when(f.dueAt)}${f.overdue ? " (overdue)" : ""}`,
313
+ ]),
314
+ ])
315
+ : "No flaky tests.", all);
316
+ return 0;
317
+ }
318
+ case "flake": {
319
+ const [sub, ...more] = rest;
320
+ if (sub === "prove" || sub === "hand" || sub === "delete") {
321
+ const args = parse(more, [], ["reason"]);
322
+ const id = need(args.positional[0], "a flake's id (runtime ci flakes)");
323
+ const change = sub === "prove"
324
+ ? { prove: true }
325
+ : sub === "hand"
326
+ ? { owner: need(args.positional[1], "a GitHub login, or org/team") }
327
+ : { deleteReason: need(flag(args, "reason"), "--reason <why its test is deleted>") };
328
+ const f = await api.flakes.update(id, change);
329
+ print(sub === "prove"
330
+ ? `${f.file}: its 100 proof runs come at main's next commit.\n`
331
+ : sub === "hand"
332
+ ? `${f.file}: handed to ${ownerOf(f)}.\n`
333
+ : `${f.file}: it ends once main no longer holds the file.\n`, f);
334
+ return 0;
335
+ }
336
+ const f = await api.flakes.get(need(sub, "a flake's id (runtime ci flakes)"));
337
+ const runs = f.runs
338
+ .slice(0, 20)
339
+ .map((r) => `${r.passed ? "pass" : "FAIL"} ${r.attempt}`)
340
+ .join(", ");
341
+ print([
342
+ `${f.file}: ${f.state}${f.gates ? ` (still gates: ${f.gates.words})` : ""}`,
343
+ ` owner ${ownerOf(f)}, due ${when(f.dueAt)}${f.overdue ? " (overdue)" : ""}`,
344
+ ` proof passed and failed under key ${f.proof.key.slice(0, 12)} at ${f.proof.commit.slice(0, 9)}`,
345
+ ` runs ${runs || "-"}`,
346
+ ...f.failures
347
+ .slice(0, 5)
348
+ .map((x) => ` failed ${x.jobId} at ${x.commit.slice(0, 9)}: ${x.message ?? ""} (${me} ci logs ${x.jobId})`),
349
+ ...(f.deleteReason ? [` delete ${f.deleteReason}`] : []),
350
+ "",
351
+ ].join("\n"), f);
352
+ return 0;
353
+ }
354
+ case "tests": {
355
+ const args = parse(rest, ["quarantined"], []);
356
+ const repositoryId = need(args.positional[0], "a repository's id (runtime ci repos)");
357
+ const all = await (await api.tests.list(repositoryId, {
358
+ limit: 100,
359
+ ...(has(args, "quarantined") ? { quarantined: true } : {}),
360
+ })).toArray(Infinity);
361
+ print(all.length
362
+ ? table([
363
+ ["FILE", "TOOK", "RECENT", "QUARANTINED"],
364
+ ...all.map((t) => [
365
+ t.file,
366
+ `${(t.ms / 1000).toFixed(1)}s`,
367
+ t.runs
368
+ .slice(-5)
369
+ .map((r) => r.outcome)
370
+ .join(" "),
371
+ t.quarantinedSince === null ? "-" : when(t.quarantinedSince),
372
+ ]),
373
+ ])
374
+ : "No test files yet.", all);
375
+ return 0;
376
+ }
377
+ }
378
+ throw unknown("ci command", verb, [
379
+ "connect",
380
+ "repos",
381
+ "disconnect",
382
+ "ls",
383
+ "get",
384
+ "cancel",
385
+ "logs",
386
+ "tests",
387
+ "flakes",
388
+ "flake",
389
+ ]);
390
+ }
391
+ /** A review's verdict in words: each finding with its place and fix, then
392
+ * the notes, then who reviewed. */
393
+ function reviewed(r) {
394
+ const at = (f) => (f.line > 0 ? `${f.file}:${f.line}` : f.file);
395
+ return [
396
+ `\n${r.verdict === "pass" ? "Passed review" : `Blocked: ${r.findings.length} finding(s)`}. ${r.summary}`,
397
+ ...r.findings.map((f) => ` ${f.kind} at ${at(f)}: ${f.what}\n Fix: ${f.fix}`),
398
+ ...r.notes.map((n) => ` note at ${at(n)}: ${n.what}`),
399
+ `Reviewed by agent ${r.agent} (${r.model}) on ${r.account}${r.cut ? ", diff cut short" : ""}: session ${r.session}`,
400
+ "",
401
+ ].join("\n");
402
+ }
@@ -0,0 +1,23 @@
1
+ export type CiSource = {
2
+ path: string;
3
+ text: string;
4
+ };
5
+ export type CiImport = {
6
+ /** The `runtime.yml`, its notes on top as comments; `null` when no job runs on a push or pull request. */
7
+ yaml: string | null;
8
+ /** What was not carried over, or carried differently, one line each. */
9
+ notes: string[];
10
+ /** The steps written, by name. */
11
+ steps: string[];
12
+ };
13
+ /** Every combination of a GitHub `strategy.matrix`, its `exclude` and `include` applied. */
14
+ export declare function matrixCells(matrix: Record<string, unknown> | undefined): Record<string, string>[];
15
+ /** Files a cache key hashes: GitHub's `hashFiles('a', 'b')` or CircleCI's `{{ checksum "a" }}`. */
16
+ export declare function cacheFiles(key: string): string[];
17
+ /** Whether a line installs the project's own dependencies and nothing else:
18
+ * `npm ci` or `pip install -r requirements.txt`, never `npm install pino@9`,
19
+ * which one matrix cell asks and another doesn't. */
20
+ export declare function installsOnly(line: string): boolean;
21
+ /** `runtime.yml` from GitHub Actions workflows (`.github/workflows/*.yml`)
22
+ * and a CircleCI config (`.circleci/config.yml`), each given with its path. */
23
+ export declare function importCi(files: CiSource[]): CiImport;