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/index.cjs CHANGED
@@ -43,18 +43,33 @@ var init_types = __esm({
43
43
  function isValidHuntName(name) {
44
44
  return HUNT_NAME_PATTERN.test(name);
45
45
  }
46
+ function normalizeHuntName(input) {
47
+ let name = input.trim();
48
+ if (name.startsWith("./")) {
49
+ name = name.slice(2);
50
+ }
51
+ const isSupportedHuntPath = HUNT_FILE_EXTENSION_PATTERN.test(name);
52
+ if (isSupportedHuntPath && name.startsWith(".prowl/hunts/")) {
53
+ name = name.slice(".prowl/hunts/".length);
54
+ } else if (isSupportedHuntPath && name.startsWith("hunts/")) {
55
+ name = name.slice("hunts/".length);
56
+ }
57
+ name = name.replace(HUNT_FILE_EXTENSION_PATTERN, "");
58
+ return name;
59
+ }
46
60
  function assertValidHuntName(name) {
47
61
  if (!isValidHuntName(name)) {
48
62
  throw new Error(
49
- `Invalid hunt name: "${name}". Use only letters, numbers, hyphens, underscores, and forward slashes.`
63
+ `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).`
50
64
  );
51
65
  }
52
66
  }
53
- var HUNT_NAME_PATTERN;
67
+ var HUNT_NAME_PATTERN, HUNT_FILE_EXTENSION_PATTERN;
54
68
  var init_hunt_name = __esm({
55
69
  "src/config/hunt-name.ts"() {
56
70
  "use strict";
57
71
  HUNT_NAME_PATTERN = /^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/;
72
+ HUNT_FILE_EXTENSION_PATTERN = /\.yml$/i;
58
73
  }
59
74
  });
60
75
 
@@ -699,29 +714,29 @@ async function compareScreenshots(baselinePath, currentPath, diffPath, threshold
699
714
  };
700
715
  }
701
716
  function ensureBaselineDir(configDir) {
702
- const baselineDir = import_node_path8.default.join(configDir, "baselines");
717
+ const baselineDir = import_node_path9.default.join(configDir, "baselines");
703
718
  import_node_fs8.default.mkdirSync(baselineDir, { recursive: true });
704
719
  return baselineDir;
705
720
  }
706
- var import_node_fs8, import_node_path8, import_pngjs, import_pixelmatch;
721
+ var import_node_fs8, import_node_path9, import_pngjs, import_pixelmatch;
707
722
  var init_visual = __esm({
708
723
  "src/runner/visual.ts"() {
709
724
  "use strict";
710
725
  import_node_fs8 = __toESM(require("fs"), 1);
711
- import_node_path8 = __toESM(require("path"), 1);
726
+ import_node_path9 = __toESM(require("path"), 1);
712
727
  import_pngjs = require("pngjs");
713
728
  import_pixelmatch = __toESM(require("pixelmatch"), 1);
714
729
  }
715
730
  });
716
731
 
717
732
  // src/cli/program.ts
718
- var import_commander13 = require("commander");
733
+ var import_commander14 = require("commander");
719
734
 
720
735
  // package.json
721
736
  var package_default = {
722
737
  name: "prowl-tools",
723
- version: "0.1.6",
724
- description: "CLI-first QA testing tool for deterministic Playwright flows.",
738
+ version: "0.1.7",
739
+ description: "E2E testing for native macOS apps and web apps from declarative YAML hunts.",
725
740
  type: "module",
726
741
  license: "Apache-2.0",
727
742
  author: "Prowl Tools",
@@ -736,9 +751,12 @@ var package_default = {
736
751
  keywords: [
737
752
  "testing",
738
753
  "qa",
754
+ "e2e",
755
+ "macos",
756
+ "desktop-testing",
757
+ "accessibility",
739
758
  "playwright",
740
759
  "yaml",
741
- "e2e",
742
760
  "browser-testing",
743
761
  "automation",
744
762
  "web-testing",
@@ -812,7 +830,7 @@ var import_chalk3 = __toESM(require("chalk"), 1);
812
830
 
813
831
  // src/runner/index.ts
814
832
  var import_node_fs14 = __toESM(require("fs"), 1);
815
- var import_node_path14 = __toESM(require("path"), 1);
833
+ var import_node_path15 = __toESM(require("path"), 1);
816
834
  init_loader();
817
835
 
818
836
  // src/config/interpolate.ts
@@ -1606,7 +1624,8 @@ function createPlaywrightDriver(page) {
1606
1624
  // src/browser/mac-helper.ts
1607
1625
  var import_node_child_process2 = require("child_process");
1608
1626
  var import_node_fs4 = __toESM(require("fs"), 1);
1609
- var import_node_path4 = __toESM(require("path"), 1);
1627
+ var import_node_os2 = __toESM(require("os"), 1);
1628
+ var import_node_path5 = __toESM(require("path"), 1);
1610
1629
  var import_node_url = require("url");
1611
1630
 
1612
1631
  // src/browser/mac-driver.ts
@@ -1785,24 +1804,67 @@ function createMacDriver(client, options = {}) {
1785
1804
  };
1786
1805
  }
1787
1806
 
1807
+ // src/browser/macdriver-release.ts
1808
+ var import_node_os = __toESM(require("os"), 1);
1809
+ var import_node_path4 = __toESM(require("path"), 1);
1810
+ var HELPER_BINARY = "prowl-macdriver";
1811
+ var MACDRIVER_VERSION = "0.1.0";
1812
+ var MACDRIVER_REPO = "prowl-tools/prowl";
1813
+ var MACDRIVER_SIGNING_IDENTIFIER = "tools.prowl.macdriver";
1814
+ var MACDRIVER_SIGNING_AUTHORITY_PREFIX = "Developer ID Application: Genkei Labs";
1815
+ var MACDRIVER_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
1816
+ function validateMacdriverVersion(version) {
1817
+ if (!MACDRIVER_VERSION_PATTERN.test(version)) {
1818
+ throw new Error(
1819
+ `Invalid prowl-macdriver version "${version}". Expected a release version like 0.1.0.`
1820
+ );
1821
+ }
1822
+ return version;
1823
+ }
1824
+ function macdriverReleaseTag(version = MACDRIVER_VERSION) {
1825
+ return `macdriver-v${validateMacdriverVersion(version)}`;
1826
+ }
1827
+ function macdriverAssetName(version = MACDRIVER_VERSION) {
1828
+ return `prowl-macdriver-v${validateMacdriverVersion(version)}-universal.zip`;
1829
+ }
1830
+ function macdriverChecksumName(version = MACDRIVER_VERSION) {
1831
+ return `${macdriverAssetName(version)}.sha256`;
1832
+ }
1833
+ function macdriverAssetUrl(assetName, version = MACDRIVER_VERSION) {
1834
+ return `https://github.com/${MACDRIVER_REPO}/releases/download/${macdriverReleaseTag(version)}/${assetName}`;
1835
+ }
1836
+ function macdriverInstallRoot(homedir = import_node_os.default.homedir()) {
1837
+ return import_node_path4.default.join(homedir, ".prowl", "macdriver");
1838
+ }
1839
+ function macdriverVersionDir(version = MACDRIVER_VERSION, homedir = import_node_os.default.homedir()) {
1840
+ const root = import_node_path4.default.resolve(macdriverInstallRoot(homedir));
1841
+ const versionDir = import_node_path4.default.resolve(root, validateMacdriverVersion(version));
1842
+ if (!versionDir.startsWith(root + import_node_path4.default.sep)) {
1843
+ throw new Error(`Resolved prowl-macdriver version directory escaped install root: ${versionDir}`);
1844
+ }
1845
+ return versionDir;
1846
+ }
1847
+ function macdriverInstalledBinary(version = MACDRIVER_VERSION, homedir = import_node_os.default.homedir()) {
1848
+ return import_node_path4.default.join(macdriverVersionDir(version, homedir), HELPER_BINARY);
1849
+ }
1850
+
1788
1851
  // src/browser/mac-helper.ts
1789
1852
  var import_meta = {};
1790
- var HELPER_BINARY = "prowl-macdriver";
1791
1853
  function macdriverBuildInstructions() {
1792
- 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.";
1854
+ 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.";
1793
1855
  }
1794
1856
  function getPackageRoot() {
1795
- let dir = import_node_path4.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
1796
- const root = import_node_path4.default.parse(dir).root;
1857
+ let dir = import_node_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
1858
+ const root = import_node_path5.default.parse(dir).root;
1797
1859
  while (dir !== root) {
1798
- if (import_node_fs4.default.existsSync(import_node_path4.default.join(dir, "package.json"))) {
1860
+ if (import_node_fs4.default.existsSync(import_node_path5.default.join(dir, "package.json"))) {
1799
1861
  return dir;
1800
1862
  }
1801
- dir = import_node_path4.default.dirname(dir);
1863
+ dir = import_node_path5.default.dirname(dir);
1802
1864
  }
1803
1865
  return root;
1804
1866
  }
1805
- function resolveHelperBinary(env = process.env) {
1867
+ function resolveHelperBinary(env = process.env, options = {}) {
1806
1868
  const override = env.PROWL_MACDRIVER_BIN;
1807
1869
  if (override) {
1808
1870
  if (!import_node_fs4.default.existsSync(override)) {
@@ -1813,10 +1875,15 @@ ${macdriverBuildInstructions()}`
1813
1875
  }
1814
1876
  return override;
1815
1877
  }
1878
+ const homedir = options.homedir ?? import_node_os2.default.homedir();
1879
+ const userBinary = macdriverInstalledBinary(MACDRIVER_VERSION, homedir);
1880
+ if (import_node_fs4.default.existsSync(userBinary)) {
1881
+ return userBinary;
1882
+ }
1816
1883
  const root = getPackageRoot();
1817
1884
  const candidates = [
1818
- import_node_path4.default.join(root, "macdriver", ".build", "release", HELPER_BINARY),
1819
- import_node_path4.default.join(root, "macdriver", ".build", "debug", HELPER_BINARY)
1885
+ import_node_path5.default.join(root, "macdriver", ".build", "release", HELPER_BINARY),
1886
+ import_node_path5.default.join(root, "macdriver", ".build", "debug", HELPER_BINARY)
1820
1887
  ];
1821
1888
  for (const candidate of candidates) {
1822
1889
  if (import_node_fs4.default.existsSync(candidate)) {
@@ -1992,7 +2059,7 @@ async function closeMacSession(session) {
1992
2059
  var import_node_module = require("module");
1993
2060
  var import_node_child_process4 = require("child_process");
1994
2061
  var import_node_fs6 = __toESM(require("fs"), 1);
1995
- var import_node_path5 = __toESM(require("path"), 1);
2062
+ var import_node_path6 = __toESM(require("path"), 1);
1996
2063
 
1997
2064
  // src/browser/android-driver.ts
1998
2065
  var import_node_fs5 = __toESM(require("fs"), 1);
@@ -2678,12 +2745,12 @@ var Uia2Transport = class {
2678
2745
  * {@link Uia2HttpError} on a non-2xx response, or a timeout error when the
2679
2746
  * per-request deadline elapses.
2680
2747
  */
2681
- async request(method, path25, body, timeoutMs) {
2748
+ async request(method, path27, body, timeoutMs) {
2682
2749
  const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
2683
2750
  const controller = new AbortController();
2684
2751
  const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
2685
2752
  timer.unref?.();
2686
- const url = `${this.baseUrl}${path25}`;
2753
+ const url = `${this.baseUrl}${path27}`;
2687
2754
  let response;
2688
2755
  try {
2689
2756
  response = await this.fetchImpl(url, {
@@ -2695,7 +2762,7 @@ var Uia2Transport = class {
2695
2762
  } catch (error) {
2696
2763
  if (controller.signal.aborted) {
2697
2764
  const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
2698
- throw new Error(`uiautomator2 request ${method} ${path25} timed out after ${shown}`);
2765
+ throw new Error(`uiautomator2 request ${method} ${path27} timed out after ${shown}`);
2699
2766
  }
2700
2767
  throw error instanceof Error ? error : new Error(String(error));
2701
2768
  } finally {
@@ -2706,7 +2773,7 @@ var Uia2Transport = class {
2706
2773
  if (!response.ok) {
2707
2774
  const wdError = extractWebdriverError(parsed);
2708
2775
  throw new Uia2HttpError(
2709
- `uiautomator2 ${method} ${path25} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
2776
+ `uiautomator2 ${method} ${path27} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
2710
2777
  response.status,
2711
2778
  wdError
2712
2779
  );
@@ -2789,10 +2856,10 @@ function sleep(ms) {
2789
2856
  }
2790
2857
  function createUia2AgentClient(transport, sessionId, options = {}) {
2791
2858
  const base = `/session/${sessionId}`;
2792
- async function locate(query, path25) {
2859
+ async function locate(query, path27) {
2793
2860
  return transport.request(
2794
2861
  "POST",
2795
- `${base}${path25}`,
2862
+ `${base}${path27}`,
2796
2863
  androidQueryToLocator(query, { appPackage: options.appPackage })
2797
2864
  );
2798
2865
  }
@@ -2859,10 +2926,10 @@ function resolveAgentApks(requireFn = (0, import_node_module.createRequire)(impo
2859
2926
  "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"
2860
2927
  );
2861
2928
  }
2862
- const pkgDir = import_node_path5.default.dirname(pkgJsonPath);
2929
+ const pkgDir = import_node_path6.default.dirname(pkgJsonPath);
2863
2930
  const version = requireFn(pkgJsonPath).version;
2864
- const serverApk = import_node_path5.default.join(pkgDir, "apks", `appium-uiautomator2-server-v${version}.apk`);
2865
- const testApk = import_node_path5.default.join(pkgDir, "apks", "appium-uiautomator2-server-debug-androidTest.apk");
2931
+ const serverApk = import_node_path6.default.join(pkgDir, "apks", `appium-uiautomator2-server-v${version}.apk`);
2932
+ const testApk = import_node_path6.default.join(pkgDir, "apks", "appium-uiautomator2-server-debug-androidTest.apk");
2866
2933
  for (const apk of [serverApk, testApk]) {
2867
2934
  if (!import_node_fs6.default.existsSync(apk)) {
2868
2935
  throw new Error(`Expected uiautomator2 agent APK is missing: ${apk}. Reinstall dependencies.`);
@@ -2910,7 +2977,7 @@ async function resolvePackage(app, runner, serial, aaptResolver, allowedApps) {
2910
2977
  assertAndroidAppAllowed(allowedApps, app);
2911
2978
  return app;
2912
2979
  }
2913
- const apkPath = import_node_path5.default.resolve(app);
2980
+ const apkPath = import_node_path6.default.resolve(app);
2914
2981
  if (!import_node_fs6.default.existsSync(apkPath)) {
2915
2982
  throw new Error(`APK not found: ${apkPath}`);
2916
2983
  }
@@ -2986,8 +3053,8 @@ async function closeAndroidSession(session) {
2986
3053
  var import_node_module2 = require("module");
2987
3054
  var import_node_child_process6 = require("child_process");
2988
3055
  var import_node_fs7 = __toESM(require("fs"), 1);
2989
- var import_node_os2 = __toESM(require("os"), 1);
2990
- var import_node_path7 = __toESM(require("path"), 1);
3056
+ var import_node_os4 = __toESM(require("os"), 1);
3057
+ var import_node_path8 = __toESM(require("path"), 1);
2991
3058
 
2992
3059
  // src/browser/ios-driver.ts
2993
3060
  var IOS_CAPABILITIES = /* @__PURE__ */ new Set([
@@ -3211,8 +3278,8 @@ function createIosDriver(client, options) {
3211
3278
  var import_node_child_process5 = require("child_process");
3212
3279
  var import_promises = require("fs/promises");
3213
3280
  var import_node_net = __toESM(require("net"), 1);
3214
- var import_node_os = __toESM(require("os"), 1);
3215
- var import_node_path6 = __toESM(require("path"), 1);
3281
+ var import_node_os3 = __toESM(require("os"), 1);
3282
+ var import_node_path7 = __toESM(require("path"), 1);
3216
3283
  var spawnXcrunProcess = (args, options) => {
3217
3284
  const child = (0, import_node_child_process5.spawn)("xcrun", args, {
3218
3285
  stdio: "ignore",
@@ -3226,7 +3293,7 @@ var spawnXcrunProcess = (args, options) => {
3226
3293
  }
3227
3294
  };
3228
3295
  };
3229
- var DEFAULT_SIMULATOR_LOCK_ROOT = import_node_path6.default.join(import_node_os.default.tmpdir(), "prowl-ios-simulator-locks");
3296
+ var DEFAULT_SIMULATOR_LOCK_ROOT = import_node_path7.default.join(import_node_os3.default.tmpdir(), "prowl-ios-simulator-locks");
3230
3297
  var SIMULATOR_LOCK_OWNER_FILE = "owner.json";
3231
3298
  var execFileXcrunRunner = (args, options) => new Promise((resolve) => {
3232
3299
  (0, import_node_child_process5.execFile)(
@@ -3266,7 +3333,7 @@ function isProcessAlive(pid) {
3266
3333
  }
3267
3334
  async function removeStaleSimulatorLock(lockPath) {
3268
3335
  try {
3269
- const ownerText = await (0, import_promises.readFile)(import_node_path6.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE), "utf8");
3336
+ const ownerText = await (0, import_promises.readFile)(import_node_path7.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE), "utf8");
3270
3337
  const owner = JSON.parse(ownerText);
3271
3338
  if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0 && !isProcessAlive(owner.pid)) {
3272
3339
  await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
@@ -3284,7 +3351,7 @@ function simulatorReservedError(udid) {
3284
3351
  }
3285
3352
  async function reserveSimulatorUdid(udid, options = {}) {
3286
3353
  const lockRoot = options.lockRoot ?? DEFAULT_SIMULATOR_LOCK_ROOT;
3287
- const lockPath = import_node_path6.default.join(lockRoot, simulatorLockName(udid));
3354
+ const lockPath = import_node_path7.default.join(lockRoot, simulatorLockName(udid));
3288
3355
  await (0, import_promises.mkdir)(lockRoot, { recursive: true });
3289
3356
  for (let attempt = 0; attempt < 2; attempt += 1) {
3290
3357
  try {
@@ -3308,7 +3375,7 @@ async function reserveSimulatorUdid(udid, options = {}) {
3308
3375
  };
3309
3376
  try {
3310
3377
  await (0, import_promises.writeFile)(
3311
- import_node_path6.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE),
3378
+ import_node_path7.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE),
3312
3379
  `${JSON.stringify({ pid: process.pid, udid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
3313
3380
  `,
3314
3381
  { flag: "wx" }
@@ -3500,15 +3567,15 @@ var WdaTransport = class {
3500
3567
  * {@link WdaHttpError} on a non-2xx response, or a timeout error when the
3501
3568
  * per-request deadline elapses.
3502
3569
  */
3503
- async requestFull(method, path25, body, timeoutMs) {
3570
+ async requestFull(method, path27, body, timeoutMs) {
3504
3571
  const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
3505
3572
  const controller = new AbortController();
3506
3573
  const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
3507
3574
  timer.unref?.();
3508
- const url = `${this.baseUrl}${path25}`;
3575
+ const url = `${this.baseUrl}${path27}`;
3509
3576
  const timeoutError = () => {
3510
3577
  const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
3511
- return new Error(`WebDriverAgent request ${method} ${path25} timed out after ${shown}`);
3578
+ return new Error(`WebDriverAgent request ${method} ${path27} timed out after ${shown}`);
3512
3579
  };
3513
3580
  let response;
3514
3581
  try {
@@ -3540,7 +3607,7 @@ var WdaTransport = class {
3540
3607
  if (!response.ok) {
3541
3608
  const wdError = extractWebdriverError2(parsed);
3542
3609
  throw new WdaHttpError(
3543
- `WebDriverAgent ${method} ${path25} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
3610
+ `WebDriverAgent ${method} ${path27} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
3544
3611
  response.status,
3545
3612
  wdError
3546
3613
  );
@@ -3548,8 +3615,8 @@ var WdaTransport = class {
3548
3615
  return parsed;
3549
3616
  }
3550
3617
  /** Like {@link requestFull} but returns just the `value` field. */
3551
- async request(method, path25, body, timeoutMs) {
3552
- const parsed = await this.requestFull(method, path25, body, timeoutMs);
3618
+ async request(method, path27, body, timeoutMs) {
3619
+ const parsed = await this.requestFull(method, path27, body, timeoutMs);
3553
3620
  return parsed?.value;
3554
3621
  }
3555
3622
  };
@@ -3625,8 +3692,8 @@ function sleep2(ms) {
3625
3692
  }
3626
3693
  function createWdaAgentClient(transport, sessionId) {
3627
3694
  const base = `/session/${sessionId}`;
3628
- async function locate(query, path25) {
3629
- return transport.request("POST", `${base}${path25}`, iosQueryToLocator(query));
3695
+ async function locate(query, path27) {
3696
+ return transport.request("POST", `${base}${path27}`, iosQueryToLocator(query));
3630
3697
  }
3631
3698
  return {
3632
3699
  async findElement(query) {
@@ -3702,19 +3769,19 @@ function resolveWdaProject(requireFn = (0, import_node_module2.createRequire)(im
3702
3769
  "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"
3703
3770
  );
3704
3771
  }
3705
- const pkgDir = import_node_path7.default.dirname(pkgJsonPath);
3772
+ const pkgDir = import_node_path8.default.dirname(pkgJsonPath);
3706
3773
  const version = requireFn(pkgJsonPath).version;
3707
- const projectPath = import_node_path7.default.join(pkgDir, "WebDriverAgent.xcodeproj");
3774
+ const projectPath = import_node_path8.default.join(pkgDir, "WebDriverAgent.xcodeproj");
3708
3775
  if (!import_node_fs7.default.existsSync(projectPath)) {
3709
3776
  throw new Error(`Expected WebDriverAgent project is missing: ${projectPath}. Reinstall dependencies.`);
3710
3777
  }
3711
3778
  return { projectPath, version };
3712
3779
  }
3713
- function wdaCacheDir(wdaVersion, xcode, homeDir = import_node_os2.default.homedir()) {
3714
- return import_node_path7.default.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
3780
+ function wdaCacheDir(wdaVersion, xcode, homeDir = import_node_os4.default.homedir()) {
3781
+ return import_node_path8.default.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
3715
3782
  }
3716
3783
  function productsDir(derivedDataPath) {
3717
- return import_node_path7.default.join(derivedDataPath, "Build", "Products");
3784
+ return import_node_path8.default.join(derivedDataPath, "Build", "Products");
3718
3785
  }
3719
3786
  function findXctestrunIn(dir) {
3720
3787
  let entries;
@@ -3724,7 +3791,7 @@ function findXctestrunIn(dir) {
3724
3791
  return null;
3725
3792
  }
3726
3793
  const match = entries.filter((name) => name.endsWith(".xctestrun") && !name.startsWith(PREPARED_XCTESTRUN_PREFIX)).sort()[0];
3727
- return match ? import_node_path7.default.join(dir, match) : null;
3794
+ return match ? import_node_path8.default.join(dir, match) : null;
3728
3795
  }
3729
3796
  function resolveOverrideXctestrun(override) {
3730
3797
  if (!import_node_fs7.default.existsSync(override)) {
@@ -3736,7 +3803,7 @@ function resolveOverrideXctestrun(override) {
3736
3803
  const candidates = [];
3737
3804
  const stat = import_node_fs7.default.statSync(override);
3738
3805
  if (override.endsWith(".app")) {
3739
- candidates.push(import_node_path7.default.dirname(import_node_path7.default.dirname(override)));
3806
+ candidates.push(import_node_path8.default.dirname(import_node_path8.default.dirname(override)));
3740
3807
  } else if (stat.isDirectory()) {
3741
3808
  candidates.push(override, productsDir(override));
3742
3809
  }
@@ -3753,7 +3820,7 @@ function resolveOverrideXctestrun(override) {
3753
3820
  async function resolveWdaTestRun(options = {}) {
3754
3821
  const runner = options.runner ?? execFileXcrunRunner;
3755
3822
  const env = options.env ?? process.env;
3756
- const homeDir = options.homeDir ?? import_node_os2.default.homedir();
3823
+ const homeDir = options.homeDir ?? import_node_os4.default.homedir();
3757
3824
  const log = options.logger ?? ((message) => process.stderr.write(`${message}
3758
3825
  `));
3759
3826
  const override = env.PROWL_WDA_RUNNER;
@@ -3869,8 +3936,8 @@ var defaultWdaTestRunPreparer = async ({ xctestrunPath, port }) => {
3869
3936
  }
3870
3937
  const injected = injectUsePortIntoXctestrun(parsed, port);
3871
3938
  const stem = `${PREPARED_XCTESTRUN_PREFIX}${port}-${process.pid}`;
3872
- const outPath = import_node_path7.default.join(import_node_path7.default.dirname(xctestrunPath), `${stem}.xctestrun`);
3873
- const jsonPath = import_node_path7.default.join(import_node_os2.default.tmpdir(), `${stem}.json`);
3939
+ const outPath = import_node_path8.default.join(import_node_path8.default.dirname(xctestrunPath), `${stem}.xctestrun`);
3940
+ const jsonPath = import_node_path8.default.join(import_node_os4.default.tmpdir(), `${stem}.json`);
3874
3941
  import_node_fs7.default.writeFileSync(jsonPath, JSON.stringify(injected));
3875
3942
  try {
3876
3943
  await runPlutil(["-convert", "xml1", jsonPath, "-o", outPath]);
@@ -3883,7 +3950,7 @@ function cleanupPreparedTestRun(preparedPath) {
3883
3950
  if (!preparedPath) {
3884
3951
  return;
3885
3952
  }
3886
- if (!import_node_path7.default.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
3953
+ if (!import_node_path8.default.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
3887
3954
  return;
3888
3955
  }
3889
3956
  import_node_fs7.default.rmSync(preparedPath, { force: true });
@@ -3908,7 +3975,7 @@ async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
3908
3975
  }
3909
3976
  return app;
3910
3977
  }
3911
- const appPath = import_node_path7.default.resolve(app);
3978
+ const appPath = import_node_path8.default.resolve(app);
3912
3979
  if (!import_node_fs7.default.existsSync(appPath)) {
3913
3980
  throw new Error(`.app bundle not found: ${appPath}`);
3914
3981
  }
@@ -4013,7 +4080,7 @@ async function closeIosSession(session) {
4013
4080
 
4014
4081
  // src/runner/steps.ts
4015
4082
  var import_node_fs9 = __toESM(require("fs"), 1);
4016
- var import_node_path9 = __toESM(require("path"), 1);
4083
+ var import_node_path10 = __toESM(require("path"), 1);
4017
4084
  init_loader();
4018
4085
 
4019
4086
  // src/runner/healing.ts
@@ -4748,7 +4815,7 @@ async function runInlineAssert(driver, policy, assertion) {
4748
4815
  throw new Error("assert step is missing an assertion type");
4749
4816
  }
4750
4817
  function screenshotPath(screenshotsDir, fileName) {
4751
- return import_node_path9.default.join(screenshotsDir, fileName);
4818
+ return import_node_path10.default.join(screenshotsDir, fileName);
4752
4819
  }
4753
4820
  function stepPath(prefix, index) {
4754
4821
  return prefix ? `${prefix}.${index}` : `${index}`;
@@ -4765,7 +4832,7 @@ function validateDownloadFilename(suggestedFilename) {
4765
4832
  const safeFilename = suggestedFilename.trim();
4766
4833
  const allowedFilenamePattern = /^[^<>:"/\\|?*]+$/;
4767
4834
  const hasControlCharacter = Array.from(safeFilename).some((char) => char.charCodeAt(0) < 32);
4768
- if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !== import_node_path9.default.basename(safeFilename) || safeFilename.includes("..") || /[/\\]/.test(safeFilename) || hasControlCharacter || !allowedFilenamePattern.test(safeFilename)) {
4835
+ if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !== import_node_path10.default.basename(safeFilename) || safeFilename.includes("..") || /[/\\]/.test(safeFilename) || hasControlCharacter || !allowedFilenamePattern.test(safeFilename)) {
4769
4836
  throw new Error(`Invalid download filename: "${suggestedFilename}"`);
4770
4837
  }
4771
4838
  return safeFilename;
@@ -4945,7 +5012,7 @@ var STEP_HANDLERS = {
4945
5012
  if (!("setInputFiles" in h.step)) unknownStep();
4946
5013
  const resolvedInput = await h.policy.resolveActionSelector(h.step.setInputFiles.selector);
4947
5014
  const rawFiles = h.step.setInputFiles.files;
4948
- const resolveFile = (f) => import_node_path9.default.isAbsolute(f) ? f : import_node_path9.default.join(h.context.configDir, f);
5015
+ const resolveFile = (f) => import_node_path10.default.isAbsolute(f) ? f : import_node_path10.default.join(h.context.configDir, f);
4949
5016
  const resolvedFiles = Array.isArray(rawFiles) ? rawFiles.map(resolveFile) : resolveFile(rawFiles);
4950
5017
  await h.driver.setInputFiles(resolvedInput.selector, resolvedFiles);
4951
5018
  h.policy.ensureLocationAllowed(h.driver);
@@ -5307,11 +5374,11 @@ var STEP_HANDLERS = {
5307
5374
  if (!responseFile) {
5308
5375
  throw new Error("mock.response must include either body or file");
5309
5376
  }
5310
- const candidateFilePath = import_node_path9.default.isAbsolute(responseFile) ? responseFile : import_node_path9.default.join(h.context.configDir, responseFile);
5311
- const resolvedConfigDir = import_node_path9.default.resolve(h.context.configDir);
5312
- const resolvedFilePath = import_node_path9.default.resolve(candidateFilePath);
5313
- const relativePath = import_node_path9.default.relative(resolvedConfigDir, resolvedFilePath);
5314
- const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${import_node_path9.default.sep}`) && !import_node_path9.default.isAbsolute(relativePath);
5377
+ const candidateFilePath = import_node_path10.default.isAbsolute(responseFile) ? responseFile : import_node_path10.default.join(h.context.configDir, responseFile);
5378
+ const resolvedConfigDir = import_node_path10.default.resolve(h.context.configDir);
5379
+ const resolvedFilePath = import_node_path10.default.resolve(candidateFilePath);
5380
+ const relativePath = import_node_path10.default.relative(resolvedConfigDir, resolvedFilePath);
5381
+ const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${import_node_path10.default.sep}`) && !import_node_path10.default.isAbsolute(relativePath);
5315
5382
  if (!isWithinConfigDir) {
5316
5383
  throw new Error("mock.response.file must resolve within config directory");
5317
5384
  }
@@ -5378,7 +5445,7 @@ var STEP_HANDLERS = {
5378
5445
  capabilities: ["evaluate"],
5379
5446
  run: async (h) => {
5380
5447
  if (!("runScript" in h.step)) unknownStep();
5381
- 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);
5448
+ const filePath = import_node_path10.default.isAbsolute(h.step.runScript.file) ? h.step.runScript.file : import_node_path10.default.join(h.context.configDir, h.step.runScript.file);
5382
5449
  const fileContents = import_node_fs9.default.readFileSync(filePath, "utf-8");
5383
5450
  await h.driver.evaluate(fileContents);
5384
5451
  return {
@@ -5400,11 +5467,11 @@ var STEP_HANDLERS = {
5400
5467
  const name = h.step.assertScreenshot.name;
5401
5468
  const threshold = h.step.assertScreenshot.threshold ?? 0.1;
5402
5469
  const baselineDir = ensureBaselineDir2(h.context.configDir);
5403
- const baselinePath = import_node_path9.default.join(baselineDir, `${name}.png`);
5404
- const currentScreenshotPath = import_node_path9.default.join(h.context.runDir, "screenshots", `${name}-current.png`);
5405
- import_node_fs9.default.mkdirSync(import_node_path9.default.dirname(currentScreenshotPath), { recursive: true });
5470
+ const baselinePath = import_node_path10.default.join(baselineDir, `${name}.png`);
5471
+ const currentScreenshotPath = import_node_path10.default.join(h.context.runDir, "screenshots", `${name}-current.png`);
5472
+ import_node_fs9.default.mkdirSync(import_node_path10.default.dirname(currentScreenshotPath), { recursive: true });
5406
5473
  await h.driver.screenshot({ path: currentScreenshotPath, fullPage: true });
5407
- h.screenshots.push(import_node_path9.default.join("screenshots", `${name}-current.png`));
5474
+ h.screenshots.push(import_node_path10.default.join("screenshots", `${name}-current.png`));
5408
5475
  if (!import_node_fs9.default.existsSync(baselinePath)) {
5409
5476
  import_node_fs9.default.copyFileSync(currentScreenshotPath, baselinePath);
5410
5477
  return {
@@ -5412,7 +5479,7 @@ var STEP_HANDLERS = {
5412
5479
  result: { type: "assertScreenshot", status: "pass", durationMs: Date.now() - h.stepStart, value: "baseline created" }
5413
5480
  };
5414
5481
  }
5415
- const diffPath = import_node_path9.default.join(h.context.runDir, "screenshots", `${name}-diff.png`);
5482
+ const diffPath = import_node_path10.default.join(h.context.runDir, "screenshots", `${name}-diff.png`);
5416
5483
  const comparison = await compareScreenshots2(baselinePath, currentScreenshotPath, diffPath, threshold);
5417
5484
  if (comparison.match) {
5418
5485
  return {
@@ -5425,7 +5492,7 @@ var STEP_HANDLERS = {
5425
5492
  }
5426
5493
  };
5427
5494
  }
5428
- h.screenshots.push(import_node_path9.default.join("screenshots", `${name}-diff.png`));
5495
+ h.screenshots.push(import_node_path10.default.join("screenshots", `${name}-diff.png`));
5429
5496
  throw new Error(
5430
5497
  `Visual regression: ${(comparison.diffPercentage * 100).toFixed(2)}% diff exceeds threshold ${(threshold * 100).toFixed(0)}%`
5431
5498
  );
@@ -5451,7 +5518,7 @@ var STEP_HANDLERS = {
5451
5518
  }
5452
5519
  const fileName = `assertWithAI_step_${h.index + 1}.png`;
5453
5520
  const relative = await h.addScreenshot(fileName);
5454
- const screenshotFullPath = import_node_path9.default.join(h.context.runDir, relative);
5521
+ const screenshotFullPath = import_node_path10.default.join(h.context.runDir, relative);
5455
5522
  const imageBase64 = import_node_fs9.default.readFileSync(screenshotFullPath).toString("base64");
5456
5523
  const assertVision = h.context.assertVision ?? assertWithAiVision;
5457
5524
  const verdict = await assertVision(
@@ -5509,7 +5576,7 @@ var STEP_HANDLERS = {
5509
5576
  `Download filename mismatch: expected "${opts.filename}", got "${suggestedFilename}"`
5510
5577
  );
5511
5578
  }
5512
- const savePath = import_node_path9.default.join(h.context.runDir, suggestedFilename);
5579
+ const savePath = import_node_path10.default.join(h.context.runDir, suggestedFilename);
5513
5580
  await download.saveAs(savePath);
5514
5581
  return {
5515
5582
  kind: "result",
@@ -5538,7 +5605,7 @@ async function executeSteps(context) {
5538
5605
  maxSteps: context.maxSteps,
5539
5606
  selfHealing: context.selfHealing
5540
5607
  });
5541
- const screenshotsDir = import_node_path9.default.join(context.runDir, "screenshots");
5608
+ const screenshotsDir = import_node_path10.default.join(context.runDir, "screenshots");
5542
5609
  import_node_fs9.default.mkdirSync(screenshotsDir, { recursive: true });
5543
5610
  const currentHuntName = context.huntStack?.[context.huntStack.length - 1];
5544
5611
  policy.assertWithinMaxSteps(context.steps.length, currentHuntName);
@@ -5549,7 +5616,7 @@ async function executeSteps(context) {
5549
5616
  const addScreenshot = async (fileName) => {
5550
5617
  const fullPath = screenshotPath(screenshotsDir, fileName);
5551
5618
  await captureScreenshot2(driver, fullPath);
5552
- const relative = import_node_path9.default.join("screenshots", fileName);
5619
+ const relative = import_node_path10.default.join("screenshots", fileName);
5553
5620
  screenshots.push(relative);
5554
5621
  return relative;
5555
5622
  };
@@ -5637,12 +5704,12 @@ async function executeSteps(context) {
5637
5704
  return { results, screenshots, failed: false };
5638
5705
  }
5639
5706
  async function captureFinalScreenshot(page, runDir) {
5640
- const screenshotsDir = import_node_path9.default.join(runDir, "screenshots");
5707
+ const screenshotsDir = import_node_path10.default.join(runDir, "screenshots");
5641
5708
  import_node_fs9.default.mkdirSync(screenshotsDir, { recursive: true });
5642
5709
  const fileName = "final.png";
5643
5710
  const filePath = screenshotPath(screenshotsDir, fileName);
5644
5711
  await captureScreenshot2(page, filePath);
5645
- return import_node_path9.default.join("screenshots", fileName);
5712
+ return import_node_path10.default.join("screenshots", fileName);
5646
5713
  }
5647
5714
 
5648
5715
  // src/runner/assertions.ts
@@ -5764,6 +5831,59 @@ async function evaluateAssertions(options) {
5764
5831
  }
5765
5832
  return results;
5766
5833
  }
5834
+ async function evaluateNativeAssertions(options) {
5835
+ const assertions = mergeAssertions(options.config, options.huntAssertions);
5836
+ const authoredTypes = new Set(
5837
+ (options.huntAssertions ?? []).map((assertion) => Object.keys(assertion)[0])
5838
+ );
5839
+ const results = [];
5840
+ const warnings = [];
5841
+ for (const assertion of assertions) {
5842
+ const type = Object.keys(assertion)[0] ?? "assertion";
5843
+ try {
5844
+ if ("selectorExists" in assertion) {
5845
+ options.assertAllowedSelector?.(assertion.selectorExists);
5846
+ const count = await options.driver.count(assertion.selectorExists);
5847
+ results.push({
5848
+ type: "selectorExists",
5849
+ value: assertion.selectorExists,
5850
+ status: count > 0 ? "pass" : "fail",
5851
+ error: count > 0 ? void 0 : "Selector not found"
5852
+ });
5853
+ continue;
5854
+ }
5855
+ if ("selectorNotExists" in assertion) {
5856
+ options.assertAllowedSelector?.(assertion.selectorNotExists);
5857
+ const count = await options.driver.count(assertion.selectorNotExists);
5858
+ results.push({
5859
+ type: "selectorNotExists",
5860
+ value: assertion.selectorNotExists,
5861
+ status: count === 0 ? "pass" : "fail",
5862
+ error: count === 0 ? void 0 : "Selector exists"
5863
+ });
5864
+ continue;
5865
+ }
5866
+ const rawValue = assertion[type];
5867
+ results.push({
5868
+ type,
5869
+ value: rawValue,
5870
+ status: "skipped",
5871
+ error: `skipped (web-only): not supported on the ${options.targetLabel} target`
5872
+ });
5873
+ if (authoredTypes.has(type)) {
5874
+ warnings.push(`${type} is web-only; skipped on ${options.targetLabel} target`);
5875
+ }
5876
+ } catch (error) {
5877
+ const detail = error instanceof Error ? error.message : "no error details";
5878
+ results.push({
5879
+ type,
5880
+ status: "fail",
5881
+ error: `Native assertion "${type}" failed: ${detail}`
5882
+ });
5883
+ }
5884
+ }
5885
+ return { results, warnings };
5886
+ }
5767
5887
 
5768
5888
  // src/runner/tracing.ts
5769
5889
  var DEFAULT_TRACE_HEADER = "traceparent";
@@ -5803,17 +5923,17 @@ function captureTraceCorrelation(response, headerName, sink, redactionValues = [
5803
5923
 
5804
5924
  // src/reporter/result.ts
5805
5925
  var import_node_fs10 = __toESM(require("fs"), 1);
5806
- var import_node_path10 = __toESM(require("path"), 1);
5926
+ var import_node_path11 = __toESM(require("path"), 1);
5807
5927
  function writeResult(runDir, result) {
5808
5928
  const fileName = "result.json";
5809
- const fullPath = import_node_path10.default.join(runDir, fileName);
5929
+ const fullPath = import_node_path11.default.join(runDir, fileName);
5810
5930
  import_node_fs10.default.writeFileSync(fullPath, JSON.stringify(result, null, 2));
5811
5931
  return fileName;
5812
5932
  }
5813
5933
 
5814
5934
  // src/reporter/summary.ts
5815
5935
  var import_node_fs11 = __toESM(require("fs"), 1);
5816
- var import_node_path11 = __toESM(require("path"), 1);
5936
+ var import_node_path12 = __toESM(require("path"), 1);
5817
5937
  function escapeMd(text) {
5818
5938
  return text.replace(/([|`*_{}[\]()#+\-!\\])/g, "\\$1");
5819
5939
  }
@@ -5891,7 +6011,7 @@ function writeSummary(runDir, result) {
5891
6011
  }
5892
6012
  }
5893
6013
  const fileName = "summary.md";
5894
- const fullPath = import_node_path11.default.join(runDir, fileName);
6014
+ const fullPath = import_node_path12.default.join(runDir, fileName);
5895
6015
  import_node_fs11.default.writeFileSync(fullPath, `${lines.join("\n")}
5896
6016
  `);
5897
6017
  return fileName;
@@ -5899,20 +6019,21 @@ function writeSummary(runDir, result) {
5899
6019
 
5900
6020
  // src/reporter/junit.ts
5901
6021
  var import_node_fs12 = __toESM(require("fs"), 1);
5902
- var import_node_path12 = __toESM(require("path"), 1);
6022
+ var import_node_path13 = __toESM(require("path"), 1);
5903
6023
  function escapeXml(text) {
5904
6024
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5905
6025
  }
5906
6026
  function writeJunit(runDir, result) {
5907
6027
  const totalTests = result.steps.length + result.assertions.length;
5908
6028
  const failures = result.steps.filter((s) => s.status === "fail").length + result.assertions.filter((a) => a.status === "fail").length;
6029
+ const skipped = result.assertions.filter((a) => a.status === "skipped").length;
5909
6030
  const timeSeconds = (result.durationMs / 1e3).toFixed(3);
5910
6031
  const huntName = escapeXml(result.hunt);
5911
6032
  const lines = [];
5912
6033
  lines.push('<?xml version="1.0" encoding="UTF-8"?>');
5913
6034
  lines.push("<testsuites>");
5914
6035
  lines.push(
5915
- ` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
6036
+ ` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" skipped="${skipped}" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
5916
6037
  );
5917
6038
  for (let i = 0; i < result.steps.length; i++) {
5918
6039
  const step = result.steps[i];
@@ -5936,6 +6057,11 @@ function writeJunit(runDir, result) {
5936
6057
  lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
5937
6058
  lines.push(` <failure message="${escapedFailureText}" type="assertion">${escapedFailureText}</failure>`);
5938
6059
  lines.push(" </testcase>");
6060
+ } else if (assertion.status === "skipped") {
6061
+ const skipText = escapeXml(assertion.error ?? "skipped");
6062
+ lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
6063
+ lines.push(` <skipped message="${skipText}"/>`);
6064
+ lines.push(" </testcase>");
5939
6065
  } else {
5940
6066
  lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0"/>`);
5941
6067
  }
@@ -5943,7 +6069,7 @@ function writeJunit(runDir, result) {
5943
6069
  lines.push(" </testsuite>");
5944
6070
  lines.push("</testsuites>");
5945
6071
  const fileName = "junit.xml";
5946
- const fullPath = import_node_path12.default.join(runDir, fileName);
6072
+ const fullPath = import_node_path13.default.join(runDir, fileName);
5947
6073
  import_node_fs12.default.writeFileSync(fullPath, `${lines.join("\n")}
5948
6074
  `);
5949
6075
  return fileName;
@@ -5979,14 +6105,14 @@ function timestamp(prefix) {
5979
6105
 
5980
6106
  // src/runner/history.ts
5981
6107
  var import_node_fs13 = __toESM(require("fs"), 1);
5982
- var import_node_path13 = __toESM(require("path"), 1);
6108
+ var import_node_path14 = __toESM(require("path"), 1);
5983
6109
  var HISTORY_FILE = "history.json";
5984
6110
  var LOCK_FILE_SUFFIX = ".lock";
5985
6111
  var LOCK_RETRY_MS = 10;
5986
6112
  var LOCK_TIMEOUT_MS = 5e3;
5987
6113
  var SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
5988
6114
  function historyPath(configDir) {
5989
- return import_node_path13.default.join(configDir, HISTORY_FILE);
6115
+ return import_node_path14.default.join(configDir, HISTORY_FILE);
5990
6116
  }
5991
6117
  function isHistoryEntry(value) {
5992
6118
  if (!value || typeof value !== "object") {
@@ -6040,7 +6166,7 @@ function sleepSync(ms) {
6040
6166
  function withHistoryLock(configDir, fn) {
6041
6167
  const filePath = historyPath(configDir);
6042
6168
  const lockPath = `${filePath}${LOCK_FILE_SUFFIX}`;
6043
- import_node_fs13.default.mkdirSync(import_node_path13.default.dirname(filePath), { recursive: true });
6169
+ import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(filePath), { recursive: true });
6044
6170
  const startedAt = Date.now();
6045
6171
  while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
6046
6172
  let fd;
@@ -6088,11 +6214,11 @@ function parseViewportFlag(value) {
6088
6214
  return value;
6089
6215
  }
6090
6216
  function resolvePath(configDir, inputPath) {
6091
- if (import_node_path14.default.isAbsolute(inputPath)) {
6217
+ if (import_node_path15.default.isAbsolute(inputPath)) {
6092
6218
  return inputPath;
6093
6219
  }
6094
- const projectRoot = import_node_path14.default.dirname(configDir);
6095
- return import_node_path14.default.join(projectRoot, inputPath);
6220
+ const projectRoot = import_node_path15.default.dirname(configDir);
6221
+ return import_node_path15.default.join(projectRoot, inputPath);
6096
6222
  }
6097
6223
  function buildRunResult(options) {
6098
6224
  return {
@@ -6111,7 +6237,7 @@ function buildRunResult(options) {
6111
6237
  }
6112
6238
  function writeConsoleLog(runDir, entries) {
6113
6239
  const fileName = "console.log";
6114
- const filePath = import_node_path14.default.join(runDir, fileName);
6240
+ const filePath = import_node_path15.default.join(runDir, fileName);
6115
6241
  const lines = entries.map((entry) => {
6116
6242
  const location = entry.location ? ` (${entry.location})` : "";
6117
6243
  return `[${entry.type}] ${entry.text}${location}`;
@@ -6124,7 +6250,7 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
6124
6250
  const headless = options.headed ? false : config.browser.headless;
6125
6251
  const slowMo = options.slowMo ?? config.browser.slowMo;
6126
6252
  const maxSteps = config.guardrails.maxSteps;
6127
- const runDir = import_node_path14.default.join(configDir, "runs", timestamp());
6253
+ const runDir = import_node_path15.default.join(configDir, "runs", timestamp());
6128
6254
  import_node_fs14.default.mkdirSync(runDir, { recursive: true });
6129
6255
  const storageStatePath = config.auth.storageStatePath ? resolvePath(configDir, config.auth.storageStatePath) : void 0;
6130
6256
  const engine = options.browser ?? config.browser.engine;
@@ -6308,7 +6434,7 @@ async function runHunt(options) {
6308
6434
  }
6309
6435
  async function executeNativeHuntAttempt(options, config, configDir, interpolatedHunt, redactedFillSteps, randomVars, allowedApps, native) {
6310
6436
  const maxSteps = config.guardrails.maxSteps;
6311
- const runDir = import_node_path14.default.join(configDir, "runs", timestamp());
6437
+ const runDir = import_node_path15.default.join(configDir, "runs", timestamp());
6312
6438
  import_node_fs14.default.mkdirSync(runDir, { recursive: true });
6313
6439
  const session = await native.launchSession();
6314
6440
  let result;
@@ -6317,6 +6443,13 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
6317
6443
  const appIdentity = native.sessionAppIdentity(session);
6318
6444
  const targetLabel = `${native.targetType}:${appIdentity}`;
6319
6445
  const effectiveAllowedApps = [.../* @__PURE__ */ new Set([...allowedApps, native.targetApp, appIdentity])];
6446
+ const assertionPolicy = createRunPolicy(driver, {
6447
+ forbiddenSelectors: config.guardrails.forbiddenSelectors,
6448
+ allowedDomains: [],
6449
+ allowedApps: effectiveAllowedApps,
6450
+ maxSteps,
6451
+ selfHealing: config.guardrails.selfHealing
6452
+ });
6320
6453
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6321
6454
  const startTime = Date.now();
6322
6455
  let stepResults = [];
@@ -6356,8 +6489,19 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
6356
6489
  } catch {
6357
6490
  finalScreenshot = void 0;
6358
6491
  }
6492
+ const { results: assertionResults, warnings: assertionWarnings } = await evaluateNativeAssertions({
6493
+ driver,
6494
+ config,
6495
+ huntAssertions: interpolatedHunt.assertions,
6496
+ assertAllowedSelector: assertionPolicy.assertAllowedSelector,
6497
+ targetLabel: nativeTargetLabel(native.targetType)
6498
+ });
6499
+ for (const warning of assertionWarnings) {
6500
+ console.warn(warning);
6501
+ }
6359
6502
  const durationMs = Date.now() - startTime;
6360
- const status = stepFailed ? "fail" : "pass";
6503
+ const assertionsFailed = assertionResults.some((assertion) => assertion.status === "fail");
6504
+ const status = stepFailed || assertionsFailed ? "fail" : "pass";
6361
6505
  const artifacts = {
6362
6506
  screenshots: finalScreenshot ? [...stepScreenshots, finalScreenshot] : stepScreenshots
6363
6507
  };
@@ -6368,7 +6512,7 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
6368
6512
  hunt: options.huntName,
6369
6513
  targetUrl: targetLabel,
6370
6514
  steps: stepResults,
6371
- assertions: [],
6515
+ assertions: assertionResults,
6372
6516
  artifacts
6373
6517
  });
6374
6518
  result = writeReports(runDir, runResult, { junit: options.junit ?? config.artifacts.junit });
@@ -6481,7 +6625,6 @@ async function runNativeHunt(options, config, configDir, target, native) {
6481
6625
  const hunt = loadHunt(options.huntName, configDir);
6482
6626
  const { hunt: interpolatedHunt, redactedFillSteps, randomVars } = interpolateHunt(hunt, process.env);
6483
6627
  assertStepsSupportedByTarget(interpolatedHunt.steps, native.targetType);
6484
- assertHuntAssertionsSupportedByTarget(interpolatedHunt.assertions, native.targetType);
6485
6628
  native.assertAppAllowed(config.guardrails.allowedApps, target);
6486
6629
  const maxSteps = config.guardrails.maxSteps;
6487
6630
  if (interpolatedHunt.steps.length > maxSteps) {
@@ -6522,7 +6665,7 @@ async function runNativeHunt(options, config, configDir, target, native) {
6522
6665
  }
6523
6666
  function recordHistory(configDir, outcome, maxRuns) {
6524
6667
  try {
6525
- const relativeRunDir = import_node_path14.default.relative(configDir, outcome.runDir);
6668
+ const relativeRunDir = import_node_path15.default.relative(configDir, outcome.runDir);
6526
6669
  appendEntry(
6527
6670
  configDir,
6528
6671
  {
@@ -6540,6 +6683,7 @@ function recordHistory(configDir, outcome, maxRuns) {
6540
6683
 
6541
6684
  // src/cli/commands/run.ts
6542
6685
  init_loader();
6686
+ init_hunt_name();
6543
6687
 
6544
6688
  // src/cli/output.ts
6545
6689
  var import_chalk = __toESM(require("chalk"), 1);
@@ -6685,7 +6829,11 @@ ${bot}`);
6685
6829
 
6686
6830
  // src/cli/commands/run.ts
6687
6831
  function buildRunCommand() {
6688
- const command = new import_commander.Command("run").argument("<hunt-name>", "Hunt name or path (e.g. homepage or admin/users-crud)").option("--url <target>", "Override target URL").option("--headed", "Show browser window").option("--slow-mo <ms>", "Slow down Playwright actions", (value) => Number(value)).option("--trace", "Capture Playwright trace").option("--browser <engine>", "Browser engine: chromium, firefox, or webkit").option("--channel <name>", "Browser channel: chrome, msedge, chrome-beta, etc.").option("--viewport <size>", "Viewport size: WxH (e.g. 1920x1080) or preset (mobile, tablet, desktop)").option("--include-tags <tags>", "Only run hunts matching these tags (comma-separated)").option("--exclude-tags <tags>", "Skip hunts matching these tags (comma-separated)").option("--junit", "Generate JUnit XML report").option("--config <path>", "Custom config path").option("--json", "Output results as JSON").action(async (huntName, options) => {
6832
+ const command = new import_commander.Command("run").argument(
6833
+ "<hunt-name>",
6834
+ "Hunt name or path (e.g. homepage, admin/users-crud, or .prowl/hunts/homepage.yml)"
6835
+ ).option("--url <target>", "Override target URL").option("--headed", "Show browser window").option("--slow-mo <ms>", "Slow down Playwright actions", (value) => Number(value)).option("--trace", "Capture Playwright trace").option("--browser <engine>", "Browser engine: chromium, firefox, or webkit").option("--channel <name>", "Browser channel: chrome, msedge, chrome-beta, etc.").option("--viewport <size>", "Viewport size: WxH (e.g. 1920x1080) or preset (mobile, tablet, desktop)").option("--include-tags <tags>", "Only run hunts matching these tags (comma-separated)").option("--exclude-tags <tags>", "Skip hunts matching these tags (comma-separated)").option("--junit", "Generate JUnit XML report").option("--config <path>", "Custom config path").option("--json", "Output results as JSON").action(async (huntArg, options) => {
6836
+ const huntName = normalizeHuntName(huntArg);
6689
6837
  try {
6690
6838
  if (options.includeTags || options.excludeTags) {
6691
6839
  const { configDir } = loadConfig(options.config);
@@ -6752,7 +6900,7 @@ function buildRunCommand() {
6752
6900
 
6753
6901
  // src/cli/commands/init.ts
6754
6902
  var import_node_fs15 = __toESM(require("fs"), 1);
6755
- var import_node_path15 = __toESM(require("path"), 1);
6903
+ var import_node_path16 = __toESM(require("path"), 1);
6756
6904
  var import_node_url2 = require("url");
6757
6905
  var import_commander2 = require("commander");
6758
6906
  var import_chalk4 = __toESM(require("chalk"), 1);
@@ -6760,27 +6908,27 @@ init_loader();
6760
6908
  var import_meta4 = {};
6761
6909
  function getPackageRoot2() {
6762
6910
  const currentFile = (0, import_node_url2.fileURLToPath)(import_meta4.url);
6763
- let dir = import_node_path15.default.dirname(currentFile);
6764
- const root = import_node_path15.default.parse(dir).root;
6911
+ let dir = import_node_path16.default.dirname(currentFile);
6912
+ const root = import_node_path16.default.parse(dir).root;
6765
6913
  while (dir !== root) {
6766
- if (import_node_fs15.default.existsSync(import_node_path15.default.join(dir, "package.json"))) {
6914
+ if (import_node_fs15.default.existsSync(import_node_path16.default.join(dir, "package.json"))) {
6767
6915
  return dir;
6768
6916
  }
6769
- dir = import_node_path15.default.dirname(dir);
6917
+ dir = import_node_path16.default.dirname(dir);
6770
6918
  }
6771
- if (import_node_fs15.default.existsSync(import_node_path15.default.join(root, "package.json"))) {
6919
+ if (import_node_fs15.default.existsSync(import_node_path16.default.join(root, "package.json"))) {
6772
6920
  return root;
6773
6921
  }
6774
6922
  throw new Error("Cannot find package root. Reinstall prowl-tools.");
6775
6923
  }
6776
6924
  function copyFile(source, destination) {
6777
- import_node_fs15.default.mkdirSync(import_node_path15.default.dirname(destination), { recursive: true });
6925
+ import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(destination), { recursive: true });
6778
6926
  import_node_fs15.default.copyFileSync(source, destination);
6779
6927
  }
6780
6928
  function buildInitCommand() {
6781
6929
  const command = new import_commander2.Command("init").option("--force", `Overwrite existing ${CONFIG_DIR} directory`).action((options) => {
6782
6930
  const root = process.cwd();
6783
- const prowlDir = import_node_path15.default.join(root, CONFIG_DIR);
6931
+ const prowlDir = import_node_path16.default.join(root, CONFIG_DIR);
6784
6932
  if (import_node_fs15.default.existsSync(prowlDir) && !options.force) {
6785
6933
  console.error(
6786
6934
  import_chalk4.default.red(
@@ -6791,20 +6939,20 @@ function buildInitCommand() {
6791
6939
  return;
6792
6940
  }
6793
6941
  const packageRoot = getPackageRoot2();
6794
- const examplesDir = import_node_path15.default.join(packageRoot, "examples");
6795
- const exampleConfig = import_node_path15.default.join(examplesDir, "config.yml");
6796
- const exampleHuntsDir = import_node_path15.default.join(examplesDir, "hunts");
6942
+ const examplesDir = import_node_path16.default.join(packageRoot, "examples");
6943
+ const exampleConfig = import_node_path16.default.join(examplesDir, "config.yml");
6944
+ const exampleHuntsDir = import_node_path16.default.join(examplesDir, "hunts");
6797
6945
  if (!import_node_fs15.default.existsSync(exampleConfig) || !import_node_fs15.default.existsSync(exampleHuntsDir)) {
6798
6946
  console.error(import_chalk4.default.red("Examples not found in package. Reinstall prowl-tools."));
6799
6947
  process.exitCode = 1;
6800
6948
  return;
6801
6949
  }
6802
- copyFile(exampleConfig, import_node_path15.default.join(prowlDir, "config.yml"));
6950
+ copyFile(exampleConfig, import_node_path16.default.join(prowlDir, "config.yml"));
6803
6951
  const huntFiles = import_node_fs15.default.readdirSync(exampleHuntsDir).filter((f) => f.endsWith(".yml"));
6804
6952
  for (const huntFile of huntFiles) {
6805
6953
  copyFile(
6806
- import_node_path15.default.join(exampleHuntsDir, huntFile),
6807
- import_node_path15.default.join(prowlDir, "hunts", huntFile)
6954
+ import_node_path16.default.join(exampleHuntsDir, huntFile),
6955
+ import_node_path16.default.join(prowlDir, "hunts", huntFile)
6808
6956
  );
6809
6957
  }
6810
6958
  const gitignore = [
@@ -6818,27 +6966,27 @@ function buildInitCommand() {
6818
6966
  ".env",
6819
6967
  ""
6820
6968
  ].join("\n");
6821
- import_node_fs15.default.writeFileSync(import_node_path15.default.join(prowlDir, ".gitignore"), gitignore);
6969
+ import_node_fs15.default.writeFileSync(import_node_path16.default.join(prowlDir, ".gitignore"), gitignore);
6822
6970
  console.log(welcomeBanner());
6823
6971
  console.log(import_chalk4.default.green(` Initialized ${CONFIG_DIR} directory.`));
6824
6972
  console.log(import_chalk4.default.gray(" Run ") + import_chalk4.default.bold("prowl run hello") + import_chalk4.default.gray(" to get started."));
6825
- console.log(import_chalk4.default.gray(" Browse hunt templates at ") + import_chalk4.default.cyan("https://hub.prowl.tools") + "\n");
6973
+ console.log(import_chalk4.default.gray(" See ") + import_chalk4.default.cyan(`${CONFIG_DIR}/hunts/login-flow.yml`) + import_chalk4.default.gray(" for a fuller example.") + "\n");
6826
6974
  });
6827
6975
  return command;
6828
6976
  }
6829
6977
 
6830
6978
  // src/cli/commands/login.ts
6831
- var import_node_path16 = __toESM(require("path"), 1);
6979
+ var import_node_path17 = __toESM(require("path"), 1);
6832
6980
  var import_node_readline = __toESM(require("readline"), 1);
6833
6981
  var import_chalk5 = __toESM(require("chalk"), 1);
6834
6982
  var import_commander3 = require("commander");
6835
6983
  init_loader();
6836
6984
  function resolvePath2(configDir, inputPath) {
6837
- if (import_node_path16.default.isAbsolute(inputPath)) {
6985
+ if (import_node_path17.default.isAbsolute(inputPath)) {
6838
6986
  return inputPath;
6839
6987
  }
6840
- const projectRoot = import_node_path16.default.dirname(configDir);
6841
- return import_node_path16.default.join(projectRoot, inputPath);
6988
+ const projectRoot = import_node_path17.default.dirname(configDir);
6989
+ return import_node_path17.default.join(projectRoot, inputPath);
6842
6990
  }
6843
6991
  function waitForEnter(prompt) {
6844
6992
  return new Promise((resolve) => {
@@ -6929,14 +7077,15 @@ var import_node_fs16 = __toESM(require("fs"), 1);
6929
7077
  var import_chalk7 = __toESM(require("chalk"), 1);
6930
7078
  var import_commander5 = require("commander");
6931
7079
  init_loader();
7080
+ init_hunt_name();
6932
7081
 
6933
7082
  // src/cli/watch-utils.ts
6934
- var import_node_path17 = __toESM(require("path"), 1);
7083
+ var import_node_path18 = __toESM(require("path"), 1);
6935
7084
  function getWatchTargets(configDir, huntName) {
6936
7085
  return [
6937
- import_node_path17.default.join(configDir, "hunts", `${huntName}.yml`),
6938
- import_node_path17.default.join(configDir, "config.yml"),
6939
- import_node_path17.default.join(configDir, ".env")
7086
+ import_node_path18.default.join(configDir, "hunts", `${huntName}.yml`),
7087
+ import_node_path18.default.join(configDir, "config.yml"),
7088
+ import_node_path18.default.join(configDir, ".env")
6940
7089
  ];
6941
7090
  }
6942
7091
  function createDebouncer(delayMs, fn) {
@@ -6962,7 +7111,11 @@ function createDebouncer(delayMs, fn) {
6962
7111
 
6963
7112
  // src/cli/commands/watch.ts
6964
7113
  function buildWatchCommand() {
6965
- const command = new import_commander5.Command("watch").argument("<hunt-name>", "Hunt name or path (e.g. homepage or admin/users-crud)").option("--url <target>", "Override target URL").option("--headed", "Show browser window").option("--slow-mo <ms>", "Slow down Playwright actions", (value) => Number(value)).option("--trace", "Capture Playwright trace").option("--config <path>", "Custom config path").action(async (huntName, options) => {
7114
+ const command = new import_commander5.Command("watch").argument(
7115
+ "<hunt-name>",
7116
+ "Hunt name or path (e.g. homepage, admin/users-crud, or .prowl/hunts/homepage.yml)"
7117
+ ).option("--url <target>", "Override target URL").option("--headed", "Show browser window").option("--slow-mo <ms>", "Slow down Playwright actions", (value) => Number(value)).option("--trace", "Capture Playwright trace").option("--config <path>", "Custom config path").action(async (huntArg, options) => {
7118
+ const huntName = normalizeHuntName(huntArg);
6966
7119
  const { configDir } = loadConfig(options.config);
6967
7120
  const watchTargets = getWatchTargets(configDir, huntName);
6968
7121
  let running = false;
@@ -7038,12 +7191,12 @@ var import_commander6 = require("commander");
7038
7191
  var import_chalk9 = __toESM(require("chalk"), 1);
7039
7192
 
7040
7193
  // src/runner/suite.ts
7041
- var import_node_path20 = __toESM(require("path"), 1);
7194
+ var import_node_path21 = __toESM(require("path"), 1);
7042
7195
  init_loader();
7043
7196
 
7044
7197
  // src/reporter/ci-summary.ts
7045
7198
  var import_node_fs17 = __toESM(require("fs"), 1);
7046
- var import_node_path18 = __toESM(require("path"), 1);
7199
+ var import_node_path19 = __toESM(require("path"), 1);
7047
7200
  var import_chalk8 = __toESM(require("chalk"), 1);
7048
7201
  function countCiResults(results) {
7049
7202
  return {
@@ -7108,7 +7261,7 @@ function writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky = []
7108
7261
  ...clusters.length > 0 ? { clusters } : {}
7109
7262
  };
7110
7263
  import_node_fs17.default.mkdirSync(ciRunDir, { recursive: true });
7111
- const filePath = import_node_path18.default.join(ciRunDir, "ci-result.json");
7264
+ const filePath = import_node_path19.default.join(ciRunDir, "ci-result.json");
7112
7265
  import_node_fs17.default.writeFileSync(filePath, JSON.stringify(ciResult, null, 2) + "\n");
7113
7266
  return filePath;
7114
7267
  }
@@ -7244,7 +7397,7 @@ function clusterFailures(failures) {
7244
7397
 
7245
7398
  // src/backlog/index.ts
7246
7399
  var import_node_fs18 = __toESM(require("fs"), 1);
7247
- var import_node_path19 = __toESM(require("path"), 1);
7400
+ var import_node_path20 = __toESM(require("path"), 1);
7248
7401
 
7249
7402
  // src/backlog/parse.ts
7250
7403
  var MARKER_FP = /<!--\s*prowl:fp=([0-9a-f]+)/;
@@ -7350,7 +7503,7 @@ function buildFailure(hunt) {
7350
7503
  if (!hunt.runDir) return failure;
7351
7504
  let run;
7352
7505
  try {
7353
- const resultJson = readFileOrEmpty(import_node_path19.default.join(hunt.runDir, "result.json"));
7506
+ const resultJson = readFileOrEmpty(import_node_path20.default.join(hunt.runDir, "result.json"));
7354
7507
  if (!resultJson) return failure;
7355
7508
  run = JSON.parse(resultJson);
7356
7509
  } catch (error) {
@@ -7379,8 +7532,8 @@ function extractFailures(suiteResult) {
7379
7532
  }
7380
7533
  function updateBacklogFromSuite(suiteResult, options = {}) {
7381
7534
  const projectRoot = options.projectRoot ?? process.cwd();
7382
- const backlogPath = options.backlogPath ?? import_node_path19.default.join(projectRoot, "docs", "backlog.md");
7383
- const resolvedPath = options.resolvedPath ?? import_node_path19.default.join(projectRoot, "docs", "resolved.md");
7535
+ const backlogPath = options.backlogPath ?? import_node_path20.default.join(projectRoot, "docs", "backlog.md");
7536
+ const resolvedPath = options.resolvedPath ?? import_node_path20.default.join(projectRoot, "docs", "resolved.md");
7384
7537
  const date = options.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7385
7538
  const summary = { created: [], regressions: [], skipped: [], backlogPath };
7386
7539
  const failures = extractFailures(suiteResult);
@@ -7412,7 +7565,7 @@ function updateBacklogFromSuite(suiteResult, options = {}) {
7412
7565
  }
7413
7566
  }
7414
7567
  if (ticketsToAdd.length > 0) {
7415
- import_node_fs18.default.mkdirSync(import_node_path19.default.dirname(backlogPath), { recursive: true });
7568
+ import_node_fs18.default.mkdirSync(import_node_path20.default.dirname(backlogPath), { recursive: true });
7416
7569
  import_node_fs18.default.writeFileSync(backlogPath, insertTickets(backlogContent, ticketsToAdd));
7417
7570
  }
7418
7571
  return summary;
@@ -7574,7 +7727,7 @@ async function runSuite(options = {}) {
7574
7727
  const clusters = clusterFailures(
7575
7728
  extractFailures({ result: { hunts: results }, resultPath: null })
7576
7729
  ).filter((cluster) => cluster.count > 1);
7577
- const ciRunDir = import_node_path20.default.join(configDir, "runs", timestamp("ci"));
7730
+ const ciRunDir = import_node_path21.default.join(configDir, "runs", timestamp("ci"));
7578
7731
  const resultPath = writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky, clusters);
7579
7732
  const { passed, failed, skipped } = countCiResults(results);
7580
7733
  return {
@@ -7693,7 +7846,7 @@ function buildCiCommand() {
7693
7846
 
7694
7847
  // src/cli/commands/update-baselines.ts
7695
7848
  var import_node_fs19 = __toESM(require("fs"), 1);
7696
- var import_node_path21 = __toESM(require("path"), 1);
7849
+ var import_node_path22 = __toESM(require("path"), 1);
7697
7850
  var import_commander7 = require("commander");
7698
7851
  var import_chalk10 = __toESM(require("chalk"), 1);
7699
7852
  init_loader();
@@ -7701,27 +7854,27 @@ function buildUpdateBaselinesCommand() {
7701
7854
  const command = new import_commander7.Command("update-baselines").description("Accept current screenshots as new visual regression baselines").option("--run <dir>", "Specific run directory to use").option("--name <name>", "Update only a specific baseline by name").option("--config <path>", "Custom config path").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
7702
7855
  try {
7703
7856
  const { configDir } = loadConfig(options.config);
7704
- const baselinesDir = import_node_path21.default.join(configDir, "baselines");
7857
+ const baselinesDir = import_node_path22.default.join(configDir, "baselines");
7705
7858
  import_node_fs19.default.mkdirSync(baselinesDir, { recursive: true });
7706
7859
  let runDir;
7707
7860
  if (options.run) {
7708
- runDir = import_node_path21.default.isAbsolute(options.run) ? options.run : import_node_path21.default.resolve(options.run);
7861
+ runDir = import_node_path22.default.isAbsolute(options.run) ? options.run : import_node_path22.default.resolve(options.run);
7709
7862
  } else {
7710
- const runsDir = import_node_path21.default.join(configDir, "runs");
7863
+ const runsDir = import_node_path22.default.join(configDir, "runs");
7711
7864
  if (!import_node_fs19.default.existsSync(runsDir)) {
7712
7865
  console.error(import_chalk10.default.red(" No runs directory found. Run a hunt first."));
7713
7866
  process.exitCode = 1;
7714
7867
  return;
7715
7868
  }
7716
- const entries = import_node_fs19.default.readdirSync(runsDir).filter((e) => import_node_fs19.default.statSync(import_node_path21.default.join(runsDir, e)).isDirectory()).sort().reverse();
7869
+ const entries = import_node_fs19.default.readdirSync(runsDir).filter((e) => import_node_fs19.default.statSync(import_node_path22.default.join(runsDir, e)).isDirectory()).sort().reverse();
7717
7870
  if (entries.length === 0) {
7718
7871
  console.error(import_chalk10.default.red(" No run directories found. Run a hunt first."));
7719
7872
  process.exitCode = 1;
7720
7873
  return;
7721
7874
  }
7722
- runDir = import_node_path21.default.join(runsDir, entries[0]);
7875
+ runDir = import_node_path22.default.join(runsDir, entries[0]);
7723
7876
  }
7724
- const screenshotsDir = import_node_path21.default.join(runDir, "screenshots");
7877
+ const screenshotsDir = import_node_path22.default.join(runDir, "screenshots");
7725
7878
  if (!import_node_fs19.default.existsSync(screenshotsDir)) {
7726
7879
  console.error(import_chalk10.default.red(` No screenshots found in ${runDir}`));
7727
7880
  process.exitCode = 1;
@@ -7741,8 +7894,8 @@ function buildUpdateBaselinesCommand() {
7741
7894
  let updated = 0;
7742
7895
  for (const file of filtered) {
7743
7896
  const baselineName = file.replace("-current.png", ".png");
7744
- const sourcePath = import_node_path21.default.join(screenshotsDir, file);
7745
- const destPath = import_node_path21.default.join(baselinesDir, baselineName);
7897
+ const sourcePath = import_node_path22.default.join(screenshotsDir, file);
7898
+ const destPath = import_node_path22.default.join(baselinesDir, baselineName);
7746
7899
  const exists = import_node_fs19.default.existsSync(destPath);
7747
7900
  import_node_fs19.default.copyFileSync(sourcePath, destPath);
7748
7901
  updated++;
@@ -8572,7 +8725,7 @@ function buildAnalyzeCommand() {
8572
8725
 
8573
8726
  // src/cli/commands/generate.ts
8574
8727
  var import_node_fs20 = __toESM(require("fs"), 1);
8575
- var import_node_path22 = __toESM(require("path"), 1);
8728
+ var import_node_path23 = __toESM(require("path"), 1);
8576
8729
  var import_commander9 = require("commander");
8577
8730
  var import_chalk12 = __toESM(require("chalk"), 1);
8578
8731
  var import_ora = __toESM(require("ora"), 1);
@@ -8767,12 +8920,12 @@ function buildGenerateCommand() {
8767
8920
  const result = loadConfig2(options.config);
8768
8921
  configDir = result.configDir;
8769
8922
  } catch {
8770
- configDir = import_node_path22.default.join(process.cwd(), ".prowl");
8923
+ configDir = import_node_path23.default.join(process.cwd(), ".prowl");
8771
8924
  }
8772
- const huntsDir = import_node_path22.default.join(configDir, "hunts");
8925
+ const huntsDir = import_node_path23.default.join(configDir, "hunts");
8773
8926
  import_node_fs20.default.mkdirSync(huntsDir, { recursive: true });
8774
8927
  const fileName = options.output.endsWith(".yml") ? options.output : `${options.output}.yml`;
8775
- const filePath = import_node_path22.default.join(huntsDir, fileName);
8928
+ const filePath = import_node_path23.default.join(huntsDir, fileName);
8776
8929
  import_node_fs20.default.writeFileSync(filePath, yamlStr + "\n", "utf-8");
8777
8930
  console.log(import_chalk12.default.green(` Saved to ${filePath}`));
8778
8931
  } else {
@@ -8797,14 +8950,19 @@ function buildGenerateCommand() {
8797
8950
  var import_commander10 = require("commander");
8798
8951
  var import_chalk13 = __toESM(require("chalk"), 1);
8799
8952
  init_loader();
8953
+ init_hunt_name();
8800
8954
  function buildHistoryCommand() {
8801
- const command = new import_commander10.Command("history").argument("<hunt-name>", "Hunt name or path (e.g. homepage or admin/users-crud)").description("Show run history for a hunt").option("--config <path>", "Custom config path").option("--limit <n>", "Show the last N runs (default: 20)", (value) => {
8955
+ const command = new import_commander10.Command("history").argument(
8956
+ "<hunt-name>",
8957
+ "Hunt name or path (e.g. homepage, admin/users-crud, or .prowl/hunts/homepage.yml)"
8958
+ ).description("Show run history for a hunt").option("--config <path>", "Custom config path").option("--limit <n>", "Show the last N runs (default: 20)", (value) => {
8802
8959
  const n = Number(value);
8803
8960
  if (!Number.isInteger(n) || n < 1) {
8804
8961
  throw new Error("--limit must be a positive integer");
8805
8962
  }
8806
8963
  return n;
8807
- }).option("--json", "Output as JSON").action((huntName, options) => {
8964
+ }).option("--json", "Output as JSON").action((huntArg, options) => {
8965
+ const huntName = normalizeHuntName(huntArg);
8808
8966
  try {
8809
8967
  const { configDir } = loadConfig(options.config);
8810
8968
  const entries = readHuntHistory(configDir, huntName);
@@ -8970,7 +9128,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
8970
9128
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
8971
9129
 
8972
9130
  // src/mcp/tools.ts
8973
- var import_node_path23 = __toESM(require("path"), 1);
9131
+ var import_node_path24 = __toESM(require("path"), 1);
8974
9132
  init_loader();
8975
9133
  function listHuntsTool(configPath) {
8976
9134
  const { configDir } = loadConfig(configPath);
@@ -8978,7 +9136,7 @@ function listHuntsTool(configPath) {
8978
9136
  }
8979
9137
  async function runSuiteTool(args = {}, options = {}) {
8980
9138
  const { configPath: resolvedConfigPath, configDir, config } = loadConfig(options.configPath);
8981
- const projectRoot = options.projectRoot ?? import_node_path23.default.dirname(configDir);
9139
+ const projectRoot = options.projectRoot ?? import_node_path24.default.dirname(configDir);
8982
9140
  const suite = await runSuite({
8983
9141
  configPath: resolvedConfigPath,
8984
9142
  includeTags: args.includeTags,
@@ -8987,8 +9145,8 @@ async function runSuiteTool(args = {}, options = {}) {
8987
9145
  });
8988
9146
  const bugLogCfg = config.bugLog ?? {};
8989
9147
  const logBugs = args.logBugs ?? bugLogCfg.enabled ?? true;
8990
- const backlogPath = bugLogCfg.backlogPath ? import_node_path23.default.resolve(projectRoot, bugLogCfg.backlogPath) : void 0;
8991
- const resolvedPath = bugLogCfg.resolvedPath ? import_node_path23.default.resolve(projectRoot, bugLogCfg.resolvedPath) : backlogPath ? import_node_path23.default.join(import_node_path23.default.dirname(backlogPath), "resolved.md") : void 0;
9148
+ const backlogPath = bugLogCfg.backlogPath ? import_node_path24.default.resolve(projectRoot, bugLogCfg.backlogPath) : void 0;
9149
+ const resolvedPath = bugLogCfg.resolvedPath ? import_node_path24.default.resolve(projectRoot, bugLogCfg.resolvedPath) : backlogPath ? import_node_path24.default.join(import_node_path24.default.dirname(backlogPath), "resolved.md") : void 0;
8992
9150
  const bugs = logBugs ? updateBacklogFromSuite(suite, { projectRoot, backlogPath, resolvedPath }) : { created: [], regressions: [], skipped: [], backlogPath: null };
8993
9151
  const { status, totalHunts, passed, failed, skipped } = suite.result;
8994
9152
  return {
@@ -9016,8 +9174,8 @@ async function runHuntTool(args, configPath) {
9016
9174
 
9017
9175
  // src/mcp/projects.ts
9018
9176
  var import_node_fs21 = __toESM(require("fs"), 1);
9019
- var import_node_os3 = __toESM(require("os"), 1);
9020
- var import_node_path24 = __toESM(require("path"), 1);
9177
+ var import_node_os5 = __toESM(require("os"), 1);
9178
+ var import_node_path25 = __toESM(require("path"), 1);
9021
9179
  var import_yaml3 = __toESM(require("yaml"), 1);
9022
9180
  var import_zod2 = require("zod");
9023
9181
  var projectEntrySchema = import_zod2.z.object({
@@ -9029,25 +9187,25 @@ var projectRegistrySchema = import_zod2.z.object({
9029
9187
  projects: import_zod2.z.record(import_zod2.z.string().min(1), projectEntrySchema)
9030
9188
  }).strict();
9031
9189
  function defaultRegistryPath() {
9032
- return import_node_path24.default.join(import_node_os3.default.homedir(), ".prowl", "projects.yml");
9190
+ return import_node_path25.default.join(import_node_os5.default.homedir(), ".prowl", "projects.yml");
9033
9191
  }
9034
9192
  function legacyRegistryPath() {
9035
- return import_node_path24.default.join(import_node_os3.default.homedir(), ".prowlqa", "projects.yml");
9193
+ return import_node_path25.default.join(import_node_os5.default.homedir(), ".prowlqa", "projects.yml");
9036
9194
  }
9037
9195
  function resolveProjectConfigPath(root) {
9038
- const preferred = import_node_path24.default.join(root, ".prowl", "config.yml");
9196
+ const preferred = import_node_path25.default.join(root, ".prowl", "config.yml");
9039
9197
  if (import_node_fs21.default.existsSync(preferred)) return preferred;
9040
- const legacy = import_node_path24.default.join(root, ".prowlqa", "config.yml");
9198
+ const legacy = import_node_path25.default.join(root, ".prowlqa", "config.yml");
9041
9199
  if (import_node_fs21.default.existsSync(legacy)) return legacy;
9042
9200
  return preferred;
9043
9201
  }
9044
9202
  function resolveRegistryRelativePath(registry, inputPath) {
9045
- return import_node_path24.default.isAbsolute(inputPath) ? inputPath : import_node_path24.default.resolve(import_node_path24.default.dirname(registry.registryPath), inputPath);
9203
+ return import_node_path25.default.isAbsolute(inputPath) ? inputPath : import_node_path25.default.resolve(import_node_path25.default.dirname(registry.registryPath), inputPath);
9046
9204
  }
9047
9205
  function resolveRegistryPath(explicitPath) {
9048
- if (explicitPath) return import_node_path24.default.resolve(explicitPath);
9206
+ if (explicitPath) return import_node_path25.default.resolve(explicitPath);
9049
9207
  const envPath = process.env.PROWL_PROJECTS ?? process.env.PROWLQA_PROJECTS;
9050
- if (envPath) return import_node_path24.default.resolve(envPath);
9208
+ if (envPath) return import_node_path25.default.resolve(envPath);
9051
9209
  const fallback = defaultRegistryPath();
9052
9210
  if (import_node_fs21.default.existsSync(fallback)) return fallback;
9053
9211
  const legacy = legacyRegistryPath();
@@ -9194,10 +9352,377 @@ function buildMcpCommand() {
9194
9352
  });
9195
9353
  }
9196
9354
 
9355
+ // src/cli/commands/macdriver.ts
9356
+ var import_chalk15 = __toESM(require("chalk"), 1);
9357
+ var import_commander13 = require("commander");
9358
+
9359
+ // src/browser/macdriver-install.ts
9360
+ var import_node_child_process7 = require("child_process");
9361
+ var import_node_crypto3 = require("crypto");
9362
+ var import_node_fs22 = __toESM(require("fs"), 1);
9363
+ var import_node_os6 = __toESM(require("os"), 1);
9364
+ var import_node_path26 = __toESM(require("path"), 1);
9365
+ var import_node_util = require("util");
9366
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process7.execFile);
9367
+ function parseChecksumFile(text) {
9368
+ const token = text.trim().split(/\s+/, 1)[0] ?? "";
9369
+ const digest = token.toLowerCase();
9370
+ if (!/^[0-9a-f]{64}$/.test(digest)) {
9371
+ throw new Error(`Malformed .sha256 checksum file (expected a 64-char hex digest, got: ${text.trim().slice(0, 80)})`);
9372
+ }
9373
+ return digest;
9374
+ }
9375
+ function sha256Hex(bytes) {
9376
+ return (0, import_node_crypto3.createHash)("sha256").update(bytes).digest("hex");
9377
+ }
9378
+ var commandRunner = async (file, args) => execFileAsync(file, args);
9379
+ function commandOutput(result) {
9380
+ return [result.stdout, result.stderr].filter((value) => value !== void 0).map((value) => value.toString()).join("\n").trim();
9381
+ }
9382
+ function commandErrorDetail(error) {
9383
+ const err = error;
9384
+ return [err.stderr, err.stdout, err.message].filter((value) => value !== void 0 && value !== "").map((value) => value.toString()).join("\n").trim();
9385
+ }
9386
+ async function runRequiredCommand(run, binaryPath, command, args, label) {
9387
+ try {
9388
+ return await run(command, args);
9389
+ } catch (error) {
9390
+ const detail = commandErrorDetail(error);
9391
+ throw new Error(`${label} failed for ${binaryPath}${detail ? `: ${detail}` : ""}`);
9392
+ }
9393
+ }
9394
+ var zipinfoArchiveLister = async (zipPath) => {
9395
+ try {
9396
+ const { stdout } = await execFileAsync("zipinfo", ["-1", zipPath]);
9397
+ return stdout.toString().split(/\r?\n/).filter((entry) => entry.length > 0);
9398
+ } catch (error) {
9399
+ const detail = commandErrorDetail(error);
9400
+ throw new Error(`Failed to inspect release archive ${zipPath} with zipinfo${detail ? `: ${detail}` : ""}`);
9401
+ }
9402
+ };
9403
+ function normalizeArchiveEntryName(entry) {
9404
+ if (entry.length === 0 || entry !== entry.trim() || entry.includes("\0") || entry.includes("\\")) {
9405
+ throw new Error(`Unsafe path in prowl-macdriver release archive: ${JSON.stringify(entry)}`);
9406
+ }
9407
+ if (entry.endsWith("/")) {
9408
+ throw new Error(`Unexpected directory in prowl-macdriver release archive: ${entry}`);
9409
+ }
9410
+ if (import_node_path26.default.posix.isAbsolute(entry)) {
9411
+ throw new Error(`Unsafe absolute path in prowl-macdriver release archive: ${entry}`);
9412
+ }
9413
+ const parts = entry.split("/");
9414
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
9415
+ throw new Error(`Unsafe path in prowl-macdriver release archive: ${entry}`);
9416
+ }
9417
+ return entry;
9418
+ }
9419
+ function validateMacdriverArchiveEntries(entries) {
9420
+ const normalized = entries.map(normalizeArchiveEntryName);
9421
+ if (normalized.length !== 1 || normalized[0] !== HELPER_BINARY) {
9422
+ const shown = normalized.length > 0 ? normalized.join(", ") : "(empty archive)";
9423
+ throw new Error(
9424
+ `Unexpected prowl-macdriver release archive contents: ${shown}. Expected exactly "${HELPER_BINARY}" at the archive root.`
9425
+ );
9426
+ }
9427
+ }
9428
+ var dittoExtractor = async (zipPath, destDir) => {
9429
+ await execFileAsync("ditto", ["-x", "-k", zipPath, destDir]);
9430
+ };
9431
+ function parseCodesignDetails(text) {
9432
+ const details = { identifier: null, authorities: [], teamIdentifier: null };
9433
+ for (const line of text.split(/\r?\n/)) {
9434
+ const trimmed = line.trim();
9435
+ if (trimmed.startsWith("Identifier=")) {
9436
+ details.identifier = trimmed.slice("Identifier=".length);
9437
+ } else if (trimmed.startsWith("Authority=")) {
9438
+ details.authorities.push(trimmed.slice("Authority=".length));
9439
+ } else if (trimmed.startsWith("TeamIdentifier=")) {
9440
+ details.teamIdentifier = trimmed.slice("TeamIdentifier=".length);
9441
+ }
9442
+ }
9443
+ return details;
9444
+ }
9445
+ function validateCodesignDetails(details, binaryPath) {
9446
+ if (details.identifier !== MACDRIVER_SIGNING_IDENTIFIER) {
9447
+ throw new Error(
9448
+ `codesign verification failed for ${binaryPath}: expected identifier "${MACDRIVER_SIGNING_IDENTIFIER}", got "${details.identifier ?? "missing"}"`
9449
+ );
9450
+ }
9451
+ const developerIdAuthority = details.authorities.find(
9452
+ (authority) => authority.startsWith(`${MACDRIVER_SIGNING_AUTHORITY_PREFIX} (`)
9453
+ );
9454
+ const authorityTeamId = developerIdAuthority?.match(/\(([A-Z0-9]{10})\)$/)?.[1] ?? null;
9455
+ if (!developerIdAuthority || !authorityTeamId) {
9456
+ const shown = details.authorities.length > 0 ? details.authorities.join(" / ") : "missing";
9457
+ throw new Error(
9458
+ `codesign verification failed for ${binaryPath}: expected ${MACDRIVER_SIGNING_AUTHORITY_PREFIX} signer, got ${shown}`
9459
+ );
9460
+ }
9461
+ if (!details.teamIdentifier) {
9462
+ throw new Error(`codesign verification failed for ${binaryPath}: missing TeamIdentifier`);
9463
+ }
9464
+ if (details.teamIdentifier !== authorityTeamId) {
9465
+ throw new Error(
9466
+ `codesign verification failed for ${binaryPath}: TeamIdentifier ${details.teamIdentifier} does not match Developer ID authority team ${authorityTeamId}`
9467
+ );
9468
+ }
9469
+ }
9470
+ async function verifyMacdriverSignature(binaryPath, run = commandRunner) {
9471
+ await runRequiredCommand(run, binaryPath, "codesign", ["--verify", "--strict", binaryPath], "codesign verification");
9472
+ const display = await runRequiredCommand(
9473
+ run,
9474
+ binaryPath,
9475
+ "codesign",
9476
+ ["--display", "--verbose=4", binaryPath],
9477
+ "codesign detail inspection"
9478
+ );
9479
+ validateCodesignDetails(parseCodesignDetails(commandOutput(display)), binaryPath);
9480
+ await runRequiredCommand(
9481
+ run,
9482
+ binaryPath,
9483
+ "spctl",
9484
+ ["--assess", "--type", "execute", "--verbose=4", binaryPath],
9485
+ "spctl assessment"
9486
+ );
9487
+ }
9488
+ var codesignVerifier = async (binaryPath) => {
9489
+ await verifyMacdriverSignature(binaryPath);
9490
+ };
9491
+ async function downloadAndVerify(options = {}) {
9492
+ const version = options.version ?? MACDRIVER_VERSION;
9493
+ const fetchImpl = options.fetchImpl ?? fetch;
9494
+ const assetName = macdriverAssetName(version);
9495
+ const zipUrl = macdriverAssetUrl(assetName, version);
9496
+ const sumUrl = macdriverAssetUrl(macdriverChecksumName(version), version);
9497
+ const [zipRes, sumRes] = await Promise.all([
9498
+ fetchImpl(zipUrl, { redirect: "follow" }),
9499
+ fetchImpl(sumUrl, { redirect: "follow" })
9500
+ ]);
9501
+ if (zipRes.status === 404 || sumRes.status === 404) {
9502
+ throw new Error(
9503
+ `No published prowl-macdriver release for ${macdriverReleaseTag(version)} yet.
9504
+ The signed binary is cut by the maintainer; until then build from source:
9505
+ cd macdriver && swift build -c release`
9506
+ );
9507
+ }
9508
+ if (!zipRes.ok) {
9509
+ throw new Error(`Failed to download ${assetName} (HTTP ${zipRes.status}) from ${zipUrl}`);
9510
+ }
9511
+ if (!sumRes.ok) {
9512
+ throw new Error(`Failed to download the checksum (HTTP ${sumRes.status}) from ${sumUrl}`);
9513
+ }
9514
+ const zipBytes = Buffer.from(await zipRes.arrayBuffer());
9515
+ const expected = parseChecksumFile(await sumRes.text());
9516
+ const actual = sha256Hex(zipBytes);
9517
+ if (actual !== expected) {
9518
+ throw new Error(
9519
+ `Checksum mismatch for ${assetName}.
9520
+ expected: ${expected}
9521
+ actual: ${actual}
9522
+ The download was rejected and discarded; re-run the install, and report it if it repeats.`
9523
+ );
9524
+ }
9525
+ return zipBytes;
9526
+ }
9527
+ async function installMacdriver(options = {}) {
9528
+ const version = validateMacdriverVersion(options.version ?? MACDRIVER_VERSION);
9529
+ const homedir = options.homedir ?? import_node_os6.default.homedir();
9530
+ const listArchiveEntries = options.listArchiveEntries ?? zipinfoArchiveLister;
9531
+ const extract = options.extract ?? dittoExtractor;
9532
+ const verifySignature = options.verifySignature ?? codesignVerifier;
9533
+ const installRoot = macdriverInstallRoot(homedir);
9534
+ const versionDir = macdriverVersionDir(version, homedir);
9535
+ const binaryPath = macdriverInstalledBinary(version, homedir);
9536
+ if (!options.force && import_node_fs22.default.existsSync(binaryPath)) {
9537
+ return { version, binaryPath, alreadyInstalled: true };
9538
+ }
9539
+ const zipBytes = await downloadAndVerify({ version, fetchImpl: options.fetchImpl });
9540
+ import_node_fs22.default.mkdirSync(installRoot, { recursive: true });
9541
+ const stagingDir = import_node_fs22.default.mkdtempSync(import_node_path26.default.join(installRoot, `.tmp-${version}-`));
9542
+ const extractDir = import_node_path26.default.join(stagingDir, "extract");
9543
+ const zipPath = import_node_path26.default.join(stagingDir, macdriverAssetName(version));
9544
+ try {
9545
+ import_node_fs22.default.mkdirSync(extractDir);
9546
+ import_node_fs22.default.writeFileSync(zipPath, zipBytes);
9547
+ validateMacdriverArchiveEntries(await listArchiveEntries(zipPath));
9548
+ await extract(zipPath, extractDir);
9549
+ const stagedBinaryPath = import_node_path26.default.join(extractDir, HELPER_BINARY);
9550
+ assertExtractedHelper(extractDir, stagedBinaryPath);
9551
+ import_node_fs22.default.chmodSync(stagedBinaryPath, 493);
9552
+ await verifySignature(stagedBinaryPath);
9553
+ replaceVersionDir(versionDir, extractDir, installRoot);
9554
+ } finally {
9555
+ import_node_fs22.default.rmSync(stagingDir, { recursive: true, force: true });
9556
+ }
9557
+ return { version, binaryPath, alreadyInstalled: false };
9558
+ }
9559
+ function assertExtractedHelper(extractDir, binaryPath) {
9560
+ const entries = import_node_fs22.default.readdirSync(extractDir);
9561
+ if (!entries.includes(HELPER_BINARY)) {
9562
+ throw new Error(`The release archive did not contain a "${HELPER_BINARY}" binary.`);
9563
+ }
9564
+ if (entries.length !== 1 || entries[0] !== HELPER_BINARY) {
9565
+ const shown = entries.length > 0 ? entries.join(", ") : "(empty directory)";
9566
+ throw new Error(
9567
+ `Unexpected extracted prowl-macdriver archive contents: ${shown}. Expected exactly "${HELPER_BINARY}".`
9568
+ );
9569
+ }
9570
+ let stat = null;
9571
+ try {
9572
+ stat = import_node_fs22.default.lstatSync(binaryPath);
9573
+ } catch {
9574
+ }
9575
+ if (!stat?.isFile()) {
9576
+ throw new Error(`The release archive "${HELPER_BINARY}" entry is not a regular file.`);
9577
+ }
9578
+ }
9579
+ function replaceVersionDir(versionDir, stagedVersionDir, installRoot) {
9580
+ const backupDir = import_node_path26.default.join(installRoot, `.previous-${import_node_path26.default.basename(versionDir)}-${process.pid}-${Date.now()}`);
9581
+ let backedUp = false;
9582
+ try {
9583
+ if (import_node_fs22.default.existsSync(versionDir)) {
9584
+ import_node_fs22.default.renameSync(versionDir, backupDir);
9585
+ backedUp = true;
9586
+ }
9587
+ import_node_fs22.default.renameSync(stagedVersionDir, versionDir);
9588
+ if (backedUp) {
9589
+ import_node_fs22.default.rmSync(backupDir, { recursive: true, force: true });
9590
+ }
9591
+ } catch (error) {
9592
+ if (backedUp && !import_node_fs22.default.existsSync(versionDir) && import_node_fs22.default.existsSync(backupDir)) {
9593
+ import_node_fs22.default.renameSync(backupDir, versionDir);
9594
+ }
9595
+ throw error;
9596
+ }
9597
+ }
9598
+ var runVersionProbe = async (binaryPath) => {
9599
+ try {
9600
+ const { stdout } = await execFileAsync(binaryPath, ["version"], { timeout: 5e3 });
9601
+ const match = stdout.match(/prowl-macdriver\s+(\S+)/);
9602
+ return match ? match[1] : stdout.trim() || null;
9603
+ } catch {
9604
+ return null;
9605
+ }
9606
+ };
9607
+ async function collectMacdriverStatus(options = {}) {
9608
+ const env = options.env ?? process.env;
9609
+ const homedir = options.homedir ?? import_node_os6.default.homedir();
9610
+ const probe = options.probe ?? runVersionProbe;
9611
+ let resolved = null;
9612
+ try {
9613
+ const resolvedPath = resolveHelperBinary(env, { homedir });
9614
+ resolved = { path: resolvedPath, source: classifyResolvedSource(resolvedPath, env, homedir) };
9615
+ } catch {
9616
+ resolved = null;
9617
+ }
9618
+ const installed = [];
9619
+ const root = macdriverInstallRoot(homedir);
9620
+ if (import_node_fs22.default.existsSync(root)) {
9621
+ for (const entry of import_node_fs22.default.readdirSync(root, { withFileTypes: true })) {
9622
+ if (!entry.isDirectory()) continue;
9623
+ let binaryPath;
9624
+ try {
9625
+ binaryPath = macdriverInstalledBinary(entry.name, homedir);
9626
+ } catch {
9627
+ continue;
9628
+ }
9629
+ if (import_node_fs22.default.existsSync(binaryPath)) {
9630
+ installed.push({ version: entry.name, binaryPath });
9631
+ }
9632
+ }
9633
+ installed.sort((a, b) => a.version.localeCompare(b.version));
9634
+ }
9635
+ const probedVersion = resolved ? await probe(resolved.path) : null;
9636
+ return { resolved, pinnedVersion: MACDRIVER_VERSION, installed, probedVersion };
9637
+ }
9638
+ function classifyResolvedSource(resolvedPath, env, homedir) {
9639
+ if (env.PROWL_MACDRIVER_BIN && resolvedPath === env.PROWL_MACDRIVER_BIN) {
9640
+ return "env";
9641
+ }
9642
+ if (resolvedPath.startsWith(macdriverInstallRoot(homedir) + import_node_path26.default.sep)) {
9643
+ return "user-install";
9644
+ }
9645
+ return "source-build";
9646
+ }
9647
+ function tccGuidance() {
9648
+ 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.";
9649
+ }
9650
+
9651
+ // src/cli/commands/macdriver.ts
9652
+ function buildInstallCommand() {
9653
+ return new import_commander13.Command("install").description("Download and install the prebuilt, signed prowl-macdriver helper").option("--force", "Reinstall even if the pinned version is already present").action(async (options) => {
9654
+ try {
9655
+ console.log(
9656
+ import_chalk15.default.cyan(
9657
+ `Installing prowl-macdriver ${MACDRIVER_VERSION} (${macdriverReleaseTag(MACDRIVER_VERSION)})\u2026`
9658
+ )
9659
+ );
9660
+ const result = await installMacdriver({ force: options.force });
9661
+ if (result.alreadyInstalled) {
9662
+ console.log(
9663
+ import_chalk15.default.green(`Already installed at ${result.binaryPath}`) + import_chalk15.default.dim(" (use --force to reinstall)")
9664
+ );
9665
+ } else {
9666
+ console.log(import_chalk15.default.green(`Installed prowl-macdriver ${result.version} \u2192 ${result.binaryPath}`));
9667
+ }
9668
+ console.log("");
9669
+ console.log(tccGuidance());
9670
+ } catch (error) {
9671
+ const message = error instanceof Error ? error.message : "Install failed";
9672
+ console.error(import_chalk15.default.red(`Error: ${message}`));
9673
+ process.exitCode = 1;
9674
+ }
9675
+ });
9676
+ }
9677
+ function buildStatusCommand() {
9678
+ return new import_commander13.Command("status").description("Report the resolved prowl-macdriver binary, installed versions, and permissions guidance").action(async () => {
9679
+ try {
9680
+ const status = await collectMacdriverStatus();
9681
+ console.log(import_chalk15.default.bold("prowl-macdriver status"));
9682
+ console.log(` pinned version: ${status.pinnedVersion}`);
9683
+ if (status.resolved) {
9684
+ const label = status.resolved.source === "env" ? "PROWL_MACDRIVER_BIN override" : status.resolved.source === "user-install" ? "user install (~/.prowl/macdriver)" : "repo source build (macdriver/.build)";
9685
+ console.log(` resolved binary: ${status.resolved.path}`);
9686
+ console.log(` resolved via: ${label}`);
9687
+ console.log(
9688
+ ` runs: ${status.probedVersion ? import_chalk15.default.green(`yes (reports ${status.probedVersion})`) : import_chalk15.default.yellow("no (binary did not respond to `version`)")}`
9689
+ );
9690
+ } else {
9691
+ console.log(` resolved binary: ${import_chalk15.default.yellow("none")}`);
9692
+ console.log(import_chalk15.default.dim(" search order: PROWL_MACDRIVER_BIN \u2192 ~/.prowl/macdriver \u2192 macdriver/.build"));
9693
+ console.log(import_chalk15.default.cyan(" Run `prowl macdriver install` to install the prebuilt helper."));
9694
+ }
9695
+ if (status.installed.length > 0) {
9696
+ console.log(" installed versions:");
9697
+ for (const entry of status.installed) {
9698
+ const marker = entry.version === status.pinnedVersion ? import_chalk15.default.green(" (pinned)") : "";
9699
+ console.log(` - ${entry.version}${marker} ${import_chalk15.default.dim(entry.binaryPath)}`);
9700
+ }
9701
+ } else {
9702
+ console.log(" installed versions: none");
9703
+ }
9704
+ console.log("");
9705
+ console.log(tccGuidance());
9706
+ } catch (error) {
9707
+ const message = error instanceof Error ? error.message : "Status failed";
9708
+ console.error(import_chalk15.default.red(`Error: ${message}`));
9709
+ process.exitCode = 1;
9710
+ }
9711
+ });
9712
+ }
9713
+ function buildMacdriverCommand() {
9714
+ const command = new import_commander13.Command("macdriver").description(
9715
+ "Manage the macOS helper binary (prowl-macdriver) for the macOS target"
9716
+ );
9717
+ command.addCommand(buildInstallCommand());
9718
+ command.addCommand(buildStatusCommand());
9719
+ return command;
9720
+ }
9721
+
9197
9722
  // src/cli/program.ts
9198
9723
  var CLI_VERSION = package_default.version;
9199
9724
  function buildProgram() {
9200
- const program2 = new import_commander13.Command();
9725
+ const program2 = new import_commander14.Command();
9201
9726
  program2.name("prowl").description("CLI-first QA testing tool for deterministic Playwright flows").version(CLI_VERSION);
9202
9727
  program2.addCommand(buildRunCommand());
9203
9728
  program2.addCommand(buildCiCommand());
@@ -9211,6 +9736,7 @@ function buildProgram() {
9211
9736
  program2.addCommand(buildHistoryCommand());
9212
9737
  program2.addCommand(buildFlakyCommand());
9213
9738
  program2.addCommand(buildMcpCommand());
9739
+ program2.addCommand(buildMacdriverCommand());
9214
9740
  return program2;
9215
9741
  }
9216
9742