@deeeed/metamask-harness 0.26.4 → 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,24 @@
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
+
15
+ ## 0.26.5 - 2026-07-31
16
+
17
+ ### Fixed
18
+
19
+ - Forward Mobile launch's resolved Metro port to every runtime leaf so stale slot context cannot start, compile, launch, or observe a second Metro.
20
+ - Propagate inline Segment configuration into detached Mobile Metro runs and invalidate the Metro cache when it changes.
21
+ - Navigate through Mobile's root settings navigator before changing analytics consent.
22
+
5
23
  ## 0.26.4 - 2026-07-31
6
24
 
7
25
  ### 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
@@ -57,6 +57,7 @@ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
57
57
  mkdir -p "$LOG_DIR"
58
58
  LOG_FILE="$LOG_DIR/metro.log"
59
59
  PID_FILE="$LOG_DIR/metro.pid"
60
+ INLINE_ENV_FILE="$LOG_DIR/metro-inline-env.sh"
60
61
 
61
62
  # --- helpers ------------------------------------------------------------------
62
63
 
@@ -73,6 +74,24 @@ default_metro_workers() {
73
74
  fi
74
75
  }
75
76
 
77
+ write_inline_metro_env() {
78
+ local temporary name
79
+ temporary="$(mktemp "$LOG_DIR/.metro-inline-env.XXXXXX")" || return 1
80
+ chmod 600 "$temporary" || { rm -f "$temporary"; return 1; }
81
+ for name in \
82
+ SEGMENT_PROXY_URL \
83
+ SEGMENT_WRITE_KEY \
84
+ SEGMENT_FLUSH_INTERVAL \
85
+ SEGMENT_FLUSH_EVENT_LIMIT
86
+ do
87
+ if declare -p "$name" >/dev/null 2>&1; then
88
+ printf 'export %s=%q\n' "$name" "${!name}" >> "$temporary" \
89
+ || { rm -f "$temporary"; return 1; }
90
+ fi
91
+ done
92
+ mv -f "$temporary" "$INLINE_ENV_FILE"
93
+ }
94
+
76
95
  # Log-tail viewer window (session-safe): sourced so its run-owned-session rule is
77
96
  # unit-testable and can never leak a window into an unrelated user session.
78
97
  # shellcheck disable=SC1091
@@ -96,6 +115,10 @@ set +a
96
115
  # build.sh remaps MM_SENTRY_DSN from MM_SENTRY_DSN_DEV for dev builds; quick-launch
97
116
  # bypasses it, so without this Sentry never initializes in the dev client.
