prowl-tools 0.1.8 → 0.1.10
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 +103 -14
- package/dist/{chunk-CWLRDV5P.js → chunk-SRWAF2RI.js} +180 -19
- package/dist/chunk-SRWAF2RI.js.map +1 -0
- package/dist/{chunk-JFJQNJSJ.js → chunk-TTZ2IL3P.js} +27 -1
- package/dist/chunk-TTZ2IL3P.js.map +1 -0
- package/dist/index.cjs +843 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +622 -43
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +204 -17
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +72 -1
- package/dist/lib.d.ts +72 -1
- package/dist/lib.js +2 -2
- package/dist/{loader-JTHA4BYG.js → loader-CAE4FPBK.js} +2 -2
- package/examples/hunts/form.yml +53 -0
- package/examples/hunts/macos-hello.yml +49 -0
- package/package.json +4 -2
- package/dist/chunk-CWLRDV5P.js.map +0 -1
- package/dist/chunk-JFJQNJSJ.js.map +0 -1
- /package/dist/{loader-JTHA4BYG.js.map → loader-CAE4FPBK.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -74,7 +74,7 @@ var init_hunt_name = __esm({
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
// src/config/schema.ts
|
|
77
|
-
var import_zod, webTargetSchema, macosTargetSchema, androidTargetSchema, iosTargetSchema, targetSchema, configSchema, navigateStepSchema, clickStepSchema, singleKeyValueSchema, fillStepSchema, typeStepSchema, pressStepSchema, waitForSelectorStepSchema, waitStepSchema, waitForUrlStepSchema, waitForNetworkIdleStepSchema, selectOptionStepSchema, selectStepSchema, onDialogStepSchema, setInputFilesStepSchema, inlineAssertStepSchema, runHuntStepSchema, hoverStepSchema, scrollStepSchema, scrollToStepSchema, screenshotStepSchema, ifStepSchema, repeatStepSchema, mockRouteStepSchema, unmockRouteStepSchema, evalScriptStepSchema, runScriptStepSchema, assertScreenshotStepSchema, assertWithAiStepSchema, copyTextStepSchema, waitForDownloadStepSchema, stepSchema, assertionSchema, huntSchema;
|
|
77
|
+
var import_zod, webTargetSchema, macosTargetSchema, androidTargetSchema, iosTargetSchema, targetSchema, configSchema, navigateStepSchema, clickStepSchema, singleKeyValueSchema, fillStepSchema, typeStepSchema, pressStepSchema, waitForSelectorStepSchema, waitStepSchema, waitForUrlStepSchema, waitForNetworkIdleStepSchema, waitForResponseStatusSchema, waitForResponseTimeoutSchema, waitForResponseStepSchema, selectOptionStepSchema, selectStepSchema, onDialogStepSchema, setInputFilesStepSchema, inlineAssertStepSchema, runHuntStepSchema, hoverStepSchema, scrollStepSchema, scrollToStepSchema, screenshotStepSchema, ifStepSchema, repeatStepSchema, mockRouteStepSchema, unmockRouteStepSchema, evalScriptStepSchema, runScriptStepSchema, assertScreenshotStepSchema, assertWithAiStepSchema, copyTextStepSchema, waitForDownloadStepSchema, stepSchema, assertionSchema, huntSchema;
|
|
78
78
|
var init_schema = __esm({
|
|
79
79
|
"src/config/schema.ts"() {
|
|
80
80
|
"use strict";
|
|
@@ -199,6 +199,21 @@ var init_schema = __esm({
|
|
|
199
199
|
waitForNetworkIdleStepSchema = import_zod.z.object({
|
|
200
200
|
waitForNetworkIdle: import_zod.z.object({ timeout: import_zod.z.number().optional() }).strict()
|
|
201
201
|
}).strict();
|
|
202
|
+
waitForResponseStatusSchema = import_zod.z.number().int().min(100).max(599);
|
|
203
|
+
waitForResponseTimeoutSchema = import_zod.z.number().int().min(0).finite();
|
|
204
|
+
waitForResponseStepSchema = import_zod.z.object({
|
|
205
|
+
waitForResponse: import_zod.z.object({
|
|
206
|
+
// Matched against each response URL as a glob (`*`/`**` match any run of
|
|
207
|
+
// characters including `/`, `?` matches one character); matching is
|
|
208
|
+
// unanchored, so a pattern with no wildcards behaves as a substring match.
|
|
209
|
+
url: import_zod.z.string().min(1).describe(
|
|
210
|
+
"URL glob to wait for; unanchored, so a wildcard-free pattern is a substring match"
|
|
211
|
+
),
|
|
212
|
+
// Optional status filter: only resolve on a response with this exact HTTP status.
|
|
213
|
+
status: waitForResponseStatusSchema.optional(),
|
|
214
|
+
timeout: waitForResponseTimeoutSchema.optional()
|
|
215
|
+
}).strict()
|
|
216
|
+
}).strict();
|
|
202
217
|
selectOptionStepSchema = import_zod.z.object({
|
|
203
218
|
selectOption: import_zod.z.object({ selector: import_zod.z.string().min(1), value: import_zod.z.string() }).strict()
|
|
204
219
|
}).strict();
|
|
@@ -360,6 +375,7 @@ var init_schema = __esm({
|
|
|
360
375
|
waitForSelectorStepSchema,
|
|
361
376
|
waitForUrlStepSchema,
|
|
362
377
|
waitForNetworkIdleStepSchema,
|
|
378
|
+
waitForResponseStepSchema,
|
|
363
379
|
hoverStepSchema,
|
|
364
380
|
scrollStepSchema,
|
|
365
381
|
scrollToStepSchema,
|
|
@@ -592,6 +608,16 @@ function interpolateStep(step, vars, stepPath2, redacted) {
|
|
|
592
608
|
if ("waitForNetworkIdle" in step) {
|
|
593
609
|
return { waitForNetworkIdle: { timeout: step.waitForNetworkIdle.timeout } };
|
|
594
610
|
}
|
|
611
|
+
if ("waitForResponse" in step) {
|
|
612
|
+
const urlResult = interpolateString(step.waitForResponse.url, vars);
|
|
613
|
+
return {
|
|
614
|
+
waitForResponse: {
|
|
615
|
+
url: urlResult.value,
|
|
616
|
+
...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
|
|
617
|
+
timeout: step.waitForResponse.timeout
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
595
621
|
if ("hover" in step) {
|
|
596
622
|
const selectorResult = interpolateString(step.hover.selector, vars);
|
|
597
623
|
return { hover: { selector: selectorResult.value } };
|
|
@@ -1128,12 +1154,12 @@ var init_visual = __esm({
|
|
|
1128
1154
|
});
|
|
1129
1155
|
|
|
1130
1156
|
// src/cli/program.ts
|
|
1131
|
-
var
|
|
1157
|
+
var import_commander15 = require("commander");
|
|
1132
1158
|
|
|
1133
1159
|
// package.json
|
|
1134
1160
|
var package_default = {
|
|
1135
1161
|
name: "prowl-tools",
|
|
1136
|
-
version: "0.1.
|
|
1162
|
+
version: "0.1.10",
|
|
1137
1163
|
description: "E2E testing for native macOS apps and web apps from declarative YAML hunts.",
|
|
1138
1164
|
type: "module",
|
|
1139
1165
|
license: "Apache-2.0",
|
|
@@ -1192,7 +1218,8 @@ var package_default = {
|
|
|
1192
1218
|
build: "tsup",
|
|
1193
1219
|
lint: "eslint .",
|
|
1194
1220
|
test: "vitest run",
|
|
1195
|
-
"test:watch": "vitest"
|
|
1221
|
+
"test:watch": "vitest",
|
|
1222
|
+
"audit:licenses": "node scripts/audit-licenses.mjs"
|
|
1196
1223
|
},
|
|
1197
1224
|
dependencies: {
|
|
1198
1225
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -1216,6 +1243,7 @@ var package_default = {
|
|
|
1216
1243
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
1217
1244
|
"@typescript-eslint/parser": "^7.18.0",
|
|
1218
1245
|
eslint: "^8.57.1",
|
|
1246
|
+
"license-checker-rseidelsohn": "^4.4.2",
|
|
1219
1247
|
tsup: "^8.3.5",
|
|
1220
1248
|
typescript: "^5.7.3",
|
|
1221
1249
|
vitest: "^2.1.8"
|
|
@@ -1240,6 +1268,7 @@ var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
|
|
|
1240
1268
|
"navigate",
|
|
1241
1269
|
"waitForUrl",
|
|
1242
1270
|
"waitForNetworkIdle",
|
|
1271
|
+
"waitForResponse",
|
|
1243
1272
|
"mockRoute",
|
|
1244
1273
|
"unmockRoute",
|
|
1245
1274
|
"evalScript",
|
|
@@ -1625,6 +1654,9 @@ function createPlaywrightDriver(page) {
|
|
|
1625
1654
|
async waitForNetworkIdle(options) {
|
|
1626
1655
|
await page.waitForLoadState("networkidle", { timeout: options?.timeout });
|
|
1627
1656
|
},
|
|
1657
|
+
async waitForResponse(predicate, options) {
|
|
1658
|
+
await page.waitForResponse((response) => predicate(response), { timeout: options?.timeout });
|
|
1659
|
+
},
|
|
1628
1660
|
evaluate(pageFunction, arg) {
|
|
1629
1661
|
const raw = page.evaluate;
|
|
1630
1662
|
const result = arg === void 0 ? raw.call(page, pageFunction) : raw.call(page, pageFunction, arg);
|
|
@@ -1827,6 +1859,9 @@ function createMacDriver(client, options = {}) {
|
|
|
1827
1859
|
waitForNetworkIdle() {
|
|
1828
1860
|
return rejectUnsupported("waitForNetworkIdle");
|
|
1829
1861
|
},
|
|
1862
|
+
waitForResponse() {
|
|
1863
|
+
return rejectUnsupported("waitForResponse");
|
|
1864
|
+
},
|
|
1830
1865
|
// scripting & artifacts ------------------------------------------------
|
|
1831
1866
|
evaluate() {
|
|
1832
1867
|
return rejectUnsupported("evalScript");
|
|
@@ -1950,10 +1985,15 @@ ${macdriverBuildInstructions()}`
|
|
|
1950
1985
|
${macdriverBuildInstructions()}`);
|
|
1951
1986
|
}
|
|
1952
1987
|
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
1988
|
+
function errorMessage(error) {
|
|
1989
|
+
return error instanceof Error ? error.message : String(error);
|
|
1990
|
+
}
|
|
1953
1991
|
var SpawnMacHelperClient = class {
|
|
1954
1992
|
child;
|
|
1955
1993
|
pending = /* @__PURE__ */ new Map();
|
|
1956
1994
|
requestTimeoutMs;
|
|
1995
|
+
onEvent;
|
|
1996
|
+
onEventError;
|
|
1957
1997
|
stdoutBuffer = "";
|
|
1958
1998
|
stderrBuffer = "";
|
|
1959
1999
|
nextId = 1;
|
|
@@ -1961,6 +2001,9 @@ var SpawnMacHelperClient = class {
|
|
|
1961
2001
|
terminalError;
|
|
1962
2002
|
constructor(binaryPath, options = {}) {
|
|
1963
2003
|
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2004
|
+
this.onEvent = options.onEvent;
|
|
2005
|
+
this.onEventError = options.onEventError ?? ((message) => process.stderr.write(`${message}
|
|
2006
|
+
`));
|
|
1964
2007
|
this.child = (0, import_node_child_process2.spawn)(binaryPath, ["serve"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
1965
2008
|
this.child.stdout?.setEncoding("utf-8");
|
|
1966
2009
|
this.child.stderr?.setEncoding("utf-8");
|
|
@@ -1997,6 +2040,10 @@ var SpawnMacHelperClient = class {
|
|
|
1997
2040
|
} catch {
|
|
1998
2041
|
return;
|
|
1999
2042
|
}
|
|
2043
|
+
if (typeof message.event === "string" && typeof message.id !== "number") {
|
|
2044
|
+
this.handleEvent(message);
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2000
2047
|
const id = typeof message.id === "number" ? message.id : void 0;
|
|
2001
2048
|
if (id === void 0) {
|
|
2002
2049
|
return;
|
|
@@ -2013,6 +2060,18 @@ var SpawnMacHelperClient = class {
|
|
|
2013
2060
|
pending.reject(new Error(typeof message.error === "string" ? message.error : "prowl-macdriver error"));
|
|
2014
2061
|
}
|
|
2015
2062
|
}
|
|
2063
|
+
handleEvent(event) {
|
|
2064
|
+
try {
|
|
2065
|
+
this.onEvent?.(event);
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
try {
|
|
2068
|
+
this.onEventError(
|
|
2069
|
+
`prowl-macdriver event sink failed for event "${event.event}": ${errorMessage(error)}`
|
|
2070
|
+
);
|
|
2071
|
+
} catch {
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2016
2075
|
failAll(error) {
|
|
2017
2076
|
for (const pending of this.pending.values()) {
|
|
2018
2077
|
clearTimeout(pending.timer);
|
|
@@ -2651,6 +2710,9 @@ function createAndroidDriver(client, options = {}) {
|
|
|
2651
2710
|
waitForNetworkIdle() {
|
|
2652
2711
|
return rejectUnsupported("waitForNetworkIdle");
|
|
2653
2712
|
},
|
|
2713
|
+
waitForResponse() {
|
|
2714
|
+
return rejectUnsupported("waitForResponse");
|
|
2715
|
+
},
|
|
2654
2716
|
// scripting & artifacts ------------------------------------------------
|
|
2655
2717
|
evaluate() {
|
|
2656
2718
|
return rejectUnsupported("evalScript");
|
|
@@ -2924,12 +2986,12 @@ var Uia2Transport = class {
|
|
|
2924
2986
|
* {@link Uia2HttpError} on a non-2xx response, or a timeout error when the
|
|
2925
2987
|
* per-request deadline elapses.
|
|
2926
2988
|
*/
|
|
2927
|
-
async request(method,
|
|
2989
|
+
async request(method, path28, body, timeoutMs) {
|
|
2928
2990
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
2929
2991
|
const controller = new AbortController();
|
|
2930
2992
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
2931
2993
|
timer.unref?.();
|
|
2932
|
-
const url = `${this.baseUrl}${
|
|
2994
|
+
const url = `${this.baseUrl}${path28}`;
|
|
2933
2995
|
let response;
|
|
2934
2996
|
try {
|
|
2935
2997
|
response = await this.fetchImpl(url, {
|
|
@@ -2941,7 +3003,7 @@ var Uia2Transport = class {
|
|
|
2941
3003
|
} catch (error) {
|
|
2942
3004
|
if (controller.signal.aborted) {
|
|
2943
3005
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
2944
|
-
throw new Error(`uiautomator2 request ${method} ${
|
|
3006
|
+
throw new Error(`uiautomator2 request ${method} ${path28} timed out after ${shown}`);
|
|
2945
3007
|
}
|
|
2946
3008
|
throw error instanceof Error ? error : new Error(String(error));
|
|
2947
3009
|
} finally {
|
|
@@ -2952,7 +3014,7 @@ var Uia2Transport = class {
|
|
|
2952
3014
|
if (!response.ok) {
|
|
2953
3015
|
const wdError = extractWebdriverError(parsed);
|
|
2954
3016
|
throw new Uia2HttpError(
|
|
2955
|
-
`uiautomator2 ${method} ${
|
|
3017
|
+
`uiautomator2 ${method} ${path28} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
2956
3018
|
response.status,
|
|
2957
3019
|
wdError
|
|
2958
3020
|
);
|
|
@@ -3035,10 +3097,10 @@ function sleep(ms) {
|
|
|
3035
3097
|
}
|
|
3036
3098
|
function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
3037
3099
|
const base = `/session/${sessionId}`;
|
|
3038
|
-
async function locate(query,
|
|
3100
|
+
async function locate(query, path28) {
|
|
3039
3101
|
return transport.request(
|
|
3040
3102
|
"POST",
|
|
3041
|
-
`${base}${
|
|
3103
|
+
`${base}${path28}`,
|
|
3042
3104
|
androidQueryToLocator(query, { appPackage: options.appPackage })
|
|
3043
3105
|
);
|
|
3044
3106
|
}
|
|
@@ -3517,6 +3579,9 @@ function createIosDriver(client, options) {
|
|
|
3517
3579
|
waitForNetworkIdle() {
|
|
3518
3580
|
return rejectUnsupported("waitForNetworkIdle");
|
|
3519
3581
|
},
|
|
3582
|
+
waitForResponse() {
|
|
3583
|
+
return rejectUnsupported("waitForResponse");
|
|
3584
|
+
},
|
|
3520
3585
|
// scripting & artifacts ------------------------------------------------
|
|
3521
3586
|
evaluate() {
|
|
3522
3587
|
return rejectUnsupported("evalScript");
|
|
@@ -3839,15 +3904,15 @@ var WdaTransport = class {
|
|
|
3839
3904
|
* {@link WdaHttpError} on a non-2xx response, or a timeout error when the
|
|
3840
3905
|
* per-request deadline elapses.
|
|
3841
3906
|
*/
|
|
3842
|
-
async requestFull(method,
|
|
3907
|
+
async requestFull(method, path28, body, timeoutMs) {
|
|
3843
3908
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
3844
3909
|
const controller = new AbortController();
|
|
3845
3910
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
3846
3911
|
timer.unref?.();
|
|
3847
|
-
const url = `${this.baseUrl}${
|
|
3912
|
+
const url = `${this.baseUrl}${path28}`;
|
|
3848
3913
|
const timeoutError = () => {
|
|
3849
3914
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
3850
|
-
return new Error(`WebDriverAgent request ${method} ${
|
|
3915
|
+
return new Error(`WebDriverAgent request ${method} ${path28} timed out after ${shown}`);
|
|
3851
3916
|
};
|
|
3852
3917
|
let response;
|
|
3853
3918
|
try {
|
|
@@ -3879,7 +3944,7 @@ var WdaTransport = class {
|
|
|
3879
3944
|
if (!response.ok) {
|
|
3880
3945
|
const wdError = extractWebdriverError2(parsed);
|
|
3881
3946
|
throw new WdaHttpError(
|
|
3882
|
-
`WebDriverAgent ${method} ${
|
|
3947
|
+
`WebDriverAgent ${method} ${path28} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
3883
3948
|
response.status,
|
|
3884
3949
|
wdError
|
|
3885
3950
|
);
|
|
@@ -3887,8 +3952,8 @@ var WdaTransport = class {
|
|
|
3887
3952
|
return parsed;
|
|
3888
3953
|
}
|
|
3889
3954
|
/** Like {@link requestFull} but returns just the `value` field. */
|
|
3890
|
-
async request(method,
|
|
3891
|
-
const parsed = await this.requestFull(method,
|
|
3955
|
+
async request(method, path28, body, timeoutMs) {
|
|
3956
|
+
const parsed = await this.requestFull(method, path28, body, timeoutMs);
|
|
3892
3957
|
return parsed?.value;
|
|
3893
3958
|
}
|
|
3894
3959
|
};
|
|
@@ -3964,8 +4029,8 @@ function sleep2(ms) {
|
|
|
3964
4029
|
}
|
|
3965
4030
|
function createWdaAgentClient(transport, sessionId) {
|
|
3966
4031
|
const base = `/session/${sessionId}`;
|
|
3967
|
-
async function locate(query,
|
|
3968
|
-
return transport.request("POST", `${base}${
|
|
4032
|
+
async function locate(query, path28) {
|
|
4033
|
+
return transport.request("POST", `${base}${path28}`, iosQueryToLocator(query));
|
|
3969
4034
|
}
|
|
3970
4035
|
return {
|
|
3971
4036
|
async findElement(query) {
|
|
@@ -4781,6 +4846,7 @@ function getStepType(step) {
|
|
|
4781
4846
|
if ("waitForSelector" in step) return "waitForSelector";
|
|
4782
4847
|
if ("waitForUrl" in step) return "waitForUrl";
|
|
4783
4848
|
if ("waitForNetworkIdle" in step) return "waitForNetworkIdle";
|
|
4849
|
+
if ("waitForResponse" in step) return "waitForResponse";
|
|
4784
4850
|
if ("hover" in step) return "hover";
|
|
4785
4851
|
if ("scroll" in step) return "scroll";
|
|
4786
4852
|
if ("scrollTo" in step) return "scrollTo";
|
|
@@ -4797,6 +4863,11 @@ function getStepType(step) {
|
|
|
4797
4863
|
if ("waitForDownload" in step) return "waitForDownload";
|
|
4798
4864
|
return "step";
|
|
4799
4865
|
}
|
|
4866
|
+
function compileResponseUrlMatcher(pattern) {
|
|
4867
|
+
const regexBody = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
|
|
4868
|
+
const regex = new RegExp(regexBody);
|
|
4869
|
+
return (url) => regex.test(url);
|
|
4870
|
+
}
|
|
4800
4871
|
var RUNTIME_VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
4801
4872
|
function substituteRuntimeVars(input, vars) {
|
|
4802
4873
|
return input.replace(RUNTIME_VAR_PATTERN, (match, name) => {
|
|
@@ -4835,6 +4906,15 @@ function applyRuntimeVars(step, vars) {
|
|
|
4835
4906
|
if ("waitForSelector" in step) {
|
|
4836
4907
|
return { waitForSelector: { selector: sub(step.waitForSelector.selector), timeout: step.waitForSelector.timeout } };
|
|
4837
4908
|
}
|
|
4909
|
+
if ("waitForResponse" in step) {
|
|
4910
|
+
return {
|
|
4911
|
+
waitForResponse: {
|
|
4912
|
+
url: sub(step.waitForResponse.url),
|
|
4913
|
+
...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
|
|
4914
|
+
...step.waitForResponse.timeout !== void 0 ? { timeout: step.waitForResponse.timeout } : {}
|
|
4915
|
+
}
|
|
4916
|
+
};
|
|
4917
|
+
}
|
|
4838
4918
|
if ("evalScript" in step) {
|
|
4839
4919
|
if (typeof step.evalScript === "string") return { evalScript: sub(step.evalScript) };
|
|
4840
4920
|
return {
|
|
@@ -5444,6 +5524,32 @@ var STEP_HANDLERS = {
|
|
|
5444
5524
|
};
|
|
5445
5525
|
}
|
|
5446
5526
|
},
|
|
5527
|
+
waitForResponse: {
|
|
5528
|
+
capabilities: ["response", "wait"],
|
|
5529
|
+
run: async (h) => {
|
|
5530
|
+
if (!("waitForResponse" in h.step)) unknownStep();
|
|
5531
|
+
const { url: pattern, status, timeout } = h.step.waitForResponse;
|
|
5532
|
+
const matchesResponseUrl = compileResponseUrlMatcher(pattern);
|
|
5533
|
+
try {
|
|
5534
|
+
await h.driver.waitForResponse(
|
|
5535
|
+
(response) => matchesResponseUrl(response.url()) && (status === void 0 || response.status() === status),
|
|
5536
|
+
{ timeout }
|
|
5537
|
+
);
|
|
5538
|
+
} catch (err) {
|
|
5539
|
+
const isTimeout = err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(err.message));
|
|
5540
|
+
if (!isTimeout) throw err;
|
|
5541
|
+
const statusPart = status !== void 0 ? ` with status ${status}` : "";
|
|
5542
|
+
const timeoutPart = timeout !== void 0 ? ` within ${timeout}ms` : "";
|
|
5543
|
+
throw new Error(
|
|
5544
|
+
`waitForResponse: no response matching "${pattern}"${statusPart} was received${timeoutPart}.`
|
|
5545
|
+
);
|
|
5546
|
+
}
|
|
5547
|
+
return {
|
|
5548
|
+
kind: "result",
|
|
5549
|
+
result: { type: "waitForResponse", status: "pass", durationMs: Date.now() - h.stepStart, value: pattern }
|
|
5550
|
+
};
|
|
5551
|
+
}
|
|
5552
|
+
},
|
|
5447
5553
|
waitForNetworkIdle: {
|
|
5448
5554
|
capabilities: ["wait"],
|
|
5449
5555
|
run: async (h) => {
|
|
@@ -6234,6 +6340,15 @@ function formatStep(step) {
|
|
|
6234
6340
|
const error = step.error ? ` error=${escapeMd(step.error)}` : "";
|
|
6235
6341
|
return `${base}${selector}${healed}${value}${error}`;
|
|
6236
6342
|
}
|
|
6343
|
+
function formatRetryAttempt(attempt) {
|
|
6344
|
+
const base = `- attempt ${attempt.attempt}: ${attempt.status.toUpperCase()} (${attempt.durationMs}ms)`;
|
|
6345
|
+
if (attempt.status === "pass") {
|
|
6346
|
+
return base;
|
|
6347
|
+
}
|
|
6348
|
+
const step = attempt.failedStep ? ` failed-step=[${attempt.failedStep.index}] ${escapeMd(attempt.failedStep.type)}` : "";
|
|
6349
|
+
const error = attempt.error ? ` error=${escapeMd(attempt.error)}` : "";
|
|
6350
|
+
return `${base}${step}${error}`;
|
|
6351
|
+
}
|
|
6237
6352
|
function formatAssertion(assertion) {
|
|
6238
6353
|
const value = assertion.value !== void 0 ? ` value=${typeof assertion.value === "string" ? escapeMd(assertion.value) : assertion.value}` : "";
|
|
6239
6354
|
const error = assertion.error ? ` error=${escapeMd(assertion.error)}` : "";
|
|
@@ -6248,6 +6363,16 @@ function writeSummary(runDir, result) {
|
|
|
6248
6363
|
lines.push(`Target: ${result.targetUrl}`);
|
|
6249
6364
|
lines.push(`Started: ${result.startedAt}`);
|
|
6250
6365
|
lines.push(`Duration: ${result.durationMs}ms`);
|
|
6366
|
+
if (result.retryHistory && result.retryHistory.length > 1) {
|
|
6367
|
+
lines.push("");
|
|
6368
|
+
lines.push("## Retries");
|
|
6369
|
+
if (result.retrySummary) {
|
|
6370
|
+
lines.push(escapeMd(result.retrySummary));
|
|
6371
|
+
}
|
|
6372
|
+
for (const attempt of result.retryHistory) {
|
|
6373
|
+
lines.push(formatRetryAttempt(attempt));
|
|
6374
|
+
}
|
|
6375
|
+
}
|
|
6251
6376
|
lines.push("");
|
|
6252
6377
|
lines.push("## Steps");
|
|
6253
6378
|
for (const step of result.steps) {
|
|
@@ -6408,7 +6533,8 @@ function isHistoryEntry(value) {
|
|
|
6408
6533
|
return false;
|
|
6409
6534
|
}
|
|
6410
6535
|
const entry = value;
|
|
6411
|
-
return typeof entry.hunt === "string" && (entry.status === "pass" || entry.status === "fail") && typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs) && typeof entry.startedAt === "string" && (entry.runDir === void 0 || typeof entry.runDir === "string")
|
|
6536
|
+
return typeof entry.hunt === "string" && (entry.status === "pass" || entry.status === "fail") && typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs) && typeof entry.startedAt === "string" && (entry.runDir === void 0 || typeof entry.runDir === "string") && // Optional (PROWL-033): older history.json files predate `retries`.
|
|
6537
|
+
(entry.retries === void 0 || typeof entry.retries === "number" && Number.isInteger(entry.retries) && entry.retries >= 0);
|
|
6412
6538
|
}
|
|
6413
6539
|
function readHistory(configDir) {
|
|
6414
6540
|
const filePath = historyPath(configDir);
|
|
@@ -6660,6 +6786,60 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
|
|
|
6660
6786
|
function delay3(ms) {
|
|
6661
6787
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6662
6788
|
}
|
|
6789
|
+
function toRetryAttempt(attempt, result) {
|
|
6790
|
+
const record = {
|
|
6791
|
+
attempt,
|
|
6792
|
+
status: result.status,
|
|
6793
|
+
durationMs: result.durationMs
|
|
6794
|
+
};
|
|
6795
|
+
if (result.status === "fail") {
|
|
6796
|
+
const failedIndex = result.steps.findIndex((step) => step.status === "fail");
|
|
6797
|
+
if (failedIndex >= 0) {
|
|
6798
|
+
const failed = result.steps[failedIndex];
|
|
6799
|
+
record.failedStep = { index: failedIndex, type: failed.type };
|
|
6800
|
+
if (failed.error) {
|
|
6801
|
+
record.error = failed.error;
|
|
6802
|
+
}
|
|
6803
|
+
}
|
|
6804
|
+
if (!record.error) {
|
|
6805
|
+
const failedAssertion = result.assertions.find((assertion) => assertion.status === "fail");
|
|
6806
|
+
if (failedAssertion?.error) {
|
|
6807
|
+
record.error = failedAssertion.error;
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
return record;
|
|
6812
|
+
}
|
|
6813
|
+
function describeAttemptFailure(attempt) {
|
|
6814
|
+
if (attempt.status !== "fail") {
|
|
6815
|
+
return void 0;
|
|
6816
|
+
}
|
|
6817
|
+
const step = attempt.failedStep?.type;
|
|
6818
|
+
if (step && attempt.error) {
|
|
6819
|
+
return `${step} (${attempt.error})`;
|
|
6820
|
+
}
|
|
6821
|
+
return step ?? attempt.error;
|
|
6822
|
+
}
|
|
6823
|
+
function buildRetrySummary(attempts, maxRetries) {
|
|
6824
|
+
const final = attempts[attempts.length - 1];
|
|
6825
|
+
const firstFailure = describeAttemptFailure(attempts[0]);
|
|
6826
|
+
const suffix = firstFailure ? ` \u2014 first failure: ${firstFailure}` : "";
|
|
6827
|
+
if (final.status === "pass") {
|
|
6828
|
+
return `Passed on attempt ${final.attempt} of ${maxRetries + 1}${suffix}`;
|
|
6829
|
+
}
|
|
6830
|
+
return `Failed after ${attempts.length} attempts${suffix}`;
|
|
6831
|
+
}
|
|
6832
|
+
function finalizeRetryOutcome(outcome, attempts, maxRetries, junit) {
|
|
6833
|
+
outcome.result = writeReports(
|
|
6834
|
+
outcome.runDir,
|
|
6835
|
+
{
|
|
6836
|
+
...outcome.result,
|
|
6837
|
+
retryHistory: attempts,
|
|
6838
|
+
retrySummary: buildRetrySummary(attempts, maxRetries)
|
|
6839
|
+
},
|
|
6840
|
+
{ junit }
|
|
6841
|
+
);
|
|
6842
|
+
}
|
|
6663
6843
|
async function runHunt(options) {
|
|
6664
6844
|
const { config, configDir } = loadConfig(options.configPath);
|
|
6665
6845
|
if (config.target.type === "macos") {
|
|
@@ -6689,7 +6869,9 @@ async function runHunt(options) {
|
|
|
6689
6869
|
}
|
|
6690
6870
|
const maxRetries = hunt.retry?.maxRetries ?? 0;
|
|
6691
6871
|
const retryDelay = hunt.retry?.delay ?? 0;
|
|
6872
|
+
const junit = options.junit ?? config.artifacts.junit;
|
|
6692
6873
|
let lastResult;
|
|
6874
|
+
const attempts = [];
|
|
6693
6875
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
6694
6876
|
if (attempt > 0 && retryDelay > 0) {
|
|
6695
6877
|
await delay3(retryDelay);
|
|
@@ -6705,19 +6887,20 @@ async function runHunt(options) {
|
|
|
6705
6887
|
targetUrl,
|
|
6706
6888
|
allowedDomains
|
|
6707
6889
|
);
|
|
6890
|
+
attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
|
|
6708
6891
|
if (lastResult.result.status === "pass") {
|
|
6709
|
-
if (
|
|
6710
|
-
lastResult
|
|
6892
|
+
if (attempts.length > 1) {
|
|
6893
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
6711
6894
|
}
|
|
6712
|
-
recordHistory(configDir, lastResult, config.history.maxRuns);
|
|
6895
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
6713
6896
|
return lastResult;
|
|
6714
6897
|
}
|
|
6715
6898
|
}
|
|
6716
|
-
if (maxRetries > 0 && lastResult) {
|
|
6717
|
-
lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
|
|
6718
|
-
}
|
|
6719
6899
|
if (lastResult) {
|
|
6720
|
-
|
|
6900
|
+
if (attempts.length > 1) {
|
|
6901
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
6902
|
+
}
|
|
6903
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
6721
6904
|
}
|
|
6722
6905
|
return lastResult;
|
|
6723
6906
|
}
|
|
@@ -6921,7 +7104,9 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
6921
7104
|
}
|
|
6922
7105
|
const maxRetries = hunt.retry?.maxRetries ?? 0;
|
|
6923
7106
|
const retryDelay = hunt.retry?.delay ?? 0;
|
|
7107
|
+
const junit = options.junit ?? config.artifacts.junit;
|
|
6924
7108
|
let lastResult;
|
|
7109
|
+
const attempts = [];
|
|
6925
7110
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
6926
7111
|
if (attempt > 0 && retryDelay > 0) {
|
|
6927
7112
|
await delay3(retryDelay);
|
|
@@ -6936,23 +7121,24 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
6936
7121
|
randomVars,
|
|
6937
7122
|
config.guardrails.allowedApps
|
|
6938
7123
|
);
|
|
7124
|
+
attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
|
|
6939
7125
|
if (lastResult.result.status === "pass") {
|
|
6940
|
-
if (
|
|
6941
|
-
lastResult
|
|
7126
|
+
if (attempts.length > 1) {
|
|
7127
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
6942
7128
|
}
|
|
6943
|
-
recordHistory(configDir, lastResult, config.history.maxRuns);
|
|
7129
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
6944
7130
|
return lastResult;
|
|
6945
7131
|
}
|
|
6946
7132
|
}
|
|
6947
|
-
if (maxRetries > 0 && lastResult) {
|
|
6948
|
-
lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
|
|
6949
|
-
}
|
|
6950
7133
|
if (lastResult) {
|
|
6951
|
-
|
|
7134
|
+
if (attempts.length > 1) {
|
|
7135
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
7136
|
+
}
|
|
7137
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
6952
7138
|
}
|
|
6953
7139
|
return lastResult;
|
|
6954
7140
|
}
|
|
6955
|
-
function recordHistory(configDir, outcome, maxRuns) {
|
|
7141
|
+
function recordHistory(configDir, outcome, maxRuns, retries = 0) {
|
|
6956
7142
|
try {
|
|
6957
7143
|
const relativeRunDir = import_node_path15.default.relative(configDir, outcome.runDir);
|
|
6958
7144
|
appendEntry(
|
|
@@ -6962,7 +7148,9 @@ function recordHistory(configDir, outcome, maxRuns) {
|
|
|
6962
7148
|
status: outcome.result.status,
|
|
6963
7149
|
durationMs: outcome.result.durationMs,
|
|
6964
7150
|
startedAt: outcome.result.startedAt,
|
|
6965
|
-
runDir: relativeRunDir || void 0
|
|
7151
|
+
runDir: relativeRunDir || void 0,
|
|
7152
|
+
// Omit when no retry ran so entries stay additive/backward-compatible.
|
|
7153
|
+
retries: retries > 0 ? retries : void 0
|
|
6966
7154
|
},
|
|
6967
7155
|
maxRuns
|
|
6968
7156
|
);
|
|
@@ -7017,6 +7205,10 @@ function describeStep(step) {
|
|
|
7017
7205
|
if ("waitForSelector" in step) return `waitForSelector "${step.waitForSelector.selector}"`;
|
|
7018
7206
|
if ("waitForUrl" in step) return `waitForUrl "${step.waitForUrl.value}"`;
|
|
7019
7207
|
if ("waitForNetworkIdle" in step) return "waitForNetworkIdle";
|
|
7208
|
+
if ("waitForResponse" in step) {
|
|
7209
|
+
const statusSuffix = step.waitForResponse.status !== void 0 ? ` (status ${step.waitForResponse.status})` : "";
|
|
7210
|
+
return `waitForResponse "${step.waitForResponse.url}"${statusSuffix}`;
|
|
7211
|
+
}
|
|
7020
7212
|
if ("hover" in step) return `hover "${step.hover.selector}"`;
|
|
7021
7213
|
if ("scroll" in step) return `scroll ${step.scroll.direction} ${step.scroll.amount ?? 500}px`;
|
|
7022
7214
|
if ("scrollTo" in step) return `scrollTo "${step.scrollTo.selector}"`;
|
|
@@ -7080,6 +7272,10 @@ function printHuntSummary(result, runDir) {
|
|
|
7080
7272
|
const duration = import_chalk.default.gray(`(${result.durationMs}ms)`);
|
|
7081
7273
|
console.log(`
|
|
7082
7274
|
${status} ${import_chalk.default.bold(result.hunt)} ${duration} ${stepCount}`);
|
|
7275
|
+
if (result.retrySummary) {
|
|
7276
|
+
const color = result.status === "pass" ? import_chalk.default.yellow : import_chalk.default.red;
|
|
7277
|
+
console.log(` ${color(result.retrySummary)}`);
|
|
7278
|
+
}
|
|
7083
7279
|
console.log(` ${import_chalk.default.gray("Artifacts:")} ${runDir}
|
|
7084
7280
|
`);
|
|
7085
7281
|
}
|
|
@@ -7189,6 +7385,7 @@ function buildRunCommand() {
|
|
|
7189
7385
|
|
|
7190
7386
|
// src/cli/commands/init.ts
|
|
7191
7387
|
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
7388
|
+
var import_node_os5 = __toESM(require("os"), 1);
|
|
7192
7389
|
var import_node_path16 = __toESM(require("path"), 1);
|
|
7193
7390
|
var import_node_url2 = require("url");
|
|
7194
7391
|
var import_commander2 = require("commander");
|
|
@@ -7214,6 +7411,192 @@ function copyFile(source, destination) {
|
|
|
7214
7411
|
import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(destination), { recursive: true });
|
|
7215
7412
|
import_node_fs15.default.copyFileSync(source, destination);
|
|
7216
7413
|
}
|
|
7414
|
+
function isInsideDir(parent, child) {
|
|
7415
|
+
const relative = import_node_path16.default.relative(parent, child);
|
|
7416
|
+
return relative.length > 0 && !relative.startsWith("..") && !import_node_path16.default.isAbsolute(relative);
|
|
7417
|
+
}
|
|
7418
|
+
function lstatIfExists(target) {
|
|
7419
|
+
try {
|
|
7420
|
+
return import_node_fs15.default.lstatSync(target);
|
|
7421
|
+
} catch (error) {
|
|
7422
|
+
const code = error.code;
|
|
7423
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
7424
|
+
return null;
|
|
7425
|
+
}
|
|
7426
|
+
throw error;
|
|
7427
|
+
}
|
|
7428
|
+
}
|
|
7429
|
+
function errorMessage2(error) {
|
|
7430
|
+
return error instanceof Error ? error.message : String(error);
|
|
7431
|
+
}
|
|
7432
|
+
function assertNoSymlinkDestination(prowlDir, destination) {
|
|
7433
|
+
const relative = import_node_path16.default.relative(prowlDir, destination);
|
|
7434
|
+
if (relative.startsWith("..") || import_node_path16.default.isAbsolute(relative)) {
|
|
7435
|
+
throw new Error(`Refusing to write outside ${CONFIG_DIR}: ${destination}`);
|
|
7436
|
+
}
|
|
7437
|
+
const parts = relative.split(import_node_path16.default.sep).filter((part) => part.length > 0);
|
|
7438
|
+
let current = prowlDir;
|
|
7439
|
+
const rootStat = lstatIfExists(current);
|
|
7440
|
+
if (rootStat?.isSymbolicLink()) {
|
|
7441
|
+
throw new Error(
|
|
7442
|
+
`${CONFIG_DIR} scaffold destination path contains a symlink: ${current}. Replace it with a real file or directory before running prowl init --force.`
|
|
7443
|
+
);
|
|
7444
|
+
}
|
|
7445
|
+
if (rootStat && !rootStat.isDirectory()) {
|
|
7446
|
+
throw new Error(`${CONFIG_DIR} scaffold path is not a directory: ${current}`);
|
|
7447
|
+
}
|
|
7448
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
7449
|
+
current = import_node_path16.default.join(current, parts[index] ?? "");
|
|
7450
|
+
const stat = lstatIfExists(current);
|
|
7451
|
+
if (stat?.isSymbolicLink()) {
|
|
7452
|
+
throw new Error(
|
|
7453
|
+
`${CONFIG_DIR} scaffold destination path contains a symlink: ${current}. Replace it with a real file or directory before running prowl init --force.`
|
|
7454
|
+
);
|
|
7455
|
+
}
|
|
7456
|
+
if (stat && index < parts.length - 1 && !stat.isDirectory()) {
|
|
7457
|
+
throw new Error(`${CONFIG_DIR} scaffold parent path is not a directory: ${current}`);
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7460
|
+
}
|
|
7461
|
+
function gitignoreTemplate() {
|
|
7462
|
+
return [
|
|
7463
|
+
"# Run artifacts (screenshots, logs, reports)",
|
|
7464
|
+
"runs/",
|
|
7465
|
+
"",
|
|
7466
|
+
"# Auth state (tokens, cookies)",
|
|
7467
|
+
"auth-state.json",
|
|
7468
|
+
"",
|
|
7469
|
+
"# Environment variables (credentials)",
|
|
7470
|
+
".env",
|
|
7471
|
+
""
|
|
7472
|
+
].join("\n");
|
|
7473
|
+
}
|
|
7474
|
+
function stageScaffoldTemplates(examplesDir) {
|
|
7475
|
+
const stageDir = import_node_fs15.default.mkdtempSync(import_node_path16.default.join(import_node_os5.default.tmpdir(), "prowl-init-"));
|
|
7476
|
+
const files = [];
|
|
7477
|
+
try {
|
|
7478
|
+
copyFile(import_node_path16.default.join(examplesDir, "config.yml"), import_node_path16.default.join(stageDir, "config.yml"));
|
|
7479
|
+
files.push({ source: import_node_path16.default.join(stageDir, "config.yml"), relativePath: "config.yml" });
|
|
7480
|
+
const exampleHuntsDir = import_node_path16.default.join(examplesDir, "hunts");
|
|
7481
|
+
const huntFiles = import_node_fs15.default.readdirSync(exampleHuntsDir).filter((f) => f.endsWith(".yml"));
|
|
7482
|
+
for (const huntFile of huntFiles) {
|
|
7483
|
+
const relativePath = import_node_path16.default.join("hunts", huntFile);
|
|
7484
|
+
copyFile(import_node_path16.default.join(exampleHuntsDir, huntFile), import_node_path16.default.join(stageDir, relativePath));
|
|
7485
|
+
files.push({ source: import_node_path16.default.join(stageDir, relativePath), relativePath });
|
|
7486
|
+
}
|
|
7487
|
+
const gitignorePath = import_node_path16.default.join(stageDir, ".gitignore");
|
|
7488
|
+
import_node_fs15.default.writeFileSync(gitignorePath, gitignoreTemplate());
|
|
7489
|
+
files.push({ source: gitignorePath, relativePath: ".gitignore" });
|
|
7490
|
+
return { stageDir, files };
|
|
7491
|
+
} catch (error) {
|
|
7492
|
+
import_node_fs15.default.rmSync(stageDir, { recursive: true, force: true });
|
|
7493
|
+
throw error;
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
function trackCreatedParentDirs(destination, prowlDir, createdDirs) {
|
|
7497
|
+
let dir = import_node_path16.default.dirname(destination);
|
|
7498
|
+
while (isInsideDir(prowlDir, dir) && !import_node_fs15.default.existsSync(dir)) {
|
|
7499
|
+
createdDirs.add(dir);
|
|
7500
|
+
dir = import_node_path16.default.dirname(dir);
|
|
7501
|
+
}
|
|
7502
|
+
}
|
|
7503
|
+
function prepareDestinationRollback(prowlDir, files) {
|
|
7504
|
+
const backupDir = import_node_fs15.default.mkdtempSync(import_node_path16.default.join(import_node_os5.default.tmpdir(), "prowl-init-rollback-"));
|
|
7505
|
+
const backups = [];
|
|
7506
|
+
const createdFiles = [];
|
|
7507
|
+
const createdDirs = /* @__PURE__ */ new Set();
|
|
7508
|
+
const prowlDirExisted = import_node_fs15.default.existsSync(prowlDir);
|
|
7509
|
+
try {
|
|
7510
|
+
for (const file of files) {
|
|
7511
|
+
const destination = import_node_path16.default.join(prowlDir, file.relativePath);
|
|
7512
|
+
assertNoSymlinkDestination(prowlDir, destination);
|
|
7513
|
+
const stat = lstatIfExists(destination);
|
|
7514
|
+
if (stat) {
|
|
7515
|
+
if (!stat.isFile()) {
|
|
7516
|
+
throw new Error(`${CONFIG_DIR} scaffold destination is not a regular file: ${destination}`);
|
|
7517
|
+
}
|
|
7518
|
+
const backup = import_node_path16.default.join(backupDir, file.relativePath);
|
|
7519
|
+
copyFile(destination, backup);
|
|
7520
|
+
backups.push({ destination, backup });
|
|
7521
|
+
} else {
|
|
7522
|
+
createdFiles.push(destination);
|
|
7523
|
+
}
|
|
7524
|
+
if (prowlDirExisted) {
|
|
7525
|
+
trackCreatedParentDirs(destination, prowlDir, createdDirs);
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
return {
|
|
7529
|
+
prowlDirExisted,
|
|
7530
|
+
backupDir,
|
|
7531
|
+
backups,
|
|
7532
|
+
createdFiles,
|
|
7533
|
+
createdDirs: [...createdDirs].sort((a, b) => b.length - a.length)
|
|
7534
|
+
};
|
|
7535
|
+
} catch (error) {
|
|
7536
|
+
import_node_fs15.default.rmSync(backupDir, { recursive: true, force: true });
|
|
7537
|
+
throw error;
|
|
7538
|
+
}
|
|
7539
|
+
}
|
|
7540
|
+
function rollbackDestination(prowlDir, rollback) {
|
|
7541
|
+
if (!rollback.prowlDirExisted) {
|
|
7542
|
+
import_node_fs15.default.rmSync(prowlDir, { recursive: true, force: true });
|
|
7543
|
+
return;
|
|
7544
|
+
}
|
|
7545
|
+
for (const file of rollback.createdFiles) {
|
|
7546
|
+
import_node_fs15.default.rmSync(file, { force: true });
|
|
7547
|
+
}
|
|
7548
|
+
for (const backup of rollback.backups) {
|
|
7549
|
+
try {
|
|
7550
|
+
copyFile(backup.backup, backup.destination);
|
|
7551
|
+
} catch (error) {
|
|
7552
|
+
throw new Error(
|
|
7553
|
+
`Failed to restore ${backup.destination} from backup ${backup.backup}: ${errorMessage2(error)}`
|
|
7554
|
+
);
|
|
7555
|
+
}
|
|
7556
|
+
}
|
|
7557
|
+
for (const dir of rollback.createdDirs) {
|
|
7558
|
+
try {
|
|
7559
|
+
import_node_fs15.default.rmdirSync(dir);
|
|
7560
|
+
} catch {
|
|
7561
|
+
}
|
|
7562
|
+
}
|
|
7563
|
+
}
|
|
7564
|
+
function scaffoldProwlDir(root) {
|
|
7565
|
+
const prowlDir = import_node_path16.default.join(root, CONFIG_DIR);
|
|
7566
|
+
const packageRoot = getPackageRoot2();
|
|
7567
|
+
const examplesDir = import_node_path16.default.join(packageRoot, "examples");
|
|
7568
|
+
const exampleConfig = import_node_path16.default.join(examplesDir, "config.yml");
|
|
7569
|
+
const exampleHuntsDir = import_node_path16.default.join(examplesDir, "hunts");
|
|
7570
|
+
if (!import_node_fs15.default.existsSync(exampleConfig) || !import_node_fs15.default.existsSync(exampleHuntsDir)) {
|
|
7571
|
+
throw new Error("Examples not found in package. Reinstall prowl-tools.");
|
|
7572
|
+
}
|
|
7573
|
+
const staged = stageScaffoldTemplates(examplesDir);
|
|
7574
|
+
let rollback = null;
|
|
7575
|
+
let keepBackup = false;
|
|
7576
|
+
try {
|
|
7577
|
+
rollback = prepareDestinationRollback(prowlDir, staged.files);
|
|
7578
|
+
for (const file of staged.files) {
|
|
7579
|
+
copyFile(file.source, import_node_path16.default.join(prowlDir, file.relativePath));
|
|
7580
|
+
}
|
|
7581
|
+
} catch (error) {
|
|
7582
|
+
if (rollback) {
|
|
7583
|
+
try {
|
|
7584
|
+
rollbackDestination(prowlDir, rollback);
|
|
7585
|
+
} catch (rollbackError) {
|
|
7586
|
+
keepBackup = true;
|
|
7587
|
+
throw new Error(
|
|
7588
|
+
`Scaffold failed: ${errorMessage2(error)}. Rollback failed: ${errorMessage2(rollbackError)}. Backup preserved at ${rollback.backupDir}.`
|
|
7589
|
+
);
|
|
7590
|
+
}
|
|
7591
|
+
}
|
|
7592
|
+
throw error;
|
|
7593
|
+
} finally {
|
|
7594
|
+
if (rollback && !keepBackup) {
|
|
7595
|
+
import_node_fs15.default.rmSync(rollback.backupDir, { recursive: true, force: true });
|
|
7596
|
+
}
|
|
7597
|
+
import_node_fs15.default.rmSync(staged.stageDir, { recursive: true, force: true });
|
|
7598
|
+
}
|
|
7599
|
+
}
|
|
7217
7600
|
function buildInitCommand() {
|
|
7218
7601
|
const command = new import_commander2.Command("init").option("--force", `Overwrite existing ${CONFIG_DIR} directory`).action((options) => {
|
|
7219
7602
|
const root = process.cwd();
|
|
@@ -7227,39 +7610,18 @@ function buildInitCommand() {
|
|
|
7227
7610
|
process.exitCode = 1;
|
|
7228
7611
|
return;
|
|
7229
7612
|
}
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
if (!import_node_fs15.default.existsSync(exampleConfig) || !import_node_fs15.default.existsSync(exampleHuntsDir)) {
|
|
7235
|
-
console.error(import_chalk4.default.red("Examples not found in package. Reinstall prowl-tools."));
|
|
7613
|
+
try {
|
|
7614
|
+
scaffoldProwlDir(root);
|
|
7615
|
+
} catch (error) {
|
|
7616
|
+
console.error(import_chalk4.default.red(error instanceof Error ? error.message : "init failed"));
|
|
7236
7617
|
process.exitCode = 1;
|
|
7237
7618
|
return;
|
|
7238
7619
|
}
|
|
7239
|
-
copyFile(exampleConfig, import_node_path16.default.join(prowlDir, "config.yml"));
|
|
7240
|
-
const huntFiles = import_node_fs15.default.readdirSync(exampleHuntsDir).filter((f) => f.endsWith(".yml"));
|
|
7241
|
-
for (const huntFile of huntFiles) {
|
|
7242
|
-
copyFile(
|
|
7243
|
-
import_node_path16.default.join(exampleHuntsDir, huntFile),
|
|
7244
|
-
import_node_path16.default.join(prowlDir, "hunts", huntFile)
|
|
7245
|
-
);
|
|
7246
|
-
}
|
|
7247
|
-
const gitignore = [
|
|
7248
|
-
"# Run artifacts (screenshots, logs, reports)",
|
|
7249
|
-
"runs/",
|
|
7250
|
-
"",
|
|
7251
|
-
"# Auth state (tokens, cookies)",
|
|
7252
|
-
"auth-state.json",
|
|
7253
|
-
"",
|
|
7254
|
-
"# Environment variables (credentials)",
|
|
7255
|
-
".env",
|
|
7256
|
-
""
|
|
7257
|
-
].join("\n");
|
|
7258
|
-
import_node_fs15.default.writeFileSync(import_node_path16.default.join(prowlDir, ".gitignore"), gitignore);
|
|
7259
7620
|
console.log(welcomeBanner());
|
|
7260
7621
|
console.log(import_chalk4.default.green(` Initialized ${CONFIG_DIR} directory.`));
|
|
7261
7622
|
console.log(import_chalk4.default.gray(" Run ") + import_chalk4.default.bold("prowl run hello") + import_chalk4.default.gray(" to get started."));
|
|
7262
|
-
console.log(import_chalk4.default.gray(" See ") + import_chalk4.default.cyan(`${CONFIG_DIR}/hunts/login-flow.yml`) + import_chalk4.default.gray("
|
|
7623
|
+
console.log(import_chalk4.default.gray(" See ") + import_chalk4.default.cyan(`${CONFIG_DIR}/hunts/login-flow.yml`) + import_chalk4.default.gray(" (auth) and ") + import_chalk4.default.cyan(`${CONFIG_DIR}/hunts/form.yml`) + import_chalk4.default.gray(" (web forms) for fuller examples."));
|
|
7624
|
+
console.log(import_chalk4.default.gray(" Desktop-first? ") + import_chalk4.default.cyan(`${CONFIG_DIR}/hunts/macos-hello.yml`) + import_chalk4.default.gray(" is a macOS starter (experimental \u2014 see its comments to enable).") + "\n");
|
|
7263
7625
|
});
|
|
7264
7626
|
return command;
|
|
7265
7627
|
}
|
|
@@ -9033,6 +9395,7 @@ var STEP_REFERENCE = `
|
|
|
9033
9395
|
- waitForSelector: { selector: "#el", timeout: 5000 }
|
|
9034
9396
|
- waitForUrl: { value: "/path", timeout: 5000 }
|
|
9035
9397
|
- waitForNetworkIdle: { timeout: 5000 }
|
|
9398
|
+
- waitForResponse: { url: "**/api/orders", status: 200, timeout: 10000 } \u2014 wait for a matching network response (url is a glob/substring; status is optional)
|
|
9036
9399
|
|
|
9037
9400
|
### Interaction
|
|
9038
9401
|
- click: "Button Text" \u2014 click by text (tries role=button first)
|
|
@@ -9273,6 +9636,7 @@ function buildHistoryCommand() {
|
|
|
9273
9636
|
}
|
|
9274
9637
|
console.log();
|
|
9275
9638
|
console.log(` ${import_chalk13.default.bold(huntName)} \u2014 last ${recent.length} of ${entries.length} runs`);
|
|
9639
|
+
console.log(` ${import_chalk13.default.gray(formatRetryFrequency(recent))}`);
|
|
9276
9640
|
console.log();
|
|
9277
9641
|
console.log(formatTable(recent));
|
|
9278
9642
|
console.log();
|
|
@@ -9288,12 +9652,23 @@ function buildHistoryCommand() {
|
|
|
9288
9652
|
});
|
|
9289
9653
|
return command;
|
|
9290
9654
|
}
|
|
9655
|
+
function formatRetryFrequency(entries) {
|
|
9656
|
+
const retried = entries.filter((entry) => (entry.retries ?? 0) > 0).length;
|
|
9657
|
+
if (retried === 0) {
|
|
9658
|
+
return "No retries in these runs";
|
|
9659
|
+
}
|
|
9660
|
+
return `Retried in ${retried} of ${entries.length} runs`;
|
|
9661
|
+
}
|
|
9662
|
+
function formatRetries(retries) {
|
|
9663
|
+
return retries && retries > 0 ? String(retries) : "-";
|
|
9664
|
+
}
|
|
9291
9665
|
function formatTable(entries) {
|
|
9292
|
-
const headers = ["Status", "Started", "Duration"];
|
|
9666
|
+
const headers = ["Status", "Started", "Duration", "Retries"];
|
|
9293
9667
|
const rows = entries.map((entry) => [
|
|
9294
9668
|
formatStatus(entry.status),
|
|
9295
9669
|
entry.startedAt,
|
|
9296
|
-
formatDuration(entry.durationMs)
|
|
9670
|
+
formatDuration(entry.durationMs),
|
|
9671
|
+
formatRetries(entry.retries)
|
|
9297
9672
|
]);
|
|
9298
9673
|
const widths = headers.map(
|
|
9299
9674
|
(header, index) => Math.max(
|
|
@@ -9463,7 +9838,7 @@ async function runHuntTool(args, configPath) {
|
|
|
9463
9838
|
|
|
9464
9839
|
// src/mcp/projects.ts
|
|
9465
9840
|
var import_node_fs21 = __toESM(require("fs"), 1);
|
|
9466
|
-
var
|
|
9841
|
+
var import_node_os6 = __toESM(require("os"), 1);
|
|
9467
9842
|
var import_node_path25 = __toESM(require("path"), 1);
|
|
9468
9843
|
var import_yaml3 = __toESM(require("yaml"), 1);
|
|
9469
9844
|
var import_zod2 = require("zod");
|
|
@@ -9476,10 +9851,10 @@ var projectRegistrySchema = import_zod2.z.object({
|
|
|
9476
9851
|
projects: import_zod2.z.record(import_zod2.z.string().min(1), projectEntrySchema)
|
|
9477
9852
|
}).strict();
|
|
9478
9853
|
function defaultRegistryPath() {
|
|
9479
|
-
return import_node_path25.default.join(
|
|
9854
|
+
return import_node_path25.default.join(import_node_os6.default.homedir(), ".prowl", "projects.yml");
|
|
9480
9855
|
}
|
|
9481
9856
|
function legacyRegistryPath() {
|
|
9482
|
-
return import_node_path25.default.join(
|
|
9857
|
+
return import_node_path25.default.join(import_node_os6.default.homedir(), ".prowlqa", "projects.yml");
|
|
9483
9858
|
}
|
|
9484
9859
|
function resolveProjectConfigPath(root) {
|
|
9485
9860
|
const preferred = import_node_path25.default.join(root, ".prowl", "config.yml");
|
|
@@ -9533,7 +9908,7 @@ function listRegisteredProjects(registry) {
|
|
|
9533
9908
|
function textResult(data) {
|
|
9534
9909
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
9535
9910
|
}
|
|
9536
|
-
function
|
|
9911
|
+
function errorMessage3(error) {
|
|
9537
9912
|
return error instanceof Error ? error.message : String(error);
|
|
9538
9913
|
}
|
|
9539
9914
|
function safeStringify(value) {
|
|
@@ -9547,7 +9922,7 @@ async function toolResult(toolName, args, action) {
|
|
|
9547
9922
|
try {
|
|
9548
9923
|
return textResult(await action());
|
|
9549
9924
|
} catch (error) {
|
|
9550
|
-
throw new Error(`${toolName} failed for args=${safeStringify(args)}: ${
|
|
9925
|
+
throw new Error(`${toolName} failed for args=${safeStringify(args)}: ${errorMessage3(error)}`, {
|
|
9551
9926
|
cause: error
|
|
9552
9927
|
});
|
|
9553
9928
|
}
|
|
@@ -9624,7 +9999,7 @@ async function startMcpServer(options = {}) {
|
|
|
9624
9999
|
}
|
|
9625
10000
|
|
|
9626
10001
|
// src/cli/commands/mcp.ts
|
|
9627
|
-
function
|
|
10002
|
+
function errorMessage4(error) {
|
|
9628
10003
|
return error instanceof Error ? error.message : String(error);
|
|
9629
10004
|
}
|
|
9630
10005
|
function buildMcpCommand() {
|
|
@@ -9635,7 +10010,7 @@ function buildMcpCommand() {
|
|
|
9635
10010
|
try {
|
|
9636
10011
|
await startMcpServer({ registryPath: options.projects });
|
|
9637
10012
|
} catch (error) {
|
|
9638
|
-
console.error(`Failed to start MCP server: ${
|
|
10013
|
+
console.error(`Failed to start MCP server: ${errorMessage4(error)}`);
|
|
9639
10014
|
process.exit(1);
|
|
9640
10015
|
}
|
|
9641
10016
|
});
|
|
@@ -9649,7 +10024,7 @@ var import_commander13 = require("commander");
|
|
|
9649
10024
|
var import_node_child_process7 = require("child_process");
|
|
9650
10025
|
var import_node_crypto3 = require("crypto");
|
|
9651
10026
|
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
9652
|
-
var
|
|
10027
|
+
var import_node_os7 = __toESM(require("os"), 1);
|
|
9653
10028
|
var import_node_path26 = __toESM(require("path"), 1);
|
|
9654
10029
|
var import_node_util = require("util");
|
|
9655
10030
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process7.execFile);
|
|
@@ -9815,7 +10190,7 @@ The download was rejected and discarded; re-run the install, and report it if it
|
|
|
9815
10190
|
}
|
|
9816
10191
|
async function installMacdriver(options = {}) {
|
|
9817
10192
|
const version = validateMacdriverVersion(options.version ?? MACDRIVER_VERSION);
|
|
9818
|
-
const homedir = options.homedir ??
|
|
10193
|
+
const homedir = options.homedir ?? import_node_os7.default.homedir();
|
|
9819
10194
|
const listArchiveEntries = options.listArchiveEntries ?? zipinfoArchiveLister;
|
|
9820
10195
|
const extract = options.extract ?? dittoExtractor;
|
|
9821
10196
|
const verifySignature = options.verifySignature ?? codesignVerifier;
|
|
@@ -9895,7 +10270,7 @@ var runVersionProbe = async (binaryPath) => {
|
|
|
9895
10270
|
};
|
|
9896
10271
|
async function collectMacdriverStatus(options = {}) {
|
|
9897
10272
|
const env = options.env ?? process.env;
|
|
9898
|
-
const homedir = options.homedir ??
|
|
10273
|
+
const homedir = options.homedir ?? import_node_os7.default.homedir();
|
|
9899
10274
|
const probe = options.probe ?? runVersionProbe;
|
|
9900
10275
|
let resolved = null;
|
|
9901
10276
|
try {
|
|
@@ -10008,10 +10383,402 @@ function buildMacdriverCommand() {
|
|
|
10008
10383
|
return command;
|
|
10009
10384
|
}
|
|
10010
10385
|
|
|
10386
|
+
// src/cli/commands/doctor.ts
|
|
10387
|
+
var import_chalk16 = __toESM(require("chalk"), 1);
|
|
10388
|
+
var import_commander14 = require("commander");
|
|
10389
|
+
|
|
10390
|
+
// src/doctor/checks.ts
|
|
10391
|
+
var import_node_child_process8 = require("child_process");
|
|
10392
|
+
var import_node_module3 = require("module");
|
|
10393
|
+
var import_node_fs23 = __toESM(require("fs"), 1);
|
|
10394
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
10395
|
+
var import_node_util2 = require("util");
|
|
10396
|
+
init_loader();
|
|
10397
|
+
var import_meta5 = {};
|
|
10398
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process8.execFile);
|
|
10399
|
+
var MIN_NODE_MAJOR = 20;
|
|
10400
|
+
var CHECK_NODE = "Node.js version";
|
|
10401
|
+
var CHECK_PLAYWRIGHT = "Playwright installed";
|
|
10402
|
+
var CHECK_CHROMIUM = "Chromium browser";
|
|
10403
|
+
var CHECK_PROWL_DIR = ".prowl/ directory";
|
|
10404
|
+
var CHECK_CONFIG = "config.yml valid";
|
|
10405
|
+
var CHECK_MACOS = "macOS helper (prowl-macdriver)";
|
|
10406
|
+
var CHECK_IOS = "iOS tooling (xcrun simctl)";
|
|
10407
|
+
var CHECK_ANDROID = "Android tooling (adb)";
|
|
10408
|
+
var INSTALL_CHROMIUM_TIMEOUT_MS = 3e5;
|
|
10409
|
+
function nodeMajor(version) {
|
|
10410
|
+
const cleaned = version.startsWith("v") ? version.slice(1) : version;
|
|
10411
|
+
return Number.parseInt(cleaned.split(".")[0] ?? "", 10);
|
|
10412
|
+
}
|
|
10413
|
+
function checkNode(nodeVersion) {
|
|
10414
|
+
const major = nodeMajor(nodeVersion);
|
|
10415
|
+
if (Number.isFinite(major) && major >= MIN_NODE_MAJOR) {
|
|
10416
|
+
return {
|
|
10417
|
+
name: CHECK_NODE,
|
|
10418
|
+
status: "pass",
|
|
10419
|
+
message: `Node ${nodeVersion} (>= ${MIN_NODE_MAJOR} required)`,
|
|
10420
|
+
fixable: false
|
|
10421
|
+
};
|
|
10422
|
+
}
|
|
10423
|
+
return {
|
|
10424
|
+
name: CHECK_NODE,
|
|
10425
|
+
status: "fail",
|
|
10426
|
+
message: `Node ${nodeVersion} is too old \u2014 Prowl needs Node >= ${MIN_NODE_MAJOR}. Upgrade with nvm (\`nvm install 20\`), Homebrew (\`brew install node\`), or nodejs.org.`,
|
|
10427
|
+
fixable: false
|
|
10428
|
+
};
|
|
10429
|
+
}
|
|
10430
|
+
function checkPlaywright(probe) {
|
|
10431
|
+
if (probe.playwrightInstalled) {
|
|
10432
|
+
return { name: CHECK_PLAYWRIGHT, status: "pass", message: "Playwright is installed", fixable: false };
|
|
10433
|
+
}
|
|
10434
|
+
return {
|
|
10435
|
+
name: CHECK_PLAYWRIGHT,
|
|
10436
|
+
status: "fail",
|
|
10437
|
+
message: "Playwright is not installed. It ships as a dependency of prowl-tools \u2014 reinstall with `npm install` (local) or `npm install -g prowl-tools` (global).",
|
|
10438
|
+
fixable: false
|
|
10439
|
+
};
|
|
10440
|
+
}
|
|
10441
|
+
function checkChromium(probe) {
|
|
10442
|
+
if (!probe.playwrightInstalled) {
|
|
10443
|
+
return {
|
|
10444
|
+
name: CHECK_CHROMIUM,
|
|
10445
|
+
status: "skip",
|
|
10446
|
+
message: "Skipped \u2014 Playwright is not installed",
|
|
10447
|
+
fixable: false
|
|
10448
|
+
};
|
|
10449
|
+
}
|
|
10450
|
+
if (probe.chromiumInstalled) {
|
|
10451
|
+
return {
|
|
10452
|
+
name: CHECK_CHROMIUM,
|
|
10453
|
+
status: "pass",
|
|
10454
|
+
message: `Chromium is available${probe.chromiumPath ? ` (${probe.chromiumPath})` : ""}`,
|
|
10455
|
+
fixable: false
|
|
10456
|
+
};
|
|
10457
|
+
}
|
|
10458
|
+
return {
|
|
10459
|
+
name: CHECK_CHROMIUM,
|
|
10460
|
+
status: "fail",
|
|
10461
|
+
message: "Chromium is not installed. Run `npx playwright install chromium` (or `prowl doctor --fix`).",
|
|
10462
|
+
fixable: true
|
|
10463
|
+
};
|
|
10464
|
+
}
|
|
10465
|
+
function checkProwlDir(exists) {
|
|
10466
|
+
if (exists) {
|
|
10467
|
+
return { name: CHECK_PROWL_DIR, status: "pass", message: `${CONFIG_DIR}/ exists`, fixable: false };
|
|
10468
|
+
}
|
|
10469
|
+
return {
|
|
10470
|
+
name: CHECK_PROWL_DIR,
|
|
10471
|
+
status: "warn",
|
|
10472
|
+
message: `No ${CONFIG_DIR}/ directory here. Run \`prowl init\` (or \`prowl doctor --fix\`) to scaffold config and starter hunts.`,
|
|
10473
|
+
fixable: true
|
|
10474
|
+
};
|
|
10475
|
+
}
|
|
10476
|
+
function checkConfig(prowlDirExists, loadConfig2) {
|
|
10477
|
+
if (!prowlDirExists) {
|
|
10478
|
+
return {
|
|
10479
|
+
result: {
|
|
10480
|
+
name: CHECK_CONFIG,
|
|
10481
|
+
status: "skip",
|
|
10482
|
+
message: `Skipped \u2014 no ${CONFIG_DIR}/ directory`,
|
|
10483
|
+
fixable: false
|
|
10484
|
+
},
|
|
10485
|
+
config: null
|
|
10486
|
+
};
|
|
10487
|
+
}
|
|
10488
|
+
try {
|
|
10489
|
+
const { config } = loadConfig2();
|
|
10490
|
+
return {
|
|
10491
|
+
result: {
|
|
10492
|
+
name: CHECK_CONFIG,
|
|
10493
|
+
status: "pass",
|
|
10494
|
+
message: `Valid (target: ${config.target.type})`,
|
|
10495
|
+
fixable: false
|
|
10496
|
+
},
|
|
10497
|
+
config
|
|
10498
|
+
};
|
|
10499
|
+
} catch (error) {
|
|
10500
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10501
|
+
return {
|
|
10502
|
+
result: {
|
|
10503
|
+
name: CHECK_CONFIG,
|
|
10504
|
+
status: "fail",
|
|
10505
|
+
message: `Invalid config: ${detail}`,
|
|
10506
|
+
fixable: false
|
|
10507
|
+
},
|
|
10508
|
+
config: null
|
|
10509
|
+
};
|
|
10510
|
+
}
|
|
10511
|
+
}
|
|
10512
|
+
function macdriverSourceLabel(source) {
|
|
10513
|
+
switch (source) {
|
|
10514
|
+
case "env":
|
|
10515
|
+
return "PROWL_MACDRIVER_BIN override";
|
|
10516
|
+
case "user-install":
|
|
10517
|
+
return "user install (~/.prowl/macdriver)";
|
|
10518
|
+
case "source-build":
|
|
10519
|
+
return "repo source build";
|
|
10520
|
+
}
|
|
10521
|
+
}
|
|
10522
|
+
async function checkMacTarget(collect2, env) {
|
|
10523
|
+
let status;
|
|
10524
|
+
try {
|
|
10525
|
+
status = await collect2({ env });
|
|
10526
|
+
} catch (error) {
|
|
10527
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10528
|
+
return {
|
|
10529
|
+
name: CHECK_MACOS,
|
|
10530
|
+
status: "fail",
|
|
10531
|
+
message: `Could not inspect prowl-macdriver status (${detail}). Run \`prowl macdriver status\` for details.`,
|
|
10532
|
+
fixable: false
|
|
10533
|
+
};
|
|
10534
|
+
}
|
|
10535
|
+
if (status.resolved && status.probedVersion) {
|
|
10536
|
+
return {
|
|
10537
|
+
name: CHECK_MACOS,
|
|
10538
|
+
status: "pass",
|
|
10539
|
+
message: `Resolved via ${macdriverSourceLabel(status.resolved.source)}: ${status.resolved.path} (reports ${status.probedVersion}). Verify Accessibility/Screen Recording permissions with \`prowl macdriver status\`.`,
|
|
10540
|
+
fixable: false
|
|
10541
|
+
};
|
|
10542
|
+
}
|
|
10543
|
+
if (status.resolved) {
|
|
10544
|
+
return {
|
|
10545
|
+
name: CHECK_MACOS,
|
|
10546
|
+
status: "fail",
|
|
10547
|
+
message: `prowl-macdriver resolved via ${macdriverSourceLabel(status.resolved.source)} (${status.resolved.path}), but the helper version probe failed. Run \`prowl macdriver status\` for details.`,
|
|
10548
|
+
fixable: false
|
|
10549
|
+
};
|
|
10550
|
+
}
|
|
10551
|
+
return {
|
|
10552
|
+
name: CHECK_MACOS,
|
|
10553
|
+
status: "fail",
|
|
10554
|
+
message: "prowl-macdriver helper not found. Install it with `prowl macdriver install`, then check permissions with `prowl macdriver status`.",
|
|
10555
|
+
fixable: false
|
|
10556
|
+
};
|
|
10557
|
+
}
|
|
10558
|
+
async function checkIosTarget(platform, run) {
|
|
10559
|
+
if (platform !== "darwin") {
|
|
10560
|
+
return {
|
|
10561
|
+
name: CHECK_IOS,
|
|
10562
|
+
status: "fail",
|
|
10563
|
+
message: `The iOS target requires macOS; xcrun simctl is unavailable on ${platform}.`,
|
|
10564
|
+
fixable: false
|
|
10565
|
+
};
|
|
10566
|
+
}
|
|
10567
|
+
try {
|
|
10568
|
+
await run("xcrun", ["simctl", "help"]);
|
|
10569
|
+
return { name: CHECK_IOS, status: "pass", message: "xcrun simctl is reachable", fixable: false };
|
|
10570
|
+
} catch (error) {
|
|
10571
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10572
|
+
return {
|
|
10573
|
+
name: CHECK_IOS,
|
|
10574
|
+
status: "fail",
|
|
10575
|
+
message: `xcrun simctl is not reachable (${detail}). Install Xcode and its command-line tools (\`xcode-select --install\`), then open Xcode once to finish setup.`,
|
|
10576
|
+
fixable: false
|
|
10577
|
+
};
|
|
10578
|
+
}
|
|
10579
|
+
}
|
|
10580
|
+
async function checkAndroidTarget(run) {
|
|
10581
|
+
try {
|
|
10582
|
+
await run("adb", ["version"]);
|
|
10583
|
+
return { name: CHECK_ANDROID, status: "pass", message: "adb is reachable", fixable: false };
|
|
10584
|
+
} catch (error) {
|
|
10585
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10586
|
+
return {
|
|
10587
|
+
name: CHECK_ANDROID,
|
|
10588
|
+
status: "fail",
|
|
10589
|
+
message: `adb is not reachable (${detail}). Install Android platform-tools and add it to your PATH (e.g. \`brew install --cask android-platform-tools\`, or via Android Studio's SDK Manager).`,
|
|
10590
|
+
fixable: false
|
|
10591
|
+
};
|
|
10592
|
+
}
|
|
10593
|
+
}
|
|
10594
|
+
async function runDoctorChecks(deps) {
|
|
10595
|
+
const results = [];
|
|
10596
|
+
const probe = await deps.probePlaywright();
|
|
10597
|
+
results.push(checkNode(deps.nodeVersion));
|
|
10598
|
+
results.push(checkPlaywright(probe));
|
|
10599
|
+
results.push(checkChromium(probe));
|
|
10600
|
+
const prowlExists = deps.prowlDirExists(deps.cwd);
|
|
10601
|
+
results.push(checkProwlDir(prowlExists));
|
|
10602
|
+
const { result: configResult, config } = checkConfig(prowlExists, deps.loadConfig);
|
|
10603
|
+
results.push(configResult);
|
|
10604
|
+
if (config) {
|
|
10605
|
+
switch (config.target.type) {
|
|
10606
|
+
case "macos":
|
|
10607
|
+
results.push(await checkMacTarget(deps.collectMacdriverStatus, deps.env));
|
|
10608
|
+
break;
|
|
10609
|
+
case "ios":
|
|
10610
|
+
results.push(await checkIosTarget(deps.platform, deps.runCommand));
|
|
10611
|
+
break;
|
|
10612
|
+
case "android":
|
|
10613
|
+
results.push(await checkAndroidTarget(deps.runCommand));
|
|
10614
|
+
break;
|
|
10615
|
+
default:
|
|
10616
|
+
break;
|
|
10617
|
+
}
|
|
10618
|
+
}
|
|
10619
|
+
return results;
|
|
10620
|
+
}
|
|
10621
|
+
async function runDoctor(deps, options = {}) {
|
|
10622
|
+
let results = await runDoctorChecks(deps);
|
|
10623
|
+
const fixes = [];
|
|
10624
|
+
if (options.fix) {
|
|
10625
|
+
for (const result of results) {
|
|
10626
|
+
if (!result.fixable || result.status === "pass" || result.status === "skip") {
|
|
10627
|
+
continue;
|
|
10628
|
+
}
|
|
10629
|
+
try {
|
|
10630
|
+
if (result.name === CHECK_CHROMIUM) {
|
|
10631
|
+
await deps.installChromium();
|
|
10632
|
+
fixes.push({ name: result.name });
|
|
10633
|
+
} else if (result.name === CHECK_PROWL_DIR) {
|
|
10634
|
+
deps.scaffoldProwlDir(deps.cwd);
|
|
10635
|
+
fixes.push({ name: result.name });
|
|
10636
|
+
}
|
|
10637
|
+
} catch (error) {
|
|
10638
|
+
fixes.push({
|
|
10639
|
+
name: result.name,
|
|
10640
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10641
|
+
});
|
|
10642
|
+
}
|
|
10643
|
+
}
|
|
10644
|
+
if (fixes.length > 0) {
|
|
10645
|
+
results = await runDoctorChecks(deps);
|
|
10646
|
+
}
|
|
10647
|
+
}
|
|
10648
|
+
const ok = !results.some((result) => result.status === "fail");
|
|
10649
|
+
return { results, fixes, ok };
|
|
10650
|
+
}
|
|
10651
|
+
async function defaultProbePlaywright() {
|
|
10652
|
+
try {
|
|
10653
|
+
const pw = await import("playwright");
|
|
10654
|
+
let chromiumPath = null;
|
|
10655
|
+
try {
|
|
10656
|
+
chromiumPath = pw.chromium.executablePath();
|
|
10657
|
+
} catch {
|
|
10658
|
+
chromiumPath = null;
|
|
10659
|
+
}
|
|
10660
|
+
const chromiumInstalled = chromiumPath ? import_node_fs23.default.existsSync(chromiumPath) : false;
|
|
10661
|
+
return { playwrightInstalled: true, chromiumPath, chromiumInstalled };
|
|
10662
|
+
} catch {
|
|
10663
|
+
return { playwrightInstalled: false, chromiumPath: null, chromiumInstalled: false };
|
|
10664
|
+
}
|
|
10665
|
+
}
|
|
10666
|
+
var defaultRunCommand = async (file, args) => execFileAsync2(file, args, { timeout: 5e3 });
|
|
10667
|
+
function createLocalRequire() {
|
|
10668
|
+
if (typeof __filename === "string") {
|
|
10669
|
+
return (0, import_node_module3.createRequire)(__filename);
|
|
10670
|
+
}
|
|
10671
|
+
return (0, import_node_module3.createRequire)(import_meta5.url);
|
|
10672
|
+
}
|
|
10673
|
+
function resolvePlaywrightCliPath(requireFn = createLocalRequire()) {
|
|
10674
|
+
return import_node_path27.default.join(import_node_path27.default.dirname(requireFn.resolve("playwright/package.json")), "cli.js");
|
|
10675
|
+
}
|
|
10676
|
+
function playwrightInstallChromiumCommand() {
|
|
10677
|
+
return {
|
|
10678
|
+
file: process.execPath,
|
|
10679
|
+
args: [resolvePlaywrightCliPath(), "install", "chromium"]
|
|
10680
|
+
};
|
|
10681
|
+
}
|
|
10682
|
+
async function defaultInstallChromium() {
|
|
10683
|
+
const command = playwrightInstallChromiumCommand();
|
|
10684
|
+
await execFileAsync2(command.file, command.args, { timeout: INSTALL_CHROMIUM_TIMEOUT_MS });
|
|
10685
|
+
}
|
|
10686
|
+
function defaultDoctorDeps() {
|
|
10687
|
+
return {
|
|
10688
|
+
nodeVersion: process.versions.node,
|
|
10689
|
+
platform: process.platform,
|
|
10690
|
+
cwd: process.cwd(),
|
|
10691
|
+
env: process.env,
|
|
10692
|
+
probePlaywright: defaultProbePlaywright,
|
|
10693
|
+
runCommand: defaultRunCommand,
|
|
10694
|
+
prowlDirExists: (cwd) => import_node_fs23.default.existsSync(import_node_path27.default.join(cwd, CONFIG_DIR)),
|
|
10695
|
+
loadConfig: () => loadConfig(),
|
|
10696
|
+
collectMacdriverStatus,
|
|
10697
|
+
installChromium: defaultInstallChromium,
|
|
10698
|
+
scaffoldProwlDir
|
|
10699
|
+
};
|
|
10700
|
+
}
|
|
10701
|
+
|
|
10702
|
+
// src/cli/commands/doctor.ts
|
|
10703
|
+
function symbol(status) {
|
|
10704
|
+
switch (status) {
|
|
10705
|
+
case "pass":
|
|
10706
|
+
return import_chalk16.default.green("\u2713");
|
|
10707
|
+
// ✓
|
|
10708
|
+
case "fail":
|
|
10709
|
+
return import_chalk16.default.red("\u2717");
|
|
10710
|
+
// ✗
|
|
10711
|
+
case "warn":
|
|
10712
|
+
return import_chalk16.default.yellow("\u26A0");
|
|
10713
|
+
// ⚠
|
|
10714
|
+
case "skip":
|
|
10715
|
+
return import_chalk16.default.gray("\u25CB");
|
|
10716
|
+
}
|
|
10717
|
+
}
|
|
10718
|
+
function renderResult(result) {
|
|
10719
|
+
console.log(` ${symbol(result.status)} ${import_chalk16.default.bold(result.name)}`);
|
|
10720
|
+
console.log(` ${import_chalk16.default.gray(result.message)}`);
|
|
10721
|
+
}
|
|
10722
|
+
function renderReport(report, fixRequested) {
|
|
10723
|
+
console.log(import_chalk16.default.bold("\nprowl doctor\n"));
|
|
10724
|
+
for (const result of report.results) {
|
|
10725
|
+
renderResult(result);
|
|
10726
|
+
}
|
|
10727
|
+
if (fixRequested) {
|
|
10728
|
+
console.log("");
|
|
10729
|
+
if (report.fixes.length === 0) {
|
|
10730
|
+
console.log(import_chalk16.default.gray(" No auto-fixable issues to repair."));
|
|
10731
|
+
} else {
|
|
10732
|
+
console.log(import_chalk16.default.bold(" Attempted fixes:"));
|
|
10733
|
+
for (const fix of report.fixes) {
|
|
10734
|
+
if (fix.error) {
|
|
10735
|
+
console.log(` ${import_chalk16.default.red("\u2717")} ${fix.name} \u2014 ${import_chalk16.default.gray(fix.error)}`);
|
|
10736
|
+
} else {
|
|
10737
|
+
console.log(` ${import_chalk16.default.green("\u2713")} ${fix.name}`);
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
10740
|
+
}
|
|
10741
|
+
}
|
|
10742
|
+
const counts = { pass: 0, warn: 0, fail: 0, skip: 0 };
|
|
10743
|
+
for (const result of report.results) {
|
|
10744
|
+
counts[result.status] += 1;
|
|
10745
|
+
}
|
|
10746
|
+
const parts = [
|
|
10747
|
+
import_chalk16.default.green(`${counts.pass} passed`),
|
|
10748
|
+
counts.warn > 0 ? import_chalk16.default.yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`) : null,
|
|
10749
|
+
counts.fail > 0 ? import_chalk16.default.red(`${counts.fail} failed`) : null,
|
|
10750
|
+
counts.skip > 0 ? import_chalk16.default.gray(`${counts.skip} skipped`) : null
|
|
10751
|
+
].filter((part) => part !== null);
|
|
10752
|
+
console.log("\n " + parts.join(import_chalk16.default.gray(", ")));
|
|
10753
|
+
if (counts.fail > 0) {
|
|
10754
|
+
console.log(import_chalk16.default.red.bold(" Environment has problems \u2014 fix the failures above, then re-run `prowl doctor`.\n"));
|
|
10755
|
+
} else if (counts.warn > 0) {
|
|
10756
|
+
console.log(import_chalk16.default.yellow.bold(" Environment is usable, with warnings.\n"));
|
|
10757
|
+
} else {
|
|
10758
|
+
console.log(import_chalk16.default.green.bold(" Environment is healthy.\n"));
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
function buildDoctorCommand(deps = defaultDoctorDeps()) {
|
|
10762
|
+
return new import_commander14.Command("doctor").description("Check that your environment is set up to run Prowl hunts").option("--fix", "Attempt safe auto-repairs: install Chromium and scaffold a missing .prowl/").action(async (options) => {
|
|
10763
|
+
try {
|
|
10764
|
+
const report = await runDoctor(deps, { fix: options.fix });
|
|
10765
|
+
renderReport(report, Boolean(options.fix));
|
|
10766
|
+
if (!report.ok) {
|
|
10767
|
+
process.exitCode = 1;
|
|
10768
|
+
}
|
|
10769
|
+
} catch (error) {
|
|
10770
|
+
const message = error instanceof Error ? error.message : "doctor failed";
|
|
10771
|
+
console.error(import_chalk16.default.red(`Error: ${message}
|
|
10772
|
+
Re-run \`prowl doctor\`; if it fails again, report the error.`));
|
|
10773
|
+
process.exitCode = 1;
|
|
10774
|
+
}
|
|
10775
|
+
});
|
|
10776
|
+
}
|
|
10777
|
+
|
|
10011
10778
|
// src/cli/program.ts
|
|
10012
10779
|
var CLI_VERSION = package_default.version;
|
|
10013
10780
|
function buildProgram() {
|
|
10014
|
-
const program2 = new
|
|
10781
|
+
const program2 = new import_commander15.Command();
|
|
10015
10782
|
program2.name("prowl").description("CLI-first QA testing tool for deterministic Playwright flows").version(CLI_VERSION);
|
|
10016
10783
|
program2.addCommand(buildRunCommand());
|
|
10017
10784
|
program2.addCommand(buildCiCommand());
|
|
@@ -10026,6 +10793,7 @@ function buildProgram() {
|
|
|
10026
10793
|
program2.addCommand(buildFlakyCommand());
|
|
10027
10794
|
program2.addCommand(buildMcpCommand());
|
|
10028
10795
|
program2.addCommand(buildMacdriverCommand());
|
|
10796
|
+
program2.addCommand(buildDoctorCommand());
|
|
10029
10797
|
return program2;
|
|
10030
10798
|
}
|
|
10031
10799
|
|