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/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-2KD2XCTH.js";
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-ITOSUJCN.js";
41
+ } from "./chunk-O3OUTZ2P.js";
30
42
 
31
43
  // src/cli/program.ts
32
- import { Command as Command13 } from "commander";
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.5",
38
- description: "CLI-first QA testing tool for deterministic Playwright flows.",
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("<hunt-name>", "Hunt name or path (e.g. homepage or admin/users-crud)").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 (huntName, options) => {
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(" Browse hunt templates at ") + chalk4.cyan("https://hub.prowl.tools") + "\n");
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("<hunt-name>", "Hunt name or path (e.g. homepage or admin/users-crud)").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 (huntName, options) => {
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 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) => {
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 (macOS), not both.");
1092
+ throw new Error("Pass either a URL argument (web) or --app (native), not both.");
927
1093
  }
928
1094
  if (options.app) {
929
- await runMacAnalyze(options.app, tryLoadConfig(options.config), options);
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
- if (config?.target.type === "macos") {
938
- await runMacAnalyze(config.target.app, config, options);
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-path> (or set target.type: macos in .prowl/config.yml) for the macOS target."
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-FCXPARP7.js");
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("<hunt-name>", "Hunt name or path (e.g. homepage or admin/users-crud)").description("Show run history for a hunt").option("--config <path>", "Custom config path").option("--limit <n>", "Show the last N runs (default: 20)", (value) => {
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((huntName, options) => {
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 Command13();
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