98
117
  [ -n "\${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="\${MM_SENTRY_DSN_DEV:-}"
118
+ [ ! -f $(printf '%q' "$INLINE_ENV_FILE") ] || {
119
+ . $(printf '%q' "$INLINE_ENV_FILE")
120
+ rm -f $(printf '%q' "$INLINE_ENV_FILE")
121
+ }
99
122
  export EXPO_NO_TYPESCRIPT_SETUP=1
100
123
  export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
101
124
  export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
@@ -165,6 +188,11 @@ printf 'Starting Metro on port %s (workers=%s%s)\n' \
165
188
  "$PORT" "$METRO_WORKERS" "$CLEAR_LABEL" >&2
166
189
  printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2
167
190
 
191
+ write_inline_metro_env || {
192
+ printf 'start-metro: could not stage inline Metro environment\n' >&2
193
+ exit 1
194
+ }
195
+
168
196
  if ! start_metro_tmux; then
169
197
  # Metro runs detached, writing to the log. The tmux window is a read-only tail.
170
198
  (
@@ -189,6 +217,11 @@ if ! start_metro_tmux; then
189
217
  # = build.sh ios main dev); quick-launch bypasses build.sh, so without this the
190
218
  # dev client's MM_SENTRY_DSN stays empty and Sentry never initializes.
191
219
  [ -n "${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="${MM_SENTRY_DSN_DEV:-}"
220
+ [ ! -f "$INLINE_ENV_FILE" ] || {
221
+ # shellcheck disable=SC1090
222
+ . "$INLINE_ENV_FILE"
223
+ rm -f "$INLINE_ENV_FILE"
224
+ }
192
225
  export EXPO_NO_TYPESCRIPT_SETUP=1
193
226
  export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
194
227
  export METRO_MAX_WORKERS="${METRO_WORKERS}"
@@ -3,6 +3,12 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { recipeRuntimePath } from "../../paths.js";
5
5
  const ENV_INPUTS = [".js.env", ".env", ".env.local"];
6
+ const PROCESS_ENV_INPUTS = [
7
+ "SEGMENT_PROXY_URL",
8
+ "SEGMENT_WRITE_KEY",
9
+ "SEGMENT_FLUSH_INTERVAL",
10
+ "SEGMENT_FLUSH_EVENT_LIMIT"
11
+ ];
6
12
  const BASELINE_FILE = "metro-env-baseline.json";
7
13
  function mobileMetroEnvFingerprint(target) {
8
14
  const hash = createHash("sha256");
@@ -13,6 +19,9 @@ function mobileMetroEnvFingerprint(target) {
13
19
  else hash.update("<absent>");
14
20
  hash.update("\0");
15
21
  }
22
+ for (const name of PROCESS_ENV_INPUTS) {
23
+ hash.update(`${name}\0${process.env[name] ?? "<absent>"}\0`);
24
+ }
16
25
  return hash.digest("hex");
17
26
  }
18
27
  function mobileMetroEnvCheck(target) {
@@ -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,27 +59,42 @@ 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,
57
68
  platform,
58
69
  json,
59
- preflightMode
70
+ preflightMode,
71
+ opts.watcherPort
60
72
  );
61
- return launch2.status === 0 ? startMobileConsoleForwarder(target, platform, json) : launch2;
73
+ return launch2.status === 0 ? startMobileConsoleForwarder(target, platform, json, opts.watcherPort) : launch2;
62
74
  }
63
- const surface = await ensureMobileHumanSurface(target, platform, json);
75
+ const surface = await ensureMobileHumanSurface(
76
+ target,
77
+ platform,
78
+ json,
79
+ opts.watcherPort
80
+ );
64
81
  if (surface.status !== 0) return surface;
65
82
  const confirm = await dispatchAction(
66
83
  { id: "wait-for-bridge", cwd: target, argv: ["--max-polls", String(READY_BRIDGE_CONFIRM_POLLS)] },
67
84
  target,
68
85
  platform,
69
86
  json,
70
- preflightMode
87
+ preflightMode,
88
+ opts.watcherPort
71
89
  );
72
- if (confirm.status === 0) return startMobileConsoleForwarder(target, platform, json);
90
+ if (confirm.status === 0) {
91
+ return startMobileConsoleForwarder(
92
+ target,
93
+ platform,
94
+ json,
95
+ opts.watcherPort
96
+ );
97
+ }
73
98
  if (!json) {
74
99
  process.stderr.write(
75
100
  `launch: runtime looked ready but no ${platform} bridge target answered; launching the app.
@@ -77,13 +102,17 @@ async function prepareMobile(target, opts = {}) {
77
102
  );
78
103
  }
79
104
  const launch = await dispatchActionSequence(
80
- withAppRestart(launchActions(path.resolve(target)), restartApp),
105
+ withAppRestart(
106
+ launchActions(path.resolve(target)),
107
+ restartApp || iosAccessibilityChanged
108
+ ),
81
109
  target,
82
110
  platform,
83
111
  json,
84
- preflightMode
112
+ preflightMode,
113
+ opts.watcherPort
85
114
  );
86
- return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json) : launch;
115
+ return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json, opts.watcherPort) : launch;
87
116
  }
88
117
  if (report.decision === "unknown") {
89
118
  const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
@@ -95,9 +124,16 @@ async function prepareMobile(target, opts = {}) {
95
124
  clearMetro ? report.actions.map(
96
125
  (a) => a.id === "start-metro" && !a.argv?.includes("--clear") ? { ...a, argv: [...a.argv ?? [], "--clear"] } : a
97
126
  ) : report.actions,
98
- restartApp
127
+ restartApp || iosAccessibilityChanged
128
+ );
129
+ const actionResult = await dispatchActionSequence(
130
+ actions,
131
+ target,
132
+ platform,
133
+ json,
134
+ preflightMode,
135
+ opts.watcherPort
99
136
  );
100
- const actionResult = await dispatchActionSequence(actions, target, platform, json, preflightMode);
101
137
  if (actionResult.status !== 0) return actionResult;
102
138
  if (report.decision === "install" && !process.env["RECIPE_UP_INSTALL_ATTEMPTED"]) {
103
139
  process.env["RECIPE_UP_INSTALL_ATTEMPTED"] = "1";
@@ -116,19 +152,35 @@ async function prepareMobile(target, opts = {}) {
116
152
  return { status: EXIT.runtime, output: msg };
117
153
  }
118
154
  case "ready": {
119
- const surface = await ensureMobileHumanSurface(target, platform, json);
155
+ const surface = await ensureMobileHumanSurface(
156
+ target,
157
+ platform,
158
+ json,
159
+ opts.watcherPort
160
+ );
120
161
  if (surface.status !== 0) return surface;
121
- const bridge = await dispatchAction({ id: "wait-for-bridge", cwd: target }, target, platform, json, preflightMode);
162
+ const bridge = await dispatchAction(
163
+ { id: "wait-for-bridge", cwd: target },
164
+ target,
165
+ platform,
166
+ json,
167
+ preflightMode,
168
+ opts.watcherPort
169
+ );
122
170
  if (bridge.status !== 0) return bridge;
123
171
  break;
124
172
  }
125
173
  case "launch": {
126
174
  const result = await dispatchActionSequence(
127
- withAppRestart(postInstall.actions, restartApp),
175
+ withAppRestart(
176
+ postInstall.actions,
177
+ restartApp || iosAccessibilityChanged
178
+ ),
128
179
  target,
129
180
  platform,
130
181
  json,
131
- preflightMode
182
+ preflightMode,
183
+ opts.watcherPort
132
184
  );
133
185
  if (result.status !== 0) return result;
134
186
  break;
@@ -142,19 +194,33 @@ async function prepareMobile(target, opts = {}) {
142
194
  }
143
195
  }
144
196
  }
145
- return startMobileConsoleForwarder(target, platform, json);
197
+ return startMobileConsoleForwarder(
198
+ target,
199
+ platform,
200
+ json,
201
+ opts.watcherPort
202
+ );
146
203
  }
147
- function startMobileConsoleForwarder(target, platform, json) {
204
+ function startMobileConsoleForwarder(target, platform, json, watcherPort) {
148
205
  return dispatchAction(
149
206
  { id: "start-console-forwarder", cwd: target, argv: json ? ["--quiet"] : [] },
150
207
  target,
151
208
  platform,
152
- json
209
+ json,
210
+ "fast",
211
+ watcherPort
153
212
  );
154
213
  }
155
- function ensureMobileHumanSurface(target, platform, json) {
214
+ function ensureMobileHumanSurface(target, platform, json, watcherPort) {
156
215
  if (platform !== "ios") return Promise.resolve({ status: 0, output: "" });
157
- return dispatchAction({ id: "ensure-device-ui", cwd: target }, target, platform, json);
216
+ return dispatchAction(
217
+ { id: "ensure-device-ui", cwd: target },
218
+ target,
219
+ platform,
220
+ json,
221
+ "fast",
222
+ watcherPort
223
+ );
158
224
  }
159
225
  function withAppRestart(actions, restartApp) {
160
226
  if (!restartApp) return actions;
@@ -162,7 +228,7 @@ function withAppRestart(actions, restartApp) {
162
228
  (action) => action.id === "launch-mobile-runtime" ? { ...action, argv: [...action.argv ?? [], "--restart"] } : action
163
229
  );
164
230
  }
165
- async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode) {
231
+ async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode, watcherPort) {
166
232
  const resolved = path.resolve(target);
167
233
  const actions = applyMobileMetroEnvPolicy(resolved, requestedActions);
168
234
  let pendingMetroEnvFingerprint;
@@ -170,7 +236,14 @@ async function dispatchActionSequence(requestedActions, target, platform, json,
170
236
  if (action.id === "start-metro") {
171
237
  pendingMetroEnvFingerprint = mobileMetroEnvCheck(resolved).fingerprint;
172
238
  }
173
- const result = await dispatchAction(action, target, platform, json, preflightMode);
239
+ const result = await dispatchAction(
240
+ action,
241
+ target,
242
+ platform,
243
+ json,
244
+ preflightMode,
245
+ watcherPort
246
+ );
174
247
  if (result.status !== 0) return result;
175
248
  if (action.id === "yarn-setup") recordDepsBaseline(resolved);
176
249
  if (action.id === "prewarm-bundle" && pendingMetroEnvFingerprint) {
@@ -184,7 +257,100 @@ async function dispatchActionSequence(requestedActions, target, platform, json,
184
257
  }
185
258
  return { status: 0, output: "" };
186
259
  }
187
- async function dispatchAction(action, target, platform, json, preflightMode = "fast") {
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
+ }
353
+ async function dispatchAction(action, target, platform, json, preflightMode = "fast", watcherPort) {
188
354
  const cwd = action.cwd ?? target;
189
355
  switch (action.id) {
190
356
  case "yarn-setup": {
@@ -194,34 +360,62 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
194
360
  case "start-metro": {
195
361
  const leaf = path.join(runnerDir, "adapters/mobile/start-metro.sh");
196
362
  const extra = action.argv ?? [];
197
- return spawnScriptStreaming(leaf, ["--target", cwd, ...extra], target);
363
+ return spawnScriptStreaming(
364
+ leaf,
365
+ withWatcherPort(["--target", cwd, ...extra], watcherPort),
366
+ target
367
+ );
198
368
  }
199
369
  case "start-console-forwarder": {
200
370
  const leaf = path.join(runnerDir, "adapters/mobile/start-console-forwarder.sh");
201
- return spawnScriptStreaming(leaf, ["--target", cwd, ...action.argv ?? []], target);
371
+ return spawnScriptStreaming(
372
+ leaf,
373
+ withWatcherPort(
374
+ ["--target", cwd, ...action.argv ?? []],
375
+ watcherPort
376
+ ),
377
+ target
378
+ );
202
379
  }
203
380
  case "prewarm-bundle": {
204
381
  const leaf = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
205
- return spawnScriptStreaming(leaf, ["--platform", platform, "--target", cwd], target);
382
+ return spawnScriptStreaming(
383
+ leaf,
384
+ withWatcherPort(
385
+ ["--platform", platform, "--target", cwd, ...action.argv ?? []],
386
+ watcherPort
387
+ ),
388
+ target
389
+ );
206
390
  }
207
391
  case "wait-for-bridge": {
208
392
  const leaf = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
209
393
  const extra = action.argv ?? [];
210
- return spawnScriptStreaming(leaf, ["--target", cwd, "--platform", platform, ...extra], target);
394
+ return spawnScriptStreaming(
395
+ leaf,
396
+ withWatcherPort(
397
+ ["--target", cwd, "--platform", platform, ...extra],
398
+ watcherPort
399
+ ),
400
+ target
401
+ );
211
402
  }
212
403
  case "launch-mobile-runtime": {
213
404
  const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
214
405
  return spawnScriptStreaming(
215
406
  leaf,
216
- [
217
- "--platform",
218
- platform,
219
- "--target",
220
- cwd,
221
- "--preflight-mode",
222
- preflightMode,
223
- ...action.argv ?? []
224
- ],
407
+ withWatcherPort(
408
+ [
409
+ "--platform",
410
+ platform,
411
+ "--target",
412
+ cwd,
413
+ "--preflight-mode",
414
+ preflightMode,
415
+ ...action.argv ?? []
416
+ ],
417
+ watcherPort
418
+ ),
225
419
  target,
226
420
  POD_PROBE_ENV
227
421
  );
@@ -230,7 +424,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
230
424
  const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
231
425
  return spawnScriptStreaming(
232
426
  leaf,
233
- ["--platform", platform, "--target", cwd, "--ui-only"],
427
+ withWatcherPort(
428
+ ["--platform", platform, "--target", cwd, "--ui-only"],
429
+ watcherPort
430
+ ),
234
431
  target,
235
432
  POD_PROBE_ENV
236
433
  );
@@ -245,6 +442,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
245
442
  return { status: 0, output: "" };
246
443
  }
247
444
  }
445
+ function withWatcherPort(args, watcherPort) {
446
+ if (watcherPort === void 0 || args.includes("--port")) return args;
447
+ return [...args, "--port", String(watcherPort)];
448
+ }
248
449
  export {
249
450
  mobileRuntimeStatus,
250
451
  prepareMobile
@@ -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