@shipvo/cli 0.1.3 → 0.1.5

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 (58) hide show
  1. package/dist/adopt-bare.d.ts +39 -0
  2. package/dist/adopt-bare.js +236 -0
  3. package/dist/adopt.d.ts +26 -0
  4. package/dist/adopt.js +257 -0
  5. package/dist/agents.d.ts +14 -0
  6. package/dist/agents.js +124 -0
  7. package/dist/audit.d.ts +13 -0
  8. package/dist/audit.js +75 -0
  9. package/dist/autohalt.d.ts +5 -0
  10. package/dist/autohalt.js +84 -0
  11. package/dist/ci.d.ts +3 -0
  12. package/dist/ci.js +42 -0
  13. package/dist/cli.js +142 -88
  14. package/dist/config.d.ts +10 -0
  15. package/dist/config.js +38 -10
  16. package/dist/doctor.d.ts +14 -0
  17. package/dist/doctor.js +132 -7
  18. package/dist/eas.d.ts +29 -0
  19. package/dist/eas.js +258 -0
  20. package/dist/ensure-playwright.d.ts +21 -0
  21. package/dist/ensure-playwright.js +64 -0
  22. package/dist/explore.d.ts +84 -0
  23. package/dist/explore.js +247 -0
  24. package/dist/help.d.ts +4 -2
  25. package/dist/help.js +39 -3
  26. package/dist/init.js +501 -17
  27. package/dist/native.d.ts +3 -0
  28. package/dist/native.js +39 -0
  29. package/dist/policy.d.ts +4 -0
  30. package/dist/policy.js +17 -0
  31. package/dist/preview-recorder.d.ts +1 -0
  32. package/dist/preview-recorder.js +62 -0
  33. package/dist/preview.d.ts +35 -0
  34. package/dist/preview.js +346 -0
  35. package/dist/promote.d.ts +9 -0
  36. package/dist/promote.js +153 -0
  37. package/dist/publish.d.ts +1 -1
  38. package/dist/publish.js +73 -29
  39. package/dist/report-md.d.ts +26 -0
  40. package/dist/report-md.js +58 -0
  41. package/dist/run-bridge.d.ts +7 -0
  42. package/dist/run-bridge.js +374 -0
  43. package/dist/run-protocol.d.ts +32 -0
  44. package/dist/run-protocol.js +22 -0
  45. package/dist/run-ws.d.ts +8 -0
  46. package/dist/run-ws.js +89 -0
  47. package/dist/run.d.ts +86 -0
  48. package/dist/run.js +301 -0
  49. package/dist/sim.d.ts +31 -0
  50. package/dist/sim.js +124 -5
  51. package/dist/status.d.ts +64 -0
  52. package/dist/status.js +147 -0
  53. package/dist/store.d.ts +38 -0
  54. package/dist/store.js +297 -0
  55. package/dist/templates/capacitor/github-actions/build.yml +122 -0
  56. package/dist/test.d.ts +46 -0
  57. package/dist/test.js +190 -0
  58. package/package.json +10 -5
