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/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  saveStorageState,
29
29
  tccGuidance,
30
30
  updateBacklogFromSuite
31
- } from "./chunk-R7NUH44M.js";
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-O3OUTZ2P.js";
41
+ } from "./chunk-JFJQNJSJ.js";
42
42
 
43
43
  // src/cli/program.ts
44
- import { Command as Command14 } from "commander";
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.7",
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
- const packageRoot = getPackageRoot();
394
- const examplesDir = path.join(packageRoot, "examples");
395
- const exampleConfig = path.join(examplesDir, "config.yml");
396
- const exampleHuntsDir = path.join(examplesDir, "hunts");
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(" for a fuller example.") + "\n");
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-X37URHUV.js");
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 os from "os";
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(os.homedir(), ".prowl", "projects.yml");
1630
+ return path7.join(os2.homedir(), ".prowl", "projects.yml");
1465
1631
  }
1466
1632
  function legacyRegistryPath() {
1467
- return path7.join(os.homedir(), ".prowlqa", "projects.yml");
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 errorMessage(error) {
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)}: ${errorMessage(error)}`, {
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 errorMessage2(error) {
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: ${errorMessage2(error)}`);
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 Command14();
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