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
@@ -0,0 +1,565 @@
1
+ import { parse, stringify } from "yaml";
2
+ /** The most a step's `timeout:` may say, in seconds: GitHub's own 6 hours. */
3
+ const MOST_TIMEOUT_S = 6 * 3600;
4
+ const MOST_STEPS = 64;
5
+ const MOST_CACHE = 32;
6
+ // A project's own install: run once in setup and kept in the warm machine,
7
+ // so a check runs only its tests.
8
+ const INSTALL = /^(npm (ci|install|i)|yarn( install)?|pnpm (install|i)|bun (install|i)|pip3? install|python3? -m pip install|poetry install|uv sync|pipenv install|go mod download|cargo fetch|bundle install|composer install|mix deps\.get)(?=\s|$)/;
9
+ // Actions whose work a Runtime machine already did: the commit is checked
10
+ // out, and Node, Bun, Python, git and a C compiler are on it.
11
+ const DONE = /^actions\/checkout@/;
12
+ const TOOLS = /^(actions\/setup-(node|python|go|java|dotnet)|oven-sh\/setup-bun|pnpm\/action-setup|dtolnay\/rust-toolchain|actions-rs\/toolchain|ruby\/setup-ruby|astral-sh\/setup-uv|denoland\/setup-deno)@/;
13
+ const CACHE = /^actions\/cache(\/restore|\/save)?@/;
14
+ const HAS = "a machine has Node, Bun, Python, git and a C compiler; install anything else in setup";
15
+ const stepName = (raw) => raw
16
+ .replace(/[^A-Za-z0-9 _.:-]+/g, " ")
17
+ .replace(/\s+/g, " ")
18
+ .trim()
19
+ .replace(/^[^A-Za-z0-9]+/, "")
20
+ .slice(0, 63)
21
+ .trim() || "job";
22
+ const envName = (k) => /^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(k) && !k.startsWith("RUNTIME_");
23
+ const quoteSh = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
24
+ /** A value as one shell word: its literal text single-quoted, each variable
25
+ * `expand` put in (`${RUNTIME_CI_COMMIT}` for `${{ github.sha }}`) in double
26
+ * quotes, so the shell fills it in and nothing else in the value is read. */
27
+ const wordSh = (parts) => parts.map((p) => (typeof p === "string" ? quoteSh(p) : `"\${${p.v}}"`)).join("") || "''";
28
+ const asText = (v) => v === null || v === undefined
29
+ ? ""
30
+ : typeof v === "string"
31
+ ? v
32
+ : typeof v === "number" || typeof v === "boolean"
33
+ ? `${v}`
34
+ : JSON.stringify(v);
35
+ /** An action's name with its pinned ref left off: `actions/cache@<sha>` is `actions/cache`. */
36
+ const action = (uses) => uses.split("@")[0];
37
+ /** Every combination of a GitHub `strategy.matrix`, its `exclude` and `include` applied. */
38
+ export function matrixCells(matrix) {
39
+ if (!matrix || typeof matrix !== "object")
40
+ return [{}];
41
+ const keys = Object.keys(matrix).filter((k) => k !== "include" && k !== "exclude");
42
+ let cells = keys.length ? [{}] : [];
43
+ for (const key of keys) {
44
+ const values = matrix[key];
45
+ if (!Array.isArray(values))
46
+ continue;
47
+ cells = cells.flatMap((c) => values.map((v) => ({ ...c, [key]: asText(v) })));
48
+ }
49
+ const fits = (cell, rule) => Object.entries(rule).every(([k, v]) => cell[k] === v);
50
+ const text = (rule) => Object.fromEntries(Object.entries(rule && typeof rule === "object" ? rule : {}).map(([k, v]) => [k, asText(v)]));
51
+ for (const rule of matrix.exclude ?? [])
52
+ cells = cells.filter((c) => !fits(c, text(rule)));
53
+ for (const rule of matrix.include ?? []) {
54
+ const add = text(rule);
55
+ const base = Object.fromEntries(Object.entries(add).filter(([k]) => keys.includes(k)));
56
+ const matched = Object.keys(base).length ? cells.filter((c) => fits(c, base)) : [];
57
+ if (matched.length)
58
+ for (const c of matched)
59
+ Object.assign(c, add);
60
+ else
61
+ cells.push(add);
62
+ }
63
+ return cells.length ? cells : [{}];
64
+ }
65
+ /** Files a cache key hashes: GitHub's `hashFiles('a', 'b')` or CircleCI's `{{ checksum "a" }}`. */
66
+ export function cacheFiles(key) {
67
+ const out = [];
68
+ for (const m of key.matchAll(/hashFiles\(([^)]*)\)/g))
69
+ for (const f of m[1].matchAll(/['"]([^'"]+)['"]/g))
70
+ out.push(f[1]);
71
+ for (const m of key.matchAll(/checksum\s+"([^"]+)"/g))
72
+ out.push(m[1]);
73
+ return out.filter((f) => !f.includes("{{") && !f.startsWith("/") && !f.startsWith("~"));
74
+ }
75
+ /** Whether a line installs the project's own dependencies and nothing else:
76
+ * `npm ci` or `pip install -r requirements.txt`, never `npm install pino@9`,
77
+ * which one matrix cell asks and another doesn't. */
78
+ export function installsOnly(line) {
79
+ const t = line.trim();
80
+ const m = INSTALL.exec(t);
81
+ if (!m || /[|;&<>$`]/.test(t))
82
+ return false;
83
+ const words = t.slice(m[0].length).trim().split(/\s+/).filter(Boolean);
84
+ for (let i = 0; i < words.length; i++) {
85
+ const w = words[i];
86
+ if (["-r", "--requirement", "-e", "--editable", "-c"].includes(w))
87
+ i++;
88
+ else if (!w.startsWith("-") && w !== ".")
89
+ return false;
90
+ }
91
+ return true;
92
+ }
93
+ class Writer {
94
+ steps = [];
95
+ setup = [];
96
+ cache = new Set();
97
+ notes = [];
98
+ note(where, what) {
99
+ const line = `${where}: ${what}`.replace(/\s+/g, " ");
100
+ if (!this.notes.includes(line))
101
+ this.notes.push(line);
102
+ }
103
+ /** A step's lines, its installs moved to setup when they run there as
104
+ * they did in the step: `movable` (the step names no folder and no env of
105
+ * its own) and no line before it changes folder. Setup runs at the
106
+ * repository's root with no step's env, so `npm ci` under
107
+ * `working-directory: web`, or after `cd web`, stays in its step. */
108
+ lines(script, movable = true) {
109
+ const kept = [];
110
+ let moved = !movable;
111
+ for (const line of script.split("\n")) {
112
+ if (/(^|[;&|(]\s*)(cd|pushd)\b/.test(line.trim()))
113
+ moved = true;
114
+ if (!moved && installsOnly(line)) {
115
+ if (!this.setup.includes(line.trim()))
116
+ this.setup.push(line.trim());
117
+ }
118
+ else
119
+ kept.push(line.replace(/\s+$/, ""));
120
+ }
121
+ while (kept.length && !kept[kept.length - 1].trim())
122
+ kept.pop();
123
+ while (kept.length && !kept[0].trim())
124
+ kept.shift();
125
+ return kept;
126
+ }
127
+ unique(name) {
128
+ let n = stepName(name);
129
+ for (let i = 2; this.steps.some((s) => s.name === n); i++)
130
+ n = stepName(`${name} ${i}`);
131
+ return n;
132
+ }
133
+ }
134
+ /** A job's steps as one script. Each step of GitHub's or CircleCI's ran in a
135
+ * shell of its own, so a `cd` or an `export` in one never reached the next:
136
+ * with more than one, each is a subshell. Undefined when none runs anything. */
137
+ function script(blocks) {
138
+ const runs = blocks.filter((b) => b.some((l) => l.trim() && !l.trim().startsWith("#")));
139
+ if (!runs.length)
140
+ return undefined;
141
+ const lines = blocks.flatMap((b) => runs.length > 1 && runs.includes(b) ? ["(", ...b.map((l) => ` ${l}`), ")"] : b);
142
+ return ["set -e", ...lines].join("\n");
143
+ }
144
+ /** `${{ ... }}` in a GitHub value: matrix cells, the commit and the branch
145
+ * filled in; a secret, or anything else only GitHub knows, leaves the value
146
+ * out (undefined), and `bad` is told what. */
147
+ function expand(value, cell, bad) {
148
+ const parts = expandParts(value, cell, bad);
149
+ return parts?.map((p) => (typeof p === "string" ? p : `\${${p.v}}`)).join("");
150
+ }
151
+ /** `expand`'s value as its literal parts and the variables put in for
152
+ * GitHub's values, for a value written as one shell word (`wordSh`). */
153
+ function expandParts(value, cell, bad) {
154
+ let ok = true;
155
+ const parts = [];
156
+ const text = (t) => {
157
+ if (!t)
158
+ return;
159
+ const last = parts[parts.length - 1];
160
+ if (typeof last === "string")
161
+ parts[parts.length - 1] = last + t;
162
+ else
163
+ parts.push(t);
164
+ };
165
+ let at = 0;
166
+ for (const m of value.matchAll(/\$\{\{([\s\S]*?)\}\}/g)) {
167
+ text(value.slice(at, m.index));
168
+ at = m.index + m[0].length;
169
+ const expr = m[1].trim();
170
+ const c = /^matrix\.([A-Za-z0-9_-]+)$/.exec(expr);
171
+ const e = /^env\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(expr);
172
+ if (c && cell[c[1]] !== undefined)
173
+ text(cell[c[1]]);
174
+ else if (expr === "github.sha")
175
+ parts.push({ v: "RUNTIME_CI_COMMIT" });
176
+ else if (expr === "github.ref_name" || expr === "github.head_ref")
177
+ parts.push({ v: "RUNTIME_CI_BRANCH" });
178
+ else if (expr === "github.workspace")
179
+ parts.push({ v: "PWD" });
180
+ else if (e)
181
+ parts.push({ v: e[1] });
182
+ else {
183
+ const s = /^secrets\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(expr);
184
+ bad(s ? `secret ${s[1]}` : m[0]);
185
+ ok = false;
186
+ }
187
+ }
188
+ text(value.slice(at));
189
+ return ok ? parts : undefined;
190
+ }
191
+ /** A YAML mapping, or an empty one for anything else. */
192
+ const obj = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
193
+ const list = (v) => (Array.isArray(v) ? v : []);
194
+ /** A key that is one name or a list of them (`needs: a` or `needs: [a, b]`). */
195
+ const names = (v) => (typeof v === "string" ? [v] : list(v).map(asText));
196
+ const runsOnPush = (on) => {
197
+ const events = typeof on === "string" ? [on] : Array.isArray(on) ? on : Object.keys(obj(on));
198
+ return events.some((e) => e === "push" || e === "pull_request" || e === "pull_request_target");
199
+ };
200
+ function read(w, file) {
201
+ try {
202
+ return obj(parse(file.text));
203
+ }
204
+ catch (error) {
205
+ w.note(file.path, `not read: ${error.message.split("\n")[0]}`);
206
+ return undefined;
207
+ }
208
+ }
209
+ function fromActions(w, file) {
210
+ const doc = read(w, file);
211
+ if (!doc?.jobs)
212
+ return;
213
+ if (!runsOnPush(doc.on)) {
214
+ w.note(file.path, "skipped: it runs on neither push nor pull_request");
215
+ return;
216
+ }
217
+ const top = obj(doc.env);
218
+ const named = new Map();
219
+ const made = [];
220
+ for (const [id, raw] of Object.entries(obj(doc.jobs))) {
221
+ const job = obj(raw);
222
+ const where = `${file.path} ${id}`;
223
+ if (job.uses) {
224
+ w.note(where, `skipped: a reusable workflow (${asText(job.uses)}); import that file instead`);
225
+ continue;
226
+ }
227
+ const runsOn = asText(job["runs-on"]);
228
+ if (job.if)
229
+ w.note(where, `its if (${asText(job.if)}) is not carried: it runs on every check`);
230
+ const timeout = jobTimeout(w, where, job["timeout-minutes"]);
231
+ if (job["continue-on-error"])
232
+ w.note(where, "continue-on-error is not carried: a failure fails the check");
233
+ const dir = asText(obj(obj(job.defaults).run)["working-directory"]);
234
+ const cells = matrixCells(obj(obj(job.strategy).matrix));
235
+ const jobSteps = list(job.steps).map(obj);
236
+ // What the kept commands read: a matrix key only a left-out setup
237
+ // action read (a Node version, say) names no step.
238
+ const reads = JSON.stringify([
239
+ job.env,
240
+ job.services,
241
+ jobSteps.map((s) => [s.run, s.env, s["working-directory"]]),
242
+ ]);
243
+ const steps = [];
244
+ const bodies = new Set();
245
+ for (const cell of cells) {
246
+ const os = expand(runsOn, cell, () => { }) ?? runsOn;
247
+ if (/macos|windows/i.test(os)) {
248
+ w.note(where, `${os} is left out: Runtime machines are Linux`);
249
+ continue;
250
+ }
251
+ const bad = (what) => w.note(where, `${what} is not carried; set it in the step's env or a Runtime secret`);
252
+ const env = {};
253
+ for (const [k, v] of Object.entries({ ...top, ...obj(job.env) })) {
254
+ const value = expand(asText(v), cell, bad);
255
+ if (value !== undefined && envName(k))
256
+ env[k] = value;
257
+ }
258
+ const blocks = [];
259
+ for (const [name, raw] of Object.entries(obj(job.services))) {
260
+ const svc = obj(raw);
261
+ const image = expand(asText(typeof raw === "string" ? raw : svc.image), cell, bad);
262
+ if (!image)
263
+ continue;
264
+ const flags = Object.entries(obj(svc.env)).map(([k, v]) => `-e ${k}=${wordSh(expandParts(asText(v), cell, bad) ?? [])}`);
265
+ const ports = list(svc.ports).map(asText);
266
+ const moved = ports.some((p) => {
267
+ const [a, b] = p.split(":");
268
+ return b !== undefined && a !== b;
269
+ });
270
+ const net = moved ? ports.map((p) => `-p ${p}`) : ["--network host"];
271
+ blocks.push([
272
+ ["docker run -d --name", stepName(name).replace(/ /g, "-"), ...net, ...flags, image].join(" "),
273
+ ]);
274
+ w.note(where, `service ${name} starts with Docker in the step; wait for it as its health check did`);
275
+ }
276
+ for (const s of jobSteps) {
277
+ const uses = asText(s.uses);
278
+ if (uses) {
279
+ if (DONE.test(uses))
280
+ continue;
281
+ if (TOOLS.test(uses))
282
+ w.note(where, `${action(uses)} is not needed: ${HAS}`);
283
+ else if (CACHE.test(uses))
284
+ for (const f of cacheFiles(asText(obj(s.with).key)))
285
+ w.cache.add(f);
286
+ else {
287
+ w.note(where, `action ${action(uses)} is not carried; run its commands in the step`);
288
+ blocks.push([`# was: uses: ${action(uses)}`]);
289
+ }
290
+ continue;
291
+ }
292
+ if (!s.run)
293
+ continue;
294
+ const said = asText(s.name ?? s.run).split("\n")[0];
295
+ if (s.if && /failure\(\)|cancelled\(\)/.test(asText(s.if))) {
296
+ w.note(where, `step "${said}" ran only on failure; left out`);
297
+ continue;
298
+ }
299
+ if (s.if)
300
+ w.note(where, `step "${said}": its if (${asText(s.if)}) is not carried`);
301
+ const run = expand(asText(s.run), cell, bad);
302
+ if (run === undefined) {
303
+ blocks.push([`# left out (it needs a secret or a GitHub value): ${said}`]);
304
+ continue;
305
+ }
306
+ const exports = [];
307
+ for (const [k, v] of Object.entries(obj(s.env))) {
308
+ const value = expandParts(asText(v), cell, bad);
309
+ if (value !== undefined && envName(k))
310
+ exports.push(`export ${k}=${wordSh(value)}`);
311
+ }
312
+ const wd = asText(s["working-directory"]) || dir;
313
+ const lines = w.lines(run, !wd && !exports.length);
314
+ if (!lines.length)
315
+ continue;
316
+ blocks.push([...(wd ? [`cd ${quoteSh(wd)}`] : []), ...exports, ...lines]);
317
+ }
318
+ const run = script(blocks);
319
+ if (!run) {
320
+ w.note(where, "has no command left to run here; not written");
321
+ continue;
322
+ }
323
+ // Cells that differ only in what a left-out action read run the same
324
+ // commands: one step runs them.
325
+ const body = JSON.stringify([run, env]);
326
+ if (bodies.has(body)) {
327
+ w.note(where, "matrix cells that differ only in a tool's version run once, on the machine's own");
328
+ continue;
329
+ }
330
+ bodies.add(body);
331
+ const label = Object.entries(cell)
332
+ .filter(([k]) => reads.includes(`matrix.${k}`))
333
+ .map(([, v]) => v)
334
+ .join(" ");
335
+ const title = asText(job.name || id).includes("${{") ? id : asText(job.name || id);
336
+ steps.push({
337
+ name: w.unique(label ? `${title} ${label}` : title),
338
+ run,
339
+ needs: [],
340
+ env,
341
+ ...(timeout ? { timeout } : {}),
342
+ });
343
+ }
344
+ made.push({ steps, needs: names(job.needs) });
345
+ named.set(id, steps.map((s) => s.name));
346
+ }
347
+ for (const m of made)
348
+ for (const s of m.steps) {
349
+ s.needs = m.needs.flatMap((n) => named.get(n) ?? []);
350
+ w.steps.push(s);
351
+ }
352
+ }
353
+ /** A GitHub job's `timeout-minutes` as a step's `timeout:` in seconds;
354
+ * undefined (the default, 6 hours as GitHub's) when it says none, or a value
355
+ * that is not a number, which is noted. */
356
+ function jobTimeout(w, where, raw) {
357
+ if (raw === undefined || raw === null || raw === "")
358
+ return undefined;
359
+ const minutes = Number(raw);
360
+ if (!Number.isFinite(minutes) || minutes <= 0) {
361
+ w.note(where, `timeout-minutes ${asText(raw)} is not carried; the step has the default 6 hours`);
362
+ return undefined;
363
+ }
364
+ const seconds = Math.ceil(minutes * 60);
365
+ if (seconds > MOST_TIMEOUT_S) {
366
+ w.note(where, `asks ${asText(raw)} minutes; a step here runs at most 360, as on GitHub`);
367
+ return MOST_TIMEOUT_S;
368
+ }
369
+ return seconds;
370
+ }
371
+ // CircleCI's resource classes by vCPUs: medium is 2, large 4, xlarge 8, 2xlarge 16.
372
+ const CIRCLE_MACHINES = {
373
+ small: "2cpu",
374
+ medium: "2cpu",
375
+ large: "4cpu",
376
+ xlarge: "8cpu",
377
+ "2xlarge": "16cpu",
378
+ "2xlarge+": "16cpu",
379
+ };
380
+ const KEPT_BY_CHECK = [
381
+ "store_artifacts",
382
+ "store_test_results",
383
+ "persist_to_workspace",
384
+ "attach_workspace",
385
+ ];
386
+ function fromCircle(w, file) {
387
+ const doc = read(w, file);
388
+ if (!doc)
389
+ return;
390
+ const jobs = obj(doc.jobs);
391
+ const commands = obj(doc.commands);
392
+ if (doc.orbs)
393
+ w.note(file.path, `orbs (${Object.keys(obj(doc.orbs)).join(", ")}) are not carried; run their commands in the step`);
394
+ // The jobs each workflow runs, with what each requires; every job when no workflow says.
395
+ const runs = [];
396
+ const workflows = Object.entries(obj(doc.workflows))
397
+ .filter(([k, v]) => k !== "version" && obj(v).jobs)
398
+ .map(([k, v]) => [k, obj(v)]);
399
+ for (const [wf, flow] of workflows) {
400
+ if (flow.triggers) {
401
+ w.note(`${file.path} ${wf}`, "skipped: a scheduled workflow");
402
+ continue;
403
+ }
404
+ for (const entry of list(flow.jobs)) {
405
+ if (typeof entry === "string")
406
+ runs.push({ job: entry, name: entry, requires: [] });
407
+ else
408
+ for (const [job, raw] of Object.entries(obj(entry))) {
409
+ const opts = obj(raw);
410
+ if (opts.type === "approval") {
411
+ w.note(`${file.path} ${job}`, "an approval hold is not carried");
412
+ continue;
413
+ }
414
+ if (opts.matrix)
415
+ w.note(`${file.path} ${job}`, "its matrix is not carried; one step is written");
416
+ runs.push({ job, name: asText(opts.name ?? job), requires: names(opts.requires) });
417
+ }
418
+ }
419
+ }
420
+ if (!workflows.length)
421
+ for (const job of Object.keys(jobs))
422
+ runs.push({ job, name: job, requires: [] });
423
+ const named = new Map();
424
+ const pending = [];
425
+ for (const r of runs) {
426
+ if (named.has(r.name))
427
+ continue;
428
+ const where = `${file.path} ${r.job}`;
429
+ if (!jobs[r.job]) {
430
+ w.note(where, "is an orb's job; not carried");
431
+ continue;
432
+ }
433
+ const job = obj(jobs[r.job]);
434
+ if (job.macos || job.windows) {
435
+ w.note(where, "skipped: Runtime machines are Linux");
436
+ continue;
437
+ }
438
+ const env = {};
439
+ for (const [k, v] of Object.entries(obj(job.environment)))
440
+ if (envName(k))
441
+ env[k] = asText(v);
442
+ const blocks = [];
443
+ const docker = list(job.docker).map(obj);
444
+ if (docker[0]?.image)
445
+ w.note(where, `ran in ${asText(docker[0].image)}; here ${HAS}`);
446
+ for (const svc of docker.slice(1)) {
447
+ const image = asText(svc.image);
448
+ const flags = Object.entries(obj(svc.environment)).map(([k, v]) => `-e ${k}=${quoteSh(asText(v))}`);
449
+ const name = stepName(asText(svc.name) || (image.split(":")[0].split("/").pop() ?? "")).replace(/ /g, "-");
450
+ blocks.push([["docker run -d --name", name, "--network host", ...flags, image].join(" ")]);
451
+ w.note(where, `service ${image} starts with Docker in the step; wait for it before the tests`);
452
+ }
453
+ const dir = asText(job.working_directory);
454
+ const walk = (steps, depth) => {
455
+ for (const raw of steps) {
456
+ const [kind, opts] = typeof raw === "string"
457
+ ? [raw, undefined]
458
+ : (Object.entries(obj(raw))[0] ?? ["", undefined]);
459
+ // The checkout is made; Docker runs on the machine itself.
460
+ if (kind === "checkout" || kind === "setup_remote_docker")
461
+ continue;
462
+ if (kind === "run") {
463
+ const o = obj(opts);
464
+ const cmd = asText(typeof opts === "string" ? opts : o.command);
465
+ if (o.when === "on_fail") {
466
+ w.note(where, `step "${asText(o.name ?? cmd.split("\n")[0])}" ran only on failure; left out`);
467
+ continue;
468
+ }
469
+ const exports = Object.entries(obj(o.environment))
470
+ .filter(([k]) => envName(k))
471
+ .map(([k, v]) => `export ${k}=${quoteSh(asText(v))}`);
472
+ // A step's working_directory is under the job's, which is where
473
+ // the checkout went: here, the repository's root.
474
+ const wd = asText(o.working_directory);
475
+ const cd = wd && !/^[~/$]/.test(wd) && wd !== "." ? [`cd ${quoteSh(wd)}`] : [];
476
+ const lines = w.lines(cmd, !cd.length && !exports.length);
477
+ if (!lines.length)
478
+ continue;
479
+ if (wd && !cd.length && wd !== "." && wd !== dir)
480
+ w.note(where, `working_directory ${wd} is not carried; the step runs at the repository's root`);
481
+ blocks.push([...cd, ...exports, ...lines]);
482
+ }
483
+ else if (kind === "restore_cache" || kind === "save_cache") {
484
+ const o = obj(opts);
485
+ for (const key of [o.key, ...list(o.keys)])
486
+ for (const f of cacheFiles(asText(key)))
487
+ w.cache.add(f);
488
+ }
489
+ else if (commands[kind] && depth < 4) {
490
+ if (opts)
491
+ w.note(where, `command ${kind}'s parameters are not filled in`);
492
+ walk(list(obj(commands[kind]).steps), depth + 1);
493
+ }
494
+ else if (KEPT_BY_CHECK.includes(kind)) {
495
+ w.note(where, `${kind} is not carried: a check keeps each job's log, and a failed job's sandbox`);
496
+ }
497
+ else if (kind) {
498
+ w.note(where, `${kind} (an orb's or unknown step) is not carried; run its commands in the step`);
499
+ blocks.push([`# was: ${kind}`]);
500
+ }
501
+ }
502
+ };
503
+ walk(list(job.steps), 0);
504
+ const run = script(blocks);
505
+ if (!run) {
506
+ w.note(where, "has no command left to run here; not written");
507
+ continue;
508
+ }
509
+ const machine = CIRCLE_MACHINES[asText(job.resource_class).replace(/^.*\./, "")];
510
+ const step = {
511
+ name: w.unique(r.name),
512
+ run,
513
+ needs: [],
514
+ env,
515
+ ...(machine && machine !== "4cpu" ? { machine } : {}),
516
+ };
517
+ named.set(r.name, step.name);
518
+ pending.push({ step, requires: r.requires });
519
+ }
520
+ for (const p of pending) {
521
+ p.step.needs = p.requires.map((n) => named.get(n)).filter((n) => !!n);
522
+ w.steps.push(p.step);
523
+ }
524
+ }
525
+ /** `runtime.yml` from GitHub Actions workflows (`.github/workflows/*.yml`)
526
+ * and a CircleCI config (`.circleci/config.yml`), each given with its path. */
527
+ export function importCi(files) {
528
+ const w = new Writer();
529
+ for (const f of [...files].sort((a, b) => a.path.localeCompare(b.path)))
530
+ if (f.path.includes(".circleci/"))
531
+ fromCircle(w, f);
532
+ else
533
+ fromActions(w, f);
534
+ if (!w.steps.length)
535
+ return { yaml: null, notes: w.notes, steps: [] };
536
+ if (w.steps.length > MOST_STEPS) {
537
+ w.note("runtime.yml", `${w.steps.length} steps; a file holds ${MOST_STEPS}, the rest are left out`);
538
+ w.steps = w.steps.slice(0, MOST_STEPS);
539
+ }
540
+ const cache = [...w.cache].slice(0, MOST_CACHE);
541
+ const doc = {};
542
+ if (w.setup.length)
543
+ doc.setup = w.setup.join("\n");
544
+ if (cache.length)
545
+ doc.cache = cache;
546
+ doc.steps = w.steps.map((s) => ({
547
+ name: s.name,
548
+ run: s.run,
549
+ ...(s.needs.length ? { needs: s.needs } : {}),
550
+ ...(s.machine ? { machine: s.machine } : {}),
551
+ ...(s.timeout ? { timeout: s.timeout } : {}),
552
+ ...(Object.keys(s.env).length ? { env: s.env } : {}),
553
+ }));
554
+ const head = [
555
+ `# Written by \`runtime ci import\` from ${files.map((f) => f.path).join(", ")}.`,
556
+ ...(w.notes.length
557
+ ? ["# Not carried over, or carried differently:", ...w.notes.map((n) => `# - ${n}`)]
558
+ : []),
559
+ ];
560
+ return {
561
+ yaml: `${head.join("\n")}\n${stringify(doc, { lineWidth: 0 })}`,
562
+ notes: w.notes,
563
+ steps: w.steps.map((s) => s.name),
564
+ };
565
+ }
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { type Sandbox } from "./sandbox.js";
2
3
  import { RuntimeError } from "./errors.js";
