prowl-tools 0.1.6 → 0.1.7

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/lib.cjs CHANGED
@@ -37,12 +37,12 @@ __export(visual_exports, {
37
37
  ensureBaselineDir: () => ensureBaselineDir
38
38
  });
39
39
  async function compareScreenshots(baselinePath, currentPath, diffPath, threshold) {
40
- const baselineData = import_pngjs.PNG.sync.read(import_node_fs8.default.readFileSync(baselinePath));
41
- const currentData = import_pngjs.PNG.sync.read(import_node_fs8.default.readFileSync(currentPath));
40
+ const baselineData = import_pngjs.PNG.sync.read(import_node_fs9.default.readFileSync(baselinePath));
41
+ const currentData = import_pngjs.PNG.sync.read(import_node_fs9.default.readFileSync(currentPath));
42
42
  const { width, height } = baselineData;
43
43
  if (currentData.width !== width || currentData.height !== height) {
44
44
  const diff2 = new import_pngjs.PNG({ width, height });
45
- import_node_fs8.default.writeFileSync(diffPath, import_pngjs.PNG.sync.write(diff2));
45
+ import_node_fs9.default.writeFileSync(diffPath, import_pngjs.PNG.sync.write(diff2));
46
46
  return {
47
47
  match: false,
48
48
  diffPercentage: 1,
@@ -60,7 +60,7 @@ async function compareScreenshots(baselinePath, currentPath, diffPath, threshold
60
60
  );
61
61
  const totalPixels = width * height;
62
62
  const diffPercentage = diffPixels / totalPixels;
63
- import_node_fs8.default.writeFileSync(diffPath, import_pngjs.PNG.sync.write(diff));
63
+ import_node_fs9.default.writeFileSync(diffPath, import_pngjs.PNG.sync.write(diff));
64
64
  return {
65
65
  match: diffPercentage <= threshold,
66
66
  diffPercentage,
@@ -68,16 +68,16 @@ async function compareScreenshots(baselinePath, currentPath, diffPath, threshold
68
68
  };
69
69
  }
70
70
  function ensureBaselineDir(configDir) {
71
- const baselineDir = import_node_path8.default.join(configDir, "baselines");
72
- import_node_fs8.default.mkdirSync(baselineDir, { recursive: true });
71
+ const baselineDir = import_node_path10.default.join(configDir, "baselines");
72
+ import_node_fs9.default.mkdirSync(baselineDir, { recursive: true });
73
73
  return baselineDir;
74
74
  }
75
- var import_node_fs8, import_node_path8, import_pngjs, import_pixelmatch;
75
+ var import_node_fs9, import_node_path10, import_pngjs, import_pixelmatch;
76
76
  var init_visual = __esm({
77
77
  "src/runner/visual.ts"() {
78
78
  "use strict";
79
- import_node_fs8 = __toESM(require("fs"), 1);
80
- import_node_path8 = __toESM(require("path"), 1);
79
+ import_node_fs9 = __toESM(require("fs"), 1);
80
+ import_node_path10 = __toESM(require("path"), 1);
81
81
  import_pngjs = require("pngjs");
82
82
  import_pixelmatch = __toESM(require("pixelmatch"), 1);
83
83
  }
@@ -95,11 +95,17 @@ __export(src_exports, {
95
95
  DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
96
96
  DEFAULT_SIMULATOR_LOCK_ROOT: () => DEFAULT_SIMULATOR_LOCK_ROOT,
97
97
  DEFAULT_WDA_REQUEST_TIMEOUT_MS: () => DEFAULT_WDA_REQUEST_TIMEOUT_MS,
98
+ HELPER_BINARY: () => HELPER_BINARY,
98
99
  INTERACTIVE_ROLES: () => INTERACTIVE_ROLES,
99
100
  IOS_INTERACTIVE_TYPES: () => IOS_INTERACTIVE_TYPES,
100
101
  IOS_MATCH_DIALECT: () => IOS_MATCH_DIALECT,
101
102
  IOS_PRESS_KEYS: () => IOS_PRESS_KEYS,
102
103
  IOS_WINDOW_TYPE: () => IOS_WINDOW_TYPE,
104
+ MACDRIVER_REPO: () => MACDRIVER_REPO,
105
+ MACDRIVER_SIGNING_AUTHORITY_PREFIX: () => MACDRIVER_SIGNING_AUTHORITY_PREFIX,
106
+ MACDRIVER_SIGNING_IDENTIFIER: () => MACDRIVER_SIGNING_IDENTIFIER,
107
+ MACDRIVER_VERSION: () => MACDRIVER_VERSION,
108
+ MACDRIVER_VERSION_PATTERN: () => MACDRIVER_VERSION_PATTERN,
103
109
  MACOS_MATCH_DIALECT: () => MACOS_MATCH_DIALECT,
104
110
  NATIVE_ATTRIBUTE_MAP: () => NATIVE_ATTRIBUTE_MAP,
105
111
  SpawnMacHelperClient: () => SpawnMacHelperClient,
@@ -129,6 +135,8 @@ __export(src_exports, {
129
135
  closeIosSession: () => closeIosSession,
130
136
  closeMacSession: () => closeMacSession,
131
137
  clusterFailures: () => clusterFailures,
138
+ codesignVerifier: () => codesignVerifier,
139
+ collectMacdriverStatus: () => collectMacdriverStatus,
132
140
  computeFlakeScore: () => computeFlakeScore,
133
141
  configSchema: () => configSchema,
134
142
  createAndroidDriver: () => createAndroidDriver,
@@ -142,6 +150,8 @@ __export(src_exports, {
142
150
  defaultAgentConnector: () => defaultAgentConnector,
143
151
  defaultIosAgentConnector: () => defaultIosAgentConnector,
144
152
  defaultWdaTestRunPreparer: () => defaultWdaTestRunPreparer,
153
+ dittoExtractor: () => dittoExtractor,
154
+ downloadAndVerify: () => downloadAndVerify,
145
155
  escapePredicateArg: () => escapePredicateArg,
146
156
  escapeUiSelectorArg: () => escapeUiSelectorArg,
147
157
  extractElementId: () => extractElementId,
@@ -152,6 +162,7 @@ __export(src_exports, {
152
162
  healSelector: () => healSelector,
153
163
  huntSchema: () => huntSchema,
154
164
  injectUsePortIntoXctestrun: () => injectUsePortIntoXctestrun,
165
+ installMacdriver: () => installMacdriver,
155
166
  interpolateHunt: () => interpolateHunt,
156
167
  iosAppAllowedIdentities: () => iosAppAllowedIdentities,
157
168
  iosNodeToNative: () => iosNodeToNative,
@@ -168,7 +179,14 @@ __export(src_exports, {
168
179
  loadHunt: () => loadHunt,
169
180
  loadHuntMeta: () => loadHuntMeta,
170
181
  loadHuntTags: () => loadHuntTags,
182
+ macdriverAssetName: () => macdriverAssetName,
183
+ macdriverAssetUrl: () => macdriverAssetUrl,
171
184
  macdriverBuildInstructions: () => macdriverBuildInstructions,
185
+ macdriverChecksumName: () => macdriverChecksumName,
186
+ macdriverInstallRoot: () => macdriverInstallRoot,
187
+ macdriverInstalledBinary: () => macdriverInstalledBinary,
188
+ macdriverReleaseTag: () => macdriverReleaseTag,
189
+ macdriverVersionDir: () => macdriverVersionDir,
172
190
  matchAndroidSelector: () => matchAndroidSelector,
173
191
  matchIosSelector: () => matchIosSelector,
174
192
  matchNativeTree: () => matchNativeTree,
@@ -178,6 +196,8 @@ __export(src_exports, {
178
196
  parseAdbDevices: () => parseAdbDevices,
179
197
  parseAndroidHierarchy: () => parseAndroidHierarchy,
180
198
  parseAndroidSelector: () => parseAndroidSelector,
199
+ parseChecksumFile: () => parseChecksumFile,
200
+ parseCodesignDetails: () => parseCodesignDetails,
181
201
  parseForwardPort: () => parseForwardPort,
182
202
  parseIosHierarchy: () => parseIosHierarchy,
183
203
  parseIosSelector: () => parseIosSelector,
@@ -204,20 +224,27 @@ __export(src_exports, {
204
224
  resolveWdaTestRun: () => resolveWdaTestRun,
205
225
  runHunt: () => runHunt,
206
226
  runSuite: () => runSuite,
227
+ runVersionProbe: () => runVersionProbe,
207
228
  selectDeviceSerial: () => selectDeviceSerial,
208
229
  selectSimulatorUdid: () => selectSimulatorUdid,
230
+ sha256Hex: () => sha256Hex,
209
231
  shortIosType: () => shortIosType,
210
232
  stepSchema: () => stepSchema,
233
+ tccGuidance: () => tccGuidance,
211
234
  unquoteSelectorValue: () => unquoteSelectorValue,
212
235
  unwrapAndroidTextSelector: () => unwrapAndroidTextSelector,
213
236
  unwrapIosTextSelector: () => unwrapIosTextSelector,
214
237
  unwrapNativeTextSelector: () => unwrapNativeTextSelector,
215
238
  updateBacklogFromSuite: () => updateBacklogFromSuite,
239
+ validateMacdriverArchiveEntries: () => validateMacdriverArchiveEntries,
240
+ validateMacdriverVersion: () => validateMacdriverVersion,
241
+ verifyMacdriverSignature: () => verifyMacdriverSignature,
216
242
  waitForAgentReady: () => waitForAgentReady,
217
243
  waitForWdaReady: () => waitForWdaReady,
218
244
  wdaCacheDir: () => wdaCacheDir,
219
245
  wdaTestRunArgs: () => wdaTestRunArgs,
220
- webOnlyReason: () => webOnlyReason
246
+ webOnlyReason: () => webOnlyReason,
247
+ zipinfoArchiveLister: () => zipinfoArchiveLister
221
248
  });
222
249
  module.exports = __toCommonJS(src_exports);
223
250
 
@@ -400,25 +427,71 @@ function createMacDriver(client, options = {}) {
400
427
  // src/browser/mac-helper.ts
401
428
  var import_node_child_process = require("child_process");
402
429
  var import_node_fs = __toESM(require("fs"), 1);
403
- var import_node_path = __toESM(require("path"), 1);
430
+ var import_node_os2 = __toESM(require("os"), 1);
431
+ var import_node_path2 = __toESM(require("path"), 1);
404
432
  var import_node_url = require("url");
405
- var import_meta = {};
433
+
434
+ // src/browser/macdriver-release.ts
435
+ var import_node_os = __toESM(require("os"), 1);
436
+ var import_node_path = __toESM(require("path"), 1);
406
437
  var HELPER_BINARY = "prowl-macdriver";
438
+ var MACDRIVER_VERSION = "0.1.0";
439
+ var MACDRIVER_REPO = "prowl-tools/prowl";
440
+ var MACDRIVER_SIGNING_IDENTIFIER = "tools.prowl.macdriver";
441
+ var MACDRIVER_SIGNING_AUTHORITY_PREFIX = "Developer ID Application: Genkei Labs";
442
+ var MACDRIVER_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
443
+ function validateMacdriverVersion(version) {
444
+ if (!MACDRIVER_VERSION_PATTERN.test(version)) {
445
+ throw new Error(
446
+ `Invalid prowl-macdriver version "${version}". Expected a release version like 0.1.0.`
447
+ );
448
+ }
449
+ return version;
450
+ }
451
+ function macdriverReleaseTag(version = MACDRIVER_VERSION) {
452
+ return `macdriver-v${validateMacdriverVersion(version)}`;
453
+ }
454
+ function macdriverAssetName(version = MACDRIVER_VERSION) {
455
+ return `prowl-macdriver-v${validateMacdriverVersion(version)}-universal.zip`;
456
+ }
457
+ function macdriverChecksumName(version = MACDRIVER_VERSION) {
458
+ return `${macdriverAssetName(version)}.sha256`;
459
+ }
460
+ function macdriverAssetUrl(assetName, version = MACDRIVER_VERSION) {
461
+ return `https://github.com/${MACDRIVER_REPO}/releases/download/${macdriverReleaseTag(version)}/${assetName}`;
462
+ }
463
+ function macdriverInstallRoot(homedir = import_node_os.default.homedir()) {
464
+ return import_node_path.default.join(homedir, ".prowl", "macdriver");
465
+ }
466
+ function macdriverVersionDir(version = MACDRIVER_VERSION, homedir = import_node_os.default.homedir()) {
467
+ const root = import_node_path.default.resolve(macdriverInstallRoot(homedir));
468
+ const versionDir = import_node_path.default.resolve(root, validateMacdriverVersion(version));
469
+ if (!versionDir.startsWith(root + import_node_path.default.sep)) {
470
+ throw new Error(`Resolved prowl-macdriver version directory escaped install root: ${versionDir}`);
471
+ }
472
+ return versionDir;
473
+ }
474
+ function macdriverInstalledBinary(version = MACDRIVER_VERSION, homedir = import_node_os.default.homedir()) {
475
+ return import_node_path.default.join(macdriverVersionDir(version, homedir), HELPER_BINARY);
476
+ }
477
+
478
+ // src/browser/mac-helper.ts
479
+ var import_meta = {};
407
480
  function macdriverBuildInstructions() {
408
- return "The macOS target requires the experimental `prowl-macdriver` helper, which is not shipped in the npm package. Build it locally:\n cd macdriver && swift build -c release\nor point Prowl at a prebuilt binary via the PROWL_MACDRIVER_BIN environment variable.";
481
+ return "The macOS target needs the `prowl-macdriver` helper. Install the prebuilt, signed binary (recommended):\n prowl macdriver install\nContributors building from source can instead run:\n cd macdriver && swift build -c release\nor point Prowl at a prebuilt binary via the PROWL_MACDRIVER_BIN environment variable.";
409
482
  }
410
483
  function getPackageRoot() {
411
- let dir = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
412
- const root = import_node_path.default.parse(dir).root;
484
+ let dir = import_node_path2.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
485
+ const root = import_node_path2.default.parse(dir).root;
413
486
  while (dir !== root) {
414
- if (import_node_fs.default.existsSync(import_node_path.default.join(dir, "package.json"))) {
487
+ if (import_node_fs.default.existsSync(import_node_path2.default.join(dir, "package.json"))) {
415
488
  return dir;
416
489
  }
417
- dir = import_node_path.default.dirname(dir);
490
+ dir = import_node_path2.default.dirname(dir);
418
491
  }
419
492
  return root;
420
493
  }
421
- function resolveHelperBinary(env = process.env) {
494
+ function resolveHelperBinary(env = process.env, options = {}) {
422
495
  const override = env.PROWL_MACDRIVER_BIN;
423
496
  if (override) {
424
497
  if (!import_node_fs.default.existsSync(override)) {
@@ -429,10 +502,15 @@ ${macdriverBuildInstructions()}`
429
502
  }
430
503
  return override;
431
504
  }
505
+ const homedir = options.homedir ?? import_node_os2.default.homedir();
506
+ const userBinary = macdriverInstalledBinary(MACDRIVER_VERSION, homedir);
507
+ if (import_node_fs.default.existsSync(userBinary)) {
508
+ return userBinary;
509
+ }
432
510
  const root = getPackageRoot();
433
511
  const candidates = [
434
- import_node_path.default.join(root, "macdriver", ".build", "release", HELPER_BINARY),
435
- import_node_path.default.join(root, "macdriver", ".build", "debug", HELPER_BINARY)
512
+ import_node_path2.default.join(root, "macdriver", ".build", "release", HELPER_BINARY),
513
+ import_node_path2.default.join(root, "macdriver", ".build", "debug", HELPER_BINARY)
436
514
  ];
437
515
  for (const candidate of candidates) {
438
516
  if (import_node_fs.default.existsSync(candidate)) {
@@ -604,8 +682,300 @@ async function closeMacSession(session) {
604
682
  }
605
683
  }
606
684
 
607
- // src/browser/android-driver.ts
685
+ // src/browser/macdriver-install.ts
686
+ var import_node_child_process2 = require("child_process");
687
+ var import_node_crypto = require("crypto");
608
688
  var import_node_fs2 = __toESM(require("fs"), 1);
689
+ var import_node_os3 = __toESM(require("os"), 1);
690
+ var import_node_path3 = __toESM(require("path"), 1);
691
+ var import_node_util = require("util");
692
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
693
+ function parseChecksumFile(text) {
694
+ const token = text.trim().split(/\s+/, 1)[0] ?? "";
695
+ const digest = token.toLowerCase();
696
+ if (!/^[0-9a-f]{64}$/.test(digest)) {
697
+ throw new Error(`Malformed .sha256 checksum file (expected a 64-char hex digest, got: ${text.trim().slice(0, 80)})`);
698
+ }
699
+ return digest;
700
+ }
701
+ function sha256Hex(bytes) {
702
+ return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
703
+ }
704
+ var commandRunner = async (file, args) => execFileAsync(file, args);
705
+ function commandOutput(result) {
706
+ return [result.stdout, result.stderr].filter((value) => value !== void 0).map((value) => value.toString()).join("\n").trim();
707
+ }
708
+ function commandErrorDetail(error) {
709
+ const err = error;
710
+ return [err.stderr, err.stdout, err.message].filter((value) => value !== void 0 && value !== "").map((value) => value.toString()).join("\n").trim();
711
+ }
712
+ async function runRequiredCommand(run, binaryPath, command, args, label) {
713
+ try {
714
+ return await run(command, args);
715
+ } catch (error) {
716
+ const detail = commandErrorDetail(error);
717
+ throw new Error(`${label} failed for ${binaryPath}${detail ? `: ${detail}` : ""}`);
718
+ }
719
+ }
720
+ var zipinfoArchiveLister = async (zipPath) => {
721
+ try {
722
+ const { stdout } = await execFileAsync("zipinfo", ["-1", zipPath]);
723
+ return stdout.toString().split(/\r?\n/).filter((entry) => entry.length > 0);
724
+ } catch (error) {
725
+ const detail = commandErrorDetail(error);
726
+ throw new Error(`Failed to inspect release archive ${zipPath} with zipinfo${detail ? `: ${detail}` : ""}`);
727
+ }
728
+ };
729
+ function normalizeArchiveEntryName(entry) {
730
+ if (entry.length === 0 || entry !== entry.trim() || entry.includes("\0") || entry.includes("\\")) {
731
+ throw new Error(`Unsafe path in prowl-macdriver release archive: ${JSON.stringify(entry)}`);
732
+ }
733
+ if (entry.endsWith("/")) {
734
+ throw new Error(`Unexpected directory in prowl-macdriver release archive: ${entry}`);
735
+ }
736
+ if (import_node_path3.default.posix.isAbsolute(entry)) {
737
+ throw new Error(`Unsafe absolute path in prowl-macdriver release archive: ${entry}`);
738
+ }
739
+ const parts = entry.split("/");
740
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
741
+ throw new Error(`Unsafe path in prowl-macdriver release archive: ${entry}`);
742
+ }
743
+ return entry;
744
+ }
745
+ function validateMacdriverArchiveEntries(entries) {
746
+ const normalized = entries.map(normalizeArchiveEntryName);
747
+ if (normalized.length !== 1 || normalized[0] !== HELPER_BINARY) {
748
+ const shown = normalized.length > 0 ? normalized.join(", ") : "(empty archive)";
749
+ throw new Error(
750
+ `Unexpected prowl-macdriver release archive contents: ${shown}. Expected exactly "${HELPER_BINARY}" at the archive root.`
751
+ );
752
+ }
753
+ }
754
+ var dittoExtractor = async (zipPath, destDir) => {
755
+ await execFileAsync("ditto", ["-x", "-k", zipPath, destDir]);
756
+ };
757
+ function parseCodesignDetails(text) {
758
+ const details = { identifier: null, authorities: [], teamIdentifier: null };
759
+ for (const line of text.split(/\r?\n/)) {
760
+ const trimmed = line.trim();
761
+ if (trimmed.startsWith("Identifier=")) {
762
+ details.identifier = trimmed.slice("Identifier=".length);
763
+ } else if (trimmed.startsWith("Authority=")) {
764
+ details.authorities.push(trimmed.slice("Authority=".length));
765
+ } else if (trimmed.startsWith("TeamIdentifier=")) {
766
+ details.teamIdentifier = trimmed.slice("TeamIdentifier=".length);
767
+ }
768
+ }
769
+ return details;
770
+ }
771
+ function validateCodesignDetails(details, binaryPath) {
772
+ if (details.identifier !== MACDRIVER_SIGNING_IDENTIFIER) {
773
+ throw new Error(
774
+ `codesign verification failed for ${binaryPath}: expected identifier "${MACDRIVER_SIGNING_IDENTIFIER}", got "${details.identifier ?? "missing"}"`
775
+ );
776
+ }
777
+ const developerIdAuthority = details.authorities.find(
778
+ (authority) => authority.startsWith(`${MACDRIVER_SIGNING_AUTHORITY_PREFIX} (`)
779
+ );
780
+ const authorityTeamId = developerIdAuthority?.match(/\(([A-Z0-9]{10})\)$/)?.[1] ?? null;
781
+ if (!developerIdAuthority || !authorityTeamId) {
782
+ const shown = details.authorities.length > 0 ? details.authorities.join(" / ") : "missing";
783
+ throw new Error(
784
+ `codesign verification failed for ${binaryPath}: expected ${MACDRIVER_SIGNING_AUTHORITY_PREFIX} signer, got ${shown}`
785
+ );
786
+ }
787
+ if (!details.teamIdentifier) {
788
+ throw new Error(`codesign verification failed for ${binaryPath}: missing TeamIdentifier`);
789
+ }
790
+ if (details.teamIdentifier !== authorityTeamId) {
791
+ throw new Error(
792
+ `codesign verification failed for ${binaryPath}: TeamIdentifier ${details.teamIdentifier} does not match Developer ID authority team ${authorityTeamId}`
793
+ );
794
+ }
795
+ }
796
+ async function verifyMacdriverSignature(binaryPath, run = commandRunner) {
797
+ await runRequiredCommand(run, binaryPath, "codesign", ["--verify", "--strict", binaryPath], "codesign verification");
798
+ const display = await runRequiredCommand(
799
+ run,
800
+ binaryPath,
801
+ "codesign",
802
+ ["--display", "--verbose=4", binaryPath],
803
+ "codesign detail inspection"
804
+ );
805
+ validateCodesignDetails(parseCodesignDetails(commandOutput(display)), binaryPath);
806
+ await runRequiredCommand(
807
+ run,
808
+ binaryPath,
809
+ "spctl",
810
+ ["--assess", "--type", "execute", "--verbose=4", binaryPath],
811
+ "spctl assessment"
812
+ );
813
+ }
814
+ var codesignVerifier = async (binaryPath) => {
815
+ await verifyMacdriverSignature(binaryPath);
816
+ };
817
+ async function downloadAndVerify(options = {}) {
818
+ const version = options.version ?? MACDRIVER_VERSION;
819
+ const fetchImpl = options.fetchImpl ?? fetch;
820
+ const assetName = macdriverAssetName(version);
821
+ const zipUrl = macdriverAssetUrl(assetName, version);
822
+ const sumUrl = macdriverAssetUrl(macdriverChecksumName(version), version);
823
+ const [zipRes, sumRes] = await Promise.all([
824
+ fetchImpl(zipUrl, { redirect: "follow" }),
825
+ fetchImpl(sumUrl, { redirect: "follow" })
826
+ ]);
827
+ if (zipRes.status === 404 || sumRes.status === 404) {
828
+ throw new Error(
829
+ `No published prowl-macdriver release for ${macdriverReleaseTag(version)} yet.
830
+ The signed binary is cut by the maintainer; until then build from source:
831
+ cd macdriver && swift build -c release`
832
+ );
833
+ }
834
+ if (!zipRes.ok) {
835
+ throw new Error(`Failed to download ${assetName} (HTTP ${zipRes.status}) from ${zipUrl}`);
836
+ }
837
+ if (!sumRes.ok) {
838
+ throw new Error(`Failed to download the checksum (HTTP ${sumRes.status}) from ${sumUrl}`);
839
+ }
840
+ const zipBytes = Buffer.from(await zipRes.arrayBuffer());
841
+ const expected = parseChecksumFile(await sumRes.text());
842
+ const actual = sha256Hex(zipBytes);
843
+ if (actual !== expected) {
844
+ throw new Error(
845
+ `Checksum mismatch for ${assetName}.
846
+ expected: ${expected}
847
+ actual: ${actual}
848
+ The download was rejected and discarded; re-run the install, and report it if it repeats.`
849
+ );
850
+ }
851
+ return zipBytes;
852
+ }
853
+ async function installMacdriver(options = {}) {
854
+ const version = validateMacdriverVersion(options.version ?? MACDRIVER_VERSION);
855
+ const homedir = options.homedir ?? import_node_os3.default.homedir();
856
+ const listArchiveEntries = options.listArchiveEntries ?? zipinfoArchiveLister;
857
+ const extract = options.extract ?? dittoExtractor;
858
+ const verifySignature = options.verifySignature ?? codesignVerifier;
859
+ const installRoot = macdriverInstallRoot(homedir);
860
+ const versionDir = macdriverVersionDir(version, homedir);
861
+ const binaryPath = macdriverInstalledBinary(version, homedir);
862
+ if (!options.force && import_node_fs2.default.existsSync(binaryPath)) {
863
+ return { version, binaryPath, alreadyInstalled: true };
864
+ }
865
+ const zipBytes = await downloadAndVerify({ version, fetchImpl: options.fetchImpl });
866
+ import_node_fs2.default.mkdirSync(installRoot, { recursive: true });
867
+ const stagingDir = import_node_fs2.default.mkdtempSync(import_node_path3.default.join(installRoot, `.tmp-${version}-`));
868
+ const extractDir = import_node_path3.default.join(stagingDir, "extract");
869
+ const zipPath = import_node_path3.default.join(stagingDir, macdriverAssetName(version));
870
+ try {
871
+ import_node_fs2.default.mkdirSync(extractDir);
872
+ import_node_fs2.default.writeFileSync(zipPath, zipBytes);
873
+ validateMacdriverArchiveEntries(await listArchiveEntries(zipPath));
874
+ await extract(zipPath, extractDir);
875
+ const stagedBinaryPath = import_node_path3.default.join(extractDir, HELPER_BINARY);
876
+ assertExtractedHelper(extractDir, stagedBinaryPath);
877
+ import_node_fs2.default.chmodSync(stagedBinaryPath, 493);
878
+ await verifySignature(stagedBinaryPath);
879
+ replaceVersionDir(versionDir, extractDir, installRoot);
880
+ } finally {
881
+ import_node_fs2.default.rmSync(stagingDir, { recursive: true, force: true });
882
+ }
883
+ return { version, binaryPath, alreadyInstalled: false };
884
+ }
885
+ function assertExtractedHelper(extractDir, binaryPath) {
886
+ const entries = import_node_fs2.default.readdirSync(extractDir);
887
+ if (!entries.includes(HELPER_BINARY)) {
888
+ throw new Error(`The release archive did not contain a "${HELPER_BINARY}" binary.`);
889
+ }
890
+ if (entries.length !== 1 || entries[0] !== HELPER_BINARY) {
891
+ const shown = entries.length > 0 ? entries.join(", ") : "(empty directory)";
892
+ throw new Error(
893
+ `Unexpected extracted prowl-macdriver archive contents: ${shown}. Expected exactly "${HELPER_BINARY}".`
894
+ );
895
+ }
896
+ let stat = null;
897
+ try {
898
+ stat = import_node_fs2.default.lstatSync(binaryPath);
899
+ } catch {
900
+ }
901
+ if (!stat?.isFile()) {
902
+ throw new Error(`The release archive "${HELPER_BINARY}" entry is not a regular file.`);
903
+ }
904
+ }
905
+ function replaceVersionDir(versionDir, stagedVersionDir, installRoot) {
906
+ const backupDir = import_node_path3.default.join(installRoot, `.previous-${import_node_path3.default.basename(versionDir)}-${process.pid}-${Date.now()}`);
907
+ let backedUp = false;
908
+ try {
909
+ if (import_node_fs2.default.existsSync(versionDir)) {
910
+ import_node_fs2.default.renameSync(versionDir, backupDir);
911
+ backedUp = true;
912
+ }
913
+ import_node_fs2.default.renameSync(stagedVersionDir, versionDir);
914
+ if (backedUp) {
915
+ import_node_fs2.default.rmSync(backupDir, { recursive: true, force: true });
916
+ }
917
+ } catch (error) {
918
+ if (backedUp && !import_node_fs2.default.existsSync(versionDir) && import_node_fs2.default.existsSync(backupDir)) {
919
+ import_node_fs2.default.renameSync(backupDir, versionDir);
920
+ }
921
+ throw error;
922
+ }
923
+ }
924
+ var runVersionProbe = async (binaryPath) => {
925
+ try {
926
+ const { stdout } = await execFileAsync(binaryPath, ["version"], { timeout: 5e3 });
927
+ const match = stdout.match(/prowl-macdriver\s+(\S+)/);
928
+ return match ? match[1] : stdout.trim() || null;
929
+ } catch {
930
+ return null;
931
+ }
932
+ };
933
+ async function collectMacdriverStatus(options = {}) {
934
+ const env = options.env ?? process.env;
935
+ const homedir = options.homedir ?? import_node_os3.default.homedir();
936
+ const probe = options.probe ?? runVersionProbe;
937
+ let resolved = null;
938
+ try {
939
+ const resolvedPath = resolveHelperBinary(env, { homedir });
940
+ resolved = { path: resolvedPath, source: classifyResolvedSource(resolvedPath, env, homedir) };
941
+ } catch {
942
+ resolved = null;
943
+ }
944
+ const installed = [];
945
+ const root = macdriverInstallRoot(homedir);
946
+ if (import_node_fs2.default.existsSync(root)) {
947
+ for (const entry of import_node_fs2.default.readdirSync(root, { withFileTypes: true })) {
948
+ if (!entry.isDirectory()) continue;
949
+ let binaryPath;
950
+ try {
951
+ binaryPath = macdriverInstalledBinary(entry.name, homedir);
952
+ } catch {
953
+ continue;
954
+ }
955
+ if (import_node_fs2.default.existsSync(binaryPath)) {
956
+ installed.push({ version: entry.name, binaryPath });
957
+ }
958
+ }
959
+ installed.sort((a, b) => a.version.localeCompare(b.version));
960
+ }
961
+ const probedVersion = resolved ? await probe(resolved.path) : null;
962
+ return { resolved, pinnedVersion: MACDRIVER_VERSION, installed, probedVersion };
963
+ }
964
+ function classifyResolvedSource(resolvedPath, env, homedir) {
965
+ if (env.PROWL_MACDRIVER_BIN && resolvedPath === env.PROWL_MACDRIVER_BIN) {
966
+ return "env";
967
+ }
968
+ if (resolvedPath.startsWith(macdriverInstallRoot(homedir) + import_node_path3.default.sep)) {
969
+ return "user-install";
970
+ }
971
+ return "source-build";
972
+ }
973
+ function tccGuidance() {
974
+ return "macOS permissions: the app that hosts Prowl (your terminal \u2014 Terminal, iTerm, VS Code, \u2026)\nmust be granted, in System Settings \u2192 Privacy & Security:\n \u2022 Accessibility \u2014 required to drive the target app\n \u2022 Screen Recording \u2014 required for screenshots / visual baselines\nGrant both to the terminal app, not to prowl-macdriver itself, then re-run your hunt.";
975
+ }
976
+
977
+ // src/browser/android-driver.ts
978
+ var import_node_fs3 = __toESM(require("fs"), 1);
609
979
 
610
980
  // src/analyzer/xml.ts
611
981
  var ENTITIES = {
@@ -1097,7 +1467,7 @@ function createAndroidDriver(client, options = {}) {
1097
1467
  },
1098
1468
  async screenshot(screenshotOptions) {
1099
1469
  const png = await client.screenshotPng();
1100
- import_node_fs2.default.writeFileSync(screenshotOptions.path, png);
1470
+ import_node_fs3.default.writeFileSync(screenshotOptions.path, png);
1101
1471
  },
1102
1472
  // network / dialogs / downloads (all web-only) -------------------------
1103
1473
  onResponse(_handler) {
@@ -1123,14 +1493,14 @@ function createAndroidDriver(client, options = {}) {
1123
1493
 
1124
1494
  // src/browser/android-helper.ts
1125
1495
  var import_node_module = require("module");
1126
- var import_node_child_process4 = require("child_process");
1127
- var import_node_fs4 = __toESM(require("fs"), 1);
1128
- var import_node_path3 = __toESM(require("path"), 1);
1496
+ var import_node_child_process5 = require("child_process");
1497
+ var import_node_fs5 = __toESM(require("fs"), 1);
1498
+ var import_node_path5 = __toESM(require("path"), 1);
1129
1499
 
1130
1500
  // src/browser/android-adb.ts
1131
- var import_node_child_process2 = require("child_process");
1501
+ var import_node_child_process3 = require("child_process");
1132
1502
  var execFileAdbRunner = (args, options) => new Promise((resolve) => {
1133
- (0, import_node_child_process2.execFile)(
1503
+ (0, import_node_child_process3.execFile)(
1134
1504
  "adb",
1135
1505
  args,
1136
1506
  { encoding: "utf-8", timeout: options?.timeoutMs, maxBuffer: 16 * 1024 * 1024 },
@@ -1146,7 +1516,7 @@ var execFileAdbRunner = (args, options) => new Promise((resolve) => {
1146
1516
  );
1147
1517
  });
1148
1518
  var spawnAdbProcess = (args) => {
1149
- const child = (0, import_node_child_process2.spawn)("adb", args, { stdio: "ignore" });
1519
+ const child = (0, import_node_child_process3.spawn)("adb", args, { stdio: "ignore" });
1150
1520
  child.on("error", () => {
1151
1521
  });
1152
1522
  return { kill: () => child.kill() };
@@ -1370,12 +1740,12 @@ var Uia2Transport = class {
1370
1740
  * {@link Uia2HttpError} on a non-2xx response, or a timeout error when the
1371
1741
  * per-request deadline elapses.
1372
1742
  */
1373
- async request(method, path18, body, timeoutMs) {
1743
+ async request(method, path20, body, timeoutMs) {
1374
1744
  const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
1375
1745
  const controller = new AbortController();
1376
1746
  const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
1377
1747
  timer.unref?.();
1378
- const url = `${this.baseUrl}${path18}`;
1748
+ const url = `${this.baseUrl}${path20}`;
1379
1749
  let response;
1380
1750
  try {
1381
1751
  response = await this.fetchImpl(url, {
@@ -1387,7 +1757,7 @@ var Uia2Transport = class {
1387
1757
  } catch (error) {
1388
1758
  if (controller.signal.aborted) {
1389
1759
  const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
1390
- throw new Error(`uiautomator2 request ${method} ${path18} timed out after ${shown}`);
1760
+ throw new Error(`uiautomator2 request ${method} ${path20} timed out after ${shown}`);
1391
1761
  }
1392
1762
  throw error instanceof Error ? error : new Error(String(error));
1393
1763
  } finally {
@@ -1398,7 +1768,7 @@ var Uia2Transport = class {
1398
1768
  if (!response.ok) {
1399
1769
  const wdError = extractWebdriverError(parsed);
1400
1770
  throw new Uia2HttpError(
1401
- `uiautomator2 ${method} ${path18} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
1771
+ `uiautomator2 ${method} ${path20} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
1402
1772
  response.status,
1403
1773
  wdError
1404
1774
  );
@@ -1481,10 +1851,10 @@ function sleep(ms) {
1481
1851
  }
1482
1852
  function createUia2AgentClient(transport, sessionId, options = {}) {
1483
1853
  const base = `/session/${sessionId}`;
1484
- async function locate(query, path18) {
1854
+ async function locate(query, path20) {
1485
1855
  return transport.request(
1486
1856
  "POST",
1487
- `${base}${path18}`,
1857
+ `${base}${path20}`,
1488
1858
  androidQueryToLocator(query, { appPackage: options.appPackage })
1489
1859
  );
1490
1860
  }
@@ -1540,9 +1910,9 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
1540
1910
  }
1541
1911
 
1542
1912
  // src/config/target.ts
1543
- var import_node_child_process3 = require("child_process");
1544
- var import_node_fs3 = __toESM(require("fs"), 1);
1545
- var import_node_path2 = __toESM(require("path"), 1);
1913
+ var import_node_child_process4 = require("child_process");
1914
+ var import_node_fs4 = __toESM(require("fs"), 1);
1915
+ var import_node_path4 = __toESM(require("path"), 1);
1546
1916
  var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
1547
1917
  "navigate",
1548
1918
  "waitForUrl",
@@ -1627,7 +1997,7 @@ function looksLikeIosAppPath(app) {
1627
1997
  }
1628
1998
  if (trimmed.toLowerCase().endsWith(".app")) {
1629
1999
  try {
1630
- return import_node_fs3.default.statSync(normalizeAppPath(app)).isDirectory();
2000
+ return import_node_fs4.default.statSync(normalizeAppPath(app)).isDirectory();
1631
2001
  } catch {
1632
2002
  return false;
1633
2003
  }
@@ -1635,29 +2005,29 @@ function looksLikeIosAppPath(app) {
1635
2005
  return false;
1636
2006
  }
1637
2007
  function normalizeAppPath(app) {
1638
- return import_node_path2.default.resolve(trimTrailingPathSeparators(app));
2008
+ return import_node_path4.default.resolve(trimTrailingPathSeparators(app));
1639
2009
  }
1640
2010
  function parseBundleIdentifier(plist) {
1641
2011
  const match = /<key>\s*CFBundleIdentifier\s*<\/key>\s*<string>\s*([^<]+?)\s*<\/string>/s.exec(plist);
1642
2012
  return match?.[1]?.trim() || null;
1643
2013
  }
1644
2014
  function readBundleIdentifier(appPath, ...plistSubPath) {
1645
- const infoPlistPath = import_node_path2.default.join(normalizeAppPath(appPath), ...plistSubPath);
1646
- if (!import_node_fs3.default.existsSync(infoPlistPath)) {
2015
+ const infoPlistPath = import_node_path4.default.join(normalizeAppPath(appPath), ...plistSubPath);
2016
+ if (!import_node_fs4.default.existsSync(infoPlistPath)) {
1647
2017
  return null;
1648
2018
  }
1649
2019
  try {
1650
- const parsed = parseBundleIdentifier(import_node_fs3.default.readFileSync(infoPlistPath, "utf-8"));
2020
+ const parsed = parseBundleIdentifier(import_node_fs4.default.readFileSync(infoPlistPath, "utf-8"));
1651
2021
  if (parsed) {
1652
2022
  return parsed;
1653
2023
  }
1654
2024
  } catch {
1655
2025
  }
1656
- if (process.platform !== "darwin" || !import_node_fs3.default.existsSync("/usr/bin/plutil")) {
2026
+ if (process.platform !== "darwin" || !import_node_fs4.default.existsSync("/usr/bin/plutil")) {
1657
2027
  return null;
1658
2028
  }
1659
2029
  try {
1660
- const output = (0, import_node_child_process3.execFileSync)(
2030
+ const output = (0, import_node_child_process4.execFileSync)(
1661
2031
  "/usr/bin/plutil",
1662
2032
  ["-extract", "CFBundleIdentifier", "raw", "-o", "-", infoPlistPath],
1663
2033
  { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }
@@ -1679,7 +2049,7 @@ function appBundleAllowedIdentities(app, readBundleId, isAppPath = looksLikeAppB
1679
2049
  const normalizedPath = normalizeAppPath(app);
1680
2050
  identities.add(trimTrailingPathSeparators(app));
1681
2051
  identities.add(normalizedPath);
1682
- const bundleName = import_node_path2.default.basename(normalizedPath).replace(/\.app$/i, "");
2052
+ const bundleName = import_node_path4.default.basename(normalizedPath).replace(/\.app$/i, "");
1683
2053
  if (bundleName) {
1684
2054
  identities.add(bundleName);
1685
2055
  }
@@ -1707,9 +2077,9 @@ function looksLikeApkPath(app) {
1707
2077
  return trimmed.includes("/") || trimmed.includes("\\") || trimmed.toLowerCase().endsWith(".apk");
1708
2078
  }
1709
2079
  function normalizeAndroidApkPath(app) {
1710
- const resolved = import_node_path2.default.resolve(trimTrailingPathSeparators(app));
2080
+ const resolved = import_node_path4.default.resolve(trimTrailingPathSeparators(app));
1711
2081
  try {
1712
- return import_node_fs3.default.realpathSync.native(resolved);
2082
+ return import_node_fs4.default.realpathSync.native(resolved);
1713
2083
  } catch {
1714
2084
  return resolved;
1715
2085
  }
@@ -1752,12 +2122,12 @@ function resolveAgentApks(requireFn = (0, import_node_module.createRequire)(impo
1752
2122
  "The Android target requires the `appium-uiautomator2-server` package (its prebuilt APKs). It is an optional dependency of prowl-tools; it may have been skipped (--omit=optional) or failed to install. Restore it for a global Prowl install with: npm install -g appium-uiautomator2-server@10.6.2. If Prowl is installed locally in a project, run: npm install appium-uiautomator2-server@10.6.2"
1753
2123
  );
1754
2124
  }
1755
- const pkgDir = import_node_path3.default.dirname(pkgJsonPath);
2125
+ const pkgDir = import_node_path5.default.dirname(pkgJsonPath);
1756
2126
  const version = requireFn(pkgJsonPath).version;
1757
- const serverApk = import_node_path3.default.join(pkgDir, "apks", `appium-uiautomator2-server-v${version}.apk`);
1758
- const testApk = import_node_path3.default.join(pkgDir, "apks", "appium-uiautomator2-server-debug-androidTest.apk");
2127
+ const serverApk = import_node_path5.default.join(pkgDir, "apks", `appium-uiautomator2-server-v${version}.apk`);
2128
+ const testApk = import_node_path5.default.join(pkgDir, "apks", "appium-uiautomator2-server-debug-androidTest.apk");
1759
2129
  for (const apk of [serverApk, testApk]) {
1760
- if (!import_node_fs4.default.existsSync(apk)) {
2130
+ if (!import_node_fs5.default.existsSync(apk)) {
1761
2131
  throw new Error(`Expected uiautomator2 agent APK is missing: ${apk}. Reinstall dependencies.`);
1762
2132
  }
1763
2133
  }
@@ -1769,7 +2139,7 @@ function looksLikeApk(app) {
1769
2139
  var execFileAaptResolver = async (apkPath) => {
1770
2140
  for (const tool of ["aapt", "aapt2"]) {
1771
2141
  const output = await new Promise((resolve) => {
1772
- (0, import_node_child_process4.execFile)(
2142
+ (0, import_node_child_process5.execFile)(
1773
2143
  tool,
1774
2144
  ["dump", "badging", apkPath],
1775
2145
  { encoding: "utf-8", timeout: 2e4, maxBuffer: 8 * 1024 * 1024 },
@@ -1803,8 +2173,8 @@ async function resolvePackage(app, runner, serial, aaptResolver, allowedApps) {
1803
2173
  assertAndroidAppAllowed(allowedApps, app);
1804
2174
  return app;
1805
2175
  }
1806
- const apkPath = import_node_path3.default.resolve(app);
1807
- if (!import_node_fs4.default.existsSync(apkPath)) {
2176
+ const apkPath = import_node_path5.default.resolve(app);
2177
+ if (!import_node_fs5.default.existsSync(apkPath)) {
1808
2178
  throw new Error(`APK not found: ${apkPath}`);
1809
2179
  }
1810
2180
  const pkg = await aaptResolver(apkPath);
@@ -2095,19 +2465,19 @@ function createIosDriver(client, options) {
2095
2465
 
2096
2466
  // src/browser/ios-helper.ts
2097
2467
  var import_node_module2 = require("module");
2098
- var import_node_child_process6 = require("child_process");
2099
- var import_node_fs5 = __toESM(require("fs"), 1);
2100
- var import_node_os2 = __toESM(require("os"), 1);
2101
- var import_node_path5 = __toESM(require("path"), 1);
2468
+ var import_node_child_process7 = require("child_process");
2469
+ var import_node_fs6 = __toESM(require("fs"), 1);
2470
+ var import_node_os5 = __toESM(require("os"), 1);
2471
+ var import_node_path7 = __toESM(require("path"), 1);
2102
2472
 
2103
2473
  // src/browser/ios-simctl.ts
2104
- var import_node_child_process5 = require("child_process");
2474
+ var import_node_child_process6 = require("child_process");
2105
2475
  var import_promises = require("fs/promises");
2106
2476
  var import_node_net = __toESM(require("net"), 1);
2107
- var import_node_os = __toESM(require("os"), 1);
2108
- var import_node_path4 = __toESM(require("path"), 1);
2477
+ var import_node_os4 = __toESM(require("os"), 1);
2478
+ var import_node_path6 = __toESM(require("path"), 1);
2109
2479
  var spawnXcrunProcess = (args, options) => {
2110
- const child = (0, import_node_child_process5.spawn)("xcrun", args, {
2480
+ const child = (0, import_node_child_process6.spawn)("xcrun", args, {
2111
2481
  stdio: "ignore",
2112
2482
  env: options?.env ? { ...process.env, ...options.env } : process.env
2113
2483
  });
@@ -2119,10 +2489,10 @@ var spawnXcrunProcess = (args, options) => {
2119
2489
  }
2120
2490
  };
2121
2491
  };
2122
- var DEFAULT_SIMULATOR_LOCK_ROOT = import_node_path4.default.join(import_node_os.default.tmpdir(), "prowl-ios-simulator-locks");
2492
+ var DEFAULT_SIMULATOR_LOCK_ROOT = import_node_path6.default.join(import_node_os4.default.tmpdir(), "prowl-ios-simulator-locks");
2123
2493
  var SIMULATOR_LOCK_OWNER_FILE = "owner.json";
2124
2494
  var execFileXcrunRunner = (args, options) => new Promise((resolve) => {
2125
- (0, import_node_child_process5.execFile)(
2495
+ (0, import_node_child_process6.execFile)(
2126
2496
  "xcrun",
2127
2497
  args,
2128
2498
  {
@@ -2159,7 +2529,7 @@ function isProcessAlive(pid) {
2159
2529
  }
2160
2530
  async function removeStaleSimulatorLock(lockPath) {
2161
2531
  try {
2162
- const ownerText = await (0, import_promises.readFile)(import_node_path4.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE), "utf8");
2532
+ const ownerText = await (0, import_promises.readFile)(import_node_path6.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE), "utf8");
2163
2533
  const owner = JSON.parse(ownerText);
2164
2534
  if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0 && !isProcessAlive(owner.pid)) {
2165
2535
  await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
@@ -2177,7 +2547,7 @@ function simulatorReservedError(udid) {
2177
2547
  }
2178
2548
  async function reserveSimulatorUdid(udid, options = {}) {
2179
2549
  const lockRoot = options.lockRoot ?? DEFAULT_SIMULATOR_LOCK_ROOT;
2180
- const lockPath = import_node_path4.default.join(lockRoot, simulatorLockName(udid));
2550
+ const lockPath = import_node_path6.default.join(lockRoot, simulatorLockName(udid));
2181
2551
  await (0, import_promises.mkdir)(lockRoot, { recursive: true });
2182
2552
  for (let attempt = 0; attempt < 2; attempt += 1) {
2183
2553
  try {
@@ -2201,7 +2571,7 @@ async function reserveSimulatorUdid(udid, options = {}) {
2201
2571
  };
2202
2572
  try {
2203
2573
  await (0, import_promises.writeFile)(
2204
- import_node_path4.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE),
2574
+ import_node_path6.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE),
2205
2575
  `${JSON.stringify({ pid: process.pid, udid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
2206
2576
  `,
2207
2577
  { flag: "wx" }
@@ -2393,15 +2763,15 @@ var WdaTransport = class {
2393
2763
  * {@link WdaHttpError} on a non-2xx response, or a timeout error when the
2394
2764
  * per-request deadline elapses.
2395
2765
  */
2396
- async requestFull(method, path18, body, timeoutMs) {
2766
+ async requestFull(method, path20, body, timeoutMs) {
2397
2767
  const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
2398
2768
  const controller = new AbortController();
2399
2769
  const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
2400
2770
  timer.unref?.();
2401
- const url = `${this.baseUrl}${path18}`;
2771
+ const url = `${this.baseUrl}${path20}`;
2402
2772
  const timeoutError = () => {
2403
2773
  const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
2404
- return new Error(`WebDriverAgent request ${method} ${path18} timed out after ${shown}`);
2774
+ return new Error(`WebDriverAgent request ${method} ${path20} timed out after ${shown}`);
2405
2775
  };
2406
2776
  let response;
2407
2777
  try {
@@ -2433,7 +2803,7 @@ var WdaTransport = class {
2433
2803
  if (!response.ok) {
2434
2804
  const wdError = extractWebdriverError2(parsed);
2435
2805
  throw new WdaHttpError(
2436
- `WebDriverAgent ${method} ${path18} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
2806
+ `WebDriverAgent ${method} ${path20} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
2437
2807
  response.status,
2438
2808
  wdError
2439
2809
  );
@@ -2441,8 +2811,8 @@ var WdaTransport = class {
2441
2811
  return parsed;
2442
2812
  }
2443
2813
  /** Like {@link requestFull} but returns just the `value` field. */
2444
- async request(method, path18, body, timeoutMs) {
2445
- const parsed = await this.requestFull(method, path18, body, timeoutMs);
2814
+ async request(method, path20, body, timeoutMs) {
2815
+ const parsed = await this.requestFull(method, path20, body, timeoutMs);
2446
2816
  return parsed?.value;
2447
2817
  }
2448
2818
  };
@@ -2518,8 +2888,8 @@ function sleep2(ms) {
2518
2888
  }
2519
2889
  function createWdaAgentClient(transport, sessionId) {
2520
2890
  const base = `/session/${sessionId}`;
2521
- async function locate(query, path18) {
2522
- return transport.request("POST", `${base}${path18}`, iosQueryToLocator(query));
2891
+ async function locate(query, path20) {
2892
+ return transport.request("POST", `${base}${path20}`, iosQueryToLocator(query));
2523
2893
  }
2524
2894
  return {
2525
2895
  async findElement(query) {
@@ -2595,41 +2965,41 @@ function resolveWdaProject(requireFn = (0, import_node_module2.createRequire)(im
2595
2965
  "The iOS target requires the `appium-webdriveragent` package (its WDA Xcode project). It is an optional dependency of prowl-tools; it may have been skipped (--omit=optional) or failed to install. Restore it for a global Prowl install with: npm install -g appium-webdriveragent@16.4.0. If Prowl is installed locally in a project, run: npm install appium-webdriveragent@16.4.0"
2596
2966
  );
2597
2967
  }
2598
- const pkgDir = import_node_path5.default.dirname(pkgJsonPath);
2968
+ const pkgDir = import_node_path7.default.dirname(pkgJsonPath);
2599
2969
  const version = requireFn(pkgJsonPath).version;
2600
- const projectPath = import_node_path5.default.join(pkgDir, "WebDriverAgent.xcodeproj");
2601
- if (!import_node_fs5.default.existsSync(projectPath)) {
2970
+ const projectPath = import_node_path7.default.join(pkgDir, "WebDriverAgent.xcodeproj");
2971
+ if (!import_node_fs6.default.existsSync(projectPath)) {
2602
2972
  throw new Error(`Expected WebDriverAgent project is missing: ${projectPath}. Reinstall dependencies.`);
2603
2973
  }
2604
2974
  return { projectPath, version };
2605
2975
  }
2606
- function wdaCacheDir(wdaVersion, xcode, homeDir = import_node_os2.default.homedir()) {
2607
- return import_node_path5.default.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
2976
+ function wdaCacheDir(wdaVersion, xcode, homeDir = import_node_os5.default.homedir()) {
2977
+ return import_node_path7.default.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
2608
2978
  }
2609
2979
  function productsDir(derivedDataPath) {
2610
- return import_node_path5.default.join(derivedDataPath, "Build", "Products");
2980
+ return import_node_path7.default.join(derivedDataPath, "Build", "Products");
2611
2981
  }
2612
2982
  function findXctestrunIn(dir) {
2613
2983
  let entries;
2614
2984
  try {
2615
- entries = import_node_fs5.default.readdirSync(dir);
2985
+ entries = import_node_fs6.default.readdirSync(dir);
2616
2986
  } catch {
2617
2987
  return null;
2618
2988
  }
2619
2989
  const match = entries.filter((name) => name.endsWith(".xctestrun") && !name.startsWith(PREPARED_XCTESTRUN_PREFIX)).sort()[0];
2620
- return match ? import_node_path5.default.join(dir, match) : null;
2990
+ return match ? import_node_path7.default.join(dir, match) : null;
2621
2991
  }
2622
2992
  function resolveOverrideXctestrun(override) {
2623
- if (!import_node_fs5.default.existsSync(override)) {
2993
+ if (!import_node_fs6.default.existsSync(override)) {
2624
2994
  throw new Error(`PROWL_WDA_RUNNER points at a missing path: ${override}`);
2625
2995
  }
2626
2996
  if (override.endsWith(".xctestrun")) {
2627
2997
  return override;
2628
2998
  }
2629
2999
  const candidates = [];
2630
- const stat = import_node_fs5.default.statSync(override);
3000
+ const stat = import_node_fs6.default.statSync(override);
2631
3001
  if (override.endsWith(".app")) {
2632
- candidates.push(import_node_path5.default.dirname(import_node_path5.default.dirname(override)));
3002
+ candidates.push(import_node_path7.default.dirname(import_node_path7.default.dirname(override)));
2633
3003
  } else if (stat.isDirectory()) {
2634
3004
  candidates.push(override, productsDir(override));
2635
3005
  }
@@ -2646,7 +3016,7 @@ function resolveOverrideXctestrun(override) {
2646
3016
  async function resolveWdaTestRun(options = {}) {
2647
3017
  const runner = options.runner ?? execFileXcrunRunner;
2648
3018
  const env = options.env ?? process.env;
2649
- const homeDir = options.homeDir ?? import_node_os2.default.homedir();
3019
+ const homeDir = options.homeDir ?? import_node_os5.default.homedir();
2650
3020
  const log = options.logger ?? ((message) => process.stderr.write(`${message}
2651
3021
  `));
2652
3022
  const override = env.PROWL_WDA_RUNNER;
@@ -2668,7 +3038,7 @@ async function resolveWdaTestRun(options = {}) {
2668
3038
  log(
2669
3039
  `Prowl: building WebDriverAgent for the iOS target (first run only; this can take a few minutes)\u2026`
2670
3040
  );
2671
- import_node_fs5.default.mkdirSync(cacheDir, { recursive: true });
3041
+ import_node_fs6.default.mkdirSync(cacheDir, { recursive: true });
2672
3042
  const result = await runner(
2673
3043
  [
2674
3044
  "xcodebuild",
@@ -2734,7 +3104,7 @@ function injectUsePortIntoXctestrun(plist, port) {
2734
3104
  }
2735
3105
  function runPlutil(args) {
2736
3106
  return new Promise((resolve, reject) => {
2737
- (0, import_node_child_process6.execFile)(
3107
+ (0, import_node_child_process7.execFile)(
2738
3108
  "plutil",
2739
3109
  args,
2740
3110
  { encoding: "utf-8", maxBuffer: 32 * 1024 * 1024 },
@@ -2762,13 +3132,13 @@ var defaultWdaTestRunPreparer = async ({ xctestrunPath, port }) => {
2762
3132
  }
2763
3133
  const injected = injectUsePortIntoXctestrun(parsed, port);
2764
3134
  const stem = `${PREPARED_XCTESTRUN_PREFIX}${port}-${process.pid}`;
2765
- const outPath = import_node_path5.default.join(import_node_path5.default.dirname(xctestrunPath), `${stem}.xctestrun`);
2766
- const jsonPath = import_node_path5.default.join(import_node_os2.default.tmpdir(), `${stem}.json`);
2767
- import_node_fs5.default.writeFileSync(jsonPath, JSON.stringify(injected));
3135
+ const outPath = import_node_path7.default.join(import_node_path7.default.dirname(xctestrunPath), `${stem}.xctestrun`);
3136
+ const jsonPath = import_node_path7.default.join(import_node_os5.default.tmpdir(), `${stem}.json`);
3137
+ import_node_fs6.default.writeFileSync(jsonPath, JSON.stringify(injected));
2768
3138
  try {
2769
3139
  await runPlutil(["-convert", "xml1", jsonPath, "-o", outPath]);
2770
3140
  } finally {
2771
- import_node_fs5.default.rmSync(jsonPath, { force: true });
3141
+ import_node_fs6.default.rmSync(jsonPath, { force: true });
2772
3142
  }
2773
3143
  return outPath;
2774
3144
  };
@@ -2776,10 +3146,10 @@ function cleanupPreparedTestRun(preparedPath) {
2776
3146
  if (!preparedPath) {
2777
3147
  return;
2778
3148
  }
2779
- if (!import_node_path5.default.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
3149
+ if (!import_node_path7.default.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
2780
3150
  return;
2781
3151
  }
2782
- import_node_fs5.default.rmSync(preparedPath, { force: true });
3152
+ import_node_fs6.default.rmSync(preparedPath, { force: true });
2783
3153
  }
2784
3154
  function wdaTestRunArgs(xctestrunPath, udid) {
2785
3155
  return [
@@ -2801,8 +3171,8 @@ async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
2801
3171
  }
2802
3172
  return app;
2803
3173
  }
2804
- const appPath = import_node_path5.default.resolve(app);
2805
- if (!import_node_fs5.default.existsSync(appPath)) {
3174
+ const appPath = import_node_path7.default.resolve(app);
3175
+ if (!import_node_fs6.default.existsSync(appPath)) {
2806
3176
  throw new Error(`.app bundle not found: ${appPath}`);
2807
3177
  }
2808
3178
  const bundleId = readIosBundleIdentifier(appPath);
@@ -2905,12 +3275,12 @@ async function closeIosSession(session) {
2905
3275
  }
2906
3276
 
2907
3277
  // src/runner/index.ts
2908
- var import_node_fs14 = __toESM(require("fs"), 1);
2909
- var import_node_path14 = __toESM(require("path"), 1);
3278
+ var import_node_fs15 = __toESM(require("fs"), 1);
3279
+ var import_node_path16 = __toESM(require("path"), 1);
2910
3280
 
2911
3281
  // src/config/loader.ts
2912
- var import_node_fs6 = __toESM(require("fs"), 1);
2913
- var import_node_path6 = __toESM(require("path"), 1);
3282
+ var import_node_fs7 = __toESM(require("fs"), 1);
3283
+ var import_node_path8 = __toESM(require("path"), 1);
2914
3284
  var import_yaml = __toESM(require("yaml"), 1);
2915
3285
  var import_dotenv = __toESM(require("dotenv"), 1);
2916
3286
 
@@ -2928,7 +3298,7 @@ function isValidHuntName(name) {
2928
3298
  function assertValidHuntName(name) {
2929
3299
  if (!isValidHuntName(name)) {
2930
3300
  throw new Error(
2931
- `Invalid hunt name: "${name}". Use only letters, numbers, hyphens, underscores, and forward slashes.`
3301
+ `Invalid hunt name: "${name}". A hunt name is its file name under .prowl/hunts/ (letters, numbers, hyphens, underscores, and forward slashes for nested hunts). Accepted forms: a bare name (homepage), a nested name (admin/users), or a path (.prowl/hunts/homepage.yml or hunts/homepage.yml).`
2932
3302
  );
2933
3303
  }
2934
3304
  }
@@ -3305,12 +3675,12 @@ function findConfigPath(startDir) {
3305
3675
  let current = startDir;
3306
3676
  while (current) {
3307
3677
  for (const dir of [CONFIG_DIR, LEGACY_CONFIG_DIR]) {
3308
- const candidate = import_node_path6.default.join(current, dir, "config.yml");
3309
- if (import_node_fs6.default.existsSync(candidate)) {
3678
+ const candidate = import_node_path8.default.join(current, dir, "config.yml");
3679
+ if (import_node_fs7.default.existsSync(candidate)) {
3310
3680
  return candidate;
3311
3681
  }
3312
3682
  }
3313
- const parent = import_node_path6.default.dirname(current);
3683
+ const parent = import_node_path8.default.dirname(current);
3314
3684
  if (parent === current) {
3315
3685
  break;
3316
3686
  }
@@ -3417,19 +3787,19 @@ function ensureAllowedDomain(allowed, urlValue) {
3417
3787
  return allowed;
3418
3788
  }
3419
3789
  function loadConfig(configPath) {
3420
- const resolvedPath = configPath ? import_node_path6.default.resolve(configPath) : findConfigPath(process.cwd());
3790
+ const resolvedPath = configPath ? import_node_path8.default.resolve(configPath) : findConfigPath(process.cwd());
3421
3791
  if (!resolvedPath) {
3422
3792
  throw new Error("Could not find .prowl/config.yml. Run `prowl init` first.");
3423
3793
  }
3424
- if (!import_node_fs6.default.existsSync(resolvedPath)) {
3794
+ if (!import_node_fs7.default.existsSync(resolvedPath)) {
3425
3795
  throw new Error(`Config file not found at ${resolvedPath}`);
3426
3796
  }
3427
- const configDir = import_node_path6.default.dirname(resolvedPath);
3428
- if (import_node_path6.default.basename(configDir) === LEGACY_CONFIG_DIR) {
3797
+ const configDir = import_node_path8.default.dirname(resolvedPath);
3798
+ if (import_node_path8.default.basename(configDir) === LEGACY_CONFIG_DIR) {
3429
3799
  warnLegacyConfigDir();
3430
3800
  }
3431
- import_dotenv.default.config({ path: import_node_path6.default.join(configDir, ".env"), override: false });
3432
- const raw = import_node_fs6.default.readFileSync(resolvedPath, "utf-8");
3801
+ import_dotenv.default.config({ path: import_node_path8.default.join(configDir, ".env"), override: false });
3802
+ const raw = import_node_fs7.default.readFileSync(resolvedPath, "utf-8");
3433
3803
  const parsed = import_yaml.default.parse(raw) ?? {};
3434
3804
  const validated = configSchema.parse(parsed);
3435
3805
  const config = mergeConfig(validated);
@@ -3443,32 +3813,32 @@ function loadConfig(configPath) {
3443
3813
  }
3444
3814
  function loadHunt(huntName, configDir) {
3445
3815
  assertValidHuntName(huntName);
3446
- const huntPath = import_node_path6.default.join(configDir, "hunts", `${huntName}.yml`);
3447
- if (!import_node_fs6.default.existsSync(huntPath)) {
3816
+ const huntPath = import_node_path8.default.join(configDir, "hunts", `${huntName}.yml`);
3817
+ if (!import_node_fs7.default.existsSync(huntPath)) {
3448
3818
  throw new Error(`Hunt file not found: ${huntPath}`);
3449
3819
  }
3450
- const raw = import_node_fs6.default.readFileSync(huntPath, "utf-8");
3820
+ const raw = import_node_fs7.default.readFileSync(huntPath, "utf-8");
3451
3821
  const parsed = import_yaml.default.parse(raw) ?? {};
3452
3822
  const validated = huntSchema.parse(parsed);
3453
3823
  return validated;
3454
3824
  }
3455
3825
  function loadHuntTags(huntName, configDir) {
3456
3826
  assertValidHuntName(huntName);
3457
- const huntPath = import_node_path6.default.join(configDir, "hunts", `${huntName}.yml`);
3458
- if (!import_node_fs6.default.existsSync(huntPath)) {
3827
+ const huntPath = import_node_path8.default.join(configDir, "hunts", `${huntName}.yml`);
3828
+ if (!import_node_fs7.default.existsSync(huntPath)) {
3459
3829
  return [];
3460
3830
  }
3461
- const raw = import_node_fs6.default.readFileSync(huntPath, "utf-8");
3831
+ const raw = import_node_fs7.default.readFileSync(huntPath, "utf-8");
3462
3832
  const parsed = import_yaml.default.parse(raw) ?? {};
3463
3833
  return Array.isArray(parsed.tags) ? parsed.tags : [];
3464
3834
  }
3465
3835
  function loadHuntMeta(huntName, configDir) {
3466
3836
  assertValidHuntName(huntName);
3467
- const huntPath = import_node_path6.default.join(configDir, "hunts", `${huntName}.yml`);
3468
- if (!import_node_fs6.default.existsSync(huntPath)) {
3837
+ const huntPath = import_node_path8.default.join(configDir, "hunts", `${huntName}.yml`);
3838
+ if (!import_node_fs7.default.existsSync(huntPath)) {
3469
3839
  return { tags: [] };
3470
3840
  }
3471
- const raw = import_node_fs6.default.readFileSync(huntPath, "utf-8");
3841
+ const raw = import_node_fs7.default.readFileSync(huntPath, "utf-8");
3472
3842
  const parsed = import_yaml.default.parse(raw) ?? {};
3473
3843
  return {
3474
3844
  description: typeof parsed.description === "string" ? parsed.description : void 0,
@@ -3476,24 +3846,24 @@ function loadHuntMeta(huntName, configDir) {
3476
3846
  };
3477
3847
  }
3478
3848
  function listHunts(configDir) {
3479
- const huntsDir = import_node_path6.default.join(configDir, "hunts");
3480
- if (!import_node_fs6.default.existsSync(huntsDir)) {
3849
+ const huntsDir = import_node_path8.default.join(configDir, "hunts");
3850
+ if (!import_node_fs7.default.existsSync(huntsDir)) {
3481
3851
  return [];
3482
3852
  }
3483
- const stats = import_node_fs6.default.statSync(huntsDir);
3853
+ const stats = import_node_fs7.default.statSync(huntsDir);
3484
3854
  if (!stats.isDirectory()) {
3485
3855
  throw new Error(`Hunts path is not a directory: ${huntsDir}`);
3486
3856
  }
3487
3857
  const results = [];
3488
3858
  function scanDir(dir) {
3489
- const entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
3859
+ const entries = import_node_fs7.default.readdirSync(dir, { withFileTypes: true });
3490
3860
  for (const entry of entries) {
3491
3861
  if (entry.isFile() && entry.name.endsWith(".yml")) {
3492
- const fullPath = import_node_path6.default.join(dir, entry.name);
3493
- const relative = import_node_path6.default.relative(huntsDir, fullPath);
3862
+ const fullPath = import_node_path8.default.join(dir, entry.name);
3863
+ const relative = import_node_path8.default.relative(huntsDir, fullPath);
3494
3864
  results.push(relative.replace(/\.yml$/, ""));
3495
3865
  } else if (entry.isDirectory()) {
3496
- scanDir(import_node_path6.default.join(dir, entry.name));
3866
+ scanDir(import_node_path8.default.join(dir, entry.name));
3497
3867
  }
3498
3868
  }
3499
3869
  }
@@ -3502,7 +3872,7 @@ function listHunts(configDir) {
3502
3872
  }
3503
3873
 
3504
3874
  // src/config/interpolate.ts
3505
- var import_node_crypto = __toESM(require("crypto"), 1);
3875
+ var import_node_crypto2 = __toESM(require("crypto"), 1);
3506
3876
  var VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
3507
3877
  function collectInterpolatedValues(input, vars, values) {
3508
3878
  if (typeof input === "string") {
@@ -3541,8 +3911,8 @@ var RANDOM_FIRST_NAMES = ["Alex", "Jordan", "Morgan", "Taylor", "Casey", "Riley"
3541
3911
  var RANDOM_LAST_NAMES = ["Smith", "Johnson", "Brown", "Davis", "Wilson", "Clark", "Hall", "Young"];
3542
3912
  function generateRandomVars(randomSource) {
3543
3913
  const random = randomSource?.random ?? Math.random;
3544
- const randomBytes = randomSource?.randomBytes ?? import_node_crypto.default.randomBytes;
3545
- const randomUUID = randomSource?.randomUUID ?? import_node_crypto.default.randomUUID;
3914
+ const randomBytes = randomSource?.randomBytes ?? import_node_crypto2.default.randomBytes;
3915
+ const randomUUID = randomSource?.randomUUID ?? import_node_crypto2.default.randomUUID;
3546
3916
  const hex = randomBytes(4).toString("hex");
3547
3917
  const firstIndex = Math.floor(random() * RANDOM_FIRST_NAMES.length);
3548
3918
  const lastIndex = Math.floor(random() * RANDOM_LAST_NAMES.length);
@@ -3873,8 +4243,8 @@ function interpolateHunt(hunt, env, randomVars = generateRandomVars()) {
3873
4243
  }
3874
4244
 
3875
4245
  // src/browser/playwright-driver.ts
3876
- var import_node_fs7 = __toESM(require("fs"), 1);
3877
- var import_node_path7 = __toESM(require("path"), 1);
4246
+ var import_node_fs8 = __toESM(require("fs"), 1);
4247
+ var import_node_path9 = __toESM(require("path"), 1);
3878
4248
  var import_playwright = require("playwright");
3879
4249
  var ENGINES = { chromium: import_playwright.chromium, firefox: import_playwright.firefox, webkit: import_playwright.webkit };
3880
4250
  async function launchBrowser(options) {
@@ -3896,14 +4266,14 @@ async function launchBrowser(options) {
3896
4266
  contextOptions.viewport = options.viewport;
3897
4267
  }
3898
4268
  if (options.storageStatePath) {
3899
- if (import_node_fs7.default.existsSync(options.storageStatePath)) {
4269
+ if (import_node_fs8.default.existsSync(options.storageStatePath)) {
3900
4270
  contextOptions.storageState = options.storageStatePath;
3901
4271
  } else {
3902
4272
  console.warn(`Auth state file not found: ${options.storageStatePath}. Run "prowl login" to create it.`);
3903
4273
  }
3904
4274
  }
3905
4275
  if (options.recordHar) {
3906
- contextOptions.recordHar = { path: import_node_path7.default.join(options.runDir, "network.har") };
4276
+ contextOptions.recordHar = { path: import_node_path9.default.join(options.runDir, "network.har") };
3907
4277
  }
3908
4278
  const context = await browser.newContext(contextOptions);
3909
4279
  const page = await context.newPage();
@@ -3911,7 +4281,7 @@ async function launchBrowser(options) {
3911
4281
  page.setDefaultNavigationTimeout(options.timeout);
3912
4282
  let tracePath;
3913
4283
  if (options.trace) {
3914
- tracePath = import_node_path7.default.join(options.runDir, "trace.zip");
4284
+ tracePath = import_node_path9.default.join(options.runDir, "trace.zip");
3915
4285
  await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
3916
4286
  }
3917
4287
  return { browser, context, page, tracePath };
@@ -4086,8 +4456,8 @@ function createPlaywrightDriver(page) {
4086
4456
  }
4087
4457
 
4088
4458
  // src/runner/steps.ts
4089
- var import_node_fs9 = __toESM(require("fs"), 1);
4090
- var import_node_path9 = __toESM(require("path"), 1);
4459
+ var import_node_fs10 = __toESM(require("fs"), 1);
4460
+ var import_node_path11 = __toESM(require("path"), 1);
4091
4461
 
4092
4462
  // src/runner/healing.ts
4093
4463
  var INTERACTIVE_TAGS = ["button", "a", "input", "select", "textarea"];
@@ -4821,7 +5191,7 @@ async function runInlineAssert(driver, policy, assertion) {
4821
5191
  throw new Error("assert step is missing an assertion type");
4822
5192
  }
4823
5193
  function screenshotPath(screenshotsDir, fileName) {
4824
- return import_node_path9.default.join(screenshotsDir, fileName);
5194
+ return import_node_path11.default.join(screenshotsDir, fileName);
4825
5195
  }
4826
5196
  function stepPath(prefix, index) {
4827
5197
  return prefix ? `${prefix}.${index}` : `${index}`;
@@ -4838,7 +5208,7 @@ function validateDownloadFilename(suggestedFilename) {
4838
5208
  const safeFilename = suggestedFilename.trim();
4839
5209
  const allowedFilenamePattern = /^[^<>:"/\\|?*]+$/;
4840
5210
  const hasControlCharacter = Array.from(safeFilename).some((char) => char.charCodeAt(0) < 32);
4841
- if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !== import_node_path9.default.basename(safeFilename) || safeFilename.includes("..") || /[/\\]/.test(safeFilename) || hasControlCharacter || !allowedFilenamePattern.test(safeFilename)) {
5211
+ if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !== import_node_path11.default.basename(safeFilename) || safeFilename.includes("..") || /[/\\]/.test(safeFilename) || hasControlCharacter || !allowedFilenamePattern.test(safeFilename)) {
4842
5212
  throw new Error(`Invalid download filename: "${suggestedFilename}"`);
4843
5213
  }
4844
5214
  return safeFilename;
@@ -5018,7 +5388,7 @@ var STEP_HANDLERS = {
5018
5388
  if (!("setInputFiles" in h.step)) unknownStep();
5019
5389
  const resolvedInput = await h.policy.resolveActionSelector(h.step.setInputFiles.selector);
5020
5390
  const rawFiles = h.step.setInputFiles.files;
5021
- const resolveFile = (f) => import_node_path9.default.isAbsolute(f) ? f : import_node_path9.default.join(h.context.configDir, f);
5391
+ const resolveFile = (f) => import_node_path11.default.isAbsolute(f) ? f : import_node_path11.default.join(h.context.configDir, f);
5022
5392
  const resolvedFiles = Array.isArray(rawFiles) ? rawFiles.map(resolveFile) : resolveFile(rawFiles);
5023
5393
  await h.driver.setInputFiles(resolvedInput.selector, resolvedFiles);
5024
5394
  h.policy.ensureLocationAllowed(h.driver);
@@ -5380,15 +5750,15 @@ var STEP_HANDLERS = {
5380
5750
  if (!responseFile) {
5381
5751
  throw new Error("mock.response must include either body or file");
5382
5752
  }
5383
- const candidateFilePath = import_node_path9.default.isAbsolute(responseFile) ? responseFile : import_node_path9.default.join(h.context.configDir, responseFile);
5384
- const resolvedConfigDir = import_node_path9.default.resolve(h.context.configDir);
5385
- const resolvedFilePath = import_node_path9.default.resolve(candidateFilePath);
5386
- const relativePath = import_node_path9.default.relative(resolvedConfigDir, resolvedFilePath);
5387
- const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${import_node_path9.default.sep}`) && !import_node_path9.default.isAbsolute(relativePath);
5753
+ const candidateFilePath = import_node_path11.default.isAbsolute(responseFile) ? responseFile : import_node_path11.default.join(h.context.configDir, responseFile);
5754
+ const resolvedConfigDir = import_node_path11.default.resolve(h.context.configDir);
5755
+ const resolvedFilePath = import_node_path11.default.resolve(candidateFilePath);
5756
+ const relativePath = import_node_path11.default.relative(resolvedConfigDir, resolvedFilePath);
5757
+ const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${import_node_path11.default.sep}`) && !import_node_path11.default.isAbsolute(relativePath);
5388
5758
  if (!isWithinConfigDir) {
5389
5759
  throw new Error("mock.response.file must resolve within config directory");
5390
5760
  }
5391
- responseBody = await import_node_fs9.default.promises.readFile(resolvedFilePath, "utf-8");
5761
+ responseBody = await import_node_fs10.default.promises.readFile(resolvedFilePath, "utf-8");
5392
5762
  }
5393
5763
  const contentType = mock.response.contentType ?? "application/json";
5394
5764
  const status = mock.response.status;
@@ -5451,8 +5821,8 @@ var STEP_HANDLERS = {
5451
5821
  capabilities: ["evaluate"],
5452
5822
  run: async (h) => {
5453
5823
  if (!("runScript" in h.step)) unknownStep();
5454
- const filePath = import_node_path9.default.isAbsolute(h.step.runScript.file) ? h.step.runScript.file : import_node_path9.default.join(h.context.configDir, h.step.runScript.file);
5455
- const fileContents = import_node_fs9.default.readFileSync(filePath, "utf-8");
5824
+ const filePath = import_node_path11.default.isAbsolute(h.step.runScript.file) ? h.step.runScript.file : import_node_path11.default.join(h.context.configDir, h.step.runScript.file);
5825
+ const fileContents = import_node_fs10.default.readFileSync(filePath, "utf-8");
5456
5826
  await h.driver.evaluate(fileContents);
5457
5827
  return {
5458
5828
  kind: "result",
@@ -5473,19 +5843,19 @@ var STEP_HANDLERS = {
5473
5843
  const name = h.step.assertScreenshot.name;
5474
5844
  const threshold = h.step.assertScreenshot.threshold ?? 0.1;
5475
5845
  const baselineDir = ensureBaselineDir2(h.context.configDir);
5476
- const baselinePath = import_node_path9.default.join(baselineDir, `${name}.png`);
5477
- const currentScreenshotPath = import_node_path9.default.join(h.context.runDir, "screenshots", `${name}-current.png`);
5478
- import_node_fs9.default.mkdirSync(import_node_path9.default.dirname(currentScreenshotPath), { recursive: true });
5846
+ const baselinePath = import_node_path11.default.join(baselineDir, `${name}.png`);
5847
+ const currentScreenshotPath = import_node_path11.default.join(h.context.runDir, "screenshots", `${name}-current.png`);
5848
+ import_node_fs10.default.mkdirSync(import_node_path11.default.dirname(currentScreenshotPath), { recursive: true });
5479
5849
  await h.driver.screenshot({ path: currentScreenshotPath, fullPage: true });
5480
- h.screenshots.push(import_node_path9.default.join("screenshots", `${name}-current.png`));
5481
- if (!import_node_fs9.default.existsSync(baselinePath)) {
5482
- import_node_fs9.default.copyFileSync(currentScreenshotPath, baselinePath);
5850
+ h.screenshots.push(import_node_path11.default.join("screenshots", `${name}-current.png`));
5851
+ if (!import_node_fs10.default.existsSync(baselinePath)) {
5852
+ import_node_fs10.default.copyFileSync(currentScreenshotPath, baselinePath);
5483
5853
  return {
5484
5854
  kind: "result",
5485
5855
  result: { type: "assertScreenshot", status: "pass", durationMs: Date.now() - h.stepStart, value: "baseline created" }
5486
5856
  };
5487
5857
  }
5488
- const diffPath = import_node_path9.default.join(h.context.runDir, "screenshots", `${name}-diff.png`);
5858
+ const diffPath = import_node_path11.default.join(h.context.runDir, "screenshots", `${name}-diff.png`);
5489
5859
  const comparison = await compareScreenshots2(baselinePath, currentScreenshotPath, diffPath, threshold);
5490
5860
  if (comparison.match) {
5491
5861
  return {
@@ -5498,7 +5868,7 @@ var STEP_HANDLERS = {
5498
5868
  }
5499
5869
  };
5500
5870
  }
5501
- h.screenshots.push(import_node_path9.default.join("screenshots", `${name}-diff.png`));
5871
+ h.screenshots.push(import_node_path11.default.join("screenshots", `${name}-diff.png`));
5502
5872
  throw new Error(
5503
5873
  `Visual regression: ${(comparison.diffPercentage * 100).toFixed(2)}% diff exceeds threshold ${(threshold * 100).toFixed(0)}%`
5504
5874
  );
@@ -5524,8 +5894,8 @@ var STEP_HANDLERS = {
5524
5894
  }
5525
5895
  const fileName = `assertWithAI_step_${h.index + 1}.png`;
5526
5896
  const relative = await h.addScreenshot(fileName);
5527
- const screenshotFullPath = import_node_path9.default.join(h.context.runDir, relative);
5528
- const imageBase64 = import_node_fs9.default.readFileSync(screenshotFullPath).toString("base64");
5897
+ const screenshotFullPath = import_node_path11.default.join(h.context.runDir, relative);
5898
+ const imageBase64 = import_node_fs10.default.readFileSync(screenshotFullPath).toString("base64");
5529
5899
  const assertVision = h.context.assertVision ?? assertWithAiVision;
5530
5900
  const verdict = await assertVision(
5531
5901
  { imageBase64, mediaType: "image/png", assertion },
@@ -5582,7 +5952,7 @@ var STEP_HANDLERS = {
5582
5952
  `Download filename mismatch: expected "${opts.filename}", got "${suggestedFilename}"`
5583
5953
  );
5584
5954
  }
5585
- const savePath = import_node_path9.default.join(h.context.runDir, suggestedFilename);
5955
+ const savePath = import_node_path11.default.join(h.context.runDir, suggestedFilename);
5586
5956
  await download.saveAs(savePath);
5587
5957
  return {
5588
5958
  kind: "result",
@@ -5611,8 +5981,8 @@ async function executeSteps(context) {
5611
5981
  maxSteps: context.maxSteps,
5612
5982
  selfHealing: context.selfHealing
5613
5983
  });
5614
- const screenshotsDir = import_node_path9.default.join(context.runDir, "screenshots");
5615
- import_node_fs9.default.mkdirSync(screenshotsDir, { recursive: true });
5984
+ const screenshotsDir = import_node_path11.default.join(context.runDir, "screenshots");
5985
+ import_node_fs10.default.mkdirSync(screenshotsDir, { recursive: true });
5616
5986
  const currentHuntName = context.huntStack?.[context.huntStack.length - 1];
5617
5987
  policy.assertWithinMaxSteps(context.steps.length, currentHuntName);
5618
5988
  const results = [];
@@ -5622,7 +5992,7 @@ async function executeSteps(context) {
5622
5992
  const addScreenshot = async (fileName) => {
5623
5993
  const fullPath = screenshotPath(screenshotsDir, fileName);
5624
5994
  await captureScreenshot2(driver, fullPath);
5625
- const relative = import_node_path9.default.join("screenshots", fileName);
5995
+ const relative = import_node_path11.default.join("screenshots", fileName);
5626
5996
  screenshots.push(relative);
5627
5997
  return relative;
5628
5998
  };
@@ -5710,12 +6080,12 @@ async function executeSteps(context) {
5710
6080
  return { results, screenshots, failed: false };
5711
6081
  }
5712
6082
  async function captureFinalScreenshot(page, runDir) {
5713
- const screenshotsDir = import_node_path9.default.join(runDir, "screenshots");
5714
- import_node_fs9.default.mkdirSync(screenshotsDir, { recursive: true });
6083
+ const screenshotsDir = import_node_path11.default.join(runDir, "screenshots");
6084
+ import_node_fs10.default.mkdirSync(screenshotsDir, { recursive: true });
5715
6085
  const fileName = "final.png";
5716
6086
  const filePath = screenshotPath(screenshotsDir, fileName);
5717
6087
  await captureScreenshot2(page, filePath);
5718
- return import_node_path9.default.join("screenshots", fileName);
6088
+ return import_node_path11.default.join("screenshots", fileName);
5719
6089
  }
5720
6090
 
5721
6091
  // src/runner/assertions.ts
@@ -5837,6 +6207,59 @@ async function evaluateAssertions(options) {
5837
6207
  }
5838
6208
  return results;
5839
6209
  }
6210
+ async function evaluateNativeAssertions(options) {
6211
+ const assertions = mergeAssertions(options.config, options.huntAssertions);
6212
+ const authoredTypes = new Set(
6213
+ (options.huntAssertions ?? []).map((assertion) => Object.keys(assertion)[0])
6214
+ );
6215
+ const results = [];
6216
+ const warnings = [];
6217
+ for (const assertion of assertions) {
6218
+ const type = Object.keys(assertion)[0] ?? "assertion";
6219
+ try {
6220
+ if ("selectorExists" in assertion) {
6221
+ options.assertAllowedSelector?.(assertion.selectorExists);
6222
+ const count = await options.driver.count(assertion.selectorExists);
6223
+ results.push({
6224
+ type: "selectorExists",
6225
+ value: assertion.selectorExists,
6226
+ status: count > 0 ? "pass" : "fail",
6227
+ error: count > 0 ? void 0 : "Selector not found"
6228
+ });
6229
+ continue;
6230
+ }
6231
+ if ("selectorNotExists" in assertion) {
6232
+ options.assertAllowedSelector?.(assertion.selectorNotExists);
6233
+ const count = await options.driver.count(assertion.selectorNotExists);
6234
+ results.push({
6235
+ type: "selectorNotExists",
6236
+ value: assertion.selectorNotExists,
6237
+ status: count === 0 ? "pass" : "fail",
6238
+ error: count === 0 ? void 0 : "Selector exists"
6239
+ });
6240
+ continue;
6241
+ }
6242
+ const rawValue = assertion[type];
6243
+ results.push({
6244
+ type,
6245
+ value: rawValue,
6246
+ status: "skipped",
6247
+ error: `skipped (web-only): not supported on the ${options.targetLabel} target`
6248
+ });
6249
+ if (authoredTypes.has(type)) {
6250
+ warnings.push(`${type} is web-only; skipped on ${options.targetLabel} target`);
6251
+ }
6252
+ } catch (error) {
6253
+ const detail = error instanceof Error ? error.message : "no error details";
6254
+ results.push({
6255
+ type,
6256
+ status: "fail",
6257
+ error: `Native assertion "${type}" failed: ${detail}`
6258
+ });
6259
+ }
6260
+ }
6261
+ return { results, warnings };
6262
+ }
5840
6263
 
5841
6264
  // src/runner/tracing.ts
5842
6265
  var DEFAULT_TRACE_HEADER = "traceparent";
@@ -5875,18 +6298,18 @@ function captureTraceCorrelation(response, headerName, sink, redactionValues = [
5875
6298
  }
5876
6299
 
5877
6300
  // src/reporter/result.ts
5878
- var import_node_fs10 = __toESM(require("fs"), 1);
5879
- var import_node_path10 = __toESM(require("path"), 1);
6301
+ var import_node_fs11 = __toESM(require("fs"), 1);
6302
+ var import_node_path12 = __toESM(require("path"), 1);
5880
6303
  function writeResult(runDir, result) {
5881
6304
  const fileName = "result.json";
5882
- const fullPath = import_node_path10.default.join(runDir, fileName);
5883
- import_node_fs10.default.writeFileSync(fullPath, JSON.stringify(result, null, 2));
6305
+ const fullPath = import_node_path12.default.join(runDir, fileName);
6306
+ import_node_fs11.default.writeFileSync(fullPath, JSON.stringify(result, null, 2));
5884
6307
  return fileName;
5885
6308
  }
5886
6309
 
5887
6310
  // src/reporter/summary.ts
5888
- var import_node_fs11 = __toESM(require("fs"), 1);
5889
- var import_node_path11 = __toESM(require("path"), 1);
6311
+ var import_node_fs12 = __toESM(require("fs"), 1);
6312
+ var import_node_path13 = __toESM(require("path"), 1);
5890
6313
  function escapeMd(text) {
5891
6314
  return text.replace(/([|`*_{}[\]()#+\-!\\])/g, "\\$1");
5892
6315
  }
@@ -5964,28 +6387,29 @@ function writeSummary(runDir, result) {
5964
6387
  }
5965
6388
  }
5966
6389
  const fileName = "summary.md";
5967
- const fullPath = import_node_path11.default.join(runDir, fileName);
5968
- import_node_fs11.default.writeFileSync(fullPath, `${lines.join("\n")}
6390
+ const fullPath = import_node_path13.default.join(runDir, fileName);
6391
+ import_node_fs12.default.writeFileSync(fullPath, `${lines.join("\n")}
5969
6392
  `);
5970
6393
  return fileName;
5971
6394
  }
5972
6395
 
5973
6396
  // src/reporter/junit.ts
5974
- var import_node_fs12 = __toESM(require("fs"), 1);
5975
- var import_node_path12 = __toESM(require("path"), 1);
6397
+ var import_node_fs13 = __toESM(require("fs"), 1);
6398
+ var import_node_path14 = __toESM(require("path"), 1);
5976
6399
  function escapeXml(text) {
5977
6400
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5978
6401
  }
5979
6402
  function writeJunit(runDir, result) {
5980
6403
  const totalTests = result.steps.length + result.assertions.length;
5981
6404
  const failures = result.steps.filter((s) => s.status === "fail").length + result.assertions.filter((a) => a.status === "fail").length;
6405
+ const skipped = result.assertions.filter((a) => a.status === "skipped").length;
5982
6406
  const timeSeconds = (result.durationMs / 1e3).toFixed(3);
5983
6407
  const huntName = escapeXml(result.hunt);
5984
6408
  const lines = [];
5985
6409
  lines.push('<?xml version="1.0" encoding="UTF-8"?>');
5986
6410
  lines.push("<testsuites>");
5987
6411
  lines.push(
5988
- ` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
6412
+ ` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" skipped="${skipped}" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
5989
6413
  );
5990
6414
  for (let i = 0; i < result.steps.length; i++) {
5991
6415
  const step = result.steps[i];
@@ -6009,6 +6433,11 @@ function writeJunit(runDir, result) {
6009
6433
  lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
6010
6434
  lines.push(` <failure message="${escapedFailureText}" type="assertion">${escapedFailureText}</failure>`);
6011
6435
  lines.push(" </testcase>");
6436
+ } else if (assertion.status === "skipped") {
6437
+ const skipText = escapeXml(assertion.error ?? "skipped");
6438
+ lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
6439
+ lines.push(` <skipped message="${skipText}"/>`);
6440
+ lines.push(" </testcase>");
6012
6441
  } else {
6013
6442
  lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0"/>`);
6014
6443
  }
@@ -6016,8 +6445,8 @@ function writeJunit(runDir, result) {
6016
6445
  lines.push(" </testsuite>");
6017
6446
  lines.push("</testsuites>");
6018
6447
  const fileName = "junit.xml";
6019
- const fullPath = import_node_path12.default.join(runDir, fileName);
6020
- import_node_fs12.default.writeFileSync(fullPath, `${lines.join("\n")}
6448
+ const fullPath = import_node_path14.default.join(runDir, fileName);
6449
+ import_node_fs13.default.writeFileSync(fullPath, `${lines.join("\n")}
6021
6450
  `);
6022
6451
  return fileName;
6023
6452
  }
@@ -6051,15 +6480,15 @@ function timestamp(prefix) {
6051
6480
  }
6052
6481
 
6053
6482
  // src/runner/history.ts
6054
- var import_node_fs13 = __toESM(require("fs"), 1);
6055
- var import_node_path13 = __toESM(require("path"), 1);
6483
+ var import_node_fs14 = __toESM(require("fs"), 1);
6484
+ var import_node_path15 = __toESM(require("path"), 1);
6056
6485
  var HISTORY_FILE = "history.json";
6057
6486
  var LOCK_FILE_SUFFIX = ".lock";
6058
6487
  var LOCK_RETRY_MS = 10;
6059
6488
  var LOCK_TIMEOUT_MS = 5e3;
6060
6489
  var SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
6061
6490
  function historyPath(configDir) {
6062
- return import_node_path13.default.join(configDir, HISTORY_FILE);
6491
+ return import_node_path15.default.join(configDir, HISTORY_FILE);
6063
6492
  }
6064
6493
  function isHistoryEntry(value) {
6065
6494
  if (!value || typeof value !== "object") {
@@ -6070,11 +6499,11 @@ function isHistoryEntry(value) {
6070
6499
  }
6071
6500
  function readHistory(configDir) {
6072
6501
  const filePath = historyPath(configDir);
6073
- if (!import_node_fs13.default.existsSync(filePath)) {
6502
+ if (!import_node_fs14.default.existsSync(filePath)) {
6074
6503
  return { entries: [] };
6075
6504
  }
6076
6505
  try {
6077
- const raw = import_node_fs13.default.readFileSync(filePath, "utf-8");
6506
+ const raw = import_node_fs14.default.readFileSync(filePath, "utf-8");
6078
6507
  const parsed = JSON.parse(raw);
6079
6508
  if (parsed && typeof parsed === "object" && "entries" in parsed && Array.isArray(parsed.entries)) {
6080
6509
  const validatedEntries = parsed.entries.filter(isHistoryEntry);
@@ -6113,12 +6542,12 @@ function sleepSync(ms) {
6113
6542
  function withHistoryLock(configDir, fn) {
6114
6543
  const filePath = historyPath(configDir);
6115
6544
  const lockPath = `${filePath}${LOCK_FILE_SUFFIX}`;
6116
- import_node_fs13.default.mkdirSync(import_node_path13.default.dirname(filePath), { recursive: true });
6545
+ import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(filePath), { recursive: true });
6117
6546
  const startedAt = Date.now();
6118
6547
  while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
6119
6548
  let fd;
6120
6549
  try {
6121
- fd = import_node_fs13.default.openSync(lockPath, "wx");
6550
+ fd = import_node_fs14.default.openSync(lockPath, "wx");
6122
6551
  } catch (error) {
6123
6552
  if (error.code === "EEXIST") {
6124
6553
  sleepSync(LOCK_RETRY_MS);
@@ -6130,10 +6559,10 @@ function withHistoryLock(configDir, fn) {
6130
6559
  return fn();
6131
6560
  } finally {
6132
6561
  try {
6133
- import_node_fs13.default.closeSync(fd);
6562
+ import_node_fs14.default.closeSync(fd);
6134
6563
  } catch {
6135
6564
  }
6136
- import_node_fs13.default.rmSync(lockPath, { force: true });
6565
+ import_node_fs14.default.rmSync(lockPath, { force: true });
6137
6566
  }
6138
6567
  }
6139
6568
  throw new Error(
@@ -6146,9 +6575,9 @@ function appendEntry(configDir, entry, maxRuns) {
6146
6575
  const current = readHistory(configDir);
6147
6576
  const next = pruneEntries([...current.entries, entry], maxRuns);
6148
6577
  const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
6149
- import_node_fs13.default.writeFileSync(tempPath, `${JSON.stringify({ entries: next }, null, 2)}
6578
+ import_node_fs14.default.writeFileSync(tempPath, `${JSON.stringify({ entries: next }, null, 2)}
6150
6579
  `);
6151
- import_node_fs13.default.renameSync(tempPath, filePath);
6580
+ import_node_fs14.default.renameSync(tempPath, filePath);
6152
6581
  });
6153
6582
  }
6154
6583
 
@@ -6161,11 +6590,11 @@ function parseViewportFlag(value) {
6161
6590
  return value;
6162
6591
  }
6163
6592
  function resolvePath(configDir, inputPath) {
6164
- if (import_node_path14.default.isAbsolute(inputPath)) {
6593
+ if (import_node_path16.default.isAbsolute(inputPath)) {
6165
6594
  return inputPath;
6166
6595
  }
6167
- const projectRoot = import_node_path14.default.dirname(configDir);
6168
- return import_node_path14.default.join(projectRoot, inputPath);
6596
+ const projectRoot = import_node_path16.default.dirname(configDir);
6597
+ return import_node_path16.default.join(projectRoot, inputPath);
6169
6598
  }
6170
6599
  function buildRunResult(options) {
6171
6600
  return {
@@ -6184,12 +6613,12 @@ function buildRunResult(options) {
6184
6613
  }
6185
6614
  function writeConsoleLog(runDir, entries) {
6186
6615
  const fileName = "console.log";
6187
- const filePath = import_node_path14.default.join(runDir, fileName);
6616
+ const filePath = import_node_path16.default.join(runDir, fileName);
6188
6617
  const lines = entries.map((entry) => {
6189
6618
  const location = entry.location ? ` (${entry.location})` : "";
6190
6619
  return `[${entry.type}] ${entry.text}${location}`;
6191
6620
  });
6192
- import_node_fs14.default.writeFileSync(filePath, `${lines.join("\n")}
6621
+ import_node_fs15.default.writeFileSync(filePath, `${lines.join("\n")}
6193
6622
  `);
6194
6623
  return fileName;
6195
6624
  }
@@ -6197,8 +6626,8 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
6197
6626
  const headless = options.headed ? false : config.browser.headless;
6198
6627
  const slowMo = options.slowMo ?? config.browser.slowMo;
6199
6628
  const maxSteps = config.guardrails.maxSteps;
6200
- const runDir = import_node_path14.default.join(configDir, "runs", timestamp());
6201
- import_node_fs14.default.mkdirSync(runDir, { recursive: true });
6629
+ const runDir = import_node_path16.default.join(configDir, "runs", timestamp());
6630
+ import_node_fs15.default.mkdirSync(runDir, { recursive: true });
6202
6631
  const storageStatePath = config.auth.storageStatePath ? resolvePath(configDir, config.auth.storageStatePath) : void 0;
6203
6632
  const engine = options.browser ?? config.browser.engine;
6204
6633
  const channel = options.channel ?? config.browser.channel;
@@ -6381,8 +6810,8 @@ async function runHunt(options) {
6381
6810
  }
6382
6811
  async function executeNativeHuntAttempt(options, config, configDir, interpolatedHunt, redactedFillSteps, randomVars, allowedApps, native) {
6383
6812
  const maxSteps = config.guardrails.maxSteps;
6384
- const runDir = import_node_path14.default.join(configDir, "runs", timestamp());
6385
- import_node_fs14.default.mkdirSync(runDir, { recursive: true });
6813
+ const runDir = import_node_path16.default.join(configDir, "runs", timestamp());
6814
+ import_node_fs15.default.mkdirSync(runDir, { recursive: true });
6386
6815
  const session = await native.launchSession();
6387
6816
  let result;
6388
6817
  try {
@@ -6390,6 +6819,13 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
6390
6819
  const appIdentity = native.sessionAppIdentity(session);
6391
6820
  const targetLabel = `${native.targetType}:${appIdentity}`;
6392
6821
  const effectiveAllowedApps = [.../* @__PURE__ */ new Set([...allowedApps, native.targetApp, appIdentity])];
6822
+ const assertionPolicy = createRunPolicy(driver, {
6823
+ forbiddenSelectors: config.guardrails.forbiddenSelectors,
6824
+ allowedDomains: [],
6825
+ allowedApps: effectiveAllowedApps,
6826
+ maxSteps,
6827
+ selfHealing: config.guardrails.selfHealing
6828
+ });
6393
6829
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6394
6830
  const startTime = Date.now();
6395
6831
  let stepResults = [];
@@ -6429,8 +6865,19 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
6429
6865
  } catch {
6430
6866
  finalScreenshot = void 0;
6431
6867
  }
6868
+ const { results: assertionResults, warnings: assertionWarnings } = await evaluateNativeAssertions({
6869
+ driver,
6870
+ config,
6871
+ huntAssertions: interpolatedHunt.assertions,
6872
+ assertAllowedSelector: assertionPolicy.assertAllowedSelector,
6873
+ targetLabel: nativeTargetLabel(native.targetType)
6874
+ });
6875
+ for (const warning of assertionWarnings) {
6876
+ console.warn(warning);
6877
+ }
6432
6878
  const durationMs = Date.now() - startTime;
6433
- const status = stepFailed ? "fail" : "pass";
6879
+ const assertionsFailed = assertionResults.some((assertion) => assertion.status === "fail");
6880
+ const status = stepFailed || assertionsFailed ? "fail" : "pass";
6434
6881
  const artifacts = {
6435
6882
  screenshots: finalScreenshot ? [...stepScreenshots, finalScreenshot] : stepScreenshots
6436
6883
  };
@@ -6441,7 +6888,7 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
6441
6888
  hunt: options.huntName,
6442
6889
  targetUrl: targetLabel,
6443
6890
  steps: stepResults,
6444
- assertions: [],
6891
+ assertions: assertionResults,
6445
6892
  artifacts
6446
6893
  });
6447
6894
  result = writeReports(runDir, runResult, { junit: options.junit ?? config.artifacts.junit });
@@ -6554,7 +7001,6 @@ async function runNativeHunt(options, config, configDir, target, native) {
6554
7001
  const hunt = loadHunt(options.huntName, configDir);
6555
7002
  const { hunt: interpolatedHunt, redactedFillSteps, randomVars } = interpolateHunt(hunt, process.env);
6556
7003
  assertStepsSupportedByTarget(interpolatedHunt.steps, native.targetType);
6557
- assertHuntAssertionsSupportedByTarget(interpolatedHunt.assertions, native.targetType);
6558
7004
  native.assertAppAllowed(config.guardrails.allowedApps, target);
6559
7005
  const maxSteps = config.guardrails.maxSteps;
6560
7006
  if (interpolatedHunt.steps.length > maxSteps) {
@@ -6595,7 +7041,7 @@ async function runNativeHunt(options, config, configDir, target, native) {
6595
7041
  }
6596
7042
  function recordHistory(configDir, outcome, maxRuns) {
6597
7043
  try {
6598
- const relativeRunDir = import_node_path14.default.relative(configDir, outcome.runDir);
7044
+ const relativeRunDir = import_node_path16.default.relative(configDir, outcome.runDir);
6599
7045
  appendEntry(
6600
7046
  configDir,
6601
7047
  {
@@ -6612,11 +7058,11 @@ function recordHistory(configDir, outcome, maxRuns) {
6612
7058
  }
6613
7059
 
6614
7060
  // src/runner/suite.ts
6615
- var import_node_path17 = __toESM(require("path"), 1);
7061
+ var import_node_path19 = __toESM(require("path"), 1);
6616
7062
 
6617
7063
  // src/reporter/ci-summary.ts
6618
- var import_node_fs15 = __toESM(require("fs"), 1);
6619
- var import_node_path15 = __toESM(require("path"), 1);
7064
+ var import_node_fs16 = __toESM(require("fs"), 1);
7065
+ var import_node_path17 = __toESM(require("path"), 1);
6620
7066
  var import_chalk = __toESM(require("chalk"), 1);
6621
7067
  function countCiResults(results) {
6622
7068
  return {
@@ -6646,9 +7092,9 @@ function writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky = []
6646
7092
  ...flaky.length > 0 ? { flaky } : {},
6647
7093
  ...clusters.length > 0 ? { clusters } : {}
6648
7094
  };
6649
- import_node_fs15.default.mkdirSync(ciRunDir, { recursive: true });
6650
- const filePath = import_node_path15.default.join(ciRunDir, "ci-result.json");
6651
- import_node_fs15.default.writeFileSync(filePath, JSON.stringify(ciResult, null, 2) + "\n");
7095
+ import_node_fs16.default.mkdirSync(ciRunDir, { recursive: true });
7096
+ const filePath = import_node_path17.default.join(ciRunDir, "ci-result.json");
7097
+ import_node_fs16.default.writeFileSync(filePath, JSON.stringify(ciResult, null, 2) + "\n");
6652
7098
  return filePath;
6653
7099
  }
6654
7100
 
@@ -6715,7 +7161,7 @@ function rankFlaky(configDir, options = {}) {
6715
7161
  }
6716
7162
 
6717
7163
  // src/backlog/fingerprint.ts
6718
- var import_node_crypto2 = require("crypto");
7164
+ var import_node_crypto3 = require("crypto");
6719
7165
  function normalizeError(error) {
6720
7166
  return error.toLowerCase().replace(/0x[0-9a-f]+/g, "").replace(/\b\d+(?:\.\d+)?\s*(?:ms|s|px)\b/g, "").replace(/\s+/g, " ").trim();
6721
7167
  }
@@ -6738,7 +7184,7 @@ function computeFingerprint(failure) {
6738
7184
  failure.selector ?? "",
6739
7185
  normalizeError(failure.error)
6740
7186
  ];
6741
- return (0, import_node_crypto2.createHash)("sha1").update(parts.join("|")).digest("hex").slice(0, 8);
7187
+ return (0, import_node_crypto3.createHash)("sha1").update(parts.join("|")).digest("hex").slice(0, 8);
6742
7188
  }
6743
7189
  function buildMarker(failure, hash) {
6744
7190
  const hunt = sanitizeMarkerValue(failure.hunt);
@@ -6782,8 +7228,8 @@ function clusterFailures(failures) {
6782
7228
  }
6783
7229
 
6784
7230
  // src/backlog/index.ts
6785
- var import_node_fs16 = __toESM(require("fs"), 1);
6786
- var import_node_path16 = __toESM(require("path"), 1);
7231
+ var import_node_fs17 = __toESM(require("fs"), 1);
7232
+ var import_node_path18 = __toESM(require("path"), 1);
6787
7233
 
6788
7234
  // src/backlog/parse.ts
6789
7235
  var MARKER_FP = /<!--\s*prowl:fp=([0-9a-f]+)/;
@@ -6873,7 +7319,7 @@ ${after}`;
6873
7319
  // src/backlog/index.ts
6874
7320
  function readFileOrEmpty(filePath) {
6875
7321
  try {
6876
- return import_node_fs16.default.readFileSync(filePath, "utf-8");
7322
+ return import_node_fs17.default.readFileSync(filePath, "utf-8");
6877
7323
  } catch (error) {
6878
7324
  const err = error;
6879
7325
  if (err.code === "ENOENT") return "";
@@ -6889,7 +7335,7 @@ function buildFailure(hunt) {
6889
7335
  if (!hunt.runDir) return failure;
6890
7336
  let run;
6891
7337
  try {
6892
- const resultJson = readFileOrEmpty(import_node_path16.default.join(hunt.runDir, "result.json"));
7338
+ const resultJson = readFileOrEmpty(import_node_path18.default.join(hunt.runDir, "result.json"));
6893
7339
  if (!resultJson) return failure;
6894
7340
  run = JSON.parse(resultJson);
6895
7341
  } catch (error) {
@@ -6918,8 +7364,8 @@ function extractFailures(suiteResult) {
6918
7364
  }
6919
7365
  function updateBacklogFromSuite(suiteResult, options = {}) {
6920
7366
  const projectRoot = options.projectRoot ?? process.cwd();
6921
- const backlogPath = options.backlogPath ?? import_node_path16.default.join(projectRoot, "docs", "backlog.md");
6922
- const resolvedPath = options.resolvedPath ?? import_node_path16.default.join(projectRoot, "docs", "resolved.md");
7367
+ const backlogPath = options.backlogPath ?? import_node_path18.default.join(projectRoot, "docs", "backlog.md");
7368
+ const resolvedPath = options.resolvedPath ?? import_node_path18.default.join(projectRoot, "docs", "resolved.md");
6923
7369
  const date = options.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6924
7370
  const summary = { created: [], regressions: [], skipped: [], backlogPath };
6925
7371
  const failures = extractFailures(suiteResult);
@@ -6951,8 +7397,8 @@ function updateBacklogFromSuite(suiteResult, options = {}) {
6951
7397
  }
6952
7398
  }
6953
7399
  if (ticketsToAdd.length > 0) {
6954
- import_node_fs16.default.mkdirSync(import_node_path16.default.dirname(backlogPath), { recursive: true });
6955
- import_node_fs16.default.writeFileSync(backlogPath, insertTickets(backlogContent, ticketsToAdd));
7400
+ import_node_fs17.default.mkdirSync(import_node_path18.default.dirname(backlogPath), { recursive: true });
7401
+ import_node_fs17.default.writeFileSync(backlogPath, insertTickets(backlogContent, ticketsToAdd));
6956
7402
  }
6957
7403
  return summary;
6958
7404
  }
@@ -7113,7 +7559,7 @@ async function runSuite(options = {}) {
7113
7559
  const clusters = clusterFailures(
7114
7560
  extractFailures({ result: { hunts: results }, resultPath: null })
7115
7561
  ).filter((cluster) => cluster.count > 1);
7116
- const ciRunDir = import_node_path17.default.join(configDir, "runs", timestamp("ci"));
7562
+ const ciRunDir = import_node_path19.default.join(configDir, "runs", timestamp("ci"));
7117
7563
  const resultPath = writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky, clusters);
7118
7564
  const { passed, failed, skipped } = countCiResults(results);
7119
7565
  return {
@@ -7798,11 +8244,17 @@ async function generateHunt(options) {
7798
8244
  DEFAULT_REQUEST_TIMEOUT_MS,
7799
8245
  DEFAULT_SIMULATOR_LOCK_ROOT,
7800
8246
  DEFAULT_WDA_REQUEST_TIMEOUT_MS,
8247
+ HELPER_BINARY,
7801
8248
  INTERACTIVE_ROLES,
7802
8249
  IOS_INTERACTIVE_TYPES,
7803
8250
  IOS_MATCH_DIALECT,
7804
8251
  IOS_PRESS_KEYS,
7805
8252
  IOS_WINDOW_TYPE,
8253
+ MACDRIVER_REPO,
8254
+ MACDRIVER_SIGNING_AUTHORITY_PREFIX,
8255
+ MACDRIVER_SIGNING_IDENTIFIER,
8256
+ MACDRIVER_VERSION,
8257
+ MACDRIVER_VERSION_PATTERN,
7806
8258
  MACOS_MATCH_DIALECT,
7807
8259
  NATIVE_ATTRIBUTE_MAP,
7808
8260
  SpawnMacHelperClient,
@@ -7832,6 +8284,8 @@ async function generateHunt(options) {
7832
8284
  closeIosSession,
7833
8285
  closeMacSession,
7834
8286
  clusterFailures,
8287
+ codesignVerifier,
8288
+ collectMacdriverStatus,
7835
8289
  computeFlakeScore,
7836
8290
  configSchema,
7837
8291
  createAndroidDriver,
@@ -7845,6 +8299,8 @@ async function generateHunt(options) {
7845
8299
  defaultAgentConnector,
7846
8300
  defaultIosAgentConnector,
7847
8301
  defaultWdaTestRunPreparer,
8302
+ dittoExtractor,
8303
+ downloadAndVerify,
7848
8304
  escapePredicateArg,
7849
8305
  escapeUiSelectorArg,
7850
8306
  extractElementId,
@@ -7855,6 +8311,7 @@ async function generateHunt(options) {
7855
8311
  healSelector,
7856
8312
  huntSchema,
7857
8313
  injectUsePortIntoXctestrun,
8314
+ installMacdriver,
7858
8315
  interpolateHunt,
7859
8316
  iosAppAllowedIdentities,
7860
8317
  iosNodeToNative,
@@ -7871,7 +8328,14 @@ async function generateHunt(options) {
7871
8328
  loadHunt,
7872
8329
  loadHuntMeta,
7873
8330
  loadHuntTags,
8331
+ macdriverAssetName,
8332
+ macdriverAssetUrl,
7874
8333
  macdriverBuildInstructions,
8334
+ macdriverChecksumName,
8335
+ macdriverInstallRoot,
8336
+ macdriverInstalledBinary,
8337
+ macdriverReleaseTag,
8338
+ macdriverVersionDir,
7875
8339
  matchAndroidSelector,
7876
8340
  matchIosSelector,
7877
8341
  matchNativeTree,
@@ -7881,6 +8345,8 @@ async function generateHunt(options) {
7881
8345
  parseAdbDevices,
7882
8346
  parseAndroidHierarchy,
7883
8347
  parseAndroidSelector,
8348
+ parseChecksumFile,
8349
+ parseCodesignDetails,
7884
8350
  parseForwardPort,
7885
8351
  parseIosHierarchy,
7886
8352
  parseIosSelector,
@@ -7907,19 +8373,26 @@ async function generateHunt(options) {
7907
8373
  resolveWdaTestRun,
7908
8374
  runHunt,
7909
8375
  runSuite,
8376
+ runVersionProbe,
7910
8377
  selectDeviceSerial,
7911
8378
  selectSimulatorUdid,
8379
+ sha256Hex,
7912
8380
  shortIosType,
7913
8381
  stepSchema,
8382
+ tccGuidance,
7914
8383
  unquoteSelectorValue,
7915
8384
  unwrapAndroidTextSelector,
7916
8385
  unwrapIosTextSelector,
7917
8386
  unwrapNativeTextSelector,
7918
8387
  updateBacklogFromSuite,
8388
+ validateMacdriverArchiveEntries,
8389
+ validateMacdriverVersion,
8390
+ verifyMacdriverSignature,
7919
8391
  waitForAgentReady,
7920
8392
  waitForWdaReady,
7921
8393
  wdaCacheDir,
7922
8394
  wdaTestRunArgs,
7923
- webOnlyReason
8395
+ webOnlyReason,
8396
+ zipinfoArchiveLister
7924
8397
  });
7925
8398
  //# sourceMappingURL=lib.cjs.map