@deeeed/metamask-harness 0.2.0 → 0.3.1

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 (110) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +47 -17
  3. package/{orchestration → adapters}/core/cleanup.sh +2 -2
  4. package/{orchestration → adapters}/core/inject.sh +8 -8
  5. package/{orchestration → adapters}/extension/cleanup.mjs +1 -1
  6. package/{orchestration → adapters}/extension/console-tail.mjs +3 -0
  7. package/{orchestration → adapters}/extension/ensure-browser.sh +4 -4
  8. package/{orchestration → adapters}/extension/inject.mjs +27 -31
  9. package/{orchestration → adapters}/extension/launch.sh +4 -4
  10. package/{orchestration → adapters}/extension/live.sh +5 -5
  11. package/{orchestration → adapters}/extension/readiness.mjs +12 -2
  12. package/{orchestration → adapters}/extension/refresh-build.sh +1 -1
  13. package/{orchestration → adapters}/extension/seed-fixture.sh +1 -1
  14. package/{orchestration → adapters}/extension/start-watch.sh +7 -7
  15. package/{runner → adapters}/extension/verify.sh +11 -11
  16. package/{orchestration → adapters}/extension/wallet-fixture-state.cjs +2 -2
  17. package/{orchestration → adapters}/manifest.json +96 -96
  18. package/{library/actions → adapters}/mobile/bridge-runtime/cdp-bridge.cjs +5 -5
  19. package/{orchestration → adapters}/mobile/cleanup.sh +3 -3
  20. package/{orchestration → adapters}/mobile/inject.sh +22 -24
  21. package/adapters/mobile/open-device.sh +260 -0
  22. package/{orchestration/compat-overlays → adapters/mobile/overlay/compat}/README.md +13 -4
  23. package/adapters/mobile/prewarm-bundle.sh +95 -0
  24. package/adapters/mobile/start-metro.sh +228 -0
  25. package/{runner → adapters}/mobile/verify.sh +6 -6
  26. package/adapters/mobile/wait-for-bridge.sh +108 -0
  27. package/adapters/mobile/yarn-setup.sh +20 -0
  28. package/{orchestration/lib → adapters/shared}/cli-ux.sh +5 -9
  29. package/{orchestration/lib → adapters/shared}/log-tui.mjs +57 -1
  30. package/{orchestration/lib → adapters/shared}/open-debug.mjs +32 -10
  31. package/{orchestration/lib → adapters/shared}/resolve-farmslot-ports.sh +47 -0
  32. package/adapters/shared/sync-wallet-fixture.sh +78 -0
  33. package/bin/mm-harness +37 -6
  34. package/docs/CLI-SPEC.md +41 -2
  35. package/docs/architecture.md +69 -38
  36. package/docs/runtime-file-conventions.md +3 -3
  37. package/library/actions/core/perps/_controller.mjs +1 -1
  38. package/library/actions/extension/platform/cdp.mjs +2 -2
  39. package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
  40. package/library/actions/mobile/platform/bridge.mjs +1 -1
  41. package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
  42. package/library/actions/mobile/wallet/setup.mjs +2 -2
  43. package/library/library.json +1 -1
  44. package/package.json +8 -8
  45. package/{orchestration → src/adapters}/extension/runtime-decision.ts +5 -5
  46. package/{orchestration → src/adapters}/extension/runtime.ts +1 -1
  47. package/src/adapters/mobile/prepare.ts +210 -0
  48. package/{orchestration → src/adapters}/mobile/runtime-decision.ts +105 -5
  49. package/{runner/src → src}/adapters.ts +3 -3
  50. package/{orchestration/lib/cli-color.mjs → src/cli-color.ts} +21 -13
  51. package/{orchestration/lib/cli-commands.mjs → src/cli-commands.ts} +52 -45
  52. package/{orchestration/lib/cli-version.mjs → src/cli-version.ts} +35 -17
  53. package/{runner/src → src}/cli.ts +48 -40
  54. package/src/commands/debug.ts +63 -0
  55. package/src/commands/fixtures.ts +193 -0
  56. package/{runner/src → src}/commands/launch.ts +200 -54
  57. package/src/commands/logs.ts +83 -0
  58. package/{runner/src → src}/commands/shared.ts +40 -30
  59. package/src/commands/update.ts +316 -0
  60. package/{runner/src → src}/harness.ts +119 -16
  61. package/{runner/src → src}/heal-bounds.ts +20 -1
  62. package/src/index.ts +15 -0
  63. package/{runner/src → src}/live-adapter-contract.ts +1 -1
  64. package/{runner/src → src}/mm-harness-cli.ts +64 -3
  65. package/{runner/src → src}/paths.ts +2 -2
  66. package/{orchestration/lib/progress.mjs → src/progress.ts} +31 -21
  67. package/docs/extension-runtime-commands.md +0 -60
  68. package/docs/harness-cli.md +0 -43
  69. package/orchestration/compat-overlays/mobile/README.md +0 -13
  70. package/orchestration/doctor.mjs +0 -72
  71. package/orchestration/lib/cli-home.mjs +0 -354
  72. package/orchestration/lib/recipe-paths.mjs +0 -26
  73. package/orchestration/mobile/launch.sh +0 -137
  74. package/orchestration/mobile/live.sh +0 -125
  75. package/orchestration/porcelain/metamask-recipe +0 -99
  76. package/orchestration/porcelain/mm-recipe +0 -1591
  77. package/orchestration/porcelain/mme-recipe +0 -1181
  78. package/runner/src/commands/debug.ts +0 -44
  79. package/runner/src/commands/fixtures.ts +0 -99
  80. package/runner/src/commands/logs.ts +0 -60
  81. package/runner/src/index.ts +0 -6
  82. /package/{orchestration → adapters}/extension/launch-browser.cjs +0 -0
  83. /package/{orchestration → adapters}/extension/pin-remote-flags.cjs +0 -0
  84. /package/{orchestration → adapters}/extension/sidepanel-toggle.sh +0 -0
  85. /package/{orchestration → adapters}/extension/snapshot-dist.sh +0 -0
  86. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -0
  87. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/config.cjs +0 -0
  88. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/issue-capture.cjs +0 -0
  89. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/target-discovery.cjs +0 -0
  90. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/ws-client.cjs +0 -0
  91. /package/{library/actions → adapters}/mobile/bridge-runtime/setup-wallet.sh +0 -0
  92. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -0
  93. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -0
  94. /package/{orchestration/compat-overlays/mobile → adapters/mobile/overlay/compat}/rn81-message-event-source.patch +0 -0
  95. /package/{orchestration/lib → adapters/shared}/activate-repo-node.sh +0 -0
  96. /package/{orchestration/lib → adapters/shared}/ensure-runner-deps.sh +0 -0
  97. /package/{orchestration/lib → adapters/shared}/harness-path.sh +0 -0
  98. /package/{orchestration/lib → adapters/shared}/hash-helpers.sh +0 -0
  99. /package/{orchestration/lib → adapters/shared}/json-field.sh +0 -0
  100. /package/{orchestration/lib → adapters/shared}/path-defaults.json +0 -0
  101. /package/{orchestration → src/adapters}/extension/ensure-ready.ts +0 -0
  102. /package/{orchestration → src/adapters}/extension/extension-id.ts +0 -0
  103. /package/{orchestration → src/adapters}/mobile/deps-markers.ts +0 -0
  104. /package/{runner/src → src}/completions-cache.ts +0 -0
  105. /package/{runner/src → src}/doctor.ts +0 -0
  106. /package/{runner/src → src}/manifest.ts +0 -0
  107. /package/{runner/src → src}/recording-target.ts +0 -0
  108. /package/{runner/src → src}/run-recording.ts +0 -0
  109. /package/{runner/src → src}/runner.ts +0 -0
  110. /package/{runner/src → src}/types.ts +0 -0
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env bash
2
+ # start-metro — ensure the Metro bundler is running for this MetaMask Mobile checkout.
3
+ #
4
+ # Checks if Metro is already running and owned by this checkout with required env vars.
5
+ # Clears stale or foreign listeners before starting. Uses yarn expo start --port (not
6
+ # yarn start), sources .js.env/.env/.env.local, sets EXPO_NO_TYPESCRIPT_SETUP=1, and
7
+ # caps METRO_MAX_WORKERS to 6 (or the physical CPU count).
8
+ #
9
+ # Inputs:
10
+ # --target <metamask-mobile dir> (default $PWD)
11
+ # --port <number> (default: WATCHER_PORT env, else METRO_PORT, else 8081)
12
+ # --clear reset Metro bundler cache before starting
13
+ # Outputs:
14
+ # Progress on stderr; exit 0 Metro ready; 1 failed to start; 2 bad args.
15
+ #
16
+ # Single device op: ensure Metro is running. Does not open the dev client or install deps.
17
+ set -uo pipefail
18
+
19
+ TARGET="$PWD"
20
+ PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
21
+ CLEAR=false
22
+
23
+ while [ "$#" -gt 0 ]; do
24
+ case "$1" in
25
+ --target) TARGET="$2"; shift 2 ;;
26
+ --port) PORT="$2"; shift 2 ;;
27
+ --clear) CLEAR=true; shift ;;
28
+ -h|--help)
29
+ printf 'Usage: start-metro.sh [--target <dir>] [--port <port>] [--clear]\n'
30
+ printf ' --target MetaMask Mobile checkout directory\n'
31
+ printf ' --port Metro port (default: WATCHER_PORT env or 8081)\n'
32
+ printf ' --clear restart with cleared cache\n'
33
+ exit 0
34
+ ;;
35
+ *) printf 'start-metro: unknown arg: %s\n' "$1" >&2; exit 2 ;;
36
+ esac
37
+ done
38
+
39
+ TARGET="$(cd "$TARGET" && pwd)"
40
+ LOG_DIR="$TARGET/temp/recipe/runtime"
41
+ mkdir -p "$LOG_DIR"
42
+ LOG_FILE="$LOG_DIR/metro.log"
43
+ PID_FILE="$LOG_DIR/metro.pid"
44
+
45
+ # --- helpers ------------------------------------------------------------------
46
+
47
+ metro_ready() {
48
+ curl -sf --max-time 2 "http://localhost:${PORT}/status" 2>/dev/null | grep -q 'packager-status:running'
49
+ }
50
+
51
+ metro_listener_pids() {
52
+ lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true
53
+ }
54
+
55
+ metro_listener_valid() {
56
+ local pids pid command rv
57
+ pids="$(metro_listener_pids)"
58
+ [ -n "$(printf '%s' "$pids" | tr -d ' ')" ] || return 1
59
+ rv=0
60
+ for pid in $pids; do
61
+ command="$(ps eww -p "$pid" -o command= 2>/dev/null || true)"
62
+ case "$command" in
63
+ *"$TARGET"*) ;;
64
+ *)
65
+ printf 'Metro on port %s not owned by this checkout (pid %s); restarting\n' "$PORT" "$pid" >&2
66
+ rv=1; break
67
+ ;;
68
+ esac
69
+ for required_var in ${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID}; do
70
+ if ! printf '%s\n' "$command" | grep -Eq "(^|[[:space:]])${required_var}="; then
71
+ printf 'Metro on port %s missing required env %s (pid %s); restarting\n' "$PORT" "$required_var" "$pid" >&2
72
+ rv=1; break 2
73
+ fi
74
+ done
75
+ done
76
+ return "$rv"
77
+ }
78
+
79
+ stop_metro_listener() {
80
+ local pids i
81
+ pids="$(metro_listener_pids)"
82
+ [ -n "$(printf '%s' "$pids" | tr -d ' ')" ] || return 0
83
+ printf 'Stopping Metro listener on port %s: %s\n' "$PORT" "$pids" >&2
84
+ # shellcheck disable=SC2086
85
+ kill $pids 2>/dev/null || true
86
+ for i in $(seq 1 20); do
87
+ pids="$(metro_listener_pids)"
88
+ [ -z "$(printf '%s' "$pids" | tr -d ' ')" ] && return 0
89
+ sleep 0.25
90
+ done
91
+ printf 'Metro listener still held; force-killing: %s\n' "$pids" >&2
92
+ # shellcheck disable=SC2086
93
+ kill -KILL $pids 2>/dev/null || true
94
+ for i in $(seq 1 20); do
95
+ pids="$(metro_listener_pids)"
96
+ [ -z "$(printf '%s' "$pids" | tr -d ' ')" ] && return 0
97
+ sleep 0.25
98
+ done
99
+ printf 'start-metro: port %s still occupied after force-kill: %s\n' "$PORT" "$pids" >&2
100
+ return 1
101
+ }
102
+
103
+ default_metro_workers() {
104
+ local cores=""
105
+ if command -v sysctl >/dev/null 2>&1; then
106
+ cores="$(sysctl -n hw.perflevel0.physicalcpu 2>/dev/null || sysctl -n hw.physicalcpu 2>/dev/null || true)"
107
+ fi
108
+ if printf '%s' "${cores:-}" | grep -Eq '^[0-9]+$' && [ "${cores:-0}" -gt 0 ]; then
109
+ [ "${cores:-4}" -gt 6 ] && cores=6
110
+ printf '%s\n' "$cores"
111
+ else
112
+ printf '4\n'
113
+ fi
114
+ }
115
+
116
+ start_viewer_window() {
117
+ local metro_log="$1"
118
+ command -v tmux >/dev/null 2>&1 || return 0
119
+ local window="metro-${PORT}"
120
+ local metro_tmux="$LOG_DIR/metro.tmux"
121
+ local session=""
122
+ if [ -n "${slot:-}" ] && tmux has-session -t "=mm-${slot}" 2>/dev/null; then
123
+ session="mm-${slot}"
124
+ fi
125
+ if [ -z "$session" ]; then
126
+ session="$(tmux display-message -p '#S' 2>/dev/null || true)"
127
+ fi
128
+ [ -n "$session" ] || return 0
129
+ tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
130
+ tmux new-window -d -t "$session" -n "$window" "exec tail -n +1 -F $(printf '%q' "$metro_log")"
131
+ printf '%s:%s\n' "$session" "$window" > "$metro_tmux"
132
+ printf 'Metro log viewer → tmux %s:%s\n' "$session" "$window" >&2
133
+ }
134
+
135
+ # --- main ---------------------------------------------------------------------
136
+
137
+ # Decide whether to (re)start or skip.
138
+ if metro_ready; then
139
+ if [ "$CLEAR" = true ]; then
140
+ printf 'Restarting Metro on port %s with cleared cache\n' "$PORT" >&2
141
+ stop_metro_listener || exit 1
142
+ rm -f "$PID_FILE"
143
+ elif metro_listener_valid; then
144
+ printf 'Metro already running and valid on port %s\n' "$PORT" >&2
145
+ exit 0
146
+ else
147
+ stop_metro_listener || exit 1
148
+ rm -f "$PID_FILE"
149
+ fi
150
+ fi
151
+
152
+ stop_metro_listener || exit 1
153
+
154
+ METRO_WORKERS="${METRO_MAX_WORKERS:-$(default_metro_workers)}"
155
+ CLEAR_SUFFIX=""
156
+ [ "$CLEAR" = true ] && CLEAR_SUFFIX=" --clear"
157
+
158
+ printf 'Starting Metro on port %s (workers=%s%s)\n' \
159
+ "$PORT" "$METRO_WORKERS" "${CLEAR:+, clear}" >&2
160
+ printf '(Full log: %s — recipe logs | recipe logs --full)\n' "$LOG_FILE" >&2
161
+
162
+ # Metro runs detached, writing to the log. The tmux window is a read-only tail.
163
+ (
164
+ cd "$TARGET"
165
+ : > "$LOG_FILE"
166
+ # Source product env files (provides MM_INFURA_PROJECT_ID and friends).
167
+ set -a
168
+ # shellcheck disable=SC1091
169
+ [ ! -f .js.env ] || . ./.js.env
170
+ # shellcheck disable=SC1091
171
+ [ ! -f .env ] || . ./.env
172
+ # shellcheck disable=SC1091
173
+ [ ! -f .env.local ] || . ./.env.local
174
+ set +a
175
+ export EXPO_NO_TYPESCRIPT_SETUP=1
176
+ export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
177
+ export METRO_MAX_WORKERS="${METRO_WORKERS}"
178
+ # shellcheck disable=SC2094
179
+ bash -c "exec yarn expo start --port ${PORT}${CLEAR_SUFFIX}" \
180
+ </dev/null >> "$LOG_FILE" 2>&1 &
181
+ echo "$!" > "$PID_FILE"
182
+ )
183
+ # Disown the background process so it outlives this script.
184
+ disown "$(cat "$PID_FILE" 2>/dev/null || true)" 2>/dev/null || true
185
+
186
+ start_viewer_window "$LOG_FILE"
187
+
188
+ # Attach a log-tui watcher for compact event display (best-effort; non-blocking).
189
+ LOG_TUI="$(dirname "$0")/../shared/log-tui.mjs"
190
+ TAIL_PID=""
191
+ if [ -f "$LOG_TUI" ] && command -v node >/dev/null 2>&1 \
192
+ && [ "${METAMASK_RECIPE_AUTO_HUD:-1}" != "0" ]; then
193
+ node "$LOG_TUI" watch \
194
+ --log "$LOG_FILE" \
195
+ --events "${RECIPE_LOG_EVENTS:-12}" \
196
+ --mode compact \
197
+ --label "Metro bundler" \
198
+ --success-pattern 'Dev server ready' &
199
+ TAIL_PID=$!
200
+ fi
201
+
202
+ # Ready-poll: 90 × 1s = 90 s timeout.
203
+ READY=false
204
+ for _i in $(seq 1 90); do
205
+ if metro_ready; then
206
+ READY=true
207
+ break
208
+ fi
209
+ sleep 1
210
+ done
211
+
212
+ [ -n "$TAIL_PID" ] && { kill "$TAIL_PID" 2>/dev/null; wait "$TAIL_PID" 2>/dev/null || true; }
213
+
214
+ if [ "$READY" = true ]; then
215
+ printf 'Metro ready on port %s\n' "$PORT" >&2
216
+ printf ' Next: mm-harness logs (tail Metro)\n' >&2
217
+ exit 0
218
+ fi
219
+
220
+ # Failure: dump log events for diagnosis.
221
+ if [ -f "$LOG_TUI" ] && command -v node >/dev/null 2>&1; then
222
+ node "$LOG_TUI" events --log "$LOG_FILE" --events 20 >&2 || true
223
+ else
224
+ tail -40 "$LOG_FILE" >&2 || true
225
+ fi
226
+ printf 'start-metro: Metro did not become ready on port %s within 90s\n' "$PORT" >&2
227
+ printf ' Next: check %s for errors, or run mm-harness logs\n' "$LOG_FILE" >&2
228
+ exit 1
@@ -58,12 +58,12 @@ esac
58
58
 
