prowl-tools 0.1.8 → 0.1.9
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 +42 -6
- package/dist/{chunk-CWLRDV5P.js → chunk-2RNOK64Y.js} +25 -1
- package/dist/chunk-2RNOK64Y.js.map +1 -0
- package/dist/index.cjs +637 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +595 -38
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +24 -0
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +24 -0
- package/dist/lib.d.ts +24 -0
- package/dist/lib.js +1 -1
- package/examples/hunts/form.yml +53 -0
- package/examples/hunts/macos-hello.yml +49 -0
- package/package.json +1 -1
- package/dist/chunk-CWLRDV5P.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1128,12 +1128,12 @@ var init_visual = __esm({
|
|
|
1128
1128
|
});
|
|
1129
1129
|
|
|
1130
1130
|
// src/cli/program.ts
|
|
1131
|
-
var
|
|
1131
|
+
var import_commander15 = require("commander");
|
|
1132
1132
|
|
|
1133
1133
|
// package.json
|
|
1134
1134
|
var package_default = {
|
|
1135
1135
|
name: "prowl-tools",
|
|
1136
|
-
version: "0.1.
|
|
1136
|
+
version: "0.1.9",
|
|
1137
1137
|
description: "E2E testing for native macOS apps and web apps from declarative YAML hunts.",
|
|
1138
1138
|
type: "module",
|
|
1139
1139
|
license: "Apache-2.0",
|
|
@@ -1950,10 +1950,15 @@ ${macdriverBuildInstructions()}`
|
|
|
1950
1950
|
${macdriverBuildInstructions()}`);
|
|
1951
1951
|
}
|
|
1952
1952
|
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
1953
|
+
function errorMessage(error) {
|
|
1954
|
+
return error instanceof Error ? error.message : String(error);
|
|
1955
|
+
}
|
|
1953
1956
|
var SpawnMacHelperClient = class {
|
|
1954
1957
|
child;
|
|
1955
1958
|
pending = /* @__PURE__ */ new Map();
|
|
1956
1959
|
requestTimeoutMs;
|
|
1960
|
+
onEvent;
|
|
1961
|
+
onEventError;
|
|
1957
1962
|
stdoutBuffer = "";
|
|
1958
1963
|
stderrBuffer = "";
|
|
1959
1964
|
nextId = 1;
|
|
@@ -1961,6 +1966,9 @@ var SpawnMacHelperClient = class {
|
|
|
1961
1966
|
terminalError;
|
|
1962
1967
|
constructor(binaryPath, options = {}) {
|
|
1963
1968
|
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
1969
|
+
this.onEvent = options.onEvent;
|
|
1970
|
+
this.onEventError = options.onEventError ?? ((message) => process.stderr.write(`${message}
|
|
1971
|
+
`));
|
|
1964
1972
|
this.child = (0, import_node_child_process2.spawn)(binaryPath, ["serve"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
1965
1973
|
this.child.stdout?.setEncoding("utf-8");
|
|
1966
1974
|
this.child.stderr?.setEncoding("utf-8");
|
|
@@ -1997,6 +2005,10 @@ var SpawnMacHelperClient = class {
|
|
|
1997
2005
|
} catch {
|
|
1998
2006
|
return;
|
|
1999
2007
|
}
|
|
2008
|
+
if (typeof message.event === "string" && typeof message.id !== "number") {
|
|
2009
|
+
this.handleEvent(message);
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2000
2012
|
const id = typeof message.id === "number" ? message.id : void 0;
|
|
2001
2013
|
if (id === void 0) {
|
|
2002
2014
|
return;
|
|
@@ -2013,6 +2025,18 @@ var SpawnMacHelperClient = class {
|
|
|
2013
2025
|
pending.reject(new Error(typeof message.error === "string" ? message.error : "prowl-macdriver error"));
|
|
2014
2026
|
}
|
|
2015
2027
|
}
|
|
2028
|
+
handleEvent(event) {
|
|
2029
|
+
try {
|
|
2030
|
+
this.onEvent?.(event);
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
try {
|
|
2033
|
+
this.onEventError(
|
|
2034
|
+
`prowl-macdriver event sink failed for event "${event.event}": ${errorMessage(error)}`
|
|
2035
|
+
);
|
|
2036
|
+
} catch {
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2016
2040
|
failAll(error) {
|
|
2017
2041
|
for (const pending of this.pending.values()) {
|
|
2018
2042
|
clearTimeout(pending.timer);
|
|
@@ -2924,12 +2948,12 @@ var Uia2Transport = class {
|
|
|
2924
2948
|
* {@link Uia2HttpError} on a non-2xx response, or a timeout error when the
|
|
2925
2949
|
* per-request deadline elapses.
|
|
2926
2950
|
*/
|
|
2927
|
-
async request(method,
|
|
2951
|
+
async request(method, path28, body, timeoutMs) {
|
|
2928
2952
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
2929
2953
|
const controller = new AbortController();
|
|
2930
2954
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
2931
2955
|
timer.unref?.();
|
|
2932
|
-
const url = `${this.baseUrl}${
|
|
2956
|
+
const url = `${this.baseUrl}${path28}`;
|
|
2933
2957
|
let response;
|
|
2934
2958
|
try {
|
|
2935
2959
|
response = await this.fetchImpl(url, {
|
|
@@ -2941,7 +2965,7 @@ var Uia2Transport = class {
|
|
|
2941
2965
|
} catch (error) {
|
|
2942
2966
|
if (controller.signal.aborted) {
|
|
2943
2967
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
2944
|
-
throw new Error(`uiautomator2 request ${method} ${
|
|
2968
|
+
throw new Error(`uiautomator2 request ${method} ${path28} timed out after ${shown}`);
|
|
2945
2969
|
}
|
|
2946
2970
|
throw error instanceof Error ? error : new Error(String(error));
|
|
2947
2971
|
} finally {
|
|
@@ -2952,7 +2976,7 @@ var Uia2Transport = class {
|
|
|
2952
2976
|
if (!response.ok) {
|
|
2953
2977
|
const wdError = extractWebdriverError(parsed);
|
|
2954
2978
|
throw new Uia2HttpError(
|
|
2955
|
-
`uiautomator2 ${method} ${
|
|
2979
|
+
`uiautomator2 ${method} ${path28} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
2956
2980
|
response.status,
|
|
2957
2981
|
wdError
|
|
2958
2982
|
);
|
|
@@ -3035,10 +3059,10 @@ function sleep(ms) {
|
|
|
3035
3059
|
}
|
|
3036
3060
|
function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
3037
3061
|
const base = `/session/${sessionId}`;
|
|
3038
|
-
async function locate(query,
|
|
3062
|
+
async function locate(query, path28) {
|
|
3039
3063
|
return transport.request(
|
|
3040
3064
|
"POST",
|
|
3041
|
-
`${base}${
|
|
3065
|
+
`${base}${path28}`,
|
|
3042
3066
|
androidQueryToLocator(query, { appPackage: options.appPackage })
|
|
3043
3067
|
);
|
|
3044
3068
|
}
|
|
@@ -3839,15 +3863,15 @@ var WdaTransport = class {
|
|
|
3839
3863
|
* {@link WdaHttpError} on a non-2xx response, or a timeout error when the
|
|
3840
3864
|
* per-request deadline elapses.
|
|
3841
3865
|
*/
|
|
3842
|
-
async requestFull(method,
|
|
3866
|
+
async requestFull(method, path28, body, timeoutMs) {
|
|
3843
3867
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
3844
3868
|
const controller = new AbortController();
|
|
3845
3869
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
3846
3870
|
timer.unref?.();
|
|
3847
|
-
const url = `${this.baseUrl}${
|
|
3871
|
+
const url = `${this.baseUrl}${path28}`;
|
|
3848
3872
|
const timeoutError = () => {
|
|
3849
3873
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
3850
|
-
return new Error(`WebDriverAgent request ${method} ${
|
|
3874
|
+
return new Error(`WebDriverAgent request ${method} ${path28} timed out after ${shown}`);
|
|
3851
3875
|
};
|
|
3852
3876
|
let response;
|
|
3853
3877
|
try {
|
|
@@ -3879,7 +3903,7 @@ var WdaTransport = class {
|
|
|
3879
3903
|
if (!response.ok) {
|
|
3880
3904
|
const wdError = extractWebdriverError2(parsed);
|
|
3881
3905
|
throw new WdaHttpError(
|
|
3882
|
-
`WebDriverAgent ${method} ${
|
|
3906
|
+
`WebDriverAgent ${method} ${path28} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
3883
3907
|
response.status,
|
|
3884
3908
|
wdError
|
|
3885
3909
|
);
|
|
@@ -3887,8 +3911,8 @@ var WdaTransport = class {
|
|
|
3887
3911
|
return parsed;
|
|
3888
3912
|
}
|
|
3889
3913
|
/** Like {@link requestFull} but returns just the `value` field. */
|
|
3890
|
-
async request(method,
|
|
3891
|
-
const parsed = await this.requestFull(method,
|
|
3914
|
+
async request(method, path28, body, timeoutMs) {
|
|
3915
|
+
const parsed = await this.requestFull(method, path28, body, timeoutMs);
|
|
3892
3916
|
return parsed?.value;
|
|
3893
3917
|
}
|
|
3894
3918
|
};
|
|
@@ -3964,8 +3988,8 @@ function sleep2(ms) {
|
|
|
3964
3988
|
}
|
|
3965
3989
|
function createWdaAgentClient(transport, sessionId) {
|
|
3966
3990
|
const base = `/session/${sessionId}`;
|
|
3967
|
-
async function locate(query,
|
|
3968
|
-
return transport.request("POST", `${base}${
|
|
3991
|
+
async function locate(query, path28) {
|
|
3992
|
+
return transport.request("POST", `${base}${path28}`, iosQueryToLocator(query));
|
|
3969
3993
|
}
|
|
3970
3994
|
return {
|
|
3971
3995
|
async findElement(query) {
|
|
@@ -7189,6 +7213,7 @@ function buildRunCommand() {
|
|
|
7189
7213
|
|
|
7190
7214
|
// src/cli/commands/init.ts
|
|
7191
7215
|
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
7216
|
+
var import_node_os5 = __toESM(require("os"), 1);
|
|
7192
7217
|
var import_node_path16 = __toESM(require("path"), 1);
|
|
7193
7218
|
var import_node_url2 = require("url");
|
|
7194
7219
|
var import_commander2 = require("commander");
|
|
@@ -7214,6 +7239,192 @@ function copyFile(source, destination) {
|
|
|
7214
7239
|
import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(destination), { recursive: true });
|
|
7215
7240
|
import_node_fs15.default.copyFileSync(source, destination);
|
|
7216
7241
|
}
|
|
7242
|
+
function isInsideDir(parent, child) {
|
|
7243
|
+
const relative = import_node_path16.default.relative(parent, child);
|
|
7244
|
+
return relative.length > 0 && !relative.startsWith("..") && !import_node_path16.default.isAbsolute(relative);
|
|
7245
|
+
}
|
|
7246
|
+
function lstatIfExists(target) {
|
|
7247
|
+
try {
|
|
7248
|
+
return import_node_fs15.default.lstatSync(target);
|
|
7249
|
+
} catch (error) {
|
|
7250
|
+
const code = error.code;
|
|
7251
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
7252
|
+
return null;
|
|
7253
|
+
}
|
|
7254
|
+
throw error;
|
|
7255
|
+
}
|
|
7256
|
+
}
|
|
7257
|
+
function errorMessage2(error) {
|
|
7258
|
+
return error instanceof Error ? error.message : String(error);
|
|
7259
|
+
}
|
|
7260
|
+
function assertNoSymlinkDestination(prowlDir, destination) {
|
|
7261
|
+
const relative = import_node_path16.default.relative(prowlDir, destination);
|
|
7262
|
+
if (relative.startsWith("..") || import_node_path16.default.isAbsolute(relative)) {
|
|
7263
|
+
throw new Error(`Refusing to write outside ${CONFIG_DIR}: ${destination}`);
|
|
7264
|
+
}
|
|
7265
|
+
const parts = relative.split(import_node_path16.default.sep).filter((part) => part.length > 0);
|
|
7266
|
+
let current = prowlDir;
|
|
7267
|
+
const rootStat = lstatIfExists(current);
|
|
7268
|
+
if (rootStat?.isSymbolicLink()) {
|
|
7269
|
+
throw new Error(
|
|
7270
|
+
`${CONFIG_DIR} scaffold destination path contains a symlink: ${current}. Replace it with a real file or directory before running prowl init --force.`
|
|
7271
|
+
);
|
|
7272
|
+
}
|
|
7273
|
+
if (rootStat && !rootStat.isDirectory()) {
|
|
7274
|
+
throw new Error(`${CONFIG_DIR} scaffold path is not a directory: ${current}`);
|
|
7275
|
+
}
|
|
7276
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
7277
|
+
current = import_node_path16.default.join(current, parts[index] ?? "");
|
|
7278
|
+
const stat = lstatIfExists(current);
|
|
7279
|
+
if (stat?.isSymbolicLink()) {
|
|
7280
|
+
throw new Error(
|
|
7281
|
+
`${CONFIG_DIR} scaffold destination path contains a symlink: ${current}. Replace it with a real file or directory before running prowl init --force.`
|
|
7282
|
+
);
|
|
7283
|
+
}
|
|
7284
|
+
if (stat && index < parts.length - 1 && !stat.isDirectory()) {
|
|
7285
|
+
throw new Error(`${CONFIG_DIR} scaffold parent path is not a directory: ${current}`);
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
function gitignoreTemplate() {
|
|
7290
|
+
return [
|
|
7291
|
+
"# Run artifacts (screenshots, logs, reports)",
|
|
7292
|
+
"runs/",
|
|
7293
|
+
"",
|
|
7294
|
+
"# Auth state (tokens, cookies)",
|
|
7295
|
+
"auth-state.json",
|
|
7296
|
+
"",
|
|
7297
|
+
"# Environment variables (credentials)",
|
|
7298
|
+
".env",
|
|
7299
|
+
""
|
|
7300
|
+
].join("\n");
|
|
7301
|
+
}
|
|
7302
|
+
function stageScaffoldTemplates(examplesDir) {
|
|
7303
|
+
const stageDir = import_node_fs15.default.mkdtempSync(import_node_path16.default.join(import_node_os5.default.tmpdir(), "prowl-init-"));
|
|
7304
|
+
const files = [];
|
|
7305
|
+
try {
|
|
7306
|
+
copyFile(import_node_path16.default.join(examplesDir, "config.yml"), import_node_path16.default.join(stageDir, "config.yml"));
|
|
7307
|
+
files.push({ source: import_node_path16.default.join(stageDir, "config.yml"), relativePath: "config.yml" });
|
|
7308
|
+
const exampleHuntsDir = import_node_path16.default.join(examplesDir, "hunts");
|
|
7309
|
+
const huntFiles = import_node_fs15.default.readdirSync(exampleHuntsDir).filter((f) => f.endsWith(".yml"));
|
|
7310
|
+
for (const huntFile of huntFiles) {
|
|
7311
|
+
const relativePath = import_node_path16.default.join("hunts", huntFile);
|
|
7312
|
+
copyFile(import_node_path16.default.join(exampleHuntsDir, huntFile), import_node_path16.default.join(stageDir, relativePath));
|
|
7313
|
+
files.push({ source: import_node_path16.default.join(stageDir, relativePath), relativePath });
|
|
7314
|
+
}
|
|
7315
|
+
const gitignorePath = import_node_path16.default.join(stageDir, ".gitignore");
|
|
7316
|
+
import_node_fs15.default.writeFileSync(gitignorePath, gitignoreTemplate());
|
|
7317
|
+
files.push({ source: gitignorePath, relativePath: ".gitignore" });
|
|
7318
|
+
return { stageDir, files };
|
|
7319
|
+
} catch (error) {
|
|
7320
|
+
import_node_fs15.default.rmSync(stageDir, { recursive: true, force: true });
|
|
7321
|
+
throw error;
|
|
7322
|
+
}
|
|
7323
|
+
}
|
|
7324
|
+
function trackCreatedParentDirs(destination, prowlDir, createdDirs) {
|
|
7325
|
+
let dir = import_node_path16.default.dirname(destination);
|
|
7326
|
+
while (isInsideDir(prowlDir, dir) && !import_node_fs15.default.existsSync(dir)) {
|
|
7327
|
+
createdDirs.add(dir);
|
|
7328
|
+
dir = import_node_path16.default.dirname(dir);
|
|
7329
|
+
}
|
|
7330
|
+
}
|
|
7331
|
+
function prepareDestinationRollback(prowlDir, files) {
|
|
7332
|
+
const backupDir = import_node_fs15.default.mkdtempSync(import_node_path16.default.join(import_node_os5.default.tmpdir(), "prowl-init-rollback-"));
|
|
7333
|
+
const backups = [];
|
|
7334
|
+
const createdFiles = [];
|
|
7335
|
+
const createdDirs = /* @__PURE__ */ new Set();
|
|
7336
|
+
const prowlDirExisted = import_node_fs15.default.existsSync(prowlDir);
|
|
7337
|
+
try {
|
|
7338
|
+
for (const file of files) {
|
|
7339
|
+
const destination = import_node_path16.default.join(prowlDir, file.relativePath);
|
|
7340
|
+
assertNoSymlinkDestination(prowlDir, destination);
|
|
7341
|
+
const stat = lstatIfExists(destination);
|
|
7342
|
+
if (stat) {
|
|
7343
|
+
if (!stat.isFile()) {
|
|
7344
|
+
throw new Error(`${CONFIG_DIR} scaffold destination is not a regular file: ${destination}`);
|
|
7345
|
+
}
|
|
7346
|
+
const backup = import_node_path16.default.join(backupDir, file.relativePath);
|
|
7347
|
+
copyFile(destination, backup);
|
|
7348
|
+
backups.push({ destination, backup });
|
|
7349
|
+
} else {
|
|
7350
|
+
createdFiles.push(destination);
|
|
7351
|
+
}
|
|
7352
|
+
if (prowlDirExisted) {
|
|
7353
|
+
trackCreatedParentDirs(destination, prowlDir, createdDirs);
|
|
7354
|
+
}
|
|
7355
|
+
}
|
|
7356
|
+
return {
|
|
7357
|
+
prowlDirExisted,
|
|
7358
|
+
backupDir,
|
|
7359
|
+
backups,
|
|
7360
|
+
createdFiles,
|
|
7361
|
+
createdDirs: [...createdDirs].sort((a, b) => b.length - a.length)
|
|
7362
|
+
};
|
|
7363
|
+
} catch (error) {
|
|
7364
|
+
import_node_fs15.default.rmSync(backupDir, { recursive: true, force: true });
|
|
7365
|
+
throw error;
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
function rollbackDestination(prowlDir, rollback) {
|
|
7369
|
+
if (!rollback.prowlDirExisted) {
|
|
7370
|
+
import_node_fs15.default.rmSync(prowlDir, { recursive: true, force: true });
|
|
7371
|
+
return;
|
|
7372
|
+
}
|
|
7373
|
+
for (const file of rollback.createdFiles) {
|
|
7374
|
+
import_node_fs15.default.rmSync(file, { force: true });
|
|
7375
|
+
}
|
|
7376
|
+
for (const backup of rollback.backups) {
|
|
7377
|
+
try {
|
|
7378
|
+
copyFile(backup.backup, backup.destination);
|
|
7379
|
+
} catch (error) {
|
|
7380
|
+
throw new Error(
|
|
7381
|
+
`Failed to restore ${backup.destination} from backup ${backup.backup}: ${errorMessage2(error)}`
|
|
7382
|
+
);
|
|
7383
|
+
}
|
|
7384
|
+
}
|
|
7385
|
+
for (const dir of rollback.createdDirs) {
|
|
7386
|
+
try {
|
|
7387
|
+
import_node_fs15.default.rmdirSync(dir);
|
|
7388
|
+
} catch {
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
7391
|
+
}
|
|
7392
|
+
function scaffoldProwlDir(root) {
|
|
7393
|
+
const prowlDir = import_node_path16.default.join(root, CONFIG_DIR);
|
|
7394
|
+
const packageRoot = getPackageRoot2();
|
|
7395
|
+
const examplesDir = import_node_path16.default.join(packageRoot, "examples");
|
|
7396
|
+
const exampleConfig = import_node_path16.default.join(examplesDir, "config.yml");
|
|
7397
|
+
const exampleHuntsDir = import_node_path16.default.join(examplesDir, "hunts");
|
|
7398
|
+
if (!import_node_fs15.default.existsSync(exampleConfig) || !import_node_fs15.default.existsSync(exampleHuntsDir)) {
|
|
7399
|
+
throw new Error("Examples not found in package. Reinstall prowl-tools.");
|
|
7400
|
+
}
|
|
7401
|
+
const staged = stageScaffoldTemplates(examplesDir);
|
|
7402
|
+
let rollback = null;
|
|
7403
|
+
let keepBackup = false;
|
|
7404
|
+
try {
|
|
7405
|
+
rollback = prepareDestinationRollback(prowlDir, staged.files);
|
|
7406
|
+
for (const file of staged.files) {
|
|
7407
|
+
copyFile(file.source, import_node_path16.default.join(prowlDir, file.relativePath));
|
|
7408
|
+
}
|
|
7409
|
+
} catch (error) {
|
|
7410
|
+
if (rollback) {
|
|
7411
|
+
try {
|
|
7412
|
+
rollbackDestination(prowlDir, rollback);
|
|
7413
|
+
} catch (rollbackError) {
|
|
7414
|
+
keepBackup = true;
|
|
7415
|
+
throw new Error(
|
|
7416
|
+
`Scaffold failed: ${errorMessage2(error)}. Rollback failed: ${errorMessage2(rollbackError)}. Backup preserved at ${rollback.backupDir}.`
|
|
7417
|
+
);
|
|
7418
|
+
}
|
|
7419
|
+
}
|
|
7420
|
+
throw error;
|
|
7421
|
+
} finally {
|
|
7422
|
+
if (rollback && !keepBackup) {
|
|
7423
|
+
import_node_fs15.default.rmSync(rollback.backupDir, { recursive: true, force: true });
|
|
7424
|
+
}
|
|
7425
|
+
import_node_fs15.default.rmSync(staged.stageDir, { recursive: true, force: true });
|
|
7426
|
+
}
|
|
7427
|
+
}
|
|
7217
7428
|
function buildInitCommand() {
|
|
7218
7429
|
const command = new import_commander2.Command("init").option("--force", `Overwrite existing ${CONFIG_DIR} directory`).action((options) => {
|
|
7219
7430
|
const root = process.cwd();
|
|
@@ -7227,39 +7438,18 @@ function buildInitCommand() {
|
|
|
7227
7438
|
process.exitCode = 1;
|
|
7228
7439
|
return;
|
|
7229
7440
|
}
|
|
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."));
|
|
7441
|
+
try {
|
|
7442
|
+
scaffoldProwlDir(root);
|
|
7443
|
+
} catch (error) {
|
|
7444
|
+
console.error(import_chalk4.default.red(error instanceof Error ? error.message : "init failed"));
|
|
7236
7445
|
process.exitCode = 1;
|
|
7237
7446
|
return;
|
|
7238
7447
|
}
|
|
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
7448
|
console.log(welcomeBanner());
|
|
7260
7449
|
console.log(import_chalk4.default.green(` Initialized ${CONFIG_DIR} directory.`));
|
|
7261
7450
|
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("
|
|
7451
|
+
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."));
|
|
7452
|
+
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
7453
|
});
|
|
7264
7454
|
return command;
|
|
7265
7455
|
}
|
|
@@ -9463,7 +9653,7 @@ async function runHuntTool(args, configPath) {
|
|
|
9463
9653
|
|
|
9464
9654
|
// src/mcp/projects.ts
|
|
9465
9655
|
var import_node_fs21 = __toESM(require("fs"), 1);
|
|
9466
|
-
var
|
|
9656
|
+
var import_node_os6 = __toESM(require("os"), 1);
|
|
9467
9657
|
var import_node_path25 = __toESM(require("path"), 1);
|
|
9468
9658
|
var import_yaml3 = __toESM(require("yaml"), 1);
|
|
9469
9659
|
var import_zod2 = require("zod");
|
|
@@ -9476,10 +9666,10 @@ var projectRegistrySchema = import_zod2.z.object({
|
|
|
9476
9666
|
projects: import_zod2.z.record(import_zod2.z.string().min(1), projectEntrySchema)
|
|
9477
9667
|
}).strict();
|
|
9478
9668
|
function defaultRegistryPath() {
|
|
9479
|
-
return import_node_path25.default.join(
|
|
9669
|
+
return import_node_path25.default.join(import_node_os6.default.homedir(), ".prowl", "projects.yml");
|
|
9480
9670
|
}
|
|
9481
9671
|
function legacyRegistryPath() {
|
|
9482
|
-
return import_node_path25.default.join(
|
|
9672
|
+
return import_node_path25.default.join(import_node_os6.default.homedir(), ".prowlqa", "projects.yml");
|
|
9483
9673
|
}
|
|
9484
9674
|
function resolveProjectConfigPath(root) {
|
|
9485
9675
|
const preferred = import_node_path25.default.join(root, ".prowl", "config.yml");
|
|
@@ -9533,7 +9723,7 @@ function listRegisteredProjects(registry) {
|
|
|
9533
9723
|
function textResult(data) {
|
|
9534
9724
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
9535
9725
|
}
|
|
9536
|
-
function
|
|
9726
|
+
function errorMessage3(error) {
|
|
9537
9727
|
return error instanceof Error ? error.message : String(error);
|
|
9538
9728
|
}
|
|
9539
9729
|
function safeStringify(value) {
|
|
@@ -9547,7 +9737,7 @@ async function toolResult(toolName, args, action) {
|
|
|
9547
9737
|
try {
|
|
9548
9738
|
return textResult(await action());
|
|
9549
9739
|
} catch (error) {
|
|
9550
|
-
throw new Error(`${toolName} failed for args=${safeStringify(args)}: ${
|
|
9740
|
+
throw new Error(`${toolName} failed for args=${safeStringify(args)}: ${errorMessage3(error)}`, {
|
|
9551
9741
|
cause: error
|
|
9552
9742
|
});
|
|
9553
9743
|
}
|
|
@@ -9624,7 +9814,7 @@ async function startMcpServer(options = {}) {
|
|
|
9624
9814
|
}
|
|
9625
9815
|
|
|
9626
9816
|
// src/cli/commands/mcp.ts
|
|
9627
|
-
function
|
|
9817
|
+
function errorMessage4(error) {
|
|
9628
9818
|
return error instanceof Error ? error.message : String(error);
|
|
9629
9819
|
}
|
|
9630
9820
|
function buildMcpCommand() {
|
|
@@ -9635,7 +9825,7 @@ function buildMcpCommand() {
|
|
|
9635
9825
|
try {
|
|
9636
9826
|
await startMcpServer({ registryPath: options.projects });
|
|
9637
9827
|
} catch (error) {
|
|
9638
|
-
console.error(`Failed to start MCP server: ${
|
|
9828
|
+
console.error(`Failed to start MCP server: ${errorMessage4(error)}`);
|
|
9639
9829
|
process.exit(1);
|
|
9640
9830
|
}
|
|
9641
9831
|
});
|
|
@@ -9649,7 +9839,7 @@ var import_commander13 = require("commander");
|
|
|
9649
9839
|
var import_node_child_process7 = require("child_process");
|
|
9650
9840
|
var import_node_crypto3 = require("crypto");
|
|
9651
9841
|
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
9652
|
-
var
|
|
9842
|
+
var import_node_os7 = __toESM(require("os"), 1);
|
|
9653
9843
|
var import_node_path26 = __toESM(require("path"), 1);
|
|
9654
9844
|
var import_node_util = require("util");
|
|
9655
9845
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process7.execFile);
|
|
@@ -9815,7 +10005,7 @@ The download was rejected and discarded; re-run the install, and report it if it
|
|
|
9815
10005
|
}
|
|
9816
10006
|
async function installMacdriver(options = {}) {
|
|
9817
10007
|
const version = validateMacdriverVersion(options.version ?? MACDRIVER_VERSION);
|
|
9818
|
-
const homedir = options.homedir ??
|
|
10008
|
+
const homedir = options.homedir ?? import_node_os7.default.homedir();
|
|
9819
10009
|
const listArchiveEntries = options.listArchiveEntries ?? zipinfoArchiveLister;
|
|
9820
10010
|
const extract = options.extract ?? dittoExtractor;
|
|
9821
10011
|
const verifySignature = options.verifySignature ?? codesignVerifier;
|
|
@@ -9895,7 +10085,7 @@ var runVersionProbe = async (binaryPath) => {
|
|
|
9895
10085
|
};
|
|
9896
10086
|
async function collectMacdriverStatus(options = {}) {
|
|
9897
10087
|
const env = options.env ?? process.env;
|
|
9898
|
-
const homedir = options.homedir ??
|
|
10088
|
+
const homedir = options.homedir ?? import_node_os7.default.homedir();
|
|
9899
10089
|
const probe = options.probe ?? runVersionProbe;
|
|
9900
10090
|
let resolved = null;
|
|
9901
10091
|
try {
|
|
@@ -10008,10 +10198,402 @@ function buildMacdriverCommand() {
|
|
|
10008
10198
|
return command;
|
|
10009
10199
|
}
|
|
10010
10200
|
|
|
10201
|
+
// src/cli/commands/doctor.ts
|
|
10202
|
+
var import_chalk16 = __toESM(require("chalk"), 1);
|
|
10203
|
+
var import_commander14 = require("commander");
|
|
10204
|
+
|
|
10205
|
+
// src/doctor/checks.ts
|
|
10206
|
+
var import_node_child_process8 = require("child_process");
|
|
10207
|
+
var import_node_module3 = require("module");
|
|
10208
|
+
var import_node_fs23 = __toESM(require("fs"), 1);
|
|
10209
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
10210
|
+
var import_node_util2 = require("util");
|
|
10211
|
+
init_loader();
|
|
10212
|
+
var import_meta5 = {};
|
|
10213
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process8.execFile);
|
|
10214
|
+
var MIN_NODE_MAJOR = 20;
|
|
10215
|
+
var CHECK_NODE = "Node.js version";
|
|
10216
|
+
var CHECK_PLAYWRIGHT = "Playwright installed";
|
|
10217
|
+
var CHECK_CHROMIUM = "Chromium browser";
|
|
10218
|
+
var CHECK_PROWL_DIR = ".prowl/ directory";
|
|
10219
|
+
var CHECK_CONFIG = "config.yml valid";
|
|
10220
|
+
var CHECK_MACOS = "macOS helper (prowl-macdriver)";
|
|
10221
|
+
var CHECK_IOS = "iOS tooling (xcrun simctl)";
|
|
10222
|
+
var CHECK_ANDROID = "Android tooling (adb)";
|
|
10223
|
+
var INSTALL_CHROMIUM_TIMEOUT_MS = 3e5;
|
|
10224
|
+
function nodeMajor(version) {
|
|
10225
|
+
const cleaned = version.startsWith("v") ? version.slice(1) : version;
|
|
10226
|
+
return Number.parseInt(cleaned.split(".")[0] ?? "", 10);
|
|
10227
|
+
}
|
|
10228
|
+
function checkNode(nodeVersion) {
|
|
10229
|
+
const major = nodeMajor(nodeVersion);
|
|
10230
|
+
if (Number.isFinite(major) && major >= MIN_NODE_MAJOR) {
|
|
10231
|
+
return {
|
|
10232
|
+
name: CHECK_NODE,
|
|
10233
|
+
status: "pass",
|
|
10234
|
+
message: `Node ${nodeVersion} (>= ${MIN_NODE_MAJOR} required)`,
|
|
10235
|
+
fixable: false
|
|
10236
|
+
};
|
|
10237
|
+
}
|
|
10238
|
+
return {
|
|
10239
|
+
name: CHECK_NODE,
|
|
10240
|
+
status: "fail",
|
|
10241
|
+
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.`,
|
|
10242
|
+
fixable: false
|
|
10243
|
+
};
|
|
10244
|
+
}
|
|
10245
|
+
function checkPlaywright(probe) {
|
|
10246
|
+
if (probe.playwrightInstalled) {
|
|
10247
|
+
return { name: CHECK_PLAYWRIGHT, status: "pass", message: "Playwright is installed", fixable: false };
|
|
10248
|
+
}
|
|
10249
|
+
return {
|
|
10250
|
+
name: CHECK_PLAYWRIGHT,
|
|
10251
|
+
status: "fail",
|
|
10252
|
+
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).",
|
|
10253
|
+
fixable: false
|
|
10254
|
+
};
|
|
10255
|
+
}
|
|
10256
|
+
function checkChromium(probe) {
|
|
10257
|
+
if (!probe.playwrightInstalled) {
|
|
10258
|
+
return {
|
|
10259
|
+
name: CHECK_CHROMIUM,
|
|
10260
|
+
status: "skip",
|
|
10261
|
+
message: "Skipped \u2014 Playwright is not installed",
|
|
10262
|
+
fixable: false
|
|
10263
|
+
};
|
|
10264
|
+
}
|
|
10265
|
+
if (probe.chromiumInstalled) {
|
|
10266
|
+
return {
|
|
10267
|
+
name: CHECK_CHROMIUM,
|
|
10268
|
+
status: "pass",
|
|
10269
|
+
message: `Chromium is available${probe.chromiumPath ? ` (${probe.chromiumPath})` : ""}`,
|
|
10270
|
+
fixable: false
|
|
10271
|
+
};
|
|
10272
|
+
}
|
|
10273
|
+
return {
|
|
10274
|
+
name: CHECK_CHROMIUM,
|
|
10275
|
+
status: "fail",
|
|
10276
|
+
message: "Chromium is not installed. Run `npx playwright install chromium` (or `prowl doctor --fix`).",
|
|
10277
|
+
fixable: true
|
|
10278
|
+
};
|
|
10279
|
+
}
|
|
10280
|
+
function checkProwlDir(exists) {
|
|
10281
|
+
if (exists) {
|
|
10282
|
+
return { name: CHECK_PROWL_DIR, status: "pass", message: `${CONFIG_DIR}/ exists`, fixable: false };
|
|
10283
|
+
}
|
|
10284
|
+
return {
|
|
10285
|
+
name: CHECK_PROWL_DIR,
|
|
10286
|
+
status: "warn",
|
|
10287
|
+
message: `No ${CONFIG_DIR}/ directory here. Run \`prowl init\` (or \`prowl doctor --fix\`) to scaffold config and starter hunts.`,
|
|
10288
|
+
fixable: true
|
|
10289
|
+
};
|
|
10290
|
+
}
|
|
10291
|
+
function checkConfig(prowlDirExists, loadConfig2) {
|
|
10292
|
+
if (!prowlDirExists) {
|
|
10293
|
+
return {
|
|
10294
|
+
result: {
|
|
10295
|
+
name: CHECK_CONFIG,
|
|
10296
|
+
status: "skip",
|
|
10297
|
+
message: `Skipped \u2014 no ${CONFIG_DIR}/ directory`,
|
|
10298
|
+
fixable: false
|
|
10299
|
+
},
|
|
10300
|
+
config: null
|
|
10301
|
+
};
|
|
10302
|
+
}
|
|
10303
|
+
try {
|
|
10304
|
+
const { config } = loadConfig2();
|
|
10305
|
+
return {
|
|
10306
|
+
result: {
|
|
10307
|
+
name: CHECK_CONFIG,
|
|
10308
|
+
status: "pass",
|
|
10309
|
+
message: `Valid (target: ${config.target.type})`,
|
|
10310
|
+
fixable: false
|
|
10311
|
+
},
|
|
10312
|
+
config
|
|
10313
|
+
};
|
|
10314
|
+
} catch (error) {
|
|
10315
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10316
|
+
return {
|
|
10317
|
+
result: {
|
|
10318
|
+
name: CHECK_CONFIG,
|
|
10319
|
+
status: "fail",
|
|
10320
|
+
message: `Invalid config: ${detail}`,
|
|
10321
|
+
fixable: false
|
|
10322
|
+
},
|
|
10323
|
+
config: null
|
|
10324
|
+
};
|
|
10325
|
+
}
|
|
10326
|
+
}
|
|
10327
|
+
function macdriverSourceLabel(source) {
|
|
10328
|
+
switch (source) {
|
|
10329
|
+
case "env":
|
|
10330
|
+
return "PROWL_MACDRIVER_BIN override";
|
|
10331
|
+
case "user-install":
|
|
10332
|
+
return "user install (~/.prowl/macdriver)";
|
|
10333
|
+
case "source-build":
|
|
10334
|
+
return "repo source build";
|
|
10335
|
+
}
|
|
10336
|
+
}
|
|
10337
|
+
async function checkMacTarget(collect2, env) {
|
|
10338
|
+
let status;
|
|
10339
|
+
try {
|
|
10340
|
+
status = await collect2({ env });
|
|
10341
|
+
} catch (error) {
|
|
10342
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10343
|
+
return {
|
|
10344
|
+
name: CHECK_MACOS,
|
|
10345
|
+
status: "fail",
|
|
10346
|
+
message: `Could not inspect prowl-macdriver status (${detail}). Run \`prowl macdriver status\` for details.`,
|
|
10347
|
+
fixable: false
|
|
10348
|
+
};
|
|
10349
|
+
}
|
|
10350
|
+
if (status.resolved && status.probedVersion) {
|
|
10351
|
+
return {
|
|
10352
|
+
name: CHECK_MACOS,
|
|
10353
|
+
status: "pass",
|
|
10354
|
+
message: `Resolved via ${macdriverSourceLabel(status.resolved.source)}: ${status.resolved.path} (reports ${status.probedVersion}). Verify Accessibility/Screen Recording permissions with \`prowl macdriver status\`.`,
|
|
10355
|
+
fixable: false
|
|
10356
|
+
};
|
|
10357
|
+
}
|
|
10358
|
+
if (status.resolved) {
|
|
10359
|
+
return {
|
|
10360
|
+
name: CHECK_MACOS,
|
|
10361
|
+
status: "fail",
|
|
10362
|
+
message: `prowl-macdriver resolved via ${macdriverSourceLabel(status.resolved.source)} (${status.resolved.path}), but the helper version probe failed. Run \`prowl macdriver status\` for details.`,
|
|
10363
|
+
fixable: false
|
|
10364
|
+
};
|
|
10365
|
+
}
|
|
10366
|
+
return {
|
|
10367
|
+
name: CHECK_MACOS,
|
|
10368
|
+
status: "fail",
|
|
10369
|
+
message: "prowl-macdriver helper not found. Install it with `prowl macdriver install`, then check permissions with `prowl macdriver status`.",
|
|
10370
|
+
fixable: false
|
|
10371
|
+
};
|
|
10372
|
+
}
|
|
10373
|
+
async function checkIosTarget(platform, run) {
|
|
10374
|
+
if (platform !== "darwin") {
|
|
10375
|
+
return {
|
|
10376
|
+
name: CHECK_IOS,
|
|
10377
|
+
status: "fail",
|
|
10378
|
+
message: `The iOS target requires macOS; xcrun simctl is unavailable on ${platform}.`,
|
|
10379
|
+
fixable: false
|
|
10380
|
+
};
|
|
10381
|
+
}
|
|
10382
|
+
try {
|
|
10383
|
+
await run("xcrun", ["simctl", "help"]);
|
|
10384
|
+
return { name: CHECK_IOS, status: "pass", message: "xcrun simctl is reachable", fixable: false };
|
|
10385
|
+
} catch (error) {
|
|
10386
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10387
|
+
return {
|
|
10388
|
+
name: CHECK_IOS,
|
|
10389
|
+
status: "fail",
|
|
10390
|
+
message: `xcrun simctl is not reachable (${detail}). Install Xcode and its command-line tools (\`xcode-select --install\`), then open Xcode once to finish setup.`,
|
|
10391
|
+
fixable: false
|
|
10392
|
+
};
|
|
10393
|
+
}
|
|
10394
|
+
}
|
|
10395
|
+
async function checkAndroidTarget(run) {
|
|
10396
|
+
try {
|
|
10397
|
+
await run("adb", ["version"]);
|
|
10398
|
+
return { name: CHECK_ANDROID, status: "pass", message: "adb is reachable", fixable: false };
|
|
10399
|
+
} catch (error) {
|
|
10400
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10401
|
+
return {
|
|
10402
|
+
name: CHECK_ANDROID,
|
|
10403
|
+
status: "fail",
|
|
10404
|
+
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).`,
|
|
10405
|
+
fixable: false
|
|
10406
|
+
};
|
|
10407
|
+
}
|
|
10408
|
+
}
|
|
10409
|
+
async function runDoctorChecks(deps) {
|
|
10410
|
+
const results = [];
|
|
10411
|
+
const probe = await deps.probePlaywright();
|
|
10412
|
+
results.push(checkNode(deps.nodeVersion));
|
|
10413
|
+
results.push(checkPlaywright(probe));
|
|
10414
|
+
results.push(checkChromium(probe));
|
|
10415
|
+
const prowlExists = deps.prowlDirExists(deps.cwd);
|
|
10416
|
+
results.push(checkProwlDir(prowlExists));
|
|
10417
|
+
const { result: configResult, config } = checkConfig(prowlExists, deps.loadConfig);
|
|
10418
|
+
results.push(configResult);
|
|
10419
|
+
if (config) {
|
|
10420
|
+
switch (config.target.type) {
|
|
10421
|
+
case "macos":
|
|
10422
|
+
results.push(await checkMacTarget(deps.collectMacdriverStatus, deps.env));
|
|
10423
|
+
break;
|
|
10424
|
+
case "ios":
|
|
10425
|
+
results.push(await checkIosTarget(deps.platform, deps.runCommand));
|
|
10426
|
+
break;
|
|
10427
|
+
case "android":
|
|
10428
|
+
results.push(await checkAndroidTarget(deps.runCommand));
|
|
10429
|
+
break;
|
|
10430
|
+
default:
|
|
10431
|
+
break;
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
return results;
|
|
10435
|
+
}
|
|
10436
|
+
async function runDoctor(deps, options = {}) {
|
|
10437
|
+
let results = await runDoctorChecks(deps);
|
|
10438
|
+
const fixes = [];
|
|
10439
|
+
if (options.fix) {
|
|
10440
|
+
for (const result of results) {
|
|
10441
|
+
if (!result.fixable || result.status === "pass" || result.status === "skip") {
|
|
10442
|
+
continue;
|
|
10443
|
+
}
|
|
10444
|
+
try {
|
|
10445
|
+
if (result.name === CHECK_CHROMIUM) {
|
|
10446
|
+
await deps.installChromium();
|
|
10447
|
+
fixes.push({ name: result.name });
|
|
10448
|
+
} else if (result.name === CHECK_PROWL_DIR) {
|
|
10449
|
+
deps.scaffoldProwlDir(deps.cwd);
|
|
10450
|
+
fixes.push({ name: result.name });
|
|
10451
|
+
}
|
|
10452
|
+
} catch (error) {
|
|
10453
|
+
fixes.push({
|
|
10454
|
+
name: result.name,
|
|
10455
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10456
|
+
});
|
|
10457
|
+
}
|
|
10458
|
+
}
|
|
10459
|
+
if (fixes.length > 0) {
|
|
10460
|
+
results = await runDoctorChecks(deps);
|
|
10461
|
+
}
|
|
10462
|
+
}
|
|
10463
|
+
const ok = !results.some((result) => result.status === "fail");
|
|
10464
|
+
return { results, fixes, ok };
|
|
10465
|
+
}
|
|
10466
|
+
async function defaultProbePlaywright() {
|
|
10467
|
+
try {
|
|
10468
|
+
const pw = await import("playwright");
|
|
10469
|
+
let chromiumPath = null;
|
|
10470
|
+
try {
|
|
10471
|
+
chromiumPath = pw.chromium.executablePath();
|
|
10472
|
+
} catch {
|
|
10473
|
+
chromiumPath = null;
|
|
10474
|
+
}
|
|
10475
|
+
const chromiumInstalled = chromiumPath ? import_node_fs23.default.existsSync(chromiumPath) : false;
|
|
10476
|
+
return { playwrightInstalled: true, chromiumPath, chromiumInstalled };
|
|
10477
|
+
} catch {
|
|
10478
|
+
return { playwrightInstalled: false, chromiumPath: null, chromiumInstalled: false };
|
|
10479
|
+
}
|
|
10480
|
+
}
|
|
10481
|
+
var defaultRunCommand = async (file, args) => execFileAsync2(file, args, { timeout: 5e3 });
|
|
10482
|
+
function createLocalRequire() {
|
|
10483
|
+
if (typeof __filename === "string") {
|
|
10484
|
+
return (0, import_node_module3.createRequire)(__filename);
|
|
10485
|
+
}
|
|
10486
|
+
return (0, import_node_module3.createRequire)(import_meta5.url);
|
|
10487
|
+
}
|
|
10488
|
+
function resolvePlaywrightCliPath(requireFn = createLocalRequire()) {
|
|
10489
|
+
return import_node_path27.default.join(import_node_path27.default.dirname(requireFn.resolve("playwright/package.json")), "cli.js");
|
|
10490
|
+
}
|
|
10491
|
+
function playwrightInstallChromiumCommand() {
|
|
10492
|
+
return {
|
|
10493
|
+
file: process.execPath,
|
|
10494
|
+
args: [resolvePlaywrightCliPath(), "install", "chromium"]
|
|
10495
|
+
};
|
|
10496
|
+
}
|
|
10497
|
+
async function defaultInstallChromium() {
|
|
10498
|
+
const command = playwrightInstallChromiumCommand();
|
|
10499
|
+
await execFileAsync2(command.file, command.args, { timeout: INSTALL_CHROMIUM_TIMEOUT_MS });
|
|
10500
|
+
}
|
|
10501
|
+
function defaultDoctorDeps() {
|
|
10502
|
+
return {
|
|
10503
|
+
nodeVersion: process.versions.node,
|
|
10504
|
+
platform: process.platform,
|
|
10505
|
+
cwd: process.cwd(),
|
|
10506
|
+
env: process.env,
|
|
10507
|
+
probePlaywright: defaultProbePlaywright,
|
|
10508
|
+
runCommand: defaultRunCommand,
|
|
10509
|
+
prowlDirExists: (cwd) => import_node_fs23.default.existsSync(import_node_path27.default.join(cwd, CONFIG_DIR)),
|
|
10510
|
+
loadConfig: () => loadConfig(),
|
|
10511
|
+
collectMacdriverStatus,
|
|
10512
|
+
installChromium: defaultInstallChromium,
|
|
10513
|
+
scaffoldProwlDir
|
|
10514
|
+
};
|
|
10515
|
+
}
|
|
10516
|
+
|
|
10517
|
+
// src/cli/commands/doctor.ts
|
|
10518
|
+
function symbol(status) {
|
|
10519
|
+
switch (status) {
|
|
10520
|
+
case "pass":
|
|
10521
|
+
return import_chalk16.default.green("\u2713");
|
|
10522
|
+
// ✓
|
|
10523
|
+
case "fail":
|
|
10524
|
+
return import_chalk16.default.red("\u2717");
|
|
10525
|
+
// ✗
|
|
10526
|
+
case "warn":
|
|
10527
|
+
return import_chalk16.default.yellow("\u26A0");
|
|
10528
|
+
// ⚠
|
|
10529
|
+
case "skip":
|
|
10530
|
+
return import_chalk16.default.gray("\u25CB");
|
|
10531
|
+
}
|
|
10532
|
+
}
|
|
10533
|
+
function renderResult(result) {
|
|
10534
|
+
console.log(` ${symbol(result.status)} ${import_chalk16.default.bold(result.name)}`);
|
|
10535
|
+
console.log(` ${import_chalk16.default.gray(result.message)}`);
|
|
10536
|
+
}
|
|
10537
|
+
function renderReport(report, fixRequested) {
|
|
10538
|
+
console.log(import_chalk16.default.bold("\nprowl doctor\n"));
|
|
10539
|
+
for (const result of report.results) {
|
|
10540
|
+
renderResult(result);
|
|
10541
|
+
}
|
|
10542
|
+
if (fixRequested) {
|
|
10543
|
+
console.log("");
|
|
10544
|
+
if (report.fixes.length === 0) {
|
|
10545
|
+
console.log(import_chalk16.default.gray(" No auto-fixable issues to repair."));
|
|
10546
|
+
} else {
|
|
10547
|
+
console.log(import_chalk16.default.bold(" Attempted fixes:"));
|
|
10548
|
+
for (const fix of report.fixes) {
|
|
10549
|
+
if (fix.error) {
|
|
10550
|
+
console.log(` ${import_chalk16.default.red("\u2717")} ${fix.name} \u2014 ${import_chalk16.default.gray(fix.error)}`);
|
|
10551
|
+
} else {
|
|
10552
|
+
console.log(` ${import_chalk16.default.green("\u2713")} ${fix.name}`);
|
|
10553
|
+
}
|
|
10554
|
+
}
|
|
10555
|
+
}
|
|
10556
|
+
}
|
|
10557
|
+
const counts = { pass: 0, warn: 0, fail: 0, skip: 0 };
|
|
10558
|
+
for (const result of report.results) {
|
|
10559
|
+
counts[result.status] += 1;
|
|
10560
|
+
}
|
|
10561
|
+
const parts = [
|
|
10562
|
+
import_chalk16.default.green(`${counts.pass} passed`),
|
|
10563
|
+
counts.warn > 0 ? import_chalk16.default.yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`) : null,
|
|
10564
|
+
counts.fail > 0 ? import_chalk16.default.red(`${counts.fail} failed`) : null,
|
|
10565
|
+
counts.skip > 0 ? import_chalk16.default.gray(`${counts.skip} skipped`) : null
|
|
10566
|
+
].filter((part) => part !== null);
|
|
10567
|
+
console.log("\n " + parts.join(import_chalk16.default.gray(", ")));
|
|
10568
|
+
if (counts.fail > 0) {
|
|
10569
|
+
console.log(import_chalk16.default.red.bold(" Environment has problems \u2014 fix the failures above, then re-run `prowl doctor`.\n"));
|
|
10570
|
+
} else if (counts.warn > 0) {
|
|
10571
|
+
console.log(import_chalk16.default.yellow.bold(" Environment is usable, with warnings.\n"));
|
|
10572
|
+
} else {
|
|
10573
|
+
console.log(import_chalk16.default.green.bold(" Environment is healthy.\n"));
|
|
10574
|
+
}
|
|
10575
|
+
}
|
|
10576
|
+
function buildDoctorCommand(deps = defaultDoctorDeps()) {
|
|
10577
|
+
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) => {
|
|
10578
|
+
try {
|
|
10579
|
+
const report = await runDoctor(deps, { fix: options.fix });
|
|
10580
|
+
renderReport(report, Boolean(options.fix));
|
|
10581
|
+
if (!report.ok) {
|
|
10582
|
+
process.exitCode = 1;
|
|
10583
|
+
}
|
|
10584
|
+
} catch (error) {
|
|
10585
|
+
const message = error instanceof Error ? error.message : "doctor failed";
|
|
10586
|
+
console.error(import_chalk16.default.red(`Error: ${message}
|
|
10587
|
+
Re-run \`prowl doctor\`; if it fails again, report the error.`));
|
|
10588
|
+
process.exitCode = 1;
|
|
10589
|
+
}
|
|
10590
|
+
});
|
|
10591
|
+
}
|
|
10592
|
+
|
|
10011
10593
|
// src/cli/program.ts
|
|
10012
10594
|
var CLI_VERSION = package_default.version;
|
|
10013
10595
|
function buildProgram() {
|
|
10014
|
-
const program2 = new
|
|
10596
|
+
const program2 = new import_commander15.Command();
|
|
10015
10597
|
program2.name("prowl").description("CLI-first QA testing tool for deterministic Playwright flows").version(CLI_VERSION);
|
|
10016
10598
|
program2.addCommand(buildRunCommand());
|
|
10017
10599
|
program2.addCommand(buildCiCommand());
|
|
@@ -10026,6 +10608,7 @@ function buildProgram() {
|
|
|
10026
10608
|
program2.addCommand(buildFlakyCommand());
|
|
10027
10609
|
program2.addCommand(buildMcpCommand());
|
|
10028
10610
|
program2.addCommand(buildMacdriverCommand());
|
|
10611
|
+
program2.addCommand(buildDoctorCommand());
|
|
10029
10612
|
return program2;
|
|
10030
10613
|
}
|
|
10031
10614
|
|