prowl-tools 0.1.4 → 0.1.5

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
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DEFAULT_FLAKY_THRESHOLD,
4
+ analyzeMacApp,
4
5
  analyzePage,
6
+ assertTargetAppAllowed,
5
7
  closeBrowser,
6
8
  createPlaywrightDriver,
7
9
  generateHunt,
8
10
  launchBrowser,
11
+ launchMacSession,
9
12
  parseBrowserEngine,
10
13
  printCiSummary,
11
14
  rankFlaky,
@@ -14,15 +17,16 @@ import {
14
17
  runSuite,
15
18
  saveStorageState,
16
19
  updateBacklogFromSuite
17
- } from "./chunk-ZEFVTKQT.js";
20
+ } from "./chunk-2KD2XCTH.js";
18
21
  import {
19
22
  CONFIG_DIR,
23
+ findConfigPath,
20
24
  listHunts,
21
25
  loadConfig,
22
26
  loadHuntMeta,
23
27
  loadHuntTags,
24
28
  resolveViewport
25
- } from "./chunk-MBAIGNVO.js";
29
+ } from "./chunk-ITOSUJCN.js";
26
30
 
27
31
  // src/cli/program.ts
28
32
  import { Command as Command13 } from "commander";
@@ -30,7 +34,7 @@ import { Command as Command13 } from "commander";
30
34
  // package.json
