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.
@@ -7,7 +7,7 @@ import {
7
7
  loadHunt,
8
8
  loadHuntTags,
9
9
  resolveViewport
10
- } from "./chunk-WHAMB4TY.js";
10
+ } from "./chunk-O3OUTZ2P.js";
11
11
 
12
12
  // src/config/interpolate.ts
13
13
  import crypto from "crypto";
@@ -757,27 +757,71 @@ function createMacDriver(client, options = {}) {
757
757
  };
758
758
  }
759
759
 
760
+ // src/browser/macdriver-release.ts
761
+ import os from "os";
762
+ import path2 from "path";
763
+ var HELPER_BINARY = "prowl-macdriver";
764
+ var MACDRIVER_VERSION = "0.1.0";
765
+ var MACDRIVER_REPO = "prowl-tools/prowl";
766
+ var MACDRIVER_SIGNING_IDENTIFIER = "tools.prowl.macdriver";
767
+ var MACDRIVER_SIGNING_AUTHORITY_PREFIX = "Developer ID Application: Genkei Labs";
768
+ var MACDRIVER_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
769
+ function validateMacdriverVersion(version) {
770
+ if (!MACDRIVER_VERSION_PATTERN.test(version)) {
771
+ throw new Error(
772
+ `Invalid prowl-macdriver version "${version}". Expected a release version like 0.1.0.`
773
+ );
774
+ }
775
+ return version;
776
+ }
777
+ function macdriverReleaseTag(version = MACDRIVER_VERSION) {
778
+ return `macdriver-v${validateMacdriverVersion(version)}`;
779
+ }
780
+ function macdriverAssetName(version = MACDRIVER_VERSION) {
781
+ return `prowl-macdriver-v${validateMacdriverVersion(version)}-universal.zip`;
782
+ }
783
+ function macdriverChecksumName(version = MACDRIVER_VERSION) {
784
+ return `${macdriverAssetName(version)}.sha256`;
785
+ }
786
+ function macdriverAssetUrl(assetName, version = MACDRIVER_VERSION) {
787
+ return `https://github.com/${MACDRIVER_REPO}/releases/download/${macdriverReleaseTag(version)}/${assetName}`;
788
+ }
789
+ function macdriverInstallRoot(homedir = os.homedir()) {
790
+ return path2.join(homedir, ".prowl", "macdriver");
791
+ }
792
+ function macdriverVersionDir(version = MACDRIVER_VERSION, homedir = os.homedir()) {
793
+ const root = path2.resolve(macdriverInstallRoot(homedir));
794
+ const versionDir = path2.resolve(root, validateMacdriverVersion(version));
795
+ if (!versionDir.startsWith(root + path2.sep)) {
796
+ throw new Error(`Resolved prowl-macdriver version directory escaped install root: ${versionDir}`);
797
+ }
798
+ return versionDir;
799
+ }
800
+ function macdriverInstalledBinary(version = MACDRIVER_VERSION, homedir = os.homedir()) {
801
+ return path2.join(macdriverVersionDir(version, homedir), HELPER_BINARY);
802
+ }
803
+
760
804
  // src/browser/mac-helper.ts
761
805
  import { spawn } from "child_process";
762
806
  import fs2 from "fs";
763
- import path2 from "path";
807
+ import os2 from "os";
808
+ import path3 from "path";
764
809
  import { fileURLToPath } from "url";
765
- var HELPER_BINARY = "prowl-macdriver";
766
810
  function macdriverBuildInstructions() {
767
- 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.";
811
+ 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.";
768
812
  }