3
4
  import type { OutOfMemory, Usage } from "./types.js";
4
5
  import type { SwitchProvider } from "./products/switching.js";
@@ -31,6 +32,9 @@ declare const kit: {
31
32
  table: (rows: string[][]) => string;
32
33
  usage: (message: string) => RuntimeError;
33
34
  unknown: (what: string, word: string, choices: readonly string[]) => RuntimeError;
35
+ /** `sandboxes exec`'s run and output, for any machine that execs. */
36
+ execute: (machine: Pick<Sandbox, "id" | "exec">, command: string[], args: Args, out: Out, env: NodeJS.ProcessEnv) => Promise<number>;
37
+ copyFile: (from: string, to: string, use: (id: string) => Promise<Pick<Sandbox, "files">>, side: string) => Promise<void>;
34
38
  };
35
39
  export type CliKit = typeof kit;
36
40
  export declare function run(argv: string[], env?: NodeJS.ProcessEnv, out?: Out): Promise<number>;
@@ -46,8 +50,9 @@ export declare function usageCsv(u: Usage): string;
46
50
  * command: the API's message and hint, on standard error. A bare exit -9 (or
47
51
  * 137) said nothing of memory until 9 October 2026. */
48
52
  export declare function outOfMemoryLines(memory: OutOfMemory | undefined): string | undefined;
