@deeeed/metamask-harness 0.33.3 → 0.34.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.
- package/CHANGELOG.md +35 -0
- package/adapters/manifest.json +0 -8
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +32 -5
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +91 -24
- package/adapters/mobile/launch-metro.cjs +6 -4
- package/adapters/mobile/open-device.sh +164 -15
- package/adapters/mobile/start-metro.sh +165 -93
- package/adapters/mobile/stop-metro.sh +1 -0
- package/dist/adapters/mobile/prepare.js +14 -0
- package/dist/adapters/mobile/video-recorder.js +284 -0
- package/dist/adapters.js +225 -35
- package/dist/command-contract.js +1 -0
- package/dist/commands/device-target.js +17 -6
- package/dist/commands/launch/index.js +36 -5
- package/dist/commands/launch/mobile.js +10 -3
- package/dist/commands/run-engine.js +34 -2
- package/dist/devices.js +9 -2
- package/dist/heal-bounds.js +10 -0
- package/dist/live-adapter-contract.js +7 -0
- package/dist/mm-harness-cli.js +4 -2
- package/dist/recipe-security.js +1 -0
- package/dist/recording-target.js +23 -5
- package/dist/runner.js +107 -6
- package/dist/runtime-context.js +33 -1
- package/docs/RECIPES.md +41 -0
- package/library/actions/mobile/app/network-control.mjs +135 -0
- package/library/actions/mobile/app/network.mjs +4 -0
- package/library/actions/mobile/perps/capture_performance.mjs +4 -0
- package/library/actions/mobile/perps/performance-capture.mjs +465 -0
- package/library/actions/mobile/perps/perps.mjs +16 -0
- package/library/actions/mobile/platform/bridge.mjs +121 -3
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +56 -12
- package/library/actions/mobile/wallet/select_account.mjs +21 -17
- package/library/manifests/mobile.action-manifest.json +148 -1
- package/library/recipes/mobile/perps/performance.homepage.android-background-reconnect.recipe.json +159 -0
- package/library/recipes/mobile/perps/performance.homepage.android-background-short.recipe.json +157 -0
- package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +165 -0
- package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +139 -0
- package/library/recipes/mobile/perps/performance.homepage.android-network-recovery.recipe.json +149 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +110 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-background-short.recipe.json +108 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-disk-cache.recipe.json +122 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +95 -0
- package/package.json +1 -1
- package/adapters/mobile/metro-config.cjs +0 -93
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.34.1 - 2026-08-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Launch Mobile Metro through the product's canonical watcher while preserving slot, build, Sentry, and Segment environment settings, applying cache clears only to the selected Metro instance, and removing the config override that could corrupt cold bundles.
|
|
10
|
+
- Pin native Mobile builds to the exact supported Main or Flask app identity and Android device, rejecting ambiguous physical-device names instead of building or launching on the wrong target.
|
|
11
|
+
- Clean up timed-out Metro process groups safely and require the cold-disk-cache Perps freshness gate to report the expected lifecycle.
|
|
12
|
+
- Allow performance recipes to enforce a first-fresh socket-to-subscriber latency budget for a stream and subscriber throttle, optionally scoped to a lifecycle while always reporting the observed lifecycle.
|
|
13
|
+
- Allow Mobile wallet unlock actions to use their declared target and unlock timeouts without the live-adapter process ending first.
|
|
14
|
+
- Remove Android's 180-second `screenrecord` cap so `--record-video=full-run` covers long recipe runs.
|
|
15
|
+
- Retry empty Metro target lists, derive the Android model pin from `ADB_SERIAL`, and use pin-aware wallet status while Hermes re-registers between recipe actions.
|
|
16
|
+
- Keep compound Mobile actions under one debugger lease so nested bridge calls cannot race another recipe action.
|
|
17
|
+
- Let Android `scroll_into_view` fall back to a scrollable ancestor when the target itself is not a scroll container.
|
|
18
|
+
- Leave Metro's worker count at its native default unless `METRO_MAX_WORKERS` is explicitly configured.
|
|
19
|
+
- Bound each Mobile wallet-status probe by its polling deadline, raise the default unlock verification window to 30 seconds, and give the Android cold-no-cache recipe an explicit cold-start allowance, preventing a successful physical-device unlock from being reported as a timeout.
|
|
20
|
+
|
|
21
|
+
## 0.34.0 - 2026-08-07
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Record Mobile Android recipe runs with the selected device's native `screenrecord` capability when `--record-video=full-run` is requested.
|
|
26
|
+
- Record iOS Simulator recipe runs with `simctl recordVideo`, avoiding hidden/offscreen Simulator-window captures that produce empty videos.
|
|
27
|
+
- Keep pinned Android commands on their requested device while its Hermes target re-registers with Metro after a runtime rotation.
|
|
28
|
+
- Make Mobile account selection wait for the selected account state without disconnecting or reinitializing the Perps controller, preventing duplicate account-change reconnections and transient Perps errors.
|
|
29
|
+
- Resolve shutdown iOS simulators to their canonical bridge-visible name while preserving the requested UDID for native launch.
|
|
30
|
+
- Keep lifecycle performance recipes free from HUD bridge timeouts while the app is backgrounded or its runtime target is rotating.
|
|
31
|
+
- Make Android coordinate swipes and scroll-into-view use the selected device consistently, including the native fallback path.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Add manifest-declared Mobile `app.lifecycle` and `app.network` actions with device targeting, runtime readiness, and Android offline/online recovery.
|
|
36
|
+
- Add reusable Perps Homepage performance capture with lifecycle, delivery-source, visible-content, and socket-pipeline validation.
|
|
37
|
+
- Bundle the device-proven Android/iOS Perps Homepage short-resume, reconnect, cold-cache, cold-no-cache, and Android network-recovery recipes with explicit cleanup and full-run evidence commands.
|
|
38
|
+
- Add wallet setup/account-selection readiness checks and source-freshness recovery evidence for Mobile recipe runs.
|
|
39
|
+
|
|
5
40
|
## 0.33.3 - 2026-08-05
|
|
6
41
|
|
|
7
42
|
### Changed
|
package/adapters/manifest.json
CHANGED
|
@@ -42,14 +42,6 @@
|
|
|
42
42
|
"inputs": "--target --port --log --pid-file [--workers] [--clear]",
|
|
43
43
|
"outputs": "Metro PID/launch metadata and redirected log; exit 0/1/2"
|
|
44
44
|
},
|
|
45
|
-
{
|
|
46
|
-
"id": "mobile/metro-config",
|
|
47
|
-
"entry": "adapters/mobile/metro-config.cjs",
|
|
48
|
-
"kind": "module",
|
|
49
|
-
"purpose": "Preserve the product Metro config while excluding harness-owned checkout temp artifacts.",
|
|
50
|
-
"inputs": "base Metro config; env MM_HARNESS_METRO_PROJECT_ROOT, MM_HARNESS_METRO_BASE_CONFIG",
|
|
51
|
-
"outputs": "merged Metro config with resolver.blockList for <projectRoot>/temp"
|
|
52
|
-
},
|
|
53
45
|
{
|
|
54
46
|
"id": "mobile/stop-metro",
|
|
55
47
|
"entry": "adapters/mobile/stop-metro.sh",
|
|
@@ -305,6 +305,7 @@ const COMMANDS = {
|
|
|
305
305
|
let testId;
|
|
306
306
|
let offset = 300;
|
|
307
307
|
let animated = false;
|
|
308
|
+
let intoView = false;
|
|
308
309
|
for (let i = 0; i < args.length; i++) {
|
|
309
310
|
if (args[i] === '--test-id' && i + 1 < args.length) {
|
|
310
311
|
testId = args[++i];
|
|
@@ -312,14 +313,20 @@ const COMMANDS = {
|
|
|
312
313
|
offset = Number(args[++i]);
|
|
313
314
|
} else if (args[i] === '--animated') {
|
|
314
315
|
animated = true;
|
|
316
|
+
} else if (args[i] === '--into-view') {
|
|
317
|
+
intoView = true;
|
|
315
318
|
} else if (args[i] === '--no-animated') {
|
|
316
319
|
animated = false;
|
|
317
320
|
}
|
|
318
321
|
}
|
|
319
|
-
const optsJson = JSON.stringify({ testId, offset, animated });
|
|
322
|
+
const optsJson = JSON.stringify({ testId, offset, animated, intoView });
|
|
320
323
|
// Try __AGENTIC__ bridge first, fall back to inline fiber walking
|
|
321
324
|
const expr = `(function() {
|
|
322
|
-
|
|
325
|
+
// The app helper implements generic scrolling by searching inside the
|
|
326
|
+
// selector fiber. For into-view requests the scroll container is commonly
|
|
327
|
+
// an ancestor (for example, a Wallet Home section row), so use the
|
|
328
|
+
// ancestor-aware bridge fallback below instead.
|
|
329
|
+
if (!${intoView} && globalThis.__AGENTIC__?.scrollView) return globalThis.__AGENTIC__.scrollView(${optsJson});
|
|
323
330
|
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
324
331
|
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
325
332
|
var renderers = hook.renderers;
|
|
@@ -340,6 +347,19 @@ const COMMANDS = {
|
|
|
340
347
|
}
|
|
341
348
|
return false;
|
|
342
349
|
}
|
|
350
|
+
function tryScrollNear(anchor) {
|
|
351
|
+
if (!opts.intoView && tryScroll(anchor, false)) return true;
|
|
352
|
+
var current = anchor ? anchor.return : null;
|
|
353
|
+
while (current) {
|
|
354
|
+
var sn = current.stateNode;
|
|
355
|
+
if (sn) {
|
|
356
|
+
if (typeof sn.scrollTo === 'function') { sn.scrollTo({ y: opts.offset, animated: opts.animated }); return true; }
|
|
357
|
+
if (typeof sn.scrollToOffset === 'function') { sn.scrollToOffset({ offset: opts.offset, animated: opts.animated }); return true; }
|
|
358
|
+
}
|
|
359
|
+
current = current.return;
|
|
360
|
+
}
|
|
361
|
+
return opts.intoView ? tryScroll(anchor, false) : false;
|
|
362
|
+
}
|
|
343
363
|
function findTestId(fiber) {
|
|
344
364
|
if (!fiber) return null;
|
|
345
365
|
var props = fiber.memoizedProps;
|
|
@@ -354,7 +374,7 @@ const COMMANDS = {
|
|
|
354
374
|
if (scrolled) return;
|
|
355
375
|
if (opts.testId) {
|
|
356
376
|
var anchor = findTestId(r.current);
|
|
357
|
-
if (anchor) scrolled =
|
|
377
|
+
if (anchor) scrolled = tryScrollNear(anchor);
|
|
358
378
|
} else {
|
|
359
379
|
scrolled = tryScroll(r.current);
|
|
360
380
|
}
|
|
@@ -665,6 +685,11 @@ const BRIDGE_LOCK_FILE = path.join(
|
|
|
665
685
|
process.env.RECIPE_RUNTIME_DIR || path.join('temp', 'recipe', 'runtime'),
|
|
666
686
|
'cdp-bridge.lock',
|
|
667
687
|
);
|
|
688
|
+
const BRIDGE_LOCK_OWNER_PID = /^\d+$/.test(
|
|
689
|
+
process.env.CDP_BRIDGE_LOCK_OWNER_PID || '',
|
|
690
|
+
)
|
|
691
|
+
? process.env.CDP_BRIDGE_LOCK_OWNER_PID
|
|
692
|
+
: String(process.pid);
|
|
668
693
|
|
|
669
694
|
// Ownership semantics: last writer wins. Each bridge stamps its own pid; the
|
|
670
695
|
// forwarder honors the lock while the pid IN THE FILE is alive. Overlapping
|
|
@@ -675,7 +700,7 @@ let bridgeLockHeld = false;
|
|
|
675
700
|
|
|
676
701
|
function acquireBridgeLock() {
|
|
677
702
|
try {
|
|
678
|
-
fs.writeFileSync(BRIDGE_LOCK_FILE,
|
|
703
|
+
fs.writeFileSync(BRIDGE_LOCK_FILE, BRIDGE_LOCK_OWNER_PID);
|
|
679
704
|
bridgeLockHeld = true;
|
|
680
705
|
} catch {
|
|
681
706
|
// Lock is best-effort: a missing runtime dir must not break bridge commands.
|
|
@@ -749,7 +774,7 @@ Environment:
|
|
|
749
774
|
process.exit(0);
|
|
750
775
|
}
|
|
751
776
|
|
|
752
|
-
const handler = COMMANDS[command];
|
|
777
|
+
const handler = COMMANDS[command === 'status-selected' ? 'status' : command];
|
|
753
778
|
if (!handler) {
|
|
754
779
|
console.error(`Unknown command: ${command}`);
|
|
755
780
|
console.error(`Available: ${Object.keys(COMMANDS).join(', ')}`);
|
|
@@ -764,6 +789,8 @@ Environment:
|
|
|
764
789
|
const timeout = Number.parseInt(process.env.CDP_TIMEOUT || '5000', 10);
|
|
765
790
|
|
|
766
791
|
// `status` probes ALL connected targets so both platforms are visible.
|
|
792
|
+
// `status-selected` falls through to the normal pinned discovery path for
|
|
793
|
+
// action code that must survive one device's Hermes runtime rotation.
|
|
767
794
|
if (command === 'status') {
|
|
768
795
|
const { discoverAllTargets } = require('./lib/target-discovery.cjs');
|
|
769
796
|
const allTargets = await discoverAllTargets(port);
|
|
@@ -6,7 +6,12 @@ const { createWSClient } = require('./ws-client.cjs');
|
|
|
6
6
|
const { BRIDGE_ERROR_CODES, coded } = require('./bridge-errors.cjs');
|
|
7
7
|
|
|
8
8
|
const FETCH_TIMEOUT_MS = Number.parseInt(process.env.CDP_TIMEOUT || '30000', 10);
|
|
9
|
-
const
|
|
9
|
+
const DISCOVERY_RETRY_DELAY_MS = 500;
|
|
10
|
+
const FETCH_RETRIES = Number.parseInt(
|
|
11
|
+
process.env.CDP_DISCOVERY_RETRIES ||
|
|
12
|
+
String(Math.max(3, Math.ceil(FETCH_TIMEOUT_MS / DISCOVERY_RETRY_DELAY_MS))),
|
|
13
|
+
10,
|
|
14
|
+
);
|
|
10
15
|
|
|
11
16
|
/** Fetch JSON from a URL (http only, no external deps) */
|
|
12
17
|
function fetchJSONOnce(url) {
|
|
@@ -30,17 +35,25 @@ function fetchJSONOnce(url) {
|
|
|
30
35
|
});
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
async function fetchJSON(url) {
|
|
38
|
+
async function fetchJSON(url, acceptResponse) {
|
|
34
39
|
let lastError;
|
|
40
|
+
let lastResponse;
|
|
35
41
|
for (let attempt = 1; attempt <= FETCH_RETRIES; attempt++) {
|
|
36
42
|
try {
|
|
37
|
-
|
|
43
|
+
const response = await fetchJSONOnce(url);
|
|
44
|
+
lastError = undefined;
|
|
45
|
+
lastResponse = response;
|
|
46
|
+
if (!acceptResponse || await acceptResponse(response)) return response;
|
|
38
47
|
} catch (error) {
|
|
39
48
|
lastError = error;
|
|
40
|
-
if (attempt === FETCH_RETRIES) break;
|
|
41
|
-
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
|
|
42
49
|
}
|
|
50
|
+
if (attempt === FETCH_RETRIES) break;
|
|
51
|
+
await new Promise((resolve) =>
|
|
52
|
+
setTimeout(resolve, DISCOVERY_RETRY_DELAY_MS),
|
|
53
|
+
);
|
|
43
54
|
}
|
|
55
|
+
if (lastError !== undefined) throw lastError;
|
|
56
|
+
if (lastResponse !== undefined) return lastResponse;
|
|
44
57
|
throw lastError;
|
|
45
58
|
}
|
|
46
59
|
|
|
@@ -103,6 +116,19 @@ function rankRuntimeCandidates(targets) {
|
|
|
103
116
|
return candidates;
|
|
104
117
|
}
|
|
105
118
|
|
|
119
|
+
function targetDeviceIdentity(target) {
|
|
120
|
+
try {
|
|
121
|
+
const device = new URL(target.webSocketDebuggerUrl).searchParams.get('device');
|
|
122
|
+
if (device) return device;
|
|
123
|
+
} catch {
|
|
124
|
+
// Fall back to Metro's display identity when the debugger URL is malformed.
|
|
125
|
+
}
|
|
126
|
+
// Without Metro's device id there is no safe way to collapse same-name pages
|
|
127
|
+
// into one physical device. Keep each debugger target distinct and fail closed
|
|
128
|
+
// on ambiguity instead of silently choosing between devices.
|
|
129
|
+
return target.webSocketDebuggerUrl || target.deviceName || '';
|
|
130
|
+
}
|
|
131
|
+
|
|
106
132
|
/**
|
|
107
133
|
* Discover the Hermes CDP WebSocket URL from Metro's /json/list endpoint.
|
|
108
134
|
*
|
|
@@ -114,9 +140,34 @@ function rankRuntimeCandidates(targets) {
|
|
|
114
140
|
*/
|
|
115
141
|
async function discoverTarget(port) {
|
|
116
142
|
const listUrl = `http://localhost:${port}/json/list`;
|
|
143
|
+
const androidTargetName = loadAndroidTargetDeviceName();
|
|
144
|
+
const androidDevice = loadAndroidDevice();
|
|
145
|
+
const adbSerial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || '';
|
|
146
|
+
const androidPinned = Boolean(androidTargetName || androidDevice);
|
|
147
|
+
const simName = loadSimulatorName();
|
|
148
|
+
const matchesAndroidPin = (target) => androidTargetName
|
|
149
|
+
? target.deviceName !== simName &&
|
|
150
|
+
(target.deviceName === androidTargetName ||
|
|
151
|
+
(target.deviceName != null && target.deviceName.startsWith(androidTargetName)))
|
|
152
|
+
: target.deviceName === androidDevice;
|
|
153
|
+
let acceptedPinnedCandidate;
|
|
117
154
|
let targets;
|
|
118
155
|
try {
|
|
119
|
-
targets = await fetchJSON(listUrl)
|
|
156
|
+
targets = await fetchJSON(listUrl, async (response) => {
|
|
157
|
+
const runtimeCandidates = rankRuntimeCandidates(response);
|
|
158
|
+
if (runtimeCandidates.length === 0) return false;
|
|
159
|
+
if (androidPinned) {
|
|
160
|
+
const pinnedCandidates = runtimeCandidates.filter(matchesAndroidPin);
|
|
161
|
+
for (const candidate of pinnedCandidates) {
|
|
162
|
+
if (await probeTarget(candidate.webSocketDebuggerUrl)) {
|
|
163
|
+
acceptedPinnedCandidate = candidate;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
});
|
|
120
171
|
} catch (e) {
|
|
121
172
|
throw coded(
|
|
122
173
|
new Error(`Cannot reach Metro at ${listUrl}. Is Metro running?\n ${e.message}`),
|
|
@@ -137,17 +188,11 @@ async function discoverTarget(port) {
|
|
|
137
188
|
// from the adb serial via `adb -s <serial> shell getprop ro.product.model`.
|
|
138
189
|
// Example: "Pixel 6" matches Metro deviceName "Pixel 6 - 16 - API 36".
|
|
139
190
|
// ANDROID_DEVICE — exact user-specified Metro deviceName.
|
|
140
|
-
const androidTargetName = loadAndroidTargetDeviceName();
|
|
141
|
-
const androidDevice = loadAndroidDevice();
|
|
142
|
-
const adbSerial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || '';
|
|
143
|
-
const androidPinned = Boolean(androidTargetName || androidDevice);
|
|
144
|
-
|
|
145
191
|
// Filter by simulator name if IOS_SIMULATOR is set — but never when an android
|
|
146
192
|
// pin is present: dual-platform slots carry an ambient IOS_SIMULATOR in their
|
|
147
193
|
// context, and letting it win here sends a pinned android action to the iOS
|
|
148
194
|
// target. No-match keeps the full candidate set (ambient sim configs tolerate a
|
|
149
195
|
// sim that is not currently attached).
|
|
150
|
-
const simName = loadSimulatorName();
|
|
151
196
|
if (simName && !androidPinned && candidates.length > 1) {
|
|
152
197
|
const deviceFiltered = candidates.filter(
|
|
153
198
|
(t) => t.deviceName === simName,
|
|
@@ -163,7 +208,7 @@ async function discoverTarget(port) {
|
|
|
163
208
|
// remained; the pin was ignored and the recipe drove the simulator). When a
|
|
164
209
|
// device is pinned, we MUST NOT silently fall back to another target; an
|
|
165
210
|
// unmatchable pin fails fast with diagnostics.
|
|
166
|
-
if (androidPinned
|
|
211
|
+
if (androidPinned) {
|
|
167
212
|
let androidFiltered = [];
|
|
168
213
|
|
|
169
214
|
if (androidTargetName) {
|
|
@@ -171,20 +216,19 @@ async function discoverTarget(port) {
|
|
|
171
216
|
// /json/list has no explicit platform field, so scope the ANDROID pin by
|
|
172
217
|
// excluding the one iOS identity we do know (the pinned simulator name) —
|
|
173
218
|
// a simulator named to shadow a model string must not satisfy an android pin.
|
|
174
|
-
androidFiltered = candidates.filter(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
)
|
|
179
|
-
if (androidFiltered.length > 1) {
|
|
219
|
+
androidFiltered = candidates.filter(matchesAndroidPin);
|
|
220
|
+
const matchingDeviceIdentities = [...new Set(
|
|
221
|
+
androidFiltered.map(targetDeviceIdentity),
|
|
222
|
+
)];
|
|
223
|
+
if (matchingDeviceIdentities.length > 1) {
|
|
180
224
|
// Two same-model devices produce identical Metro model prefixes — the pin
|
|
181
|
-
// is genuinely ambiguous
|
|
182
|
-
//
|
|
225
|
+
// is genuinely ambiguous. Native + JS pages share the same Metro device
|
|
226
|
+
// identity and must remain available for probing.
|
|
183
227
|
const ambiguousList = androidFiltered
|
|
184
|
-
.map((
|
|
228
|
+
.map((target) => ` deviceName=${JSON.stringify(target.deviceName || '')} ws=${target.webSocketDebuggerUrl || ''}`)
|
|
185
229
|
.join('\n');
|
|
186
230
|
throw coded(new Error(
|
|
187
|
-
`Pinned Android device is ambiguous: model '${androidTargetName}' matches ${
|
|
231
|
+
`Pinned Android device is ambiguous: model '${androidTargetName}' matches ${matchingDeviceIdentities.length} Metro devices.\n` +
|
|
188
232
|
` Requested --device (ADB_SERIAL): ${adbSerial || '(not set)'}\n` +
|
|
189
233
|
` Matching Metro targets:\n${ambiguousList}\n` +
|
|
190
234
|
` Set ANDROID_DEVICE to the exact Metro deviceName to disambiguate.`,
|
|
@@ -206,7 +250,7 @@ async function discoverTarget(port) {
|
|
|
206
250
|
candidates = androidFiltered;
|
|
207
251
|
} else if (androidDevice) {
|
|
208
252
|
// Exact Metro deviceName match when the user set ANDROID_DEVICE directly.
|
|
209
|
-
androidFiltered = candidates.filter(
|
|
253
|
+
androidFiltered = candidates.filter(matchesAndroidPin);
|
|
210
254
|
if (androidFiltered.length === 0) {
|
|
211
255
|
// Pinned by exact Metro name but no candidate matched — fail fast.
|
|
212
256
|
const candidateList = targets
|
|
@@ -233,6 +277,17 @@ async function discoverTarget(port) {
|
|
|
233
277
|
);
|
|
234
278
|
}
|
|
235
279
|
|
|
280
|
+
if (acceptedPinnedCandidate) {
|
|
281
|
+
for (const candidate of candidates) {
|
|
282
|
+
if (candidate.webSocketDebuggerUrl === acceptedPinnedCandidate.webSocketDebuggerUrl) {
|
|
283
|
+
return {
|
|
284
|
+
wsUrl: acceptedPinnedCandidate.webSocketDebuggerUrl,
|
|
285
|
+
deviceName: acceptedPinnedCandidate.deviceName || '',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
236
291
|
// Sort by page number descending (JS runtime has higher page number than C++
|
|
237
292
|
// native). rankRuntimeCandidates output arrives pre-sorted; this re-sort is
|
|
238
293
|
// load-bearing only when the raw targets.filter() fallback above repopulated
|
|
@@ -251,6 +306,18 @@ async function discoverTarget(port) {
|
|
|
251
306
|
}
|
|
252
307
|
}
|
|
253
308
|
|
|
309
|
+
if (androidPinned) {
|
|
310
|
+
const candidateList = candidates
|
|
311
|
+
.map((target) => ` deviceName=${JSON.stringify(target.deviceName || '')} ws=${target.webSocketDebuggerUrl || ''}`)
|
|
312
|
+
.join('\n');
|
|
313
|
+
throw coded(new Error(
|
|
314
|
+
`Pinned Android device has no responding __AGENTIC__ JS runtime after ${FETCH_RETRIES} discovery attempt(s).\n` +
|
|
315
|
+
` Requested --device (ADB_SERIAL): ${adbSerial || '(not set)'}\n` +
|
|
316
|
+
` Matching non-agentic targets:\n${candidateList}\n` +
|
|
317
|
+
` The native C++ page is not a valid recipe target; wait for Android Hermes to re-register or relaunch the Android app.`,
|
|
318
|
+
), BRIDGE_ERROR_CODES.NO_TARGET);
|
|
319
|
+
}
|
|
320
|
+
|
|
254
321
|
// Fallback: return highest page number (most likely the JS runtime)
|
|
255
322
|
return { wsUrl: candidates[0].webSocketDebuggerUrl, deviceName: candidates[0].deviceName || '' };
|
|
256
323
|
}
|
|
@@ -26,10 +26,10 @@ function parseArgs(argv) {
|
|
|
26
26
|
async function main() {
|
|
27
27
|
const args = parseArgs(process.argv.slice(2));
|
|
28
28
|
if (args.help) {
|
|
29
|
-
console.log('Usage: launch-metro.cjs --target <path> --port <port> --log <path> --pid-file <path> [--workers <n>] [--clear]');
|
|
29
|
+
console.log('Usage: launch-metro.cjs --target <path> --port <port> --log <path> --pid-file <path> --build-env <path> [--workers <n>] [--clear]');
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
for (const required of ['target', 'port', 'log', 'pid-file']) {
|
|
32
|
+
for (const required of ['target', 'port', 'log', 'pid-file', 'build-env']) {
|
|
33
33
|
if (!args[required]) throw new Error(`--${required} is required`);
|
|
34
34
|
}
|
|
35
35
|
const target = path.resolve(args.target);
|
|
@@ -38,9 +38,9 @@ async function main() {
|
|
|
38
38
|
fs.mkdirSync(path.dirname(log), { recursive: true });
|
|
39
39
|
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
40
40
|
const logFd = fs.openSync(log, 'a');
|
|
41
|
-
const childArgs = ['
|
|
42
|
-
if (args.clear) childArgs.push('--clear');
|
|
41
|
+
const childArgs = ['watch'];
|
|
43
42
|
const env = { ...process.env };
|
|
43
|
+
env.BASH_ENV = args['build-env'];
|
|
44
44
|
if (args.workers) env.METRO_MAX_WORKERS = String(args.workers);
|
|
45
45
|
const child = spawn('yarn', childArgs, {
|
|
46
46
|
cwd: target,
|
|
@@ -59,10 +59,12 @@ async function main() {
|
|
|
59
59
|
`${JSON.stringify({
|
|
60
60
|
schemaVersion: 1,
|
|
61
61
|
pid: child.pid,
|
|
62
|
+
pgid: child.pid,
|
|
62
63
|
startedAt: new Date().toISOString(),
|
|
63
64
|
target,
|
|
64
65
|
port: Number(args.port),
|
|
65
66
|
clear: Boolean(args.clear),
|
|
67
|
+
command: `yarn ${childArgs[0]}`,
|
|
66
68
|
}, null, 2)}\n`,
|
|
67
69
|
);
|
|
68
70
|
child.unref();
|
|
@@ -90,19 +90,29 @@ ios_app_installed() {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
run_ios_native_build() (
|
|
93
|
-
local sim_target="$1"
|
|
94
|
-
|
|
93
|
+
local sim_target="$1" bundle_id="$2" reason="$3" build_env="" build_script="" build_type=""
|
|
94
|
+
case "$bundle_id" in
|
|
95
|
+
io.metamask.MetaMask|io.metamask) build_script="start:ios"; build_type="main" ;;
|
|
96
|
+
io.metamask.MetaMask-Flask) build_script="start:ios:flask"; build_type="flask" ;;
|
|
97
|
+
*)
|
|
98
|
+
printf 'open-device: no supported local build script for iOS bundle %s\n' "$bundle_id" >&2
|
|
99
|
+
return 1
|
|
100
|
+
;;
|
|
101
|
+
esac
|
|
102
|
+
printf '%s; installing with yarn %s (WATCHER_PORT=%s)\n' "$reason" "$build_script" "$PORT" >&2
|
|
95
103
|
build_env="$(mktemp "${TMPDIR:-/tmp}/mm-harness-ios-build-env.XXXXXX")" || return 1
|
|
96
104
|
chmod 600 "$build_env" || { rm -f "$build_env"; return 1; }
|
|
97
105
|
trap 'rm -f "$build_env"' EXIT HUP INT TERM
|
|
98
106
|
{
|
|
99
107
|
printf 'export MM_HARNESS_BUILD_WATCHER_PORT=%q\n' "$PORT"
|
|
100
108
|
printf 'export MM_HARNESS_BUILD_IOS_SIMULATOR=%q\n' "$sim_target"
|
|
109
|
+
printf 'export MM_HARNESS_BUILD_TYPE=%q\n' "$build_type"
|
|
101
110
|
cat <<'BUILD_ENV'
|
|
102
111
|
mm_harness_restore_build_target() {
|
|
103
112
|
export WATCHER_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
|
|
104
113
|
export METRO_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
|
|
105
114
|
export IOS_SIMULATOR="$MM_HARNESS_BUILD_IOS_SIMULATOR"
|
|
115
|
+
export METAMASK_BUILD_TYPE="$MM_HARNESS_BUILD_TYPE"
|
|
106
116
|
}
|
|
107
117
|
eval() {
|
|
108
118
|
builtin eval "$@"
|
|
@@ -122,7 +132,7 @@ BUILD_ENV
|
|
|
122
132
|
(
|
|
123
133
|
cd "$TARGET"
|
|
124
134
|
command -v activate_repo_ruby_best_effort >/dev/null 2>&1 && activate_repo_ruby_best_effort "$TARGET"
|
|
125
|
-
BASH_ENV="$build_env" WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" EXPO_NO_TYPESCRIPT_SETUP=1 yarn
|
|
135
|
+
BASH_ENV="$build_env" WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" METAMASK_BUILD_TYPE="$build_type" EXPO_NO_TYPESCRIPT_SETUP=1 yarn "$build_script" 2>&1
|
|
126
136
|
)
|
|
127
137
|
)
|
|
128
138
|
|
|
@@ -138,10 +148,16 @@ ensure_ios_app_for_mode() {
|
|
|
138
148
|
;;
|
|
139
149
|
auto|default)
|
|
140
150
|
if ios_app_installed "$sim_target" "$bundle_id"; then return 0; fi
|
|
141
|
-
run_ios_native_build "$sim_target" "iOS dev client ${bundle_id} not installed on ${sim_target}"
|
|
151
|
+
if ! run_ios_native_build "$sim_target" "$bundle_id" "iOS dev client ${bundle_id} not installed on ${sim_target}"; then
|
|
152
|
+
printf 'open-device: iOS native build failed\n' >&2
|
|
153
|
+
return 1
|
|
154
|
+
fi
|
|
142
155
|
;;
|
|
143
156
|
rebuild-native|clean)
|
|
144
|
-
run_ios_native_build "$sim_target" "Rebuilding iOS dev client ${bundle_id} (--preflight-mode ${mode})"
|
|
157
|
+
if ! run_ios_native_build "$sim_target" "$bundle_id" "Rebuilding iOS dev client ${bundle_id} (--preflight-mode ${mode})"; then
|
|
158
|
+
printf 'open-device: iOS native build failed\n' >&2
|
|
159
|
+
return 1
|
|
160
|
+
fi
|
|
145
161
|
;;
|
|
146
162
|
*)
|
|
147
163
|
printf 'open-device: unknown --preflight-mode: %s\n' "$mode" >&2; return 2 ;;
|
|
@@ -264,14 +280,113 @@ android_app_installed() {
|
|
|
264
280
|
"$ADB_BIN" "$@" shell pm list packages 2>/dev/null | grep -q "package:${pkg}"
|
|
265
281
|
}
|
|
266
282
|
|
|
267
|
-
run_android_native_build()
|
|
268
|
-
local
|
|
269
|
-
|
|
270
|
-
|
|
283
|
+
run_android_native_build() (
|
|
284
|
+
local serial="$1" expo_device="$2" pkg="$3" reason="$4" build_env="" build_script=""
|
|
285
|
+
case "$pkg" in
|
|
286
|
+
io.metamask) build_script="start:android" ;;
|
|
287
|
+
io.metamask.flask) build_script="start:android:flask" ;;
|
|
288
|
+
*)
|
|
289
|
+
printf 'open-device: no supported local build script for Android package %s\n' "$pkg" >&2
|
|
290
|
+
return 1
|
|
291
|
+
;;
|
|
292
|
+
esac
|
|
293
|
+
printf '%s; installing with yarn %s (WATCHER_PORT=%s)\n' "$reason" "$build_script" "$PORT" >&2
|
|
294
|
+
build_env="$(mktemp "${TMPDIR:-/tmp}/mm-harness-android-build-env.XXXXXX")" || return 1
|
|
295
|
+
chmod 600 "$build_env" || { rm -f "$build_env"; return 1; }
|
|
296
|
+
trap 'rm -f "$build_env"' EXIT HUP INT TERM
|
|
297
|
+
{
|
|
298
|
+
printf 'export MM_HARNESS_BUILD_ANDROID_SERIAL=%q\n' "$serial"
|
|
299
|
+
printf 'export MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE=%q\n' "$expo_device"
|
|
300
|
+
printf 'export MM_HARNESS_BUILD_WATCHER_PORT=%q\n' "$PORT"
|
|
301
|
+
cat <<'BUILD_ENV'
|
|
302
|
+
mm_harness_restore_android_build_target() {
|
|
303
|
+
export WATCHER_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
|
|
304
|
+
export METRO_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
|
|
305
|
+
export ADB_SERIAL="$MM_HARNESS_BUILD_ANDROID_SERIAL"
|
|
306
|
+
export ANDROID_SERIAL="$MM_HARNESS_BUILD_ANDROID_SERIAL"
|
|
307
|
+
}
|
|
308
|
+
eval() {
|
|
309
|
+
builtin eval "$@"
|
|
310
|
+
local status=$?
|
|
311
|
+
mm_harness_restore_android_build_target
|
|
312
|
+
return "$status"
|
|
313
|
+
}
|
|
314
|
+
source() {
|
|
315
|
+
builtin source "$@"
|
|
316
|
+
local status=$?
|
|
317
|
+
mm_harness_restore_android_build_target
|
|
318
|
+
return "$status"
|
|
319
|
+
}
|
|
320
|
+
yarn() {
|
|
321
|
+
if [ "${1:-}" = expo ] && [ "${2:-}" = run:android ]; then
|
|
322
|
+
command yarn "$@" "$MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE"
|
|
323
|
+
else
|
|
324
|
+
command yarn "$@"
|
|
325
|
+
fi
|
|
326
|
+
}
|
|
327
|
+
mm_harness_restore_android_build_target
|
|
328
|
+
BUILD_ENV
|
|
329
|
+
} > "$build_env"
|
|
330
|
+
(
|
|
331
|
+
cd "$TARGET"
|
|
332
|
+
BASH_ENV="$build_env" \
|
|
333
|
+
WATCHER_PORT="$PORT" METRO_PORT="$PORT" \
|
|
334
|
+
ADB_SERIAL="$serial" ANDROID_SERIAL="$serial" \
|
|
335
|
+
EXPO_NO_TYPESCRIPT_SETUP=1 yarn "$build_script" 2>&1
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
resolve_android_expo_device() {
|
|
340
|
+
local serial="$1" rows="" selected_line="" expo_device="" row_serial="" row_state="" row_name="" matches=0
|
|
341
|
+
[ -n "$serial" ] || return 0
|
|
342
|
+
rows="$($ADB_BIN devices -l 2>/dev/null || true)"
|
|
343
|
+
selected_line="$(printf '%s\n' "$rows" | awk -v wanted="$serial" '$1 == wanted { print; exit }')"
|
|
344
|
+
if [ -z "$selected_line" ]; then
|
|
345
|
+
printf 'open-device: configured Android device %s is not attached\n' "$serial" >&2
|
|
346
|
+
return 1
|
|
347
|
+
fi
|
|
348
|
+
row_state="$(printf '%s\n' "$selected_line" | awk '{print $2}')"
|
|
349
|
+
if [ "$row_state" != "device" ]; then
|
|
350
|
+
printf 'open-device: configured Android device %s is not ready (state: %s)\n' "$serial" "$row_state" >&2
|
|
351
|
+
return 1
|
|
352
|
+
fi
|
|
353
|
+
if printf '%s' "$serial" | grep -q '^emulator-'; then
|
|
354
|
+
expo_device="$($ADB_BIN -s "$serial" emu avd name 2>/dev/null | tr -d '\r' | sed '/^OK$/d' | head -1 || true)"
|
|
355
|
+
else
|
|
356
|
+
expo_device="$(printf '%s\n' "$selected_line" | awk '{ for (i = 3; i <= NF; i += 1) if ($i ~ /^model:/) { sub(/^model:/, "", $i); print $i; exit } }')"
|
|
357
|
+
fi
|
|
358
|
+
[ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
|
|
359
|
+
expo_device="${expo_device// /_}"
|
|
360
|
+
if [ -z "$expo_device" ]; then
|
|
361
|
+
printf 'open-device: could not resolve Expo device name for Android serial %s\n' "$serial" >&2
|
|
362
|
+
return 1
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
while IFS= read -r selected_line; do
|
|
366
|
+
row_serial="$(printf '%s\n' "$selected_line" | awk '{print $1}')"
|
|
367
|
+
row_state="$(printf '%s\n' "$selected_line" | awk '{print $2}')"
|
|
368
|
+
[ "$row_state" = "device" ] || continue
|
|
369
|
+
if printf '%s' "$row_serial" | grep -q '^emulator-'; then
|
|
370
|
+
row_name="$($ADB_BIN -s "$row_serial" emu avd name 2>/dev/null | tr -d '\r' | sed '/^OK$/d' | head -1 || true)"
|
|
371
|
+
else
|
|
372
|
+
row_name="$(printf '%s\n' "$selected_line" | awk '{ for (i = 3; i <= NF; i += 1) if ($i ~ /^model:/) { sub(/^model:/, "", $i); print $i; exit } }')"
|
|
373
|
+
fi
|
|
374
|
+
row_name="${row_name// /_}"
|
|
375
|
+
[ "$row_name" = "$expo_device" ] && matches=$((matches + 1))
|
|
376
|
+
done <<EOF
|
|
377
|
+
$rows
|
|
378
|
+
EOF
|
|
379
|
+
if [ "$matches" -ne 1 ]; then
|
|
380
|
+
printf 'open-device: Expo device name %s is ambiguous across %s attached Android devices; Expo cannot select serial %s safely\n' \
|
|
381
|
+
"$expo_device" "$matches" "$serial" >&2
|
|
382
|
+
return 1
|
|
383
|
+
fi
|
|
384
|
+
printf '%s\n' "$expo_device"
|
|
271
385
|
}
|
|
272
386
|
|
|
273
387
|
ensure_android_app_for_mode() {
|
|
274
388
|
local pkg="$1" mode="$2"
|
|
389
|
+
local expo_device=""
|
|
275
390
|
shift 2
|
|
276
391
|
case "$mode" in
|
|
277
392
|
fast)
|
|
@@ -281,10 +396,26 @@ ensure_android_app_for_mode() {
|
|
|
281
396
|
;;
|
|
282
397
|
auto|default)
|
|
283
398
|
if android_app_installed "$pkg" "$@"; then return 0; fi
|
|
284
|
-
|
|
399
|
+
if [ -n "$ADB_SERIAL_ARG" ]; then
|
|
400
|
+
expo_device="$(resolve_android_expo_device "$ADB_SERIAL_ARG")" || return 1
|
|
401
|
+
fi
|
|
402
|
+
[ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
|
|
403
|
+
expo_device="${expo_device// /_}"
|
|
404
|
+
if ! run_android_native_build "$ADB_SERIAL_ARG" "$expo_device" "$pkg" "Android dev client ${pkg} not installed"; then
|
|
405
|
+
printf 'open-device: Android native build failed\n' >&2
|
|
406
|
+
return 1
|
|
407
|
+
fi
|
|
285
408
|
;;
|
|
286
409
|
rebuild-native|clean)
|
|
287
|
-
|
|
410
|
+
if [ -n "$ADB_SERIAL_ARG" ]; then
|
|
411
|
+
expo_device="$(resolve_android_expo_device "$ADB_SERIAL_ARG")" || return 1
|
|
412
|
+
fi
|
|
413
|
+
[ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
|
|
414
|
+
expo_device="${expo_device// /_}"
|
|
415
|
+
if ! run_android_native_build "$ADB_SERIAL_ARG" "$expo_device" "$pkg" "Rebuilding Android dev client ${pkg} (--preflight-mode ${mode})"; then
|
|
416
|
+
printf 'open-device: Android native build failed\n' >&2
|
|
417
|
+
return 1
|
|
418
|
+
fi
|
|
288
419
|
;;
|
|
289
420
|
*)
|
|
290
421
|
printf 'open-device: unknown --preflight-mode: %s\n' "$mode" >&2; return 2 ;;
|
|
@@ -301,8 +432,16 @@ PY
|
|
|
301
432
|
|
|
302
433
|
# --- dispatch -----------------------------------------------------------------
|
|
303
434
|
|
|
304
|
-
|
|
305
|
-
|
|
435
|
+
if [ -n "${IOS_BUNDLE_ID:-}" ]; then
|
|
436
|
+
BUNDLE_IDS=("$IOS_BUNDLE_ID")
|
|
437
|
+
else
|
|
438
|
+
BUNDLE_IDS=("io.metamask.MetaMask" "io.metamask" "io.metamask.MetaMask-Flask" "io.metamask.qa")
|
|
439
|
+
fi
|
|
440
|
+
if [ -n "${ANDROID_PACKAGE_ID:-}" ]; then
|
|
441
|
+
ANDROID_PKGS=("$ANDROID_PACKAGE_ID")
|
|
442
|
+
else
|
|
443
|
+
ANDROID_PKGS=("io.metamask" "io.metamask.flask" "io.metamask.qa")
|
|
444
|
+
fi
|
|
306
445
|
|
|
307
446
|
if [ "$PLATFORM" = "ios" ]; then
|
|
308
447
|
SIM_TARGET="$(sim_udid_for_target "$SIMULATOR")"
|
|
@@ -330,7 +469,12 @@ if [ "$PLATFORM" = "ios" ]; then
|
|
|
330
469
|
|
|
331
470
|
LAUNCHED=false
|
|
332
471
|
for BUNDLE_ID in "${BUNDLE_IDS[@]}"; do
|
|
333
|
-
ensure_ios_app_for_mode "$SIM_TARGET" "$BUNDLE_ID" "$PREFLIGHT_MODE"
|
|
472
|
+
if ! ensure_ios_app_for_mode "$SIM_TARGET" "$BUNDLE_ID" "$PREFLIGHT_MODE"; then
|
|
473
|
+
case "$PREFLIGHT_MODE" in
|
|
474
|
+
fast) continue ;;
|
|
475
|
+
*) exit 1 ;;
|
|
476
|
+
esac
|
|
477
|
+
fi
|
|
334
478
|
if $RESTART; then
|
|
335
479
|
xcrun simctl terminate "$SIM_TARGET" "$BUNDLE_ID" >/dev/null 2>&1 || true
|
|
336
480
|
printf 'Restarting iOS dev client %s on %s\n' "$BUNDLE_ID" "$SIM_TARGET" >&2
|
|
@@ -384,7 +528,12 @@ else
|
|
|
384
528
|
|
|
385
529
|
LAUNCHED=false
|
|
386
530
|
for PKG in "${ANDROID_PKGS[@]}"; do
|
|
387
|
-
ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}"
|
|
531
|
+
if ! ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}"; then
|
|
532
|
+
case "$PREFLIGHT_MODE" in
|
|
533
|
+
fast) continue ;;
|
|
534
|
+
*) exit 1 ;;
|
|
535
|
+
esac
|
|
536
|
+
fi
|
|
388
537
|
if $RESTART; then
|
|
389
538
|
"$ADB_BIN" "${ADB_ARGS[@]}" shell am force-stop "$PKG" >/dev/null 2>&1 || true
|
|
390
539
|
printf 'Restarting Android dev client %s\n' "$PKG" >&2
|