31
35
  var package_default = {
32
36
  name: "prowl-tools",
33
- version: "0.1.4",
37
+ version: "0.1.5",
34
38
  description: "CLI-first QA testing tool for deterministic Playwright flows.",
35
39
  type: "module",
36
40
  license: "Apache-2.0",
@@ -100,6 +104,10 @@ var package_default = {
100
104
  yaml: "^2.6.1",
101
105
  zod: "^3.23.8"
102
106
  },
107
+ optionalDependencies: {
108
+ "appium-uiautomator2-server": "10.6.2",
109
+ "appium-webdriveragent": "16.4.0"
110
+ },
103
111
  devDependencies: {
104
112
  "@types/node": "^22.13.1",
105
113
  "@types/pngjs": "^6.0.5",
@@ -422,7 +430,7 @@ function buildLoginCommand() {
422
430
  let session = null;
423
431
  try {
424
432
  const { config, configDir } = loadConfig(options.config);
425
- if (config.target.type === "macos") {
433
+ if (config.target.type !== "web") {
426
434
  throw new Error("`prowl login` captures browser auth state and only applies to web targets.");
427
435
  }
428
436
  const targetUrl = options.url ?? config.target.url;
@@ -779,73 +787,160 @@ function parseViewportFlag(value) {
779
787
  }
780
788
  return value;
781
789
  }
782
- function buildAnalyzeCommand() {
783
- const command = new Command8("analyze").argument("<url>", "URL to analyze").description("Analyze a page to discover interactive elements and selectors").option("--json", "Output as JSON").option("--browser <engine>", "Browser engine: chromium, firefox, or webkit").option("--channel <name>", "Browser channel: chrome, msedge, etc.").option("--viewport <size>", "Viewport size: WxH or preset (mobile, tablet, desktop)").option("--headed", "Show browser window").option("--config <path>", "Custom config path").action(async (url, options) => {
784
- try {
785
- const engine = parseBrowserEngine(options.browser);
786
- const channel = options.channel;
787
- const viewport = options.viewport ? resolveViewport(parseViewportFlag(options.viewport)) : { width: 1280, height: 720 };
788
- const session = await launchBrowser({
789
- headless: !options.headed,
790
- slowMo: 0,
791
- timeout: 3e4,
792
- trace: false,
793
- recordHar: false,
794
- runDir: process.cwd(),
795
- engine,
796
- channel,
797
- viewport
798
- });
799
- const driver = createPlaywrightDriver(session.page);
800
- try {
801
- await driver.goto(url, { waitUntil: "networkidle" });
802
- const result = await analyzePage(driver);
803
- if (options.json) {
804
- console.log(JSON.stringify(result, null, 2));
805
- } else {
806
- console.log(`
790
+ function tryLoadConfig(configPath) {
791
+ const configLocation = configPath !== void 0 ? configPath : findConfigPath(process.cwd());
792
+ if (configLocation === null) {
793
+ return null;
794
+ }
795
+ try {
796
+ return loadConfig(configLocation).config;
797
+ } catch (error) {
798
+ const message = error instanceof Error ? error.message : "Unknown configuration error";
799
+ throw new Error(`Failed to load config at ${configLocation}: ${message}`);
800
+ }
801
+ }
802
+ async function runWebAnalyze(url, options) {
803
+ const engine = parseBrowserEngine(options.browser);
804
+ const channel = options.channel;
805
+ const viewport = options.viewport ? resolveViewport(parseViewportFlag(options.viewport)) : { width: 1280, height: 720 };
806
+ const session = await launchBrowser({
807
+ headless: !options.headed,
808
+ slowMo: 0,
809
+ timeout: 3e4,
810
+ trace: false,
811
+ recordHar: false,
812
+ runDir: process.cwd(),
813
+ engine,
814
+ channel,
815
+ viewport
816
+ });
817
+ const driver = createPlaywrightDriver(session.page);
818
+ try {
819
+ await driver.goto(url, { waitUntil: "networkidle" });
820
+ const result = await analyzePage(driver);
821
+ if (options.json) {
822
+ console.log(JSON.stringify(result, null, 2));
823
+ } else {
824
+ console.log(`
807
825
  ${chalk10.bold("Page Analysis:")} ${result.title}`);
808
- console.log(` ${chalk10.gray("URL:")} ${result.url}
826
+ console.log(` ${chalk10.gray("URL:")} ${result.url}
809
827
  `);
810
- if (result.forms.length > 0) {
811
- console.log(chalk10.bold(" Forms:"));
812
- for (const form of result.forms) {
813
- const method = form.method ? chalk10.cyan(form.method) : "";
814
- const action = form.action ? chalk10.gray(form.action) : "";
815
- console.log(` [${form.index}] ${method} ${action} (${form.fieldCount} fields)`);
816
- }
817
- console.log();
818
- }
819
- if (result.elements.length > 0) {
820
- console.log(chalk10.bold(" Interactive Elements:"));
821
- for (const el of result.elements) {
822
- const tag = chalk10.cyan(el.tag);
823
- const type = el.type ? chalk10.gray(`[${el.type}]`) : "";
824
- const bestSelector = el.selectors.testId ?? el.selectors.label ?? el.selectors.ariaLabel ?? el.selectors.css ?? el.selectors.name ?? "";
825
- const selectorStr = bestSelector ? chalk10.yellow(bestSelector) : chalk10.gray("(no selector)");
826
- const req = el.required ? chalk10.red(" *") : "";
827
- const form = el.formGroup !== void 0 ? chalk10.gray(` form[${el.formGroup}]`) : "";
828
- console.log(` ${tag}${type} ${selectorStr}${req}${form}`);
829
- }
830
- console.log();
831
- }
832
- if (result.links.length > 0) {
833
- console.log(chalk10.bold(" Links:"));
834
- for (const link of result.links.slice(0, 20)) {
835
- const text = link.text || chalk10.gray("(no text)");
836
- console.log(` ${text} ${chalk10.gray("\u2192")} ${chalk10.blue(link.href)}`);
837
- }
838
- if (result.links.length > 20) {
839
- console.log(chalk10.gray(` ... and ${result.links.length - 20} more`));
840
- }
841
- console.log();
842
- }
843
- console.log(chalk10.gray(` ${result.elements.length} elements, ${result.forms.length} forms, ${result.links.length} links
828
+ if (result.forms.length > 0) {
829
+ console.log(chalk10.bold(" Forms:"));
830
+ for (const form of result.forms) {
831
+ const method = form.method ? chalk10.cyan(form.method) : "";
832
+ const action = form.action ? chalk10.gray(form.action) : "";
833
+ console.log(` [${form.index}] ${method} ${action} (${form.fieldCount} fields)`);
834
+ }
835
+ console.log();
836
+ }
837
+ if (result.elements.length > 0) {
838
+ console.log(chalk10.bold(" Interactive Elements:"));
839
+ for (const el of result.elements) {
840
+ const tag = chalk10.cyan(el.tag);
841
+ const type = el.type ? chalk10.gray(`[${el.type}]`) : "";
842
+ const bestSelector = el.selectors.testId ?? el.selectors.label ?? el.selectors.ariaLabel ?? el.selectors.css ?? el.selectors.name ?? "";
843
+ const selectorStr = bestSelector ? chalk10.yellow(bestSelector) : chalk10.gray("(no selector)");
844
+ const req = el.required ? chalk10.red(" *") : "";
845
+ const form = el.formGroup !== void 0 ? chalk10.gray(` form[${el.formGroup}]`) : "";
846
+ console.log(` ${tag}${type} ${selectorStr}${req}${form}`);
847
+ }
848
+ console.log();
849
+ }
850
+ if (result.links.length > 0) {
851
+ console.log(chalk10.bold(" Links:"));
852
+ for (const link of result.links.slice(0, 20)) {
853
+ const text = link.text || chalk10.gray("(no text)");
854
+ console.log(` ${text} ${chalk10.gray("\u2192")} ${chalk10.blue(link.href)}`);
855
+ }
856
+ if (result.links.length > 20) {
857
+ console.log(chalk10.gray(` ... and ${result.links.length - 20} more`));
858
+ }
859
+ console.log();
860
+ }
861
+ console.log(chalk10.gray(` ${result.elements.length} elements, ${result.forms.length} forms, ${result.links.length} links
844
862
  `));
863
+ }
864
+ } finally {
865
+ await closeBrowser(session);
866
+ }
867
+ }
868
+ function printMacElement(el) {
869
+ const role = chalk10.cyan(el.role);
870
+ const [best] = el.selectors;
871
+ const selectorStr = best ? chalk10.yellow(best) : chalk10.gray("(no selector)");
872
+ const label = el.title ?? el.description ?? el.value;
873
+ const labelStr = label ? ` ${chalk10.gray(`"${label}"`)}` : "";
874
+ const disabled = el.enabled === false ? chalk10.gray(" (disabled)") : "";
875
+ console.log(` ${role} ${selectorStr}${labelStr}${disabled}`);
876
+ }
877
+ async function runMacAnalyze(app, config, options) {
878
+ const allowedApps = config?.guardrails.allowedApps ?? [];
879
+ assertTargetAppAllowed(allowedApps, app);
880
+ const session = await launchMacSession({ app, timeoutMs: config?.browser.timeout });
881
+ try {
882
+ const result = await analyzeMacApp(session.client, { app: session.bundleId });
883
+ if (options.json) {
884
+ console.log(JSON.stringify(result, null, 2));
885
+ } else {
886
+ console.log(`
887
+ ${chalk10.bold("App Analysis:")} ${result.app}
888
+ `);
889
+ if (result.windows.length > 0) {
890
+ console.log(chalk10.bold(" Windows:"));
891
+ for (const win of result.windows) {
892
+ const title = win.title ? chalk10.gray(`"${win.title}"`) : chalk10.gray("(untitled)");
893
+ console.log(` ${title} ${chalk10.yellow(win.selector)}`);
845
894
  }
846
- } finally {
847
- await closeBrowser(session);
895
+ console.log();
896
+ }
897
+ if (result.elements.length > 0) {
898
+ console.log(chalk10.bold(" Interactive Elements:"));
899
+ for (const el of result.elements) {
900
+ printMacElement(el);
901
+ }
902
+ console.log();
903
+ }
904
+ if (result.menuItems.length > 0) {
905
+ console.log(chalk10.bold(" Menu Bar:"));
906
+ for (const el of result.menuItems) {
907
+ printMacElement(el);
908
+ }
909
+ console.log();
910
+ }
911
+ console.log(
912
+ chalk10.gray(
913
+ ` ${result.elements.length} elements, ${result.windows.length} windows, ${result.menuItems.length} menu items
914
+ `
915
+ )
916
+ );
917
+ }
918
+ } finally {
919
+ await session.client.close();
920
+ }
921
+ }
922
+ function buildAnalyzeCommand() {
923
+ const command = new Command8("analyze").argument("[url]", "URL to analyze (web target)").description("Analyze a page or macOS app to discover interactive elements and selectors").option("--json", "Output as JSON").option("--app <app>", "macOS app to analyze: bundle id or /path/to/App.app (macOS target)").option("--browser <engine>", "Browser engine: chromium, firefox, or webkit").option("--channel <name>", "Browser channel: chrome, msedge, etc.").option("--viewport <size>", "Viewport size: WxH or preset (mobile, tablet, desktop)").option("--headed", "Show browser window").option("--config <path>", "Custom config path").action(async (url, options) => {
924
+ try {
925
+ if (options.app && url) {
926
+ throw new Error("Pass either a URL argument (web) or --app (macOS), not both.");
848
927
  }
928
+ if (options.app) {
929
+ await runMacAnalyze(options.app, tryLoadConfig(options.config), options);
930
+ return;
931
+ }
932
+ if (url) {
933
+ await runWebAnalyze(url, options);
934
+ return;
935
+ }
936
+ const config = tryLoadConfig(options.config);
937
+ if (config?.target.type === "macos") {
938
+ await runMacAnalyze(config.target.app, config, options);
939
+ return;
940
+ }
941
+ throw new Error(
942
+ "analyze needs a target: pass a URL for the web target, or use --app <bundle-id|.app-path> (or set target.type: macos in .prowl/config.yml) for the macOS target."
943
+ );
849
944
  } catch (error) {
850
945
  const message = error instanceof Error ? error.message : "Analysis failed";
851
946
  if (options.json) {
@@ -919,7 +1014,7 @@ function buildGenerateCommand() {
919
1014
  } else if (options.output) {
920
1015
  let configDir;
921
1016
  try {
922
- const { loadConfig: loadConfig2 } = await import("./loader-XMTDB6OL.js");
1017
+ const { loadConfig: loadConfig2 } = await import("./loader-FCXPARP7.js");
923
1018
  const result = loadConfig2(options.config);
924
1019
  configDir = result.configDir;
925
1020
  } catch {