@deeeed/metamask-harness 0.78.0 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/adapters/extension/ensure-browser.sh +2 -2
  4. package/adapters/extension/inject.mjs +0 -1
  5. package/adapters/extension/launch-browser.cjs +39 -613
  6. package/adapters/extension/readiness.mjs +2 -5
  7. package/adapters/extension/reattach.sh +2 -2
  8. package/adapters/extension/stamp-runtime-title.cjs +1 -1
  9. package/adapters/manifest.json +2 -34
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
  11. package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
  12. package/adapters/mobile/inject.sh +16 -0
  13. package/adapters/mobile/launch-console-forwarder.cjs +1 -2
  14. package/adapters/mobile/open-device.sh +2 -2
  15. package/adapters/mobile/reload-app.mjs +4 -4
  16. package/adapters/mobile/reset-app-data.sh +1 -1
  17. package/adapters/mobile/start-console-forwarder.sh +3 -2
  18. package/adapters/mobile/start-metro.sh +17 -12
  19. package/adapters/mobile/wait-for-bridge.cjs +2 -2
  20. package/adapters/shared/ensure-runner-deps.sh +2 -2
  21. package/adapters/shared/harness-source-fingerprint.mjs +13 -8
  22. package/adapters/shared/open-debug.mjs +2 -2
  23. package/adapters/shared/runner-deps-ready.mjs +1 -1
  24. package/adapters/shared/sync-wallet-fixture.sh +8 -5
  25. package/adapters/terminal/cleanup.mjs +2 -1
  26. package/adapters/terminal/inject.mjs +2 -1
  27. package/adapters/terminal/lib/cdp-client.mjs +1 -1
  28. package/bin/mm-harness +9 -28
  29. package/dist/adapters/checkout-files.js +16 -0
  30. package/dist/adapters/console-capture.js +14 -1
  31. package/dist/adapters/core/actions.js +4 -3
  32. package/dist/adapters/core/overlay.js +1 -1
  33. package/dist/adapters/core/readiness.js +28 -1
  34. package/dist/adapters/core/run.js +3 -3
  35. package/dist/adapters/core/surface.js +22 -5
  36. package/dist/adapters/extension/browser-pid.js +56 -0
  37. package/dist/adapters/extension/console-capture.js +1 -1
  38. package/dist/adapters/extension/doctor-checks.js +1 -1
  39. package/dist/adapters/extension/ensure-ready.js +5 -4
  40. package/dist/adapters/extension/extension-id.js +1 -1
  41. package/dist/adapters/extension/feature-flags.js +1 -1
  42. package/dist/adapters/extension/fixtures.js +1 -2
  43. package/dist/adapters/extension/launch.js +4 -5
  44. package/dist/adapters/extension/launched-browser.js +1 -2
  45. package/dist/adapters/extension/network-observer.js +1 -1
  46. package/dist/adapters/extension/overlay.js +1 -1
  47. package/dist/adapters/extension/product-config.js +1 -1
  48. package/dist/adapters/extension/readiness.js +1 -1
  49. package/dist/adapters/extension/reload.js +1 -1
  50. package/dist/adapters/extension/run.js +24 -10
  51. package/dist/adapters/extension/runtime-decision.js +3 -2
  52. package/dist/adapters/extension/runtime.js +9 -5
  53. package/dist/adapters/extension/surface.js +20 -5
  54. package/dist/adapters/extension/verify.js +2 -2
  55. package/dist/adapters/harness-freshness.js +2 -1
  56. package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
  57. package/dist/adapters/mobile/app-identity.js +10 -0
  58. package/dist/adapters/mobile/doctor-checks.js +1 -1
  59. package/dist/adapters/mobile/fixtures.js +1 -1
  60. package/dist/adapters/mobile/launch.js +27 -28
  61. package/dist/adapters/mobile/lifecycle.js +1 -1
  62. package/dist/adapters/mobile/metro-env.js +9 -59
  63. package/dist/adapters/mobile/performance-observer.js +1 -1
  64. package/dist/adapters/mobile/prepare-target.js +1 -1
  65. package/dist/adapters/mobile/prepare.js +4 -5
  66. package/dist/adapters/mobile/provision.js +1 -1
  67. package/dist/adapters/mobile/readiness.js +1 -1
  68. package/dist/adapters/mobile/recording-target.js +15 -0
  69. package/dist/adapters/mobile/release-artifact-state.js +1 -1
  70. package/dist/adapters/mobile/reload.js +1 -1
  71. package/dist/adapters/mobile/run.js +9 -8
  72. package/dist/adapters/mobile/runtime-decision.js +5 -7
  73. package/dist/adapters/mobile/source-freshness.js +22 -126
  74. package/dist/adapters/mobile/surface.js +31 -6
  75. package/dist/adapters/mobile/ui.js +16 -5
  76. package/dist/adapters/mobile/verify.js +2 -3
  77. package/dist/adapters/performance/cdp-trace.js +1 -1
  78. package/dist/adapters/performance/device-platform.js +1 -1
  79. package/dist/adapters/registry.js +2 -14
  80. package/dist/adapters/slot-ports.js +1 -1
  81. package/dist/adapters/surface.js +1 -0
  82. package/dist/adapters/terminal/console-capture.js +1 -1
  83. package/dist/adapters/terminal/launch.js +1 -1
  84. package/dist/adapters/terminal/run.js +25 -2
  85. package/dist/adapters/terminal/surface.js +12 -4
  86. package/dist/adapters.js +8 -4
  87. package/dist/advisor-policy.js +1 -1
  88. package/dist/checkout-view.js +1 -2
  89. package/dist/cli.js +28 -15
  90. package/dist/command-contract.js +1 -26
  91. package/dist/commands/call.js +10 -24
  92. package/dist/commands/check.js +3 -4
  93. package/dist/commands/checklist.js +1 -1
  94. package/dist/commands/completion-candidates.js +2 -10
  95. package/dist/commands/config.js +2 -2
  96. package/dist/commands/core-readiness.js +12 -7
  97. package/dist/commands/debug.js +3 -3
  98. package/dist/commands/device-target.js +5 -5
  99. package/dist/commands/doctor.js +2 -14
  100. package/dist/commands/domain.js +2 -2
  101. package/dist/commands/ensure-ready.js +1 -7
  102. package/dist/commands/fixtures.js +5 -6
  103. package/dist/commands/help.js +3 -4
  104. package/dist/commands/launch/device.js +10 -17
  105. package/dist/commands/launch/extension.js +3 -9
  106. package/dist/commands/launch/mobile.js +1 -1
  107. package/dist/commands/list-executables.js +1 -3
  108. package/dist/commands/logs.js +3 -3
  109. package/dist/commands/manifest.js +1 -12
  110. package/dist/commands/mobile-device-view.js +4 -2
  111. package/dist/commands/overlay.js +110 -0
  112. package/dist/commands/pin.js +1 -1
  113. package/dist/commands/pr-body.js +2 -2
  114. package/dist/commands/prepare.js +6 -11
  115. package/dist/commands/provision.js +3 -12
  116. package/dist/commands/recipe-advice.js +1 -2
  117. package/dist/commands/recipe-quality.js +2 -3
  118. package/dist/commands/resolve-extension.js +1 -7
  119. package/dist/commands/review.js +4 -4
  120. package/dist/commands/run-engine.js +12 -51
  121. package/dist/commands/run.js +4 -25
  122. package/dist/commands/runtime-decision.js +1 -6
  123. package/dist/commands/runtime-health.js +1 -7
  124. package/dist/commands/runtime-launch.js +1 -13
  125. package/dist/commands/self-test.js +2 -6
  126. package/dist/commands/status-watch.js +1 -2
  127. package/dist/commands/status.js +2 -5
  128. package/dist/commands/task-init.js +2 -3
  129. package/dist/commands/tutorial.js +2 -2
  130. package/dist/commands/update.js +1 -1
  131. package/dist/completions-cache.js +1 -1
  132. package/dist/console-classifier.js +19 -0
  133. package/dist/discovery.js +1 -1
  134. package/dist/doctor.js +3 -4
  135. package/dist/funding-execution-context.js +15 -1
  136. package/dist/host.js +32 -0
  137. package/dist/index.js +1 -6
  138. package/dist/live-tsx.js +49 -0
  139. package/dist/mm-harness-cli.js +2 -4
  140. package/dist/network-observation.js +4 -4
  141. package/dist/paths.js +48 -78
  142. package/dist/performance-observation.js +2 -2
  143. package/dist/progress.js +1 -1
  144. package/dist/recipe-assistance-context.js +2 -4
  145. package/dist/recipe-security.js +2 -59
  146. package/dist/review/knowledge.js +3 -3
  147. package/dist/runner.js +5 -3
  148. package/dist/runtime-context.js +1 -1
  149. package/dist/task-view.js +3 -4
  150. package/docs/CONTRIBUTING.md +3 -3
  151. package/docs/cli-contract.md +2 -2
  152. package/docs/feature-guard.md +90 -0
  153. package/library/README.md +3 -1
  154. package/library/actions/core/perps/_controller.mjs +153 -130
  155. package/library/actions/core/perps/quote_fees.mjs +58 -0
  156. package/library/actions/core/wallet/_fixture-accounts.mjs +69 -0
  157. package/library/actions/core/wallet/list_accounts.mjs +14 -57
  158. package/library/actions/extension/feature_flags/state.mjs +1 -1
  159. package/library/actions/extension/platform/cdp.mjs +2 -8
  160. package/library/actions/harness-exports.mjs +1 -6
  161. package/library/actions/mobile/app/network-control.mjs +6 -8
  162. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
  163. package/library/actions/mobile/platform/bridge.mjs +8 -8
  164. package/library/actions/mobile/platform/native-session-name.mjs +21 -1
  165. package/library/actions/mobile/platform/native-session.mjs +3 -4
  166. package/library/actions/mobile/platform/observe-ui.mjs +12 -6
  167. package/library/actions/mobile/ui/native-navigation.mjs +4 -3
  168. package/library/actions/mobile/wallet/native-ui.mjs +3 -2
  169. package/library/actions/shared/analytics/_adapter.mjs +1 -1
  170. package/library/actions/shared/console-findings.mjs +1 -1
  171. package/library/actions/terminal/platform/runtime.mjs +2 -1
  172. package/library/manifests/core.action-manifest.json +83 -13
  173. package/package.json +8 -8
  174. package/adapters/extension/lib/slot-title.cjs +0 -292
  175. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
  176. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
  177. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
  178. package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
  179. package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
  180. package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
  181. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
  182. package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
  183. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
  184. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
  185. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
  186. package/adapters/mobile/coalesce-metro-log.cjs +0 -24
  187. package/adapters/mobile/launch-metro.cjs +0 -77
  188. package/adapters/mobile/metro-config.cjs +0 -98
  189. package/adapters/mobile/metro-log-generation.cjs +0 -106
  190. package/dist/adapters/mobile/frame-metrics.js +0 -118
  191. package/dist/adapters/mobile/video-recorder.js +0 -289
  192. package/dist/artifact-files.js +0 -92
  193. package/dist/checkout-lock.js +0 -149
  194. package/dist/cli-color.js +0 -98
  195. package/dist/command-journal.js +0 -308
  196. package/dist/commands/last.js +0 -60
  197. package/dist/commands/launch/index.js +0 -109
  198. package/dist/commands/parse-args.js +0 -279
  199. package/dist/commands/reload.js +0 -41
  200. package/dist/commands/run-report.js +0 -241
  201. package/dist/commands/shared.js +0 -323
  202. package/dist/commands/stop.js +0 -60
  203. package/dist/devices.js +0 -100
  204. package/dist/execution-provenance.js +0 -332
  205. package/dist/harness.js +0 -451
  206. package/dist/heal-bounds.js +0 -137
  207. package/dist/json-stream.js +0 -60
  208. package/dist/leaf-invoke.js +0 -19
  209. package/dist/library-provenance.js +0 -33
  210. package/dist/live-adapter-contract.js +0 -380
  211. package/dist/recording-target.js +0 -120
  212. package/dist/run-diagnostics.js +0 -364
  213. package/dist/run-recording.js +0 -481
  214. package/dist/runtime-proof.js +0 -90
  215. package/library/actions/mobile/platform/tool-paths.mjs +0 -123
