@shipvo/cli 0.1.4 → 0.1.6
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.
- package/README.md +3 -0
- package/dist/adopt.js +1 -2
- package/dist/agents.d.ts +29 -0
- package/dist/agents.js +183 -0
- package/dist/approvals.d.ts +9 -0
- package/dist/approvals.js +106 -0
- package/dist/audit.d.ts +13 -0
- package/dist/audit.js +92 -0
- package/dist/autohalt.d.ts +1 -1
- package/dist/autohalt.js +28 -16
- package/dist/bsdiff.d.ts +4 -0
- package/dist/bsdiff.js +465 -0
- package/dist/ci-comment.d.ts +38 -0
- package/dist/ci-comment.js +169 -0
- package/dist/ci-compare.d.ts +10 -0
- package/dist/ci-compare.js +25 -0
- package/dist/ci-shots.d.ts +9 -0
- package/dist/ci-shots.js +81 -0
- package/dist/ci.d.ts +11 -0
- package/dist/ci.js +100 -0
- package/dist/cli.js +51 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +68 -13
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +88 -8
- package/dist/eas.d.ts +29 -0
- package/dist/eas.js +258 -0
- package/dist/ensure-playwright.d.ts +25 -0
- package/dist/ensure-playwright.js +69 -0
- package/dist/explore-issues.d.ts +12 -0
- package/dist/explore-issues.js +50 -0
- package/dist/explore.d.ts +160 -0
- package/dist/explore.js +496 -0
- package/dist/flows.d.ts +31 -0
- package/dist/flows.js +264 -0
- package/dist/help.d.ts +2 -2
- package/dist/help.js +37 -7
- package/dist/hermes.d.ts +10 -0
- package/dist/hermes.js +50 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +71 -19
- package/dist/json-out.d.ts +53 -0
- package/dist/json-out.js +91 -0
- package/dist/keys-json.d.ts +1 -0
- package/dist/keys-json.js +71 -0
- package/dist/login.js +27 -11
- package/dist/lumi.d.ts +104 -0
- package/dist/lumi.js +364 -0
- package/dist/migrate.d.ts +27 -0
- package/dist/migrate.js +521 -0
- package/dist/policy.d.ts +2 -16
- package/dist/policy.js +2 -18
- package/dist/preview-recorder.d.ts +1 -0
- package/dist/preview-recorder.js +62 -0
- package/dist/preview.d.ts +4 -3
- package/dist/preview.js +114 -15
- package/dist/promote.d.ts +1 -1
- package/dist/promote.js +63 -14
- package/dist/publish.d.ts +14 -2
- package/dist/publish.js +131 -56
- package/dist/qr.d.ts +9 -0
- package/dist/qr.js +213 -0
- package/dist/report-md.d.ts +27 -0
- package/dist/report-md.js +58 -0
- package/dist/rotation.d.ts +57 -0
- package/dist/rotation.js +157 -0
- package/dist/run-bridge.d.ts +7 -0
- package/dist/run-bridge.js +728 -0
- package/dist/run-net.d.ts +39 -0
- package/dist/run-net.js +266 -0
- package/dist/run-protocol.d.ts +32 -0
- package/dist/run-protocol.js +22 -0
- package/dist/run-ws.d.ts +8 -0
- package/dist/run-ws.js +89 -0
- package/dist/run.d.ts +109 -0
- package/dist/run.js +385 -0
- package/dist/signing.js +13 -0
- package/dist/sim.d.ts +4 -0
- package/dist/sim.js +25 -7
- package/dist/smoke.d.ts +28 -0
- package/dist/smoke.js +117 -0
- package/dist/spawn-bin.d.ts +11 -0
- package/dist/spawn-bin.js +21 -0
- package/dist/status.d.ts +71 -1
- package/dist/status.js +162 -5
- package/dist/store.d.ts +36 -0
- package/dist/store.js +266 -3
- package/dist/templates/capacitor/github-actions/build.yml +122 -0
- package/dist/templates/github-action/shipvo-check.yml +18 -0
- package/dist/test.d.ts +46 -0
- package/dist/test.js +253 -0
- package/package.json +7 -4
- package/skills/shipvo-cli/SKILL.md +31 -0
package/dist/publish.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// `shipvo publish`: build the Hermes bundle, sign a release pointer, publish bundle + patches + the agent-facing web
|
|
2
2
|
// artifact to the control plane. `shipvo rollback --to N`: republish an earlier bundle as a new release.
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
-
import { existsSync, mkdirSync,
|
|
5
|
-
import {
|
|
6
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path, { dirname, join, relative, resolve } from "node:path";
|
|
7
6
|
import { sha256Hex, signBytes } from "./index.js";
|
|
8
|
-
import {
|
|
7
|
+
import { bsdiff } from "./bsdiff.js";
|
|
8
|
+
import { checkBundle, checkEngine, installedHermesEngine, parseDeclared, parseEngine } from "./hermes.js";
|
|
9
9
|
import { requiredNativeModules } from "./native.js";
|
|
10
10
|
import { checkPublishPolicy } from "./policy.js";
|
|
11
|
-
import {
|
|
11
|
+
import { CliFailure, UsageError, runJson } from "./json-out.js";
|
|
12
|
+
import { args as parse, hasStore, kv as kvFor, loadConfig, offlineKv, seed } from "./config.js";
|
|
12
13
|
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" };
|
|
13
14
|
const walk = (d) => readdirSync(d).flatMap((f) => (statSync(join(d, f)).isDirectory() ? walk(join(d, f)) : [join(d, f)]));
|
|
14
15
|
/** Release numbers are shared across platforms, so one number means one commit everywhere. */
|
|
@@ -34,7 +35,7 @@ const platformsOf = (a) => {
|
|
|
34
35
|
return ["android", "ios"];
|
|
35
36
|
if (p === "android" || p === "ios")
|
|
36
37
|
return [p];
|
|
37
|
-
throw new
|
|
38
|
+
throw new UsageError(`--platform must be android, ios or all (got "${a.platform}")`);
|
|
38
39
|
};
|
|
39
40
|
/** Unknown flags are errors: a typo like --dryrun must not fall through to a real publish. */
|
|
40
41
|
function checkFlags(argv, known) {
|
|
@@ -42,7 +43,7 @@ function checkFlags(argv, known) {
|
|
|
42
43
|
if (bad.length) {
|
|
43
44
|
const eq = bad.find((x) => x.includes("="));
|
|
44
45
|
const hint = eq ? ` Write "${eq.replace("=", " ")}" (a space, not =).` : "";
|
|
45
|
-
throw new
|
|
46
|
+
throw new UsageError(`Unknown flag ${bad.join(", ")}.${hint} Known flags: ${known.map((k) => "--" + k).join(" ")}`);
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
/** An on/off flag: bare or "true" is on, "false" is off. Anything else is an error, so a typo never flips it silently. */
|
|
@@ -52,7 +53,12 @@ function switchFlag(a, name) {
|
|
|
52
53
|
return false;
|
|
53
54
|
if (v === "true")
|
|
54
55
|
return true;
|
|
55
|
-
throw new
|
|
56
|
+
throw new UsageError(`--${name} is an on/off flag: use --${name}, --${name} true or --${name} false (got "${v}")`);
|
|
57
|
+
}
|
|
58
|
+
/** True when `file` is strictly inside `root`. Uses the platform's separator, so it works with Windows paths too. */
|
|
59
|
+
export function isInside(root, file, p = path) {
|
|
60
|
+
const rel = p.relative(root, file);
|
|
61
|
+
return rel !== "" && !rel.startsWith("..") && !p.isAbsolute(rel);
|
|
56
62
|
}
|
|
57
63
|
/** 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. */
|
|
58
64
|
function localKv(dir) {
|
|
@@ -65,7 +71,7 @@ function localKv(dir) {
|
|
|
65
71
|
async keys(prefix) { return [...mem.keys()].filter((k) => k.startsWith(prefix)); },
|
|
66
72
|
async put(k, body, _type) {
|
|
67
73
|
const f = resolve(root, k);
|
|
68
|
-
if (!
|
|
74
|
+
if (!isInside(root, f))
|
|
69
75
|
throw new Error(`refusing to write outside ${dir}: ${k}`);
|
|
70
76
|
const bytes = typeof body === "string" ? enc.encode(body) : new Uint8Array(body);
|
|
71
77
|
mkdirSync(dirname(f), { recursive: true });
|
|
@@ -74,7 +80,7 @@ function localKv(dir) {
|
|
|
74
80
|
},
|
|
75
81
|
};
|
|
76
82
|
}
|
|
77
|
-
const PUBLISH_FLAGS = ["bands", "platform", "release", "min-shell", "hermes-bytecode-version", "max-bases", "skip-export", "skip-web", "dry-run", "out"];
|
|
83
|
+
const PUBLISH_FLAGS = ["bands", "platform", "release", "min-shell", "hermes-bytecode-version", "max-bases", "skip-export", "skip-web", "dry-run", "dry-run-policy", "out", "json"];
|
|
78
84
|
export const PUBLISH_HELP = `Usage: shipvo publish [flags]
|
|
79
85
|
|
|
80
86
|
Build, sign and publish an over-the-air release.
|
|
@@ -89,28 +95,48 @@ Flags (write a value after a space, not =; on/off flags can be bare, or true/fal
|
|
|
89
95
|
--skip-export Reuse the last build in dist-ota/
|
|
90
96
|
--skip-web Do not publish the web copy for the simulator
|
|
91
97
|
--dry-run Build and sign, write files locally, send nothing
|
|
98
|
+
--dry-run-policy Check the policy in shipvo.json for these --bands, print the result, publish nothing
|
|
92
99
|
--out <dir> Where --dry-run writes files (default .shipvo/dry-run)
|
|
100
|
+
--json Print one JSON envelope on stdout; text and build output go to stderr
|
|
93
101
|
--help Show this help
|
|
94
102
|
`;
|
|
103
|
+
const DOCTOR = [{ command: "shipvo doctor --json", why: "Check your setup." }];
|
|
95
104
|
export async function publish(argv) {
|
|
96
105
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
97
106
|
console.log(PUBLISH_HELP);
|
|
98
107
|
return;
|
|
99
108
|
}
|
|
109
|
+
if (argv.includes("--json"))
|
|
110
|
+
return runJson(() => publishRun(argv, true), DOCTOR);
|
|
111
|
+
await publishRun(argv, false);
|
|
112
|
+
}
|
|
113
|
+
async function publishRun(argv, json) {
|
|
114
|
+
// In --json mode the build tools must not write to stdout either.
|
|
115
|
+
const io = json ? ["inherit", 2, 2] : "inherit";
|
|
100
116
|
checkFlags(argv, PUBLISH_FLAGS);
|
|
101
117
|
const a = parse(argv), cfg = loadConfig(), SEED = seed();
|
|
118
|
+
if (switchFlag(a, "dry-run-policy")) {
|
|
119
|
+
const verdict = checkPublishPolicy(cfg.policy, (a.bands || "b0,b1,b2").split(","));
|
|
120
|
+
if (verdict.ok) {
|
|
121
|
+
const msg = cfg.policy === undefined ? "No policy in shipvo.json: nothing to check." : "Policy allows this publish.";
|
|
122
|
+
console.log(msg);
|
|
123
|
+
return { code: "policy_ok", summary: msg, data: { policy: cfg.policy === undefined ? "none" : "allowed", bands: (a.bands || "b0,b1,b2").split(",") }, next: [{ command: "shipvo publish --dry-run --json", why: "Build and sign without sending." }] };
|
|
124
|
+
}
|
|
125
|
+
return refused(verdict, json);
|
|
126
|
+
}
|
|
102
127
|
const dry = switchFlag(a, "dry-run"), skipExport = switchFlag(a, "skip-export"), skipWeb = switchFlag(a, "skip-web");
|
|
103
128
|
if (a.out !== undefined && !dry)
|
|
104
|
-
throw new
|
|
129
|
+
throw new UsageError("--out only works with --dry-run");
|
|
105
130
|
if (a.out === "true")
|
|
106
|
-
throw new
|
|
131
|
+
throw new UsageError("--out needs a directory: pass --out <dir>");
|
|
107
132
|
const outDir = a.out || ".shipvo/dry-run";
|
|
108
133
|
const kv = dry ? localKv(outDir) : kvFor(cfg);
|
|
109
134
|
const APP = cfg.appId, MAJOR = cfg.shellMajor;
|
|
110
135
|
const MIN_SHELL = a["min-shell"] || JSON.parse(readFileSync("app.json", "utf8")).expo.version; // a bundle needs the shell it was built against
|
|
111
136
|
if (a["hermes-bytecode-version"] === "true")
|
|
112
|
-
throw new
|
|
137
|
+
throw new UsageError("--hermes-bytecode-version needs a value: pass a value like 96 (or 96,98)");
|
|
113
138
|
const declaredHermes = parseDeclared(a["hermes-bytecode-version"] ?? cfg.hermesBytecodeVersion);
|
|
139
|
+
const hermesEngine = checkEngine(installedHermesEngine(process.cwd()), parseEngine(cfg.hermesEngine)); // fails before anything is uploaded
|
|
114
140
|
const skippedNative = [];
|
|
115
141
|
const nativeModules = requiredNativeModules(".", skippedNative);
|
|
116
142
|
if (skippedNative.length)
|
|
@@ -120,11 +146,8 @@ export async function publish(argv) {
|
|
|
120
146
|
const bands = (a.bands || "b0,b1,b2").split(",");
|
|
121
147
|
if (dry) {
|
|
122
148
|
const verdict = checkPublishPolicy(cfg.policy, bands);
|
|
123
|
-
if (!verdict.ok)
|
|
124
|
-
|
|
125
|
-
process.exitCode = 1;
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
149
|
+
if (!verdict.ok)
|
|
150
|
+
return refused(verdict, json);
|
|
128
151
|
}
|
|
129
152
|
const platforms = platformsOf(a);
|
|
130
153
|
const release = Number(a.release || 0) || (await currentRelease(kv, APP, MAJOR)) + 1;
|
|
@@ -132,16 +155,16 @@ export async function publish(argv) {
|
|
|
132
155
|
if (dry)
|
|
133
156
|
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)"}.`);
|
|
134
157
|
let totalBytes = 0;
|
|
135
|
-
const patchInfo = { bases: 0,
|
|
158
|
+
const patchInfo = { bases: 0, patches: 0 };
|
|
136
159
|
for (const pf of platforms) {
|
|
137
160
|
// Android published alone keeps the original dist-ota/ layout, so existing CI keeps working.
|
|
138
161
|
const root = platforms.length === 1 && pf === "android" ? "dist-ota" : `dist-ota/${pf}`;
|
|
139
162
|
if (!skipExport)
|
|
140
|
-
execSync(`npx expo export --platform ${pf} --output-dir ${root} --clear`, { stdio:
|
|
163
|
+
execSync(`npx expo export --platform ${pf} --output-dir ${root} --clear`, { stdio: io });
|
|
141
164
|
const jsDir = `${root}/_expo/static/js/${pf}`;
|
|
142
165
|
const file = readdirSync(jsDir).find((f) => f.endsWith(".hbc")) || readdirSync(jsDir).find((f) => f.endsWith(".js"));
|
|
143
166
|
if (!file)
|
|
144
|
-
throw new
|
|
167
|
+
throw new CliFailure("no bundle in " + jsDir, "no_bundle", 1, [{ command: "shipvo publish --json", why: "Run again without --skip-export so the bundle is built." }]);
|
|
145
168
|
const bytes = new Uint8Array(readFileSync(join(jsDir, file)));
|
|
146
169
|
const hermesVersion = checkBundle(file, bytes, declaredHermes); // fails before anything is uploaded
|
|
147
170
|
const name = bundleName(release, pf), newSha = sha256Hex(bytes);
|
|
@@ -169,29 +192,23 @@ export async function publish(argv) {
|
|
|
169
192
|
}
|
|
170
193
|
catch { }
|
|
171
194
|
}
|
|
172
|
-
//
|
|
173
|
-
|
|
195
|
+
// Memory grows with bundle size (plain arrays); only a thrown error falls back to the full bundle.
|
|
196
|
+
// Patches are built in-process (no Python or other tool); a first release has no older bundle to patch from.
|
|
174
197
|
if (bases.size > 0) {
|
|
175
|
-
try {
|
|
176
|
-
execSync("python3 -c 'import bsdiff4'", { stdio: "ignore" });
|
|
177
|
-
bsdiff = true;
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
console.error("Small patches are off because the Python tool bsdiff4 is not installed (pip install bsdiff4). Full bundles are published instead. Updates still work, they are just bigger.");
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (bsdiff) {
|
|
184
|
-
const work = mkdtempSync(join(tmpdir(), "shipvo-"));
|
|
185
|
-
writeFileSync(join(work, "new.hbc"), bytes);
|
|
186
198
|
for (const [fromSha, key] of bases) {
|
|
187
199
|
if (fromSha === newSha)
|
|
188
200
|
continue;
|
|
189
201
|
const base = await kv.getBytes(key);
|
|
190
202
|
if (!base)
|
|
191
203
|
continue;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
let patch;
|
|
205
|
+
try {
|
|
206
|
+
patch = bsdiff(new Uint8Array(base), bytes);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
console.error(`Could not build a small patch from release ${fromSha.slice(0, 8)}; publishing the full bundle`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
195
212
|
if (patch.length >= bytes.length * 0.6)
|
|
196
213
|
continue;
|
|
197
214
|
const pkey = `b/${APP}/${pf === "ios" ? "ios-" : ""}d-${fromSha.slice(0, 16)}-r${release}.bsdiff`;
|
|
@@ -200,14 +217,13 @@ export async function publish(argv) {
|
|
|
200
217
|
}
|
|
201
218
|
}
|
|
202
219
|
patchInfo.bases = bases.size;
|
|
203
|
-
patchInfo.bsdiff = bsdiff;
|
|
204
220
|
patchInfo.patches = patches.length;
|
|
205
221
|
console.error(`patches: ${patches.map((p) => `${p.from_sha256.slice(0, 8)}=${(p.size / 1024).toFixed(1)}KB`).join(", ") || "none"}`);
|
|
206
222
|
}
|
|
207
223
|
// The agent-facing web artifact of the same commit, served at /w/<app>/r<N>/ for the simulator (built once, with the first platform).
|
|
208
224
|
let web = null;
|
|
209
225
|
if (!skipWeb && pf === platforms[0]) {
|
|
210
|
-
web = await publishWeb(kv, cfg, APP, release, dry ? outDir : undefined);
|
|
226
|
+
web = await publishWeb(kv, cfg, APP, release, dry ? outDir : undefined, io);
|
|
211
227
|
}
|
|
212
228
|
else if (!skipWeb) {
|
|
213
229
|
try {
|
|
@@ -219,10 +235,10 @@ export async function publish(argv) {
|
|
|
219
235
|
for (const band of bands) {
|
|
220
236
|
const body = JSON.stringify({ schema_version: "1.0", app_id: APP, shell_major: MAJOR, band, ...(pf === "ios" ? { platform: "ios" } : {}), release, issued_at,
|
|
221
237
|
core: { version: `1.0.${release}`, sha256: newSha, size: bytes.length, url: `b/${APP}/${name}`, patches },
|
|
222
|
-
addons: [], screens: [], min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), ...(nativeModules ? { required_native_modules: nativeModules } : {}), ...(web ? { web } : {}) });
|
|
238
|
+
addons: [], screens: [], min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), ...(hermesVersion && hermesEngine ? { hermes_engine: hermesEngine } : {}), ...(nativeModules ? { required_native_modules: nativeModules } : {}), ...(web ? { web } : {}) });
|
|
223
239
|
await kv.put(pointerKey(APP, MAJOR, band, pf), JSON.stringify({ body, signature: signBytes(new TextEncoder().encode(body), SEED) }), "application/json");
|
|
224
240
|
}
|
|
225
|
-
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");
|
|
241
|
+
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 } : {}), ...(hermesVersion && hermesEngine ? { hermes_engine: hermesEngine } : {}), ...(nativeModules ? { required_native_modules: nativeModules } : {}), web }), "application/json");
|
|
226
242
|
totalBytes += bytes.length;
|
|
227
243
|
summary[pf] = { bytes: bytes.length, patches: patches.map((p) => ({ from: p.from_sha256.slice(0, 12), kb: +(p.size / 1024).toFixed(1) })) };
|
|
228
244
|
if (web)
|
|
@@ -236,14 +252,41 @@ export async function publish(argv) {
|
|
|
236
252
|
if (dry)
|
|
237
253
|
console.log(dryRunSummary(release, totalBytes, outDir, patchInfo));
|
|
238
254
|
else
|
|
239
|
-
console.log(
|
|
255
|
+
console.log(publishedSummary(release, bands));
|
|
256
|
+
const flags = [a.bands ? `--bands ${a.bands}` : "", a.platform ? `--platform ${a.platform}` : ""].filter(Boolean).join(" ");
|
|
257
|
+
const data = { ...summary, platforms, total_bytes: totalBytes };
|
|
258
|
+
if (dry)
|
|
259
|
+
return { code: "dry_run", summary: `Dry run complete: release ${release}, ${formatSize(totalBytes)}. Nothing was sent.`, data, next: [{ command: `shipvo publish ${flags} --json`.replace(/ +/g, " "), why: "Publish for real." }] };
|
|
260
|
+
const rest = ["b0", "b1", "b2"].filter((b) => !bands.includes(b));
|
|
261
|
+
const next = [{ command: "shipvo status --json", why: "See how the release does on phones." }];
|
|
262
|
+
if (rest.length)
|
|
263
|
+
next.push({ command: `shipvo promote --to ${rest.join(",")} --json`, why: `Widen release ${release} to ${rest.join(", ")} when it looks healthy.` });
|
|
264
|
+
return { code: "published", summary: `Published release ${release} to ${bands.join(", ")} (${platforms.join(", ")}).`, data, next };
|
|
265
|
+
}
|
|
266
|
+
function refused(verdict, json) {
|
|
267
|
+
const msg = `Refused by policy rule ${verdict.rule}: ${verdict.reason}`;
|
|
268
|
+
if (json)
|
|
269
|
+
throw new CliFailure(msg, "policy_refused", 1, [{ command: "shipvo publish --dry-run-policy --json", why: "Check the policy for other --bands." }]);
|
|
270
|
+
console.error(msg);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
return { code: "policy_refused", summary: msg, data: {}, next: [] };
|
|
273
|
+
}
|
|
274
|
+
/** The publish result in plain words. `--json` carries the full detail. */
|
|
275
|
+
export function publishedSummary(release, bands) {
|
|
276
|
+
const rest = ["b0", "b1", "b2"].filter((b) => !bands.includes(b));
|
|
277
|
+
const next = rest.length ? `Next: shipvo status, then shipvo promote --to ${rest.join(",")}.` : "Next: shipvo status.";
|
|
278
|
+
return `Published release ${release} to ${bands.join(", ")}. ${next}`;
|
|
279
|
+
}
|
|
280
|
+
/** The rollback result in plain words. */
|
|
281
|
+
export function rolledBackSummary(to, release) {
|
|
282
|
+
return `Rolled back to the code of release ${to} as new release ${release}. Next: shipvo status.`;
|
|
240
283
|
}
|
|
241
284
|
/** Bytes in plain units: KB below 1 MB, so a small bundle never reads as 0.00 MB. */
|
|
242
285
|
export function formatSize(bytes) {
|
|
243
286
|
return bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(2)} MB`;
|
|
244
287
|
}
|
|
245
288
|
/** The dry-run result in plain words. `info` says what happened with patches on Android. */
|
|
246
|
-
export function dryRunSummary(release, bytes, outDir, info = { bases: 0,
|
|
289
|
+
export function dryRunSummary(release, bytes, outDir, info = { bases: 0, patches: 0 }) {
|
|
247
290
|
const lines = [
|
|
248
291
|
`Dry run complete. Nothing was sent.`,
|
|
249
292
|
` Release: ${release}`,
|
|
@@ -254,14 +297,32 @@ export function dryRunSummary(release, bytes, outDir, info = { bases: 0, bsdiff:
|
|
|
254
297
|
lines.push(`Patches are not built in a dry run, because older releases are not read. Publish for real to build them.`);
|
|
255
298
|
else if (info.bases === 0)
|
|
256
299
|
lines.push(`Full bundles are fine for a first release. Small patches matter once you have older releases to compare against.`);
|
|
257
|
-
else if (
|
|
258
|
-
lines.push(`
|
|
300
|
+
else if (info.patches === 0)
|
|
301
|
+
lines.push(`No small patch was built, so this update is a full bundle. Full bundles still work.`);
|
|
259
302
|
else
|
|
260
303
|
lines.push(`Patches built: ${info.patches}.`);
|
|
261
304
|
return lines.join("\n");
|
|
262
305
|
}
|
|
263
|
-
|
|
264
|
-
|
|
306
|
+
/** The line for a listing file that is not there. It says the file is optional and what to do. */
|
|
307
|
+
export function missingListingNote(file, name) {
|
|
308
|
+
if (name === "icon")
|
|
309
|
+
return `No app icon yet (optional). Add ${file} to show one in the store.`;
|
|
310
|
+
if (name.startsWith("shot-"))
|
|
311
|
+
return `No screenshot yet (optional). Add ${file} to show it in the store.`;
|
|
312
|
+
return `No ${name} yet (optional). Add ${file} to show it in the store.`;
|
|
313
|
+
}
|
|
314
|
+
/** Upload one listing file to the store prefix. A missing file prints a note and returns null. */
|
|
315
|
+
export async function uploadListingFile(kv, store, file, name) {
|
|
316
|
+
if (!existsSync(file)) {
|
|
317
|
+
console.error(missingListingNote(file, name));
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const ext = file.slice(file.lastIndexOf(".")).toLowerCase();
|
|
321
|
+
await kv.put(`${store}/${name}${ext}`, new Uint8Array(readFileSync(file)), MIME[ext] ?? "application/octet-stream");
|
|
322
|
+
return `${name}${ext}`;
|
|
323
|
+
}
|
|
324
|
+
async function publishWeb(kv, cfg, APP, release, outDir, io = "inherit") {
|
|
325
|
+
execSync("npx expo export --platform web --output-dir dist-web --clear", { stdio: io, env: { ...process.env, PN_SIM: "1" } });
|
|
265
326
|
const base = `w/${APP}/r${release}`;
|
|
266
327
|
const files = [];
|
|
267
328
|
let indexSha = "";
|
|
@@ -293,10 +354,7 @@ async function publishWeb(kv, cfg, APP, release, outDir) {
|
|
|
293
354
|
// Store listing assets (icon, screenshots) and the release note, read by shipvo.dev/apps and list_apps.
|
|
294
355
|
const L = cfg.listing ?? {};
|
|
295
356
|
const store = `w/${APP}/store`;
|
|
296
|
-
const up =
|
|
297
|
-
console.error(`listing: ${file} not found, skipped`);
|
|
298
|
-
return null;
|
|
299
|
-
} const ext = file.slice(file.lastIndexOf(".")).toLowerCase(); await kv.put(`${store}/${name}${ext}`, new Uint8Array(readFileSync(file)), MIME[ext] ?? "application/octet-stream"); return `${name}${ext}`; };
|
|
357
|
+
const up = (file, name) => uploadListingFile(kv, store, file, name);
|
|
300
358
|
const icon = L.icon ? await up(L.icon, "icon") : null;
|
|
301
359
|
const shots = [];
|
|
302
360
|
for (const [i, f] of (L.screenshots ?? []).entries()) {
|
|
@@ -314,11 +372,23 @@ async function publishWeb(kv, cfg, APP, release, outDir) {
|
|
|
314
372
|
return web;
|
|
315
373
|
}
|
|
316
374
|
export async function rollback(argv) {
|
|
317
|
-
|
|
318
|
-
|
|
375
|
+
if (argv.includes("--json"))
|
|
376
|
+
return runJson(() => rollbackRun(argv, true), [...DOCTOR, { command: "shipvo status --json", why: "See the newest releases." }]);
|
|
377
|
+
await rollbackRun(argv, false);
|
|
378
|
+
}
|
|
379
|
+
async function rollbackRun(argv, json) {
|
|
380
|
+
checkFlags(argv, ["to", "bands", "platform", "dry-run", "json"]);
|
|
381
|
+
const a = parse(argv), dry = a["dry-run"] !== undefined, cfg = loadConfig();
|
|
382
|
+
const offline = dry && !hasStore();
|
|
383
|
+
const SEED = dry ? "" : seed(), kv = offline ? offlineKv() : kvFor(cfg);
|
|
384
|
+
if (offline)
|
|
385
|
+
console.error("Offline rehearsal: reading releases from .shipvo/dry-run (written by shipvo publish --dry-run). No login used, nothing is sent.");
|
|
319
386
|
const APP = cfg.appId, MAJOR = cfg.shellMajor, to = Number(a.to);
|
|
320
387
|
if (!to) {
|
|
321
|
-
|
|
388
|
+
const usage = "Usage: shipvo rollback --to <release> [--bands b0,b1,b2] [--platform android|ios|all] [--dry-run] [--json]";
|
|
389
|
+
if (json)
|
|
390
|
+
throw new UsageError(usage);
|
|
391
|
+
console.error(usage);
|
|
322
392
|
process.exit(2);
|
|
323
393
|
}
|
|
324
394
|
const release = (await currentRelease(kv, APP, MAJOR)) + 1, issued_at = new Date().toISOString();
|
|
@@ -327,6 +397,8 @@ export async function rollback(argv) {
|
|
|
327
397
|
for (const pf of platforms) {
|
|
328
398
|
const bytes = await kv.getBytes(`b/${APP}/${bundleName(to, pf)}`);
|
|
329
399
|
if (!bytes) {
|
|
400
|
+
if (json)
|
|
401
|
+
throw new CliFailure(`Release ${to} ${pf} bundle not found`, "not_found", 1, [{ command: "shipvo status --json", why: "See which releases exist." }]);
|
|
330
402
|
console.error(`Release ${to} ${pf} bundle not found`);
|
|
331
403
|
process.exit(1);
|
|
332
404
|
}
|
|
@@ -344,9 +416,12 @@ export async function rollback(argv) {
|
|
|
344
416
|
await kv.put(pointerKey(APP, MAJOR, band, pf), JSON.stringify({ body, signature: signBytes(new TextEncoder().encode(body), SEED) }), "application/json");
|
|
345
417
|
}
|
|
346
418
|
}
|
|
419
|
+
const data = { app: APP, rolled_back_to_code_of: to, new_release: release, min_shell_version: minShell, issued_at, platforms, bands, dry_run: dry };
|
|
347
420
|
if (dry) {
|
|
348
421
|
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"));
|
|
349
|
-
|
|
422
|
+
const real = argv.filter((x) => x !== "--dry-run").join(" ");
|
|
423
|
+
return { code: "dry_run", summary: `Rollback preview: release ${to} code becomes release ${release}. Nothing was written.`, data, next: [{ command: `shipvo rollback ${real}`, why: "Do the rollback." }] };
|
|
350
424
|
}
|
|
351
|
-
console.log(
|
|
425
|
+
console.log(rolledBackSummary(to, release));
|
|
426
|
+
return { code: "rolled_back", summary: `Rolled back to the code of release ${to} as release ${release}.`, data, next: [{ command: "shipvo status --json", why: "Check that phones pick up the rollback." }] };
|
|
352
427
|
}
|
package/dist/qr.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const QR_MAX_BYTES = 134;
|
|
2
|
+
/** Reed-Solomon error codewords for `data`. */
|
|
3
|
+
export declare function rsRemainder(data: number[], degree: number): number[];
|
|
4
|
+
/** The 15 format bits for level L and a mask number. */
|
|
5
|
+
export declare function formatBits(mask: number): number;
|
|
6
|
+
/** Encodes text as a QR code. Returns rows of modules, true for dark. Throws when the text is over 134 bytes. */
|
|
7
|
+
export declare function qrMatrix(text: string): boolean[][];
|
|
8
|
+
/** Draws the matrix for a terminal: black on white with ANSI colours, two rows per line, a 2 module quiet zone. */
|
|
9
|
+
export declare function qrToTerminal(matrix: boolean[][], quiet?: number): string;
|
package/dist/qr.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// A small QR code encoder: byte mode, error correction level L, versions 1 to 6 (up to 134 bytes). No dependencies.
|
|
2
|
+
// Enough for a LAN URL. `qrMatrix` returns the modules; `qrToTerminal` draws them with half-block characters.
|
|
3
|
+
const SIZES = [
|
|
4
|
+
[19, 7, 1], [34, 10, 1], [55, 15, 1], [80, 20, 1], [108, 26, 1], [136, 18, 2],
|
|
5
|
+
];
|
|
6
|
+
const ALIGN = [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34]];
|
|
7
|
+
export const QR_MAX_BYTES = 134;
|
|
8
|
+
const EXP = new Uint8Array(512);
|
|
9
|
+
const LOG = new Uint8Array(256);
|
|
10
|
+
for (let i = 0, x = 1; i < 255; i++) {
|
|
11
|
+
EXP[i] = x;
|
|
12
|
+
LOG[x] = i;
|
|
13
|
+
x <<= 1;
|
|
14
|
+
if (x & 256)
|
|
15
|
+
x ^= 0x11d;
|
|
16
|
+
}
|
|
17
|
+
for (let i = 255; i < 512; i++)
|
|
18
|
+
EXP[i] = EXP[i - 255];
|
|
19
|
+
const mul = (a, b) => (a && b ? EXP[LOG[a] + LOG[b]] : 0);
|
|
20
|
+
/** Reed-Solomon error codewords for `data`. */
|
|
21
|
+
export function rsRemainder(data, degree) {
|
|
22
|
+
let gen = [1];
|
|
23
|
+
for (let i = 0; i < degree; i++) {
|
|
24
|
+
const next = new Array(gen.length + 1).fill(0);
|
|
25
|
+
gen.forEach((c, j) => { next[j] ^= c; next[j + 1] ^= mul(c, EXP[i]); });
|
|
26
|
+
gen = next;
|
|
27
|
+
}
|
|
28
|
+
const rem = new Array(degree).fill(0);
|
|
29
|
+
for (const b of data) {
|
|
30
|
+
const f = b ^ rem.shift();
|
|
31
|
+
rem.push(0);
|
|
32
|
+
for (let i = 0; i < degree; i++)
|
|
33
|
+
rem[i] ^= mul(gen[i + 1], f);
|
|
34
|
+
}
|
|
35
|
+
return rem;
|
|
36
|
+
}
|
|
37
|
+
function codewordsFor(bytes, version) {
|
|
38
|
+
const [total, ecc, blocks] = SIZES[version - 1];
|
|
39
|
+
const bits = [];
|
|
40
|
+
const put = (v, n) => { for (let i = n - 1; i >= 0; i--)
|
|
41
|
+
bits.push((v >>> i) & 1); };
|
|
42
|
+
put(4, 4);
|
|
43
|
+
put(bytes.length, 8);
|
|
44
|
+
for (const b of bytes)
|
|
45
|
+
put(b, 8);
|
|
46
|
+
put(0, Math.min(4, total * 8 - bits.length));
|
|
47
|
+
while (bits.length % 8)
|
|
48
|
+
bits.push(0);
|
|
49
|
+
const data = [];
|
|
50
|
+
for (let i = 0; i < bits.length; i += 8)
|
|
51
|
+
data.push(parseInt(bits.slice(i, i + 8).join(""), 2));
|
|
52
|
+
for (let pad = 0xec; data.length < total; pad ^= 0xec ^ 0x11)
|
|
53
|
+
data.push(pad);
|
|
54
|
+
const per = total / blocks;
|
|
55
|
+
const dataBlocks = Array.from({ length: blocks }, (_, i) => data.slice(i * per, (i + 1) * per));
|
|
56
|
+
const eccBlocks = dataBlocks.map((b) => rsRemainder(b, ecc));
|
|
57
|
+
const out = [];
|
|
58
|
+
for (let i = 0; i < per; i++)
|
|
59
|
+
for (const b of dataBlocks)
|
|
60
|
+
out.push(b[i]);
|
|
61
|
+
for (let i = 0; i < ecc; i++)
|
|
62
|
+
for (const b of eccBlocks)
|
|
63
|
+
out.push(b[i]);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
const MASKS = [
|
|
67
|
+
(x, y) => (x + y) % 2 === 0, (_x, y) => y % 2 === 0, (x) => x % 3 === 0, (x, y) => (x + y) % 3 === 0,
|
|
68
|
+
(x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0, (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
69
|
+
(x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0, (x, y) => ((((x + y) % 2) + ((x * y) % 3)) % 2) === 0,
|
|
70
|
+
];
|
|
71
|
+
/** The 15 format bits for level L and a mask number. */
|
|
72
|
+
export function formatBits(mask) {
|
|
73
|
+
const data = (1 << 3) | mask; // level L is 01
|
|
74
|
+
let rem = data;
|
|
75
|
+
for (let i = 0; i < 10; i++)
|
|
76
|
+
rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
77
|
+
return ((data << 10) | rem) ^ 0x5412;
|
|
78
|
+
}
|
|
79
|
+
function penalty(m) {
|
|
80
|
+
const n = m.length;
|
|
81
|
+
let score = 0;
|
|
82
|
+
let dark = 0;
|
|
83
|
+
const line = (get) => {
|
|
84
|
+
let run = 1;
|
|
85
|
+
for (let i = 1; i <= n; i++) {
|
|
86
|
+
if (i < n && get(i) === get(i - 1))
|
|
87
|
+
run++;
|
|
88
|
+
else {
|
|
89
|
+
if (run >= 5)
|
|
90
|
+
score += run - 2;
|
|
91
|
+
run = 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const s = Array.from({ length: n }, (_, i) => (get(i) ? 1 : 0)).join("");
|
|
95
|
+
for (const p of ["10111010000", "00001011101"])
|
|
96
|
+
for (let i = s.indexOf(p); i >= 0; i = s.indexOf(p, i + 1))
|
|
97
|
+
score += 40;
|
|
98
|
+
};
|
|
99
|
+
for (let a = 0; a < n; a++) {
|
|
100
|
+
line((i) => m[a][i]);
|
|
101
|
+
line((i) => m[i][a]);
|
|
102
|
+
}
|
|
103
|
+
for (let y = 0; y < n; y++)
|
|
104
|
+
for (let x = 0; x < n; x++) {
|
|
105
|
+
if (m[y][x])
|
|
106
|
+
dark++;
|
|
107
|
+
if (x + 1 < n && y + 1 < n && m[y][x] === m[y][x + 1] && m[y][x] === m[y + 1][x] && m[y][x] === m[y + 1][x + 1])
|
|
108
|
+
score += 3;
|
|
109
|
+
}
|
|
110
|
+
return score + (Math.ceil(Math.abs(dark * 20 - n * n * 10) / (n * n)) - 1) * 10;
|
|
111
|
+
}
|
|
112
|
+
/** Encodes text as a QR code. Returns rows of modules, true for dark. Throws when the text is over 134 bytes. */
|
|
113
|
+
export function qrMatrix(text) {
|
|
114
|
+
const bytes = new TextEncoder().encode(text);
|
|
115
|
+
const version = SIZES.findIndex(([total]) => bytes.length <= total - 2) + 1;
|
|
116
|
+
if (!version)
|
|
117
|
+
throw new Error(`QR code input is ${bytes.length} bytes; the limit is ${QR_MAX_BYTES}.`);
|
|
118
|
+
const size = 17 + 4 * version;
|
|
119
|
+
const codewords = codewordsFor(bytes, version);
|
|
120
|
+
const base = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
121
|
+
const fn = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
122
|
+
const set = (x, y, dark) => { if (x >= 0 && y >= 0 && x < size && y < size) {
|
|
123
|
+
base[y][x] = dark;
|
|
124
|
+
fn[y][x] = true;
|
|
125
|
+
} };
|
|
126
|
+
for (const [cx, cy] of [[3, 3], [size - 4, 3], [3, size - 4]]) {
|
|
127
|
+
for (let dy = -4; dy <= 4; dy++)
|
|
128
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
129
|
+
const d = Math.max(Math.abs(dx), Math.abs(dy));
|
|
130
|
+
set(cx + dx, cy + dy, d !== 4 && d !== 2);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (let i = 8; i < size - 8; i++) {
|
|
134
|
+
set(i, 6, i % 2 === 0);
|
|
135
|
+
set(6, i, i % 2 === 0);
|
|
136
|
+
}
|
|
137
|
+
const pos = ALIGN[version - 1];
|
|
138
|
+
for (const cy of pos)
|
|
139
|
+
for (const cx of pos) {
|
|
140
|
+
if ((cx === 6 && cy === 6) || (cx === 6 && cy === size - 7) || (cx === size - 7 && cy === 6))
|
|
141
|
+
continue;
|
|
142
|
+
for (let dy = -2; dy <= 2; dy++)
|
|
143
|
+
for (let dx = -2; dx <= 2; dx++)
|
|
144
|
+
set(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
145
|
+
}
|
|
146
|
+
const format = (mask) => {
|
|
147
|
+
const f = formatBits(mask);
|
|
148
|
+
const bit = (i) => ((f >>> i) & 1) === 1;
|
|
149
|
+
for (let i = 0; i <= 5; i++)
|
|
150
|
+
set(8, i, bit(i));
|
|
151
|
+
set(8, 7, bit(6));
|
|
152
|
+
set(8, 8, bit(7));
|
|
153
|
+
set(7, 8, bit(8));
|
|
154
|
+
for (let i = 9; i < 15; i++)
|
|
155
|
+
set(14 - i, 8, bit(i));
|
|
156
|
+
for (let i = 0; i < 8; i++)
|
|
157
|
+
set(size - 1 - i, 8, bit(i));
|
|
158
|
+
for (let i = 8; i < 15; i++)
|
|
159
|
+
set(8, size - 15 + i, bit(i));
|
|
160
|
+
set(8, size - 8, true);
|
|
161
|
+
};
|
|
162
|
+
format(0); // reserves the areas; redone below with the chosen mask
|
|
163
|
+
let k = 0;
|
|
164
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
165
|
+
if (right === 6)
|
|
166
|
+
right = 5;
|
|
167
|
+
for (let v = 0; v < size; v++)
|
|
168
|
+
for (let j = 0; j < 2; j++) {
|
|
169
|
+
const x = right - j;
|
|
170
|
+
const y = ((right + 1) & 2) === 0 ? size - 1 - v : v;
|
|
171
|
+
if (!fn[y][x] && k < codewords.length * 8) {
|
|
172
|
+
base[y][x] = ((codewords[k >> 3] >>> (7 - (k & 7))) & 1) === 1;
|
|
173
|
+
k++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
let best;
|
|
178
|
+
let bestScore = Infinity;
|
|
179
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
180
|
+
const m = base.map((row) => row.slice());
|
|
181
|
+
for (let y = 0; y < size; y++)
|
|
182
|
+
for (let x = 0; x < size; x++)
|
|
183
|
+
if (!fn[y][x] && MASKS[mask](x, y))
|
|
184
|
+
m[y][x] = !m[y][x];
|
|
185
|
+
const saveBase = base.map((r) => r.slice());
|
|
186
|
+
for (let y = 0; y < size; y++)
|
|
187
|
+
base[y] = m[y];
|
|
188
|
+
format(mask);
|
|
189
|
+
const s = penalty(base);
|
|
190
|
+
if (s < bestScore) {
|
|
191
|
+
bestScore = s;
|
|
192
|
+
best = base.map((r) => r.slice());
|
|
193
|
+
}
|
|
194
|
+
for (let y = 0; y < size; y++)
|
|
195
|
+
base[y] = saveBase[y];
|
|
196
|
+
}
|
|
197
|
+
return best;
|
|
198
|
+
}
|
|
199
|
+
/** Draws the matrix for a terminal: black on white with ANSI colours, two rows per line, a 2 module quiet zone. */
|
|
200
|
+
export function qrToTerminal(matrix, quiet = 2) {
|
|
201
|
+
const n = matrix.length + quiet * 2;
|
|
202
|
+
const at = (x, y) => matrix[y - quiet]?.[x - quiet] === true;
|
|
203
|
+
const lines = [];
|
|
204
|
+
for (let y = 0; y < n; y += 2) {
|
|
205
|
+
let s = "\x1b[30;47m";
|
|
206
|
+
for (let x = 0; x < n; x++) {
|
|
207
|
+
const t = at(x, y), b = y + 1 < n && at(x, y + 1);
|
|
208
|
+
s += t && b ? "█" : t ? "▀" : b ? "▄" : " ";
|
|
209
|
+
}
|
|
210
|
+
lines.push(s + "\x1b[0m");
|
|
211
|
+
}
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 stepText(s: Step): string;
|
|
26
|
+
export declare function renderReportMarkdown(report: TestReport): string;
|
|
27
|
+
export {};
|