@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.
- package/dist/adopt-bare.d.ts +39 -0
- package/dist/adopt-bare.js +236 -0
- package/dist/adopt.d.ts +26 -0
- package/dist/adopt.js +257 -0
- package/dist/agents.d.ts +14 -0
- package/dist/agents.js +124 -0
- package/dist/audit.d.ts +13 -0
- package/dist/audit.js +75 -0
- package/dist/autohalt.d.ts +5 -0
- package/dist/autohalt.js +84 -0
- package/dist/ci.d.ts +3 -0
- package/dist/ci.js +42 -0
- package/dist/cli.js +142 -88
- package/dist/config.d.ts +10 -0
- package/dist/config.js +38 -10
- package/dist/doctor.d.ts +14 -0
- package/dist/doctor.js +132 -7
- package/dist/eas.d.ts +29 -0
- package/dist/eas.js +258 -0
- package/dist/ensure-playwright.d.ts +21 -0
- package/dist/ensure-playwright.js +64 -0
- package/dist/explore.d.ts +84 -0
- package/dist/explore.js +247 -0
- package/dist/help.d.ts +4 -2
- package/dist/help.js +39 -3
- package/dist/init.js +501 -17
- package/dist/native.d.ts +3 -0
- package/dist/native.js +39 -0
- package/dist/policy.d.ts +4 -0
- package/dist/policy.js +17 -0
- package/dist/preview-recorder.d.ts +1 -0
- package/dist/preview-recorder.js +62 -0
- package/dist/preview.d.ts +35 -0
- package/dist/preview.js +346 -0
- package/dist/promote.d.ts +9 -0
- package/dist/promote.js +153 -0
- package/dist/publish.d.ts +1 -1
- package/dist/publish.js +73 -29
- package/dist/report-md.d.ts +26 -0
- package/dist/report-md.js +58 -0
- package/dist/run-bridge.d.ts +7 -0
- package/dist/run-bridge.js +374 -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 +86 -0
- package/dist/run.js +301 -0
- package/dist/sim.d.ts +31 -0
- package/dist/sim.js +124 -5
- package/dist/status.d.ts +64 -0
- package/dist/status.js +147 -0
- package/dist/store.d.ts +38 -0
- package/dist/store.js +297 -0
- package/dist/templates/capacitor/github-actions/build.yml +122 -0
- package/dist/test.d.ts +46 -0
- package/dist/test.js +190 -0
- package/package.json +10 -5
package/dist/test.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// `shipvo test`: run plain-language step files (tests/**/*.steps) in the simulator. `--repair` proposes fixes for broken steps.
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { PARITY_LIMITS } from "./explore.js";
|
|
7
|
+
import { renderReportMarkdown } from "./report-md.js";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
/** The same step as a plain-language line (only the target of a tap or type step is ever rewritten). */
|
|
10
|
+
export function stepLine(s) {
|
|
11
|
+
const bad = (v) => v !== undefined && /["\r\n]/.test(v);
|
|
12
|
+
if (bad(s.target) || bad(s.text))
|
|
13
|
+
throw new Error("a step line cannot hold a double quote or a newline");
|
|
14
|
+
if (s.kind === "tap")
|
|
15
|
+
return `tap "${s.target}"`;
|
|
16
|
+
if (s.kind === "type")
|
|
17
|
+
return `type "${s.text}" into "${s.target}"${s.enter ? " and press enter" : ""}`;
|
|
18
|
+
throw new Error(`cannot write a ${s.kind} step`);
|
|
19
|
+
}
|
|
20
|
+
/** A unified diff that replaces single lines (line numbers are 1-based, old and new have the same count). Empty when nothing changes. */
|
|
21
|
+
export function unifiedDiff(file, text, edits) {
|
|
22
|
+
if (!edits.size)
|
|
23
|
+
return "";
|
|
24
|
+
const lines = text.split("\n");
|
|
25
|
+
const out = [`--- a/${file}`, `+++ b/${file}`];
|
|
26
|
+
for (const n of [...edits.keys()].sort((a, b) => a - b)) {
|
|
27
|
+
const old = lines[n - 1];
|
|
28
|
+
out.push(`@@ -${n},1 +${n},1 @@`, `-${old}`, `+${old.match(/^\s*/)[0]}${edits.get(n)}`);
|
|
29
|
+
}
|
|
30
|
+
return out.join("\n") + "\n";
|
|
31
|
+
}
|
|
32
|
+
/** Apply line edits, keeping indentation. Every other line, including comments and blank lines, is untouched. */
|
|
33
|
+
export function applyEdits(text, edits) {
|
|
34
|
+
const lines = text.split("\n");
|
|
35
|
+
for (const [n, l] of edits)
|
|
36
|
+
lines[n - 1] = lines[n - 1].match(/^\s*/)[0] + l;
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
function findStepFiles(dir) {
|
|
40
|
+
if (!existsSync(dir))
|
|
41
|
+
return [];
|
|
42
|
+
return readdirSync(dir).sort().flatMap((f) => {
|
|
43
|
+
const p = join(dir, f);
|
|
44
|
+
return statSync(p).isDirectory() ? (f === "node_modules" ? [] : findStepFiles(p)) : f.endsWith(".steps") ? [p] : [];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function loadKit() {
|
|
48
|
+
let path;
|
|
49
|
+
try {
|
|
50
|
+
path = createRequire(process.cwd() + "/package.json").resolve("@shipvo/test");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error("Install @shipvo/test in this app: npm i -D @shipvo/test");
|
|
54
|
+
}
|
|
55
|
+
return (await import(pathToFileURL(path).href));
|
|
56
|
+
}
|
|
57
|
+
function gitCommit() {
|
|
58
|
+
const sha = process.env.GITHUB_SHA?.trim();
|
|
59
|
+
if (sha)
|
|
60
|
+
return sha;
|
|
61
|
+
try {
|
|
62
|
+
return execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function appIdFromConfig() {
|
|
69
|
+
try {
|
|
70
|
+
const id = JSON.parse(readFileSync("shipvo.json", "utf8")).appId;
|
|
71
|
+
return typeof id === "string" ? id : null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Value of `--name <value>`, or undefined. */
|
|
78
|
+
function flagValue(argv, name) {
|
|
79
|
+
const i = argv.indexOf(name);
|
|
80
|
+
return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : undefined;
|
|
81
|
+
}
|
|
82
|
+
/** Runs the suite. Returns the exit code: 0 when every step passed (or every failure was repaired and written), 1 otherwise. */
|
|
83
|
+
export async function testCmd(argv, kit, rawLog = console.log) {
|
|
84
|
+
const repair = argv.includes("--repair");
|
|
85
|
+
const write = argv.includes("--write");
|
|
86
|
+
if (write && !repair) {
|
|
87
|
+
console.error("--write only works with --repair");
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
const reportPath = flagValue(argv, "--report");
|
|
91
|
+
const release = flagValue(argv, "--release");
|
|
92
|
+
const reportMdPath = flagValue(argv, "--report-md");
|
|
93
|
+
if (argv.includes("--report") && !reportPath) {
|
|
94
|
+
console.error("--report needs a file name");
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
if (argv.includes("--report-md") && !reportMdPath) {
|
|
98
|
+
console.error("--report-md needs a file name");
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
const valueIdx = new Set(["--report", "--report-md", "--release"].map((f) => argv.indexOf(f) + 1).filter((i) => i > 0));
|
|
102
|
+
const given = argv.filter((a, i) => !a.startsWith("--") && !valueIdx.has(i));
|
|
103
|
+
const flows = [];
|
|
104
|
+
const files = given.length ? given : findStepFiles("tests");
|
|
105
|
+
if (!files.length) {
|
|
106
|
+
console.error("No tests found. Put plain-language steps in tests/*.steps, or pass file names.");
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
const k = kit ?? (await loadKit());
|
|
110
|
+
const scrub = (t) => k.redact?.(t) ?? t;
|
|
111
|
+
// Every line goes through scrub: screen text and errors can echo a secret.
|
|
112
|
+
const log = (s) => rawLog(scrub(s));
|
|
113
|
+
let failed = 0, repaired = 0, unresolved = 0;
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
const text = readFileSync(file, "utf8");
|
|
116
|
+
const steps = k.parseSteps(text);
|
|
117
|
+
const phone = await k.openApp();
|
|
118
|
+
const edits = new Map();
|
|
119
|
+
const flow = { name: file, status: "passed", steps, failedStep: null, message: null };
|
|
120
|
+
flows.push(flow);
|
|
121
|
+
for (const step of steps) {
|
|
122
|
+
try {
|
|
123
|
+
await k.runSteps(phone, [step]);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
failed++;
|
|
128
|
+
if (!flow.failedStep) {
|
|
129
|
+
flow.status = "failed";
|
|
130
|
+
flow.failedStep = step;
|
|
131
|
+
flow.message = scrub(e.message.split("\n")[0]);
|
|
132
|
+
}
|
|
133
|
+
log(`FAIL ${file}:${step.line}: ${scrub(e.message.split("\n")[0])}`);
|
|
134
|
+
if (!repair)
|
|
135
|
+
break;
|
|
136
|
+
const r = k.proposeRepair(step, phone.screen);
|
|
137
|
+
if (r.kind === "unresolved") {
|
|
138
|
+
unresolved++;
|
|
139
|
+
log(` no repair: ${r.candidates.length ? "candidates:\n" + r.candidates.map((c) => ` ${c.role} "${c.name || c.text}" [ref=${c.ref}]`).join("\n") : "nothing on screen is close"}`);
|
|
140
|
+
break; // the step stays as written; later steps depend on it
|
|
141
|
+
}
|
|
142
|
+
// Prove the repaired step on the phone; it keeps its assertion, so a step that still fails stays failed.
|
|
143
|
+
try {
|
|
144
|
+
await k.runSteps(phone, [r.step]);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
unresolved++;
|
|
148
|
+
log(" the proposed repair also fails; nothing changed");
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
let line;
|
|
152
|
+
try {
|
|
153
|
+
line = stepLine(r.step);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
unresolved++;
|
|
157
|
+
log(` no repair: ${err.message}; nothing changed`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
repaired++;
|
|
161
|
+
log(` repair: ${r.reason}`);
|
|
162
|
+
edits.set(step.line, line);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
await phone.close?.();
|
|
166
|
+
const diff = unifiedDiff(file, text, edits);
|
|
167
|
+
if (diff)
|
|
168
|
+
log(diff.trimEnd());
|
|
169
|
+
if (diff && write) {
|
|
170
|
+
writeFileSync(file, applyEdits(text, edits));
|
|
171
|
+
log(`wrote ${file}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (repair)
|
|
175
|
+
log(`${failed} failed step(s): ${repaired} repaired${write ? " and written" : " (not written; add --write)"}, ${unresolved} unresolved`);
|
|
176
|
+
else
|
|
177
|
+
log(failed ? `${failed} failed` : "all steps passed");
|
|
178
|
+
if (reportPath || reportMdPath) {
|
|
179
|
+
const report = { app: appIdFromConfig(), commit: gitCommit(), release: release ?? null, time: new Date().toISOString(), parityLimits: PARITY_LIMITS, flows };
|
|
180
|
+
if (reportPath) {
|
|
181
|
+
writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
|
|
182
|
+
log(`wrote report ${reportPath}`);
|
|
183
|
+
}
|
|
184
|
+
if (reportMdPath) {
|
|
185
|
+
writeFileSync(reportMdPath, renderReportMarkdown(report));
|
|
186
|
+
log(`wrote Markdown report ${reportMdPath}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return failed === 0 || (write && repaired === failed) ? 0 : 1;
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipvo/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@noble/ed25519": "^2.1.0",
|
|
13
13
|
"@noble/hashes": "^1.5.0",
|
|
14
|
-
"
|
|
14
|
+
"plist": "^3.1.1",
|
|
15
|
+
"@shipvo/schema": "0.0.5",
|
|
16
|
+
"@shipvo/sim": "^0.1.5",
|
|
17
|
+
"@shipvo/store": "^0.0.2"
|
|
15
18
|
},
|
|
16
19
|
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.8.0",
|
|
21
|
+
"@types/plist": "^3.0.5",
|
|
22
|
+
"js-yaml": "4.3.2",
|
|
17
23
|
"typescript": "^5.6.3",
|
|
18
|
-
"vitest": "^2.1.4"
|
|
19
|
-
"@types/node": "^22.8.0"
|
|
24
|
+
"vitest": "^2.1.4"
|
|
20
25
|
},
|
|
21
26
|
"description": "Shipvo CLI: create an app (init), run it in the simulator (sim), sign and publish over-the-air releases (publish, rollback), manage signing keys.",
|
|
22
27
|
"keywords": [
|
|
@@ -58,7 +63,7 @@
|
|
|
58
63
|
"node": ">=20"
|
|
59
64
|
},
|
|
60
65
|
"scripts": {
|
|
61
|
-
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
|
|
66
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js && mkdir -p dist/templates && cp -R ../../templates/. dist/templates/",
|
|
62
67
|
"test": "vitest run"
|
|
63
68
|
}
|
|
64
69
|
}
|