@deeeed/metamask-harness 0.26.5 → 0.27.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.27.0 - 2026-07-31
6
+
7
+ ### Changed
8
+
9
+ - Mobile UI actions now record bounded native-hierarchy observations, while defaulted Extension UI actions record screen and visible observations; Extension screenshots and recordings publish through symlink-safe private staging as regular `0600` files, Mobile launch and diagnostics resolve absolute device-tool paths, and iOS preparation enables application accessibility before relaunching.
10
+
11
+ ### Fixed
12
+
13
+ - Serve analytics capture on both IPv4 and IPv6 loopback so Mobile clients resolving `localhost` to `::1` can reach the collector without exposing it on external interfaces.
14
+
5
15
  ## 0.26.5 - 2026-07-31
6
16
 
7
17
  ### Fixed
@@ -25,6 +25,7 @@ PLATFORM="ios"
25
25
  TARGET="$PWD"
26
26
  SIMULATOR="${IOS_SIMULATOR:-${SIM_UDID:-booted}}"
27
27
  ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
28
+ ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
28
29
  PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
29
30
  PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
30
31
  UI_ONLY=false
@@ -54,6 +55,17 @@ case "$PLATFORM" in
54
55
  ios|android) ;;
55
56
  *) printf 'open-device: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
56
57
  esac
58
+ if [ "$PLATFORM" = "android" ]; then
59
+ if [ -z "$ADB_BIN" ]; then
60
+ ADB_BIN="$(command -v adb 2>/dev/null || true)"
61
+ fi
62
+ if [ -z "$ADB_BIN" ] || [ "${ADB_BIN#/}" = "$ADB_BIN" ] || [ ! -x "$ADB_BIN" ]; then
63
+ printf 'open-device: Android SDK Platform-Tools (adb) is required but no executable was found.\n' >&2
64
+ printf 'Next: brew install android-platform-tools\n' >&2
65
+ exit 1
66
+ fi
67
+ export MM_HARNESS_ADB_PATH="$ADB_BIN"
68
+ fi
57
69
  if $UI_ONLY && [ "$PLATFORM" != "ios" ]; then
58
70
  printf 'open-device: --ui-only is supported only for ios.\n' >&2
59
71
  exit 2
