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/README.md +334 -183
- package/dist/{chunk-WHAMB4TY.js → chunk-O3OUTZ2P.js} +18 -2
- package/dist/{chunk-WHAMB4TY.js.map → chunk-O3OUTZ2P.js.map} +1 -1
- package/dist/{chunk-5KQR3IR3.js → chunk-R7NUH44M.js} +551 -107
- package/dist/chunk-R7NUH44M.js.map +1 -0
- package/dist/index.cjs +691 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +108 -13
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +693 -220
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +164 -7
- package/dist/lib.d.ts +164 -7
- package/dist/lib.js +58 -4
- package/dist/{loader-PBBYV3U7.js → loader-X37URHUV.js} +2 -2
- package/examples/hunts/hello.yml +1 -1
- package/examples/hunts/login-flow.yml +58 -0
- package/package.json +6 -3
- package/dist/chunk-5KQR3IR3.js.map +0 -1
- /package/dist/{loader-PBBYV3U7.js.map → loader-X37URHUV.js.map} +0 -0
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}".
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
724
|
-
description: "
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1796
|
-
const 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(
|
|
1860
|
+
if (import_node_fs4.default.existsSync(import_node_path5.default.join(dir, "package.json"))) {
|
|
1799
1861
|
return dir;
|
|
1800
1862
|
}
|
|
1801
|
-
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
|
-
|
|
1819
|
-
|
|
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
|
|
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,
|
|
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}${
|
|
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} ${
|
|
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} ${
|
|
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,
|
|
2859
|
+
async function locate(query, path27) {
|
|
2793
2860
|
return transport.request(
|
|
2794
2861
|
"POST",
|
|
2795
|
-
`${base}${
|
|
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 =
|
|
2929
|
+
const pkgDir = import_node_path6.default.dirname(pkgJsonPath);
|
|
2863
2930
|
const version = requireFn(pkgJsonPath).version;
|
|
2864
|
-
const serverApk =
|
|
2865
|
-
const testApk =
|
|
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 =
|
|
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
|
|
2990
|
-
var
|
|
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
|
|
3215
|
-
var
|
|
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 =
|
|
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)(
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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}${
|
|
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} ${
|
|
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} ${
|
|
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,
|
|
3552
|
-
const parsed = await this.requestFull(method,
|
|
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,
|
|
3629
|
-
return transport.request("POST", `${base}${
|
|
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 =
|
|
3772
|
+
const pkgDir = import_node_path8.default.dirname(pkgJsonPath);
|
|
3706
3773
|
const version = requireFn(pkgJsonPath).version;
|
|
3707
|
-
const projectPath =
|
|
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 =
|
|
3714
|
-
return
|
|
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
|
|
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 ?
|
|
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(
|
|
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 ??
|
|
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 =
|
|
3873
|
-
const jsonPath =
|
|
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 (!
|
|
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 =
|
|
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
|
|
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
|
|
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 !==
|
|
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) =>
|
|
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 =
|
|
5311
|
-
const resolvedConfigDir =
|
|
5312
|
-
const resolvedFilePath =
|
|
5313
|
-
const relativePath =
|
|
5314
|
-
const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${
|
|
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 =
|
|
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 =
|
|
5404
|
-
const currentScreenshotPath =
|
|
5405
|
-
import_node_fs9.default.mkdirSync(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
5926
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
5807
5927
|
function writeResult(runDir, result) {
|
|
5808
5928
|
const fileName = "result.json";
|
|
5809
|
-
const fullPath =
|
|
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
|
|
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 =
|
|
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
|
|
6022
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
5903
6023
|
function escapeXml(text) {
|
|
5904
6024
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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 (
|
|
6217
|
+
if (import_node_path15.default.isAbsolute(inputPath)) {
|
|
6092
6218
|
return inputPath;
|
|
6093
6219
|
}
|
|
6094
|
-
const projectRoot =
|
|
6095
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
6764
|
-
const 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(
|
|
6914
|
+
if (import_node_fs15.default.existsSync(import_node_path16.default.join(dir, "package.json"))) {
|
|
6767
6915
|
return dir;
|
|
6768
6916
|
}
|
|
6769
|
-
dir =
|
|
6917
|
+
dir = import_node_path16.default.dirname(dir);
|
|
6770
6918
|
}
|
|
6771
|
-
if (import_node_fs15.default.existsSync(
|
|
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(
|
|
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 =
|
|
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 =
|
|
6795
|
-
const exampleConfig =
|
|
6796
|
-
const exampleHuntsDir =
|
|
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,
|
|
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
|
-
|
|
6807
|
-
|
|
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(
|
|
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("
|
|
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
|
|
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 (
|
|
6985
|
+
if (import_node_path17.default.isAbsolute(inputPath)) {
|
|
6838
6986
|
return inputPath;
|
|
6839
6987
|
}
|
|
6840
|
-
const projectRoot =
|
|
6841
|
-
return
|
|
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
|
|
7083
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
6935
7084
|
function getWatchTargets(configDir, huntName) {
|
|
6936
7085
|
return [
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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 ??
|
|
7383
|
-
const resolvedPath = options.resolvedPath ??
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
7861
|
+
runDir = import_node_path22.default.isAbsolute(options.run) ? options.run : import_node_path22.default.resolve(options.run);
|
|
7709
7862
|
} else {
|
|
7710
|
-
const runsDir =
|
|
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(
|
|
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 =
|
|
7875
|
+
runDir = import_node_path22.default.join(runsDir, entries[0]);
|
|
7723
7876
|
}
|
|
7724
|
-
const screenshotsDir =
|
|
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 =
|
|
7745
|
-
const destPath =
|
|
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
|
|
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 =
|
|
8923
|
+
configDir = import_node_path23.default.join(process.cwd(), ".prowl");
|
|
8771
8924
|
}
|
|
8772
|
-
const huntsDir =
|
|
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 =
|
|
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(
|
|
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((
|
|
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
|
|
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 ??
|
|
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 ?
|
|
8991
|
-
const resolvedPath = bugLogCfg.resolvedPath ?
|
|
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
|
|
9020
|
-
var
|
|
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
|
|
9190
|
+
return import_node_path25.default.join(import_node_os5.default.homedir(), ".prowl", "projects.yml");
|
|
9033
9191
|
}
|
|
9034
9192
|
function legacyRegistryPath() {
|
|
9035
|
-
return
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|