59
59
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
60
60
  # shellcheck disable=SC1091
61
- for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../orchestration/lib/harness-path.sh" "$SCRIPT_DIR/../lib/harness-path.sh"; do
61
+ for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../adapters/shared/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
62
62
  [ -f "$_hp" ] && { . "$_hp"; break; }
63
63
  done
64
64
  unset _hp
65
65
  if ! command -v harness_root >/dev/null 2>&1; then
66
- echo "mobile verify: shared lib orchestration/lib/harness-path.sh not found; reinstall the runner." >&2
66
+ echo "mobile verify: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
67
67
  exit 1
68
68
  fi
69
69
  TARGET="$(cd "$TARGET" && pwd)"
@@ -71,7 +71,7 @@ RUNTIME_DIR="$(recipe_runtime_dir)"
71
71
  HARNESS_ROOT="$(harness_root)"
72
72
  HARNESS_REL="$HARNESS_ROOT/mobile"
73
73
  HARNESS_DIR="$(harness_dir "$TARGET" mobile)"
74
- RUNNER_BIN="$HARNESS_DIR/runner/bin/metamask-recipe"
74
+ RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
75
75
  ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
76
76
  mkdir -p "$ARTIFACTS/logs"
77
77
 
@@ -221,7 +221,7 @@ check_file() {
221
221
 
222
222
  check_file "$HARNESS_REL/manifest.json"
223
223
  check_file "$HARNESS_REL/action-manifest.json"
224
- check_file "$HARNESS_REL/runner/bin/metamask-recipe"
224
+ check_file "$HARNESS_REL/runner/bin/mm-harness"
225
225
  check_file "package.json"
226
226
  check_file "app/dev-tools/AgenticService/AgenticService.ts"
227
227
 
@@ -263,7 +263,7 @@ if (!runnerDir) {
263
263
  process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner source unavailable; overlay freshness compare skipped' }));
264
264
  process.exit(0);
265
265
  }
266
- const overlayDir = path.join(runnerDir, 'library', 'actions', 'mobile', 'app-overlay', 'app', 'core', 'AgenticService');
266
+ const overlayDir = path.join(runnerDir, 'adapters', 'mobile', 'overlay', 'app', 'core', 'AgenticService');
267
267
  let checked = 0;
268
268
  const drifted = [];
269
269
  const missing = [];
@@ -289,7 +289,7 @@ if (checked === 0) {
289
289
  const parts = [];
290
290
  if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
291
291
  if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
292
- process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun metamask-recipe install --force-overlay or recipe sync to refresh the in-repo HUD/AgenticService from the runner' }));
292
+ process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or recipe sync to refresh the in-repo HUD/AgenticService from the runner' }));
293
293
  } else {
294
294
  process.stdout.write(JSON.stringify({ name, status: 'pass' }));
295
295
  }
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # wait-for-bridge — poll the CDP bridge until a route target is registered.
3
+ #
4
+ # Calls cdp-bridge.cjs status in a poll loop. Emits structured diagnostics on
5
+ # each probe failure: Metro reachability, React Native target visibility, bundle
6
+ # progress. Fails with non-zero if the bridge is not ready within the poll limit.
7
+ #
8
+ # Inputs:
9
+ # --target <metamask-mobile dir> (default $PWD)
10
+ # --port <number> (default WATCHER_PORT env, else METRO_PORT, else 8081)
11
+ # --max-polls <n> (default MOBILE_BRIDGE_READY_POLLS env, else 90; 2s each = 180s)
12
+ # Outputs:
13
+ # Progress on stderr; exit 0 bridge ready; 1 timeout.
14
+ #
15
+ # Single device op: wait for bridge readiness. Does not start Metro or open the dev client.
16
+ set -uo pipefail
17
+
18
+ TARGET="$PWD"
19
+ PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
20
+ MAX_POLLS="${MOBILE_BRIDGE_READY_POLLS:-90}"
21
+
22
+ while [ "$#" -gt 0 ]; do
23
+ case "$1" in
24
+ --target) TARGET="$2"; shift 2 ;;
25
+ --port) PORT="$2"; shift 2 ;;
26
+ --max-polls) MAX_POLLS="$2"; shift 2 ;;
27
+ -h|--help)
28
+ printf 'Usage: wait-for-bridge.sh [--target <dir>] [--port <n>] [--max-polls <n>]\n'
29
+ exit 0
30
+ ;;
31
+ *) printf 'wait-for-bridge: unknown arg: %s\n' "$1" >&2; exit 2 ;;
32
+ esac
33
+ done
34
+
35
+ TARGET="$(cd "$TARGET" && pwd)"
36
+ LOG_DIR="$TARGET/temp/recipe/runtime"
37
+ mkdir -p "$LOG_DIR"
38
+
39
+ BRIDGE_CJS="$(dirname "$0")/bridge-runtime/cdp-bridge.cjs"
40
+ STATUS_LOG="$LOG_DIR/bridge-status.log"
41
+ METRO_LOG="$LOG_DIR/metro.log"
42
+
43
+ if [ ! -f "$BRIDGE_CJS" ]; then
44
+ printf 'wait-for-bridge: cdp-bridge.cjs not found at %s\n' "$BRIDGE_CJS" >&2
45
+ exit 1
46
+ fi
47
+
48
+ metro_ready() {
49
+ curl -sf --max-time 2 "http://localhost:${PORT}/status" 2>/dev/null | grep -q 'packager-status:running'
50
+ }
51
+
52
+ bundle_progress() {
53
+ local log="$1"
54
+ [ -f "$log" ] || return 0
55
+ local p
56
+ p="$(grep -E '^[[:space:]]*(iOS|Android).*index\.(js|tsx?).*%|Bundl(ed|ing)|Finished' "$log" 2>/dev/null | tail -1 | tr -d '\r' || true)"
57
+ [ -n "$p" ] && printf ' (%s)' "$p"
58
+ }
59
+
60
+ bridge_wait_reason() {
61
+ if ! metro_ready; then
62
+ printf 'Metro not reachable on port %s' "$PORT"
63
+ return
64
+ fi
65
+ local targets
66
+ targets="$(curl -sf --max-time 2 "http://localhost:${PORT}/json/list" 2>/dev/null || true)"
67
+ if [ "$targets" = "[]" ] || [ -z "$targets" ]; then
68
+ printf 'Metro running; no React Native debug target yet'
69
+ bundle_progress "$METRO_LOG"
70
+ return
71
+ fi
72
+ if [ -s "$STATUS_LOG" ] && grep -qxF '[]' "$STATUS_LOG"; then
73
+ printf 'React Native target visible; in-app bridge not ready yet'
74
+ bundle_progress "$METRO_LOG"
75
+ return
76
+ fi
77
+ printf 'bridge probe failed'
78
+ bundle_progress "$METRO_LOG"
79
+ }
80
+
81
+ bridge_has_route() {
82
+ (cd "$TARGET" && APP_ROOT="$TARGET" node "$BRIDGE_CJS" status > "$STATUS_LOG" 2>&1) \
83
+ && node - "$STATUS_LOG" <<'NODE'
84
+ const fs = require('fs');
85
+ try {
86
+ const value = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
87
+ const targets = Array.isArray(value) ? value : [value];
88
+ if (!targets.some((t) => t && typeof t === 'object' && t.route)) process.exit(1);
89
+ } catch { process.exit(1); }
90
+ NODE
91
+ }
92
+
93
+ for ATTEMPT in $(seq 1 "$MAX_POLLS"); do
94
+ if bridge_has_route; then
95
+ printf 'Mobile bridge ready\n' >&2
96
+ exit 0
97
+ fi
98
+ if [ "$ATTEMPT" = "1" ] || [ $((ATTEMPT % 5)) -eq 0 ]; then
99
+ printf 'Waiting for Mobile bridge (%s/%s): %s\n' "$ATTEMPT" "$MAX_POLLS" "$(bridge_wait_reason)" >&2
100
+ fi
101
+ sleep 2
102
+ done
103
+
104
+ cat "$STATUS_LOG" >&2 || true
105
+ printf 'wait-for-bridge: bridge did not become ready on port %s (%s polls × 2s = %ss)\n' \
106
+ "$PORT" "$MAX_POLLS" "$((MAX_POLLS * 2))" >&2
107
+ printf ' Next: check %s — or run mm-harness app-status\n' "$STATUS_LOG" >&2
108
+ exit 1
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # yarn-setup — install node_modules for MetaMask Mobile.
3
+ #
4
+ # Inputs: --target <metamask-mobile dir> (default $PWD)
5
+ # Outputs: yarn install output on stderr; exit 0 pass, 1 install failed, 2 bad args.
6
+ #
7
+ # Single device op: install node_modules. No other sequencing.
8
+ set -euo pipefail
9
+
10
+ TARGET="$PWD"
11
+ while [ "$#" -gt 0 ]; do
12
+ case "$1" in
13
+ --target) TARGET="$2"; shift 2 ;;
14
+ -h|--help) printf 'Usage: yarn-setup.sh [--target <dir>]\n'; exit 0 ;;
15
+ *) printf 'yarn-setup: unknown arg: %s\n' "$1" >&2; exit 2 ;;
16
+ esac
17
+ done
18
+
19
+ TARGET="$(cd "$TARGET" && pwd)"
20
+ cd "$TARGET" && exec yarn setup
@@ -26,19 +26,15 @@ recipe_color() {
26
26
  }
