@deeeed/metamask-harness 0.26.4 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/adapters/mobile/open-device.sh +16 -4
- package/adapters/mobile/start-metro.sh +33 -0
- package/dist/adapters/mobile/metro-env.js +9 -0
- package/dist/adapters/mobile/prepare.js +238 -37
- package/dist/adapters/mobile/runtime-decision.js +4 -1
- package/dist/adapters.js +19 -6
- package/dist/commands/device-target.js +4 -1
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/mobile-device-view.js +4 -1
- package/dist/devices.js +4 -1
- package/dist/doctor.js +25 -2
- package/dist/run-recording.js +128 -92
- package/library/actions/extension/platform/cdp.mjs +170 -117
- package/library/actions/mobile/analytics/set_consent.mjs +79 -43
- package/library/actions/mobile/platform/bridge.mjs +21 -9
- package/library/actions/mobile/platform/observe-ui.mjs +416 -0
- package/library/actions/mobile/platform/tool-paths.mjs +122 -0
- package/library/actions/shared/analytics/collector.mjs +50 -5
- package/library/manifests/mobile.action-manifest.json +25 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.27.0 - 2026-07-31
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Mobile UI actions now record bounded native-hierarchy observations, while defaulted Extension UI actions record screen and visible observations; Extension screenshots and recordings publish through symlink-safe private staging as regular `0600` files, Mobile launch and diagnostics resolve absolute device-tool paths, and iOS preparation enables application accessibility before relaunching.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Serve analytics capture on both IPv4 and IPv6 loopback so Mobile clients resolving `localhost` to `::1` can reach the collector without exposing it on external interfaces.
|
|
14
|
+
|
|
15
|
+
## 0.26.5 - 2026-07-31
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Forward Mobile launch's resolved Metro port to every runtime leaf so stale slot context cannot start, compile, launch, or observe a second Metro.
|
|
20
|
+
- Propagate inline Segment configuration into detached Mobile Metro runs and invalidate the Metro cache when it changes.
|
|
21
|
+
- Navigate through Mobile's root settings navigator before changing analytics consent.
|
|
22
|
+
|
|
5
23
|
## 0.26.4 - 2026-07-31
|
|
6
24
|
|
|
7
25
|
### Fixed
|
|
@@ -25,6 +25,7 @@ PLATFORM="ios"
|
|
|
25
25
|
TARGET="$PWD"
|
|
26
26
|
SIMULATOR="${IOS_SIMULATOR:-${SIM_UDID:-booted}}"
|
|
27
27
|
ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
|
|
28
|
+
ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
|
|
28
29
|
PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
|
|
29
30
|
PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
|
|
30
31
|
UI_ONLY=false
|
|
@@ -54,6 +55,17 @@ case "$PLATFORM" in
|
|
|
54
55
|
ios|android) ;;
|
|
55
56
|
*) printf 'open-device: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
|
|
56
57
|
esac
|
|
58
|
+
if [ "$PLATFORM" = "android" ]; then
|
|
59
|
+
if [ -z "$ADB_BIN" ]; then
|
|
60
|
+
ADB_BIN="$(command -v adb 2>/dev/null || true)"
|
|
61
|
+
fi
|
|
62
|
+
if [ -z "$ADB_BIN" ] || [ "${ADB_BIN#/}" = "$ADB_BIN" ] || [ ! -x "$ADB_BIN" ]; then
|
|
63
|
+
printf 'open-device: Android SDK Platform-Tools (adb) is required but no executable was found.\n' >&2
|
|
64
|
+
printf 'Next: brew install android-platform-tools\n' >&2
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
export MM_HARNESS_ADB_PATH="$ADB_BIN"
|
|
68
|
+
fi
|
|
57
69
|
if $UI_ONLY && [ "$PLATFORM" != "ios" ]; then
|
|
58
70
|
printf 'open-device: --ui-only is supported only for ios.\n' >&2
|
|
59
71
|
exit 2
|
|
@@ -207,7 +219,7 @@ focus_simulator() {
|
|
|
207
219
|
android_app_installed() {
|
|
208
220
|
local pkg="$1"
|
|
209
221
|
shift
|
|
210
|
-
|
|
222
|
+
"$ADB_BIN" "$@" shell pm list packages 2>/dev/null | grep -q "package:${pkg}"
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
run_android_native_build() {
|
|
@@ -315,14 +327,14 @@ else
|
|
|
315
327
|
[ -n "$ADB_SERIAL_ARG" ] && ADB_ARGS+=("-s" "$ADB_SERIAL_ARG")
|
|
316
328
|
|
|
317
329
|
# Reverse-tunnel Metro port so the device can reach localhost:PORT.
|
|
318
|
-
|
|
330
|
+
"$ADB_BIN" "${ADB_ARGS[@]}" reverse "tcp:${PORT}" "tcp:${PORT}" >/dev/null 2>&1 || \
|
|
319
331
|
printf 'open-device: adb reverse failed (non-fatal — device may already be reachable)\n' >&2
|
|
320
332
|
|
|
321
333
|
LAUNCHED=false
|
|
322
334
|
for PKG in "${ANDROID_PKGS[@]}"; do
|
|
323
335
|
ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}" || continue
|
|
324
336
|
if $RESTART; then
|
|
325
|
-
|
|
337
|
+
"$ADB_BIN" "${ADB_ARGS[@]}" shell am force-stop "$PKG" >/dev/null 2>&1 || true
|
|
326
338
|
printf 'Restarting Android dev client %s\n' "$PKG" >&2
|
|
327
339
|
fi
|
|
328
340
|
|
|
@@ -331,7 +343,7 @@ else
|
|
|
331
343
|
DEEP_LINK="expo-metamask://expo-development-client/?url=${ENCODED_URL}"
|
|
332
344
|
|
|
333
345
|
printf 'Launching Android dev client %s\n' "$PKG" >&2
|
|
334
|
-
if
|
|
346
|
+
if "$ADB_BIN" "${ADB_ARGS[@]}" shell am start \
|
|
335
347
|
-a android.intent.action.VIEW \
|
|
336
348
|
-d "$DEEP_LINK" >/dev/null 2>&1; then
|
|
337
349
|
LAUNCHED=true
|
|
@@ -57,6 +57,7 @@ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
|
|
|
57
57
|
mkdir -p "$LOG_DIR"
|
|
58
58
|
LOG_FILE="$LOG_DIR/metro.log"
|
|
59
59
|
PID_FILE="$LOG_DIR/metro.pid"
|
|
60
|
+
INLINE_ENV_FILE="$LOG_DIR/metro-inline-env.sh"
|
|
60
61
|
|
|
61
62
|
# --- helpers ------------------------------------------------------------------
|
|
62
63
|
|
|
@@ -73,6 +74,24 @@ default_metro_workers() {
|
|
|
73
74
|
fi
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
write_inline_metro_env() {
|
|
78
|
+
local temporary name
|
|
79
|
+
temporary="$(mktemp "$LOG_DIR/.metro-inline-env.XXXXXX")" || return 1
|
|
80
|
+
chmod 600 "$temporary" || { rm -f "$temporary"; return 1; }
|
|
81
|
+
for name in \
|
|
82
|
+
SEGMENT_PROXY_URL \
|
|
83
|
+
SEGMENT_WRITE_KEY \
|
|
84
|
+
SEGMENT_FLUSH_INTERVAL \
|
|
85
|
+
SEGMENT_FLUSH_EVENT_LIMIT
|
|
86
|
+
do
|
|
87
|
+
if declare -p "$name" >/dev/null 2>&1; then
|
|
88
|
+
printf 'export %s=%q\n' "$name" "${!name}" >> "$temporary" \
|
|
89
|
+
|| { rm -f "$temporary"; return 1; }
|
|
90
|
+
fi
|
|
91
|
+
done
|
|
92
|
+
mv -f "$temporary" "$INLINE_ENV_FILE"
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
# Log-tail viewer window (session-safe): sourced so its run-owned-session rule is
|
|
77
96
|
# unit-testable and can never leak a window into an unrelated user session.
|
|
78
97
|
# shellcheck disable=SC1091
|
|
@@ -96,6 +115,10 @@ set +a
|
|
|
96
115
|
# build.sh remaps MM_SENTRY_DSN from MM_SENTRY_DSN_DEV for dev builds; quick-launch
|
|
97
116
|
# bypasses it, so without this Sentry never initializes in the dev client.
|
|
98
117
|
[ -n "\${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="\${MM_SENTRY_DSN_DEV:-}"
|
|
118
|
+
[ ! -f $(printf '%q' "$INLINE_ENV_FILE") ] || {
|
|
119
|
+
. $(printf '%q' "$INLINE_ENV_FILE")
|
|
120
|
+
rm -f $(printf '%q' "$INLINE_ENV_FILE")
|
|
121
|
+
}
|
|
99
122
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
100
123
|
export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
|
|
101
124
|
export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
|
|
@@ -165,6 +188,11 @@ printf 'Starting Metro on port %s (workers=%s%s)\n' \
|
|
|
165
188
|
"$PORT" "$METRO_WORKERS" "$CLEAR_LABEL" >&2
|
|
166
189
|
printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2
|
|
167
190
|
|
|
191
|
+
write_inline_metro_env || {
|
|
192
|
+
printf 'start-metro: could not stage inline Metro environment\n' >&2
|
|
193
|
+
exit 1
|
|
194
|
+
}
|
|
195
|
+
|
|
168
196
|
if ! start_metro_tmux; then
|
|
169
197
|
# Metro runs detached, writing to the log. The tmux window is a read-only tail.
|
|
170
198
|
(
|
|
@@ -189,6 +217,11 @@ if ! start_metro_tmux; then
|
|
|
189
217
|
# = build.sh ios main dev); quick-launch bypasses build.sh, so without this the
|
|
190
218
|
# dev client's MM_SENTRY_DSN stays empty and Sentry never initializes.
|
|
191
219
|
[ -n "${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="${MM_SENTRY_DSN_DEV:-}"
|
|
220
|
+
[ ! -f "$INLINE_ENV_FILE" ] || {
|
|
221
|
+
# shellcheck disable=SC1090
|
|
222
|
+
. "$INLINE_ENV_FILE"
|
|
223
|
+
rm -f "$INLINE_ENV_FILE"
|
|
224
|
+
}
|
|
192
225
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
193
226
|
export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
|
|
194
227
|
export METRO_MAX_WORKERS="${METRO_WORKERS}"
|
|
@@ -3,6 +3,12 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { recipeRuntimePath } from "../../paths.js";
|
|
5
5
|
const ENV_INPUTS = [".js.env", ".env", ".env.local"];
|
|
6
|
+
const PROCESS_ENV_INPUTS = [
|
|
7
|
+
"SEGMENT_PROXY_URL",
|
|
8
|
+
"SEGMENT_WRITE_KEY",
|
|
9
|
+
"SEGMENT_FLUSH_INTERVAL",
|
|
10
|
+
"SEGMENT_FLUSH_EVENT_LIMIT"
|
|
11
|
+
];
|
|
6
12
|
const BASELINE_FILE = "metro-env-baseline.json";
|
|
7
13
|
function mobileMetroEnvFingerprint(target) {
|
|
8
14
|
const hash = createHash("sha256");
|
|
@@ -13,6 +19,9 @@ function mobileMetroEnvFingerprint(target) {
|
|
|
13
19
|
else hash.update("<absent>");
|
|
14
20
|
hash.update("\0");
|
|
15
21
|
}
|
|
22
|
+
for (const name of PROCESS_ENV_INPUTS) {
|
|
23
|
+
hash.update(`${name}\0${process.env[name] ?? "<absent>"}\0`);
|
|
24
|
+
}
|
|
16
25
|
return hash.digest("hex");
|
|
17
26
|
}
|
|
18
27
|
function mobileMetroEnvCheck(target) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { recordDepsBaseline } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
4
|
+
import { resolveAndExportMobileToolPaths } from "../../../library/actions/mobile/platform/tool-paths.mjs";
|
|
3
5
|
import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
|
|
4
6
|
import { runnerDir } from "../../paths.js";
|
|
5
7
|
import {
|
|
@@ -33,6 +35,14 @@ async function prepareMobile(target, opts = {}) {
|
|
|
33
35
|
const preflightMode = opts.preflightMode ?? "fast";
|
|
34
36
|
const clearMetro = opts.clearMetro ?? false;
|
|
35
37
|
const restartApp = opts.restartApp ?? false;
|
|
38
|
+
try {
|
|
39
|
+
resolveAndExportMobileToolPaths(platform);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
if (!json) process.stderr.write(`${message}
|
|
43
|
+
`);
|
|
44
|
+
return { status: EXIT.runtime, output: message };
|
|
45
|
+
}
|
|
36
46
|
const report = await decideMobileReadiness(target, {
|
|
37
47
|
watcherPort: opts.watcherPort,
|
|
38
48
|
metroLog: opts.metroLog,
|
|
@@ -49,27 +59,42 @@ async function prepareMobile(target, opts = {}) {
|
|
|
49
59
|
`);
|
|
50
60
|
return { status: EXIT.runtime, output: msg };
|
|
51
61
|
}
|
|
62
|
+
const iosAccessibilityChanged = platform === "ios" && report.decision !== "unknown" && enableIosAccessibility(json);
|
|
52
63
|
if (report.decision === "ready") {
|
|
53
|
-
if (restartApp) {
|
|
64
|
+
if (restartApp || iosAccessibilityChanged) {
|
|
54
65
|
const launch2 = await dispatchActionSequence(
|
|
55
66
|
withAppRestart(launchActions(path.resolve(target)), true),
|
|
56
67
|
target,
|
|
57
68
|
platform,
|
|
58
69
|
json,
|
|
59
|
-
preflightMode
|
|
70
|
+
preflightMode,
|
|
71
|
+
opts.watcherPort
|
|
60
72
|
);
|
|
61
|
-
return launch2.status === 0 ? startMobileConsoleForwarder(target, platform, json) : launch2;
|
|
73
|
+
return launch2.status === 0 ? startMobileConsoleForwarder(target, platform, json, opts.watcherPort) : launch2;
|
|
62
74
|
}
|
|
63
|
-
const surface = await ensureMobileHumanSurface(
|
|
75
|
+
const surface = await ensureMobileHumanSurface(
|
|
76
|
+
target,
|
|
77
|
+
platform,
|
|
78
|
+
json,
|
|
79
|
+
opts.watcherPort
|
|
80
|
+
);
|
|
64
81
|
if (surface.status !== 0) return surface;
|
|
65
82
|
const confirm = await dispatchAction(
|
|
66
83
|
{ id: "wait-for-bridge", cwd: target, argv: ["--max-polls", String(READY_BRIDGE_CONFIRM_POLLS)] },
|
|
67
84
|
target,
|
|
68
85
|
platform,
|
|
69
86
|
json,
|
|
70
|
-
preflightMode
|
|
87
|
+
preflightMode,
|
|
88
|
+
opts.watcherPort
|
|
71
89
|
);
|
|
72
|
-
if (confirm.status === 0)
|
|
90
|
+
if (confirm.status === 0) {
|
|
91
|
+
return startMobileConsoleForwarder(
|
|
92
|
+
target,
|
|
93
|
+
platform,
|
|
94
|
+
json,
|
|
95
|
+
opts.watcherPort
|
|
96
|
+
);
|
|
97
|
+
}
|
|
73
98
|
if (!json) {
|
|
74
99
|
process.stderr.write(
|
|
75
100
|
`launch: runtime looked ready but no ${platform} bridge target answered; launching the app.
|
|
@@ -77,13 +102,17 @@ async function prepareMobile(target, opts = {}) {
|
|
|
77
102
|
);
|
|
78
103
|
}
|
|
79
104
|
const launch = await dispatchActionSequence(
|
|
80
|
-
withAppRestart(
|
|
105
|
+
withAppRestart(
|
|
106
|
+
launchActions(path.resolve(target)),
|
|
107
|
+
restartApp || iosAccessibilityChanged
|
|
108
|
+
),
|
|
81
109
|
target,
|
|
82
110
|
platform,
|
|
83
111
|
json,
|
|
84
|
-
preflightMode
|
|
112
|
+
preflightMode,
|
|
113
|
+
opts.watcherPort
|
|
85
114
|
);
|
|
86
|
-
return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json) : launch;
|
|
115
|
+
return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json, opts.watcherPort) : launch;
|
|
87
116
|
}
|
|
88
117
|
if (report.decision === "unknown") {
|
|
89
118
|
const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
|
|
@@ -95,9 +124,16 @@ async function prepareMobile(target, opts = {}) {
|
|
|
95
124
|
clearMetro ? report.actions.map(
|
|
96
125
|
(a) => a.id === "start-metro" && !a.argv?.includes("--clear") ? { ...a, argv: [...a.argv ?? [], "--clear"] } : a
|
|
97
126
|
) : report.actions,
|
|
98
|
-
restartApp
|
|
127
|
+
restartApp || iosAccessibilityChanged
|
|
128
|
+
);
|
|
129
|
+
const actionResult = await dispatchActionSequence(
|
|
130
|
+
actions,
|
|
131
|
+
target,
|
|
132
|
+
platform,
|
|
133
|
+
json,
|
|
134
|
+
preflightMode,
|
|
135
|
+
opts.watcherPort
|
|
99
136
|
);
|
|
100
|
-
const actionResult = await dispatchActionSequence(actions, target, platform, json, preflightMode);
|
|
101
137
|
if (actionResult.status !== 0) return actionResult;
|
|
102
138
|
if (report.decision === "install" && !process.env["RECIPE_UP_INSTALL_ATTEMPTED"]) {
|
|
103
139
|
process.env["RECIPE_UP_INSTALL_ATTEMPTED"] = "1";
|
|
@@ -116,19 +152,35 @@ async function prepareMobile(target, opts = {}) {
|
|
|
116
152
|
return { status: EXIT.runtime, output: msg };
|
|
117
153
|
}
|
|
118
154
|
case "ready": {
|
|
119
|
-
const surface = await ensureMobileHumanSurface(
|
|
155
|
+
const surface = await ensureMobileHumanSurface(
|
|
156
|
+
target,
|
|
157
|
+
platform,
|
|
158
|
+
json,
|
|
159
|
+
opts.watcherPort
|
|
160
|
+
);
|
|
120
161
|
if (surface.status !== 0) return surface;
|
|
121
|
-
const bridge = await dispatchAction(
|
|
162
|
+
const bridge = await dispatchAction(
|
|
163
|
+
{ id: "wait-for-bridge", cwd: target },
|
|
164
|
+
target,
|
|
165
|
+
platform,
|
|
166
|
+
json,
|
|
167
|
+
preflightMode,
|
|
168
|
+
opts.watcherPort
|
|
169
|
+
);
|
|
122
170
|
if (bridge.status !== 0) return bridge;
|
|
123
171
|
break;
|
|
124
172
|
}
|
|
125
173
|
case "launch": {
|
|
126
174
|
const result = await dispatchActionSequence(
|
|
127
|
-
withAppRestart(
|
|
175
|
+
withAppRestart(
|
|
176
|
+
postInstall.actions,
|
|
177
|
+
restartApp || iosAccessibilityChanged
|
|
178
|
+
),
|
|
128
179
|
target,
|
|
129
180
|
platform,
|
|
130
181
|
json,
|
|
131
|
-
preflightMode
|
|
182
|
+
preflightMode,
|
|
183
|
+
opts.watcherPort
|
|
132
184
|
);
|
|
133
185
|
if (result.status !== 0) return result;
|
|
134
186
|
break;
|
|
@@ -142,19 +194,33 @@ async function prepareMobile(target, opts = {}) {
|
|
|
142
194
|
}
|
|
143
195
|
}
|
|
144
196
|
}
|
|
145
|
-
return startMobileConsoleForwarder(
|
|
197
|
+
return startMobileConsoleForwarder(
|
|
198
|
+
target,
|
|
199
|
+
platform,
|
|
200
|
+
json,
|
|
201
|
+
opts.watcherPort
|
|
202
|
+
);
|
|
146
203
|
}
|
|
147
|
-
function startMobileConsoleForwarder(target, platform, json) {
|
|
204
|
+
function startMobileConsoleForwarder(target, platform, json, watcherPort) {
|
|
148
205
|
return dispatchAction(
|
|
149
206
|
{ id: "start-console-forwarder", cwd: target, argv: json ? ["--quiet"] : [] },
|
|
150
207
|
target,
|
|
151
208
|
platform,
|
|
152
|
-
json
|
|
209
|
+
json,
|
|
210
|
+
"fast",
|
|
211
|
+
watcherPort
|
|
153
212
|
);
|
|
154
213
|
}
|
|
155
|
-
function ensureMobileHumanSurface(target, platform, json) {
|
|
214
|
+
function ensureMobileHumanSurface(target, platform, json, watcherPort) {
|
|
156
215
|
if (platform !== "ios") return Promise.resolve({ status: 0, output: "" });
|
|
157
|
-
return dispatchAction(
|
|
216
|
+
return dispatchAction(
|
|
217
|
+
{ id: "ensure-device-ui", cwd: target },
|
|
218
|
+
target,
|
|
219
|
+
platform,
|
|
220
|
+
json,
|
|
221
|
+
"fast",
|
|
222
|
+
watcherPort
|
|
223
|
+
);
|
|
158
224
|
}
|
|
159
225
|
function withAppRestart(actions, restartApp) {
|
|
160
226
|
if (!restartApp) return actions;
|
|
@@ -162,7 +228,7 @@ function withAppRestart(actions, restartApp) {
|
|
|
162
228
|
(action) => action.id === "launch-mobile-runtime" ? { ...action, argv: [...action.argv ?? [], "--restart"] } : action
|
|
163
229
|
);
|
|
164
230
|
}
|
|
165
|
-
async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode) {
|
|
231
|
+
async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode, watcherPort) {
|
|
166
232
|
const resolved = path.resolve(target);
|
|
167
233
|
const actions = applyMobileMetroEnvPolicy(resolved, requestedActions);
|
|
168
234
|
let pendingMetroEnvFingerprint;
|
|
@@ -170,7 +236,14 @@ async function dispatchActionSequence(requestedActions, target, platform, json,
|
|
|
170
236
|
if (action.id === "start-metro") {
|
|
171
237
|
pendingMetroEnvFingerprint = mobileMetroEnvCheck(resolved).fingerprint;
|
|
172
238
|
}
|
|
173
|
-
const result = await dispatchAction(
|
|
239
|
+
const result = await dispatchAction(
|
|
240
|
+
action,
|
|
241
|
+
target,
|
|
242
|
+
platform,
|
|
243
|
+
json,
|
|
244
|
+
preflightMode,
|
|
245
|
+
watcherPort
|
|
246
|
+
);
|
|
174
247
|
if (result.status !== 0) return result;
|
|
175
248
|
if (action.id === "yarn-setup") recordDepsBaseline(resolved);
|
|
176
249
|
if (action.id === "prewarm-bundle" && pendingMetroEnvFingerprint) {
|
|
@@ -184,7 +257,100 @@ async function dispatchActionSequence(requestedActions, target, platform, json,
|
|
|
184
257
|
}
|
|
185
258
|
return { status: 0, output: "" };
|
|
186
259
|
}
|
|
187
|
-
|
|
260
|
+
function enableIosAccessibility(json) {
|
|
261
|
+
const simulator = process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted";
|
|
262
|
+
const defaultsArgs = [
|
|
263
|
+
"simctl",
|
|
264
|
+
"spawn",
|
|
265
|
+
simulator,
|
|
266
|
+
"defaults"
|
|
267
|
+
];
|
|
268
|
+
try {
|
|
269
|
+
execFileSync("xcrun", ["simctl", "boot", simulator], {
|
|
270
|
+
stdio: "ignore",
|
|
271
|
+
timeout: 15e3
|
|
272
|
+
});
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
execFileSync("xcrun", ["simctl", "bootstatus", simulator, "-b"], {
|
|
277
|
+
stdio: "ignore",
|
|
278
|
+
timeout: 12e4
|
|
279
|
+
});
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (!json) {
|
|
282
|
+
process.stderr.write(
|
|
283
|
+
`mobile prepare: selected iOS simulator was unavailable (${error instanceof Error ? error.message : String(error)}); continuing with native-view fallback.
|
|
284
|
+
`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const current = execFileSync(
|
|
291
|
+
"xcrun",
|
|
292
|
+
[...defaultsArgs, "read", "com.apple.Accessibility", "ApplicationAccessibilityEnabled"],
|
|
293
|
+
{
|
|
294
|
+
encoding: "utf8",
|
|
295
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
296
|
+
timeout: 5e3
|
|
297
|
+
}
|
|
298
|
+
).trim().toLowerCase();
|
|
299
|
+
if (current === "1" || current === "true" || current === "yes") return false;
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
execFileSync(
|
|
304
|
+
"xcrun",
|
|
305
|
+
[
|
|
306
|
+
...defaultsArgs,
|
|
307
|
+
"write",
|
|
308
|
+
"com.apple.Accessibility",
|
|
309
|
+
"ApplicationAccessibilityEnabled",
|
|
310
|
+
"-bool",
|
|
311
|
+
"YES"
|
|
312
|
+
],
|
|
313
|
+
{
|
|
314
|
+
stdio: "ignore",
|
|
315
|
+
timeout: 5e3
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
try {
|
|
319
|
+
execFileSync("xcrun", ["simctl", "shutdown", simulator], {
|
|
320
|
+
stdio: "ignore",
|
|
321
|
+
timeout: 15e3
|
|
322
|
+
});
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
execFileSync("xcrun", ["simctl", "boot", simulator], {
|
|
327
|
+
stdio: "ignore",
|
|
328
|
+
timeout: 15e3
|
|
329
|
+
});
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
execFileSync("xcrun", ["simctl", "bootstatus", simulator, "-b"], {
|
|
333
|
+
stdio: "ignore",
|
|
334
|
+
timeout: 12e4
|
|
335
|
+
});
|
|
336
|
+
if (!json) {
|
|
337
|
+
process.stderr.write(
|
|
338
|
+
`Enabled iOS application accessibility on ${simulator}; restarted the simulator before app launch.
|
|
339
|
+
`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (!json) {
|
|
345
|
+
process.stderr.write(
|
|
346
|
+
`mobile prepare: iOS accessibility enablement was unavailable (${error instanceof Error ? error.message : String(error)}); continuing with native-view fallback.
|
|
347
|
+
`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function dispatchAction(action, target, platform, json, preflightMode = "fast", watcherPort) {
|
|
188
354
|
const cwd = action.cwd ?? target;
|
|
189
355
|
switch (action.id) {
|
|
190
356
|
case "yarn-setup": {
|
|
@@ -194,34 +360,62 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
194
360
|
case "start-metro": {
|
|
195
361
|
const leaf = path.join(runnerDir, "adapters/mobile/start-metro.sh");
|
|
196
362
|
const extra = action.argv ?? [];
|
|
197
|
-
return spawnScriptStreaming(
|
|
363
|
+
return spawnScriptStreaming(
|
|
364
|
+
leaf,
|
|
365
|
+
withWatcherPort(["--target", cwd, ...extra], watcherPort),
|
|
366
|
+
target
|
|
367
|
+
);
|
|
198
368
|
}
|
|
199
369
|
case "start-console-forwarder": {
|
|
200
370
|
const leaf = path.join(runnerDir, "adapters/mobile/start-console-forwarder.sh");
|
|
201
|
-
return spawnScriptStreaming(
|
|
371
|
+
return spawnScriptStreaming(
|
|
372
|
+
leaf,
|
|
373
|
+
withWatcherPort(
|
|
374
|
+
["--target", cwd, ...action.argv ?? []],
|
|
375
|
+
watcherPort
|
|
376
|
+
),
|
|
377
|
+
target
|
|
378
|
+
);
|
|
202
379
|
}
|
|
203
380
|
case "prewarm-bundle": {
|
|
204
381
|
const leaf = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
|
|
205
|
-
return spawnScriptStreaming(
|
|
382
|
+
return spawnScriptStreaming(
|
|
383
|
+
leaf,
|
|
384
|
+
withWatcherPort(
|
|
385
|
+
["--platform", platform, "--target", cwd, ...action.argv ?? []],
|
|
386
|
+
watcherPort
|
|
387
|
+
),
|
|
388
|
+
target
|
|
389
|
+
);
|
|
206
390
|
}
|
|
207
391
|
case "wait-for-bridge": {
|
|
208
392
|
const leaf = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
|
|
209
393
|
const extra = action.argv ?? [];
|
|
210
|
-
return spawnScriptStreaming(
|
|
394
|
+
return spawnScriptStreaming(
|
|
395
|
+
leaf,
|
|
396
|
+
withWatcherPort(
|
|
397
|
+
["--target", cwd, "--platform", platform, ...extra],
|
|
398
|
+
watcherPort
|
|
399
|
+
),
|
|
400
|
+
target
|
|
401
|
+
);
|
|
211
402
|
}
|
|
212
403
|
case "launch-mobile-runtime": {
|
|
213
404
|
const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
214
405
|
return spawnScriptStreaming(
|
|
215
406
|
leaf,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
407
|
+
withWatcherPort(
|
|
408
|
+
[
|
|
409
|
+
"--platform",
|
|
410
|
+
platform,
|
|
411
|
+
"--target",
|
|
412
|
+
cwd,
|
|
413
|
+
"--preflight-mode",
|
|
414
|
+
preflightMode,
|
|
415
|
+
...action.argv ?? []
|
|
416
|
+
],
|
|
417
|
+
watcherPort
|
|
418
|
+
),
|
|
225
419
|
target,
|
|
226
420
|
POD_PROBE_ENV
|
|
227
421
|
);
|
|
@@ -230,7 +424,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
230
424
|
const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
231
425
|
return spawnScriptStreaming(
|
|
232
426
|
leaf,
|
|
233
|
-
|
|
427
|
+
withWatcherPort(
|
|
428
|
+
["--platform", platform, "--target", cwd, "--ui-only"],
|
|
429
|
+
watcherPort
|
|
430
|
+
),
|
|
234
431
|
target,
|
|
235
432
|
POD_PROBE_ENV
|
|
236
433
|
);
|
|
@@ -245,6 +442,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
245
442
|
return { status: 0, output: "" };
|
|
246
443
|
}
|
|
247
444
|
}
|
|
445
|
+
function withWatcherPort(args, watcherPort) {
|
|
446
|
+
if (watcherPort === void 0 || args.includes("--port")) return args;
|
|
447
|
+
return [...args, "--port", String(watcherPort)];
|
|
448
|
+
}
|
|
248
449
|
export {
|
|
249
450
|
mobileRuntimeStatus,
|
|
250
451
|
prepareMobile
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@farmslot/recipe-harness/runtime/log-analysis";
|
|
14
14
|
import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
|
|
15
15
|
import { recipeRuntimePath } from "../../paths.js";
|
|
16
|
+
import { resolveMobileToolPath } from "../../../library/actions/mobile/platform/tool-paths.mjs";
|
|
16
17
|
import { mobileProductMarkers } from "./deps-markers.js";
|
|
17
18
|
import { applyMobileMetroEnvPolicy, mobileMetroEnvCheck } from "./metro-env.js";
|
|
18
19
|
const BUNDLE_ERR = /Bundling failed|Unable to resolve /u;
|
|
@@ -66,9 +67,11 @@ const installActions = (target) => [
|
|
|
66
67
|
function appRunningOnDevice(platform) {
|
|
67
68
|
try {
|
|
68
69
|
if (platform === "android") {
|
|
70
|
+
const adbPath = resolveMobileToolPath("adb");
|
|
71
|
+
if (!adbPath) return false;
|
|
69
72
|
const serial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL;
|
|
70
73
|
const args = serial ? ["-s", serial] : [];
|
|
71
|
-
const out2 = execFileSync(
|
|
74
|
+
const out2 = execFileSync(adbPath, [...args, "shell", "ps", "-A"], {
|
|
72
75
|
encoding: "utf8",
|
|
73
76
|
stdio: ["ignore", "pipe", "ignore"],
|
|
74
77
|
timeout: 5e3
|
package/dist/adapters.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "./live-adapter-contract.js";
|
|
6
6
|
import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs";
|
|
7
7
|
import { bridgeCommand, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
|
|
8
|
+
import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.mjs";
|
|
8
9
|
function sleep(ms) {
|
|
9
10
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
11
|
}
|
|
@@ -276,7 +277,8 @@ const MOBILE_BRIDGE_HANDLERS = {
|
|
|
276
277
|
setInput: handleMobileSetInput,
|
|
277
278
|
scroll: handleMobileScroll,
|
|
278
279
|
waitFor: handleMobileWaitFor,
|
|
279
|
-
hud: handleMobileHud
|
|
280
|
+
hud: handleMobileHud,
|
|
281
|
+
observeUi: handleMobileObserveUi
|
|
280
282
|
};
|
|
281
283
|
function createMetaMaskMobileBridge() {
|
|
282
284
|
return {
|
|
@@ -302,6 +304,20 @@ async function handleMobileScreenshot(payload, context) {
|
|
|
302
304
|
async function handleMobileStatus(payload, context) {
|
|
303
305
|
return bridgeCommand(mobileUiInput(context, "status", payload), ["status"]);
|
|
304
306
|
}
|
|
307
|
+
async function handleMobileObserveUi(payload, context) {
|
|
308
|
+
try {
|
|
309
|
+
await bridgeCommand(
|
|
310
|
+
mobileUiInput(context, "observe", {
|
|
311
|
+
...payload,
|
|
312
|
+
bridge_timeout_ms: 2e3,
|
|
313
|
+
cdp_timeout_ms: 2e3
|
|
314
|
+
}),
|
|
315
|
+
["hide-step"]
|
|
316
|
+
);
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
return observeNativeUi(payload, context);
|
|
320
|
+
}
|
|
305
321
|
async function handleMobileNavigate(payload, context) {
|
|
306
322
|
const live = await runLiveAdapterScript({
|
|
307
323
|
platform: "mobile",
|
|
@@ -496,7 +512,8 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
|
496
512
|
);
|
|
497
513
|
}
|
|
498
514
|
});
|
|
499
|
-
|
|
515
|
+
return {
|
|
516
|
+
...base.observe ? { observe: base.observe.bind(base) } : {},
|
|
500
517
|
async execute(action, node, context) {
|
|
501
518
|
if (action === "ui.navigate") {
|
|
502
519
|
const live = await runLiveFirst(
|
|
@@ -513,10 +530,6 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
|
513
530
|
return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
|
|
514
531
|
}
|
|
515
532
|
};
|
|
516
|
-
if (base.observe) {
|
|
517
|
-
transport.observe = (refs, node, context) => base.observe(refs, node, context);
|
|
518
|
-
}
|
|
519
|
-
return transport;
|
|
520
533
|
}
|
|
521
534
|
function normalizeUiWaitNode(node) {
|
|
522
535
|
if (node.expected !== void 0 || typeof node.visible !== "boolean") return node;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { resolveMobileToolPath } from "../../library/actions/mobile/platform/tool-paths.mjs";
|
|
2
3
|
import { listConnectedDevices } from "../devices.js";
|
|
3
4
|
import { optionString, shellQuoteArg, targetPath } from "./parse-args.js";
|
|
4
5
|
function normalizeDeviceName(value) {
|
|
5
6
|
return value.replace(/_/gu, " ").trim();
|
|
6
7
|
}
|
|
7
8
|
function resolveAndroidModel(serial) {
|
|
9
|
+
const adbPath = resolveMobileToolPath("adb");
|
|
10
|
+
if (!adbPath) return void 0;
|
|
8
11
|
try {
|
|
9
|
-
const output = execFileSync(
|
|
12
|
+
const output = execFileSync(adbPath, ["-s", serial, "shell", "getprop", "ro.product.model"], {
|
|
10
13
|
encoding: "utf8",
|
|
11
14
|
stdio: ["ignore", "pipe", "ignore"],
|
|
12
15
|
timeout: 5e3
|