package/dist/publish.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const PUBLISH_HELP = "Usage: shipvo publish [flags]\n\nBuild, sign and publish an over-the-air release.\n\nFlags (write a value after a space, not =):\n --bands b0,b1,b2 Cohorts to publish to (default b0,b1,b2)\n --platform android|ios|all Platform to publish (default android)\n --release N Release number (default: the next one)\n --min-shell X.Y.Z Oldest app shell that can run this release (default: version in app.json)\n --hermes-bytecode-version V Refuse bundles built for another Hermes version, for example 96 or 96,98\n --max-bases N How many older releases to build patches from (default 6)\n --skip-export true Reuse the last build in dist-ota/\n --skip-web true Do not publish the web copy for the simulator\n --dry-run Build and sign, write files locally, send nothing\n --out <dir> Where --dry-run writes files (default .shipvo/dry-run)\n --help Show this help\n";
1
+ export declare const PUBLISH_HELP = "Usage: shipvo publish [flags]\n\nBuild, sign and publish an over-the-air release.\n\nFlags (write a value after a space, not =; on/off flags can be bare, or true/false):\n --bands b0,b1,b2 Cohorts to publish to (default b0,b1,b2)\n --platform android|ios|all Platform to publish (default android)\n --release N Release number (default: the next one)\n --min-shell X.Y.Z Oldest app shell that can run this release (default: version in app.json)\n --hermes-bytecode-version V Refuse bundles built for another Hermes version, for example 96 or 96,98\n --max-bases N How many older releases to build patches from (default 6)\n --skip-export Reuse the last build in dist-ota/\n --skip-web Do not publish the web copy for the simulator\n --dry-run Build and sign, write files locally, send nothing\n --dry-run-policy Check the policy in shipvo.json for these --bands, print the result, publish nothing\n --out <dir> Where --dry-run writes files (default .shipvo/dry-run)\n --help Show this help\n";
2
2
  export declare function publish(argv: string[]): Promise<void>;
3
3
  /** Bytes in plain units: KB below 1 MB, so a small bundle never reads as 0.00 MB. */
4
4
  export declare function formatSize(bytes: number): string;
package/dist/publish.js CHANGED
@@ -3,9 +3,11 @@
3
3
  import { execSync } from "node:child_process";
4
4
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
- import { dirname, join, resolve } from "node:path";
6
+ import { dirname, join, relative, resolve } from "node:path";
7
7
  import { sha256Hex, signBytes } from "./index.js";
8
8
  import { checkBundle, parseDeclared } from "./hermes.js";
9
+ import { requiredNativeModules } from "./native.js";
10
+ import { checkPublishPolicy } from "./policy.js";
9
11
  import { args as parse, kv as kvFor, loadConfig, seed } from "./config.js";
10
12
  const MIME = { ".html": "text/html", ".js": "text/javascript", ".json": "application/json", ".css": "text/css", ".ico": "image/x-icon", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".ttf": "font/ttf", ".svg": "image/svg+xml" };
11
13
  const walk = (d) => readdirSync(d).flatMap((f) => (statSync(join(d, f)).isDirectory() ? walk(join(d, f)) : [join(d, f)]));
@@ -43,6 +45,15 @@ function checkFlags(argv, known) {
43
45
  throw new Error(`Unknown flag ${bad.join(", ")}.${hint} Known flags: ${known.map((k) => "--" + k).join(" ")}`);
44
46
  }
45
47
  }
48
+ /** An on/off flag: bare or "true" is on, "false" is off. Anything else is an error, so a typo never flips it silently. */
49
+ function switchFlag(a, name) {
50
+ const v = a[name];
51
+ if (v === undefined || v === "false")
52
+ return false;
53
+ if (v === "true")
54
+ return true;
55
+ throw new Error(`--${name} is an on/off flag: use --${name}, --${name} true or --${name} false (got "${v}")`);
56
+ }
46
57
  /** A store that only writes files under `dir` (key `p/app/1/b0` becomes `dir/p/app/1/b0`). It reads back only what this run wrote (kept in memory), never old files, so the iOS pass can find the web artifact and a dry run never needs a control plane. */
47
58
  function localKv(dir) {
48
59
  const root = resolve(dir);
@@ -63,21 +74,22 @@ function localKv(dir) {
63
74
  },
64
75
  };
65
76
  }
