prowl-tools 0.1.5 → 0.1.7
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 +578 -185
- package/dist/{chunk-ITOSUJCN.js → chunk-O3OUTZ2P.js} +22 -2
- package/dist/chunk-O3OUTZ2P.js.map +1 -0
- package/dist/{chunk-2KD2XCTH.js → chunk-R7NUH44M.js} +1690 -298
- package/dist/chunk-R7NUH44M.js.map +1 -0
- package/dist/index.cjs +1839 -365
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +277 -19
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +1864 -403
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +702 -58
- package/dist/lib.d.ts +702 -58
- package/dist/lib.js +130 -8
- package/dist/{loader-FCXPARP7.js → loader-X37URHUV.js} +2 -2
- package/examples/hunts/hello.yml +1 -1
- package/examples/hunts/login-flow.yml +58 -0
- package/package.json +6 -3
- package/dist/chunk-2KD2XCTH.js.map +0 -1
- package/dist/chunk-ITOSUJCN.js.map +0 -1
- /package/dist/{loader-FCXPARP7.js.map → loader-X37URHUV.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_FLAKY_THRESHOLD,
|
|
4
|
+
MACDRIVER_VERSION,
|
|
5
|
+
analyzeAndroidApp,
|
|
6
|
+
analyzeIosApp,
|
|
4
7
|
analyzeMacApp,
|
|
5
8
|
analyzePage,
|
|
9
|
+
assertAndroidAppAllowed,
|
|
10
|
+
assertIosAppAllowed,
|
|
6
11
|
assertTargetAppAllowed,
|
|
7
12
|
closeBrowser,
|
|
13
|
+
collectMacdriverStatus,
|
|
8
14
|
createPlaywrightDriver,
|
|
9
15
|
generateHunt,
|
|
16
|
+
installMacdriver,
|
|
17
|
+
launchAndroidSession,
|
|
10
18
|
launchBrowser,
|
|
19
|
+
launchIosSession,
|
|
11
20
|
launchMacSession,
|
|
21
|
+
macdriverReleaseTag,
|
|
12
22
|
parseBrowserEngine,
|
|
13
23
|
printCiSummary,
|
|
14
24
|
rankFlaky,
|
|
@@ -16,8 +26,9 @@ import {
|
|
|
16
26
|
runHunt,
|
|
17
27
|
runSuite,
|
|
18
28
|
saveStorageState,
|
|
29
|
+
tccGuidance,
|
|
19
30
|
updateBacklogFromSuite
|
|
20
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-R7NUH44M.js";
|
|
21
32
|
import {
|
|
22
33
|
CONFIG_DIR,
|
|
23
34
|
findConfigPath,
|
|
@@ -25,17 +36,18 @@ import {
|
|
|
25
36
|
loadConfig,
|
|
26
37
|
loadHuntMeta,
|
|
27
38
|
loadHuntTags,
|
|
39
|
+
normalizeHuntName,
|
|
28
40
|
resolveViewport
|
|
29
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-O3OUTZ2P.js";
|
|
30
42
|
|
|
31
43
|
// src/cli/program.ts
|
|
32
|
-
import { Command as
|
|
44
|
+
import { Command as Command14 } from "commander";
|
|
33
45
|
|
|
34
46
|
// package.json
|
|
35
47
|
var package_default = {
|
|
36
48
|
name: "prowl-tools",
|
|
37
|
-
version: "0.1.
|
|
38
|
-
description: "
|
|
49
|
+
version: "0.1.7",
|
|
50
|
+
description: "E2E testing for native macOS apps and web apps from declarative YAML hunts.",
|
|
39
51
|
type: "module",
|
|
40
52
|
license: "Apache-2.0",
|
|
41
53
|
author: "Prowl Tools",
|
|
@@ -50,9 +62,12 @@ var package_default = {
|
|
|
50
62
|
keywords: [
|
|
51
63
|
"testing",
|
|
52
64
|
"qa",
|
|
65
|
+
"e2e",
|
|
66
|
+
"macos",
|
|
67
|
+
"desktop-testing",
|
|
68
|
+
"accessibility",
|
|
53
69
|
"playwright",
|
|
54
70
|
"yaml",
|
|
55
|
-
"e2e",
|
|
56
71
|
"browser-testing",
|
|
57
72
|
"automation",
|
|
58
73
|
"web-testing",
|
|
@@ -198,6 +213,7 @@ function describeStep(step) {
|
|
|
198
213
|
const th = step.assertScreenshot.threshold !== void 0 ? ` (threshold: ${step.assertScreenshot.threshold})` : "";
|
|
199
214
|
return `assertScreenshot "${step.assertScreenshot.name}"${th}`;
|
|
200
215
|
}
|
|
216
|
+
if ("assertWithAI" in step) return `assertWithAI "${truncate(step.assertWithAI, 60)}"`;
|
|
201
217
|
return "unknown step";
|
|
202
218
|
}
|
|
203
219
|
function truncate(text, max) {
|
|
@@ -213,6 +229,9 @@ function printStepResult(result, step, _index) {
|
|
|
213
229
|
const duration = chalk.gray(`(${result.durationMs}ms)`);
|
|
214
230
|
if (result.status === "pass") {
|
|
215
231
|
console.log(` ${chalk.green("\u2713")} ${label} ${duration}`);
|
|
232
|
+
} else if (result.status === "warn") {
|
|
233
|
+
const note = result.value ? chalk.gray(` \u2014 ${result.value}`) : "";
|
|
234
|
+
console.log(` ${chalk.yellow("\u25CB")} ${label} ${duration}${note}`);
|
|
216
235
|
} else {
|
|
217
236
|
const error = result.error ? chalk.gray(` \u2014 ${result.error}`) : "";
|
|
218
237
|
console.log(` ${chalk.red("\u2717")} ${label} ${duration}${error}`);
|
|
@@ -264,7 +283,11 @@ ${bot}`);
|
|
|
264
283
|
|
|
265
284
|
// src/cli/commands/run.ts
|
|
266
285
|
function buildRunCommand() {
|
|
267
|
-
const command = new Command("run").argument(
|
|
286
|
+
const command = new Command("run").argument(
|
|
287
|
+
"<hunt-name>",
|
|
288
|
+
"Hunt name or path (e.g. homepage, admin/users-crud, or .prowl/hunts/homepage.yml)"
|
|
289
|
+
).option("--url <target>", "Override target URL").option("--headed", "Show browser window").option("--slow-mo <ms>", "Slow down Playwright actions", (value) => Number(value)).option("--trace", "Capture Playwright trace").option("--browser <engine>", "Browser engine: chromium, firefox, or webkit").option("--channel <name>", "Browser channel: chrome, msedge, chrome-beta, etc.").option("--viewport <size>", "Viewport size: WxH (e.g. 1920x1080) or preset (mobile, tablet, desktop)").option("--include-tags <tags>", "Only run hunts matching these tags (comma-separated)").option("--exclude-tags <tags>", "Skip hunts matching these tags (comma-separated)").option("--junit", "Generate JUnit XML report").option("--config <path>", "Custom config path").option("--json", "Output results as JSON").action(async (huntArg, options) => {
|
|
290
|
+
const huntName = normalizeHuntName(huntArg);
|
|
268
291
|
try {
|
|
269
292
|
if (options.includeTags || options.excludeTags) {
|
|
270
293
|
const { configDir } = loadConfig(options.config);
|
|
@@ -399,7 +422,7 @@ function buildInitCommand() {
|
|
|
399
422
|
console.log(welcomeBanner());
|
|
400
423
|
console.log(chalk4.green(` Initialized ${CONFIG_DIR} directory.`));
|
|
401
424
|
console.log(chalk4.gray(" Run ") + chalk4.bold("prowl run hello") + chalk4.gray(" to get started."));
|
|
402
|
-
console.log(chalk4.gray("
|
|
425
|
+
console.log(chalk4.gray(" See ") + chalk4.cyan(`${CONFIG_DIR}/hunts/login-flow.yml`) + chalk4.gray(" for a fuller example.") + "\n");
|
|
403
426
|
});
|
|
404
427
|
return command;
|
|
405
428
|
}
|
|
@@ -536,7 +559,11 @@ function createDebouncer(delayMs, fn) {
|
|
|
536
559
|
|
|
537
560
|
// src/cli/commands/watch.ts
|
|
538
561
|
function buildWatchCommand() {
|
|
539
|
-
const command = new Command5("watch").argument(
|
|
562
|
+
const command = new Command5("watch").argument(
|
|
563
|
+
"<hunt-name>",
|
|
564
|
+
"Hunt name or path (e.g. homepage, admin/users-crud, or .prowl/hunts/homepage.yml)"
|
|
565
|
+
).option("--url <target>", "Override target URL").option("--headed", "Show browser window").option("--slow-mo <ms>", "Slow down Playwright actions", (value) => Number(value)).option("--trace", "Capture Playwright trace").option("--config <path>", "Custom config path").action(async (huntArg, options) => {
|
|
566
|
+
const huntName = normalizeHuntName(huntArg);
|
|
540
567
|
const { configDir } = loadConfig(options.config);
|
|
541
568
|
const watchTargets = getWatchTargets(configDir, huntName);
|
|
542
569
|
let running = false;
|
|
@@ -799,6 +826,23 @@ function tryLoadConfig(configPath) {
|
|
|
799
826
|
throw new Error(`Failed to load config at ${configLocation}: ${message}`);
|
|
800
827
|
}
|
|
801
828
|
}
|
|
829
|
+
function looksLikeApk(app) {
|
|
830
|
+
return /\.apk$/i.test(app.trim());
|
|
831
|
+
}
|
|
832
|
+
function pickNativePlatform(platformFlag, config, app) {
|
|
833
|
+
if (platformFlag !== void 0) {
|
|
834
|
+
const normalized = platformFlag.trim().toLowerCase();
|
|
835
|
+
if (normalized === "macos" || normalized === "android" || normalized === "ios") {
|
|
836
|
+
return normalized;
|
|
837
|
+
}
|
|
838
|
+
throw new Error(`Unknown --platform "${platformFlag}". Use one of: macos, android, ios.`);
|
|
839
|
+
}
|
|
840
|
+
const configType = config?.target.type;
|
|
841
|
+
if (configType === "macos" || configType === "android" || configType === "ios") {
|
|
842
|
+
return configType;
|
|
843
|
+
}
|
|
844
|
+
return looksLikeApk(app) ? "android" : "macos";
|
|
845
|
+
}
|
|
802
846
|
async function runWebAnalyze(url, options) {
|
|
803
847
|
const engine = parseBrowserEngine(options.browser);
|
|
804
848
|
const channel = options.channel;
|
|
@@ -919,14 +963,141 @@ async function runMacAnalyze(app, config, options) {
|
|
|
919
963
|
await session.client.close();
|
|
920
964
|
}
|
|
921
965
|
}
|
|
966
|
+
function printAndroidElement(el) {
|
|
967
|
+
const role = chalk10.cyan(el.className);
|
|
968
|
+
const [best] = el.selectors;
|
|
969
|
+
const selectorStr = best ? chalk10.yellow(best) : chalk10.gray("(no selector)");
|
|
970
|
+
const label = el.contentDesc ?? el.text;
|
|
971
|
+
const labelStr = label ? ` ${chalk10.gray(`"${label}"`)}` : "";
|
|
972
|
+
const disabled = el.enabled === false ? chalk10.gray(" (disabled)") : "";
|
|
973
|
+
console.log(` ${role} ${selectorStr}${labelStr}${disabled}`);
|
|
974
|
+
}
|
|
975
|
+
async function runAndroidAnalyze(app, config, options, deviceSerial) {
|
|
976
|
+
const allowedApps = config?.guardrails.allowedApps ?? [];
|
|
977
|
+
if (!looksLikeApk(app)) {
|
|
978
|
+
assertAndroidAppAllowed(allowedApps, app);
|
|
979
|
+
}
|
|
980
|
+
const session = await launchAndroidSession({
|
|
981
|
+
app,
|
|
982
|
+
...deviceSerial ? { deviceSerial } : {},
|
|
983
|
+
timeoutMs: config?.browser.timeout,
|
|
984
|
+
allowedApps
|
|
985
|
+
});
|
|
986
|
+
try {
|
|
987
|
+
const sourceFn = session.client.source;
|
|
988
|
+
if (typeof sourceFn !== "function") {
|
|
989
|
+
throw new Error(
|
|
990
|
+
"The Android agent client does not expose source(); a live uiautomator2 session is required to analyze."
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
const result = await analyzeAndroidApp(
|
|
994
|
+
{ source: () => sourceFn.call(session.client) },
|
|
995
|
+
{ app: session.package }
|
|
996
|
+
);
|
|
997
|
+
if (options.json) {
|
|
998
|
+
console.log(JSON.stringify(result, null, 2));
|
|
999
|
+
} else {
|
|
1000
|
+
console.log(`
|
|
1001
|
+
${chalk10.bold("App Analysis:")} ${result.app}
|
|
1002
|
+
`);
|
|
1003
|
+
if (result.elements.length > 0) {
|
|
1004
|
+
console.log(chalk10.bold(" Interactive Elements:"));
|
|
1005
|
+
for (const el of result.elements) {
|
|
1006
|
+
printAndroidElement(el);
|
|
1007
|
+
}
|
|
1008
|
+
console.log();
|
|
1009
|
+
}
|
|
1010
|
+
console.log(chalk10.gray(` ${result.elements.length} elements
|
|
1011
|
+
`));
|
|
1012
|
+
}
|
|
1013
|
+
} finally {
|
|
1014
|
+
await session.teardown();
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function printIosElement(el) {
|
|
1018
|
+
const role = chalk10.cyan(el.type);
|
|
1019
|
+
const [best] = el.selectors;
|
|
1020
|
+
const selectorStr = best ? chalk10.yellow(best) : chalk10.gray("(no selector)");
|
|
1021
|
+
const label = el.label ?? el.value ?? el.name;
|
|
1022
|
+
const labelStr = label ? ` ${chalk10.gray(`"${label}"`)}` : "";
|
|
1023
|
+
const disabled = el.enabled === false ? chalk10.gray(" (disabled)") : "";
|
|
1024
|
+
console.log(` ${role} ${selectorStr}${labelStr}${disabled}`);
|
|
1025
|
+
}
|
|
1026
|
+
async function runIosAnalyze(app, config, options, udid) {
|
|
1027
|
+
const allowedApps = config?.guardrails.allowedApps ?? [];
|
|
1028
|
+
if (!/\//.test(app) && !/\.app$/i.test(app.trim())) {
|
|
1029
|
+
assertIosAppAllowed(allowedApps, app);
|
|
1030
|
+
}
|
|
1031
|
+
const session = await launchIosSession({
|
|
1032
|
+
app,
|
|
1033
|
+
...udid ? { udid } : {},
|
|
1034
|
+
timeoutMs: config?.browser.timeout,
|
|
1035
|
+
allowedApps
|
|
1036
|
+
});
|
|
1037
|
+
try {
|
|
1038
|
+
const sourceFn = session.client.source;
|
|
1039
|
+
if (typeof sourceFn !== "function") {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
"The iOS agent client does not expose source(); a live WebDriverAgent session is required to analyze."
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
const result = await analyzeIosApp(
|
|
1045
|
+
{ source: () => sourceFn.call(session.client) },
|
|
1046
|
+
{ app: session.bundleId }
|
|
1047
|
+
);
|
|
1048
|
+
if (options.json) {
|
|
1049
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1050
|
+
} else {
|
|
1051
|
+
console.log(`
|
|
1052
|
+
${chalk10.bold("App Analysis:")} ${result.app}
|
|
1053
|
+
`);
|
|
1054
|
+
if (result.windows.length > 0) {
|
|
1055
|
+
console.log(chalk10.bold(" Windows:"));
|
|
1056
|
+
for (const win of result.windows) {
|
|
1057
|
+
const title = win.label ?? win.name;
|
|
1058
|
+
const titleStr = title ? chalk10.gray(`"${title}"`) : chalk10.gray("(untitled)");
|
|
1059
|
+
console.log(` ${titleStr} ${chalk10.yellow(win.selector)}`);
|
|
1060
|
+
}
|
|
1061
|
+
console.log();
|
|
1062
|
+
}
|
|
1063
|
+
if (result.elements.length > 0) {
|
|
1064
|
+
console.log(chalk10.bold(" Interactive Elements:"));
|
|
1065
|
+
for (const el of result.elements) {
|
|
1066
|
+
printIosElement(el);
|
|
1067
|
+
}
|
|
1068
|
+
console.log();
|
|
1069
|
+
}
|
|
1070
|
+
console.log(chalk10.gray(` ${result.elements.length} elements, ${result.windows.length} windows
|
|
1071
|
+
`));
|
|
1072
|
+
}
|
|
1073
|
+
} finally {
|
|
1074
|
+
await session.teardown();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
async function runNativeAnalyze(platform, app, config, options, device) {
|
|
1078
|
+
if (platform === "android") {
|
|
1079
|
+
await runAndroidAnalyze(app, config, options, device.deviceSerial);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (platform === "ios") {
|
|
1083
|
+
await runIosAnalyze(app, config, options, device.udid);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
await runMacAnalyze(app, config, options);
|
|
1087
|
+
}
|
|
922
1088
|
function buildAnalyzeCommand() {
|
|
923
|
-
const command = new Command8("analyze").argument("[url]", "URL to analyze (web target)").description("Analyze a page or
|
|
1089
|
+
const command = new Command8("analyze").argument("[url]", "URL to analyze (web target)").description("Analyze a page or native app to discover interactive elements and selectors").option("--json", "Output as JSON").option("--app <app>", "Native app to analyze: bundle id / package / .app / .apk (macOS, Android, or iOS)").option("--platform <name>", "Native platform for --app: macos, android, or ios (else inferred)").option("--device <serial>", "Android adb device serial (native Android target)").option("--udid <udid>", "iOS simulator UDID (native iOS 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
1090
|
try {
|
|
925
1091
|
if (options.app && url) {
|
|
926
|
-
throw new Error("Pass either a URL argument (web) or --app (
|
|
1092
|
+
throw new Error("Pass either a URL argument (web) or --app (native), not both.");
|
|
927
1093
|
}
|
|
928
1094
|
if (options.app) {
|
|
929
|
-
|
|
1095
|
+
const config2 = tryLoadConfig(options.config);
|
|
1096
|
+
const platform = pickNativePlatform(options.platform, config2, options.app);
|
|
1097
|
+
await runNativeAnalyze(platform, options.app, config2, options, {
|
|
1098
|
+
deviceSerial: options.device,
|
|
1099
|
+
udid: options.udid
|
|
1100
|
+
});
|
|
930
1101
|
return;
|
|
931
1102
|
}
|
|
932
1103
|
if (url) {
|
|
@@ -934,12 +1105,21 @@ function buildAnalyzeCommand() {
|
|
|
934
1105
|
return;
|
|
935
1106
|
}
|
|
936
1107
|
const config = tryLoadConfig(options.config);
|
|
937
|
-
|
|
938
|
-
|
|
1108
|
+
const target = config?.target;
|
|
1109
|
+
if (target?.type === "macos") {
|
|
1110
|
+
await runMacAnalyze(target.app, config, options);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (target?.type === "android") {
|
|
1114
|
+
await runAndroidAnalyze(target.app, config, options, options.device ?? target.deviceSerial);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (target?.type === "ios") {
|
|
1118
|
+
await runIosAnalyze(target.app, config, options, options.udid ?? target.udid);
|
|
939
1119
|
return;
|
|
940
1120
|
}
|
|
941
1121
|
throw new Error(
|
|
942
|
-
"analyze needs a target: pass a URL for the web target, or use --app <bundle-id|.app
|
|
1122
|
+
"analyze needs a target: pass a URL for the web target, or use --app <bundle-id|package|.app|.apk> (optionally with --platform), or set a native target.type in .prowl/config.yml."
|
|
943
1123
|
);
|
|
944
1124
|
} catch (error) {
|
|
945
1125
|
const message = error instanceof Error ? error.message : "Analysis failed";
|
|
@@ -1014,7 +1194,7 @@ function buildGenerateCommand() {
|
|
|
1014
1194
|
} else if (options.output) {
|
|
1015
1195
|
let configDir;
|
|
1016
1196
|
try {
|
|
1017
|
-
const { loadConfig: loadConfig2 } = await import("./loader-
|
|
1197
|
+
const { loadConfig: loadConfig2 } = await import("./loader-X37URHUV.js");
|
|
1018
1198
|
const result = loadConfig2(options.config);
|
|
1019
1199
|
configDir = result.configDir;
|
|
1020
1200
|
} catch {
|
|
@@ -1048,13 +1228,17 @@ function buildGenerateCommand() {
|
|
|
1048
1228
|
import { Command as Command10 } from "commander";
|
|
1049
1229
|
import chalk12 from "chalk";
|
|
1050
1230
|
function buildHistoryCommand() {
|
|
1051
|
-
const command = new Command10("history").argument(
|
|
1231
|
+
const command = new Command10("history").argument(
|
|
1232
|
+
"<hunt-name>",
|
|
1233
|
+
"Hunt name or path (e.g. homepage, admin/users-crud, or .prowl/hunts/homepage.yml)"
|
|
1234
|
+
).description("Show run history for a hunt").option("--config <path>", "Custom config path").option("--limit <n>", "Show the last N runs (default: 20)", (value) => {
|
|
1052
1235
|
const n = Number(value);
|
|
1053
1236
|
if (!Number.isInteger(n) || n < 1) {
|
|
1054
1237
|
throw new Error("--limit must be a positive integer");
|
|
1055
1238
|
}
|
|
1056
1239
|
return n;
|
|
1057
|
-
}).option("--json", "Output as JSON").action((
|
|
1240
|
+
}).option("--json", "Output as JSON").action((huntArg, options) => {
|
|
1241
|
+
const huntName = normalizeHuntName(huntArg);
|
|
1058
1242
|
try {
|
|
1059
1243
|
const { configDir } = loadConfig(options.config);
|
|
1060
1244
|
const entries = readHuntHistory(configDir, huntName);
|
|
@@ -1442,10 +1626,83 @@ function buildMcpCommand() {
|
|
|
1442
1626
|
});
|
|
1443
1627
|
}
|
|
1444
1628
|
|
|
1629
|
+
// src/cli/commands/macdriver.ts
|
|
1630
|
+
import chalk14 from "chalk";
|
|
1631
|
+
import { Command as Command13 } from "commander";
|
|
1632
|
+
function buildInstallCommand() {
|
|
1633
|
+
return new Command13("install").description("Download and install the prebuilt, signed prowl-macdriver helper").option("--force", "Reinstall even if the pinned version is already present").action(async (options) => {
|
|
1634
|
+
try {
|
|
1635
|
+
console.log(
|
|
1636
|
+
chalk14.cyan(
|
|
1637
|
+
`Installing prowl-macdriver ${MACDRIVER_VERSION} (${macdriverReleaseTag(MACDRIVER_VERSION)})\u2026`
|
|
1638
|
+
)
|
|
1639
|
+
);
|
|
1640
|
+
const result = await installMacdriver({ force: options.force });
|
|
1641
|
+
if (result.alreadyInstalled) {
|
|
1642
|
+
console.log(
|
|
1643
|
+
chalk14.green(`Already installed at ${result.binaryPath}`) + chalk14.dim(" (use --force to reinstall)")
|
|
1644
|
+
);
|
|
1645
|
+
} else {
|
|
1646
|
+
console.log(chalk14.green(`Installed prowl-macdriver ${result.version} \u2192 ${result.binaryPath}`));
|
|
1647
|
+
}
|
|
1648
|
+
console.log("");
|
|
1649
|
+
console.log(tccGuidance());
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
const message = error instanceof Error ? error.message : "Install failed";
|
|
1652
|
+
console.error(chalk14.red(`Error: ${message}`));
|
|
1653
|
+
process.exitCode = 1;
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
function buildStatusCommand() {
|
|
1658
|
+
return new Command13("status").description("Report the resolved prowl-macdriver binary, installed versions, and permissions guidance").action(async () => {
|
|
1659
|
+
try {
|
|
1660
|
+
const status = await collectMacdriverStatus();
|
|
1661
|
+
console.log(chalk14.bold("prowl-macdriver status"));
|
|
1662
|
+
console.log(` pinned version: ${status.pinnedVersion}`);
|
|
1663
|
+
if (status.resolved) {
|
|
1664
|
+
const label = status.resolved.source === "env" ? "PROWL_MACDRIVER_BIN override" : status.resolved.source === "user-install" ? "user install (~/.prowl/macdriver)" : "repo source build (macdriver/.build)";
|
|
1665
|
+
console.log(` resolved binary: ${status.resolved.path}`);
|
|
1666
|
+
console.log(` resolved via: ${label}`);
|
|
1667
|
+
console.log(
|
|
1668
|
+
` runs: ${status.probedVersion ? chalk14.green(`yes (reports ${status.probedVersion})`) : chalk14.yellow("no (binary did not respond to `version`)")}`
|
|
1669
|
+
);
|
|
1670
|
+
} else {
|
|
1671
|
+
console.log(` resolved binary: ${chalk14.yellow("none")}`);
|
|
1672
|
+
console.log(chalk14.dim(" search order: PROWL_MACDRIVER_BIN \u2192 ~/.prowl/macdriver \u2192 macdriver/.build"));
|
|
1673
|
+
console.log(chalk14.cyan(" Run `prowl macdriver install` to install the prebuilt helper."));
|
|
1674
|
+
}
|
|
1675
|
+
if (status.installed.length > 0) {
|
|
1676
|
+
console.log(" installed versions:");
|
|
1677
|
+
for (const entry of status.installed) {
|
|
1678
|
+
const marker = entry.version === status.pinnedVersion ? chalk14.green(" (pinned)") : "";
|
|
1679
|
+
console.log(` - ${entry.version}${marker} ${chalk14.dim(entry.binaryPath)}`);
|
|
1680
|
+
}
|
|
1681
|
+
} else {
|
|
1682
|
+
console.log(" installed versions: none");
|
|
1683
|
+
}
|
|
1684
|
+
console.log("");
|
|
1685
|
+
console.log(tccGuidance());
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
const message = error instanceof Error ? error.message : "Status failed";
|
|
1688
|
+
console.error(chalk14.red(`Error: ${message}`));
|
|
1689
|
+
process.exitCode = 1;
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
function buildMacdriverCommand() {
|
|
1694
|
+
const command = new Command13("macdriver").description(
|
|
1695
|
+
"Manage the macOS helper binary (prowl-macdriver) for the macOS target"
|
|
1696
|
+
);
|
|
1697
|
+
command.addCommand(buildInstallCommand());
|
|
1698
|
+
command.addCommand(buildStatusCommand());
|
|
1699
|
+
return command;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1445
1702
|
// src/cli/program.ts
|
|
1446
1703
|
var CLI_VERSION = package_default.version;
|
|
1447
1704
|
function buildProgram() {
|
|
1448
|
-
const program2 = new
|
|
1705
|
+
const program2 = new Command14();
|
|
1449
1706
|
program2.name("prowl").description("CLI-first QA testing tool for deterministic Playwright flows").version(CLI_VERSION);
|
|
1450
1707
|
program2.addCommand(buildRunCommand());
|
|
1451
1708
|
program2.addCommand(buildCiCommand());
|
|
@@ -1459,6 +1716,7 @@ function buildProgram() {
|
|
|
1459
1716
|
program2.addCommand(buildHistoryCommand());
|
|
1460
1717
|
program2.addCommand(buildFlakyCommand());
|
|
1461
1718
|
program2.addCommand(buildMcpCommand());
|
|
1719
|
+
program2.addCommand(buildMacdriverCommand());
|
|
1462
1720
|
return program2;
|
|
1463
1721
|
}
|
|
1464
1722
|
|