27
27
 
28
28
  recipe_progress_script() {
29
- printf '%s' "${RECIPE_PROGRESS_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/progress.mjs}"
30
- }
31
-
32
- recipe_home_script() {
33
- printf '%s' "${RECIPE_HOME_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/cli-home.mjs}"
29
+ printf '%s' "${RECIPE_PROGRESS_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/src/progress.ts}"
34
30
  }
35
31
 
36
32
  recipe_log_tui_script() {
37
- printf '%s' "${RECIPE_LOG_TUI_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/log-tui.mjs}"
33
+ printf '%s' "${RECIPE_LOG_TUI_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/adapters/shared/log-tui.mjs}"
38
34
  }
39
35
 
40
36
  recipe_version_script() {
41
- printf '%s' "${RECIPE_VERSION_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/cli-version.mjs}"
37
+ printf '%s' "${RECIPE_VERSION_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/src/cli-version.ts}"
42
38
  }
43
39
 
44
40
  recipe_log_ui_mode() {
@@ -79,7 +75,7 @@ recipe_instant_hint() {
79
75
 
80
76
  recipe_progress() {
81
77
  local verb="$1" phase="$2" message="$3" elapsed="${4:-}"
82
- local args=(emit --verb "$verb" --phase "$phase" --message "$message")
78
+ local args=(emit --command "$verb" --phase "$phase" --message "$message")
83
79
  [ -n "$elapsed" ] && args+=(--elapsed-ms "$elapsed")
84
80
  [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && args+=(--json)
85
81
  node "$(recipe_progress_script)" "${args[@]}" || true
@@ -87,7 +83,7 @@ recipe_progress() {
87
83
 
88
84
  recipe_done() {
89
85
  local verb="$1" status="$2" message="$3" elapsed="${4:-}"
90
- local args=(done --verb "$verb" --status "$status" --message "$message")
86
+ local args=(done --command "$verb" --status "$status" --message "$message")
91
87
  [ -n "$elapsed" ] && args+=(--elapsed-ms "$elapsed")
92
88
  [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && args+=(--json)
93
89
  node "$(recipe_progress_script)" "${args[@]}" || true
@@ -4,7 +4,63 @@
4
4
  */
5
5
  import fs from 'node:fs';
6
6
  import { parseArgs } from 'node:util';
7
- import { color, colorLogEvent } from './cli-color.mjs';
7
+ // Inline color helpers self-contained so both the runner copy and any
8
+ // installed copy work without an external module dependency.
9
+ const STYLES = {
10
+ reset: '\x1b[0m',
11
+ bold: '\x1b[1m',
12
+ dim: '\x1b[2m',
13
+ label: '\x1b[1;36m',
14
+ cmd: '\x1b[1m',
15
+ ok: '\x1b[1;32m',
16
+ warn: '\x1b[1;33m',
17
+ err: '\x1b[1;31m',
18
+ info: '\x1b[0;37m',
19
+ accent: '\x1b[1;35m',
20
+ path: '\x1b[2m',
21
+ comment: '\x1b[2m',
22
+ active: '\x1b[1m',
23
+ };
24
+
25
+ function colorEnabled(stream = process.stderr) {
26
+ if (process.env.NO_COLOR != null && process.env.NO_COLOR !== '' && process.env.NO_COLOR !== '0') {
27
+ return false;
28
+ }
29
+ if (process.env.RECIPE_NO_COLOR === '1') return false;
30
+ if (process.env.RECIPE_COLOR === '1' || process.env.FORCE_COLOR === '1') return true;
31
+ return Boolean(stream?.isTTY);
32
+ }
33
+
34
+ function color(style, text, { stream = process.stderr } = {}) {
35
+ const value = String(text);
36
+ if (!colorEnabled(stream)) return value;
37
+ const code = STYLES[style];
38
+ if (!code) return value;
39
+ return `${code}${value}${STYLES.reset}`;
40
+ }
41
+
42
+ function classifyLogEvent(event) {
43
+ const text = String(event || '');
44
+ if (/Module build failed|^ERROR in |BUILD FAILED|compiled with [1-9][0-9]* error/iu.test(text)) {
45
+ return 'err';
46
+ }
47
+ if (/compiled successfully|compiled with [0-9]+ warning|PASS runtime-launch|launch pass|verify pass|build complete/iu.test(text)) {
48
+ return 'ok';
49
+ }
50
+ if (/phase [0-9]\/[0-9]|prepare pipeline|snapshot|fixture:|dist-freshness|build-health/iu.test(text)) {
51
+ return 'label';
52
+ }
53
+ if (/webpack [0-9]+%|Bundl(ed|ing)|(iOS|Android).*%/iu.test(text)) {
54
+ return 'warn';
55
+ }
56
+ return 'info';
57
+ }
58
+
59
+ function colorLogEvent(event, { latest = false, stream = process.stderr } = {}) {
60
+ const style = classifyLogEvent(event);
61
+ const painted = color(style === 'label' ? 'label' : style, event, { stream });
62
+ return latest ? color('active', painted, { stream }) : painted;
63
+ }
8
64
 
9
65
  const DEFAULT_EVENTS = 10;
10
66
 
@@ -115,16 +115,34 @@ function openUrl(url) {
115
115
  return null;
116
116
  }
117
117
 
118
+ // CDP-eval fallback: invoke DevSettings native module directly via the bridge
119
+ // when the Metro HTTP endpoint is unreachable or returns a non-OK response.
120
+ // Used for both debug (openDebugger) and dev-menu (show).
121
+ async function tryDevSettingsCdpFallback(numericPort, normalizedAction) {
122
+ const bridgePath = new URL('cdp-bridge.cjs', import.meta.url).pathname;
123
+ const expression =
124
+ normalizedAction === 'dev-menu'
125
+ ? 'NativeModules.DevSettings.show()'
126
+ : 'NativeModules.DevSettings.openDebugger()';
127
+ const result = spawnSync(process.execPath, [bridgePath, 'eval', expression], {
128
+ encoding: 'utf8',
129
+ env: { ...process.env, WATCHER_PORT: String(numericPort) },
130
+ timeout: 15000,
131
+ });
132
+ return result.status === 0;
133
+ }
134
+
118
135
  async function openMobileDebugger(port, action, shouldOpen) {
119
136
  const normalizedAction = action === 'dev-menu' ? 'dev-menu' : 'debug';
120
- const path = normalizedAction === 'dev-menu' ? '/open-dev-menu' : '/open-debugger';
137
+ const urlPath = normalizedAction === 'dev-menu' ? '/open-dev-menu' : '/open-debugger';
121
138
  const numericPort = Number.parseInt(String(port), 10);
122
139
  if (!Number.isInteger(numericPort) || numericPort <= 0) {
123
140
  return { ok: false, adapter: 'mobile', action: normalizedAction, error: 'invalid-port', port };
124
141
  }
125
142
 
143
+ let metroError = null;
126
144
  try {
127
- const opened = await fetchOk(`http://127.0.0.1:${numericPort}${path}`, { method: 'POST' });
145
+ const opened = await fetchOk(`http://127.0.0.1:${numericPort}${urlPath}`, { method: 'POST' });
128
146
  if (opened) {
129
147
  return {
130
148
  ok: true,
@@ -135,16 +153,21 @@ async function openMobileDebugger(port, action, shouldOpen) {
135
153
  opened: shouldOpen,
136
154
  };
137
155
  }
156
+ metroError = 'metro-http-nok';
138
157
  } catch (error) {
158
+ metroError = error instanceof Error ? error.message : String(error);
159
+ }
160
+
161
+ // CDP-eval fallback via DevSettings native module.
162
+ const cdpOk = await tryDevSettingsCdpFallback(numericPort, normalizedAction);
163
+ if (cdpOk) {
139
164
  return {
140
- ok: false,
165
+ ok: true,
141
166
  adapter: 'mobile',
142
167
  action: normalizedAction,
143
- method: 'metro-http',
168
+ method: 'cdp-eval',
144
169
  port: numericPort,
145
- error: error instanceof Error ? error.message : String(error),
146
- hint: 'Metro may be down or the debug target is not ready. Try recipe refresh, then recipe runtime-status.',
147
- fallback: 'cdp-eval',
170
+ opened: shouldOpen,
148
171
  };
149
172
  }
150
173
 
@@ -154,9 +177,8 @@ async function openMobileDebugger(port, action, shouldOpen) {
154
177
  action: normalizedAction,
155
178
  method: 'metro-http',
156
179
  port: numericPort,
157
- error: 'metro-http-failed',
158
- hint: 'Metro rejected the open-debugger request. Try recipe refresh, then recipe runtime-status.',
159
- fallback: 'cdp-eval',
180
+ error: metroError,
181
+ hint: 'Metro may be down or the debug target is not ready. Check metro status and try again.',
160
182
  };
161
183
  }
162
184
 
@@ -142,3 +142,50 @@ apply_resolved_extension_ports() {
142
142
  esac
143
143
  done <<< "$resolved"
144
144
  }
145
+
146
+ # Mobile slot defaults: derive Metro port + iOS simulator name from directory suffix.
147
+ # Port formula: 8060 + N (slot 1 → 8061). iOS simulator: mm-N.
148
+ # Mirrors the extension convention (6660+N / 9010+N) for multi-slot mobile farms.
149
+ resolve_mobile_slot_defaults() {
150
+ local repo="$1" n
151
+ n="$(infer_extension_slot_suffix "$repo" || true)"
152
+ [ -n "$n" ] || return 1
153
+ printf 'WATCHER_PORT=%s\n' "$((8060 + n))"
154
+ printf 'IOS_SIMULATOR=mm-%s\n' "$n"
155
+ printf 'SLOT_ID=local-mobile-%s\n' "$n"
156
+ }
157
+
158
+ resolve_mobile_runtime_ports() {
159
+ local repo="${1:-.}"
160
+ if resolve_farmslot_ports_by_repo "$repo"; then
161
+ return 0
162
+ fi
163
+ resolve_mobile_slot_defaults "$repo"
164
+ }
165
+
166
+ apply_resolved_mobile_ports() {
167
+ local repo="${1:-.}" line key val from_pool=false resolved=""
168
+ if resolved="$(resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
169
+ from_pool=true
170
+ elif resolved="$(resolve_mobile_slot_defaults "$repo" 2>/dev/null)"; then
171
+ from_pool=false
172
+ else
173
+ return 0
174
+ fi
175
+ while IFS= read -r line; do
176
+ [ -n "$line" ] || continue
177
+ key="${line%%=*}"
178
+ val="${line#*=}"
179
+ case "$key" in
180
+ WATCHER_PORT)
181
+ if [ "$from_pool" = true ] || [ -z "${WATCHER_PORT:-}" ]; then WATCHER_PORT="$val"; fi
182
+ ;;
183
+ IOS_SIMULATOR)
184
+ if [ "$from_pool" = true ] || [ -z "${IOS_SIMULATOR:-}" ]; then IOS_SIMULATOR="$val"; fi
185
+ ;;
186
+ SLOT_ID)
187
+ if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
188
+ ;;
189
+ esac
190
+ done <<< "$resolved"
191
+ }