49
- /** How to move a rival's code across, when one import does it. */
50
- export declare function dropInLines(provider: SwitchProvider | undefined): string[];
53
+ /** How to move a rival's code across, when one import (or, for Fly's
54
+ * Machines, one address) does it. `from` is the name the caller gave. */
55
+ export declare function dropInLines(provider: SwitchProvider | undefined, from?: string): string[];
51
56
  /** A secret's value, from standard input only, so it never sits in shell
52
57
  * history or a process list. Piped: everything, less one trailing newline.
53
58
  * A terminal: one line, not shown. */
@@ -62,16 +67,22 @@ export declare function cleanSecret(raw: string): string;
62
67
  * (ARCHITECTURE.md section 10, "Per-path rules on an egress secret") and says
63
68
  * which one it refused. */
64
69
  export declare function parseSecretRules(values: string[]): SecretRule[];
65
- /** `--only-label role=hub,tier=x --only-sandbox <id>`: a sandbox whose labels
66
- * hold every pair of any one --only-label gets the secret, and so does each
67
- * --only-sandbox. `--all-sandboxes`: every sandbox again. Nothing: undefined,
68
- * so a replacement keeps the scope it has. */
69
- export declare function parseSecretScope(labels: string[] | undefined, sandboxes: string[] | undefined, all: boolean): SecretScope | undefined;
70
+ /** `--only-label role=hub,tier=x --only-sandbox <id> --only-app <id>`: a
71
+ * sandbox whose labels hold every pair of any one --only-label gets the
72
+ * secret, and so does each --only-sandbox and every copy of each --only-app.
73
+ * `--all-sandboxes`: every sandbox again. Nothing: undefined, so a
74
+ * replacement keeps the scope it has. */
75
+ export declare function parseSecretScope(labels: string[] | undefined, sandboxes: string[] | undefined, all: boolean, apps?: string[]): SecretScope | undefined;
70
76
  /** Who gets a secret, in a few words. */
71
77
  export declare const scopeText: (only: SecretScope | undefined) => string;
72
78
  export declare function secretsCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out,
73
79
  /** Tests give the value here; the CLI reads standard input. */
74
80
  read?: () => Promise<string>): Promise<number>;
81
+ /** A booking's time on the command line: an ISO time, `now`, or HH:MM on this
82
+ * machine's clock. HH:MM is the next such time at or after `after` (the
83
+ * window's start for `--to`, so an end before the start is the next day, and
84
+ * `now` for `--from`: today, or tomorrow once it has passed). */
85
+ export declare function bookingTime(name: string, value: string, now: number, after?: number): Date;
75
86
  export declare function imageCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out,
76
87
  /** Tests give the registry secret here; the CLI reads standard input. */
77
88
  read?: () => Promise<string>): Promise<number>;