66
- const PUBLISH_FLAGS = ["bands", "platform", "release", "min-shell", "hermes-bytecode-version", "max-bases", "skip-export", "skip-web", "dry-run", "out"];
77
+ const PUBLISH_FLAGS = ["bands", "platform", "release", "min-shell", "hermes-bytecode-version", "max-bases", "skip-export", "skip-web", "dry-run", "dry-run-policy", "out"];
67
78
  export const PUBLISH_HELP = `Usage: shipvo publish [flags]
68
79
 
69
80
  Build, sign and publish an over-the-air release.
70
81
 
71
- Flags (write a value after a space, not =):
82
+ Flags (write a value after a space, not =; on/off flags can be bare, or true/false):
72
83
  --bands b0,b1,b2 Cohorts to publish to (default b0,b1,b2)
73
84
  --platform android|ios|all Platform to publish (default android)
74
85
  --release N Release number (default: the next one)
75
86
  --min-shell X.Y.Z Oldest app shell that can run this release (default: version in app.json)
76
87
  --hermes-bytecode-version V Refuse bundles built for another Hermes version, for example 96 or 96,98
77
88
  --max-bases N How many older releases to build patches from (default 6)
78
- --skip-export true Reuse the last build in dist-ota/
79
- --skip-web true Do not publish the web copy for the simulator
89
+ --skip-export Reuse the last build in dist-ota/
90
+ --skip-web Do not publish the web copy for the simulator
80
91
  --dry-run Build and sign, write files locally, send nothing
92
+ --dry-run-policy Check the policy in shipvo.json for these --bands, print the result, publish nothing
81
93
  --out <dir> Where --dry-run writes files (default .shipvo/dry-run)
82
94
  --help Show this help
83
95
  `;
@@ -88,7 +100,17 @@ export async function publish(argv) {
88
100
  }
89
101
  checkFlags(argv, PUBLISH_FLAGS);
90
102
  const a = parse(argv), cfg = loadConfig(), SEED = seed();
91
- const dry = a["dry-run"] !== undefined; // a plain switch: any use turns it on
103
+ if (switchFlag(a, "dry-run-policy")) {
104
+ const verdict = checkPublishPolicy(cfg.policy, (a.bands || "b0,b1,b2").split(","));
105
+ if (verdict.ok)
106
+ console.log(cfg.policy === undefined ? "No policy in shipvo.json: nothing to check." : "Policy allows this publish.");
107
+ else {
108
+ console.error(`Refused by policy rule ${verdict.rule}: ${verdict.reason}`);
109
+ process.exitCode = 1;
110
+ }
111
+ return;
112
+ }
113
+ const dry = switchFlag(a, "dry-run"), skipExport = switchFlag(a, "skip-export"), skipWeb = switchFlag(a, "skip-web");
92
114
  if (a.out !== undefined && !dry)
93
115
  throw new Error("--out only works with --dry-run");
94
116
  if (a.out === "true")
