@deeeed/metamask-harness 0.12.0 → 0.14.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +26 -5
  2. package/adapters/core/cleanup.sh +0 -0
  3. package/adapters/core/inject.sh +0 -0
  4. package/adapters/extension/cleanup.mjs +0 -0
  5. package/adapters/extension/ensure-browser.sh +0 -0
  6. package/adapters/extension/inject.mjs +0 -0
  7. package/adapters/extension/launch-browser.cjs +0 -0
  8. package/adapters/extension/launch.sh +0 -0
  9. package/adapters/extension/live.sh +0 -0
  10. package/adapters/extension/readiness.mjs +0 -0
  11. package/adapters/extension/reattach.sh +0 -0
  12. package/adapters/extension/refresh-build.sh +0 -0
  13. package/adapters/extension/seed-fixture.sh +0 -0
  14. package/adapters/extension/sidepanel-toggle.sh +0 -0
  15. package/adapters/extension/snapshot-dist.sh +0 -0
  16. package/adapters/extension/start-watch.sh +0 -0
  17. package/adapters/extension/verify.sh +4 -4
  18. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  19. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  20. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +2 -4
  21. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +106 -0
  22. package/adapters/mobile/bridge-runtime/lib/console-format.cjs +53 -0
  23. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +82 -0
  24. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +17 -13
  25. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +10 -5
  26. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  27. package/adapters/mobile/cleanup.sh +0 -0
  28. package/adapters/mobile/inject.sh +0 -0
  29. package/adapters/mobile/lib/metro-listener.sh +0 -0
  30. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  31. package/adapters/mobile/open-device.sh +0 -0
  32. package/adapters/mobile/prewarm-bundle.sh +0 -0
  33. package/adapters/mobile/start-metro.sh +22 -1
  34. package/adapters/mobile/stop-metro.sh +0 -0
  35. package/adapters/mobile/verify.sh +50 -13
  36. package/adapters/mobile/wait-for-bridge.sh +41 -23
  37. package/adapters/mobile/yarn-setup.sh +0 -0
  38. package/adapters/shared/activate-repo-node.sh +0 -0
  39. package/adapters/shared/activate-repo-ruby.sh +0 -0
  40. package/adapters/shared/cli-ux.sh +0 -0
  41. package/adapters/shared/ensure-runner-deps.sh +0 -0
  42. package/adapters/shared/harness-path.sh +0 -0
  43. package/adapters/shared/hash-helpers.sh +0 -0
  44. package/adapters/shared/json-field.sh +0 -0
  45. package/adapters/shared/log-tui.mjs +7 -3
  46. package/adapters/shared/open-log-window.sh +0 -0
  47. package/adapters/shared/reap-checkout-metros.sh +0 -0
  48. package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
  49. package/adapters/shared/resolve-farmslot-ports.sh +0 -0
  50. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  51. package/adapters/shared/resolve-slot-ports.sh +0 -0
  52. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  53. package/adapters/shared/tmux-session.sh +0 -0
  54. package/dist/adapters/mobile/prepare.js +23 -2
  55. package/dist/adapters/mobile/runtime-decision.js +2 -1
  56. package/dist/adapters.js +33 -10
  57. package/dist/cli-commands.js +5 -4
  58. package/dist/commands/call.js +138 -3
  59. package/dist/commands/device-target.js +10 -3
  60. package/dist/commands/fixtures.js +2 -1
  61. package/dist/commands/manifest.js +2 -0
  62. package/dist/commands/run-engine.js +71 -4
  63. package/dist/commands/run.js +2 -0
  64. package/dist/commands/self-test.js +1 -1
  65. package/dist/commands/status.js +19 -4
  66. package/dist/mm-harness-cli.js +19 -5
  67. package/dist/recipe-files.js +14 -0
  68. package/docs/architecture.md +2 -2
  69. package/docs/live-adapter-contract.md +1 -1
  70. package/docs/recipe-libraries.md +21 -18
  71. package/library/actions/mobile/platform/bridge.mjs +35 -4
  72. package/library/recipes/{app-lifecycle-android-smoke.mobile.recipe.json → app/lifecycle.android-smoke.mobile.recipe.json} +1 -1
  73. package/library/recipes/{perps-performance.mobile.recipe.json → perps/performance.mobile.recipe.json} +1 -1
  74. package/library/recipes/perps/smoke.core.recipe.json +39 -0
  75. package/library/recipes/perps/smoke.extension.recipe.json +51 -0
  76. package/library/recipes/perps/smoke.mobile.recipe.json +51 -0
  77. package/library/recipes/{action-validation.extension.recipe.json → runner/action-validation.extension.recipe.json} +7 -7
  78. package/library/recipes/{action-validation.mobile.recipe.json → runner/action-validation.mobile.recipe.json} +7 -7
  79. package/package.json +2 -2
  80. package/scripts/completions.sh +0 -0
  81. package/scripts/install-completions.sh +0 -0
  82. /package/library/recipes/{perps-lifecycle.recipe.json → perps/lifecycle.recipe.json} +0 -0
  83. /package/library/recipes/{order-lifecycle.core.recipe.json → perps/order-lifecycle.core.recipe.json} +0 -0
  84. /package/library/recipes/{perps-performance-background-resume.mobile.recipe.json → perps/performance.background-resume.mobile.recipe.json} +0 -0
  85. /package/library/recipes/{perps-performance-cold-start.mobile.recipe.json → perps/performance.cold-start.mobile.recipe.json} +0 -0
  86. /package/library/recipes/{perps-performance-warm-start.mobile.recipe.json → perps/performance.warm-start.mobile.recipe.json} +0 -0
  87. /package/library/recipes/{read-markets.core.recipe.json → perps/read-markets.core.recipe.json} +0 -0
  88. /package/library/recipes/{trading-lifecycle.core.recipe.json → perps/trading-lifecycle.core.recipe.json} +0 -0
  89. /package/library/recipes/{smoke.extension.recipe.json → runner/smoke.extension.recipe.json} +0 -0
  90. /package/library/recipes/{smoke.mobile.recipe.json → runner/smoke.mobile.recipe.json} +0 -0
