prowl-tools 0.1.7 → 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 +56 -7
- package/dist/{chunk-R7NUH44M.js → chunk-2RNOK64Y.js} +314 -399
- package/dist/chunk-2RNOK64Y.js.map +1 -0
- package/dist/{chunk-O3OUTZ2P.js → chunk-JFJQNJSJ.js} +394 -2
- package/dist/chunk-JFJQNJSJ.js.map +1 -0
- package/dist/index.cjs +1564 -692
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +597 -40
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +650 -346
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +95 -17
- package/dist/lib.d.ts +95 -17
- package/dist/lib.js +3 -3
- package/dist/{loader-X37URHUV.js → loader-JTHA4BYG.js} +2 -2
- package/examples/hunts/form.yml +53 -0
- package/examples/hunts/macos-hello.yml +49 -0
- package/package.json +1 -1
- package/dist/chunk-O3OUTZ2P.js.map +0 -1
- package/dist/chunk-R7NUH44M.js.map +0 -1
- /package/dist/{loader-X37URHUV.js.map → loader-JTHA4BYG.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
saveStorageState,
|
|
29
29
|
tccGuidance,
|
|
30
30
|
updateBacklogFromSuite
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-2RNOK64Y.js";
|
|
32
32
|
import {
|
|
33
33
|
CONFIG_DIR,
|
|
34
34
|
findConfigPath,
|
|
@@ -38,15 +38,15 @@ import {
|
|
|
38
38
|
loadHuntTags,
|
|
39
39
|
normalizeHuntName,
|
|
40
40
|
resolveViewport
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-JFJQNJSJ.js";
|
|
42
42
|
|
|
43
43
|
// src/cli/program.ts
|
|
44
|
-
import { Command as
|
|
44
|
+
import { Command as Command15 } from "commander";
|
|
45
45
|
|
|
46
46
|
// package.json
|
|
47
47
|
var package_default = {
|
|
48
48
|
name: "prowl-tools",
|
|
49
|
-
version: "0.1.
|
|
49
|
+
version: "0.1.9",
|
|
50
50
|
description: "E2E testing for native macOS apps and web apps from declarative YAML hunts.",
|
|
51
51
|
type: "module",
|
|
52
52
|
license: "Apache-2.0",
|
|
@@ -354,6 +354,7 @@ function buildRunCommand() {
|
|
|
354
354
|
|
|
355
355
|
// src/cli/commands/init.ts
|
|
356
356
|
import fs from "fs";
|
|
357
|
+
import os from "os";
|
|
357
358
|
import path from "path";
|
|
358
359
|
import { fileURLToPath } from "url";
|
|
359
360
|
import { Command as Command2 } from "commander";
|
|
@@ -377,6 +378,192 @@ function copyFile(source, destination) {
|
|
|
377
378
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
378
379
|
fs.copyFileSync(source, destination);
|
|
379
380
|
}
|
|
381
|
+
function isInsideDir(parent, child) {
|
|
382
|
+
const relative = path.relative(parent, child);
|
|
383
|
+
return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
384
|
+
}
|
|
385
|
+
function lstatIfExists(target) {
|
|
386
|
+
try {
|
|
387
|
+
return fs.lstatSync(target);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const code = error.code;
|
|
390
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function errorMessage(error) {
|
|
397
|
+
return error instanceof Error ? error.message : String(error);
|
|
398
|
+
}
|
|
399
|
+
function assertNoSymlinkDestination(prowlDir, destination) {
|
|
400
|
+
const relative = path.relative(prowlDir, destination);
|
|
401
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
402
|
+
throw new Error(`Refusing to write outside ${CONFIG_DIR}: ${destination}`);
|
|
403
|
+
}
|
|
404
|
+
const parts = relative.split(path.sep).filter((part) => part.length > 0);
|
|
405
|
+
let current = prowlDir;
|
|
406
|
+
const rootStat = lstatIfExists(current);
|
|
407
|
+
if (rootStat?.isSymbolicLink()) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`${CONFIG_DIR} scaffold destination path contains a symlink: ${current}. Replace it with a real file or directory before running prowl init --force.`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (rootStat && !rootStat.isDirectory()) {
|
|
413
|
+
throw new Error(`${CONFIG_DIR} scaffold path is not a directory: ${current}`);
|
|
414
|
+
}
|
|
415
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
416
|
+
current = path.join(current, parts[index] ?? "");
|
|
417
|
+
const stat = lstatIfExists(current);
|
|
418
|
+
if (stat?.isSymbolicLink()) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`${CONFIG_DIR} scaffold destination path contains a symlink: ${current}. Replace it with a real file or directory before running prowl init --force.`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (stat && index < parts.length - 1 && !stat.isDirectory()) {
|
|
424
|
+
throw new Error(`${CONFIG_DIR} scaffold parent path is not a directory: ${current}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function gitignoreTemplate() {
|
|
429
|
+
return [
|
|
430
|
+
"# Run artifacts (screenshots, logs, reports)",
|
|
431
|
+
"runs/",
|
|
432
|
+
"",
|
|
433
|
+
"# Auth state (tokens, cookies)",
|
|
434
|
+
"auth-state.json",
|
|
435
|
+
"",
|
|
436
|
+
"# Environment variables (credentials)",
|
|
437
|
+
".env",
|
|
438
|
+
""
|
|
439
|
+
].join("\n");
|
|
440
|
+
}
|
|
441
|
+
function stageScaffoldTemplates(examplesDir) {
|
|
442
|
+
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), "prowl-init-"));
|
|
443
|
+
const files = [];
|
|
444
|
+
try {
|
|
445
|
+
copyFile(path.join(examplesDir, "config.yml"), path.join(stageDir, "config.yml"));
|
|
446
|
+
files.push({ source: path.join(stageDir, "config.yml"), relativePath: "config.yml" });
|
|
447
|
+
const exampleHuntsDir = path.join(examplesDir, "hunts");
|
|
448
|
+
const huntFiles = fs.readdirSync(exampleHuntsDir).filter((f) => f.endsWith(".yml"));
|
|
449
|
+
for (const huntFile of huntFiles) {
|
|
450
|
+
const relativePath = path.join("hunts", huntFile);
|
|
451
|
+
copyFile(path.join(exampleHuntsDir, huntFile), path.join(stageDir, relativePath));
|
|
452
|
+
files.push({ source: path.join(stageDir, relativePath), relativePath });
|
|
453
|
+
}
|
|
454
|
+
const gitignorePath = path.join(stageDir, ".gitignore");
|
|
455
|
+
fs.writeFileSync(gitignorePath, gitignoreTemplate());
|
|
456
|
+
files.push({ source: gitignorePath, relativePath: ".gitignore" });
|
|
457
|
+
return { stageDir, files };
|
|
458
|
+
} catch (error) {
|
|
459
|
+
fs.rmSync(stageDir, { recursive: true, force: true });
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function trackCreatedParentDirs(destination, prowlDir, createdDirs) {
|
|
464
|
+
let dir = path.dirname(destination);
|
|
465
|
+
while (isInsideDir(prowlDir, dir) && !fs.existsSync(dir)) {
|
|
466
|
+
createdDirs.add(dir);
|
|
467
|
+
dir = path.dirname(dir);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function prepareDestinationRollback(prowlDir, files) {
|
|
471
|
+
const backupDir = fs.mkdtempSync(path.join(os.tmpdir(), "prowl-init-rollback-"));
|
|
472
|
+
const backups = [];
|
|
473
|
+
const createdFiles = [];
|
|
474
|
+
const createdDirs = /* @__PURE__ */ new Set();
|
|
475
|
+
const prowlDirExisted = fs.existsSync(prowlDir);
|
|
476
|
+
try {
|
|
477
|
+
for (const file of files) {
|
|
478
|
+
const destination = path.join(prowlDir, file.relativePath);
|
|
479
|
+
assertNoSymlinkDestination(prowlDir, destination);
|
|
480
|
+
const stat = lstatIfExists(destination);
|
|
481
|
+
if (stat) {
|
|
482
|
+
if (!stat.isFile()) {
|
|
483
|
+
throw new Error(`${CONFIG_DIR} scaffold destination is not a regular file: ${destination}`);
|
|
484
|
+
}
|
|
485
|
+
const backup = path.join(backupDir, file.relativePath);
|
|
486
|
+
copyFile(destination, backup);
|
|
487
|
+
backups.push({ destination, backup });
|
|
488
|
+
} else {
|
|
489
|
+
createdFiles.push(destination);
|
|
490
|
+
}
|
|
491
|
+
if (prowlDirExisted) {
|
|
492
|
+
trackCreatedParentDirs(destination, prowlDir, createdDirs);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
prowlDirExisted,
|
|
497
|
+
backupDir,
|
|
498
|
+
backups,
|
|
499
|
+
createdFiles,
|
|
500
|
+
createdDirs: [...createdDirs].sort((a, b) => b.length - a.length)
|
|
501
|
+
};
|
|
502
|
+
} catch (error) {
|
|
503
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function rollbackDestination(prowlDir, rollback) {
|
|
508
|
+
if (!rollback.prowlDirExisted) {
|
|
509
|
+
fs.rmSync(prowlDir, { recursive: true, force: true });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
for (const file of rollback.createdFiles) {
|
|
513
|
+
fs.rmSync(file, { force: true });
|
|
514
|
+
}
|
|
515
|
+
for (const backup of rollback.backups) {
|
|
516
|
+
try {
|
|
517
|
+
copyFile(backup.backup, backup.destination);
|
|
518
|
+
} catch (error) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`Failed to restore ${backup.destination} from backup ${backup.backup}: ${errorMessage(error)}`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
for (const dir of rollback.createdDirs) {
|
|
525
|
+
try {
|
|
526
|
+
fs.rmdirSync(dir);
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function scaffoldProwlDir(root) {
|
|
532
|
+
const prowlDir = path.join(root, CONFIG_DIR);
|
|
533
|
+
const packageRoot = getPackageRoot();
|
|
534
|
+
const examplesDir = path.join(packageRoot, "examples");
|
|
535
|
+
const exampleConfig = path.join(examplesDir, "config.yml");
|
|
536
|
+
const exampleHuntsDir = path.join(examplesDir, "hunts");
|
|
537
|
+
if (!fs.existsSync(exampleConfig) || !fs.existsSync(exampleHuntsDir)) {
|
|
538
|
+
throw new Error("Examples not found in package. Reinstall prowl-tools.");
|
|
539
|
+
}
|
|
540
|
+
const staged = stageScaffoldTemplates(examplesDir);
|
|
541
|
+
let rollback = null;
|
|
542
|
+
let keepBackup = false;
|
|
543
|
+
try {
|
|
544
|
+
rollback = prepareDestinationRollback(prowlDir, staged.files);
|
|
545
|
+
for (const file of staged.files) {
|
|
546
|
+
copyFile(file.source, path.join(prowlDir, file.relativePath));
|
|
547
|
+
}
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (rollback) {
|
|
550
|
+
try {
|
|
551
|
+
rollbackDestination(prowlDir, rollback);
|
|
552
|
+
} catch (rollbackError) {
|
|
553
|
+
keepBackup = true;
|
|
554
|
+
throw new Error(
|
|
555
|
+
`Scaffold failed: ${errorMessage(error)}. Rollback failed: ${errorMessage(rollbackError)}. Backup preserved at ${rollback.backupDir}.`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
} finally {
|
|
561
|
+
if (rollback && !keepBackup) {
|
|
562
|
+
fs.rmSync(rollback.backupDir, { recursive: true, force: true });
|
|
563
|
+
}
|
|
564
|
+
fs.rmSync(staged.stageDir, { recursive: true, force: true });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
380
567
|
function buildInitCommand() {
|
|
381
568
|
const command = new Command2("init").option("--force", `Overwrite existing ${CONFIG_DIR} directory`).action((options) => {
|
|
382
569
|
const root = process.cwd();
|
|
@@ -390,39 +577,18 @@ function buildInitCommand() {
|
|
|
390
577
|
process.exitCode = 1;
|
|
391
578
|
return;
|
|
392
579
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (!fs.existsSync(exampleConfig) || !fs.existsSync(exampleHuntsDir)) {
|
|
398
|
-
console.error(chalk4.red("Examples not found in package. Reinstall prowl-tools."));
|
|
580
|
+
try {
|
|
581
|
+
scaffoldProwlDir(root);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
console.error(chalk4.red(error instanceof Error ? error.message : "init failed"));
|
|
399
584
|
process.exitCode = 1;
|
|
400
585
|
return;
|
|
401
586
|
}
|
|
402
|
-
copyFile(exampleConfig, path.join(prowlDir, "config.yml"));
|
|
403
|
-
const huntFiles = fs.readdirSync(exampleHuntsDir).filter((f) => f.endsWith(".yml"));
|
|
404
|
-
for (const huntFile of huntFiles) {
|
|
405
|
-
copyFile(
|
|
406
|
-
path.join(exampleHuntsDir, huntFile),
|
|
407
|
-
path.join(prowlDir, "hunts", huntFile)
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
const gitignore = [
|
|
411
|
-
"# Run artifacts (screenshots, logs, reports)",
|
|
412
|
-
"runs/",
|
|
413
|
-
"",
|
|
414
|
-
"# Auth state (tokens, cookies)",
|
|
415
|
-
"auth-state.json",
|
|
416
|
-
"",
|
|
417
|
-
"# Environment variables (credentials)",
|
|
418
|
-
".env",
|
|
419
|
-
""
|
|
420
|
-
].join("\n");
|
|
421
|
-
fs.writeFileSync(path.join(prowlDir, ".gitignore"), gitignore);
|
|
422
587
|
console.log(welcomeBanner());
|
|
423
588
|
console.log(chalk4.green(` Initialized ${CONFIG_DIR} directory.`));
|
|
424
589
|
console.log(chalk4.gray(" Run ") + chalk4.bold("prowl run hello") + chalk4.gray(" to get started."));
|
|
425
|
-
console.log(chalk4.gray(" See ") + chalk4.cyan(`${CONFIG_DIR}/hunts/login-flow.yml`) + chalk4.gray("
|
|
590
|
+
console.log(chalk4.gray(" See ") + chalk4.cyan(`${CONFIG_DIR}/hunts/login-flow.yml`) + chalk4.gray(" (auth) and ") + chalk4.cyan(`${CONFIG_DIR}/hunts/form.yml`) + chalk4.gray(" (web forms) for fuller examples."));
|
|
591
|
+
console.log(chalk4.gray(" Desktop-first? ") + chalk4.cyan(`${CONFIG_DIR}/hunts/macos-hello.yml`) + chalk4.gray(" is a macOS starter (experimental \u2014 see its comments to enable).") + "\n");
|
|
426
592
|
});
|
|
427
593
|
return command;
|
|
428
594
|
}
|
|
@@ -1194,7 +1360,7 @@ function buildGenerateCommand() {
|
|
|
1194
1360
|
} else if (options.output) {
|
|
1195
1361
|
let configDir;
|
|
1196
1362
|
try {
|
|
1197
|
-
const { loadConfig: loadConfig2 } = await import("./loader-
|
|
1363
|
+
const { loadConfig: loadConfig2 } = await import("./loader-JTHA4BYG.js");
|
|
1198
1364
|
const result = loadConfig2(options.config);
|
|
1199
1365
|
configDir = result.configDir;
|
|
1200
1366
|
} catch {
|
|
@@ -1448,7 +1614,7 @@ async function runHuntTool(args, configPath) {
|
|
|
1448
1614
|
|
|
1449
1615
|
// src/mcp/projects.ts
|
|
1450
1616
|
import fs5 from "fs";
|
|
1451
|
-
import
|
|
1617
|
+
import os2 from "os";
|
|
1452
1618
|
import path7 from "path";
|
|
1453
1619
|
import yaml from "yaml";
|
|
1454
1620
|
import { z } from "zod";
|
|
@@ -1461,10 +1627,10 @@ var projectRegistrySchema = z.object({
|
|
|
1461
1627
|
projects: z.record(z.string().min(1), projectEntrySchema)
|
|
1462
1628
|
}).strict();
|
|
1463
1629
|
function defaultRegistryPath() {
|
|
1464
|
-
return path7.join(
|
|
1630
|
+
return path7.join(os2.homedir(), ".prowl", "projects.yml");
|
|
1465
1631
|
}
|
|
1466
1632
|
function legacyRegistryPath() {
|
|
1467
|
-
return path7.join(
|
|
1633
|
+
return path7.join(os2.homedir(), ".prowlqa", "projects.yml");
|
|
1468
1634
|
}
|
|
1469
1635
|
function resolveProjectConfigPath(root) {
|
|
1470
1636
|
const preferred = path7.join(root, ".prowl", "config.yml");
|
|
@@ -1518,7 +1684,7 @@ function listRegisteredProjects(registry) {
|
|
|
1518
1684
|
function textResult(data) {
|
|
1519
1685
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1520
1686
|
}
|
|
1521
|
-
function
|
|
1687
|
+
function errorMessage2(error) {
|
|
1522
1688
|
return error instanceof Error ? error.message : String(error);
|
|
1523
1689
|
}
|
|
1524
1690
|
function safeStringify(value) {
|
|
@@ -1532,7 +1698,7 @@ async function toolResult(toolName, args, action) {
|
|
|
1532
1698
|
try {
|
|
1533
1699
|
return textResult(await action());
|
|
1534
1700
|
} catch (error) {
|
|
1535
|
-
throw new Error(`${toolName} failed for args=${safeStringify(args)}: ${
|
|
1701
|
+
throw new Error(`${toolName} failed for args=${safeStringify(args)}: ${errorMessage2(error)}`, {
|
|
1536
1702
|
cause: error
|
|
1537
1703
|
});
|
|
1538
1704
|
}
|
|
@@ -1609,7 +1775,7 @@ async function startMcpServer(options = {}) {
|
|
|
1609
1775
|
}
|
|
1610
1776
|
|
|
1611
1777
|
// src/cli/commands/mcp.ts
|
|
1612
|
-
function
|
|
1778
|
+
function errorMessage3(error) {
|
|
1613
1779
|
return error instanceof Error ? error.message : String(error);
|
|
1614
1780
|
}
|
|
1615
1781
|
function buildMcpCommand() {
|
|
@@ -1620,7 +1786,7 @@ function buildMcpCommand() {
|
|
|
1620
1786
|
try {
|
|
1621
1787
|
await startMcpServer({ registryPath: options.projects });
|
|
1622
1788
|
} catch (error) {
|
|
1623
|
-
console.error(`Failed to start MCP server: ${
|
|
1789
|
+
console.error(`Failed to start MCP server: ${errorMessage3(error)}`);
|
|
1624
1790
|
process.exit(1);
|
|
1625
1791
|
}
|
|
1626
1792
|
});
|
|
@@ -1699,10 +1865,400 @@ function buildMacdriverCommand() {
|
|
|
1699
1865
|
return command;
|
|
1700
1866
|
}
|
|
1701
1867
|
|
|
1868
|
+
// src/cli/commands/doctor.ts
|
|
1869
|
+
import chalk15 from "chalk";
|
|
1870
|
+
import { Command as Command14 } from "commander";
|
|
1871
|
+
|
|
1872
|
+
// src/doctor/checks.ts
|
|
1873
|
+
import { execFile } from "child_process";
|
|
1874
|
+
import { createRequire } from "module";
|
|
1875
|
+
import fs6 from "fs";
|
|
1876
|
+
import path8 from "path";
|
|
1877
|
+
import { promisify } from "util";
|
|
1878
|
+
var execFileAsync = promisify(execFile);
|
|
1879
|
+
var MIN_NODE_MAJOR = 20;
|
|
1880
|
+
var CHECK_NODE = "Node.js version";
|
|
1881
|
+
var CHECK_PLAYWRIGHT = "Playwright installed";
|
|
1882
|
+
var CHECK_CHROMIUM = "Chromium browser";
|
|
1883
|
+
var CHECK_PROWL_DIR = ".prowl/ directory";
|
|
1884
|
+
var CHECK_CONFIG = "config.yml valid";
|
|
1885
|
+
var CHECK_MACOS = "macOS helper (prowl-macdriver)";
|
|
1886
|
+
var CHECK_IOS = "iOS tooling (xcrun simctl)";
|
|
1887
|
+
var CHECK_ANDROID = "Android tooling (adb)";
|
|
1888
|
+
var INSTALL_CHROMIUM_TIMEOUT_MS = 3e5;
|
|
1889
|
+
function nodeMajor(version) {
|
|
1890
|
+
const cleaned = version.startsWith("v") ? version.slice(1) : version;
|
|
1891
|
+
return Number.parseInt(cleaned.split(".")[0] ?? "", 10);
|
|
1892
|
+
}
|
|
1893
|
+
function checkNode(nodeVersion) {
|
|
1894
|
+
const major = nodeMajor(nodeVersion);
|
|
1895
|
+
if (Number.isFinite(major) && major >= MIN_NODE_MAJOR) {
|
|
1896
|
+
return {
|
|
1897
|
+
name: CHECK_NODE,
|
|
1898
|
+
status: "pass",
|
|
1899
|
+
message: `Node ${nodeVersion} (>= ${MIN_NODE_MAJOR} required)`,
|
|
1900
|
+
fixable: false
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
return {
|
|
1904
|
+
name: CHECK_NODE,
|
|
1905
|
+
status: "fail",
|
|
1906
|
+
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.`,
|
|
1907
|
+
fixable: false
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function checkPlaywright(probe) {
|
|
1911
|
+
if (probe.playwrightInstalled) {
|
|
1912
|
+
return { name: CHECK_PLAYWRIGHT, status: "pass", message: "Playwright is installed", fixable: false };
|
|
1913
|
+
}
|
|
1914
|
+
return {
|
|
1915
|
+
name: CHECK_PLAYWRIGHT,
|
|
1916
|
+
status: "fail",
|
|
1917
|
+
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).",
|
|
1918
|
+
fixable: false
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
function checkChromium(probe) {
|
|
1922
|
+
if (!probe.playwrightInstalled) {
|
|
1923
|
+
return {
|
|
1924
|
+
name: CHECK_CHROMIUM,
|
|
1925
|
+
status: "skip",
|
|
1926
|
+
message: "Skipped \u2014 Playwright is not installed",
|
|
1927
|
+
fixable: false
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
if (probe.chromiumInstalled) {
|
|
1931
|
+
return {
|
|
1932
|
+
name: CHECK_CHROMIUM,
|
|
1933
|
+
status: "pass",
|
|
1934
|
+
message: `Chromium is available${probe.chromiumPath ? ` (${probe.chromiumPath})` : ""}`,
|
|
1935
|
+
fixable: false
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
return {
|
|
1939
|
+
name: CHECK_CHROMIUM,
|
|
1940
|
+
status: "fail",
|
|
1941
|
+
message: "Chromium is not installed. Run `npx playwright install chromium` (or `prowl doctor --fix`).",
|
|
1942
|
+
fixable: true
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
function checkProwlDir(exists) {
|
|
1946
|
+
if (exists) {
|
|
1947
|
+
return { name: CHECK_PROWL_DIR, status: "pass", message: `${CONFIG_DIR}/ exists`, fixable: false };
|
|
1948
|
+
}
|
|
1949
|
+
return {
|
|
1950
|
+
name: CHECK_PROWL_DIR,
|
|
1951
|
+
status: "warn",
|
|
1952
|
+
message: `No ${CONFIG_DIR}/ directory here. Run \`prowl init\` (or \`prowl doctor --fix\`) to scaffold config and starter hunts.`,
|
|
1953
|
+
fixable: true
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
function checkConfig(prowlDirExists, loadConfig2) {
|
|
1957
|
+
if (!prowlDirExists) {
|
|
1958
|
+
return {
|
|
1959
|
+
result: {
|
|
1960
|
+
name: CHECK_CONFIG,
|
|
1961
|
+
status: "skip",
|
|
1962
|
+
message: `Skipped \u2014 no ${CONFIG_DIR}/ directory`,
|
|
1963
|
+
fixable: false
|
|
1964
|
+
},
|
|
1965
|
+
config: null
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
try {
|
|
1969
|
+
const { config } = loadConfig2();
|
|
1970
|
+
return {
|
|
1971
|
+
result: {
|
|
1972
|
+
name: CHECK_CONFIG,
|
|
1973
|
+
status: "pass",
|
|
1974
|
+
message: `Valid (target: ${config.target.type})`,
|
|
1975
|
+
fixable: false
|
|
1976
|
+
},
|
|
1977
|
+
config
|
|
1978
|
+
};
|
|
1979
|
+
} catch (error) {
|
|
1980
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1981
|
+
return {
|
|
1982
|
+
result: {
|
|
1983
|
+
name: CHECK_CONFIG,
|
|
1984
|
+
status: "fail",
|
|
1985
|
+
message: `Invalid config: ${detail}`,
|
|
1986
|
+
fixable: false
|
|
1987
|
+
},
|
|
1988
|
+
config: null
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
function macdriverSourceLabel(source) {
|
|
1993
|
+
switch (source) {
|
|
1994
|
+
case "env":
|
|
1995
|
+
return "PROWL_MACDRIVER_BIN override";
|
|
1996
|
+
case "user-install":
|
|
1997
|
+
return "user install (~/.prowl/macdriver)";
|
|
1998
|
+
case "source-build":
|
|
1999
|
+
return "repo source build";
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
async function checkMacTarget(collect, env) {
|
|
2003
|
+
let status;
|
|
2004
|
+
try {
|
|
2005
|
+
status = await collect({ env });
|
|
2006
|
+
} catch (error) {
|
|
2007
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2008
|
+
return {
|
|
2009
|
+
name: CHECK_MACOS,
|
|
2010
|
+
status: "fail",
|
|
2011
|
+
message: `Could not inspect prowl-macdriver status (${detail}). Run \`prowl macdriver status\` for details.`,
|
|
2012
|
+
fixable: false
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
if (status.resolved && status.probedVersion) {
|
|
2016
|
+
return {
|
|
2017
|
+
name: CHECK_MACOS,
|
|
2018
|
+
status: "pass",
|
|
2019
|
+
message: `Resolved via ${macdriverSourceLabel(status.resolved.source)}: ${status.resolved.path} (reports ${status.probedVersion}). Verify Accessibility/Screen Recording permissions with \`prowl macdriver status\`.`,
|
|
2020
|
+
fixable: false
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
if (status.resolved) {
|
|
2024
|
+
return {
|
|
2025
|
+
name: CHECK_MACOS,
|
|
2026
|
+
status: "fail",
|
|
2027
|
+
message: `prowl-macdriver resolved via ${macdriverSourceLabel(status.resolved.source)} (${status.resolved.path}), but the helper version probe failed. Run \`prowl macdriver status\` for details.`,
|
|
2028
|
+
fixable: false
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
return {
|
|
2032
|
+
name: CHECK_MACOS,
|
|
2033
|
+
status: "fail",
|
|
2034
|
+
message: "prowl-macdriver helper not found. Install it with `prowl macdriver install`, then check permissions with `prowl macdriver status`.",
|
|
2035
|
+
fixable: false
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
async function checkIosTarget(platform, run) {
|
|
2039
|
+
if (platform !== "darwin") {
|
|
2040
|
+
return {
|
|
2041
|
+
name: CHECK_IOS,
|
|
2042
|
+
status: "fail",
|
|
2043
|
+
message: `The iOS target requires macOS; xcrun simctl is unavailable on ${platform}.`,
|
|
2044
|
+
fixable: false
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
try {
|
|
2048
|
+
await run("xcrun", ["simctl", "help"]);
|
|
2049
|
+
return { name: CHECK_IOS, status: "pass", message: "xcrun simctl is reachable", fixable: false };
|
|
2050
|
+
} catch (error) {
|
|
2051
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2052
|
+
return {
|
|
2053
|
+
name: CHECK_IOS,
|
|
2054
|
+
status: "fail",
|
|
2055
|
+
message: `xcrun simctl is not reachable (${detail}). Install Xcode and its command-line tools (\`xcode-select --install\`), then open Xcode once to finish setup.`,
|
|
2056
|
+
fixable: false
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
async function checkAndroidTarget(run) {
|
|
2061
|
+
try {
|
|
2062
|
+
await run("adb", ["version"]);
|
|
2063
|
+
return { name: CHECK_ANDROID, status: "pass", message: "adb is reachable", fixable: false };
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2066
|
+
return {
|
|
2067
|
+
name: CHECK_ANDROID,
|
|
2068
|
+
status: "fail",
|
|
2069
|
+
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).`,
|
|
2070
|
+
fixable: false
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
async function runDoctorChecks(deps) {
|
|
2075
|
+
const results = [];
|
|
2076
|
+
const probe = await deps.probePlaywright();
|
|
2077
|
+
results.push(checkNode(deps.nodeVersion));
|
|
2078
|
+
results.push(checkPlaywright(probe));
|
|
2079
|
+
results.push(checkChromium(probe));
|
|
2080
|
+
const prowlExists = deps.prowlDirExists(deps.cwd);
|
|
2081
|
+
results.push(checkProwlDir(prowlExists));
|
|
2082
|
+
const { result: configResult, config } = checkConfig(prowlExists, deps.loadConfig);
|
|
2083
|
+
results.push(configResult);
|
|
2084
|
+
if (config) {
|
|
2085
|
+
switch (config.target.type) {
|
|
2086
|
+
case "macos":
|
|
2087
|
+
results.push(await checkMacTarget(deps.collectMacdriverStatus, deps.env));
|
|
2088
|
+
break;
|
|
2089
|
+
case "ios":
|
|
2090
|
+
results.push(await checkIosTarget(deps.platform, deps.runCommand));
|
|
2091
|
+
break;
|
|
2092
|
+
case "android":
|
|
2093
|
+
results.push(await checkAndroidTarget(deps.runCommand));
|
|
2094
|
+
break;
|
|
2095
|
+
default:
|
|
2096
|
+
break;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return results;
|
|
2100
|
+
}
|
|
2101
|
+
async function runDoctor(deps, options = {}) {
|
|
2102
|
+
let results = await runDoctorChecks(deps);
|
|
2103
|
+
const fixes = [];
|
|
2104
|
+
if (options.fix) {
|
|
2105
|
+
for (const result of results) {
|
|
2106
|
+
if (!result.fixable || result.status === "pass" || result.status === "skip") {
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
try {
|
|
2110
|
+
if (result.name === CHECK_CHROMIUM) {
|
|
2111
|
+
await deps.installChromium();
|
|
2112
|
+
fixes.push({ name: result.name });
|
|
2113
|
+
} else if (result.name === CHECK_PROWL_DIR) {
|
|
2114
|
+
deps.scaffoldProwlDir(deps.cwd);
|
|
2115
|
+
fixes.push({ name: result.name });
|
|
2116
|
+
}
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
fixes.push({
|
|
2119
|
+
name: result.name,
|
|
2120
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
if (fixes.length > 0) {
|
|
2125
|
+
results = await runDoctorChecks(deps);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
const ok = !results.some((result) => result.status === "fail");
|
|
2129
|
+
return { results, fixes, ok };
|
|
2130
|
+
}
|
|
2131
|
+
async function defaultProbePlaywright() {
|
|
2132
|
+
try {
|
|
2133
|
+
const pw = await import("playwright");
|
|
2134
|
+
let chromiumPath = null;
|
|
2135
|
+
try {
|
|
2136
|
+
chromiumPath = pw.chromium.executablePath();
|
|
2137
|
+
} catch {
|
|
2138
|
+
chromiumPath = null;
|
|
2139
|
+
}
|
|
2140
|
+
const chromiumInstalled = chromiumPath ? fs6.existsSync(chromiumPath) : false;
|
|
2141
|
+
return { playwrightInstalled: true, chromiumPath, chromiumInstalled };
|
|
2142
|
+
} catch {
|
|
2143
|
+
return { playwrightInstalled: false, chromiumPath: null, chromiumInstalled: false };
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
var defaultRunCommand = async (file, args) => execFileAsync(file, args, { timeout: 5e3 });
|
|
2147
|
+
function createLocalRequire() {
|
|
2148
|
+
if (typeof __filename === "string") {
|
|
2149
|
+
return createRequire(__filename);
|
|
2150
|
+
}
|
|
2151
|
+
return createRequire(import.meta.url);
|
|
2152
|
+
}
|
|
2153
|
+
function resolvePlaywrightCliPath(requireFn = createLocalRequire()) {
|
|
2154
|
+
return path8.join(path8.dirname(requireFn.resolve("playwright/package.json")), "cli.js");
|
|
2155
|
+
}
|
|
2156
|
+
function playwrightInstallChromiumCommand() {
|
|
2157
|
+
return {
|
|
2158
|
+
file: process.execPath,
|
|
2159
|
+
args: [resolvePlaywrightCliPath(), "install", "chromium"]
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
async function defaultInstallChromium() {
|
|
2163
|
+
const command = playwrightInstallChromiumCommand();
|
|
2164
|
+
await execFileAsync(command.file, command.args, { timeout: INSTALL_CHROMIUM_TIMEOUT_MS });
|
|
2165
|
+
}
|
|
2166
|
+
function defaultDoctorDeps() {
|
|
2167
|
+
return {
|
|
2168
|
+
nodeVersion: process.versions.node,
|
|
2169
|
+
platform: process.platform,
|
|
2170
|
+
cwd: process.cwd(),
|
|
2171
|
+
env: process.env,
|
|
2172
|
+
probePlaywright: defaultProbePlaywright,
|
|
2173
|
+
runCommand: defaultRunCommand,
|
|
2174
|
+
prowlDirExists: (cwd) => fs6.existsSync(path8.join(cwd, CONFIG_DIR)),
|
|
2175
|
+
loadConfig: () => loadConfig(),
|
|
2176
|
+
collectMacdriverStatus,
|
|
2177
|
+
installChromium: defaultInstallChromium,
|
|
2178
|
+
scaffoldProwlDir
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/cli/commands/doctor.ts
|
|
2183
|
+
function symbol(status) {
|
|
2184
|
+
switch (status) {
|
|
2185
|
+
case "pass":
|
|
2186
|
+
return chalk15.green("\u2713");
|
|
2187
|
+
// ✓
|
|
2188
|
+
case "fail":
|
|
2189
|
+
return chalk15.red("\u2717");
|
|
2190
|
+
// ✗
|
|
2191
|
+
case "warn":
|
|
2192
|
+
return chalk15.yellow("\u26A0");
|
|
2193
|
+
// ⚠
|
|
2194
|
+
case "skip":
|
|
2195
|
+
return chalk15.gray("\u25CB");
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
function renderResult(result) {
|
|
2199
|
+
console.log(` ${symbol(result.status)} ${chalk15.bold(result.name)}`);
|
|
2200
|
+
console.log(` ${chalk15.gray(result.message)}`);
|
|
2201
|
+
}
|
|
2202
|
+
function renderReport(report, fixRequested) {
|
|
2203
|
+
console.log(chalk15.bold("\nprowl doctor\n"));
|
|
2204
|
+
for (const result of report.results) {
|
|
2205
|
+
renderResult(result);
|
|
2206
|
+
}
|
|
2207
|
+
if (fixRequested) {
|
|
2208
|
+
console.log("");
|
|
2209
|
+
if (report.fixes.length === 0) {
|
|
2210
|
+
console.log(chalk15.gray(" No auto-fixable issues to repair."));
|
|
2211
|
+
} else {
|
|
2212
|
+
console.log(chalk15.bold(" Attempted fixes:"));
|
|
2213
|
+
for (const fix of report.fixes) {
|
|
2214
|
+
if (fix.error) {
|
|
2215
|
+
console.log(` ${chalk15.red("\u2717")} ${fix.name} \u2014 ${chalk15.gray(fix.error)}`);
|
|
2216
|
+
} else {
|
|
2217
|
+
console.log(` ${chalk15.green("\u2713")} ${fix.name}`);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
const counts = { pass: 0, warn: 0, fail: 0, skip: 0 };
|
|
2223
|
+
for (const result of report.results) {
|
|
2224
|
+
counts[result.status] += 1;
|
|
2225
|
+
}
|
|
2226
|
+
const parts = [
|
|
2227
|
+
chalk15.green(`${counts.pass} passed`),
|
|
2228
|
+
counts.warn > 0 ? chalk15.yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`) : null,
|
|
2229
|
+
counts.fail > 0 ? chalk15.red(`${counts.fail} failed`) : null,
|
|
2230
|
+
counts.skip > 0 ? chalk15.gray(`${counts.skip} skipped`) : null
|
|
2231
|
+
].filter((part) => part !== null);
|
|
2232
|
+
console.log("\n " + parts.join(chalk15.gray(", ")));
|
|
2233
|
+
if (counts.fail > 0) {
|
|
2234
|
+
console.log(chalk15.red.bold(" Environment has problems \u2014 fix the failures above, then re-run `prowl doctor`.\n"));
|
|
2235
|
+
} else if (counts.warn > 0) {
|
|
2236
|
+
console.log(chalk15.yellow.bold(" Environment is usable, with warnings.\n"));
|
|
2237
|
+
} else {
|
|
2238
|
+
console.log(chalk15.green.bold(" Environment is healthy.\n"));
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function buildDoctorCommand(deps = defaultDoctorDeps()) {
|
|
2242
|
+
return new Command14("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) => {
|
|
2243
|
+
try {
|
|
2244
|
+
const report = await runDoctor(deps, { fix: options.fix });
|
|
2245
|
+
renderReport(report, Boolean(options.fix));
|
|
2246
|
+
if (!report.ok) {
|
|
2247
|
+
process.exitCode = 1;
|
|
2248
|
+
}
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
const message = error instanceof Error ? error.message : "doctor failed";
|
|
2251
|
+
console.error(chalk15.red(`Error: ${message}
|
|
2252
|
+
Re-run \`prowl doctor\`; if it fails again, report the error.`));
|
|
2253
|
+
process.exitCode = 1;
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
|
|
1702
2258
|
// src/cli/program.ts
|
|
1703
2259
|
var CLI_VERSION = package_default.version;
|
|
1704
2260
|
function buildProgram() {
|
|
1705
|
-
const program2 = new
|
|
2261
|
+
const program2 = new Command15();
|
|
1706
2262
|
program2.name("prowl").description("CLI-first QA testing tool for deterministic Playwright flows").version(CLI_VERSION);
|
|
1707
2263
|
program2.addCommand(buildRunCommand());
|
|
1708
2264
|
program2.addCommand(buildCiCommand());
|
|
@@ -1717,6 +2273,7 @@ function buildProgram() {
|
|
|
1717
2273
|
program2.addCommand(buildFlakyCommand());
|
|
1718
2274
|
program2.addCommand(buildMcpCommand());
|
|
1719
2275
|
program2.addCommand(buildMacdriverCommand());
|
|
2276
|
+
program2.addCommand(buildDoctorCommand());
|
|
1720
2277
|
return program2;
|
|
1721
2278
|
}
|
|
1722
2279
|
|