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