@@ -216,6 +216,34 @@ process.stdout.write(value);
216
216
  NODE
217
217
  }
218
218
 
219
+ runtime_context_value() {
220
+ TARGET_FOR_RUNTIME_CONTEXT="$TARGET" RUNTIME_DIR_FOR_RUNTIME_CONTEXT="$RUNTIME_DIR" RUNTIME_CONTEXT_NAME="$1" node <<'NODE'
221
+ const fs = require('fs');
222
+ const path = require('path');
223
+ const target = process.env.TARGET_FOR_RUNTIME_CONTEXT;
224
+ const runtimeDir = process.env.RUNTIME_DIR_FOR_RUNTIME_CONTEXT;
225
+ const names = process.env.RUNTIME_CONTEXT_NAME.split(',');
226
+ const file = path.join(target, runtimeDir, 'agentic-runtime.json');
227
+ let data = {};
228
+ try { data = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
229
+ for (const name of names) {
230
+ const value = data && data[name];
231
+ if (value !== undefined && value !== null && value !== '') {
232
+ process.stdout.write(String(value));
233
+ process.exit(0);
234
+ }
235
+ }
236
+ NODE
237
+ }
238
+
239
+ mobile_device_target() {
240
+ if [ "$PLATFORM" = "android" ]; then
241
+ printf '%s' "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-$(runtime_context_value adbSerial,androidDevice,device)}}}"
242
+ return
243
+ fi
244
+ printf '%s' "${SIM_UDID:-${IOS_SIMULATOR:-$(runtime_context_value simulator,iosSimulator,device)}}"
245
+ }
246
+
219
247
  check_file() {
220
248
  local rel="$1"
221
249
  if [ -e "$TARGET/$rel" ]; then
@@ -331,6 +359,7 @@ run_with_timeout() {
331
359
 
332
360
  live_status_ok() {
333
361
  local log_path="$1"
362
+ local device_target="$2"
334
363
  cat > "$ARTIFACTS/mobile-status-smoke.recipe.json" <<'JSON'
335
364
  {
336
365
  "schema_version": 1,
@@ -349,12 +378,15 @@ live_status_ok() {
349
378
  JSON
350
379
  (
351
380
  cd "$TARGET"
352
- "$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --project-root "$TARGET" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
381
+ device_args=()
382
+ [ -n "$device_target" ] && device_args=(--device "$device_target")
383
+ "$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --project-root "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
353
384
  ) > "$log_path" 2>&1
354
385
  }
355
386
 
356
387
  ensure_live_runtime() {
357
- if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log"; then
388
+ local device_target="$1"
389
+ if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
358
390
  return 0
359
391
  fi
360
392
  if [ "$AUTO_START" = true ]; then
@@ -366,7 +398,17 @@ EOF
366
398
  }
367
399
 
368
400
  if [ "$STATIC_ONLY" = false ]; then
369
- if ensure_live_runtime; then
401
+ device_target="$(mobile_device_target)"
402
+ if [ -z "$device_target" ]; then
403
+ if [ "$PLATFORM" = "android" ]; then
404
+ device_target="$(jsenv_value ADB_SERIAL)"
405
+ [ -n "$device_target" ] || device_target="$(jsenv_value ANDROID_DEVICE)"
406
+ else
407
+ device_target="$(jsenv_value IOS_SIMULATOR)"
408
+ fi
409
+ fi
410
+
411
+ if ensure_live_runtime "$device_target"; then
370
412
  checks+=('{"name":"mobile runtime controllable precheck","status":"pass"}')
371
413
  else
372
414
  checks+=('{"name":"mobile runtime controllable precheck","status":"fail","detail":"see logs/app-status-precheck.log"}')
@@ -394,20 +436,13 @@ if [ "$STATIC_ONLY" = false ]; then
394
436
  {
395
437
  "schema_version": 1,
396
438
  "title": "Mobile v1 runner live bridge smoke",
397
- "description": "Verifies the installed MetaMask runner can drive the React Native debug bridge through Recipe v1 actions.",
439
+ "description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
398
440
  "validate": {
399
441
  "workflow": {
400
442
  "entry": "status",
401
443
  "nodes": {
402
444
  "status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
403
- "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "wallet-setup" },
404
- "wallet-setup": { "action": "metamask.wallet.setup", "intent": "Prepare the wallet fixture for the live bridge smoke", "timeout_ms": 45000, "next": "wallet-unlock" },
405
- "wallet-unlock": { "action": "metamask.wallet.ensure_unlocked", "intent": "Unlock the wallet through the manifest-declared action", "timeout_ms": 45000, "next": "wallet-read" },
406
- "wallet-read": { "action": "metamask.wallet.read_state", "intent": "Read wallet state before navigating to the wallet view", "timeout_ms": 45000, "next": "navigate-wallet" },
407
- "navigate-wallet": { "action": "ui.navigate", "intent": "Open the wallet view through the UI navigation action", "route": "WalletView", "timeout_ms": 45000, "next": "wait-wallet" },
408
- "wait-wallet": { "action": "ui.wait_for", "intent": "Wait until the wallet screen is present", "test_id": "wallet-screen", "expected": "present", "timeout_ms": 45000, "next": "hud-smoke" },
409
- "hud-smoke": { "action": "app.hud", "status": "running", "intent": "Show the Mobile live bridge smoke HUD", "progress": { "current": 1, "total": 1 }, "timeout_ms": 45000, "next": "screenshot" },
410
- "screenshot": { "action": "ui.screenshot", "intent": "Capture proof that the wallet screen is visible", "path": "screenshots/mobile-v1-live-smoke.png", "timeout_ms": 45000, "next": "done" },
445
+ "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "done" },
411
446
  "done": { "action": "end", "status": "pass" }
412
447
  }
413
448
  }
@@ -417,13 +452,15 @@ JSON
417
452
 
418
453
  ios_simulator_resolved="$(jsenv_value IOS_SIMULATOR)"
419
454
  adb_serial_resolved="$(jsenv_value ADB_SERIAL)"
455
+ device_args=()
456
+ [ -n "$device_target" ] && device_args=(--device "$device_target")
420
457
  if (
421
458
  cd "$TARGET"
422
459
  METAMASK_RECIPE_AUTO_HUD=0 \
423
460
  IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
424
461
  ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
425
462
  ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
426
- "$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --project-root "$TARGET" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
463
+ "$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --project-root "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
427
464
  ) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
428
465
  checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
429
466
  else
@@ -9,6 +9,8 @@
9
9
  # --target <metamask-mobile dir> (default $PWD)
10
10
  # --port <number> (default WATCHER_PORT env, else METRO_PORT, else 8081)
11
11
  # --max-polls <n> (default MOBILE_BRIDGE_READY_POLLS env, else 90; 2s each = 180s)
12
+ # --platform ios|android bind the required platform: the answering target
13
+ # must belong to it, regardless of ambient device env
12
14
  # Outputs:
13
15
  # Progress on stderr; exit 0 bridge ready; 1 timeout.
14
16
  #
@@ -18,14 +20,16 @@ set -uo pipefail
18
20
  TARGET="$PWD"
19
21
  PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
20
22
  MAX_POLLS="${MOBILE_BRIDGE_READY_POLLS:-90}"
23
+ REQUIRE_PLATFORM=""
21
24
 
22
25
  while [ "$#" -gt 0 ]; do
23
26
  case "$1" in
24
27
  --target) TARGET="$2"; shift 2 ;;
25
28
  --port) PORT="$2"; shift 2 ;;
26
29
  --max-polls) MAX_POLLS="$2"; shift 2 ;;
30
+ --platform) REQUIRE_PLATFORM="$2"; shift 2 ;;
27
31
  -h|--help)
28
- printf 'Usage: wait-for-bridge.sh [--target <dir>] [--port <n>] [--max-polls <n>]\n'
32
+ printf 'Usage: wait-for-bridge.sh [--target <dir>] [--port <n>] [--max-polls <n>] [--platform ios|android]\n'
29
33
  exit 0
30
34
  ;;
31
35
  *) printf 'wait-for-bridge: unknown arg: %s\n' "$1" >&2; exit 2 ;;
@@ -34,6 +38,10 @@ done
34
38
 
35
39
  TARGET="$(cd "$TARGET" && pwd)"
36
40
 
41
+ # The matcher reads the required platform from the environment so the value survives
42
+ # into the node subprocess that runs the shared matcher against the status output.
43
+ export WAIT_FOR_BRIDGE_PLATFORM="$REQUIRE_PLATFORM"
44
+
37
45
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
38
46
  # shellcheck disable=SC1091
39
47
  . "$SCRIPT_DIR/../shared/harness-path.sh"
@@ -45,6 +53,7 @@ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
45
53
  mkdir -p "$LOG_DIR"
46
54
 
47
55
  BRIDGE_CJS="$(dirname "$0")/bridge-runtime/cdp-bridge.cjs"
56
+ MATCH_LIB="$(dirname "$0")/bridge-runtime/lib/match-bridge-target.cjs"
48
57
  STATUS_LOG="$LOG_DIR/bridge-status.log"
49
58
  METRO_LOG="$LOG_DIR/metro.log"
50
59
 
@@ -86,30 +95,37 @@ bridge_wait_reason() {
86
95
  bundle_progress "$METRO_LOG"
87
96
  }
88
97
 
98
+ # Matcher is the shared boundary-safe module (match-bridge-target.cjs), so the
99
+ # launch ready-path confirm and this poll enforce identical platform/device rules.
89
100
  bridge_has_route() {
90
101
  (cd "$TARGET" && APP_ROOT="$TARGET" node "$BRIDGE_CJS" status > "$STATUS_LOG" 2>&1) \
91
- && node - "$STATUS_LOG" <<'NODE'
92
- const fs = require('fs');
102
+ && node -e '
103
+ const { hasMatchingRoute } = require(process.argv[1]);
104
+ const fs = require("fs");
93
105
  try {
94
- const value = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
95
- const targets = Array.isArray(value) ? value : [value];
96
- const androidName = process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ANDROID_DEVICE || '';
97
- const adbSerial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || '';
98
- const iosSimulator = process.env.IOS_SIMULATOR || '';
99
- const matchesTarget = (target) => {
100
- if (!target || typeof target !== 'object') return false;
101
- if (adbSerial || androidName) {
102
- if (target.platform !== 'android') return false;
103
- if (!androidName) return true;
104
- const deviceName = String(target.deviceName || '');
105
- return deviceName === androidName || deviceName.startsWith(`${androidName} -`);
106
- }
107
- if (iosSimulator) return target.deviceName === iosSimulator;
108
- return true;
109
- };
110
- if (!targets.some((t) => matchesTarget(t) && t.route)) process.exit(1);
106
+ const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
107
+ process.exit(hasMatchingRoute(value) ? 0 : 1);
111
108
  } catch { process.exit(1); }
112
- NODE
109
+ ' "$MATCH_LIB" "$STATUS_LOG"
110
+ }
111
+
112
+ # The device/platform this run is pinned to — the bridge target must belong to it.
113
+ requested_target() {
114
+ node -e 'process.stdout.write(require(process.argv[1]).describeRequested())' "$MATCH_LIB" 2>/dev/null \
115
+ || printf 'any platform'
116
+ }
117
+
118
+ # Summarise the targets that DID answer (platform / deviceName), so a timeout teaches
119
+ # what responded vs what was requested — e.g. an iOS target answering an android launch.
120
+ answered_targets() {
121
+ [ -s "$STATUS_LOG" ] || { printf 'none'; return; }
122
+ node -e '
123
+ const { describeTargets } = require(process.argv[1]);
124
+ const fs = require("fs");
125
+ try {
126
+ process.stdout.write(describeTargets(JSON.parse(fs.readFileSync(process.argv[2], "utf8"))));
127
+ } catch { process.stdout.write("none"); }
128
+ ' "$MATCH_LIB" "$STATUS_LOG" 2>/dev/null || printf 'none'
113
129
  }
114
130
 
115
131
  for ATTEMPT in $(seq 1 "$MAX_POLLS"); do
@@ -124,7 +140,9 @@ for ATTEMPT in $(seq 1 "$MAX_POLLS"); do
124
140
  done
125
141
 
126
142
  cat "$STATUS_LOG" >&2 || true
127
- printf 'wait-for-bridge: bridge did not become ready on port %s (%s polls × 2s = %ss)\n' \
128
- "$PORT" "$MAX_POLLS" "$((MAX_POLLS * 2))" >&2
143
+ printf 'wait-for-bridge: no bridge target matched the requested %s on port %s (%s polls × 2s = %ss)\n' \
144
+ "$(requested_target)" "$PORT" "$MAX_POLLS" "$((MAX_POLLS * 2))" >&2
145
+ printf ' requested: %s\n' "$(requested_target)" >&2
146
+ printf ' answered: %s\n' "$(answered_targets)" >&2
129
147
  printf ' Next: check %s — or run mm-harness app-status\n' "$STATUS_LOG" >&2
130
148
  exit 1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -222,13 +222,17 @@ async function watchLog(options) {
222
222
  const chunk = readNewContent(logPath, offset);
223
223
  offset = chunk.offset;
224
224
  const allText = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '';
225
+ // Match readiness only against bytes read after this watch started. This
226
+ // assumes line-oriented log writes; a marker split across partial writes
227
+ // waits for a later complete marker instead of stale-passing.
228
+ const matchText = chunk.text;
225
229
  const events = extractEvents(allText, eventsLimit);
226
230
 
227
- if (matchesPattern(allText, 'Module build failed|^ERROR in |compiled with [1-9][0-9]* error|FAIL runtime-launch|Extension harness launch fail')) {
231
+ if (matchesPattern(matchText, 'Module build failed|^ERROR in |compiled with [1-9][0-9]* error|FAIL runtime-launch|Extension harness launch fail')) {
228
232
  renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
229
233
  return 1;
230
234
  }
231
- if (matchesPattern(allText, successPattern)) {
235
+ if (matchesPattern(matchText, successPattern)) {
232
236
  renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
233
237
  if (mode !== 'quiet') {
234
238
  const elapsed = Math.round((Date.now() - started) / 1000);
@@ -357,4 +361,4 @@ async function main() {
357
361
  main().catch((error) => {
358
362
  console.error(error instanceof Error ? error.message : String(error));
359
363
  process.exit(1);
360
- });
364
+ });
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -3,8 +3,10 @@ import { recordDepsBaseline } from "@farmslot/recipe-harness/runtime/deps-readin
3
3
  import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
4
4
  import { runnerDir } from "../../paths.js";
5
5
  import {
6
- decideMobileReadiness
6
+ decideMobileReadiness,
7
+ launchActions
7
8
  } from "./runtime-decision.js";
9
+ const READY_BRIDGE_CONFIRM_POLLS = 3;
8
10
  const POD_PROBE_ENV = {
9
11
  FORCE_COLOR: "0",
10
12
  NO_COLOR: "1",
@@ -42,6 +44,24 @@ async function prepareMobile(target, opts = {}) {
42
44
  return { status: EXIT.runtime, output: msg };
43
45
  }
44
46
  if (report.decision === "ready") {
47
+ const confirm = await dispatchAction(
48
+ { id: "wait-for-bridge", cwd: target, argv: ["--max-polls", String(READY_BRIDGE_CONFIRM_POLLS)] },
49
+ target,
50
+ platform,
51
+ json,
52
+ preflightMode
53
+ );
54
+ if (confirm.status === 0) return { status: 0, output: "" };
55
+ if (!json) {
56
+ process.stderr.write(
57
+ `launch: runtime looked ready but no ${platform} bridge target answered; launching the app.
58
+ `
59
+ );
60
+ }
61
+ for (const action of launchActions(path.resolve(target))) {
62
+ const result = await dispatchAction(action, target, platform, json, preflightMode);
63
+ if (result.status !== 0) return result;
64
+ }
45
65
  return { status: 0, output: "" };
46
66
  }
47
67
  if (report.decision === "unknown") {
@@ -115,7 +135,8 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
115
135
  }
116
136
  case "wait-for-bridge": {
117
137
  const leaf = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
118
- return spawnScriptStreaming(leaf, ["--target", cwd], target);
138
+ const extra = action.argv ?? [];
139
+ return spawnScriptStreaming(leaf, ["--target", cwd, "--platform", platform, ...extra], target);
119
140
  }
120
141
  case "clear-metro-cache": {
121
142
  const leaf = path.join(runnerDir, "adapters/mobile/start-metro.sh");
@@ -313,5 +313,6 @@ async function computeMobileReadiness(resolved, options, fast) {
313
313
  };
314
314
  }
315
315
  export {
316
- decideMobileReadiness
316
+ decideMobileReadiness,
317
+ launchActions
317
318
  };
package/dist/adapters.js CHANGED
@@ -2,7 +2,7 @@ import http from "node:http";
2
2
  import { compatibilityMode, fixtureSummary, repoShape } from "./doctor.js";
3
3
  import { runLiveAdapterScript } from "./live-adapter-contract.js";
4
4
  import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs";
5
- import { bridgeCommand, evalSync, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
5
+ import { bridgeCommand, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
6
6
  function sleep(ms) {
7
7
  return new Promise((resolve) => setTimeout(resolve, ms));
8
8
  }
@@ -331,9 +331,21 @@ async function handleMobileHud(payload, context) {
331
331
  return mobileHudSkippedOrThrow(error, { nodeId: hud.nodeId, status: hud.status });
332
332
  }
333
333
  }
334
+ async function hideMobileHudOnTeardown(projectRoot, env = {}) {
335
+ if (process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false") return;
336
+ const input = {
337
+ node: { bridge_timeout_ms: 8e3, cdp_timeout_ms: 5e3 },
338
+ context: { nodeId: "teardown", projectRoot, artifactsDir: projectRoot, env }
339
+ };
340
+ try {
341
+ await bridgeCommand(input, ["hide-step"]);
342
+ } catch {
343
+ }
344
+ }
334
345
  function mobileHudSkippedOrThrow(error, extra = {}) {
335
346
  const reason = traceText(error);
336
- if (isMobileHudLifecycleSkip(reason)) {
347
+ const code = isRecord(error) && typeof error.code === "string" ? error.code : void 0;
348
+ if (isMobileHudLifecycleSkip(reason, code)) {
337
349
  process.stderr.write(`app.hud skipped (bridge target down or transitioning): ${reason}
338
350
  `);
339
351
  return {
@@ -346,20 +358,30 @@ function mobileHudSkippedOrThrow(error, extra = {}) {
346
358
  }
347
359
  throw new Error(`app.hud bridge command failed outside a lifecycle/no-target transition: ${reason}`);
348
360
  }
349
- function isMobileHudLifecycleSkip(reason) {
361
+ const HUD_LIFECYCLE_SKIP_CODES = /* @__PURE__ */ new Set([
362
+ MOBILE_BRIDGE_ERROR_CODES.NO_TARGET,
363
+ MOBILE_BRIDGE_ERROR_CODES.CDP_TIMEOUT,
364
+ MOBILE_BRIDGE_ERROR_CODES.WS_CLOSED,
365
+ MOBILE_BRIDGE_ERROR_CODES.METRO_UNREACHABLE
366
+ ]);
367
+ function isMobileHudLifecycleSkip(reason, code) {
368
+ if (code && HUD_LIFECYCLE_SKIP_CODES.has(code)) return true;
369
+ const haystack = reason.toLowerCase();
350
370
  return [
351
371
  "no responding bridge target",
352
- "Pinned android device",
372
+ "pinned android device",
353
373
  "bridge probe failed",
354
- "ECONNREFUSED",
374
+ "econnrefused",
355
375
  "timed out",
356
- "Timed out",
357
- "No React Native bridge target",
376
+ "no react native bridge target",
377
+ // Metro down mid-run: HUD cannot paint, so skip rather than abort (the real
378
+ // action fails on its own). Matches the METRO_UNREACHABLE code branch above.
379
+ "cannot reach metro",
358
380
  // Dev-client foreground/reload kills the Hermes page mid-flight: pending
359
381
  // CDP calls surface as a message timeout or a closed socket.
360
- "CDP message timeout",
361
- "WebSocket closed"
362
- ].some((needle) => reason.includes(needle));
382
+ "cdp message timeout",
383
+ "websocket closed"
384
+ ].some((needle) => haystack.includes(needle));
363
385
  }
364
386
  function animatedFlag(payload) {
365
387
  return payload.animated === true ? "--animated" : "--no-animated";
@@ -511,5 +533,6 @@ function targetProbeUrl(platform, targetPort) {
511
533
  export {
512
534
  createMetaMaskAdapters,
513
535
  createMetaMaskUiTransport,
536
+ hideMobileHudOnTeardown,
514
537
  isMobileHudLifecycleSkip
515
538
  };
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { listRecipeFiles } from "./recipe-files.js";
4
5
  const SPEC = {
5
6
  shim: [
6
7
  { name: "sync", desc: "Copy fixture, write slot context, install harness", flags: ["--dry-run", "--no-install"] },
@@ -16,7 +17,7 @@ const SPEC = {
16
17
  { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
17
18
  { name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
18
19
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port", "--device"] },
19
- { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
20
+ { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
20
21
  { name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
21
22
  { name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
22
23
  { name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
@@ -124,9 +125,9 @@ function listRunRecipes(adapter, root, cwd = process.cwd()) {
124
125
  for (const resolveDir of RUN_RECIPE_DIRS) {
125
126
  const dir = resolveDir(adapter, root);
126
127
  if (!fs.existsSync(dir)) continue;
127
- for (const name of fs.readdirSync(dir)) {
128
- if (!name.endsWith(".recipe.json") || byBase.has(name)) continue;
129
- byBase.set(name, path.join(dir, name));
128
+ for (const rel of listRecipeFiles(dir)) {
129
+ if (byBase.has(rel)) continue;
130
+ byBase.set(rel, path.join(dir, rel));
130
131
  }
131
132
  }
132
133
  return [...byBase.values()].map((abs) => path.relative(cwd, abs)).sort();