@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/eas.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ export declare const ANDROID_IMAGE = "latest";
2
+ export declare const IOS_IMAGE = "macos-tahoe-26.5-xcode-26.6";
3
+ export declare const NODE_VERSION = "22";
4
+ export declare const JDK_VERSION = "21";
5
+ export interface CapacitorProject {
6
+ configFile: string;
7
+ android: boolean;
8
+ ios: boolean;
9
+ }
10
+ /** Look for a Capacitor project at `root`. Returns undefined when it is not one. */
11
+ export declare function detectCapacitor(root: string): CapacitorProject | undefined;
12
+ export interface PlannedFile {
13
+ path: string;
14
+ content: string;
15
+ action: "create" | "update";
16
+ }
17
+ /** Work out which files need writing. Existing build YAMLs and schemes are never overwritten. */
18
+ export declare function planEasSetup(root: string, p: CapacitorProject): PlannedFile[];
19
+ /** Create the `ios/App.xcodeproj -> App/App.xcodeproj` link the EAS CLI scheme check needs. Returns true if it made one. */
20
+ export declare function ensureSchemeShim(root: string): boolean;
21
+ export declare const ONE_TIME_STEPS: readonly [readonly ["eas-cli", "init"], readonly ["eas-cli", "build:version:set", "-p", "ios"]];
22
+ export declare const IOS_UNPROVEN = "Note: the iOS signing steps in .eas/build/ios.yml are unproven. The step names were not checked against Expo's custom build functions (https://docs.expo.dev/custom-builds/schema/), so your first iOS build is the check.";
23
+ export type Runner = (cmd: string, args: string[], cwd: string) => number;
24
+ export interface EasOptions {
25
+ cwd?: string;
26
+ log?: (s: string) => void;
27
+ runner?: Runner;
28
+ }
29
+ export declare function easCmd(args: string[], o?: EasOptions): Promise<number>;
package/dist/eas.js ADDED
@@ -0,0 +1,258 @@
1
+ // `shipvo eas setup` and `shipvo eas build`: make a Capacitor repo ready for `eas build` and `eas submit`
2
+ // with the customer's own Expo account. Shipvo writes files; it never holds Expo credentials.
3
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ export const ANDROID_IMAGE = "latest";
7
+ export const IOS_IMAGE = "macos-tahoe-26.5-xcode-26.6";
8
+ export const NODE_VERSION = "22";
9
+ export const JDK_VERSION = "21";
10
+ const SCHEME_PATH = "ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme";
11
+ const SHIM = "ios/App.xcodeproj";
12
+ const SHIM_TARGET = "App/App.xcodeproj";
13
+ /** Look for a Capacitor project at `root`. Returns undefined when it is not one. */
14
+ export function detectCapacitor(root) {
15
+ const configFile = readdirSync(root).find((f) => /^capacitor\.config\.(ts|js|json|mjs|cjs)$/.test(f));
16
+ const android = existsSync(join(root, "android"));
17
+ const ios = existsSync(join(root, "ios/App/App.xcodeproj"));
18
+ if (!configFile || (!android && !ios))
19
+ return undefined;
20
+ return { configFile, android, ios };
21
+ }
22
+ const androidYaml = `# Written by \`shipvo eas setup\`. Safe to edit; setup will not overwrite this file.
23
+ # Docs: https://docs.expo.dev/custom-builds/get-started/
24
+ build:
25
+ name: Capacitor Android release
26
+ steps:
27
+ - eas/checkout
28
+ - eas/install_node_modules
29
+ - run:
30
+ name: Install JDK ${JDK_VERSION}
31
+ command: |
32
+ source "$HOME/.sdkman/bin/sdkman-init.sh"
33
+ sdk install java ${JDK_VERSION}-tem < /dev/null
34
+ set-env JAVA_HOME "$(sdk home java ${JDK_VERSION}-tem)"
35
+ - run:
36
+ name: Build the web app
37
+ command: npm run build
38
+ - run:
39
+ name: Sync Capacitor
40
+ command: npx cap sync android
41
+ - eas/inject_android_credentials
42
+ - eas/run_gradle:
43
+ inputs:
44
+ command: :app:bundleRelease
45
+ - eas/find_and_upload_build_artifacts
46
+ `;
47
+ const iosYaml = `# Written by \`shipvo eas setup\`. Safe to edit; setup will not overwrite this file.
48
+ # Docs: https://docs.expo.dev/custom-builds/get-started/
49
+ # Before the first build, run \`shipvo eas build\`: it creates the ios/App.xcodeproj link that the EAS CLI scheme check needs on your machine before upload.
50
+ build:
51
+ name: Capacitor iOS release
52
+ steps:
53
+ - eas/checkout
54
+ - eas/install_node_modules
55
+ - run:
56
+ name: Build the web app
57
+ command: npm run build
58
+ - run:
59
+ name: Sync Capacitor
60
+ command: npx cap sync ios
61
+ - run:
62
+ name: Install CocoaPods dependencies
63
+ command: cd ios/App && pod install
64
+ - eas/resolve_apple_team_id_from_credentials:
65
+ id: resolve_apple_team_id
66
+ - eas/prebuild_credentials_ios:
67
+ inputs:
68
+ project_dir: ios
69
+ - eas/configure_ios_credentials
70
+ - run:
71
+ name: Write ExportOptions.plist
72
+ # Nothing else creates this file. The team id comes from the EAS credentials step above;
73
+ # the profile name is read from the Xcode project that eas/configure_ios_credentials updated.
74
+ command: |
75
+ BUNDLE_ID="$(xcodebuild -project ios/App/App.xcodeproj -scheme App -configuration Release -showBuildSettings | awk -F' = ' '/ PRODUCT_BUNDLE_IDENTIFIER /{print $2; exit}')"
76
+ PROFILE="$(xcodebuild -project ios/App/App.xcodeproj -scheme App -configuration Release -showBuildSettings | awk -F' = ' '/ PROVISIONING_PROFILE_SPECIFIER /{print $2; exit}')"
77
+ cat > ios/App/ExportOptions.plist <<PLIST
78
+ <?xml version="1.0" encoding="UTF-8"?>
79
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
80
+ <plist version="1.0">
81
+ <dict>
82
+ <key>method</key><string>app-store</string>
83
+ <key>teamID</key><string>\${{ steps.resolve_apple_team_id.outputs.apple_team_id }}</string>
84
+ <key>signingStyle</key><string>manual</string>
85
+ <key>provisioningProfiles</key>
86
+ <dict><key>$BUNDLE_ID</key><string>$PROFILE</string></dict>
87
+ </dict>
88
+ </plist>
89
+ PLIST
90
+ - run:
91
+ name: Archive with distribution signing
92
+ command: |
93
+ xcodebuild archive \\
94
+ -workspace ios/App/App.xcworkspace \\
95
+ -scheme App \\
96
+ -configuration Release \\
97
+ -archivePath build/App.xcarchive \\
98
+ DEVELOPMENT_TEAM="\${{ steps.resolve_apple_team_id.outputs.apple_team_id }}" \\
99
+ CODE_SIGN_STYLE=Manual
100
+ xcodebuild -exportArchive \\
101
+ -archivePath build/App.xcarchive \\
102
+ -exportPath build/export \\
103
+ -exportOptionsPlist ios/App/ExportOptions.plist
104
+ - eas/find_and_upload_build_artifacts
105
+ `;
106
+ const scheme = `<?xml version="1.0" encoding="UTF-8"?>
107
+ <Scheme LastUpgradeVersion="1600" version="1.7">
108
+ <BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
109
+ <BuildActionEntries>
110
+ <BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
111
+ <BuildableReference BuildableIdentifier="primary" BlueprintName="App" BuildableName="App.app" BlueprintIdentifier="504EC3011FED79650016851F" ReferencedContainer="container:App.xcodeproj"/>
112
+ </BuildActionEntry>
113
+ </BuildActionEntries>
114
+ </BuildAction>
115
+ <LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES"/>
116
+ <ArchiveAction buildConfiguration="Release" revealArchiveInOrganizer="YES"/>
117
+ </Scheme>
118
+ `;
119
+ function readJson(path) {
120
+ try {
121
+ return JSON.parse(readFileSync(path, "utf8"));
122
+ }
123
+ catch (e) {
124
+ if (e.code === "ENOENT")
125
+ return {};
126
+ throw new Error(`${path} is not valid JSON. Fix it and run the command again.`);
127
+ }
128
+ }
129
+ /** Merge our profiles into an existing eas.json without touching the customer's own settings. */
130
+ function easJson(root, p) {
131
+ const j = readJson(join(root, "eas.json"));
132
+ for (const k of ["cli", "build", "submit"]) {
133
+ if (j[k] !== undefined && (j[k] === null || typeof j[k] !== "object" || Array.isArray(j[k]))) {
134
+ throw new Error(`${join(root, "eas.json")} is not valid: "${k}" must be an object. Fix it and run the command again.`);
135
+ }
136
+ }
137
+ j.cli = { appVersionSource: "remote", ...(j.cli ?? {}) };
138
+ j.build ??= {};
139
+ if (p.android)
140
+ j.build["shipvo-android"] ??= { android: { image: ANDROID_IMAGE, buildType: "app-bundle", config: "android.yml" }, distribution: "store" };
141
+ if (p.ios)
142
+ j.build["shipvo-ios"] ??= { ios: { image: IOS_IMAGE, node: NODE_VERSION, config: "ios.yml" }, distribution: "store", autoIncrement: true };
143
+ j.submit ??= {};
144
+ if (p.android)
145
+ j.submit["shipvo-android"] ??= { android: {} };
146
+ if (p.ios)
147
+ j.submit["shipvo-ios"] ??= { ios: {} };
148
+ return JSON.stringify(j, null, 2) + "\n";
149
+ }
150
+ function gitignore(root) {
151
+ const path = join(root, ".gitignore");
152
+ const cur = existsSync(path) ? readFileSync(path, "utf8") : "";
153
+ if (cur.split(/\r?\n/).some((l) => l.trim() === SHIM))
154
+ return undefined;
155
+ const sep = cur === "" || cur.endsWith("\n") ? "" : "\n";
156
+ return `${cur}${sep}# Created by \`shipvo eas build\` for EAS; never commit\n${SHIM}\n`;
157
+ }
158
+ /** Work out which files need writing. Existing build YAMLs and schemes are never overwritten. */
159
+ export function planEasSetup(root, p) {
160
+ const out = [];
161
+ const add = (path, content, overwrite) => {
162
+ const exists = existsSync(join(root, path));
163
+ if (exists && !overwrite)
164
+ return;
165
+ if (exists && readFileSync(join(root, path), "utf8") === content)
166
+ return;
167
+ out.push({ path, content, action: exists ? "update" : "create" });
168
+ };
169
+ add("eas.json", easJson(root, p), true);
170
+ if (p.android)
171
+ add(".eas/build/android.yml", androidYaml, false);
172
+ if (p.ios) {
173
+ add(".eas/build/ios.yml", iosYaml, false);
174
+ add(SCHEME_PATH, scheme, false);
175
+ }
176
+ const gi = p.ios ? gitignore(root) : undefined;
177
+ if (gi !== undefined)
178
+ out.push({ path: ".gitignore", content: gi, action: existsSync(join(root, ".gitignore")) ? "update" : "create" });
179
+ return out;
180
+ }
181
+ /** Create the `ios/App.xcodeproj -> App/App.xcodeproj` link the EAS CLI scheme check needs. Returns true if it made one. */
182
+ export function ensureSchemeShim(root) {
183
+ const link = join(root, SHIM);
184
+ try {
185
+ lstatSync(link);
186
+ return false;
187
+ }
188
+ catch { /* missing: create it */ }
189
+ try {
190
+ symlinkSync(SHIM_TARGET, link, "dir");
191
+ }
192
+ catch (e) {
193
+ throw new Error(`Could not create the link ${SHIM} -> ${SHIM_TARGET} (${e.message}). Create it yourself (on Windows, run as administrator or turn on Developer Mode), then run the command again.`);
194
+ }
195
+ return true;
196
+ }
197
+ export const ONE_TIME_STEPS = [
198
+ ["eas-cli", "init"],
199
+ ["eas-cli", "build:version:set", "-p", "ios"],
200
+ ];
201
+ export const IOS_UNPROVEN = "Note: the iOS signing steps in .eas/build/ios.yml are unproven. The step names were not checked against Expo's custom build functions (https://docs.expo.dev/custom-builds/schema/), so your first iOS build is the check.";
202
+ const defaultRunner = (cmd, args, cwd) => spawnSync(cmd, args, { cwd, stdio: "inherit" }).status ?? 1;
203
+ export async function easCmd(args, o = {}) {
204
+ const cwd = o.cwd ?? process.cwd();
205
+ const log = o.log ?? ((s) => console.log(s));
206
+ const runner = o.runner ?? defaultRunner;
207
+ const [sub, ...rest] = args;
208
+ if (sub !== "setup" && sub !== "build") {
209
+ throw new Error("Usage: shipvo eas setup [--dry-run] [--run] | shipvo eas build [eas build args]");
210
+ }
211
+ const project = detectCapacitor(cwd);
212
+ if (!project)
213
+ throw new Error("This is not a Capacitor project. Run this in a folder with capacitor.config.* and an android/ or ios/App/App.xcodeproj folder.");
214
+ if (sub === "build") {
215
+ if (project.ios && ensureSchemeShim(cwd))
216
+ log(`Linked ${SHIM} -> ${SHIM_TARGET} (git-ignored).`);
217
+ return runner("npx", ["eas-cli", "build", ...rest], cwd);
218
+ }
219
+ const known = ["--dry-run", "--run"];
220
+ const bad = rest.filter((a) => !known.includes(a));
221
+ if (bad.length)
222
+ throw new Error(`Unknown option for shipvo eas setup: ${bad.join(" ")}. Use --dry-run or --run.`);
223
+ const dry = rest.includes("--dry-run");
224
+ const runSteps = rest.includes("--run") && !dry;
225
+ const plan = planEasSetup(cwd, project);
226
+ for (const f of plan) {
227
+ log(`${dry ? "Would " + f.action : f.action === "create" ? "Created" : "Updated"} ${f.path}`);
228
+ if (dry)
229
+ log(f.content.replace(/^/gm, " "));
230
+ else {
231
+ mkdirSync(dirname(join(cwd, f.path)), { recursive: true });
232
+ writeFileSync(join(cwd, f.path), f.content);
233
+ }
234
+ }
235
+ if (!plan.length)
236
+ log("Nothing to change: this repo is already set up for EAS.");
237
+ if (project.ios)
238
+ log(IOS_UNPROVEN);
239
+ if (dry)
240
+ return 0;
241
+ log("\nRemaining one-time steps:");
242
+ for (const s of ONE_TIME_STEPS)
243
+ log(` npx ${s.join(" ")}`);
244
+ if (project.ios)
245
+ log("Before an iOS build, build with `shipvo eas build -p ios --profile shipvo-ios`: it creates the git-ignored ios/App.xcodeproj link first.");
246
+ if (runSteps) {
247
+ for (const s of ONE_TIME_STEPS) {
248
+ if (s[1] === "build:version:set" && !project.ios)
249
+ continue;
250
+ const code = runner("npx", [...s], cwd);
251
+ if (code !== 0) {
252
+ log(`\`npx ${s.join(" ")}\` failed (exit ${code}). Fix that and run it again.`);
253
+ return code;
254
+ }
255
+ }
256
+ }
257
+ return 0;
258
+ }
@@ -0,0 +1,25 @@
1
+ export type PackageManager = "pnpm" | "yarn" | "bun" | "npm";
2
+ export interface EnsurePlaywrightDeps {
3
+ cwd: string;
4
+ env: NodeJS.ProcessEnv;
5
+ /** Does this path exist? Used to find the lockfile. */
6
+ exists: (path: string) => boolean;
7
+ /** Runs a command in `cwd`. Throws if it fails. */
8
+ run: (cmd: string, args: string[], cwd: string) => void;
9
+ log: (line: string) => void;
10
+ /** True when `playwright` resolves from the app. Defaults to the same lookup order as `doctor`. */
11
+ resolves?: (cwd: string) => boolean;
12
+ /** Keep stdout clean (for `--json`): the notice and the installer's output go to stderr. */
13
+ quiet?: boolean;
14
+ }
15
+ /** stdio for the installer: stderr for both streams when quiet, so stdout stays clean. */
16
+ export declare function installerStdio(quiet: boolean): "inherit" | ["ignore", 2, 2];
17
+ export declare function detectPackageManager(cwd: string, exists: (path: string) => boolean): PackageManager;
18
+ export declare function installCommand(pm: PackageManager): {
19
+ cmd: string;
20
+ args: string[];
21
+ };
22
+ /** Installs `playwright` as a dev dependency with the app's package manager when it is missing. */
23
+ export declare function ensurePlaywright(deps: Partial<EnsurePlaywrightDeps> & {
24
+ cwd: string;
25
+ }): void;
@@ -0,0 +1,69 @@
1
+ import { execFileSync, execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { join } from "node:path";
5
+ /** stdio for the installer: stderr for both streams when quiet, so stdout stays clean. */
6
+ export function installerStdio(quiet) {
7
+ return quiet ? ["ignore", 2, 2] : "inherit";
8
+ }
9
+ const LOCKFILES = [
10
+ ["pnpm-lock.yaml", "pnpm"],
11
+ ["yarn.lock", "yarn"],
12
+ ["bun.lock", "bun"],
13
+ ["bun.lockb", "bun"],
14
+ ];
15
+ export function detectPackageManager(cwd, exists) {
16
+ for (const [file, pm] of LOCKFILES)
17
+ if (exists(join(cwd, file)))
18
+ return pm;
19
+ return "npm";
20
+ }
21
+ export function installCommand(pm) {
22
+ switch (pm) {
23
+ case "pnpm": return { cmd: "pnpm", args: ["add", "-D", "playwright"] };
24
+ case "yarn": return { cmd: "yarn", args: ["add", "-D", "playwright"] };
25
+ case "bun": return { cmd: "bun", args: ["add", "-d", "playwright"] };
26
+ default: return { cmd: "npm", args: ["install", "--save-dev", "playwright"] };
27
+ }
28
+ }
29
+ /** Same lookup order as doctor's playwrightChromiumPath: the app, this package, @shipvo/sim, then the global npm root. */
30
+ function playwrightResolves(cwd) {
31
+ const tries = [
32
+ () => createRequire(join(cwd, "package.json")).resolve("playwright"),
33
+ () => createRequire(import.meta.url).resolve("playwright"),
34
+ () => createRequire(createRequire(import.meta.url).resolve("@shipvo/sim/package.json")).resolve("playwright"),
35
+ () => createRequire(import.meta.url).resolve(join(execSync("npm root -g", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim(), "playwright")),
36
+ ];
37
+ for (const t of tries) {
38
+ try {
39
+ t();
40
+ return true;
41
+ }
42
+ catch { }
43
+ }
44
+ return false;
45
+ }
46
+ /** Installs `playwright` as a dev dependency with the app's package manager when it is missing. */
47
+ export function ensurePlaywright(deps) {
48
+ const { cwd } = deps;
49
+ const env = deps.env ?? process.env;
50
+ const exists = deps.exists ?? existsSync;
51
+ const quiet = deps.quiet ?? false;
52
+ const run = deps.run ?? ((cmd, args, dir) => { execFileSync(cmd, args, { cwd: dir, stdio: installerStdio(quiet) }); });
53
+ const log = deps.log ?? ((line) => (quiet ? console.error(line) : console.log(line)));
54
+ if ((deps.resolves ?? playwrightResolves)(cwd))
55
+ return;
56
+ const { cmd, args } = installCommand(detectPackageManager(cwd, exists));
57
+ const line = [cmd, ...args].join(" ");
58
+ if (env.SHIPVO_NO_AUTO_INSTALL === "1") {
59
+ throw new Error(`playwright is not installed and SHIPVO_NO_AUTO_INSTALL=1 is set. Run \`${line}\` in ${cwd}.`);
60
+ }
61
+ log(`playwright is not installed. Running: ${line}`);
62
+ try {
63
+ run(cmd, args, cwd);
64
+ }
65
+ catch (e) {
66
+ const msg = e instanceof Error ? e.message : String(e);
67
+ throw new Error(`\`${line}\` failed: ${msg}`);
68
+ }
69
+ }
@@ -0,0 +1,12 @@
1
+ import { type Finding, type Skin } from "./explore.js";
2
+ /** A short name for the screen: its first heading, else its first named line, else "unnamed screen". */
3
+ export declare function screenName(tree: string): string;
4
+ export declare function findingToIssue(f: Finding, skin: Skin): {
5
+ title: string;
6
+ body: string;
7
+ };
8
+ /** Writes `<n>-<kind>.md` per distinct finding. Returns the files and the `gh issue create` lines. */
9
+ export declare function writeIssues(findings: Finding[], skin: Skin, dir: string): {
10
+ files: string[];
11
+ commands: string[];
12
+ };
@@ -0,0 +1,50 @@
1
+ // `shipvo explore --issues-dir`: one ready-to-file Markdown issue per finding. Pure, so tests need no simulator.
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { describeStep, PARITY_LIMITS, signature } from "./explore.js";
5
+ /** A short name for the screen: its first heading, else its first named line, else "unnamed screen". */
6
+ export function screenName(tree) {
7
+ const h = /^\s*- heading "((?:[^"\\]|\\.)*)"/m.exec(tree);
8
+ if (h) {
9
+ try {
10
+ return JSON.parse(`"${h[1]}"`).trim() || "unnamed screen";
11
+ }
12
+ catch {
13
+ return h[1];
14
+ }
15
+ }
16
+ const any = /^\s*- [\w-]+ "((?:[^"\\]|\\.)*)"/m.exec(tree);
17
+ return any ? any[1] : "unnamed screen";
18
+ }
19
+ export function findingToIssue(f, skin) {
20
+ const title = `explore: ${f.kind} on ${screenName(f.screen)}`;
21
+ const body = [
22
+ f.message, "",
23
+ "Steps to reproduce:", "",
24
+ ...f.steps.map((s, i) => `${i + 1}. ${describeStep(s)}`), "",
25
+ `Platform skin: ${skin}`, "",
26
+ "Simulator limits:", "",
27
+ ...PARITY_LIMITS.map((p) => `- ${p}`), "",
28
+ ].join("\n");
29
+ return { title, body };
30
+ }
31
+ const quote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
32
+ /** Writes `<n>-<kind>.md` per distinct finding. Returns the files and the `gh issue create` lines. */
33
+ export function writeIssues(findings, skin, dir) {
34
+ const seen = new Set();
35
+ const files = [];
36
+ const commands = [];
37
+ const unique = findings.filter((f) => { const k = `${f.kind}|${f.message}|${signature(f.screen)}`; if (seen.has(k))
38
+ return false; seen.add(k); return true; });
39
+ if (!unique.length)
40
+ return { files, commands };
41
+ mkdirSync(dir, { recursive: true });
42
+ unique.forEach((f, i) => {
43
+ const { title, body } = findingToIssue(f, skin);
44
+ const file = join(dir, `${i + 1}-${f.kind}.md`);
45
+ writeFileSync(file, `${title}\n\n${body}`);
46
+ files.push(file);
47
+ commands.push(`gh issue create --title ${quote(title)} --body-file ${quote(file)}`);
48
+ });
49
+ return { files, commands };
50
+ }
@@ -0,0 +1,160 @@
1
+ export type Step = {
2
+ action: "open";
3
+ } | {
4
+ action: "open_url";
5
+ url: string;
6
+ } | {
7
+ action: "tap";
8
+ role: string;
9
+ name: string;
10
+ nth: number;
11
+ } | {
12
+ action: "type";
13
+ role: string;
14
+ name: string;
15
+ nth: number;
16
+ text: string;
17
+ enter?: boolean;
18
+ } | {
19
+ action: "back";
20
+ };
21
+ export type Reply = {
22
+ ok: boolean;
23
+ screen?: string | null;
24
+ error?: string;
25
+ code?: string;
26
+ delivery?: string;
27
+ reason?: string;
28
+ };
29
+ /** What the walker needs from a simulator session. */
30
+ export type Driver = {
31
+ /** Start a fresh phone on the app. */
32
+ open(): Promise<Reply>;
33
+ openUrl(url: string): Promise<Reply>;
34
+ tap(ref: string): Promise<Reply>;
35
+ back(): Promise<Reply>;
36
+ /** Type into a text field. Optional: without it the walker does not fill fields. `enter` presses Enter after the text. */
37
+ type?(ref: string, text: string, enter?: boolean): Promise<Reply>;
38
+ /** Look at the current screen again without acting. Optional: without it `stuck_loading` is not checked. */
39
+ snapshot?(): Promise<Reply>;
40
+ /** Uncaught page errors since the last call. */
41
+ errors(): Promise<string[]>;
42
+ /** Rendered size in px of each ref on the current screen. Optional: without it the size check is skipped. */
43
+ sizes?(): Promise<Record<string, [number, number]>>;
44
+ };
45
+ export type Skin = "android" | "ios";
46
+ /** Smallest tap target: 44 x 44 pt on iOS (Apple HIG), 48 x 48 dp on Android (Material and Android accessibility guide). */
47
+ export declare const MIN_TARGET: Record<Skin, number>;
48
+ export type Kind = "error" | "not_delivered" | "action_failed" | "dead_end" | "unnamed" | "small_target" | "duplicate_name" | "network_failed" | "stuck_loading" | "blank_screen";
49
+ export type Finding = {
50
+ kind: Kind;
51
+ message: string;
52
+ screen: string;
53
+ steps: Step[];
54
+ };
55
+ export type Report = {
56
+ app: string;
57
+ generatedAt: string;
58
+ steps: number;
59
+ screens: number;
60
+ maxSteps: number;
61
+ maxDepth: number;
62
+ stoppedEarly: string | null;
63
+ error: string | null;
64
+ findings: Finding[];
65
+ parityLimits: string[];
66
+ };
67
+ export declare const PARITY_LIMITS: string[];
68
+ /** A value for a field, from its label: one the field should accept, or one it should reject. */
69
+ export declare function sampleValue(name: string, invalid: boolean): string;
70
+ type El = {
71
+ role: string;
72
+ name: string;
73
+ ref: string;
74
+ nth: number;
75
+ };
76
+ /** Actionable elements in a tree, in order. `nth` counts earlier elements with the same role and name. Disabled ones are left out. */
77
+ export declare const elements: (tree: string) => El[];
78
+ export declare const fieldElements: (tree: string) => El[];
79
+ /** The tree without refs, to tell whether two screens are the same. */
80
+ export declare const signature: (tree: string) => string;
81
+ /** Steps in plain lines, for the report. */
82
+ export declare const describeStep: (s: Step) => string;
83
+ export type WalkOptions = {
84
+ routes?: string[];
85
+ onlyRoutes?: boolean;
86
+ maxDepth?: number;
87
+ maxSteps?: number;
88
+ app?: string;
89
+ skin?: Skin;
90
+ /** Seed for tie-breaks and tap order. The same seed on the same app repeats the same walk. Default 1. */
91
+ seed?: number;
92
+ /** Wait in ms between looks at a screen that shows a loading indicator. Default 400. */
93
+ settleMs?: number;
94
+ /** Start paths to walk instead of the app's start screen and `routes`. Used by `shipvo lumi` to test one flow. */
95
+ startPaths?: Step[][];
96
+ /** "bad": only try invalid values in text fields, and try them first. Default "both". */
97
+ input?: "both" | "bad";
98
+ /** Called as the walk goes: each action taken, each new screen, each new finding. */
99
+ onEvent?: (e: WalkEvent) => void;
100
+ /** Checked before every action. When it returns true the walk ends and says it was stopped. */
101
+ shouldStop?: () => boolean;
102
+ };
103
+ export type WalkEvent = {
104
+ type: "step";
105
+ n: number;
106
+ step: Step;
107
+ text: string;
108
+ } | {
109
+ type: "screen";
110
+ screens: number;
111
+ sig: string;
112
+ controls: {
113
+ role: string;
114
+ name: string;
115
+ }[];
116
+ } | {
117
+ type: "finding";
118
+ n: number;
119
+ finding: Finding;
120
+ };
121
+ /**
122
+ * Walks the app. Each round it picks the screen to work on with an upper confidence bound: screens with nothing tried yet
123
+ * come first, then screens whose taps found new screens, with a bonus for rarely tried ones. On that screen it takes safe actions
124
+ * before risky ones (log out, delete, cancel, remove). Text fields get a valid value, then an invalid one.
125
+ */
126
+ export declare function walk(driver: Driver, o?: WalkOptions): Promise<Report>;
127
+ export declare function renderMarkdown(r: Report): string;
128
+ /** Driver for a running simulator. */
129
+ export declare function simDriver(base: string, app: string, session?: string, platform?: Skin): Driver;
130
+ /** Use the simulator at `--sim <url>`, or build the app and start one on `--port` (default 4546). `stop` kills the one we started. */
131
+ export declare function startSim(a: Record<string, string | undefined>, cfg: {
132
+ name: string;
133
+ appId: string;
134
+ controlPlane: string;
135
+ }, quiet?: boolean): Promise<{
136
+ base: string;
137
+ stop: () => void;
138
+ }>;
139
+ /** Returns the exit code. With `--json`, stdout is one envelope and everything else goes to stderr. */
140
+ export declare function explore(argv: string[]): Promise<number>;
141
+ /** The app's URL scheme: `expo.scheme` in app.json, else the scheme of a configured route such as "tide://cart". */
142
+ export declare function appScheme(routes: string[] | undefined, dir?: string): string | undefined;
143
+ /**
144
+ * Deep links to walk for `--changed-from <sha>`: the changed, routable screens (up to four), as `<scheme>://<screen>`.
145
+ * The walker opens them with the simulator's `open_url` action, which needs a URL the app's `onUrl` listener understands.
146
+ * If the diff cannot be read (for example a shallow checkout), or the app has no known scheme, say so and fall back to the full walk.
147
+ */
148
+ export declare function routesFromChange(sha: string, log?: (s: string) => void, scheme?: string): Promise<{
149
+ routes?: string[];
150
+ onlyRoutes: boolean;
151
+ }>;
152
+ /** Exit code and `--json` code. A failed flow counts as a finding for `--fail-on-findings`; it has its own code when the walk found nothing. */
153
+ export declare function outcome(report: {
154
+ error?: string | null;
155
+ findings: unknown[];
156
+ }, flowsFailed: boolean, failOn: boolean): {
157
+ exit: number;
158
+ code: string;
159
+ };
160
+ export {};