769
813
  function getPackageRoot() {
770
- let dir = path2.dirname(fileURLToPath(import.meta.url));
771
- const root = path2.parse(dir).root;
814
+ let dir = path3.dirname(fileURLToPath(import.meta.url));
815
+ const root = path3.parse(dir).root;
772
816
  while (dir !== root) {
773
- if (fs2.existsSync(path2.join(dir, "package.json"))) {
817
+ if (fs2.existsSync(path3.join(dir, "package.json"))) {
774
818
  return dir;
775
819
  }
776
- dir = path2.dirname(dir);
820
+ dir = path3.dirname(dir);
777
821
  }
778
822
  return root;
779
823
  }
780
- function resolveHelperBinary(env = process.env) {
824
+ function resolveHelperBinary(env = process.env, options = {}) {
781
825
  const override = env.PROWL_MACDRIVER_BIN;
782
826
  if (override) {
783
827
  if (!fs2.existsSync(override)) {
@@ -788,10 +832,15 @@ ${macdriverBuildInstructions()}`
788
832
  }
789
833
  return override;
790
834
  }
835
+ const homedir = options.homedir ?? os2.homedir();
836
+ const userBinary = macdriverInstalledBinary(MACDRIVER_VERSION, homedir);
837
+ if (fs2.existsSync(userBinary)) {
838
+ return userBinary;
839
+ }
791
840
  const root = getPackageRoot();
792
841
  const candidates = [
793
- path2.join(root, "macdriver", ".build", "release", HELPER_BINARY),
794
- path2.join(root, "macdriver", ".build", "debug", HELPER_BINARY)
842
+ path3.join(root, "macdriver", ".build", "release", HELPER_BINARY),
843
+ path3.join(root, "macdriver", ".build", "debug", HELPER_BINARY)
795
844
  ];
796
845
  for (const candidate of candidates) {
797
846
  if (fs2.existsSync(candidate)) {
@@ -1721,12 +1770,12 @@ var Uia2Transport = class {
1721
1770
  * {@link Uia2HttpError} on a non-2xx response, or a timeout error when the
1722
1771
  * per-request deadline elapses.
1723
1772
  */
1724
- async request(method, path16, body, timeoutMs) {
1773
+ async request(method, path18, body, timeoutMs) {
1725
1774
  const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
1726
1775
  const controller = new AbortController();
1727
1776
  const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
1728
1777
  timer.unref?.();
1729
- const url = `${this.baseUrl}${path16}`;
1778
+ const url = `${this.baseUrl}${path18}`;
1730
1779
  let response;
1731
1780
  try {
1732
1781
  response = await this.fetchImpl(url, {
@@ -1738,7 +1787,7 @@ var Uia2Transport = class {
1738
1787
  } catch (error) {
1739
1788
  if (controller.signal.aborted) {
1740
1789
  const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
1741
- throw new Error(`uiautomator2 request ${method} ${path16} timed out after ${shown}`);
1790
+ throw new Error(`uiautomator2 request ${method} ${path18} timed out after ${shown}`);
1742
1791
  }
1743
1792
  throw error instanceof Error ? error : new Error(String(error));
1744
1793
  } finally {
@@ -1749,7 +1798,7 @@ var Uia2Transport = class {
1749
1798
  if (!response.ok) {
1750
1799
  const wdError = extractWebdriverError(parsed);
1751
1800
  throw new Uia2HttpError(
1752
- `uiautomator2 ${method} ${path16} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
1801
+ `uiautomator2 ${method} ${path18} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
1753
1802
  response.status,
1754
1803
  wdError
1755
1804
  );
@@ -1832,10 +1881,10 @@ function sleep(ms) {
1832
1881
  }
1833
1882
  function createUia2AgentClient(transport, sessionId, options = {}) {
1834
1883
  const base = `/session/${sessionId}`;
1835
- async function locate(query, path16) {
1884
+ async function locate(query, path18) {
1836
1885
  return transport.request(
1837
1886
  "POST",
1838
- `${base}${path16}`,
1887
+ `${base}${path18}`,
1839
1888
  androidQueryToLocator(query, { appPackage: options.appPackage })
1840
1889
  );
1841
1890
  }
@@ -1894,7 +1943,7 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
1894
1943
  import { createRequire } from "module";
1895
1944
  import { execFile as execFile2 } from "child_process";
1896
1945
  import fs4 from "fs";
1897
- import path3 from "path";
1946
+ import path4 from "path";
1898
1947
  var UIA2_REMOTE_PORT = 6790;
1899
1948
  function resolveAgentApks(requireFn = createRequire(import.meta.url)) {
1900
1949
  let pkgJsonPath;
@@ -1905,10 +1954,10 @@ function resolveAgentApks(requireFn = createRequire(import.meta.url)) {
1905
1954
  "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"
1906
1955
  );
1907
1956
  }
1908
- const pkgDir = path3.dirname(pkgJsonPath);
1957
+ const pkgDir = path4.dirname(pkgJsonPath);
1909
1958
  const version = requireFn(pkgJsonPath).version;
1910
- const serverApk = path3.join(pkgDir, "apks", `appium-uiautomator2-server-v${version}.apk`);
1911
- const testApk = path3.join(pkgDir, "apks", "appium-uiautomator2-server-debug-androidTest.apk");
1959
+ const serverApk = path4.join(pkgDir, "apks", `appium-uiautomator2-server-v${version}.apk`);
1960
+ const testApk = path4.join(pkgDir, "apks", "appium-uiautomator2-server-debug-androidTest.apk");
1912
1961
  for (const apk of [serverApk, testApk]) {
1913
1962
  if (!fs4.existsSync(apk)) {
1914
1963
  throw new Error(`Expected uiautomator2 agent APK is missing: ${apk}. Reinstall dependencies.`);
@@ -1956,7 +2005,7 @@ async function resolvePackage(app, runner, serial, aaptResolver, allowedApps) {
1956
2005
  assertAndroidAppAllowed(allowedApps, app);
1957
2006
  return app;
1958
2007
  }
1959
- const apkPath = path3.resolve(app);
2008
+ const apkPath = path4.resolve(app);
1960
2009
  if (!fs4.existsSync(apkPath)) {
1961
2010
  throw new Error(`APK not found: ${apkPath}`);
1962
2011
  }
@@ -2250,8 +2299,8 @@ function createIosDriver(client, options) {
2250
2299
  import { execFile as execFile3, spawn as spawn3 } from "child_process";
2251
2300
  import { mkdir, readFile, rm, writeFile } from "fs/promises";
2252
2301
  import net from "net";
2253
- import os from "os";
2254
- import path4 from "path";
2302
+ import os3 from "os";
2303
+ import path5 from "path";
2255
2304
  var spawnXcrunProcess = (args, options) => {
2256
2305
  const child = spawn3("xcrun", args, {
2257
2306
  stdio: "ignore",
@@ -2265,7 +2314,7 @@ var spawnXcrunProcess = (args, options) => {
2265
2314
  }
2266
2315
  };
2267
2316
  };
2268
- var DEFAULT_SIMULATOR_LOCK_ROOT = path4.join(os.tmpdir(), "prowl-ios-simulator-locks");
2317
+ var DEFAULT_SIMULATOR_LOCK_ROOT = path5.join(os3.tmpdir(), "prowl-ios-simulator-locks");
2269
2318
  var SIMULATOR_LOCK_OWNER_FILE = "owner.json";
2270
2319
  var execFileXcrunRunner = (args, options) => new Promise((resolve) => {
2271
2320
  execFile3(
@@ -2305,7 +2354,7 @@ function isProcessAlive(pid) {
2305
2354
  }
2306
2355
  async function removeStaleSimulatorLock(lockPath) {
2307
2356
  try {
2308
- const ownerText = await readFile(path4.join(lockPath, SIMULATOR_LOCK_OWNER_FILE), "utf8");
2357
+ const ownerText = await readFile(path5.join(lockPath, SIMULATOR_LOCK_OWNER_FILE), "utf8");
2309
2358
  const owner = JSON.parse(ownerText);
2310
2359
  if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0 && !isProcessAlive(owner.pid)) {
2311
2360
  await rm(lockPath, { recursive: true, force: true });
@@ -2323,7 +2372,7 @@ function simulatorReservedError(udid) {
2323
2372
  }
2324
2373
  async function reserveSimulatorUdid(udid, options = {}) {
2325
2374
  const lockRoot = options.lockRoot ?? DEFAULT_SIMULATOR_LOCK_ROOT;
2326
- const lockPath = path4.join(lockRoot, simulatorLockName(udid));
2375
+ const lockPath = path5.join(lockRoot, simulatorLockName(udid));
2327
2376
  await mkdir(lockRoot, { recursive: true });
2328
2377
  for (let attempt = 0; attempt < 2; attempt += 1) {
2329
2378
  try {
@@ -2347,7 +2396,7 @@ async function reserveSimulatorUdid(udid, options = {}) {
2347
2396
  };
2348
2397
  try {
2349
2398
  await writeFile(
2350
- path4.join(lockPath, SIMULATOR_LOCK_OWNER_FILE),
2399
+ path5.join(lockPath, SIMULATOR_LOCK_OWNER_FILE),
2351
2400
  `${JSON.stringify({ pid: process.pid, udid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
2352
2401
  `,
2353
2402
  { flag: "wx" }
@@ -2539,15 +2588,15 @@ var WdaTransport = class {
2539
2588
  * {@link WdaHttpError} on a non-2xx response, or a timeout error when the
2540
2589
  * per-request deadline elapses.
2541
2590
  */
2542
- async requestFull(method, path16, body, timeoutMs) {
2591
+ async requestFull(method, path18, body, timeoutMs) {
2543
2592
  const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
2544
2593
  const controller = new AbortController();
2545
2594
  const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
2546
2595
  timer.unref?.();
2547
- const url = `${this.baseUrl}${path16}`;
2596
+ const url = `${this.baseUrl}${path18}`;
2548
2597
  const timeoutError = () => {
2549
2598
  const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
2550
- return new Error(`WebDriverAgent request ${method} ${path16} timed out after ${shown}`);
2599
+ return new Error(`WebDriverAgent request ${method} ${path18} timed out after ${shown}`);
2551
2600
  };
2552
2601
  let response;
2553
2602
  try {
@@ -2579,7 +2628,7 @@ var WdaTransport = class {
2579
2628
  if (!response.ok) {
2580
2629
  const wdError = extractWebdriverError2(parsed);
2581
2630
  throw new WdaHttpError(
2582
- `WebDriverAgent ${method} ${path16} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
2631
+ `WebDriverAgent ${method} ${path18} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
2583
2632
  response.status,
2584
2633
  wdError
2585
2634
  );
@@ -2587,8 +2636,8 @@ var WdaTransport = class {
2587
2636
  return parsed;
2588
2637
  }
2589
2638
  /** Like {@link requestFull} but returns just the `value` field. */
2590
- async request(method, path16, body, timeoutMs) {
2591
- const parsed = await this.requestFull(method, path16, body, timeoutMs);
2639
+ async request(method, path18, body, timeoutMs) {
2640
+ const parsed = await this.requestFull(method, path18, body, timeoutMs);
2592
2641
  return parsed?.value;
2593
2642
  }
2594
2643
  };
@@ -2664,8 +2713,8 @@ function sleep2(ms) {
2664
2713
  }
2665
2714
  function createWdaAgentClient(transport, sessionId) {
2666
2715
  const base = `/session/${sessionId}`;
2667
- async function locate(query, path16) {
2668
- return transport.request("POST", `${base}${path16}`, iosQueryToLocator(query));
2716
+ async function locate(query, path18) {
2717
+ return transport.request("POST", `${base}${path18}`, iosQueryToLocator(query));
2669
2718
  }
2670
2719
  return {
2671
2720
  async findElement(query) {
@@ -2718,8 +2767,8 @@ function createWdaAgentClient(transport, sessionId) {
2718
2767
  import { createRequire as createRequire2 } from "module";
2719
2768
  import { execFile as execFile4 } from "child_process";
2720
2769
  import fs5 from "fs";
2721
- import os2 from "os";
2722
- import path5 from "path";
2770
+ import os4 from "os";
2771
+ import path6 from "path";
2723
2772
  var WDA_RUNNER_BUNDLE_ID = "com.facebook.WebDriverAgentRunner.xctrunner";
2724
2773
  var WDA_USE_PORT_ENV = "USE_PORT";
2725
2774
  var PREPARED_XCTESTRUN_PREFIX = "prowl-wda-xctestrun-";
@@ -2745,19 +2794,19 @@ function resolveWdaProject(requireFn = createRequire2(import.meta.url)) {
2745
2794
  "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"
2746
2795
  );
2747
2796
  }
2748
- const pkgDir = path5.dirname(pkgJsonPath);
2797
+ const pkgDir = path6.dirname(pkgJsonPath);
2749
2798
  const version = requireFn(pkgJsonPath).version;
2750
- const projectPath = path5.join(pkgDir, "WebDriverAgent.xcodeproj");
2799
+ const projectPath = path6.join(pkgDir, "WebDriverAgent.xcodeproj");
2751
2800
  if (!fs5.existsSync(projectPath)) {
2752
2801
  throw new Error(`Expected WebDriverAgent project is missing: ${projectPath}. Reinstall dependencies.`);
2753
2802
  }
2754
2803
  return { projectPath, version };
2755
2804
  }
2756
- function wdaCacheDir(wdaVersion, xcode, homeDir = os2.homedir()) {
2757
- return path5.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
2805
+ function wdaCacheDir(wdaVersion, xcode, homeDir = os4.homedir()) {
2806
+ return path6.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
2758
2807
  }
2759
2808
  function productsDir(derivedDataPath) {
2760
- return path5.join(derivedDataPath, "Build", "Products");
2809
+ return path6.join(derivedDataPath, "Build", "Products");
2761
2810
  }
2762
2811
  function findXctestrunIn(dir) {
2763
2812
  let entries;
@@ -2767,7 +2816,7 @@ function findXctestrunIn(dir) {
2767
2816
  return null;
2768
2817
  }
2769
2818
  const match = entries.filter((name) => name.endsWith(".xctestrun") && !name.startsWith(PREPARED_XCTESTRUN_PREFIX)).sort()[0];
2770
- return match ? path5.join(dir, match) : null;
2819
+ return match ? path6.join(dir, match) : null;
2771
2820
  }
2772
2821
  function resolveOverrideXctestrun(override) {
2773
2822
  if (!fs5.existsSync(override)) {
@@ -2779,7 +2828,7 @@ function resolveOverrideXctestrun(override) {
2779
2828
  const candidates = [];
2780
2829
  const stat = fs5.statSync(override);
2781
2830
  if (override.endsWith(".app")) {
2782
- candidates.push(path5.dirname(path5.dirname(override)));
2831
+ candidates.push(path6.dirname(path6.dirname(override)));
2783
2832
  } else if (stat.isDirectory()) {
2784
2833
  candidates.push(override, productsDir(override));
2785
2834
  }
@@ -2796,7 +2845,7 @@ function resolveOverrideXctestrun(override) {
2796
2845
  async function resolveWdaTestRun(options = {}) {
2797
2846
  const runner = options.runner ?? execFileXcrunRunner;
2798
2847
  const env = options.env ?? process.env;
2799
- const homeDir = options.homeDir ?? os2.homedir();
2848
+ const homeDir = options.homeDir ?? os4.homedir();
2800
2849
  const log = options.logger ?? ((message) => process.stderr.write(`${message}
2801
2850
  `));
2802
2851
  const override = env.PROWL_WDA_RUNNER;
@@ -2912,8 +2961,8 @@ var defaultWdaTestRunPreparer = async ({ xctestrunPath, port }) => {
2912
2961
  }
2913
2962
  const injected = injectUsePortIntoXctestrun(parsed, port);
2914
2963
  const stem = `${PREPARED_XCTESTRUN_PREFIX}${port}-${process.pid}`;
2915
- const outPath = path5.join(path5.dirname(xctestrunPath), `${stem}.xctestrun`);
2916
- const jsonPath = path5.join(os2.tmpdir(), `${stem}.json`);
2964
+ const outPath = path6.join(path6.dirname(xctestrunPath), `${stem}.xctestrun`);
2965
+ const jsonPath = path6.join(os4.tmpdir(), `${stem}.json`);
2917
2966
  fs5.writeFileSync(jsonPath, JSON.stringify(injected));
2918
2967
  try {
2919
2968
  await runPlutil(["-convert", "xml1", jsonPath, "-o", outPath]);
@@ -2926,7 +2975,7 @@ function cleanupPreparedTestRun(preparedPath) {
2926
2975
  if (!preparedPath) {
2927
2976
  return;
2928
2977
  }
2929
- if (!path5.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
2978
+ if (!path6.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
2930
2979
  return;
2931
2980
  }
2932
2981
  fs5.rmSync(preparedPath, { force: true });
@@ -2951,7 +3000,7 @@ async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
2951
3000
  }
2952
3001
  return app;
2953
3002
  }
2954
- const appPath = path5.resolve(app);
3003
+ const appPath = path6.resolve(app);
2955
3004
  if (!fs5.existsSync(appPath)) {
2956
3005
  throw new Error(`.app bundle not found: ${appPath}`);
2957
3006
  }
@@ -3110,14 +3159,14 @@ async function healSelector(probe, selector, options) {
3110
3159
 
3111
3160
  // src/runner/history.ts
3112
3161
  import fs6 from "fs";
3113
- import path6 from "path";
3162
+ import path7 from "path";
3114
3163
  var HISTORY_FILE = "history.json";
3115
3164
  var LOCK_FILE_SUFFIX = ".lock";
3116
3165
  var LOCK_RETRY_MS = 10;
3117
3166
  var LOCK_TIMEOUT_MS = 5e3;
3118
3167
  var SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
3119
3168
  function historyPath(configDir) {
3120
- return path6.join(configDir, HISTORY_FILE);
3169
+ return path7.join(configDir, HISTORY_FILE);
3121
3170
  }
3122
3171
  function isHistoryEntry(value) {
3123
3172
  if (!value || typeof value !== "object") {
@@ -3171,7 +3220,7 @@ function sleepSync(ms) {
3171
3220
  function withHistoryLock(configDir, fn) {
3172
3221
  const filePath = historyPath(configDir);
3173
3222
  const lockPath = `${filePath}${LOCK_FILE_SUFFIX}`;
3174
- fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
3223
+ fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
3175
3224
  const startedAt = Date.now();
3176
3225
  while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
3177
3226
  let fd;
@@ -3212,11 +3261,11 @@ function appendEntry(configDir, entry, maxRuns) {
3212
3261
 
3213
3262
  // src/runner/index.ts
3214
3263
  import fs12 from "fs";
3215
- import path12 from "path";
3264
+ import path13 from "path";
3216
3265
 
3217
3266
  // src/browser/playwright-driver.ts
3218
3267
  import fs7 from "fs";
3219
- import path7 from "path";
3268
+ import path8 from "path";
3220
3269
  import {
3221
3270
  chromium,
3222
3271
  firefox,
@@ -3249,7 +3298,7 @@ async function launchBrowser(options) {
3249
3298
  }
3250
3299
  }
3251
3300
  if (options.recordHar) {
3252
- contextOptions.recordHar = { path: path7.join(options.runDir, "network.har") };
3301
+ contextOptions.recordHar = { path: path8.join(options.runDir, "network.har") };
3253
3302
  }
3254
3303
  const context = await browser.newContext(contextOptions);
3255
3304
  const page = await context.newPage();
@@ -3257,7 +3306,7 @@ async function launchBrowser(options) {
3257
3306
  page.setDefaultNavigationTimeout(options.timeout);
3258
3307
  let tracePath;
3259
3308
  if (options.trace) {
3260
- tracePath = path7.join(options.runDir, "trace.zip");
3309
+ tracePath = path8.join(options.runDir, "trace.zip");
3261
3310
  await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
3262
3311
  }
3263
3312
  return { browser, context, page, tracePath };
@@ -3436,7 +3485,7 @@ function createPlaywrightDriver(page) {
3436
3485
 
3437
3486
  // src/runner/steps.ts
3438
3487
  import fs8 from "fs";
3439
- import path8 from "path";
3488
+ import path9 from "path";
3440
3489
 
3441
3490
  // src/runner/policy.ts
3442
3491
  var ALWAYS_ALLOWED_PROTOCOLS = ["about:", "data:"];
@@ -4116,7 +4165,7 @@ async function runInlineAssert(driver, policy, assertion) {
4116
4165
  throw new Error("assert step is missing an assertion type");
4117
4166
  }
4118
4167
  function screenshotPath(screenshotsDir, fileName) {
4119
- return path8.join(screenshotsDir, fileName);
4168
+ return path9.join(screenshotsDir, fileName);
4120
4169
  }
4121
4170
  function stepPath(prefix, index) {
4122
4171
  return prefix ? `${prefix}.${index}` : `${index}`;
@@ -4133,7 +4182,7 @@ function validateDownloadFilename(suggestedFilename) {
4133
4182
  const safeFilename = suggestedFilename.trim();
4134
4183
  const allowedFilenamePattern = /^[^<>:"/\\|?*]+$/;
4135
4184
  const hasControlCharacter = Array.from(safeFilename).some((char) => char.charCodeAt(0) < 32);
4136
- if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !== path8.basename(safeFilename) || safeFilename.includes("..") || /[/\\]/.test(safeFilename) || hasControlCharacter || !allowedFilenamePattern.test(safeFilename)) {
4185
+ if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !== path9.basename(safeFilename) || safeFilename.includes("..") || /[/\\]/.test(safeFilename) || hasControlCharacter || !allowedFilenamePattern.test(safeFilename)) {
4137
4186
  throw new Error(`Invalid download filename: "${suggestedFilename}"`);
4138
4187
  }
4139
4188
  return safeFilename;
@@ -4313,7 +4362,7 @@ var STEP_HANDLERS = {
4313
4362
  if (!("setInputFiles" in h.step)) unknownStep();
4314
4363
  const resolvedInput = await h.policy.resolveActionSelector(h.step.setInputFiles.selector);
4315
4364
  const rawFiles = h.step.setInputFiles.files;
4316
- const resolveFile = (f) => path8.isAbsolute(f) ? f : path8.join(h.context.configDir, f);
4365
+ const resolveFile = (f) => path9.isAbsolute(f) ? f : path9.join(h.context.configDir, f);
4317
4366
  const resolvedFiles = Array.isArray(rawFiles) ? rawFiles.map(resolveFile) : resolveFile(rawFiles);
4318
4367
  await h.driver.setInputFiles(resolvedInput.selector, resolvedFiles);
4319
4368
  h.policy.ensureLocationAllowed(h.driver);
@@ -4675,11 +4724,11 @@ var STEP_HANDLERS = {
4675
4724
  if (!responseFile) {
4676
4725
  throw new Error("mock.response must include either body or file");
4677
4726
  }
4678
- const candidateFilePath = path8.isAbsolute(responseFile) ? responseFile : path8.join(h.context.configDir, responseFile);
4679
- const resolvedConfigDir = path8.resolve(h.context.configDir);
4680
- const resolvedFilePath = path8.resolve(candidateFilePath);
4681
- const relativePath = path8.relative(resolvedConfigDir, resolvedFilePath);
4682
- const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relativePath);
4727
+ const candidateFilePath = path9.isAbsolute(responseFile) ? responseFile : path9.join(h.context.configDir, responseFile);
4728
+ const resolvedConfigDir = path9.resolve(h.context.configDir);
4729
+ const resolvedFilePath = path9.resolve(candidateFilePath);
4730
+ const relativePath = path9.relative(resolvedConfigDir, resolvedFilePath);
4731
+ const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${path9.sep}`) && !path9.isAbsolute(relativePath);
4683
4732
  if (!isWithinConfigDir) {
4684
4733
  throw new Error("mock.response.file must resolve within config directory");
4685
4734
  }
@@ -4746,7 +4795,7 @@ var STEP_HANDLERS = {
4746
4795
  capabilities: ["evaluate"],
4747
4796
  run: async (h) => {
4748
4797
  if (!("runScript" in h.step)) unknownStep();
4749
- const filePath = path8.isAbsolute(h.step.runScript.file) ? h.step.runScript.file : path8.join(h.context.configDir, h.step.runScript.file);
4798
+ const filePath = path9.isAbsolute(h.step.runScript.file) ? h.step.runScript.file : path9.join(h.context.configDir, h.step.runScript.file);
4750
4799
  const fileContents = fs8.readFileSync(filePath, "utf-8");
4751
4800
  await h.driver.evaluate(fileContents);
4752
4801
  return {
@@ -4768,11 +4817,11 @@ var STEP_HANDLERS = {
4768
4817
  const name = h.step.assertScreenshot.name;
4769
4818
  const threshold = h.step.assertScreenshot.threshold ?? 0.1;
4770
4819
  const baselineDir = ensureBaselineDir(h.context.configDir);
4771
- const baselinePath = path8.join(baselineDir, `${name}.png`);
4772
- const currentScreenshotPath = path8.join(h.context.runDir, "screenshots", `${name}-current.png`);
4773
- fs8.mkdirSync(path8.dirname(currentScreenshotPath), { recursive: true });
4820
+ const baselinePath = path9.join(baselineDir, `${name}.png`);
4821
+ const currentScreenshotPath = path9.join(h.context.runDir, "screenshots", `${name}-current.png`);
4822
+ fs8.mkdirSync(path9.dirname(currentScreenshotPath), { recursive: true });
4774
4823
  await h.driver.screenshot({ path: currentScreenshotPath, fullPage: true });
4775
- h.screenshots.push(path8.join("screenshots", `${name}-current.png`));
4824
+ h.screenshots.push(path9.join("screenshots", `${name}-current.png`));
4776
4825
  if (!fs8.existsSync(baselinePath)) {
4777
4826
  fs8.copyFileSync(currentScreenshotPath, baselinePath);
4778
4827
  return {
@@ -4780,7 +4829,7 @@ var STEP_HANDLERS = {
4780
4829
  result: { type: "assertScreenshot", status: "pass", durationMs: Date.now() - h.stepStart, value: "baseline created" }
4781
4830
  };
4782
4831
  }
4783
- const diffPath = path8.join(h.context.runDir, "screenshots", `${name}-diff.png`);
4832
+ const diffPath = path9.join(h.context.runDir, "screenshots", `${name}-diff.png`);
4784
4833
  const comparison = await compareScreenshots(baselinePath, currentScreenshotPath, diffPath, threshold);
4785
4834
  if (comparison.match) {
4786
4835
  return {
@@ -4793,7 +4842,7 @@ var STEP_HANDLERS = {
4793
4842
  }
4794
4843
  };
4795
4844
  }
4796
- h.screenshots.push(path8.join("screenshots", `${name}-diff.png`));
4845
+ h.screenshots.push(path9.join("screenshots", `${name}-diff.png`));
4797
4846
  throw new Error(
4798
4847
  `Visual regression: ${(comparison.diffPercentage * 100).toFixed(2)}% diff exceeds threshold ${(threshold * 100).toFixed(0)}%`
4799
4848
  );
@@ -4819,7 +4868,7 @@ var STEP_HANDLERS = {
4819
4868
  }
4820
4869
  const fileName = `assertWithAI_step_${h.index + 1}.png`;
4821
4870
  const relative = await h.addScreenshot(fileName);
4822
- const screenshotFullPath = path8.join(h.context.runDir, relative);
4871
+ const screenshotFullPath = path9.join(h.context.runDir, relative);
4823
4872
  const imageBase64 = fs8.readFileSync(screenshotFullPath).toString("base64");
4824
4873
  const assertVision = h.context.assertVision ?? assertWithAiVision;
4825
4874
  const verdict = await assertVision(
@@ -4877,7 +4926,7 @@ var STEP_HANDLERS = {
4877
4926
  `Download filename mismatch: expected "${opts.filename}", got "${suggestedFilename}"`
4878
4927
  );
4879
4928
  }
4880
- const savePath = path8.join(h.context.runDir, suggestedFilename);
4929
+ const savePath = path9.join(h.context.runDir, suggestedFilename);
4881
4930
  await download.saveAs(savePath);
4882
4931
  return {
4883
4932
  kind: "result",
@@ -4906,7 +4955,7 @@ async function executeSteps(context) {
4906
4955
  maxSteps: context.maxSteps,
4907
4956
  selfHealing: context.selfHealing
4908
4957
  });
4909
- const screenshotsDir = path8.join(context.runDir, "screenshots");
4958
+ const screenshotsDir = path9.join(context.runDir, "screenshots");
4910
4959
  fs8.mkdirSync(screenshotsDir, { recursive: true });
4911
4960
  const currentHuntName = context.huntStack?.[context.huntStack.length - 1];
4912
4961
  policy.assertWithinMaxSteps(context.steps.length, currentHuntName);
@@ -4917,7 +4966,7 @@ async function executeSteps(context) {
4917
4966
  const addScreenshot = async (fileName) => {
4918
4967
  const fullPath = screenshotPath(screenshotsDir, fileName);
4919
4968
  await captureScreenshot2(driver, fullPath);
4920
- const relative = path8.join("screenshots", fileName);
4969
+ const relative = path9.join("screenshots", fileName);
4921
4970
  screenshots.push(relative);
4922
4971
  return relative;
4923
4972
  };
@@ -5005,12 +5054,12 @@ async function executeSteps(context) {
5005
5054
  return { results, screenshots, failed: false };
5006
5055
  }
5007
5056
  async function captureFinalScreenshot(page, runDir) {
5008
- const screenshotsDir = path8.join(runDir, "screenshots");
5057
+ const screenshotsDir = path9.join(runDir, "screenshots");
5009
5058
  fs8.mkdirSync(screenshotsDir, { recursive: true });
5010
5059
  const fileName = "final.png";
5011
5060
  const filePath = screenshotPath(screenshotsDir, fileName);
5012
5061
  await captureScreenshot2(page, filePath);
5013
- return path8.join("screenshots", fileName);
5062
+ return path9.join("screenshots", fileName);
5014
5063
  }
5015
5064
 
5016
5065
  // src/runner/assertions.ts
@@ -5132,6 +5181,59 @@ async function evaluateAssertions(options) {
5132
5181
  }
5133
5182
  return results;
5134
5183
  }
5184
+ async function evaluateNativeAssertions(options) {
5185
+ const assertions = mergeAssertions(options.config, options.huntAssertions);
5186
+ const authoredTypes = new Set(
5187
+ (options.huntAssertions ?? []).map((assertion) => Object.keys(assertion)[0])
5188
+ );
5189
+ const results = [];
5190
+ const warnings = [];
5191
+ for (const assertion of assertions) {
5192
+ const type = Object.keys(assertion)[0] ?? "assertion";
5193
+ try {
5194
+ if ("selectorExists" in assertion) {
5195
+ options.assertAllowedSelector?.(assertion.selectorExists);
5196
+ const count = await options.driver.count(assertion.selectorExists);
5197
+ results.push({
5198
+ type: "selectorExists",
5199
+ value: assertion.selectorExists,
5200
+ status: count > 0 ? "pass" : "fail",
5201
+ error: count > 0 ? void 0 : "Selector not found"
5202
+ });
5203
+ continue;
5204
+ }
5205
+ if ("selectorNotExists" in assertion) {
5206
+ options.assertAllowedSelector?.(assertion.selectorNotExists);
5207
+ const count = await options.driver.count(assertion.selectorNotExists);
5208
+ results.push({
5209
+ type: "selectorNotExists",
5210
+ value: assertion.selectorNotExists,
5211
+ status: count === 0 ? "pass" : "fail",
5212
+ error: count === 0 ? void 0 : "Selector exists"
5213
+ });
5214
+ continue;
5215
+ }
5216
+ const rawValue = assertion[type];
5217
+ results.push({
5218
+ type,
5219
+ value: rawValue,
5220
+ status: "skipped",
5221
+ error: `skipped (web-only): not supported on the ${options.targetLabel} target`
5222
+ });
5223
+ if (authoredTypes.has(type)) {
5224
+ warnings.push(`${type} is web-only; skipped on ${options.targetLabel} target`);
5225
+ }
5226
+ } catch (error) {
5227
+ const detail = error instanceof Error ? error.message : "no error details";
5228
+ results.push({
5229
+ type,
5230
+ status: "fail",
5231
+ error: `Native assertion "${type}" failed: ${detail}`
5232
+ });
5233
+ }
5234
+ }
5235
+ return { results, warnings };
5236
+ }
5135
5237
 
5136
5238
  // src/runner/tracing.ts
5137
5239
  var DEFAULT_TRACE_HEADER = "traceparent";
@@ -5171,17 +5273,17 @@ function captureTraceCorrelation(response, headerName, sink, redactionValues = [
5171
5273
 
5172
5274
  // src/reporter/result.ts
5173
5275
  import fs9 from "fs";
5174
- import path9 from "path";
5276
+ import path10 from "path";
5175
5277
  function writeResult(runDir, result) {
5176
5278
  const fileName = "result.json";
5177
- const fullPath = path9.join(runDir, fileName);
5279
+ const fullPath = path10.join(runDir, fileName);
5178
5280
  fs9.writeFileSync(fullPath, JSON.stringify(result, null, 2));
5179
5281
  return fileName;
5180
5282
  }
5181
5283
 
5182
5284
  // src/reporter/summary.ts
5183
5285
  import fs10 from "fs";
5184
- import path10 from "path";
5286
+ import path11 from "path";
5185
5287
  function escapeMd(text) {
5186
5288
  return text.replace(/([|`*_{}[\]()#+\-!\\])/g, "\\$1");
5187
5289
  }
@@ -5259,7 +5361,7 @@ function writeSummary(runDir, result) {
5259
5361
  }
5260
5362
  }
5261
5363
  const fileName = "summary.md";
5262
- const fullPath = path10.join(runDir, fileName);
5364
+ const fullPath = path11.join(runDir, fileName);
5263
5365
  fs10.writeFileSync(fullPath, `${lines.join("\n")}
5264
5366
  `);
5265
5367
  return fileName;
@@ -5267,20 +5369,21 @@ function writeSummary(runDir, result) {
5267
5369
 
5268
5370
  // src/reporter/junit.ts
5269
5371
  import fs11 from "fs";
5270
- import path11 from "path";
5372
+ import path12 from "path";
5271
5373
  function escapeXml(text) {
5272
5374
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5273
5375
  }
5274
5376
  function writeJunit(runDir, result) {
5275
5377
  const totalTests = result.steps.length + result.assertions.length;
5276
5378
  const failures = result.steps.filter((s) => s.status === "fail").length + result.assertions.filter((a) => a.status === "fail").length;
5379
+ const skipped = result.assertions.filter((a) => a.status === "skipped").length;
5277
5380
  const timeSeconds = (result.durationMs / 1e3).toFixed(3);
5278
5381
  const huntName = escapeXml(result.hunt);
5279
5382
  const lines = [];
5280
5383
  lines.push('<?xml version="1.0" encoding="UTF-8"?>');
5281
5384
  lines.push("<testsuites>");
5282
5385
  lines.push(
5283
- ` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
5386
+ ` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" skipped="${skipped}" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
5284
5387
  );
5285
5388
  for (let i = 0; i < result.steps.length; i++) {
5286
5389
  const step = result.steps[i];
@@ -5304,6 +5407,11 @@ function writeJunit(runDir, result) {
5304
5407
  lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
5305
5408
  lines.push(` <failure message="${escapedFailureText}" type="assertion">${escapedFailureText}</failure>`);
5306
5409
  lines.push(" </testcase>");
5410
+ } else if (assertion.status === "skipped") {
5411
+ const skipText = escapeXml(assertion.error ?? "skipped");
5412
+ lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
5413
+ lines.push(` <skipped message="${skipText}"/>`);
5414
+ lines.push(" </testcase>");
5307
5415
  } else {
5308
5416
  lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0"/>`);
5309
5417
  }
@@ -5311,7 +5419,7 @@ function writeJunit(runDir, result) {
5311
5419
  lines.push(" </testsuite>");
5312
5420
  lines.push("</testsuites>");
5313
5421
  const fileName = "junit.xml";
5314
- const fullPath = path11.join(runDir, fileName);
5422
+ const fullPath = path12.join(runDir, fileName);
5315
5423
  fs11.writeFileSync(fullPath, `${lines.join("\n")}
5316
5424
  `);
5317
5425
  return fileName;
@@ -5354,11 +5462,11 @@ function parseViewportFlag(value) {
5354
5462
  return value;
5355
5463
  }
5356
5464
  function resolvePath(configDir, inputPath) {
5357
- if (path12.isAbsolute(inputPath)) {
5465
+ if (path13.isAbsolute(inputPath)) {
5358
5466
  return inputPath;
5359
5467
  }
5360
- const projectRoot = path12.dirname(configDir);
5361
- return path12.join(projectRoot, inputPath);
5468
+ const projectRoot = path13.dirname(configDir);
5469
+ return path13.join(projectRoot, inputPath);
5362
5470
  }
5363
5471
  function buildRunResult(options) {
5364
5472
  return {
@@ -5377,7 +5485,7 @@ function buildRunResult(options) {
5377
5485
  }
5378
5486
  function writeConsoleLog(runDir, entries) {
5379
5487
  const fileName = "console.log";
5380
- const filePath = path12.join(runDir, fileName);
5488
+ const filePath = path13.join(runDir, fileName);
5381
5489
  const lines = entries.map((entry) => {
5382
5490
  const location = entry.location ? ` (${entry.location})` : "";
5383
5491
  return `[${entry.type}] ${entry.text}${location}`;
@@ -5390,7 +5498,7 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
5390
5498
  const headless = options.headed ? false : config.browser.headless;
5391
5499
  const slowMo = options.slowMo ?? config.browser.slowMo;
5392
5500
  const maxSteps = config.guardrails.maxSteps;
5393
- const runDir = path12.join(configDir, "runs", timestamp());
5501
+ const runDir = path13.join(configDir, "runs", timestamp());
5394
5502
  fs12.mkdirSync(runDir, { recursive: true });
5395
5503
  const storageStatePath = config.auth.storageStatePath ? resolvePath(configDir, config.auth.storageStatePath) : void 0;
5396
5504
  const engine = options.browser ?? config.browser.engine;
@@ -5574,7 +5682,7 @@ async function runHunt(options) {
5574
5682
  }
5575
5683
  async function executeNativeHuntAttempt(options, config, configDir, interpolatedHunt, redactedFillSteps, randomVars, allowedApps, native) {
5576
5684
  const maxSteps = config.guardrails.maxSteps;
5577
- const runDir = path12.join(configDir, "runs", timestamp());
5685
+ const runDir = path13.join(configDir, "runs", timestamp());
5578
5686
  fs12.mkdirSync(runDir, { recursive: true });
5579
5687
  const session = await native.launchSession();
5580
5688
  let result;
@@ -5583,6 +5691,13 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
5583
5691
  const appIdentity = native.sessionAppIdentity(session);
5584
5692
  const targetLabel = `${native.targetType}:${appIdentity}`;
5585
5693
  const effectiveAllowedApps = [.../* @__PURE__ */ new Set([...allowedApps, native.targetApp, appIdentity])];
5694
+ const assertionPolicy = createRunPolicy(driver, {
5695
+ forbiddenSelectors: config.guardrails.forbiddenSelectors,
5696
+ allowedDomains: [],
5697
+ allowedApps: effectiveAllowedApps,
5698
+ maxSteps,
5699
+ selfHealing: config.guardrails.selfHealing
5700
+ });
5586
5701
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5587
5702
  const startTime = Date.now();
5588
5703
  let stepResults = [];
@@ -5622,8 +5737,19 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
5622
5737
  } catch {
5623
5738
  finalScreenshot = void 0;
5624
5739
  }
5740
+ const { results: assertionResults, warnings: assertionWarnings } = await evaluateNativeAssertions({
5741
+ driver,
5742
+ config,
5743
+ huntAssertions: interpolatedHunt.assertions,
5744
+ assertAllowedSelector: assertionPolicy.assertAllowedSelector,
5745
+ targetLabel: nativeTargetLabel(native.targetType)
5746
+ });
5747
+ for (const warning of assertionWarnings) {
5748
+ console.warn(warning);
5749
+ }
5625
5750
  const durationMs = Date.now() - startTime;
5626
- const status = stepFailed ? "fail" : "pass";
5751
+ const assertionsFailed = assertionResults.some((assertion) => assertion.status === "fail");
5752
+ const status = stepFailed || assertionsFailed ? "fail" : "pass";
5627
5753
  const artifacts = {
5628
5754
  screenshots: finalScreenshot ? [...stepScreenshots, finalScreenshot] : stepScreenshots
5629
5755
  };
@@ -5634,7 +5760,7 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
5634
5760
  hunt: options.huntName,
5635
5761
  targetUrl: targetLabel,
5636
5762
  steps: stepResults,
5637
- assertions: [],
5763
+ assertions: assertionResults,
5638
5764
  artifacts
5639
5765
  });
5640
5766
  result = writeReports(runDir, runResult, { junit: options.junit ?? config.artifacts.junit });
@@ -5747,7 +5873,6 @@ async function runNativeHunt(options, config, configDir, target, native) {
5747
5873
  const hunt = loadHunt(options.huntName, configDir);
5748
5874
  const { hunt: interpolatedHunt, redactedFillSteps, randomVars } = interpolateHunt(hunt, process.env);
5749
5875
  assertStepsSupportedByTarget(interpolatedHunt.steps, native.targetType);
5750
- assertHuntAssertionsSupportedByTarget(interpolatedHunt.assertions, native.targetType);
5751
5876
  native.assertAppAllowed(config.guardrails.allowedApps, target);
5752
5877
  const maxSteps = config.guardrails.maxSteps;
5753
5878
  if (interpolatedHunt.steps.length > maxSteps) {
@@ -5788,7 +5913,7 @@ async function runNativeHunt(options, config, configDir, target, native) {
5788
5913
  }
5789
5914
  function recordHistory(configDir, outcome, maxRuns) {
5790
5915
  try {
5791
- const relativeRunDir = path12.relative(configDir, outcome.runDir);
5916
+ const relativeRunDir = path13.relative(configDir, outcome.runDir);
5792
5917
  appendEntry(
5793
5918
  configDir,
5794
5919
  {
@@ -5910,7 +6035,7 @@ function clusterFailures(failures) {
5910
6035
 
5911
6036
  // src/backlog/index.ts
5912
6037
  import fs13 from "fs";
5913
- import path13 from "path";
6038
+ import path14 from "path";
5914
6039
 
5915
6040
  // src/backlog/parse.ts
5916
6041
  var MARKER_FP = /<!--\s*prowl:fp=([0-9a-f]+)/;
@@ -6016,7 +6141,7 @@ function buildFailure(hunt) {
6016
6141
  if (!hunt.runDir) return failure;
6017
6142
  let run;
6018
6143
  try {
6019
- const resultJson = readFileOrEmpty(path13.join(hunt.runDir, "result.json"));
6144
+ const resultJson = readFileOrEmpty(path14.join(hunt.runDir, "result.json"));
6020
6145
  if (!resultJson) return failure;
6021
6146
  run = JSON.parse(resultJson);
6022
6147
  } catch (error) {
@@ -6045,8 +6170,8 @@ function extractFailures(suiteResult) {
6045
6170
  }
6046
6171
  function updateBacklogFromSuite(suiteResult, options = {}) {
6047
6172
  const projectRoot = options.projectRoot ?? process.cwd();
6048
- const backlogPath = options.backlogPath ?? path13.join(projectRoot, "docs", "backlog.md");
6049
- const resolvedPath = options.resolvedPath ?? path13.join(projectRoot, "docs", "resolved.md");
6173
+ const backlogPath = options.backlogPath ?? path14.join(projectRoot, "docs", "backlog.md");
6174
+ const resolvedPath = options.resolvedPath ?? path14.join(projectRoot, "docs", "resolved.md");
6050
6175
  const date = options.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6051
6176
  const summary = { created: [], regressions: [], skipped: [], backlogPath };
6052
6177
  const failures = extractFailures(suiteResult);
@@ -6078,18 +6203,18 @@ function updateBacklogFromSuite(suiteResult, options = {}) {
6078
6203
  }
6079
6204
  }
6080
6205
  if (ticketsToAdd.length > 0) {
6081
- fs13.mkdirSync(path13.dirname(backlogPath), { recursive: true });
6206
+ fs13.mkdirSync(path14.dirname(backlogPath), { recursive: true });
6082
6207
  fs13.writeFileSync(backlogPath, insertTickets(backlogContent, ticketsToAdd));
6083
6208
  }
6084
6209
  return summary;
6085
6210
  }
6086
6211
 
6087
6212
  // src/runner/suite.ts
6088
- import path15 from "path";
6213
+ import path16 from "path";
6089
6214
 
6090
6215
  // src/reporter/ci-summary.ts
6091
6216
  import fs14 from "fs";
6092
- import path14 from "path";
6217
+ import path15 from "path";
6093
6218
  import chalk from "chalk";
6094
6219
  function countCiResults(results) {
6095
6220
  return {
@@ -6154,7 +6279,7 @@ function writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky = []
6154
6279
  ...clusters.length > 0 ? { clusters } : {}
6155
6280
  };
6156
6281
  fs14.mkdirSync(ciRunDir, { recursive: true });
6157
- const filePath = path14.join(ciRunDir, "ci-result.json");
6282
+ const filePath = path15.join(ciRunDir, "ci-result.json");
6158
6283
  fs14.writeFileSync(filePath, JSON.stringify(ciResult, null, 2) + "\n");
6159
6284
  return filePath;
6160
6285
  }
@@ -6340,7 +6465,7 @@ async function runSuite(options = {}) {
6340
6465
  const clusters = clusterFailures(
6341
6466
  extractFailures({ result: { hunts: results }, resultPath: null })
6342
6467
  ).filter((cluster) => cluster.count > 1);
6343
- const ciRunDir = path15.join(configDir, "runs", timestamp("ci"));
6468
+ const ciRunDir = path16.join(configDir, "runs", timestamp("ci"));
6344
6469
  const resultPath = writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky, clusters);
6345
6470
  const { passed, failed, skipped } = countCiResults(results);
6346
6471
  return {
@@ -7015,6 +7140,298 @@ async function generateHunt(options) {
7015
7140
  return yamlStr;
7016
7141
  }
7017
7142
 
7143
+ // src/browser/macdriver-install.ts
7144
+ import { execFile as execFile5 } from "child_process";
7145
+ import { createHash as createHash2 } from "crypto";
7146
+ import fs15 from "fs";
7147
+ import os5 from "os";
7148
+ import path17 from "path";
7149
+ import { promisify } from "util";
7150
+ var execFileAsync = promisify(execFile5);
7151
+ function parseChecksumFile(text) {
7152
+ const token = text.trim().split(/\s+/, 1)[0] ?? "";
7153
+ const digest = token.toLowerCase();
7154
+ if (!/^[0-9a-f]{64}$/.test(digest)) {
7155
+ throw new Error(`Malformed .sha256 checksum file (expected a 64-char hex digest, got: ${text.trim().slice(0, 80)})`);
7156
+ }
7157
+ return digest;
7158
+ }
7159
+ function sha256Hex(bytes) {
7160
+ return createHash2("sha256").update(bytes).digest("hex");
7161
+ }
7162
+ var commandRunner = async (file, args) => execFileAsync(file, args);
7163
+ function commandOutput(result) {
7164
+ return [result.stdout, result.stderr].filter((value) => value !== void 0).map((value) => value.toString()).join("\n").trim();
7165
+ }
7166
+ function commandErrorDetail(error) {
7167
+ const err = error;
7168
+ return [err.stderr, err.stdout, err.message].filter((value) => value !== void 0 && value !== "").map((value) => value.toString()).join("\n").trim();
7169
+ }
7170
+ async function runRequiredCommand(run, binaryPath, command, args, label) {
7171
+ try {
7172
+ return await run(command, args);
7173
+ } catch (error) {
7174
+ const detail = commandErrorDetail(error);
7175
+ throw new Error(`${label} failed for ${binaryPath}${detail ? `: ${detail}` : ""}`);
7176
+ }
7177
+ }
7178
+ var zipinfoArchiveLister = async (zipPath) => {
7179
+ try {
7180
+ const { stdout } = await execFileAsync("zipinfo", ["-1", zipPath]);
7181
+ return stdout.toString().split(/\r?\n/).filter((entry) => entry.length > 0);
7182
+ } catch (error) {
7183
+ const detail = commandErrorDetail(error);
7184
+ throw new Error(`Failed to inspect release archive ${zipPath} with zipinfo${detail ? `: ${detail}` : ""}`);
7185
+ }
7186
+ };
7187
+ function normalizeArchiveEntryName(entry) {
7188
+ if (entry.length === 0 || entry !== entry.trim() || entry.includes("\0") || entry.includes("\\")) {
7189
+ throw new Error(`Unsafe path in prowl-macdriver release archive: ${JSON.stringify(entry)}`);
7190
+ }
7191
+ if (entry.endsWith("/")) {
7192
+ throw new Error(`Unexpected directory in prowl-macdriver release archive: ${entry}`);
7193
+ }
7194
+ if (path17.posix.isAbsolute(entry)) {
7195
+ throw new Error(`Unsafe absolute path in prowl-macdriver release archive: ${entry}`);
7196
+ }
7197
+ const parts = entry.split("/");
7198
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
7199
+ throw new Error(`Unsafe path in prowl-macdriver release archive: ${entry}`);
7200
+ }
7201
+ return entry;
7202
+ }
7203
+ function validateMacdriverArchiveEntries(entries) {
7204
+ const normalized = entries.map(normalizeArchiveEntryName);
7205
+ if (normalized.length !== 1 || normalized[0] !== HELPER_BINARY) {
7206
+ const shown = normalized.length > 0 ? normalized.join(", ") : "(empty archive)";
7207
+ throw new Error(
7208
+ `Unexpected prowl-macdriver release archive contents: ${shown}. Expected exactly "${HELPER_BINARY}" at the archive root.`
7209
+ );
7210
+ }
7211
+ }
7212
+ var dittoExtractor = async (zipPath, destDir) => {
7213
+ await execFileAsync("ditto", ["-x", "-k", zipPath, destDir]);
7214
+ };
7215
+ function parseCodesignDetails(text) {
7216
+ const details = { identifier: null, authorities: [], teamIdentifier: null };
7217
+ for (const line of text.split(/\r?\n/)) {
7218
+ const trimmed = line.trim();
7219
+ if (trimmed.startsWith("Identifier=")) {
7220
+ details.identifier = trimmed.slice("Identifier=".length);
7221
+ } else if (trimmed.startsWith("Authority=")) {
7222
+ details.authorities.push(trimmed.slice("Authority=".length));
7223
+ } else if (trimmed.startsWith("TeamIdentifier=")) {
7224
+ details.teamIdentifier = trimmed.slice("TeamIdentifier=".length);
7225
+ }
7226
+ }
7227
+ return details;
7228
+ }
7229
+ function validateCodesignDetails(details, binaryPath) {
7230
+ if (details.identifier !== MACDRIVER_SIGNING_IDENTIFIER) {
7231
+ throw new Error(
7232
+ `codesign verification failed for ${binaryPath}: expected identifier "${MACDRIVER_SIGNING_IDENTIFIER}", got "${details.identifier ?? "missing"}"`
7233
+ );
7234
+ }
7235
+ const developerIdAuthority = details.authorities.find(
7236
+ (authority) => authority.startsWith(`${MACDRIVER_SIGNING_AUTHORITY_PREFIX} (`)
7237
+ );
7238
+ const authorityTeamId = developerIdAuthority?.match(/\(([A-Z0-9]{10})\)$/)?.[1] ?? null;
7239
+ if (!developerIdAuthority || !authorityTeamId) {
7240
+ const shown = details.authorities.length > 0 ? details.authorities.join(" / ") : "missing";
7241
+ throw new Error(
7242
+ `codesign verification failed for ${binaryPath}: expected ${MACDRIVER_SIGNING_AUTHORITY_PREFIX} signer, got ${shown}`
7243
+ );
7244
+ }
7245
+ if (!details.teamIdentifier) {
7246
+ throw new Error(`codesign verification failed for ${binaryPath}: missing TeamIdentifier`);
7247
+ }
7248
+ if (details.teamIdentifier !== authorityTeamId) {
7249
+ throw new Error(
7250
+ `codesign verification failed for ${binaryPath}: TeamIdentifier ${details.teamIdentifier} does not match Developer ID authority team ${authorityTeamId}`
7251
+ );
7252
+ }
7253
+ }
7254
+ async function verifyMacdriverSignature(binaryPath, run = commandRunner) {
7255
+ await runRequiredCommand(run, binaryPath, "codesign", ["--verify", "--strict", binaryPath], "codesign verification");
7256
+ const display = await runRequiredCommand(
7257
+ run,
7258
+ binaryPath,
7259
+ "codesign",
7260
+ ["--display", "--verbose=4", binaryPath],
7261
+ "codesign detail inspection"
7262
+ );
7263
+ validateCodesignDetails(parseCodesignDetails(commandOutput(display)), binaryPath);
7264
+ await runRequiredCommand(
7265
+ run,
7266
+ binaryPath,
7267
+ "spctl",
7268
+ ["--assess", "--type", "execute", "--verbose=4", binaryPath],
7269
+ "spctl assessment"
7270
+ );
7271
+ }
7272
+ var codesignVerifier = async (binaryPath) => {
7273
+ await verifyMacdriverSignature(binaryPath);
7274
+ };
7275
+ async function downloadAndVerify(options = {}) {
7276
+ const version = options.version ?? MACDRIVER_VERSION;
7277
+ const fetchImpl = options.fetchImpl ?? fetch;
7278
+ const assetName = macdriverAssetName(version);
7279
+ const zipUrl = macdriverAssetUrl(assetName, version);
7280
+ const sumUrl = macdriverAssetUrl(macdriverChecksumName(version), version);
7281
+ const [zipRes, sumRes] = await Promise.all([
7282
+ fetchImpl(zipUrl, { redirect: "follow" }),
7283
+ fetchImpl(sumUrl, { redirect: "follow" })
7284
+ ]);
7285
+ if (zipRes.status === 404 || sumRes.status === 404) {
7286
+ throw new Error(
7287
+ `No published prowl-macdriver release for ${macdriverReleaseTag(version)} yet.
7288
+ The signed binary is cut by the maintainer; until then build from source:
7289
+ cd macdriver && swift build -c release`
7290
+ );
7291
+ }
7292
+ if (!zipRes.ok) {
7293
+ throw new Error(`Failed to download ${assetName} (HTTP ${zipRes.status}) from ${zipUrl}`);
7294
+ }
7295
+ if (!sumRes.ok) {
7296
+ throw new Error(`Failed to download the checksum (HTTP ${sumRes.status}) from ${sumUrl}`);
7297
+ }
7298
+ const zipBytes = Buffer.from(await zipRes.arrayBuffer());
7299
+ const expected = parseChecksumFile(await sumRes.text());
7300
+ const actual = sha256Hex(zipBytes);
7301
+ if (actual !== expected) {
7302
+ throw new Error(
7303
+ `Checksum mismatch for ${assetName}.
7304
+ expected: ${expected}
7305
+ actual: ${actual}
7306
+ The download was rejected and discarded; re-run the install, and report it if it repeats.`
7307
+ );
7308
+ }
7309
+ return zipBytes;
7310
+ }
7311
+ async function installMacdriver(options = {}) {
7312
+ const version = validateMacdriverVersion(options.version ?? MACDRIVER_VERSION);
7313
+ const homedir = options.homedir ?? os5.homedir();
7314
+ const listArchiveEntries = options.listArchiveEntries ?? zipinfoArchiveLister;
7315
+ const extract = options.extract ?? dittoExtractor;
7316
+ const verifySignature = options.verifySignature ?? codesignVerifier;
7317
+ const installRoot = macdriverInstallRoot(homedir);
7318
+ const versionDir = macdriverVersionDir(version, homedir);
7319
+ const binaryPath = macdriverInstalledBinary(version, homedir);
7320
+ if (!options.force && fs15.existsSync(binaryPath)) {
7321
+ return { version, binaryPath, alreadyInstalled: true };
7322
+ }
7323
+ const zipBytes = await downloadAndVerify({ version, fetchImpl: options.fetchImpl });
7324
+ fs15.mkdirSync(installRoot, { recursive: true });
7325
+ const stagingDir = fs15.mkdtempSync(path17.join(installRoot, `.tmp-${version}-`));
7326
+ const extractDir = path17.join(stagingDir, "extract");
7327
+ const zipPath = path17.join(stagingDir, macdriverAssetName(version));
7328
+ try {
7329
+ fs15.mkdirSync(extractDir);
7330
+ fs15.writeFileSync(zipPath, zipBytes);
7331
+ validateMacdriverArchiveEntries(await listArchiveEntries(zipPath));
7332
+ await extract(zipPath, extractDir);
7333
+ const stagedBinaryPath = path17.join(extractDir, HELPER_BINARY);
7334
+ assertExtractedHelper(extractDir, stagedBinaryPath);
7335
+ fs15.chmodSync(stagedBinaryPath, 493);
7336
+ await verifySignature(stagedBinaryPath);
7337
+ replaceVersionDir(versionDir, extractDir, installRoot);
7338
+ } finally {
7339
+ fs15.rmSync(stagingDir, { recursive: true, force: true });
7340
+ }
7341
+ return { version, binaryPath, alreadyInstalled: false };
7342
+ }
7343
+ function assertExtractedHelper(extractDir, binaryPath) {
7344
+ const entries = fs15.readdirSync(extractDir);
7345
+ if (!entries.includes(HELPER_BINARY)) {
7346
+ throw new Error(`The release archive did not contain a "${HELPER_BINARY}" binary.`);
7347
+ }
7348
+ if (entries.length !== 1 || entries[0] !== HELPER_BINARY) {
7349
+ const shown = entries.length > 0 ? entries.join(", ") : "(empty directory)";
7350
+ throw new Error(
7351
+ `Unexpected extracted prowl-macdriver archive contents: ${shown}. Expected exactly "${HELPER_BINARY}".`
7352
+ );
7353
+ }
7354
+ let stat = null;
7355
+ try {
7356
+ stat = fs15.lstatSync(binaryPath);
7357
+ } catch {
7358
+ }
7359
+ if (!stat?.isFile()) {
7360
+ throw new Error(`The release archive "${HELPER_BINARY}" entry is not a regular file.`);
7361
+ }
7362
+ }
7363
+ function replaceVersionDir(versionDir, stagedVersionDir, installRoot) {
7364
+ const backupDir = path17.join(installRoot, `.previous-${path17.basename(versionDir)}-${process.pid}-${Date.now()}`);
7365
+ let backedUp = false;
7366
+ try {
7367
+ if (fs15.existsSync(versionDir)) {
7368
+ fs15.renameSync(versionDir, backupDir);
7369
+ backedUp = true;
7370
+ }
7371
+ fs15.renameSync(stagedVersionDir, versionDir);
7372
+ if (backedUp) {
7373
+ fs15.rmSync(backupDir, { recursive: true, force: true });
7374
+ }
7375
+ } catch (error) {
7376
+ if (backedUp && !fs15.existsSync(versionDir) && fs15.existsSync(backupDir)) {
7377
+ fs15.renameSync(backupDir, versionDir);
7378
+ }
7379
+ throw error;
7380
+ }
7381
+ }
7382
+ var runVersionProbe = async (binaryPath) => {
7383
+ try {
7384
+ const { stdout } = await execFileAsync(binaryPath, ["version"], { timeout: 5e3 });
7385
+ const match = stdout.match(/prowl-macdriver\s+(\S+)/);
7386
+ return match ? match[1] : stdout.trim() || null;
7387
+ } catch {
7388
+ return null;
7389
+ }
7390
+ };
7391
+ async function collectMacdriverStatus(options = {}) {
7392
+ const env = options.env ?? process.env;
7393
+ const homedir = options.homedir ?? os5.homedir();
7394
+ const probe = options.probe ?? runVersionProbe;
7395
+ let resolved = null;
7396
+ try {
7397
+ const resolvedPath = resolveHelperBinary(env, { homedir });
7398
+ resolved = { path: resolvedPath, source: classifyResolvedSource(resolvedPath, env, homedir) };
7399
+ } catch {
7400
+ resolved = null;
7401
+ }
7402
+ const installed = [];
7403
+ const root = macdriverInstallRoot(homedir);
7404
+ if (fs15.existsSync(root)) {
7405
+ for (const entry of fs15.readdirSync(root, { withFileTypes: true })) {
7406
+ if (!entry.isDirectory()) continue;
7407
+ let binaryPath;
7408
+ try {
7409
+ binaryPath = macdriverInstalledBinary(entry.name, homedir);
7410
+ } catch {
7411
+ continue;
7412
+ }
7413
+ if (fs15.existsSync(binaryPath)) {
7414
+ installed.push({ version: entry.name, binaryPath });
7415
+ }
7416
+ }
7417
+ installed.sort((a, b) => a.version.localeCompare(b.version));
7418
+ }
7419
+ const probedVersion = resolved ? await probe(resolved.path) : null;
7420
+ return { resolved, pinnedVersion: MACDRIVER_VERSION, installed, probedVersion };
7421
+ }
7422
+ function classifyResolvedSource(resolvedPath, env, homedir) {
7423
+ if (env.PROWL_MACDRIVER_BIN && resolvedPath === env.PROWL_MACDRIVER_BIN) {
7424
+ return "env";
7425
+ }
7426
+ if (resolvedPath.startsWith(macdriverInstallRoot(homedir) + path17.sep)) {
7427
+ return "user-install";
7428
+ }
7429
+ return "source-build";
7430
+ }
7431
+ function tccGuidance() {
7432
+ 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.";
7433
+ }
7434
+
7018
7435
  export {
7019
7436
  interpolateHunt,
7020
7437
  WEB_ONLY_STEP_TYPES,
@@ -7032,6 +7449,20 @@ export {
7032
7449
  createPlaywrightDriver,
7033
7450
  parseMacSelector,
7034
7451
  createMacDriver,
7452
+ HELPER_BINARY,
7453
+ MACDRIVER_VERSION,
7454
+ MACDRIVER_REPO,
7455
+ MACDRIVER_SIGNING_IDENTIFIER,
7456
+ MACDRIVER_SIGNING_AUTHORITY_PREFIX,
7457
+ MACDRIVER_VERSION_PATTERN,
7458
+ validateMacdriverVersion,
7459
+ macdriverReleaseTag,
7460
+ macdriverAssetName,
7461
+ macdriverChecksumName,
7462
+ macdriverAssetUrl,
7463
+ macdriverInstallRoot,
7464
+ macdriverVersionDir,
7465
+ macdriverInstalledBinary,
7035
7466
  macdriverBuildInstructions,
7036
7467
  resolveHelperBinary,
7037
7468
  DEFAULT_REQUEST_TIMEOUT_MS,
@@ -7145,6 +7576,19 @@ export {
7145
7576
  matchIosSelector,
7146
7577
  isIosInteractive,
7147
7578
  analyzeIosApp,
7148
- generateHunt
7579
+ generateHunt,
7580
+ parseChecksumFile,
7581
+ sha256Hex,
7582
+ zipinfoArchiveLister,
7583
+ validateMacdriverArchiveEntries,
7584
+ dittoExtractor,
7585
+ parseCodesignDetails,
7586
+ verifyMacdriverSignature,
7587
+ codesignVerifier,
7588
+ downloadAndVerify,
7589
+ installMacdriver,
7590
+ runVersionProbe,
7591
+ collectMacdriverStatus,
7592
+ tccGuidance
7149
7593
  };
7150
- //# sourceMappingURL=chunk-5KQR3IR3.js.map
7594
+ //# sourceMappingURL=chunk-R7NUH44M.js.map