@@ -207,7 +219,7 @@ focus_simulator() {
207
219
  android_app_installed() {
208
220
  local pkg="$1"
209
221
  shift
210
- adb "$@" shell pm list packages 2>/dev/null | grep -q "package:${pkg}"
222
+ "$ADB_BIN" "$@" shell pm list packages 2>/dev/null | grep -q "package:${pkg}"
211
223
  }
212
224
 
213
225
  run_android_native_build() {
@@ -315,14 +327,14 @@ else
315
327
  [ -n "$ADB_SERIAL_ARG" ] && ADB_ARGS+=("-s" "$ADB_SERIAL_ARG")
316
328
 
317
329
  # Reverse-tunnel Metro port so the device can reach localhost:PORT.
318
- adb "${ADB_ARGS[@]}" reverse "tcp:${PORT}" "tcp:${PORT}" >/dev/null 2>&1 || \
330
+ "$ADB_BIN" "${ADB_ARGS[@]}" reverse "tcp:${PORT}" "tcp:${PORT}" >/dev/null 2>&1 || \
319
331
  printf 'open-device: adb reverse failed (non-fatal — device may already be reachable)\n' >&2
320
332
 
321
333
  LAUNCHED=false
322
334
  for PKG in "${ANDROID_PKGS[@]}"; do
323
335
  ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}" || continue
324
336
  if $RESTART; then
325
- adb "${ADB_ARGS[@]}" shell am force-stop "$PKG" >/dev/null 2>&1 || true
337
+ "$ADB_BIN" "${ADB_ARGS[@]}" shell am force-stop "$PKG" >/dev/null 2>&1 || true
326
338
  printf 'Restarting Android dev client %s\n' "$PKG" >&2
327
339
  fi
328
340
 
@@ -331,7 +343,7 @@ else
331
343
  DEEP_LINK="expo-metamask://expo-development-client/?url=${ENCODED_URL}"
332
344
 
333
345
  printf 'Launching Android dev client %s\n' "$PKG" >&2
334
- if adb "${ADB_ARGS[@]}" shell am start \
346
+ if "$ADB_BIN" "${ADB_ARGS[@]}" shell am start \
335
347
  -a android.intent.action.VIEW \
336
348
  -d "$DEEP_LINK" >/dev/null 2>&1; then
337
349
  LAUNCHED=true
@@ -1,5 +1,7 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import path from "node:path";
2
3
  import { recordDepsBaseline } from "@farmslot/recipe-harness/runtime/deps-readiness";
4
+ import { resolveAndExportMobileToolPaths } from "../../../library/actions/mobile/platform/tool-paths.mjs";
3
5
  import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
4
6
  import { runnerDir } from "../../paths.js";
5
7
  import {
@@ -33,6 +35,14 @@ async function prepareMobile(target, opts = {}) {
33
35
  const preflightMode = opts.preflightMode ?? "fast";
34
36
  const clearMetro = opts.clearMetro ?? false;
35
37
  const restartApp = opts.restartApp ?? false;
38
+ try {
39
+ resolveAndExportMobileToolPaths(platform);
40
+ } catch (error) {
41
+ const message = error instanceof Error ? error.message : String(error);
42
+ if (!json) process.stderr.write(`${message}
43
+ `);
44
+ return { status: EXIT.runtime, output: message };
45
+ }
36
46
  const report = await decideMobileReadiness(target, {
37
47
  watcherPort: opts.watcherPort,
38
48
  metroLog: opts.metroLog,
@@ -49,8 +59,9 @@ async function prepareMobile(target, opts = {}) {
49
59
  `);
50
60
  return { status: EXIT.runtime, output: msg };
51
61
  }
62
+ const iosAccessibilityChanged = platform === "ios" && report.decision !== "unknown" && enableIosAccessibility(json);
52
63
  if (report.decision === "ready") {
53
- if (restartApp) {
64
+ if (restartApp || iosAccessibilityChanged) {
54
65
  const launch2 = await dispatchActionSequence(
55
66
  withAppRestart(launchActions(path.resolve(target)), true),
56
67
  target,
@@ -91,7 +102,10 @@ async function prepareMobile(target, opts = {}) {
91
102
  );
92
103
  }
93
104
  const launch = await dispatchActionSequence(
94
- withAppRestart(launchActions(path.resolve(target)), restartApp),
105
+ withAppRestart(
106
+ launchActions(path.resolve(target)),
107
+ restartApp || iosAccessibilityChanged
108
+ ),
95
109
  target,
96
110
  platform,
97
111
  json,
@@ -110,7 +124,7 @@ async function prepareMobile(target, opts = {}) {
110
124
  clearMetro ? report.actions.map(
111
125
  (a) => a.id === "start-metro" && !a.argv?.includes("--clear") ? { ...a, argv: [...a.argv ?? [], "--clear"] } : a
112
126
  ) : report.actions,
113
- restartApp
127
+ restartApp || iosAccessibilityChanged
114
128
  );
115
129
  const actionResult = await dispatchActionSequence(
116
130
  actions,
@@ -158,7 +172,10 @@ async function prepareMobile(target, opts = {}) {
158
172
  }
159
173
  case "launch": {
160
174
  const result = await dispatchActionSequence(
161
- withAppRestart(postInstall.actions, restartApp),
175
+ withAppRestart(
176
+ postInstall.actions,
177
+ restartApp || iosAccessibilityChanged
178
+ ),
162
179
  target,
163
180
  platform,
164
181
  json,
@@ -240,6 +257,99 @@ async function dispatchActionSequence(requestedActions, target, platform, json,
240
257
  }
241
258
  return { status: 0, output: "" };
242
259
  }
260
+ function enableIosAccessibility(json) {
261
+ const simulator = process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted";
262
+ const defaultsArgs = [
263
+ "simctl",
264
+ "spawn",
265
+ simulator,
266
+ "defaults"
267
+ ];
268
+ try {
269
+ execFileSync("xcrun", ["simctl", "boot", simulator], {
270
+ stdio: "ignore",
271
+ timeout: 15e3
272
+ });
273
+ } catch {
274
+ }
275
+ try {
276
+ execFileSync("xcrun", ["simctl", "bootstatus", simulator, "-b"], {
277
+ stdio: "ignore",
278
+ timeout: 12e4
279
+ });
280
+ } catch (error) {
281
+ if (!json) {
282
+ process.stderr.write(
283
+ `mobile prepare: selected iOS simulator was unavailable (${error instanceof Error ? error.message : String(error)}); continuing with native-view fallback.
284
+ `
285
+ );
286
+ }
287
+ return false;
288
+ }
289
+ try {
290
+ const current = execFileSync(
291
+ "xcrun",
292
+ [...defaultsArgs, "read", "com.apple.Accessibility", "ApplicationAccessibilityEnabled"],
293
+ {
294
+ encoding: "utf8",
295
+ stdio: ["ignore", "pipe", "ignore"],
296
+ timeout: 5e3
297
+ }
298
+ ).trim().toLowerCase();
299
+ if (current === "1" || current === "true" || current === "yes") return false;
300
+ } catch {
301
+ }
302
+ try {
303
+ execFileSync(
304
+ "xcrun",
305
+ [
306
+ ...defaultsArgs,
307
+ "write",
308
+ "com.apple.Accessibility",
309
+ "ApplicationAccessibilityEnabled",
310
+ "-bool",
311
+ "YES"
312
+ ],
313
+ {
314
+ stdio: "ignore",
315
+ timeout: 5e3
316
+ }
317
+ );
318
+ try {
319
+ execFileSync("xcrun", ["simctl", "shutdown", simulator], {
320
+ stdio: "ignore",
321
+ timeout: 15e3
322
+ });
323
+ } catch {
324
+ }
325
+ try {
326
+ execFileSync("xcrun", ["simctl", "boot", simulator], {
327
+ stdio: "ignore",
328
+ timeout: 15e3
329
+ });
330
+ } catch {
331
+ }
332
+ execFileSync("xcrun", ["simctl", "bootstatus", simulator, "-b"], {
333
+ stdio: "ignore",
334
+ timeout: 12e4
335
+ });
336
+ if (!json) {
337
+ process.stderr.write(
338
+ `Enabled iOS application accessibility on ${simulator}; restarted the simulator before app launch.
339
+ `
340
+ );
341
+ }
342
+ return true;
343
+ } catch (error) {
344
+ if (!json) {
345
+ process.stderr.write(
346
+ `mobile prepare: iOS accessibility enablement was unavailable (${error instanceof Error ? error.message : String(error)}); continuing with native-view fallback.
347
+ `
348
+ );
349
+ }
350
+ return false;
351
+ }
352
+ }
243
353
  async function dispatchAction(action, target, platform, json, preflightMode = "fast", watcherPort) {
244
354
  const cwd = action.cwd ?? target;
245
355
  switch (action.id) {
@@ -13,6 +13,7 @@ import {
13
13
  } from "@farmslot/recipe-harness/runtime/log-analysis";
14
14
  import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
15
15
  import { recipeRuntimePath } from "../../paths.js";
16
+ import { resolveMobileToolPath } from "../../../library/actions/mobile/platform/tool-paths.mjs";
16
17
  import { mobileProductMarkers } from "./deps-markers.js";
17
18
  import { applyMobileMetroEnvPolicy, mobileMetroEnvCheck } from "./metro-env.js";
18
19
  const BUNDLE_ERR = /Bundling failed|Unable to resolve /u;
@@ -66,9 +67,11 @@ const installActions = (target) => [
66
67
  function appRunningOnDevice(platform) {
67
68
  try {
68
69
  if (platform === "android") {
70
+ const adbPath = resolveMobileToolPath("adb");
71
+ if (!adbPath) return false;
69
72
  const serial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL;
70
73
  const args = serial ? ["-s", serial] : [];
71
- const out2 = execFileSync("adb", [...args, "shell", "ps", "-A"], {
74
+ const out2 = execFileSync(adbPath, [...args, "shell", "ps", "-A"], {
72
75
  encoding: "utf8",
73
76
  stdio: ["ignore", "pipe", "ignore"],
74
77
  timeout: 5e3
package/dist/adapters.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  } from "./live-adapter-contract.js";
6
6
  import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs";
7
7
  import { bridgeCommand, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
8
+ import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.mjs";
8
9
  function sleep(ms) {
9
10
  return new Promise((resolve) => setTimeout(resolve, ms));
10
11
  }
@@ -276,7 +277,8 @@ const MOBILE_BRIDGE_HANDLERS = {
276
277
  setInput: handleMobileSetInput,
277
278
  scroll: handleMobileScroll,
278
279
  waitFor: handleMobileWaitFor,
279
- hud: handleMobileHud
280
+ hud: handleMobileHud,
281
+ observeUi: handleMobileObserveUi
280
282
  };
281
283
  function createMetaMaskMobileBridge() {
282
284
  return {
@@ -302,6 +304,20 @@ async function handleMobileScreenshot(payload, context) {
302
304
  async function handleMobileStatus(payload, context) {
303
305
  return bridgeCommand(mobileUiInput(context, "status", payload), ["status"]);
304
306
  }
307
+ async function handleMobileObserveUi(payload, context) {
308
+ try {
309
+ await bridgeCommand(
310
+ mobileUiInput(context, "observe", {
311
+ ...payload,
312
+ bridge_timeout_ms: 2e3,
313
+ cdp_timeout_ms: 2e3
314
+ }),
315
+ ["hide-step"]
316
+ );
317
+ } catch {
318
+ }
319
+ return observeNativeUi(payload, context);
320
+ }
305
321
  async function handleMobileNavigate(payload, context) {
306
322
  const live = await runLiveAdapterScript({
307
323
  platform: "mobile",
@@ -496,7 +512,8 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
496
512
  );
497
513
  }
498
514
  });
499
- const transport = {
515
+ return {
516
+ ...base.observe ? { observe: base.observe.bind(base) } : {},
500
517
  async execute(action, node, context) {
501
518
  if (action === "ui.navigate") {
502
519
  const live = await runLiveFirst(
@@ -513,10 +530,6 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
513
530
  return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
514
531
  }
515
532
  };
516
- if (base.observe) {
517
- transport.observe = (refs, node, context) => base.observe(refs, node, context);
518
- }
519
- return transport;
520
533
  }
521
534
  function normalizeUiWaitNode(node) {
522
535
  if (node.expected !== void 0 || typeof node.visible !== "boolean") return node;
@@ -1,12 +1,15 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { resolveMobileToolPath } from "../../library/actions/mobile/platform/tool-paths.mjs";
2
3
  import { listConnectedDevices } from "../devices.js";
3
4
  import { optionString, shellQuoteArg, targetPath } from "./parse-args.js";
4
5
  function normalizeDeviceName(value) {
5
6
  return value.replace(/_/gu, " ").trim();
6
7
  }
7
8
  function resolveAndroidModel(serial) {
9
+ const adbPath = resolveMobileToolPath("adb");
10
+ if (!adbPath) return void 0;
8
11
  try {
9
- const output = execFileSync("adb", ["-s", serial, "shell", "getprop", "ro.product.model"], {
12
+ const output = execFileSync(adbPath, ["-s", serial, "shell", "getprop", "ro.product.model"], {
10
13
  encoding: "utf8",
11
14
  stdio: ["ignore", "pipe", "ignore"],
12
15
  timeout: 5e3
@@ -131,7 +131,31 @@ async function handleDoctor({ options }) {
131
131
  }
132
132
  return status === "pass" ? 0 : 1;
133
133
  }
134
- const result = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
134
+ const mobilePlatform = adapter === "mobile" && (platformOption === "ios" || platformOption === "android") ? platformOption : void 0;
135
+ const result = createDoctorReport(
136
+ adapter,
137
+ target,
138
+ manifestValidation,
139
+ actionManifestPath,
140
+ mobilePlatform
141
+ );
142
+ const missingRequiredTool = result.checks.find(
143
+ (check) => check.required && check.status === "fail" && check.id.startsWith("device-tool-") && check.userAction
144
+ );
145
+ if (missingRequiredTool) {
146
+ const error = {
147
+ code: "DOCTOR_CHECKS_FAILED",
148
+ message: missingRequiredTool.message,
149
+ userAction: missingRequiredTool.userAction
150
+ };
151
+ if (json) {
152
+ console.log(JSON.stringify({ ...result, ready: false, error }, null, 2));
153
+ } else {
154
+ console.error(`\u2717 doctor: ${error.message}
155
+ Next: ${error.userAction}`);
156
+ }
157
+ return EXIT.runtime;
158
+ }
135
159
  let runtime;
136
160
  let runtimeProbeError;
137
161
  try {
@@ -178,7 +202,9 @@ async function handleDoctor({ options }) {
178
202
  printReady
179
203
  );
180
204
  }
181
- const doctorUserAction = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
205
+ const doctorUserAction = doctorResult.checks.find(
206
+ (check) => check.required && check.status === "fail" && check.userAction
207
+ )?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
182
208
  const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
183
209
  const next = doctorError?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
184
210
  if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { resolveMobileToolPath } from "../../library/actions/mobile/platform/tool-paths.mjs";
2
3
  import { listConnectedDevices } from "../devices.js";
3
4
  import { renderDeviceList, scopedDevices } from "./device-target.js";
4
5
  import { probeMobileLiveState } from "./status-probe.js";
@@ -116,8 +117,10 @@ function selectedAndroidPortHints(devices, liveMap) {
116
117
  }).filter((hint) => hint.reversePorts.length > 0);
117
118
  }
118
119
  function androidReversePorts(serial) {
120
+ const adbPath = resolveMobileToolPath("adb");
121
+ if (!adbPath) return [];
119
122
  try {
120
- const out = execFileSync("adb", ["-s", serial, "reverse", "--list"], {
123
+ const out = execFileSync(adbPath, ["-s", serial, "reverse", "--list"], {
121
124
  encoding: "utf8",
122
125
  stdio: ["ignore", "pipe", "ignore"],
123
126
  timeout: 5e3
package/dist/devices.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
2
3
  function listConnectedDevices(platform) {
3
4
  const devices = [];
4
5
  if (!platform || platform === "android") devices.push(...listAndroidDevices());
@@ -6,9 +7,11 @@ function listConnectedDevices(platform) {
6
7
  return devices;
7
8
  }
8
9
  function listAndroidDevices() {
10
+ const adbPath = resolveMobileToolPath("adb");
11
+ if (!adbPath) return [];
9
12
  let output;
10
13
  try {
11
- output = execFileSync("adb", ["devices", "-l"], {
14
+ output = execFileSync(adbPath, ["devices", "-l"], {
12
15
  encoding: "utf8",
13
16
  timeout: 5e3,
14
17
  stdio: ["ignore", "pipe", "ignore"]
package/dist/doctor.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { color } from "./cli-color.js";
4
+ import {
5
+ mobileToolRecovery,
6
+ resolveMobileToolPath
7
+ } from "../library/actions/mobile/platform/tool-paths.mjs";
4
8
  import { mobilePerpsEnvironment } from "./adapters/mobile/perps-env.js";
5
9
  import { readRuntimeContextField, resolveRuntimeContextPath } from "./harness.js";
6
10
  import { manifestPath, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "./paths.js";
@@ -128,7 +132,7 @@ function renderRuntimeContext(runtimeContext) {
128
132
  }
129
133
  return lines.join("\n");
130
134
  }
131
- function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = manifestPath(adapter)) {
135
+ function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = manifestPath(adapter), mobilePlatform) {
132
136
  const mode = compatibilityMode(adapter, target);
133
137
  const manifestErrors = Number(manifestValidation.summary?.errors ?? 0);
134
138
  const checks = [
@@ -143,7 +147,8 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
143
147
  status: mode === "unsupported/no bridge" ? "fail" : "pass",
144
148
  required: false,
145
149
  message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
146
- }
150
+ },
151
+ ...adapter === "mobile" ? mobileToolDoctorChecks(mobilePlatform) : []
147
152
  ];
148
153
  const requiredChecks = requiredDoctorCheckSummary(checks);
149
154
  return {
@@ -164,6 +169,23 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
164
169
  manifestValidation: manifestValidation.summary
165
170
  };
166
171
  }
172
+ function mobileToolDoctorChecks(platform) {
173
+ const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
174
+ { tool: "adb", platform: "Android" },
175
+ { tool: "idb", platform: "iOS" }
176
+ ];
177
+ return specs.map(({ tool, platform: platformName }) => {
178
+ const resolved = resolveMobileToolPath(tool);
179
+ return {
180
+ id: `device-tool-${tool}`,
181
+ status: resolved ? "pass" : "fail",
182
+ required: platform !== void 0,
183
+ message: resolved ? `${platformName} device tool ${tool} is ready.` : `${platformName} device tool ${tool} is unavailable.`,
184
+ ...resolved ? { detail: resolved } : {},
185
+ ...!resolved ? { userAction: mobileToolRecovery(tool) } : {}
186
+ };
187
+ });
188
+ }
167
189
  function runnerInstallKind(runnerRoot, invokedPath, executablePath) {
168
190
  const normalizedRoot = path.normalize(runnerRoot);
169
191
  const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
@@ -230,6 +252,7 @@ export {
230
252
  createDoctorReport,
231
253
  fixtureFileSummary,
232
254
  fixtureSummary,
255
+ mobileToolDoctorChecks,
233
256
  renderRuntimeContext,
234
257
  repoShape,
235
258
  requiredDoctorCheckSummary,