@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.
Files changed (93) hide show
  1. package/README.md +3 -0
  2. package/dist/adopt.js +1 -2
  3. package/dist/agents.d.ts +29 -0
  4. package/dist/agents.js +183 -0
  5. package/dist/approvals.d.ts +9 -0
  6. package/dist/approvals.js +106 -0
  7. package/dist/audit.d.ts +13 -0
  8. package/dist/audit.js +92 -0
  9. package/dist/autohalt.d.ts +1 -1
  10. package/dist/autohalt.js +28 -16
  11. package/dist/bsdiff.d.ts +4 -0
  12. package/dist/bsdiff.js +465 -0
  13. package/dist/ci-comment.d.ts +38 -0
  14. package/dist/ci-comment.js +169 -0
  15. package/dist/ci-compare.d.ts +10 -0
  16. package/dist/ci-compare.js +25 -0
  17. package/dist/ci-shots.d.ts +9 -0
  18. package/dist/ci-shots.js +81 -0
  19. package/dist/ci.d.ts +11 -0
  20. package/dist/ci.js +100 -0
  21. package/dist/cli.js +51 -1
  22. package/dist/config.d.ts +15 -0
  23. package/dist/config.js +68 -13
  24. package/dist/doctor.d.ts +2 -0
  25. package/dist/doctor.js +88 -8
  26. package/dist/eas.d.ts +29 -0
  27. package/dist/eas.js +258 -0
  28. package/dist/ensure-playwright.d.ts +25 -0
  29. package/dist/ensure-playwright.js +69 -0
  30. package/dist/explore-issues.d.ts +12 -0
  31. package/dist/explore-issues.js +50 -0
  32. package/dist/explore.d.ts +160 -0
  33. package/dist/explore.js +496 -0
  34. package/dist/flows.d.ts +31 -0
  35. package/dist/flows.js +264 -0
  36. package/dist/help.d.ts +2 -2
  37. package/dist/help.js +37 -7
  38. package/dist/hermes.d.ts +10 -0
  39. package/dist/hermes.js +50 -0
  40. package/dist/init.d.ts +1 -0
  41. package/dist/init.js +71 -19
  42. package/dist/json-out.d.ts +53 -0
  43. package/dist/json-out.js +91 -0
  44. package/dist/keys-json.d.ts +1 -0
  45. package/dist/keys-json.js +71 -0
  46. package/dist/login.js +27 -11
  47. package/dist/lumi.d.ts +104 -0
  48. package/dist/lumi.js +364 -0
  49. package/dist/migrate.d.ts +27 -0
  50. package/dist/migrate.js +521 -0
  51. package/dist/policy.d.ts +2 -16
  52. package/dist/policy.js +2 -18
  53. package/dist/preview-recorder.d.ts +1 -0
  54. package/dist/preview-recorder.js +62 -0
  55. package/dist/preview.d.ts +4 -3
  56. package/dist/preview.js +114 -15
  57. package/dist/promote.d.ts +1 -1
  58. package/dist/promote.js +63 -14
  59. package/dist/publish.d.ts +14 -2
  60. package/dist/publish.js +131 -56
  61. package/dist/qr.d.ts +9 -0
  62. package/dist/qr.js +213 -0
  63. package/dist/report-md.d.ts +27 -0
  64. package/dist/report-md.js +58 -0
  65. package/dist/rotation.d.ts +57 -0
  66. package/dist/rotation.js +157 -0
  67. package/dist/run-bridge.d.ts +7 -0
  68. package/dist/run-bridge.js +728 -0
  69. package/dist/run-net.d.ts +39 -0
  70. package/dist/run-net.js +266 -0
  71. package/dist/run-protocol.d.ts +32 -0
  72. package/dist/run-protocol.js +22 -0
  73. package/dist/run-ws.d.ts +8 -0
  74. package/dist/run-ws.js +89 -0
  75. package/dist/run.d.ts +109 -0
  76. package/dist/run.js +385 -0
  77. package/dist/signing.js +13 -0
  78. package/dist/sim.d.ts +4 -0
  79. package/dist/sim.js +25 -7
  80. package/dist/smoke.d.ts +28 -0
  81. package/dist/smoke.js +117 -0
  82. package/dist/spawn-bin.d.ts +11 -0
  83. package/dist/spawn-bin.js +21 -0
  84. package/dist/status.d.ts +71 -1
  85. package/dist/status.js +162 -5
  86. package/dist/store.d.ts +36 -0
  87. package/dist/store.js +266 -3
  88. package/dist/templates/capacitor/github-actions/build.yml +122 -0
  89. package/dist/templates/github-action/shipvo-check.yml +18 -0
  90. package/dist/test.d.ts +46 -0
  91. package/dist/test.js +253 -0
  92. package/package.json +7 -4
  93. package/skills/shipvo-cli/SKILL.md +31 -0