@@ -1,403 +0,0 @@
1
- 'use strict';
2
-
3
- const http = require('node:http');
4
- const { loadSimulatorName, loadAndroidDevice, loadAndroidTargetDeviceName } = require('./config.cjs');
5
- const { createWSClient } = require('./ws-client.cjs');
6
- const { BRIDGE_ERROR_CODES, coded } = require('./bridge-errors.cjs');
7
-
8
- const FETCH_TIMEOUT_MS = Number.parseInt(process.env.CDP_TIMEOUT || '30000', 10);
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
- );
15
-
16
- /** Fetch JSON from a URL (http only, no external deps) */
17
- function fetchJSONOnce(url) {
18
- return new Promise((resolve, reject) => {
19
- const req = http.get(url, (res) => {
20
- let data = '';
21
- res.on('data', (chunk) => (data += chunk));
22
- res.on('end', () => {
23
- try {
24
- resolve(JSON.parse(data));
25
- } catch (e) {
26
- reject(new Error(`Failed to parse JSON from ${url}: ${e.message}`));
27
- }
28
- });
29
- });
30
- req.on('error', reject);
31
- req.setTimeout(FETCH_TIMEOUT_MS, () => {
32
- req.destroy();
33
- reject(new Error(`Timeout fetching ${url} after ${FETCH_TIMEOUT_MS}ms`));
34
- });
35
- });
36
- }
37
-
38
- async function fetchJSON(url, acceptResponse) {
39
- let lastError;
40
- let lastResponse;
41
- for (let attempt = 1; attempt <= FETCH_RETRIES; attempt++) {
42
- try {
43
- const response = await fetchJSONOnce(url);
44
- lastError = undefined;
45
- lastResponse = response;
46
- if (!acceptResponse || await acceptResponse(response)) return response;
47
- } catch (error) {
48
- lastError = error;
49
- }
50
- if (attempt === FETCH_RETRIES) break;
51
- await new Promise((resolve) =>
52
- setTimeout(resolve, DISCOVERY_RETRY_DELAY_MS),
53
- );
54
- }
55
- if (lastError !== undefined) throw lastError;
56
- if (lastResponse !== undefined) return lastResponse;
57
- throw lastError;
58
- }
59
-
60
- /**
61
- * Quick probe: connect to a CDP target, evaluate `__DEV__`, disconnect.
62
- * Returns true if __AGENTIC__ is installed, false otherwise.
63
- */
64
- async function probeTarget(wsUrl) {
65
- return (await probeTargetDetailed(wsUrl)) === 'agentic';
66
- }
67
-
68
- /**
69
- * Three-state probe so callers can tell a pre-__AGENTIC__ build apart from a
70
- * dead target:
71
- * 'agentic' — JS runtime answered and __AGENTIC__ is installed.
72
- * 'responsive' — the target evaluated JS but has no __AGENTIC__ (an app build
73
- * that predates the bridge, or a non-runtime page).
74
- * 'unreachable' — connect/eval failed.
75
- */
76
- async function probeTargetDetailed(wsUrl) {
77
- try {
78
- const client = await createWSClient(wsUrl, 3000);
79
- try {
80
- const result = await client.send('Runtime.evaluate', {
81
- expression: '(function(){ return typeof globalThis.__AGENTIC__; })()',
82
- returnByValue: true,
83
- awaitPromise: false,
84
- });
85
- return result?.result?.value === 'object' ? 'agentic' : 'responsive';
86
- } finally {
87
- client.close();
88
- }
89
- } catch {
90
- // Connection failed — target is not usable at all
91
- return 'unreachable';
92
- }
93
- }
94
-
95
- /**
96
- * Filter Metro /json/list targets to debugger-capable RN/Hermes pages and rank
97
- * them JS-runtime-first. Pre-RN-0.81 titles say "React Native"/"Hermes";
98
- * RN 0.81+ Bridgeless titles are bundle-id-only (e.g. "io.metamask.MetaMask
99
- * (mm-5)") with the runtime kind in `description`. Each device exposes
100
- * multiple pages — page 1 is the native C++ runtime; the JS runtime (console,
101
- * __AGENTIC__) has a higher page number, so after the sort the first candidate
102
- * per device is its JS runtime.
103
- */
104
- function rankRuntimeCandidates(targets) {
105
- const candidates = (Array.isArray(targets) ? targets : []).filter(
106
- (t) =>
107
- t.webSocketDebuggerUrl &&
108
- ((t.title && (/react/i.test(t.title) || /hermes/i.test(t.title))) ||
109
- /bridgeless|hermes/i.test(t.description || '')),
110
- );
111
- candidates.sort((a, b) => {
112
- const aPage = Number.parseInt((a.id || '').split('-').pop() || '0', 10);
113
- const bPage = Number.parseInt((b.id || '').split('-').pop() || '0', 10);
114
- return bPage - aPage;
115
- });
116
- return candidates;
117
- }
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
-
132
- /**
133
- * Discover the Hermes CDP WebSocket URL from Metro's /json/list endpoint.
134
- *
135
- * Multi-simulator support:
136
- * - When IOS_SIMULATOR is set, filters targets by deviceName
137
- * - With Hermes bridgeless mode, there are multiple pages per device:
138
- * page 1 = native C++ runtime, page 2+ = JS runtime (where __AGENTIC__ lives)
139
- * - We probe candidates to find the one with __AGENTIC__ installed
140
- */
141
- async function discoverTarget(port, { probe = true } = {}) {
142
- const listUrl = `http://127.0.0.1:${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 iosPinned =
149
- process.env.MM_HARNESS_EXPLICIT_PLATFORM === 'ios' ||
150
- Boolean(process.env.SIM_UDID);
151
- const matchesAndroidPin = (target) => androidTargetName
152
- ? target.deviceName !== simName &&
153
- (target.deviceName === androidTargetName ||
154
- (target.deviceName != null && target.deviceName.startsWith(androidTargetName)))
155
- : target.deviceName === androidDevice;
156
- let acceptedPinnedCandidate;
157
- let targets;
158
- try {
159
- targets = await fetchJSON(listUrl, async (response) => {
160
- const runtimeCandidates = rankRuntimeCandidates(response);
161
- if (runtimeCandidates.length === 0) return false;
162
- if (androidPinned) {
163
- const pinnedCandidates = runtimeCandidates.filter(matchesAndroidPin);
164
- for (const candidate of pinnedCandidates) {
165
- if (!probe || (await probeTarget(candidate.webSocketDebuggerUrl))) {
166
- acceptedPinnedCandidate = candidate;
167
- return true;
168
- }
169
- }
170
- return false;
171
- }
172
- if (iosPinned && simName) {
173
- const pinnedCandidates = runtimeCandidates.filter(
174
- (candidate) => candidate.deviceName === simName,
175
- );
176
- for (const candidate of pinnedCandidates) {
177
- if (!probe || (await probeTarget(candidate.webSocketDebuggerUrl))) {
178
- acceptedPinnedCandidate = candidate;
179
- return true;
180
- }
181
- }
182
- return false;
183
- }
184
- return true;
185
- });
186
- } catch (e) {
187
- throw coded(
188
- new Error(`Cannot reach Metro at ${listUrl}. Is Metro running?\n ${e.message}`),
189
- BRIDGE_ERROR_CODES.METRO_UNREACHABLE,
190
- );
191
- }
192
-
193
- if (!Array.isArray(targets) || targets.length === 0) {
194
- throw coded(new Error(`No debug targets found at ${listUrl}`), BRIDGE_ERROR_CODES.NO_TARGET);
195
- }
196
-
197
- // Filter to React Native / Hermes targets with a WebSocket URL
198
- let candidates = rankRuntimeCandidates(targets);
199
-
200
- // Android pin identities (loaded before the simulator filter so an explicit
201
- // android pin can take precedence over an ambient simulator name):
202
- // ANDROID_TARGET_DEVICE_NAME — Metro-compatible model prefix resolved by bridge.mjs
203
- // from the adb serial via `adb -s <serial> shell getprop ro.product.model`.
204
- // Example: "Pixel 6" matches Metro deviceName "Pixel 6 - 16 - API 36".
205
- // ANDROID_DEVICE — exact user-specified Metro deviceName.
206
- // Filter by simulator name if IOS_SIMULATOR is set — but never when an android
207
- // pin is present: dual-platform slots carry an ambient IOS_SIMULATOR in their
208
- // context, and letting it win here sends a pinned android action to the iOS
209
- // target. No-match keeps the full candidate set (ambient sim configs tolerate a
210
- // sim that is not currently attached).
211
- if (simName && !androidPinned) {
212
- const deviceFiltered = candidates.filter(
213
- (t) => t.deviceName === simName,
214
- );
215
- if (deviceFiltered.length > 0) {
216
- candidates = deviceFiltered;
217
- } else if (iosPinned) {
218
- throw coded(new Error(
219
- `Pinned iOS simulator '${simName}' did not match any Metro target.`,
220
- ), BRIDGE_ERROR_CODES.NO_TARGET);
221
- }
222
- }
223
-
224
- // Android pin enforcement — at ANY candidate count. A pin gated on
225
- // candidates.length > 1 silently accepted a single WRONG candidate (observed
226
- // live: the pinned Pixel's target dropped off Metro while the iOS target
227
- // remained; the pin was ignored and the recipe drove the simulator). When a
228
- // device is pinned, we MUST NOT silently fall back to another target; an
229
- // unmatchable pin fails fast with diagnostics.
230
- if (androidPinned) {
231
- let androidFiltered = [];
232
-
233
- if (androidTargetName) {
234
- // Model prefix match: "Pixel 6" matches "Pixel 6 - 16 - API 36". Metro's
235
- // /json/list has no explicit platform field, so scope the ANDROID pin by
236
- // excluding the one iOS identity we do know (the pinned simulator name) —
237
- // a simulator named to shadow a model string must not satisfy an android pin.
238
- androidFiltered = candidates.filter(matchesAndroidPin);
239
- const matchingDeviceIdentities = [...new Set(
240
- androidFiltered.map(targetDeviceIdentity),
241
- )];
242
- if (matchingDeviceIdentities.length > 1) {
243
- // Two same-model devices produce identical Metro model prefixes — the pin
244
- // is genuinely ambiguous. Native + JS pages share the same Metro device
245
- // identity and must remain available for probing.
246
- const ambiguousList = androidFiltered
247
- .map((target) => ` deviceName=${JSON.stringify(target.deviceName || '')} ws=${target.webSocketDebuggerUrl || ''}`)
248
- .join('\n');
249
- throw coded(new Error(
250
- `Pinned Android device is ambiguous: model '${androidTargetName}' matches ${matchingDeviceIdentities.length} Metro devices.\n` +
251
- ` Requested --device (ADB_SERIAL): ${adbSerial || '(not set)'}\n` +
252
- ` Matching Metro targets:\n${ambiguousList}\n` +
253
- ` Set ANDROID_DEVICE to the exact Metro deviceName to disambiguate.`,
254
- ), BRIDGE_ERROR_CODES.NO_TARGET);
255
- }
256
- if (androidFiltered.length === 0) {
257
- // Pinned device could not be matched — never silently pick another target.
258
- const candidateList = targets
259
- .map((t) => ` deviceName=${JSON.stringify(t.deviceName || '')} ws=${t.webSocketDebuggerUrl || ''}`)
260
- .join('\n');
261
- throw coded(new Error(
262
- `Pinned Android device did not match any Metro target.\n` +
263
- ` Requested --device (ADB_SERIAL): ${adbSerial || '(not set)'}\n` +
264
- ` Resolved model (ANDROID_TARGET_DEVICE_NAME): ${androidTargetName}\n` +
265
- ` ANDROID_DEVICE: ${androidDevice || '(not set)'}\n` +
266
- ` Metro /json/list candidates:\n${candidateList}`,
267
- ), BRIDGE_ERROR_CODES.NO_TARGET);
268
- }
269
- candidates = androidFiltered;
270
- } else if (androidDevice) {
271
- // Exact Metro deviceName match when the user set ANDROID_DEVICE directly.
272
- androidFiltered = candidates.filter(matchesAndroidPin);
273
- if (androidFiltered.length === 0) {
274
- // Pinned by exact Metro name but no candidate matched — fail fast.
275
- const candidateList = targets
276
- .map((t) => ` deviceName=${JSON.stringify(t.deviceName || '')} ws=${t.webSocketDebuggerUrl || ''}`)
277
- .join('\n');
278
- throw coded(new Error(
279
- `Pinned Android device (ANDROID_DEVICE='${androidDevice}') did not match any Metro target.\n` +
280
- ` ADB_SERIAL: ${adbSerial || '(not set)'}\n` +
281
- ` Metro /json/list candidates:\n${candidateList}`,
282
- ), BRIDGE_ERROR_CODES.NO_TARGET);
283
- }
284
- candidates = androidFiltered;
285
- }
286
- }
287
-
288
- if (candidates.length === 0) {
289
- candidates = targets.filter((t) => t.webSocketDebuggerUrl);
290
- }
291
-
292
- if (candidates.length === 0) {
293
- throw coded(
294
- new Error(`No suitable debug target found. Targets:\n${JSON.stringify(targets, null, 2)}`),
295
- BRIDGE_ERROR_CODES.NO_TARGET,
296
- );
297
- }
298
-
299
- if (acceptedPinnedCandidate) {
300
- for (const candidate of candidates) {
301
- if (candidate.webSocketDebuggerUrl === acceptedPinnedCandidate.webSocketDebuggerUrl) {
302
- return {
303
- id: acceptedPinnedCandidate.id || '',
304
- wsUrl: acceptedPinnedCandidate.webSocketDebuggerUrl,
305
- deviceName: acceptedPinnedCandidate.deviceName || '',
306
- };
307
- }
308
- }
309
- }
310
-
311
- // The persistent console forwarder already owns and validates brokered CDP
312
- // sessions. Re-probing here would evict that debugger; preserve the normal
313
- // platform/device filtering and select the highest-ranked runtime instead.
314
- if (!probe) {
315
- return {
316
- id: candidates[0].id || '',
317
- wsUrl: candidates[0].webSocketDebuggerUrl,
318
- deviceName: candidates[0].deviceName || '',
319
- };
320
- }
321
-
322
- // Sort by page number descending (JS runtime has higher page number than C++
323
- // native). rankRuntimeCandidates output arrives pre-sorted; this re-sort is
324
- // load-bearing only when the raw targets.filter() fallback above repopulated
325
- // the candidate list.
326
- candidates.sort((a, b) => {
327
- const aPage = Number.parseInt((a.id || '').split('-').pop() || '0', 10);
328
- const bPage = Number.parseInt((b.id || '').split('-').pop() || '0', 10);
329
- return bPage - aPage;
330
- });
331
-
332
- // Probe candidates to find the one with __AGENTIC__ installed (the JS runtime)
333
- for (const candidate of candidates) {
334
- const hasAgentic = await probeTarget(candidate.webSocketDebuggerUrl);
335
- if (hasAgentic) {
336
- return { id: candidate.id || '', wsUrl: candidate.webSocketDebuggerUrl, deviceName: candidate.deviceName || '' };
337
- }
338
- }
339
-
340
- if (androidPinned || iosPinned) {
341
- const candidateList = candidates
342
- .map((target) => ` deviceName=${JSON.stringify(target.deviceName || '')} ws=${target.webSocketDebuggerUrl || ''}`)
343
- .join('\n');
344
- throw coded(new Error(
345
- `Pinned ${androidPinned ? 'Android device' : `iOS simulator '${simName || process.env.SIM_UDID || '(unknown)'}'`} has no responding __AGENTIC__ JS runtime after ${FETCH_RETRIES} discovery attempt(s).\n` +
346
- (androidPinned ? ` Requested --device (ADB_SERIAL): ${adbSerial || '(not set)'}\n` : '') +
347
- ` Matching non-agentic targets:\n${candidateList}\n` +
348
- ` The native C++ page is not a valid recipe target; wait for Hermes to re-register or relaunch the pinned app.`,
349
- ), BRIDGE_ERROR_CODES.NO_TARGET);
350
- }
351
-
352
- // Fallback: return highest page number (most likely the JS runtime)
353
- return { id: candidates[0].id || '', wsUrl: candidates[0].webSocketDebuggerUrl, deviceName: candidates[0].deviceName || '' };
354
- }
355
-
356
- /**
357
- * Discover ALL Hermes targets with __AGENTIC__ installed (one per platform/device).
358
- * Groups by deviceName so each device returns at most one target.
359
- */
360
- async function discoverAllTargets(port) {
361
- const listUrl = `http://127.0.0.1:${port}/json/list`;
362
- let targets;
363
- try {
364
- targets = await fetchJSON(listUrl);
365
- } catch (e) {
366
- throw coded(
367
- new Error(`Cannot reach Metro at ${listUrl}. Is Metro running?\n ${e.message}`),
368
- BRIDGE_ERROR_CODES.METRO_UNREACHABLE,
369
- );
370
- }
371
-
372
- const candidates = rankRuntimeCandidates(targets);
373
-
374
- // Group by deviceName, probe each to find the JS runtime target. Prefer the
375
- // device's __AGENTIC__-bearing target; when a device has none, keep its first
376
- // RESPONSIVE candidate (candidates are sorted JS-runtime-first) so a build
377
- // that predates the bridge still surfaces — the status consumer reports it as
378
- // bridge-absent instead of the device silently vanishing from discovery.
379
- // Known ambiguity: when a device's JS runtime is attached-but-busy while its
380
- // native C++ Hermes page answers, that page is also 'responsive' (no
381
- // __AGENTIC__ in the native context), so the device reports bridge-absent even
382
- // though the app build may carry the bridge. Low probability, and the degraded
383
- // report is informative rather than harmful.
384
- const agenticByDevice = new Map();
385
- const responsiveByDevice = new Map();
386
- for (const candidate of candidates) {
387
- const device = candidate.deviceName || candidate.id || candidate.webSocketDebuggerUrl;
388
- if (agenticByDevice.has(device)) continue;
389
- const state = await probeTargetDetailed(candidate.webSocketDebuggerUrl);
390
- if (state === 'agentic') {
391
- agenticByDevice.set(device, { wsUrl: candidate.webSocketDebuggerUrl, deviceName: device });
392
- } else if (state === 'responsive' && !responsiveByDevice.has(device)) {
393
- responsiveByDevice.set(device, { wsUrl: candidate.webSocketDebuggerUrl, deviceName: device });
394
- }
395
- }
396
- const results = [...agenticByDevice.values()];
397
- for (const [device, entry] of responsiveByDevice) {
398
- if (!agenticByDevice.has(device)) results.push(entry);
399
- }
400
- return results;
401
- }
402
-
403
- module.exports = { discoverTarget, discoverAllTargets, rankRuntimeCandidates };
@@ -1,116 +0,0 @@
1
- 'use strict';
2
-
3
- const { BRIDGE_ERROR_CODES, coded } = require('./bridge-errors.cjs');
4
- const WebSocket = require('ws');
5
-
6
- function createInspectorWebSocket(wsUrl) {
7
- const origin = new URL(wsUrl).origin.replace(/^ws/, 'http');
8
- return new WebSocket(wsUrl, { origin });
9
- }
10
-
11
- /**
12
- * Minimal CDP client that supplies the debugger Origin required by Metro.
13
- */
14
- function createWSClient(wsUrl, timeout) {
15
- return new Promise((resolve, reject) => {
16
- const ws = createInspectorWebSocket(wsUrl);
17
- let msgId = 0;
18
- const pending = new Map();
19
- const eventHandlers = new Map();
20
-
21
- const timer = setTimeout(() => {
22
- ws.close();
23
- reject(coded(new Error(`CDP connection timeout after ${timeout}ms`), BRIDGE_ERROR_CODES.CDP_TIMEOUT));
24
- }, timeout);
25
-
26
- ws.onopen = () => {
27
- clearTimeout(timer);
28
- resolve({
29
- /** Send a CDP command and wait for the response */
30
- send(method, params = {}, msgTimeout = timeout) {
31
- return new Promise((res, rej) => {
32
- const id = ++msgId;
33
- const timer = setTimeout(() => {
34
- pending.delete(id);
35
- rej(
36
- coded(
37
- new Error(
38
- `CDP message timeout after ${msgTimeout}ms for ${method}`,
39
- ),
40
- BRIDGE_ERROR_CODES.CDP_TIMEOUT,
41
- ),
42
- );
43
- }, msgTimeout);
44
- pending.set(id, {
45
- resolve: (v) => {
46
- clearTimeout(timer);
47
- res(v);
48
- },
49
- reject: (e) => {
50
- clearTimeout(timer);
51
- rej(e);
52
- },
53
- });
54
- const msg = JSON.stringify({ id, method, params });
55
- ws.send(msg);
56
- });
57
- },
58
- on(method, handler) {
59
- const handlers = eventHandlers.get(method) || new Set();
60
- handlers.add(handler);
61
- eventHandlers.set(method, handlers);
62
- return () => {
63
- handlers.delete(handler);
64
- if (handlers.size === 0) eventHandlers.delete(method);
65
- };
66
- },
67
- close() {
68
- ws.close();
69
- },
70
- });
71
- };
72
-
73
- ws.onmessage = (evt) => {
74
- const data = typeof evt.data === 'string' ? evt.data : evt.data.toString();
75
- let msg;
76
- try {
77
- msg = JSON.parse(data);
78
- } catch {
79
- // Non-JSON frame — ignore
80
- return;
81
- }
82
- if (msg.id && pending.has(msg.id)) {
83
- const { resolve: res, reject: rej } = pending.get(msg.id);
84
- pending.delete(msg.id);
85
- if (msg.error) {
86
- rej(new Error(`CDP error: ${JSON.stringify(msg.error)}`));
87
- } else {
88
- res(msg.result);
89
- }
90
- return;
91
- }
92
- if (!msg.method) return;
93
- for (const handler of eventHandlers.get(msg.method) || []) {
94
- try {
95
- handler(msg.params || {});
96
- } catch {}
97
- }
98
- };
99
-
100
- ws.onerror = (err) => {
101
- clearTimeout(timer);
102
- reject(coded(new Error(`WebSocket error: ${err.message || err}`), BRIDGE_ERROR_CODES.WS_CLOSED));
103
- };
104
-
105
- ws.onclose = () => {
106
- clearTimeout(timer);
107
- for (const [, { reject: rej }] of pending) {
108
- rej(coded(new Error('WebSocket closed'), BRIDGE_ERROR_CODES.WS_CLOSED));
109
- }
110
- pending.clear();
111
- eventHandlers.clear();
112
- };
113
- });
114
- }
115
-
116
- module.exports = { createWSClient, createInspectorWebSocket };
@@ -1,24 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const readline = require('node:readline');
5
-
6
- const progressPattern = /^\s*(?:iOS|Android).*?(\d{1,3}(?:\.\d+)?)%/u;
7
- let lastPercent = null;
8
- let lastProgressAt = 0;
9
-
10
- const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity, terminal: false });
11
- lines.on('line', (line) => {
12
- const progress = progressPattern.exec(line);
13
- if (!progress) {
14
- process.stdout.write(`${line}\n`);
15
- return;
16
- }
17
- const percent = Math.floor(Number(progress[1]));
18
- const now = Date.now();
19
- if (percent !== lastPercent || now - lastProgressAt >= 15_000) {
20
- process.stdout.write(`${line} [metro-progress ${new Date(now).toISOString()}]\n`);
21
- lastPercent = percent;
22
- lastProgressAt = now;
23
- }
24
- });
@@ -1,77 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const { spawn } = require('node:child_process');
5
- const fs = require('node:fs');
6
- const path = require('node:path');
7
-
8
- function parseArgs(argv) {
9
- const values = {};
10
- for (let index = 0; index < argv.length; index += 1) {
11
- const arg = argv[index];
12
- if (arg === '-h' || arg === '--help') {
13
- values.help = true;
14
- continue;
15
- }
16
- if (arg === '--clear') {
17
- values.clear = true;
18
- continue;
19
- }
20
- if (!arg.startsWith('--') || index + 1 >= argv.length) throw new Error(`invalid argument: ${arg}`);
21
- values[arg.slice(2)] = argv[++index];
22
- }
23
- return values;
24
- }
25
-
26
- async function main() {
27
- const args = parseArgs(process.argv.slice(2));
28
- if (args.help) {
29
- console.log('Usage: launch-metro.cjs --target <path> --port <port> --log <path> --pid-file <path> --build-env <path> --runner <path> [--workers <n>] [--clear]');
30
- return;
31
- }
32
- for (const required of ['target', 'port', 'log', 'pid-file', 'build-env', 'runner']) {
33
- if (!args[required]) throw new Error(`--${required} is required`);
34
- }
35
- const target = path.resolve(args.target);
36
- const log = path.resolve(args.log);
37
- const pidFile = path.resolve(args['pid-file']);
38
- const runner = path.resolve(args.runner);
39
- fs.mkdirSync(path.dirname(log), { recursive: true });
40
- fs.mkdirSync(path.dirname(pidFile), { recursive: true });
41
- const runnerStat = fs.lstatSync(runner);
42
- if (!runnerStat.isFile()) throw new Error('--runner must be a regular file');
43
- fs.accessSync(runner, fs.constants.X_OK);
44
- const env = { ...process.env };
45
- env.BASH_ENV = args['build-env'];
46
- if (args.workers) env.METRO_MAX_WORKERS = String(args.workers);
47
- const child = spawn(runner, [], {
48
- cwd: target,
49
- detached: true,
50
- env,
51
- stdio: 'ignore',
52
- });
53
- await new Promise((resolve, reject) => {
54
- child.once('spawn', resolve);
55
- child.once('error', reject);
56
- });
57
- fs.writeFileSync(pidFile, `${String(child.pid)}\n`);
58
- fs.writeFileSync(
59
- path.join(path.dirname(pidFile), 'metro-launch.json'),
60
- `${JSON.stringify({
61
- schemaVersion: 1,
62
- pid: child.pid,
63
- pgid: child.pid,
64
- startedAt: new Date().toISOString(),
65
- target,
66
- port: Number(args.port),
67
- clear: Boolean(args.clear),
68
- command: runner,
69
- }, null, 2)}\n`,
70
- );
71
- child.unref();
72
- }
73
-
74
- main().catch((error) => {
75
- console.error(`launch-metro: ${error.message}`);
76
- process.exit(1);
77
- });