@deeeed/metamask-harness 0.10.0 → 0.12.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 +35 -0
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +0 -0
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch.sh +0 -0
- package/adapters/extension/live.sh +0 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +0 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +0 -0
- package/adapters/extension/sidepanel-toggle.sh +0 -0
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/start-watch.sh +0 -0
- package/adapters/extension/verify.sh +0 -0
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +47 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +343 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +31 -29
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +4 -0
- package/adapters/mobile/inject.sh +0 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-metro.sh +110 -36
- package/adapters/mobile/stop-metro.sh +21 -0
- package/adapters/mobile/verify.sh +0 -0
- package/adapters/mobile/wait-for-bridge.sh +15 -1
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/open-debug.mjs +5 -0
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
- package/adapters/shared/resolve-farmslot-ports.sh +0 -0
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -0
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -0
- package/dist/adapters.js +49 -19
- package/dist/app-lifecycle.js +72 -0
- package/dist/cli-commands.js +1 -1
- package/dist/commands/device-target.js +35 -9
- package/dist/commands/fixtures.js +13 -1
- package/dist/commands/launch/index.js +35 -7
- package/dist/commands/run-engine.js +1 -0
- package/dist/mm-harness-cli.js +1 -0
- package/dist/paths.js +8 -1
- package/dist/runner.js +10 -4
- package/docs/CLI-SPEC.md +2 -1
- package/docs/recipe-libraries.md +19 -0
- package/library/actions/mobile/platform/bridge.mjs +29 -7
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +66 -8
- package/library/manifests/mobile.action-manifest.json +18 -0
- package/library/recipes/app-lifecycle-android-smoke.mobile.recipe.json +87 -0
- package/library/recipes/perps-performance-background-resume.mobile.recipe.json +72 -0
- package/library/recipes/perps-performance-cold-start.mobile.recipe.json +72 -0
- package/library/recipes/perps-performance-warm-start.mobile.recipe.json +64 -0
- package/library/recipes/perps-performance.mobile.recipe.json +15 -9
- package/package.json +3 -3
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
|
@@ -77,6 +77,72 @@ default_metro_workers() {
|
|
|
77
77
|
# shellcheck disable=SC1091
|
|
78
78
|
. "$SCRIPT_DIR/lib/tmux-viewer.sh"
|
|
79
79
|
|
|
80
|
+
# Bridgeless RN gates console→Metro forwarding on console._isPolyfilled, which is
|
|
81
|
+
# false with the Hermes native console — device logs (incl. DevLogger) never reach
|
|
82
|
+
# metro.log. The forwarder attaches over CDP and appends them back. Kill switch:
|
|
83
|
+
# METAMASK_RECIPE_CONSOLE_FORWARD=0.
|
|
84
|
+
start_console_forwarder() {
|
|
85
|
+
local forwarder="$SCRIPT_DIR/bridge-runtime/console-forwarder.cjs"
|
|
86
|
+
local fwd_pid_file="$LOG_DIR/console-forwarder.pid"
|
|
87
|
+
[ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || return 0
|
|
88
|
+
{ [ -f "$forwarder" ] && command -v node >/dev/null 2>&1; } || return 0
|
|
89
|
+
if [ -f "$fwd_pid_file" ]; then
|
|
90
|
+
local old_fwd
|
|
91
|
+
old_fwd="$(cat "$fwd_pid_file" 2>/dev/null || true)"
|
|
92
|
+
# A crash can leave a stale pid file and the OS can recycle the pid for an
|
|
93
|
+
# unrelated process — only kill when the command line is really ours.
|
|
94
|
+
if [ -n "$old_fwd" ]; then
|
|
95
|
+
case "$(ps -p "$old_fwd" -o command= 2>/dev/null)" in
|
|
96
|
+
*console-forwarder.cjs*) kill "$old_fwd" 2>/dev/null ;;
|
|
97
|
+
esac
|
|
98
|
+
fi
|
|
99
|
+
rm -f "$fwd_pid_file"
|
|
100
|
+
fi
|
|
101
|
+
nohup node "$forwarder" --port "$PORT" --out "$LOG_FILE" \
|
|
102
|
+
</dev/null >> "$LOG_DIR/console-forwarder.err" 2>&1 &
|
|
103
|
+
echo "$!" > "$fwd_pid_file"
|
|
104
|
+
disown 2>/dev/null || true
|
|
105
|
+
printf 'Device console → metro.log (CDP forwarder pid %s)\n' "$(cat "$fwd_pid_file")" >&2
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
write_metro_runner() {
|
|
109
|
+
local runner="$1"
|
|
110
|
+
local clear_flag=""
|
|
111
|
+
[ "$CLEAR" = true ] && clear_flag=' --clear'
|
|
112
|
+
cat > "$runner" <<EOF
|
|
113
|
+
#!/usr/bin/env bash
|
|
114
|
+
set -uo pipefail
|
|
115
|
+
cd "$(printf '%q' "$TARGET")"
|
|
116
|
+
: > "$(printf '%q' "$LOG_FILE")"
|
|
117
|
+
set -a
|
|
118
|
+
[ ! -f .js.env ] || . ./.js.env
|
|
119
|
+
[ ! -f .env ] || . ./.env
|
|
120
|
+
[ ! -f .env.local ] || . ./.env.local
|
|
121
|
+
set +a
|
|
122
|
+
[ -n "\${METAMASK_BUILD_TYPE:-}" ] || export METAMASK_BUILD_TYPE=main
|
|
123
|
+
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
124
|
+
export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
|
|
125
|
+
export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
|
|
126
|
+
exec yarn expo start --port "$(printf '%q' "$PORT")"$clear_flag >> "$(printf '%q' "$LOG_FILE")" 2>&1
|
|
127
|
+
EOF
|
|
128
|
+
chmod +x "$runner"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
start_metro_tmux() {
|
|
132
|
+
command -v tmux >/dev/null 2>&1 || return 1
|
|
133
|
+
local session window runner
|
|
134
|
+
session="$(resolve_run_tmux_session "$LOG_DIR")"
|
|
135
|
+
{ [ -n "$session" ] && tmux has-session -t "=$session" 2>/dev/null; } || return 1
|
|
136
|
+
window="metro-${PORT}"
|
|
137
|
+
runner="$LOG_DIR/run-metro-${PORT}.sh"
|
|
138
|
+
write_metro_runner "$runner"
|
|
139
|
+
tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
|
|
140
|
+
tmux new-window -d -t "$session" -n "$window" "exec $(printf '%q' "$runner")" || return 1
|
|
141
|
+
printf '%s:%s\n' "$session" "$window" > "$LOG_DIR/metro.tmux"
|
|
142
|
+
printf 'Metro server → tmux %s:%s\n' "$session" "$window" >&2
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
|
|
80
146
|
# --- main ---------------------------------------------------------------------
|
|
81
147
|
|
|
82
148
|
# Detect stale metro.pid: if the pid file names a dead process, clean it up.
|
|
@@ -99,6 +165,7 @@ if metro_ready; then
|
|
|
99
165
|
rm -f "$PID_FILE"
|
|
100
166
|
elif metro_listener_valid; then
|
|
101
167
|
printf 'Metro already running and valid on port %s\n' "$PORT" >&2
|
|
168
|
+
start_console_forwarder
|
|
102
169
|
exit 0
|
|
103
170
|
else
|
|
104
171
|
stop_metro_listener || exit 1
|
|
@@ -116,42 +183,45 @@ printf 'Starting Metro on port %s (workers=%s%s)\n' \
|
|
|
116
183
|
"$PORT" "$METRO_WORKERS" "$CLEAR_LABEL" >&2
|
|
117
184
|
printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2
|
|
118
185
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
186
|
+
if ! start_metro_tmux; then
|
|
187
|
+
# Metro runs detached, writing to the log. The tmux window is a read-only tail.
|
|
188
|
+
(
|
|
189
|
+
cd "$TARGET"
|
|
190
|
+
: > "$LOG_FILE"
|
|
191
|
+
# Source product env files (provides MM_INFURA_PROJECT_ID and friends).
|
|
192
|
+
set -a
|
|
193
|
+
# shellcheck disable=SC1091
|
|
194
|
+
[ ! -f .js.env ] || . ./.js.env
|
|
195
|
+
# shellcheck disable=SC1091
|
|
196
|
+
[ ! -f .env ] || . ./.env
|
|
197
|
+
# shellcheck disable=SC1091
|
|
198
|
+
[ ! -f .env.local ] || . ./.env.local
|
|
199
|
+
set +a
|
|
200
|
+
# The quick-launch runs `expo start` directly, bypassing scripts/build.sh which
|
|
201
|
+
# passes the build type as an argument (start:ios = build.sh ios main dev). The
|
|
202
|
+
# fixture .js.env ships METAMASK_BUILD_TYPE="" (empty), which Metro's transform
|
|
203
|
+
# rejects. Default to the main dev client (what runway installs / launch targets)
|
|
204
|
+
# so already-installed slots work without re-syncing fixtures.
|
|
205
|
+
[ -n "${METAMASK_BUILD_TYPE:-}" ] || export METAMASK_BUILD_TYPE=main
|
|
206
|
+
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
207
|
+
export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
|
|
208
|
+
export METRO_MAX_WORKERS="${METRO_WORKERS}"
|
|
209
|
+
# shellcheck disable=SC2094
|
|
210
|
+
# nohup sets SIGHUP to ignored (inherited across exec), so Metro survives the
|
|
211
|
+
# launching shell or tmux window closing; when this subshell exits the process
|
|
212
|
+
# is reparented to init. nohup.out is suppressed (stdout/stderr both go to
|
|
213
|
+
# $LOG_FILE via >>).
|
|
214
|
+
metro_args=(expo start --port "$PORT")
|
|
215
|
+
[ "$CLEAR" = true ] && metro_args+=(--clear)
|
|
216
|
+
nohup yarn "${metro_args[@]}" \
|
|
217
|
+
</dev/null >> "$LOG_FILE" 2>&1 &
|
|
218
|
+
metro_pid="$!"
|
|
219
|
+
echo "$metro_pid" > "$PID_FILE"
|
|
220
|
+
disown "$metro_pid" 2>/dev/null || true
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
start_viewer_window "$LOG_FILE"
|
|
224
|
+
fi
|
|
155
225
|
|
|
156
226
|
# Attach a log-tui watcher for compact event display (best-effort; non-blocking).
|
|
157
227
|
LOG_TUI="$(dirname "$0")/../shared/log-tui.mjs"
|
|
@@ -180,7 +250,11 @@ done
|
|
|
180
250
|
[ -n "$TAIL_PID" ] && { kill "$TAIL_PID" 2>/dev/null; wait "$TAIL_PID" 2>/dev/null || true; }
|
|
181
251
|
|
|
182
252
|
if [ "$READY" = true ]; then
|
|
253
|
+
ready_pids="$(metro_listener_pids)"
|
|
254
|
+
first_ready_pid="${ready_pids%% *}"
|
|
255
|
+
[ -z "$first_ready_pid" ] || printf '%s\n' "$first_ready_pid" > "$PID_FILE"
|
|
183
256
|
printf 'Metro ready on port %s\n' "$PORT" >&2
|
|
257
|
+
start_console_forwarder
|
|
184
258
|
printf ' Next: mm-harness logs (tail Metro)\n' >&2
|
|
185
259
|
exit 0
|
|
186
260
|
fi
|
|
@@ -62,6 +62,27 @@ fi
|
|
|
62
62
|
set +e
|
|
63
63
|
rm -f "$PID_FILE"
|
|
64
64
|
|
|
65
|
+
# start-metro runs a per-checkout console-forwarder holding the device debugger
|
|
66
|
+
# slot; Metro going down must take it too, or the orphan keeps polling and later
|
|
67
|
+
# fights a fresh forwarder over the slot. Kill the pid this LOG_DIR recorded,
|
|
68
|
+
# then sweep forwarders started under a different RECIPE_RUNTIME_DIR of this
|
|
69
|
+
# same checkout (their pid files live elsewhere; match on the --out path).
|
|
70
|
+
FWD_PID_FILE="$LOG_DIR/console-forwarder.pid"
|
|
71
|
+
fwd_pid="$(cat "$FWD_PID_FILE" 2>/dev/null || true)"
|
|
72
|
+
# A crash can leave a stale pid file and the OS can recycle the pid for an
|
|
73
|
+
# unrelated process — only kill when the command line is really ours.
|
|
74
|
+
if [ -n "$fwd_pid" ]; then
|
|
75
|
+
case "$(ps -p "$fwd_pid" -o command= 2>/dev/null)" in
|
|
76
|
+
*console-forwarder.cjs*)
|
|
77
|
+
if kill "$fwd_pid" 2>/dev/null; then
|
|
78
|
+
printf 'Stopped console forwarder (pid %s)\n' "$fwd_pid" >&2
|
|
79
|
+
fi
|
|
80
|
+
;;
|
|
81
|
+
esac
|
|
82
|
+
fi
|
|
83
|
+
rm -f "$FWD_PID_FILE"
|
|
84
|
+
pkill -f "console-forwarder.cjs --port .* --out $TARGET/" 2>/dev/null
|
|
85
|
+
|
|
65
86
|
# Port-scoped stop above misses bundlers a prior launch left on a DIFFERENT port
|
|
66
87
|
# (port drift / missing pid file). Sweep every Metro bound to this checkout so
|
|
67
88
|
# stop fully cleans up — the leak that stacked bundlers across relaunches.
|
|
File without changes
|
|
@@ -93,7 +93,21 @@ const fs = require('fs');
|
|
|
93
93
|
try {
|
|
94
94
|
const value = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
95
95
|
const targets = Array.isArray(value) ? value : [value];
|
|
96
|
-
|
|
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);
|
|
97
111
|
} catch { process.exit(1); }
|
|
98
112
|
NODE
|
|
99
113
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -126,6 +126,11 @@ async function tryDevSettingsCdpFallback(numericPort, normalizedAction) {
|
|
|
126
126
|
: 'NativeModules.DevSettings.openDebugger()';
|
|
127
127
|
const result = spawnSync(process.execPath, [bridgePath, 'eval', expression], {
|
|
128
128
|
encoding: 'utf8',
|
|
129
|
+
// cdp-bridge resolves its debugger-slot lock (RECIPE_RUNTIME_DIR) against
|
|
130
|
+
// cwd; anchor it to the app checkout so the console forwarder sees the
|
|
131
|
+
// yield — APP_ROOT when a caller exports it, else the cwd mm-harness debug
|
|
132
|
+
// already set to the target.
|
|
133
|
+
cwd: process.env.APP_ROOT || process.cwd(),
|
|
129
134
|
env: { ...process.env, WATCHER_PORT: String(numericPort) },
|
|
130
135
|
timeout: 15000,
|
|
131
136
|
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
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, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
|
|
5
|
+
import { bridgeCommand, evalSync, 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
|
}
|
|
@@ -106,6 +106,7 @@ function firstScalarText(record, keys, label, fallback) {
|
|
|
106
106
|
}
|
|
107
107
|
function traceText(value) {
|
|
108
108
|
if (value === void 0 || value === null) return "";
|
|
109
|
+
if (value instanceof Error) return value.message;
|
|
109
110
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
110
111
|
return String(value);
|
|
111
112
|
}
|
|
@@ -189,22 +190,10 @@ function uiInputFor(context, input) {
|
|
|
189
190
|
}
|
|
190
191
|
function mobileProbeOutput(status, input, projectRoot) {
|
|
191
192
|
const entries = Array.isArray(status) ? status : [status];
|
|
192
|
-
const
|
|
193
|
-
input.node?.ios_simulator,
|
|
194
|
-
input.node?.simulator,
|
|
195
|
-
input.node?.android_device,
|
|
196
|
-
input.node?.adb_serial,
|
|
197
|
-
process.env.IOS_SIMULATOR,
|
|
198
|
-
process.env.ANDROID_DEVICE,
|
|
199
|
-
process.env.ADB_SERIAL
|
|
200
|
-
].filter((value) => typeof value === "string" && value.length > 0);
|
|
201
|
-
const selected = entries.find((entry) => {
|
|
202
|
-
if (!isRecord(entry)) return false;
|
|
203
|
-
return typeof entry.deviceName === "string" && preferredDevices.includes(entry.deviceName);
|
|
204
|
-
}) ?? entries.find((entry) => isRecord(entry) && isRecord(entry.route)) ?? null;
|
|
193
|
+
const selected = selectBridgeStatusEntry(status, input) ?? null;
|
|
205
194
|
const route = isRecord(selected) && isRecord(selected.route) ? selected.route : null;
|
|
206
195
|
return {
|
|
207
|
-
reachable:
|
|
196
|
+
reachable: isRecord(selected) && selected.agenticPresent === true && Boolean(route),
|
|
208
197
|
bridge: mobileBridgePath(projectRoot),
|
|
209
198
|
targetCount: entries.filter((entry) => isRecord(entry)).length,
|
|
210
199
|
deviceName: isRecord(selected) && typeof selected.deviceName === "string" ? selected.deviceName : null,
|
|
@@ -327,10 +316,50 @@ async function handleMobileWaitFor(payload, context) {
|
|
|
327
316
|
}
|
|
328
317
|
async function handleMobileHud(payload, context) {
|
|
329
318
|
const input = mobileUiInput(context, "hud", payload);
|
|
330
|
-
if (payload.clear === true)
|
|
319
|
+
if (payload.clear === true) {
|
|
320
|
+
try {
|
|
321
|
+
return await bridgeCommand(input, ["hide-step"]);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return mobileHudSkippedOrThrow(error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
331
326
|
const hud = mobileHudPayload(payload, context);
|
|
332
|
-
|
|
333
|
-
|
|
327
|
+
try {
|
|
328
|
+
const result = await bridgeCommand(input, ["show-step-json", JSON.stringify(hud.step)]);
|
|
329
|
+
return { hud: true, nodeId: hud.nodeId, status: hud.status, result };
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return mobileHudSkippedOrThrow(error, { nodeId: hud.nodeId, status: hud.status });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function mobileHudSkippedOrThrow(error, extra = {}) {
|
|
335
|
+
const reason = traceText(error);
|
|
336
|
+
if (isMobileHudLifecycleSkip(reason)) {
|
|
337
|
+
process.stderr.write(`app.hud skipped (bridge target down or transitioning): ${reason}
|
|
338
|
+
`);
|
|
339
|
+
return {
|
|
340
|
+
hud: false,
|
|
341
|
+
skipped: true,
|
|
342
|
+
warning: "app.hud skipped because the mobile bridge target is down during lifecycle transition.",
|
|
343
|
+
...extra,
|
|
344
|
+
reason
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
throw new Error(`app.hud bridge command failed outside a lifecycle/no-target transition: ${reason}`);
|
|
348
|
+
}
|
|
349
|
+
function isMobileHudLifecycleSkip(reason) {
|
|
350
|
+
return [
|
|
351
|
+
"no responding bridge target",
|
|
352
|
+
"Pinned android device",
|
|
353
|
+
"bridge probe failed",
|
|
354
|
+
"ECONNREFUSED",
|
|
355
|
+
"timed out",
|
|
356
|
+
"Timed out",
|
|
357
|
+
"No React Native bridge target",
|
|
358
|
+
// Dev-client foreground/reload kills the Hermes page mid-flight: pending
|
|
359
|
+
// CDP calls surface as a message timeout or a closed socket.
|
|
360
|
+
"CDP message timeout",
|
|
361
|
+
"WebSocket closed"
|
|
362
|
+
].some((needle) => reason.includes(needle));
|
|
334
363
|
}
|
|
335
364
|
function animatedFlag(payload) {
|
|
336
365
|
return payload.animated === true ? "--animated" : "--no-animated";
|
|
@@ -481,5 +510,6 @@ function targetProbeUrl(platform, targetPort) {
|
|
|
481
510
|
}
|
|
482
511
|
export {
|
|
483
512
|
createMetaMaskAdapters,
|
|
484
|
-
createMetaMaskUiTransport
|
|
513
|
+
createMetaMaskUiTransport,
|
|
514
|
+
isMobileHudLifecycleSkip
|
|
485
515
|
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function resolveMetaMaskMobileLifecycleTarget(node, rawContext) {
|
|
2
|
+
const context = rawContext;
|
|
3
|
+
const platform = resolveMobilePlatform(node, context);
|
|
4
|
+
const port = scalar(node.watcher_port ?? node.metro_port, "app.lifecycle.metro_port") ?? context.env.WATCHER_PORT ?? context.env.METRO_PORT ?? process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "8081";
|
|
5
|
+
const launchUrl = scalar(node.launch_url ?? node.launchUrl ?? node.url, "app.lifecycle.launch_url") ?? expoDevClientUrl(port, platform);
|
|
6
|
+
if (platform === "android") {
|
|
7
|
+
return {
|
|
8
|
+
platform,
|
|
9
|
+
deviceId: scalar(node.adb_serial ?? node.android_device ?? node.device, "app.lifecycle.device") ?? context.env.ADB_SERIAL ?? context.env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL,
|
|
10
|
+
appId: scalar(node.package_id ?? node.packageName ?? node.app_id, "app.lifecycle.package_id") ?? context.env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID ?? "io.metamask",
|
|
11
|
+
launchUrl,
|
|
12
|
+
metroPort: port,
|
|
13
|
+
prelaunchCalls: expoBundlePrewarmCalls(port, "android")
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
platform: "ios-simulator",
|
|
18
|
+
deviceId: scalar(node.simulator ?? node.ios_simulator ?? node.device, "app.lifecycle.device") ?? context.env.IOS_SIMULATOR ?? context.env.SIM_UDID ?? process.env.IOS_SIMULATOR ?? process.env.SIM_UDID ?? "booted",
|
|
19
|
+
appId: scalar(node.bundle_id ?? node.bundleId ?? node.app_id, "app.lifecycle.bundle_id") ?? context.env.IOS_BUNDLE_ID ?? process.env.IOS_BUNDLE_ID ?? "io.metamask.MetaMask",
|
|
20
|
+
launchUrl,
|
|
21
|
+
prelaunchCalls: expoBundlePrewarmCalls(port, "ios")
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function resolveMobilePlatform(node, context) {
|
|
25
|
+
const raw = scalar(node.platform, "app.lifecycle.platform") ?? context.env.PLATFORM ?? process.env.PLATFORM;
|
|
26
|
+
if (raw === "android" || raw === "ios") return raw;
|
|
27
|
+
if (node.adb_serial || node.android_device || context.env.ADB_SERIAL || context.env.ANDROID_SERIAL || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
|
|
28
|
+
return "android";
|
|
29
|
+
}
|
|
30
|
+
return "ios";
|
|
31
|
+
}
|
|
32
|
+
function expoDevClientUrl(port, platform) {
|
|
33
|
+
const scheme = (platform === "ios" ? process.env.IOS_DEV_CLIENT_SCHEME : process.env.ANDROID_DEV_CLIENT_SCHEME) ?? "expo-metamask";
|
|
34
|
+
const metroUrl = `http://localhost:${port}?disableOnboarding=1`;
|
|
35
|
+
return `${scheme}://expo-development-client/?url=${encodeURIComponent(metroUrl)}`;
|
|
36
|
+
}
|
|
37
|
+
function expoBundlePrewarmCalls(port, platform) {
|
|
38
|
+
const params = new URLSearchParams({
|
|
39
|
+
platform,
|
|
40
|
+
dev: "true",
|
|
41
|
+
hot: "false",
|
|
42
|
+
lazy: "true",
|
|
43
|
+
"transform.engine": "hermes",
|
|
44
|
+
"transform.bytecode": "1",
|
|
45
|
+
"transform.routerRoot": "app",
|
|
46
|
+
unstable_transformProfile: "hermes-stable"
|
|
47
|
+
});
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
file: "curl",
|
|
51
|
+
args: [
|
|
52
|
+
"-fsS",
|
|
53
|
+
"-o",
|
|
54
|
+
"/dev/null",
|
|
55
|
+
"--max-time",
|
|
56
|
+
"60",
|
|
57
|
+
`http://localhost:${port}/index.bundle?${params}`
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function scalar(value, label) {
|
|
63
|
+
if (value == null) return void 0;
|
|
64
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
65
|
+
const text = String(value);
|
|
66
|
+
return text.length > 0 ? text : void 0;
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`${label} must be a string, number, or boolean.`);
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
resolveMetaMaskMobileLifecycleTarget
|
|
72
|
+
};
|
package/dist/cli-commands.js
CHANGED
|
@@ -13,7 +13,7 @@ const SPEC = {
|
|
|
13
13
|
{ name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
|
|
14
14
|
{ name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
|
|
15
15
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
16
|
-
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--json"] },
|
|
16
|
+
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
|
|
17
17
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
|
|
18
18
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port", "--device"] },
|
|
19
19
|
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
|
|
@@ -1,16 +1,37 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { listConnectedDevices } from "../devices.js";
|
|
2
3
|
import { optionString } from "./parse-args.js";
|
|
3
|
-
function
|
|
4
|
+
function normalizeDeviceName(value) {
|
|
5
|
+
return value.replace(/_/gu, " ").trim();
|
|
6
|
+
}
|
|
7
|
+
function resolveAndroidModel(serial) {
|
|
8
|
+
try {
|
|
9
|
+
const output = execFileSync("adb", ["-s", serial, "shell", "getprop", "ro.product.model"], {
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
12
|
+
timeout: 5e3
|
|
13
|
+
}).trim();
|
|
14
|
+
return output || void 0;
|
|
15
|
+
} catch {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function setAndroidDeviceEnv(id, fallbackName) {
|
|
4
20
|
process.env.ADB_SERIAL = id;
|
|
5
21
|
process.env.ANDROID_SERIAL = id;
|
|
6
22
|
process.env.ANDROID_DEVICE = id;
|
|
23
|
+
const model = resolveAndroidModel(id) ?? (fallbackName ? normalizeDeviceName(fallbackName) : void 0);
|
|
24
|
+
if (model) process.env.ANDROID_TARGET_DEVICE_NAME = model;
|
|
25
|
+
else delete process.env.ANDROID_TARGET_DEVICE_NAME;
|
|
7
26
|
process.env.IOS_SIMULATOR = "";
|
|
8
27
|
}
|
|
9
|
-
function setIosDeviceEnv(id) {
|
|
10
|
-
process.env.IOS_SIMULATOR = id;
|
|
28
|
+
function setIosDeviceEnv(id, fallbackName) {
|
|
29
|
+
process.env.IOS_SIMULATOR = fallbackName ? normalizeDeviceName(fallbackName) : id;
|
|
30
|
+
process.env.SIM_UDID = id;
|
|
11
31
|
process.env.ADB_SERIAL = "";
|
|
12
32
|
process.env.ANDROID_SERIAL = "";
|
|
13
33
|
process.env.ANDROID_DEVICE = "";
|
|
34
|
+
process.env.ANDROID_TARGET_DEVICE_NAME = "";
|
|
14
35
|
}
|
|
15
36
|
function formatConnectedDevices(devices) {
|
|
16
37
|
return devices.map((d) => ` - ${d.id}${d.name ? ` (${d.name})` : ""} [${d.state}] ${d.platform}`).join("\n");
|
|
@@ -30,7 +51,7 @@ function deviceSelected(device) {
|
|
|
30
51
|
if (device.platform === "android") {
|
|
31
52
|
return process.env.ADB_SERIAL === device.id || process.env.ANDROID_SERIAL === device.id;
|
|
32
53
|
}
|
|
33
|
-
return process.env.IOS_SIMULATOR === device.id;
|
|
54
|
+
return process.env.IOS_SIMULATOR === device.id || process.env.SIM_UDID === device.id || process.env.IOS_SIMULATOR === device.name;
|
|
34
55
|
}
|
|
35
56
|
function isTargetable(device) {
|
|
36
57
|
if (device.platform === "android") return device.state === "device";
|
|
@@ -54,19 +75,24 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
54
75
|
const connected2 = listConnectedDevices();
|
|
55
76
|
const androidById = connected2.find((d) => d.platform === "android" && d.id === device);
|
|
56
77
|
if (androidById) {
|
|
57
|
-
setAndroidDeviceEnv(device);
|
|
78
|
+
setAndroidDeviceEnv(device, androidById.name);
|
|
58
79
|
return { ok: true };
|
|
59
80
|
}
|
|
60
81
|
const iosById = connected2.find((d) => d.platform === "ios" && d.id === device);
|
|
61
82
|
if (iosById) {
|
|
62
|
-
setIosDeviceEnv(device);
|
|
83
|
+
setIosDeviceEnv(device, iosById.name);
|
|
63
84
|
return { ok: true };
|
|
64
85
|
}
|
|
65
|
-
const
|
|
86
|
+
const normalizedDevice = normalizeDeviceName(device);
|
|
87
|
+
const byName = connected2.filter((d) => {
|
|
88
|
+
const name = d.name ?? "";
|
|
89
|
+
const normalizedName = normalizeDeviceName(name);
|
|
90
|
+
return name === device || normalizedName === normalizedDevice || d.platform === "android" && normalizedDevice.startsWith(`${normalizedName} -`);
|
|
91
|
+
});
|
|
66
92
|
if (byName.length === 1) {
|
|
67
93
|
const matched = byName[0];
|
|
68
|
-
if (matched.platform === "android") setAndroidDeviceEnv(matched.id);
|
|
69
|
-
else setIosDeviceEnv(matched.id);
|
|
94
|
+
if (matched.platform === "android") setAndroidDeviceEnv(matched.id, matched.name);
|
|
95
|
+
else setIosDeviceEnv(matched.id, matched.name);
|
|
70
96
|
return { ok: true };
|
|
71
97
|
}
|
|
72
98
|
if (byName.length > 1) {
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runnerDir, walletFixturePath } from "../paths.js";
|
|
4
4
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
|
+
import { applyDeviceTargeting } from "./device-target.js";
|
|
5
6
|
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
6
7
|
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
|
|
7
8
|
const RECOVERABLE_SETUP_WALLET_PATTERNS = [
|
|
@@ -19,6 +20,13 @@ const RECOVERABLE_SETUP_WALLET_PATTERNS = [
|
|
|
19
20
|
function isRecoverableSetupWalletFailure(output) {
|
|
20
21
|
return RECOVERABLE_SETUP_WALLET_PATTERNS.some((p) => output.includes(p));
|
|
21
22
|
}
|
|
23
|
+
function resolveMobileFixturePlatform(options) {
|
|
24
|
+
const explicit = str(options, "platform") ?? process.env["MOBILE_PLATFORM"];
|
|
25
|
+
if (explicit === "android" || explicit === "ios") return explicit;
|
|
26
|
+
if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE) return "android";
|
|
27
|
+
if (process.env.IOS_SIMULATOR) return "ios";
|
|
28
|
+
return "ios";
|
|
29
|
+
}
|
|
22
30
|
async function handleFixtures(argv, deps) {
|
|
23
31
|
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
24
32
|
const json = flag(options, "json");
|
|
@@ -31,6 +39,10 @@ async function handleFixtures(argv, deps) {
|
|
|
31
39
|
if (!adapter) {
|
|
32
40
|
return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
33
41
|
}
|
|
42
|
+
const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: false, rerun: "" });
|
|
43
|
+
if ("code" in dtResult) {
|
|
44
|
+
return usageOut(json, "fixtures", dtResult.message, "mm-harness fixtures set --adapter mobile --device <adb-serial|simulator-udid>");
|
|
45
|
+
}
|
|
34
46
|
if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
|
|
35
47
|
if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
|
|
36
48
|
const surface = getAdapterSurface(adapter);
|
|
@@ -64,7 +76,7 @@ async function handleFixtures(argv, deps) {
|
|
|
64
76
|
process.env["RECIPE_SETUP_WALLET_RETRIED"] = "1";
|
|
65
77
|
if (!json) process.stderr.write(" setup-wallet: recoverable failure \u2014 restarting Metro and retrying\n Next: wait for relaunch, then wallet setup will retry automatically\n");
|
|
66
78
|
const { prepareMobile } = await import("../adapters/mobile/prepare.js");
|
|
67
|
-
const platform =
|
|
79
|
+
const platform = resolveMobileFixturePlatform(options);
|
|
68
80
|
const relaunchResult = await prepareMobile(target, { platform, json, preflightMode: "auto", clearMetro: true });
|
|
69
81
|
if (relaunchResult.status === 0) {
|
|
70
82
|
result = await spawnScriptStreaming(setupWalletSh, ["--fixture", fixturePath], target);
|