package/dist/test.js ADDED
@@ -0,0 +1,253 @@
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
+ import { emit, envelope } from "./json-out.js";
10
+ import { loadConfig, publishToken } from "./config.js";
11
+ /** The same step as a plain-language line (only the target of a tap or type step is ever rewritten). */
12
+ export function stepLine(s) {
13
+ const bad = (v) => v !== undefined && /["\r\n]/.test(v);
14
+ if (bad(s.target) || bad(s.text))
15
+ throw new Error("a step line cannot hold a double quote or a newline");
16
+ if (s.kind === "tap")
17
+ return `tap "${s.target}"`;
18
+ if (s.kind === "type")
19
+ return `type "${s.text}" into "${s.target}"${s.enter ? " and press enter" : ""}`;
20
+ throw new Error(`cannot write a ${s.kind} step`);
21
+ }
22
+ /** A unified diff that replaces single lines (line numbers are 1-based, old and new have the same count). Empty when nothing changes. */
23
+ export function unifiedDiff(file, text, edits) {
24
+ if (!edits.size)
25
+ return "";
26
+ const lines = text.split("\n");
27
+ const out = [`--- a/${file}`, `+++ b/${file}`];
28
+ for (const n of [...edits.keys()].sort((a, b) => a - b)) {
29
+ const old = lines[n - 1];
30
+ out.push(`@@ -${n},1 +${n},1 @@`, `-${old}`, `+${old.match(/^\s*/)[0]}${edits.get(n)}`);
31
+ }
32
+ return out.join("\n") + "\n";
33
+ }
34
+ /** Apply line edits, keeping indentation. Every other line, including comments and blank lines, is untouched. */
35
+ export function applyEdits(text, edits) {
36
+ const lines = text.split("\n");
37
+ for (const [n, l] of edits)
38
+ lines[n - 1] = lines[n - 1].match(/^\s*/)[0] + l;
39
+ return lines.join("\n");
40
+ }
41
+ function findStepFiles(dir) {
42
+ if (!existsSync(dir))
43
+ return [];
44
+ return readdirSync(dir).sort().flatMap((f) => {
45
+ const p = join(dir, f);
46
+ return statSync(p).isDirectory() ? (f === "node_modules" ? [] : findStepFiles(p)) : f.endsWith(".steps") ? [p] : [];
47
+ });
48
+ }
49
+ async function loadKit() {
50
+ let path;
51
+ try {
52
+ path = createRequire(process.cwd() + "/package.json").resolve("@shipvo/test");
53
+ }
54
+ catch {
55
+ throw new Error("Install @shipvo/test in this app: npm i -D @shipvo/test");
56
+ }
57
+ return (await import(pathToFileURL(path).href));
58
+ }
59
+ function gitCommit() {
60
+ const sha = process.env.GITHUB_SHA?.trim();
61
+ if (sha)
62
+ return sha;
63
+ try {
64
+ return execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function appIdFromConfig() {
71
+ try {
72
+ const id = JSON.parse(readFileSync("shipvo.json", "utf8")).appId;
73
+ return typeof id === "string" ? id : null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ /** Value of `--name <value>`, or undefined. */
80
+ function flagValue(argv, name) {
81
+ const i = argv.indexOf(name);
82
+ return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : undefined;
83
+ }
84
+ /** Runs the suite. Returns the exit code: 0 when every step passed (or every failure was repaired and written), 1 otherwise. */
85
+ export async function testCmd(argv, kit, rawLog, fetchFn = fetch) {
86
+ const json = argv.includes("--json");
87
+ // With --json, stdout holds only the document: progress goes to stderr.
88
+ const out = rawLog ?? (json ? (s) => console.error(s) : console.log);
89
+ const flows = [];
90
+ const fail = (msg, next, code = "usage") => {
91
+ if (json)
92
+ emit(envelope(code, msg, { flows }, next, 2));
93
+ else
94
+ console.error(msg);
95
+ return 2;
96
+ };
97
+ const repair = argv.includes("--repair");
98
+ const write = argv.includes("--write");
99
+ if (write && !repair) {
100
+ return fail("--write only works with --repair", [{ command: "shipvo test --repair --write", why: "--write needs --repair." }]);
101
+ }
102
+ const reportPath = flagValue(argv, "--report");
103
+ const release = flagValue(argv, "--release");
104
+ const reportMdPath = flagValue(argv, "--report-md");
105
+ const platform = flagValue(argv, "--platform") ?? "android";
106
+ const upload = argv.includes("--upload");
107
+ if (platform !== "android" && platform !== "ios") {
108
+ return fail('--platform must be "android" or "ios"', [{ command: "shipvo test --platform ios", why: "Pass android or ios." }]);
109
+ }
110
+ if (upload && !reportPath) {
111
+ return fail("--upload needs --report <file>", [{ command: "shipvo test --report report.json --release 12 --upload", why: "Upload sends the report file." }]);
112
+ }
113
+ if (upload && !release) {
114
+ return fail("--upload needs --release <number>", [{ command: "shipvo test --report report.json --release 12 --upload", why: "Results are stored per release." }]);
115
+ }
116
+ if (argv.includes("--report") && !reportPath) {
117
+ return fail("--report needs a file name", [{ command: "shipvo test --report report.json", why: "Pass a file name." }]);
118
+ }
119
+ if (argv.includes("--report-md") && !reportMdPath) {
120
+ return fail("--report-md needs a file name", [{ command: "shipvo test --report-md report.md", why: "Pass a file name." }]);
121
+ }
122
+ const valueIdx = new Set(["--report", "--report-md", "--release", "--platform"].map((f) => argv.indexOf(f) + 1).filter((i) => i > 0));
123
+ const given = argv.filter((a, i) => !a.startsWith("--") && !valueIdx.has(i));
124
+ const files = given.length ? given : findStepFiles("tests");
125
+ if (!files.length) {
126
+ return fail("No tests found. Put plain-language steps in tests/*.steps, or pass file names.", [{ command: "shipvo test tests/example.steps", why: "Pass a steps file, or add tests/*.steps." }]);
127
+ }
128
+ let k;
129
+ try {
130
+ k = kit ?? (await loadKit());
131
+ }
132
+ catch (e) {
133
+ return fail(e.message, [{ command: "npm i -D @shipvo/test", why: "shipvo test needs @shipvo/test in the app." }]);
134
+ }
135
+ const scrub = (t) => k.redact?.(t) ?? t;
136
+ // Every line goes through scrub: screen text and errors can echo a secret.
137
+ const log = (s) => out(scrub(s));
138
+ let failed = 0, repaired = 0, unresolved = 0;
139
+ for (const file of files) {
140
+ let text, steps, phone;
141
+ try {
142
+ text = readFileSync(file, "utf8");
143
+ steps = k.parseSteps(text);
144
+ phone = await k.openApp();
145
+ }
146
+ catch (e) {
147
+ if (!json)
148
+ throw e;
149
+ return fail(`${file}: ${scrub(e.message.split("\n")[0])}`, [{ command: "shipvo doctor --json", why: "Check the steps file path, the step syntax and the simulator." }], "error");
150
+ }
151
+ const edits = new Map();
152
+ const flow = { name: file, status: "passed", steps, failedStep: null, message: null };
153
+ flows.push(flow);
154
+ for (const step of steps) {
155
+ try {
156
+ await k.runSteps(phone, [step]);
157
+ continue;
158
+ }
159
+ catch (e) {
160
+ failed++;
161
+ if (!flow.failedStep) {
162
+ flow.status = "failed";
163
+ flow.failedStep = step;
164
+ flow.message = scrub(e.message.split("\n")[0]);
165
+ }
166
+ log(`FAIL ${file}:${step.line}: ${scrub(e.message.split("\n")[0])}`);
167
+ if (!repair)
168
+ break;
169
+ const r = k.proposeRepair(step, phone.screen);
170
+ if (r.kind === "unresolved") {
171
+ unresolved++;
172
+ 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"}`);
173
+ break; // the step stays as written; later steps depend on it
174
+ }
175
+ // Prove the repaired step on the phone; it keeps its assertion, so a step that still fails stays failed.
176
+ try {
177
+ await k.runSteps(phone, [r.step]);
178
+ }
179
+ catch {
180
+ unresolved++;
181
+ log(" the proposed repair also fails; nothing changed");
182
+ break;
183
+ }
184
+ let line;
185
+ try {
186
+ line = stepLine(r.step);
187
+ }
188
+ catch (err) {
189
+ unresolved++;
190
+ log(` no repair: ${err.message}; nothing changed`);
191
+ break;
192
+ }
193
+ repaired++;
194
+ log(` repair: ${r.reason}`);
195
+ edits.set(step.line, line);
196
+ }
197
+ }
198
+ await phone.close?.();
199
+ const diff = unifiedDiff(file, text, edits);
200
+ if (diff)
201
+ log(diff.trimEnd());
202
+ if (diff && write) {
203
+ writeFileSync(file, applyEdits(text, edits));
204
+ log(`wrote ${file}`);
205
+ }
206
+ }
207
+ if (repair)
208
+ log(`${failed} failed step(s): ${repaired} repaired${write ? " and written" : " (not written; add --write)"}, ${unresolved} unresolved`);
209
+ else
210
+ log(failed ? `${failed} failed` : "all steps passed");
211
+ if (reportPath || reportMdPath) {
212
+ const report = { app: appIdFromConfig(), commit: gitCommit(), release: release ?? null, platform, time: new Date().toISOString(), parityLimits: PARITY_LIMITS, flows };
213
+ if (reportPath) {
214
+ writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
215
+ log(`wrote report ${reportPath}`);
216
+ }
217
+ if (reportMdPath) {
218
+ writeFileSync(reportMdPath, renderReportMarkdown(report));
219
+ log(`wrote Markdown report ${reportMdPath}`);
220
+ }
221
+ }
222
+ let uploadError = null;
223
+ if (upload && reportPath) {
224
+ const token = publishToken();
225
+ try {
226
+ if (!token)
227
+ throw new Error("No publish token. Set SHIPVO_PUBLISH_TOKEN, or run shipvo app create.");
228
+ const base = loadConfig().controlPlane.replace(/\/$/, "");
229
+ const appId = appIdFromConfig();
230
+ if (!appId)
231
+ throw new Error("No appId in shipvo.json.");
232
+ const r = await fetchFn(`${base}/r/${encodeURIComponent(appId)}/${encodeURIComponent(release)}/tests`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: readFileSync(reportPath, "utf8") });
233
+ if (!r.ok) {
234
+ const e = (await r.json().catch(() => null));
235
+ throw new Error(e?.message ?? e?.error ?? `HTTP ${r.status}`);
236
+ }
237
+ log(`uploaded report for release ${release} (${platform})`);
238
+ }
239
+ catch (e) {
240
+ uploadError = e.message;
241
+ log(`Could not upload the report: ${uploadError}`);
242
+ }
243
+ }
244
+ const exit = failed === 0 || (write && repaired === failed) ? 0 : 1;
245
+ if (json) {
246
+ const bad = flows.filter((f) => f.status === "failed");
247
+ const data = { uploaded: upload ? !uploadError : undefined, upload_error: uploadError ?? undefined, flows: flows.map((f) => ({ name: f.name, status: f.status, steps: f.steps.length, failedStep: f.failedStep, message: f.message })), failed, repaired, unresolved };
248
+ emit(exit === 0
249
+ ? envelope("ok", failed ? `${failed} failed step(s) repaired and written.` : `All ${flows.length} flow(s) passed.`, data, [{ command: "shipvo explore --json", why: "Walk the app for crashes and accessibility problems." }], 0)
250
+ : envelope("tests_failed", `${bad.length} of ${flows.length} flow(s) failed.`, data, [repair ? { command: "shipvo test --repair --write", why: "Write the proposed repairs to the steps files." } : { command: `shipvo test ${bad[0]?.name ?? ""} --repair --json`.replace(" ", " "), why: "Show a diff that fixes broken steps." }], 1));
251
+ }
252
+ return exit;
253
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipvo/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -12,12 +12,14 @@
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.4",
16
- "@shipvo/store": "^0.0.1"
15
+ "@shipvo/schema": "0.0.6",
16
+ "@shipvo/sim": "^0.1.6",
17
+ "@shipvo/store": "^0.0.2"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@types/node": "^22.8.0",
20
21
  "@types/plist": "^3.0.5",
22
+ "js-yaml": "4.3.2",
21
23
  "typescript": "^5.6.3",
22
24
  "vitest": "^2.1.4"
23
25
  },
@@ -50,6 +52,7 @@
50
52
  "directory": "packages/cli"
51
53
  },
52
54
  "files": [
55
+ "skills",
53
56
  "dist",
54
57
  "README.md"
55
58
  ],
@@ -61,7 +64,7 @@
61
64
  "node": ">=20"
62
65
  },
63
66
  "scripts": {
64
- "build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
67
+ "build": "tsc -p tsconfig.json && chmod +x dist/cli.js && mkdir -p dist/templates && cp -R ../../templates/. dist/templates/",
65
68
  "test": "vitest run"
66
69
  }
67
70
  }
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: shipvo-cli
3
+ description: Use the shipvo command line. Use when the project depends on @shipvo/cli and you need the right command to set up, check status, publish, roll back or configure agents.
4
+ ---
5
+
6
+ # @shipvo/cli
7
+
8
+ Run from the app's root folder. Use `npx -y @shipvo/cli <command>` if `shipvo` is not on the path.
9
+
10
+ When unsure, run these two:
11
+
12
+ ```bash
13
+ shipvo doctor
14
+ shipvo status --json
15
+ ```
16
+
17
+ `status --json` prints `ok`, `code`, `summary`, `data` and `next`. `next` is a list of `{ command, why }`: run those. Exit codes: 0 ok, 1 action needed, 2 error. `data.skills` lists the package skills and whether each is synced.
18
+
19
+ ## Common commands
20
+
21
+ ```bash
22
+ shipvo init --dry-run # print the plan, change nothing
23
+ shipvo init --yes # apply it
24
+ shipvo sim # simulator on http://localhost:4545
25
+ shipvo publish --dry-run # check a release before you publish
26
+ shipvo rollback --to <release>
27
+ shipvo agents setup # AGENTS.md, .mcp.json and package skills
28
+ shipvo skills sync # only copy package skills
29
+ ```
30
+
31
+ Add `--dry-run` to `agents setup` and `skills sync` to see what would change. Both are safe to run again. Run `shipvo help` for every command.