@@ -100,10 +122,24 @@ export async function publish(argv) {
100
122
  if (a["hermes-bytecode-version"] === "true")
101
123
  throw new Error("--hermes-bytecode-version needs a value: pass a value like 96 (or 96,98)");
102
124
  const declaredHermes = parseDeclared(a["hermes-bytecode-version"] ?? cfg.hermesBytecodeVersion);
125
+ const skippedNative = [];
126
+ const nativeModules = requiredNativeModules(".", skippedNative);
127
+ if (skippedNative.length)
128
+ console.error(`native modules not recorded (the range is not a plain numeric semver range, so the control plane cannot check it): ${skippedNative.join(", ")}`);
129
+ if (dry)
130
+ console.error(`native modules this release requires: ${nativeModules ? Object.entries(nativeModules).map(([n, r]) => `${n}@${r}`).join(", ") : "none found"}`);
103
131
  const bands = (a.bands || "b0,b1,b2").split(",");
132
+ if (dry) {
133
+ const verdict = checkPublishPolicy(cfg.policy, bands);
134
+ if (!verdict.ok) {
135
+ console.error(`Refused by policy rule ${verdict.rule}: ${verdict.reason}`);
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ }
104
140
  const platforms = platformsOf(a);
105
141
  const release = Number(a.release || 0) || (await currentRelease(kv, APP, MAJOR)) + 1;
106
- const summary = { app: APP, release, bands, min_shell_version: MIN_SHELL, ...(dry ? { dry_run: true, out: outDir } : {}) };
142
+ const summary = { app: APP, release, bands, min_shell_version: MIN_SHELL, ...(nativeModules ? { required_native_modules: nativeModules } : {}), ...(dry ? { dry_run: true, out: outDir } : {}) };
107
143
  if (dry)
108
144
  console.error(`dry run: nothing is sent anywhere; files go to ${outDir}. No patches are built offline. Release is ${release}${a.release ? "" : " (pass --release N to match the control plane)"}.`);
109
145
  let totalBytes = 0;
@@ -111,7 +147,7 @@ export async function publish(argv) {
111
147
  for (const pf of platforms) {
112
148
  // Android published alone keeps the original dist-ota/ layout, so existing CI keeps working.
113
149
  const root = platforms.length === 1 && pf === "android" ? "dist-ota" : `dist-ota/${pf}`;
114
- if (a["skip-export"] !== "true")
150
+ if (!skipExport)
115
151
  execSync(`npx expo export --platform ${pf} --output-dir ${root} --clear`, { stdio: "inherit" });
116
152
  const jsDir = `${root}/_expo/static/js/${pf}`;
117
153
  const file = readdirSync(jsDir).find((f) => f.endsWith(".hbc")) || readdirSync(jsDir).find((f) => f.endsWith(".js"));
@@ -121,8 +157,8 @@ export async function publish(argv) {
121
157
  const hermesVersion = checkBundle(file, bytes, declaredHermes); // fails before anything is uploaded
122
158
  const name = bundleName(release, pf), newSha = sha256Hex(bytes);
123
159
  await kv.put(`b/${APP}/${name}`, bytes, "application/octet-stream");
124
- // Patches: from recent releases (and, on Android, each shell's embedded bundle); devices verify the patched result against the signed sha256.
125
- // iOS patches only from earlier iOS releases: the iOS runtime has no embedded-bundle baseline yet.
160
+ // Patches: from recent releases and each shell's embedded bundle; devices verify the patched result against the signed sha256.
161
+ // Releases are per platform. Embedded-bundle records are shared under base/<app>/ and tagged with a platform (no tag means Android).
126
162
  const patches = [];
127
163
  {
128
164
  const MAX_BASES = Number(a["max-bases"] || 6);
@@ -136,14 +172,14 @@ export async function publish(argv) {
136
172
  .filter((h) => h && h.release < release).sort((x, y) => y.release - x.release).slice(0, MAX_BASES - 2);
137
173
  for (const h of hist)
138
174
  bases.set(h.sha256, `b/${APP}/${bundleName(h.release, pf)}`);
139
- if (pf === "android")
140
- for (const k of await kv.keys(`base/${APP}/`)) {
141
- try {
142
- const b = JSON.parse((await kv.get(k)));
175
+ for (const k of await kv.keys(`base/${APP}/`)) {
176
+ try {
177
+ const b = JSON.parse((await kv.get(k)));
178
+ if ((b.platform ?? "android") === pf)
143
179
  bases.set(b.sha256, b.key);
144
- }
145
- catch { }
146
180
  }
181
+ catch { }
182
+ }
147
183
  // Only look for bsdiff4 when there is an older bundle to patch from; a first release needs no patches.
148
184
  let bsdiff = false;
149
185
  if (bases.size > 0) {
@@ -181,10 +217,10 @@ export async function publish(argv) {
181
217
  }
182
218
  // The agent-facing web artifact of the same commit, served at /w/<app>/r<N>/ for the simulator (built once, with the first platform).
183
219
  let web = null;
184
- if (a["skip-web"] !== "true" && pf === platforms[0]) {
185
- web = await publishWeb(kv, cfg, APP, release);
220
+ if (!skipWeb && pf === platforms[0]) {
221
+ web = await publishWeb(kv, cfg, APP, release, dry ? outDir : undefined);
186
222
  }
187
- else if (a["skip-web"] !== "true") {
223
+ else if (!skipWeb) {
188
224
  try {
189
225
  web = JSON.parse((await kv.get(`h/${APP}/r${release}`)))?.web ?? null;
190
226
  }
@@ -194,10 +230,10 @@ export async function publish(argv) {
194
230
  for (const band of bands) {
195
231
  const body = JSON.stringify({ schema_version: "1.0", app_id: APP, shell_major: MAJOR, band, ...(pf === "ios" ? { platform: "ios" } : {}), release, issued_at,
196
232
  core: { version: `1.0.${release}`, sha256: newSha, size: bytes.length, url: `b/${APP}/${name}`, patches },
197
- addons: [], screens: [], min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), ...(web ? { web } : {}) });
233
+ addons: [], screens: [], min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), ...(nativeModules ? { required_native_modules: nativeModules } : {}), ...(web ? { web } : {}) });
198
234
  await kv.put(pointerKey(APP, MAJOR, band, pf), JSON.stringify({ body, signature: signBytes(new TextEncoder().encode(body), SEED) }), "application/json");
199
235
  }
200
- await kv.put(pf === "ios" ? `h/${APP}/ios-r${release}` : `h/${APP}/r${release}`, JSON.stringify({ release, issued_at, sha256: newSha, size: bytes.length, min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), web }), "application/json");
236
+ await kv.put(pf === "ios" ? `h/${APP}/ios-r${release}` : `h/${APP}/r${release}`, JSON.stringify({ release, issued_at, sha256: newSha, size: bytes.length, min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), ...(nativeModules ? { required_native_modules: nativeModules } : {}), web }), "application/json");
201
237
  totalBytes += bytes.length;
202
238
  summary[pf] = { bytes: bytes.length, patches: patches.map((p) => ({ from: p.from_sha256.slice(0, 12), kb: +(p.size / 1024).toFixed(1) })) };
203
239
  if (web)
@@ -235,7 +271,7 @@ export function dryRunSummary(release, bytes, outDir, info = { bases: 0, bsdiff:
235
271
  lines.push(`Patches built: ${info.patches}.`);
236
272
  return lines.join("\n");
237
273
  }
238
- async function publishWeb(kv, cfg, APP, release) {
274
+ async function publishWeb(kv, cfg, APP, release, outDir) {
239
275
  execSync("PN_SIM=1 npx expo export --platform web --output-dir dist-web --clear", { stdio: "inherit" });
240
276
  const base = `w/${APP}/r${release}`;
241
277
  const files = [];
@@ -262,7 +298,9 @@ async function publishWeb(kv, cfg, APP, release) {
262
298
  idx.releases = [...new Set([...idx.releases, release])].sort((x, y) => x - y);
263
299
  idx.latest = Math.max(...idx.releases);
264
300
  await kv.put(idxKey, JSON.stringify(idx), "application/json");
265
- console.error(`web artifact: ${files.length} files at /${base}/`);
301
+ // Show where the files are on disk, relative to this folder: the URL path (/w/...) is not a folder.
302
+ const where = outDir ? (relative(process.cwd(), resolve(outDir, base)) || ".") + "/" : `/${base}/`;
303
+ console.error(`web artifact: ${files.length} files at ${where}`);
266
304
  // Store listing assets (icon, screenshots) and the release note, read by shipvo.dev/apps and list_apps.
267
305
  const L = cfg.listing ?? {};
268
306
  const store = `w/${APP}/store`;
@@ -287,16 +325,17 @@ async function publishWeb(kv, cfg, APP, release) {
287
325
  return web;
288
326
  }
289
327
  export async function rollback(argv) {
290
- checkFlags(argv, ["to", "bands", "platform"]);
291
- const a = parse(argv), cfg = loadConfig(), SEED = seed(), kv = kvFor(cfg);
328
+ checkFlags(argv, ["to", "bands", "platform", "dry-run"]);
329
+ const a = parse(argv), dry = a["dry-run"] !== undefined, cfg = loadConfig(), SEED = seed(), kv = kvFor(cfg);
292
330
  const APP = cfg.appId, MAJOR = cfg.shellMajor, to = Number(a.to);
293
331
  if (!to) {
294
- console.error("Usage: shipvo rollback --to <release> [--bands b0,b1,b2] [--platform android|ios|all]");
332
+ console.error("Usage: shipvo rollback --to <release> [--bands b0,b1,b2] [--platform android|ios|all] [--dry-run]");
295
333
  process.exit(2);
296
334
  }
297
335
  const release = (await currentRelease(kv, APP, MAJOR)) + 1, issued_at = new Date().toISOString();
298
336
  let minShell = "1.0.0";
299
- for (const pf of platformsOf(a)) {
337
+ const platforms = platformsOf(a), bands = (a.bands || "b0,b1,b2").split(",");
338
+ for (const pf of platforms) {
300
339
  const bytes = await kv.getBytes(`b/${APP}/${bundleName(to, pf)}`);
301
340
  if (!bytes) {
302
341
  console.error(`Release ${to} ${pf} bundle not found`);
@@ -309,11 +348,16 @@ export async function rollback(argv) {
309
348
  web = h.web ?? undefined;
310
349
  }
311
350
  catch { }
312
- for (const band of (a.bands || "b0,b1,b2").split(",")) {
351
+ for (const band of bands) {
313
352
  const body = JSON.stringify({ schema_version: "1.0", app_id: APP, shell_major: MAJOR, band, ...(pf === "ios" ? { platform: "ios" } : {}), release, issued_at,
314
353
  core: { version: `1.0.${release}`, sha256: sha256Hex(bytes), size: bytes.length, url: `b/${APP}/${bundleName(to, pf)}` }, addons: [], screens: [], min_shell_version: minShell, ...(web ? { web } : {}) });
315
- await kv.put(pointerKey(APP, MAJOR, band, pf), JSON.stringify({ body, signature: signBytes(new TextEncoder().encode(body), SEED) }), "application/json");
354
+ if (!dry)
355
+ await kv.put(pointerKey(APP, MAJOR, band, pf), JSON.stringify({ body, signature: signBytes(new TextEncoder().encode(body), SEED) }), "application/json");
316
356
  }
317
357
  }
358
+ if (dry) {
359
+ console.log([`Rollback preview for ${APP}:`, ` Rolling back to the code of release ${to}`, ` New release number: ${release}`, ` Platforms: ${platforms.join(", ")}`, ` Bands: ${bands.join(", ")}`, ` Min shell version: ${minShell}`, `Dry run: nothing was written.`].join("\n"));
360
+ return;
361
+ }
318
362
  console.log(JSON.stringify({ app: APP, rolled_back_to_code_of: to, new_release: release, min_shell_version: minShell, issued_at }));
319
363
  }
@@ -0,0 +1,26 @@
1
+ type Step = {
2
+ kind?: string;
3
+ target?: string;
4
+ text?: string;
5
+ enter?: boolean;
6
+ line?: number;
7
+ };
8
+ type Flow = {
9
+ name?: string;
10
+ status?: string;
11
+ steps?: Step[];
12
+ failedStep?: Step | null;
13
+ message?: string | null;
14
+ screenshots?: string[];
15
+ screenshot?: string | null;
16
+ };
17
+ export type TestReport = {
18
+ app?: string | null;
19
+ commit?: string | null;
20
+ release?: string | null;
21
+ time?: string | null;
22
+ parityLimits?: string[];
23
+ flows?: Flow[];
24
+ };
25
+ export declare function renderReportMarkdown(report: TestReport): string;
26
+ export {};
@@ -0,0 +1,58 @@
1
+ const one = (s) => s.replace(/\s+/g, " ").trim();
2
+ /** Inline code that survives backticks in the text. */
3
+ function code(s) {
4
+ const t = one(s);
5
+ const fence = "`".repeat(Math.max(0, ...[...t.matchAll(/`+/g)].map((m) => m[0].length)) + 1);
6
+ return `${fence}${t.startsWith("`") || t.endsWith("`") ? ` ${t} ` : t}${fence}`;
7
+ }
8
+ function stepText(s) {
9
+ const kind = s.kind ?? "step";
10
+ if (kind === "tap")
11
+ return `tap "${s.target ?? ""}"`;
12
+ if (kind === "type")
13
+ return `type "${s.text ?? ""}" into "${s.target ?? ""}"${s.enter ? " and press enter" : ""}`;
14
+ const arg = s.text ?? s.target;
15
+ return arg !== undefined ? `${kind} "${arg}"` : kind;
16
+ }
17
+ function block(s) {
18
+ const fence = "`".repeat(Math.max(2, ...[...s.matchAll(/`+/g)].map((m) => m[0].length)) + 1);
19
+ return `${fence}\n${s}\n${fence}`;
20
+ }
21
+ export function renderReportMarkdown(report) {
22
+ const flows = Array.isArray(report.flows) ? report.flows : [];
23
+ const failed = flows.filter((f) => f.status === "failed");
24
+ const out = [`# Shipvo test report: ${failed.length ? "failed" : "all flows passed"}`, ""];
25
+ const meta = [];
26
+ if (report.app)
27
+ meta.push(`- App: ${code(report.app)}`);
28
+ meta.push(`- Release: ${report.release ? code(report.release) : "not set"}`);
29
+ meta.push(`- Commit: ${report.commit ? code(report.commit) : "unknown"}`);
30
+ if (report.time)
31
+ meta.push(`- Time: ${report.time}`);
32
+ meta.push(`- Flows: ${flows.length - failed.length} passed, ${failed.length} failed`);
33
+ out.push(...meta, "");
34
+ if (!failed.length)
35
+ out.push(flows.length ? `All ${flows.length} flow${flows.length === 1 ? "" : "s"} passed.` : "No flows were run.", "");
36
+ for (const f of failed) {
37
+ out.push(`## Failing flow: ${code(f.name ?? "unnamed flow")}`, "");
38
+ const bad = f.failedStep;
39
+ if (bad)
40
+ out.push(`Failed step${bad.line ? ` (line ${bad.line})` : ""}: ${code(stepText(bad))}`, "");
41
+ if (f.message)
42
+ out.push("Error:", "", block(f.message), "");
43
+ const shots = [...(f.screenshots ?? []), ...(f.screenshot ? [f.screenshot] : [])];
44
+ if (shots.length)
45
+ out.push(`Screenshots: ${shots.map(code).join(", ")}`, "");
46
+ if (f.steps?.length) {
47
+ out.push("Steps:", "");
48
+ f.steps.forEach((s, i) => {
49
+ const mark = bad && s.line !== undefined && s.line === bad.line ? " ← failed" : "";
50
+ out.push(`${i + 1}. ${code(stepText(s))}${mark}`);
51
+ });
52
+ out.push("");
53
+ }
54
+ }
55
+ if (report.parityLimits?.length)
56
+ out.push("## Parity limits", "", ...report.parityLimits.map((l) => `- ${one(l)}`), "");
57
+ return out.join("\n");
58
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The shared screen rules (`@shipvo/sim`), as source that the page can run. Same copy the simulator uses.
3
+ * Only plain `export function` / `export const` lines are supported: `export default` and `export {…}` would not be stripped.
4
+ */
5
+ export declare function screenRulesSource(): string;
6
+ /** The script text served at `/__shipvo/bridge.js`. It reads the session token from its own `data-token` attribute and then removes the attribute. */
7
+ export declare function bridgeScript(rules?: string): string;