@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
@@ -3,6 +3,11 @@
3
3
  /**
4
4
  * CDP Bridge — Connect to the running MetaMask app via Hermes Chrome DevTools Protocol.
5
5
  *
6
+ * MetaMask preset over the generic bridge CLI in
7
+ * @farmslot/adapter-rn/bridge-runtime/bridge-core.cjs: this file adds the
8
+ * wallet commands, the navigator route table, the recovery hints and the
9
+ * MetaMask help lines; every other command lives in the core.
10
+ *
6
11
  * Usage:
7
12
  * node adapters/mobile/bridge-runtime/cdp-bridge.cjs navigate PerpsMarketListView
8
13
  * node adapters/mobile/bridge-runtime/cdp-bridge.cjs get-route
@@ -16,167 +21,10 @@
16
21
 
17
22
  'use strict';
18
23
 
19
- const fs = require('node:fs');
20
- const path = require('node:path');
21
- const { execFileSync } = require('node:child_process');
22
- const { loadPort } = require('./lib/config.cjs');
23
- const {
24
- discoverTarget,
25
- } = require('./lib/target-discovery.cjs');
26
- const { createWSClient } = require('./lib/ws-client.cjs');
27
- const {
28
- brokerSocketPath,
29
- createBrokerClient,
30
- deviceIdFromUrl,
31
- } = require('./lib/cdp-broker.cjs');
32
- const {
33
- BRIDGE_ERROR_CODES,
34
- EXIT_CODE_BY_ERROR_CODE,
35
- TEACHING_BY_ERROR_CODE,
36
- classifyBridgeErrorMessage,
37
- coded,
38
- formatErrorMarker,
39
- } = require('./lib/bridge-errors.cjs');
40
- const { cdpEval, cdpEvalAsync } = require('./lib/cdp-eval.cjs');
41
- const { buildArmSnippet, buildCollectSnippet } = require('./lib/issue-capture.cjs');
42
- const APPLY_HUD_UPDATE_FUNCTION = `function(step) {
43
- const bridge = this.__AGENTIC__;
44
- if (step === null) {
45
- if (typeof bridge?.hideStep !== 'function') return false;
46
- setTimeout(() => bridge.hideStep(), 0);
47
- return true;
48
- }
49
- if (typeof bridge?.showStep !== 'function') return false;
50
- setTimeout(() => bridge.showStep(step), 0);
51
- return true;
52
- }`;
53
-
54
- function mobileToolPath(tool) {
55
- return process.env[`MM_HARNESS_${tool.toUpperCase()}_PATH`] || tool;
56
- }
57
-
58
- function bootedIosDevice(deviceName) {
59
- if (!deviceName) return null;
60
- try {
61
- const devices = JSON.parse(
62
- execFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '-j'], {
63
- encoding: 'utf8',
64
- }),
65
- );
66
- const device = Object.values(devices.devices || {})
67
- .flat()
68
- .find(
69
- (candidate) =>
70
- (candidate?.name === deviceName || candidate?.udid === deviceName) &&
71
- candidate?.state === 'Booted',
72
- );
73
- return device?.udid ? device : null;
74
- } catch {
75
- return null;
76
- }
77
- }
78
-
79
- function tapVisibleIosAccessibilityTarget(testId, deviceName, platform) {
80
- if (platform !== 'ios') return null;
81
- const device = bootedIosDevice(deviceName);
82
- if (!device) return null;
83
- try {
84
- const idb = mobileToolPath('idb');
85
- const elements = JSON.parse(
86
- execFileSync(
87
- idb,
88
- ['ui', 'describe-all', '--udid', device.udid, '--json'],
89
- { encoding: 'utf8' },
90
- ),
91
- );
92
- const windowFrame = elements
93
- .map((element) => element?.frame)
94
- .filter(
95
- (frame) =>
96
- frame &&
97
- frame.x === 0 &&
98
- frame.y === 0 &&
99
- frame.width > 0 &&
100
- frame.height > 0,
101
- )
102
- .sort(
103
- (first, second) =>
104
- second.width * second.height - first.width * first.height,
105
- )[0];
106
- if (!windowFrame) return null;
107
- const target = elements.find((element) => {
108
- const frame = element?.frame;
109
- return (
110
- element?.AXUniqueId === testId &&
111
- element?.enabled !== false &&
112
- element?.hittable !== false &&
113
- frame &&
114
- frame.width > 0 &&
115
- frame.height > 0 &&
116
- frame.x >= 0 &&
117
- frame.y >= 0 &&
118
- frame.x + frame.width <= windowFrame.width &&
119
- frame.y + frame.height <= windowFrame.height
120
- );
121
- });
122
- if (!target) return null;
123
- const x = Math.round(target.frame.x + target.frame.width / 2);
124
- const y = Math.round(target.frame.y + target.frame.height / 2);
125
- execFileSync(
126
- idb,
127
- ['ui', 'tap', String(x), String(y), '--udid', device.udid],
128
- { encoding: 'utf8' },
129
- );
130
- return { ok: true, testId, deviceName, provider: 'idb-accessibility' };
131
- } catch {
132
- return null;
133
- }
134
- }
24
+ const { runBridgeCli } = require('@farmslot/adapter-rn/bridge-runtime/bridge-core.cjs');
25
+ const { cdpEval } = require('@farmslot/adapter-rn/bridge-runtime/lib/cdp-eval.cjs');
135
26
 
136
- async function applyHudUpdate(client, step) {
137
- const globalObject = await client.send('Runtime.evaluate', {
138
- expression: 'globalThis',
139
- returnByValue: false,
140
- awaitPromise: false,
141
- });
142
- const objectId = globalObject?.result?.objectId;
143
- if (!objectId) {
144
- throw new Error('Mobile HUD runtime did not expose globalThis');
145
- }
146
- const result = await client.send('Runtime.callFunctionOn', {
147
- objectId,
148
- functionDeclaration: APPLY_HUD_UPDATE_FUNCTION,
149
- arguments: [{ value: step }],
150
- returnByValue: true,
151
- awaitPromise: false,
152
- });
153
- if (result?.exceptionDetails) {
154
- throw new Error(
155
- result.exceptionDetails.exception?.description ||
156
- result.exceptionDetails.text ||
157
- 'Mobile HUD update failed',
158
- );
159
- }
160
- return result?.result?.value === true;
161
- }
162
-
163
- function parsePerformanceConsoleEvent(params) {
164
- const markers = ['[PerpsPerf] ', '[HomepagePerf] '];
165
- for (const arg of params?.args || []) {
166
- const text = typeof arg?.value === 'string' ? arg.value : '';
167
- const marker = markers.find((candidate) => text.includes(candidate));
168
- if (!marker) continue;
169
- const markerIndex = text.indexOf(marker);
170
- try {
171
- return JSON.parse(text.slice(markerIndex + marker.length));
172
- } catch {}
173
- }
174
- return null;
175
- }
176
-
177
- // ---------------------------------------------------------------------------
178
- // Commands
179
- // ---------------------------------------------------------------------------
27
+ const { TEACHING_BY_ERROR_CODE } = require('./teaching.cjs');
180
28
 
181
29
  // Map of nested route names to their parent navigator.
182
30
  // Friendly aliases → actual route names (for developer convenience)
@@ -216,209 +64,23 @@ const NESTED_ROUTE_PARENTS = {
216
64
  DeveloperOptions: 'Settings', NotificationsSettings: 'Settings',
217
65
  };
218
66
 
219
- async function setInput(client, testId, value, { deviceName } = {}, redact = false) {
220
- if (!testId) {
221
- throw new Error('Usage: set-input <testId> <value>');
222
- }
223
- const expr = `(function() {
224
- if (globalThis.__AGENTIC__?.setInput) return Promise.resolve(globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)}));
225
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
226
- if (!hook) return { ok: false, error: 'No React DevTools hook' };
227
- var renderers = hook.renderers;
228
- if (!renderers) return { ok: false, error: 'No renderers' };
229
- var getFiberRoots = hook.getFiberRoots;
230
- function findByTestId(fiber) {
231
- if (!fiber) return null;
232
- var props = fiber.memoizedProps;
233
- if (props && props.testID === ${JSON.stringify(testId)}) return fiber;
234
- return findByTestId(fiber.child) || findByTestId(fiber.sibling);
235
- }
236
- for (var [id] of renderers) {
237
- var roots = getFiberRoots ? getFiberRoots(id) : undefined;
238
- if (!roots) continue;
239
- var result = null;
240
- roots.forEach(function(r) {
241
- if (result) return;
242
- var fiber = findByTestId(r.current);
243
- if (!fiber) return;
244
- var cur = fiber;
245
- while (cur) {
246
- if (cur.memoizedProps && typeof cur.memoizedProps.onChangeText === 'function') {
247
- cur.memoizedProps.onChangeText(${JSON.stringify(value)});
248
- result = { ok: true, testId: ${JSON.stringify(testId)} };
249
- return;
250
- }
251
- cur = cur.return || null;
252
- }
253
- });
254
- if (result) return result;
255
- }
256
- return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onChangeText' };
257
- })()`;
258
- let result;
259
- try {
260
- result = await cdpEvalAsync(client, expr);
261
- } catch (error) {
262
- if (redact) {
263
- throw new Error(`Secret input could not be applied for testID ${testId}.`);
264
- }
265
- throw error;
266
- }
267
- if (redact && result?.ok === false) {
268
- result = { ok: false, error: 'Secret input could not be applied.' };
269
- }
67
+ // Wallet readiness for `measure-scroll-transition` with `requireWalletReady: true`:
68
+ // an account is selected and the Login screen is gone.
69
+ const WALLET_READY_EXPRESSION = `(function(){
70
+ var route = globalThis.__AGENTIC__?.getRoute() || null;
71
+ var account = null;
72
+ try { account = globalThis.__AGENTIC__?.getSelectedAccount() || null; } catch(e) {}
73
+ var address = account && typeof account.address === 'string' ? account.address : '';
74
+ var id = account && typeof account.id === 'string' ? account.id : '';
270
75
  return {
271
- ...result,
272
- testId,
273
- value: redact ? '<redacted>' : value,
274
- deviceName,
76
+ ready: Boolean(address || id) && String(route && route.name || '') !== 'Login',
77
+ route: route,
78
+ accountPresent: Boolean(address || id),
79
+ observedAtMs: performance.now(),
275
80
  };
276
- }
277
-
278
- function parseHudStep(args) {
279
- const raw = args.join(' ');
280
- let step;
281
- try {
282
- step = JSON.parse(raw);
283
- } catch (error) {
284
- throw new Error(`show-step-json requires a JSON step payload: ${error.message}`);
285
- }
286
- if (!step || typeof step !== 'object' || Array.isArray(step)) {
287
- throw new Error('show-step-json requires a JSON object step payload');
288
- }
289
- if (typeof step.intent !== 'string' || !step.intent.trim()) {
290
- throw new Error('show-step-json requires step.intent');
291
- }
292
- if (step.id !== undefined && typeof step.id !== 'string') {
293
- throw new Error('show-step-json step.id must be a string when provided');
294
- }
295
- return step;
296
- }
297
-
298
- const COMMANDS = {
299
- async 'target-identity-selected'() {
300
- throw new Error('target-identity-selected is resolved before CDP attachment');
301
- },
302
- async navigate(client, args, { deviceName, platform } = {}) {
303
- const routeName = ROUTE_ALIASES[args[0]] || args[0];
304
- if (!routeName) {
305
- throw new Error('Usage: navigate <RouteName> [params-json]');
306
- }
307
- let params = {};
308
- if (args[1]) {
309
- try {
310
- params = JSON.parse(args[1]);
311
- } catch (e) {
312
- throw new Error(`Invalid params JSON: ${e.message}`);
313
- }
314
- }
315
-
316
- // Capture route before navigation
317
- const previousRoute = await cdpEval(client, 'globalThis.__AGENTIC__?.getRoute()');
318
-
319
- let expr;
320
- const parent = NESTED_ROUTE_PARENTS[routeName];
321
- if (parent) {
322
- // Nested route: navigate('Parent', { screen: 'Child', params: {...} })
323
- const navParams = Object.keys(params).length > 0
324
- ? JSON.stringify({ screen: routeName, params })
325
- : JSON.stringify({ screen: routeName });
326
- expr = `globalThis.__AGENTIC__?.navigate(${JSON.stringify(parent)}, ${navParams})`;
327
- } else {
328
- // Root-level route: navigate('Route', params)
329
- const paramsStr = JSON.stringify(params);
330
- expr = `globalThis.__AGENTIC__?.navigate(${JSON.stringify(routeName)}, ${paramsStr})`;
331
- }
332
-
333
- await cdpEval(client, expr);
334
- // Small delay for navigation to settle, then return current route
335
- await new Promise((r) => setTimeout(r, 500));
336
- const currentRoute = await cdpEval(
337
- client,
338
- 'globalThis.__AGENTIC__?.getRoute()',
339
- );
340
- // Normalize route values: cdpEval returns undefined when the optional-chain
341
- // short-circuits (mid-navigation transient or bridge not yet installed).
342
- // null is valid JSON; undefined produces the literal string "undefined".
343
- return {
344
- navigated: routeName,
345
- params,
346
- previousRoute: previousRoute ?? null,
347
- currentRoute: currentRoute ?? null,
348
- deviceName,
349
- platform,
350
- };
351
- },
352
-
353
- async 'get-route'(client) {
354
- const route = await cdpEval(
355
- client,
356
- 'globalThis.__AGENTIC__?.getRoute()',
357
- );
358
- // cdpEval returns undefined when the optional-chain short-circuits (bridge not
359
- // yet installed, mid-navigation, or route state transiently missing). Return null
360
- // so JSON.stringify produces valid JSON ("null") instead of literal "undefined".
361
- return route ?? null;
362
- },
363
-
364
- async 'get-state'(client, args) {
365
- const path = args[0] || '';
366
- if (!path) {
367
- // Return navigation state
368
- const state = await cdpEval(
369
- client,
370
- 'globalThis?.__AGENTIC__?.getState()',
371
- );
372
- return state;
373
- }
374
- // Access Redux store at the given dot-path
375
- const expr = `(() => {
376
- const store = globalThis?.store;
377
- if (!store) return { error: 'Redux store not found on globalThis.store' };
378
- const state = store.getState();
379
- const parts = ${JSON.stringify(path)}.split('.');
380
- let current = state;
381
- for (const p of parts) {
382
- if (current == null) return undefined;
383
- current = current[p];
384
- }
385
- return current;
386
- })()`;
387
- return await cdpEval(client, expr);
388
- },
389
-
390
- async eval(client, args) {
391
- const expression = args.join(' ');
392
- if (!expression) {
393
- throw new Error('Usage: eval <expression>');
394
- }
395
- return await cdpEval(client, expression);
396
- },
397
-
398
- async 'eval-async'(client, args) {
399
- const expression = args.join(' ');
400
- if (!expression) {
401
- throw new Error('Usage: eval-async <expression>');
402
- }
403
- return await cdpEvalAsync(client, expression);
404
- },
405
-
406
- async 'can-go-back'(client) {
407
- return await cdpEval(client, 'globalThis.__AGENTIC__?.canGoBack()');
408
- },
409
-
410
- async 'go-back'(client, _args, { deviceName, platform } = {}) {
411
- await cdpEval(client, 'globalThis.__AGENTIC__?.goBack()');
412
- await new Promise((r) => setTimeout(r, 300));
413
- const route = await cdpEval(
414
- client,
415
- 'globalThis.__AGENTIC__?.getRoute()',
416
- );
417
- // Same normalization as navigate/get-route: a transiently-missing route must
418
- // serialize as "currentRoute": null, not have the key silently omitted.
419
- return { currentRoute: route ?? null, deviceName, platform };
420
- },
81
+ })()`;
421
82
 
83
+ const METAMASK_COMMANDS = {
422
84
  async status(
423
85
  client,
424
86
  _args,
@@ -479,602 +141,6 @@ const COMMANDS = {
479
141
  return await cdpEval(client, `globalThis.__AGENTIC__?.switchAccount(${JSON.stringify(address)})`);
480
142
  },
481
143
 
482
- async 'press-test-id'(client, args, { deviceName, platform } = {}) {
483
- const testId = args[0];
484
- if (!testId) {
485
- throw new Error('Usage: press-test-id <testId>');
486
- }
487
- // Try the app bridge first. Native accessibility remains an iOS fallback
488
- // for controls whose React handler cannot be invoked directly.
489
- const expr = `(function() {
490
- if (globalThis.__AGENTIC__?.pressTestId) return Promise.resolve(globalThis.__AGENTIC__.pressTestId(${JSON.stringify(testId)}));
491
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
492
- if (!hook) return { ok: false, error: 'No React DevTools hook' };
493
- var renderers = hook.renderers;
494
- if (!renderers) return { ok: false, error: 'No renderers' };
495
- var getFiberRoots = hook.getFiberRoots;
496
- function walk(fiber) {
497
- if (!fiber) return false;
498
- var props = fiber.memoizedProps;
499
- if (props && props.testID === ${JSON.stringify(testId)}) {
500
- if (typeof props.onPress === 'function') { props.onPress(); return true; }
501
- }
502
- return walk(fiber.child) || walk(fiber.sibling);
503
- }
504
- for (var [id] of renderers) {
505
- var roots = getFiberRoots ? getFiberRoots(id) : undefined;
506
- if (!roots) continue;
507
- var found = false;
508
- roots.forEach(function(r) { if (!found) found = walk(r.current); });
509
- if (found) return { ok: true, testId: ${JSON.stringify(testId)} };
510
- }
511
- return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onPress' };
512
- })()`;
513
- let result;
514
- let bridgeError;
515
- try {
516
- result = await cdpEvalAsync(client, expr);
517
- if (result?.ok !== false) {
518
- return { ...result, testId, deviceName };
519
- }
520
- } catch (error) {
521
- bridgeError = error;
522
- }
523
- const nativeResult = tapVisibleIosAccessibilityTarget(
524
- testId,
525
- deviceName,
526
- platform,
527
- );
528
- if (nativeResult) return nativeResult;
529
- if (bridgeError) throw bridgeError;
530
- return { ...result, testId, deviceName };
531
- },
532
-
533
- async 'key-press'(_client, args, { deviceName, platform } = {}) {
534
- const requestedKey = String(args[0] || 'Enter').toLowerCase();
535
- const isEnter = requestedKey === 'enter' || requestedKey === 'return';
536
- const isBack = requestedKey === 'escape' || requestedKey === 'back';
537
- if (!isEnter && !isBack) {
538
- throw new Error(`Unsupported Mobile key ${JSON.stringify(args[0])}`);
539
- }
540
- if (platform === 'android') {
541
- const serial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL;
542
- if (!serial) throw new Error('key-press requires a selected Android device');
543
- const keyCode = isEnter ? '66' : '4';
544
- execFileSync(
545
- mobileToolPath('adb'),
546
- ['-s', serial, 'shell', 'input', 'keyevent', keyCode],
547
- { encoding: 'utf8' },
548
- );
549
- return {
550
- ok: true,
551
- key: args[0] || 'Enter',
552
- keyCode,
553
- deviceName,
554
- provider: 'adb-key',
555
- };
556
- }
557
- if (platform !== 'ios') {
558
- throw new Error(`key-press requires a selected Mobile platform`);
559
- }
560
- const device = bootedIosDevice(deviceName);
561
- if (!device) {
562
- throw new Error(`No booted simulator named ${JSON.stringify(deviceName)}`);
563
- }
564
- const keyCode = isEnter ? '40' : '41';
565
- execFileSync(
566
- mobileToolPath('idb'),
567
- ['ui', 'key', keyCode, '--udid', device.udid],
568
- { encoding: 'utf8' },
569
- );
570
- return {
571
- ok: true,
572
- key: args[0] || 'Enter',
573
- keyCode,
574
- deviceName,
575
- provider: 'idb-key',
576
- };
577
- },
578
-
579
- async 'long-press-test-id'(client, args, { deviceName } = {}) {
580
- const testId = args[0];
581
- if (!testId) {
582
- throw new Error('Usage: long-press-test-id <testId>');
583
- }
584
- const expr = `(function() {
585
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
586
- if (!hook) return { ok: false, error: 'No React DevTools hook' };
587
- var renderers = hook.renderers;
588
- if (!renderers) return { ok: false, error: 'No renderers' };
589
- var getFiberRoots = hook.getFiberRoots;
590
- function walk(fiber) {
591
- if (!fiber) return false;
592
- var props = fiber.memoizedProps;
593
- if (props && props.testID === ${JSON.stringify(testId)}) {
594
- if (typeof props.onLongPress === 'function') { props.onLongPress(); return true; }
595
- }
596
- return walk(fiber.child) || walk(fiber.sibling);
597
- }
598
- for (var [id] of renderers) {
599
- var roots = getFiberRoots ? getFiberRoots(id) : undefined;
600
- if (!roots) continue;
601
- var found = false;
602
- roots.forEach(function(r) { if (!found) found = walk(r.current); });
603
- if (found) return { ok: true, testId: ${JSON.stringify(testId)} };
604
- }
605
- return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onLongPress' };
606
- })()`;
607
- const result = await cdpEval(client, expr);
608
- return { ...result, testId, deviceName };
609
- },
610
-
611
- async 'press-text'(client, args, { deviceName } = {}) {
612
- const text = args[0];
613
- if (!text) {
614
- throw new Error('Usage: press-text <text>');
615
- }
616
- const expr = `(function() {
617
- if (typeof globalThis.__AGENTIC__?.pressText !== 'function') {
618
- return { ok: false, error: 'The app bridge does not expose pressText' };
619
- }
620
- return globalThis.__AGENTIC__.pressText(${JSON.stringify(text)});
621
- })()`;
622
- const result = await cdpEval(client, expr);
623
- return { ...result, text, deviceName };
624
- },
625
-
626
- async 'scroll-view'(client, args, { deviceName } = {}) {
627
- let testId;
628
- let offset = 300;
629
- let animated = false;
630
- let intoView = false;
631
- for (let i = 0; i < args.length; i++) {
632
- if (args[i] === '--test-id' && i + 1 < args.length) {
633
- testId = args[++i];
634
- } else if (args[i] === '--offset' && i + 1 < args.length) {
635
- offset = Number(args[++i]);
636
- } else if (args[i] === '--animated') {
637
- animated = true;
638
- } else if (args[i] === '--into-view') {
639
- intoView = true;
640
- } else if (args[i] === '--no-animated') {
641
- animated = false;
642
- }
643
- }
644
- const optsJson = JSON.stringify({ testId, offset, animated, intoView });
645
- // Try __AGENTIC__ bridge first, fall back to inline fiber walking
646
- const expr = `(function() {
647
- // The app helper implements generic scrolling by searching inside the
648
- // selector fiber. For into-view requests the scroll container is commonly
649
- // an ancestor (for example, a Wallet Home section row), so use the
650
- // ancestor-aware bridge fallback below instead.
651
- if (${intoView} && globalThis.__AGENTIC__?.scrollIntoView) {
652
- return globalThis.__AGENTIC__.scrollIntoView(${JSON.stringify(testId)}, ${animated});
653
- }
654
- if (!${intoView} && globalThis.__AGENTIC__?.scrollView) return globalThis.__AGENTIC__.scrollView(${optsJson});
655
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
656
- if (!hook) return { ok: false, error: 'No React DevTools hook' };
657
- var renderers = hook.renderers;
658
- if (!renderers) return { ok: false, error: 'No renderers' };
659
- var getFiberRoots = hook.getFiberRoots;
660
- var opts = ${optsJson};
661
- function tryScroll(fiber, walkSiblings) {
662
- if (walkSiblings === undefined) walkSiblings = true;
663
- var current = fiber;
664
- while (current) {
665
- var rawStateNode = current.stateNode;
666
- var sn = rawStateNode && rawStateNode.canonical && rawStateNode.canonical.publicInstance
667
- ? rawStateNode.canonical.publicInstance
668
- : rawStateNode;
669
- if (sn) {
670
- if (typeof sn.scrollTo === 'function') { sn.scrollTo({ y: opts.offset, animated: opts.animated }); return true; }
671
- if (typeof sn.scrollToOffset === 'function') { sn.scrollToOffset({ offset: opts.offset, animated: opts.animated }); return true; }
672
- }
673
- if (tryScroll(current.child)) return true;
674
- current = walkSiblings ? current.sibling : null;
675
- }
676
- return false;
677
- }
678
- function findMeasurable(fiber) {
679
- if (!fiber) return null;
680
- var rawStateNode = fiber.stateNode;
681
- var sn = rawStateNode && rawStateNode.canonical && rawStateNode.canonical.publicInstance
682
- ? rawStateNode.canonical.publicInstance
683
- : rawStateNode;
684
- if (sn && typeof sn.measureLayout === 'function') return sn;
685
- return findMeasurable(fiber.child);
686
- }
687
- function tryScrollNear(anchor) {
688
- if (!opts.intoView && tryScroll(anchor, false)) return true;
689
- var target = opts.intoView ? findMeasurable(anchor) : null;
690
- var current = anchor ? anchor.return : null;
691
- while (current) {
692
- var rawStateNode = current.stateNode;
693
- var sn = rawStateNode && rawStateNode.canonical && rawStateNode.canonical.publicInstance
694
- ? rawStateNode.canonical.publicInstance
695
- : rawStateNode;
696
- var currentProps = current.memoizedProps;
697
- if (sn) {
698
- if (typeof sn.scrollTo === 'function' && !(currentProps && currentProps.horizontal === true)) {
699
- if (target) {
700
- return new Promise(function(resolve) {
701
- target.measureLayout(
702
- sn,
703
- function(x, y) {
704
- sn.scrollTo({ y: y, animated: opts.animated });
705
- resolve({ ok: true, testId: opts.testId, measuredOffset: y, animated: opts.animated });
706
- },
707
- function() {
708
- resolve({ ok: false, error: 'Unable to measure testID=' + opts.testId + ' relative to its scroll container' });
709
- }
710
- );
711
- });
712
- }
713
- sn.scrollTo({ y: opts.offset, animated: opts.animated });
714
- return true;
715
- }
716
- if (typeof sn.scrollToOffset === 'function' && !(currentProps && currentProps.horizontal === true)) {
717
- if (target) {
718
- return new Promise(function(resolve) {
719
- target.measureLayout(
720
- sn,
721
- function(x, y) {
722
- sn.scrollToOffset({ offset: y, animated: opts.animated });
723
- resolve({ ok: true, testId: opts.testId, measuredOffset: y, animated: opts.animated });
724
- },
725
- function() {
726
- resolve({ ok: false, error: 'Unable to measure testID=' + opts.testId + ' relative to its scroll container' });
727
- }
728
- );
729
- });
730
- }
731
- sn.scrollToOffset({ offset: opts.offset, animated: opts.animated });
732
- return true;
733
- }
734
- }
735
- current = current.return;
736
- }
737
- return opts.intoView ? tryScroll(anchor, false) : false;
738
- }
739
- function findTestId(fiber) {
740
- if (!fiber) return null;
741
- var props = fiber.memoizedProps;
742
- if (props && props.testID === opts.testId) {
743
- var ancestor = fiber;
744
- var inactive = false;
745
- while (ancestor) {
746
- var ancestorProps = ancestor.memoizedProps;
747
- var ancestorStyle = ancestorProps && ancestorProps.style;
748
- if (
749
- (ancestorProps && ancestorProps.activityState === 0) ||
750
- (ancestorStyle && ancestorStyle.display === 'none')
751
- ) {
752
- inactive = true;
753
- break;
754
- }
755
- ancestor = ancestor.return;
756
- }
757
- if (!inactive) return fiber;
758
- }
759
- return findTestId(fiber.child) || findTestId(fiber.sibling);
760
- }
761
- for (var [id] of renderers) {
762
- var roots = getFiberRoots ? getFiberRoots(id) : undefined;
763
- if (!roots) continue;
764
- var scrolled = false;
765
- roots.forEach(function(r) {
766
- if (scrolled) return;
767
- if (opts.testId) {
768
- var anchor = findTestId(r.current);
769
- if (anchor) scrolled = tryScrollNear(anchor);
770
- } else {
771
- scrolled = tryScroll(r.current);
772
- }
773
- });
774
- if (scrolled) {
775
- if (typeof scrolled.then === 'function') return scrolled;
776
- return { ok: true, testId: opts.testId, offset: opts.offset, animated: opts.animated };
777
- }
778
- }
779
- return { ok: false, error: opts.testId ? 'No scrollable near testID=' + opts.testId : 'No scrollable found' };
780
- })()`;
781
- const result = intoView
782
- ? await cdpEvalAsync(client, expr)
783
- : await cdpEval(client, expr);
784
- return { ...result, deviceName };
785
- },
786
-
787
- async 'measure-scroll-transition'(client, args, { deviceName } = {}) {
788
- let options;
789
- try {
790
- options = JSON.parse(args[0] || '{}');
791
- } catch (error) {
792
- throw new Error(`Invalid measure-scroll-transition options: ${error.message}`);
793
- }
794
- const timeoutMs = Number(options.timeoutMs);
795
- const pollIntervalMs = Number(options.pollIntervalMs ?? 100);
796
- const visibleEventStage = options.visibleEventStage || null;
797
- const visibleEventGraceMs = Number(options.visibleEventGraceMs ?? 0);
798
- const targetCount = Number(Boolean(options.targetTestId)) + Number(Boolean(options.targetText));
799
- if (
800
- !options.fromTestId ||
801
- targetCount !== 1 ||
802
- !Number.isFinite(timeoutMs) ||
803
- timeoutMs <= 0 ||
804
- !Number.isFinite(pollIntervalMs) ||
805
- pollIntervalMs < 16 ||
806
- (visibleEventStage !== null && typeof visibleEventStage !== 'string') ||
807
- !Number.isFinite(visibleEventGraceMs) ||
808
- visibleEventGraceMs < 0
809
- ) {
810
- throw new Error(
811
- 'measure-scroll-transition requires fromTestId, exactly one targetTestId or targetText, a positive timeoutMs, pollIntervalMs >= 16, and a non-negative visibleEventGraceMs.',
812
- );
813
- }
814
- const visibleEvents = [];
815
- let removeConsoleListener = null;
816
- if (visibleEventStage) {
817
- removeConsoleListener = client.on('Runtime.consoleAPICalled', (params) => {
818
- const payload = parsePerformanceConsoleEvent(params);
819
- if (payload?.stage === visibleEventStage) visibleEvents.push(payload);
820
- });
821
- await client.send('Runtime.enable');
822
- }
823
- const targetQuery = options.targetTestId
824
- ? { testId: options.targetTestId, visibility: 'viewport' }
825
- : { textContains: options.targetText, visibility: 'viewport' };
826
- const from = await cdpEvalAsync(
827
- client,
828
- `globalThis.__AGENTIC__?.queryUiTarget(${JSON.stringify({
829
- testId: options.fromTestId,
830
- visibility: 'viewport',
831
- })})`,
832
- timeoutMs,
833
- );
834
- if (from?.visible !== true) {
835
- return {
836
- ok: false,
837
- error: 'measurement start target is not visible',
838
- from,
839
- deviceName,
840
- };
841
- }
842
- let walletReady = null;
843
- if (options.requireWalletReady === true) {
844
- walletReady = await cdpEval(
845
- client,
846
- `(function(){
847
- var route = globalThis.__AGENTIC__?.getRoute() || null;
848
- var account = null;
849
- try { account = globalThis.__AGENTIC__?.getSelectedAccount() || null; } catch(e) {}
850
- var address = account && typeof account.address === 'string' ? account.address : '';
851
- var id = account && typeof account.id === 'string' ? account.id : '';
852
- return {
853
- ready: Boolean(address || id) && String(route && route.name || '') !== 'Login',
854
- route: route,
855
- accountPresent: Boolean(address || id),
856
- observedAtMs: performance.now(),
857
- };
858
- })()`,
859
- );
860
- if (walletReady?.ready !== true) {
861
- return {
862
- ok: false,
863
- error: 'wallet is not ready at measurement start',
864
- walletReady,
865
- from,
866
- deviceName,
867
- };
868
- }
869
- }
870
- const started = await cdpEvalAsync(
871
- client,
872
- `(function(){
873
- const scrollStartedAtMs = performance.now();
874
- const startedAtMs = ${
875
- walletReady === null
876
- ? 'scrollStartedAtMs'
877
- : JSON.stringify(Number(walletReady.observedAtMs))
878
- };
879
- return Promise.resolve(globalThis.__AGENTIC__?.scrollView(${JSON.stringify({
880
- testId: options.scrollTestId,
881
- offset: options.offset,
882
- animated: options.animated,
883
- intoView: false,
884
- })})).then(function(scroll){ return { startedAtMs, scrollStartedAtMs, scroll }; });
885
- })()`,
886
- timeoutMs,
887
- );
888
- if (started?.scroll?.ok === false) {
889
- return {
890
- ok: false,
891
- error: 'scroll failed',
892
- ...started,
893
- from,
894
- deviceName,
895
- };
896
- }
897
- const hostDeadline = Date.now() + timeoutMs;
898
- if (visibleEventStage && visibleEventGraceMs > 0) {
899
- const eventDeadline = Math.min(hostDeadline, Date.now() + visibleEventGraceMs);
900
- while (Date.now() <= eventDeadline) {
901
- const event = visibleEvents.find(
902
- (candidate) =>
903
- Number(candidate.frame_checkpoint_monotonic_ms) >=
904
- Number(started.scrollStartedAtMs),
905
- );
906
- if (event) {
907
- removeConsoleListener?.();
908
- const observedAtMs = Number(event.frame_checkpoint_monotonic_ms);
909
- return {
910
- ok: true,
911
- clock: 'performance.now',
912
- observationSource: 'runtime-console-event',
913
- durationMs: observedAtMs - started.startedAtMs,
914
- scrollDurationMs: observedAtMs - started.scrollStartedAtMs,
915
- appVisibleDurationMs: Number(event.duration_ms),
916
- pollIntervalMs,
917
- preScrollDelayMs: started.scrollStartedAtMs - started.startedAtMs,
918
- startedAtMs: started.startedAtMs,
919
- scrollStartedAtMs: started.scrollStartedAtMs,
920
- observedAtMs,
921
- visibleEvent: event,
922
- ...(walletReady ? { walletReady } : {}),
923
- from,
924
- scroll: started.scroll,
925
- deviceName,
926
- };
927
- }
928
- await new Promise((resolve) => setTimeout(resolve, 25));
929
- }
930
- }
931
- while (Date.now() <= hostDeadline) {
932
- const remainingMs = hostDeadline - Date.now();
933
- if (remainingMs <= 1_000) break;
934
- const queryTimeoutMs = Math.min(5_000, remainingMs);
935
- const observation = await cdpEvalAsync(
936
- client,
937
- `(function(){
938
- return Promise.resolve(globalThis.__AGENTIC__?.queryUiTarget(${JSON.stringify(targetQuery)})).then(function(target){
939
- return { target, observedAtMs: performance.now() };
940
- });
941
- })()`,
942
- queryTimeoutMs,
943
- );
944
- const requiredPresent =
945
- observation?.target?.visible === true && options.requiredPresentTestId
946
- ? await cdpEvalAsync(
947
- client,
948
- `globalThis.__AGENTIC__?.queryUiTarget(${JSON.stringify({
949
- testId: options.requiredPresentTestId,
950
- visibility: 'tree',
951
- })})`,
952
- queryTimeoutMs,
953
- )
954
- : null;
955
- if (
956
- observation?.target?.visible === true &&
957
- (!options.requiredPresentTestId || requiredPresent?.present === true)
958
- ) {
959
- return {
960
- ok: true,
961
- clock: 'performance.now',
962
- durationMs: observation.observedAtMs - started.startedAtMs,
963
- scrollDurationMs:
964
- observation.observedAtMs - started.scrollStartedAtMs,
965
- pollIntervalMs,
966
- observationSource: visibleEventStage
967
- ? 'fiber-poll-after-event-grace'
968
- : 'fiber-poll',
969
- preScrollDelayMs:
970
- started.scrollStartedAtMs - started.startedAtMs,
971
- startedAtMs: started.startedAtMs,
972
- scrollStartedAtMs: started.scrollStartedAtMs,
973
- observedAtMs: observation.observedAtMs,
974
- ...(walletReady ? { walletReady } : {}),
975
- from,
976
- target: observation.target,
977
- ...(requiredPresent ? { requiredPresent } : {}),
978
- scroll: started.scroll,
979
- deviceName,
980
- };
981
- }
982
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
983
- }
984
- removeConsoleListener?.();
985
- return {
986
- ok: false,
987
- error: 'timed out waiting for visible transition target',
988
- durationMs: await cdpEval(client, `performance.now() - ${Number(started.startedAtMs)}`),
989
- startedAtMs: started.startedAtMs,
990
- scrollStartedAtMs: started.scrollStartedAtMs,
991
- deviceName,
992
- };
993
- },
994
-
995
- async 'set-input'(client, args, context = {}) {
996
- return setInput(client, args[0], args.slice(1).join(' '), context);
997
- },
998
-
999
- async 'set-input-file'(client, args, context = {}) {
1000
- const testId = args[0];
1001
- const file = args[1];
1002
- if (!testId || !file) {
1003
- throw new Error('Usage: set-input-file <testId> <value-file>');
1004
- }
1005
- const stat = fs.lstatSync(file);
1006
- if (!stat.isFile() || stat.isSymbolicLink()) {
1007
- throw new Error('set-input-file requires a regular value file.');
1008
- }
1009
- return setInput(client, testId, fs.readFileSync(file, 'utf8'), context, true);
1010
- },
1011
-
1012
- async 'sentry-debug'(client, args, { deviceName } = {}) {
1013
- const action = args[0] || 'enable';
1014
- if (action === 'enable') {
1015
- const expr = `(function() {
1016
- try {
1017
- var sentryHub = globalThis.__SENTRY__;
1018
- if (!sentryHub) return { ok: false, error: 'globalThis.__SENTRY__ not found' };
1019
- var ver = Object.keys(sentryHub).filter(function(k) { return /^[0-9]/.test(k); })[0];
1020
- if (!ver) return { ok: false, error: 'No Sentry version key found in __SENTRY__' };
1021
- var hub = sentryHub[ver];
1022
- var scope = hub.defaultCurrentScope;
1023
- if (!scope) return { ok: false, error: 'No defaultCurrentScope in Sentry hub' };
1024
- var sentryClient = scope.getClient();
1025
- if (!sentryClient) return { ok: false, error: 'No Sentry client found via scope.getClient()' };
1026
-
1027
- if (sentryClient.__agenticPatched) return { ok: true, alreadyPatched: true, version: ver };
1028
-
1029
- var origException = sentryClient.captureException.bind(sentryClient);
1030
- var origMessage = sentryClient.captureMessage.bind(sentryClient);
1031
-
1032
- sentryClient.captureException = function(err, hint, currentScope) {
1033
- var msg = err && err.message ? err.message : String(err);
1034
- console.warn('[SENTRY-DEBUG] captureException:', msg);
1035
- if (err && err.stack) console.warn('[SENTRY-DEBUG] stack:', err.stack);
1036
- return origException(err, hint, currentScope);
1037
- };
1038
- sentryClient.captureMessage = function(msg, level, hint, currentScope) {
1039
- console.warn('[SENTRY-DEBUG] captureMessage:', msg);
1040
- return origMessage(msg, level, hint, currentScope);
1041
- };
1042
- sentryClient.__agenticPatched = true;
1043
- sentryClient.__agenticOrigException = origException;
1044
- sentryClient.__agenticOrigMessage = origMessage;
1045
- return { ok: true, patched: true, version: ver };
1046
- } catch(e) {
1047
- return { ok: false, error: String(e) };
1048
- }
1049
- })()`;
1050
- return await cdpEval(client, expr);
1051
- } else if (action === 'disable') {
1052
- const expr = `(function() {
1053
- try {
1054
- var sentryHub = globalThis.__SENTRY__;
1055
- if (!sentryHub) return { ok: true, wasNotPatched: true };
1056
- var ver = Object.keys(sentryHub).filter(function(k) { return /^[0-9]/.test(k); })[0];
1057
- if (!ver) return { ok: true, wasNotPatched: true };
1058
- var hub = sentryHub[ver];
1059
- var scope = hub.defaultCurrentScope;
1060
- if (!scope) return { ok: true, wasNotPatched: true };
1061
- var sentryClient = scope.getClient();
1062
- if (!sentryClient || !sentryClient.__agenticPatched) return { ok: true, wasNotPatched: true };
1063
- sentryClient.captureException = sentryClient.__agenticOrigException;
1064
- sentryClient.captureMessage = sentryClient.__agenticOrigMessage;
1065
- delete sentryClient.__agenticPatched;
1066
- delete sentryClient.__agenticOrigException;
1067
- delete sentryClient.__agenticOrigMessage;
1068
- return { ok: true, unpatched: true, version: ver };
1069
- } catch(e) {
1070
- return { ok: false, error: String(e) };
1071
- }
1072
- })()`;
1073
- return await cdpEval(client, expr);
1074
- }
1075
- throw new Error('Usage: sentry-debug [enable|disable]');
1076
- },
1077
-
1078
144
  async unlock(client, args, { deviceName } = {}) {
1079
145
  const password = args[0];
1080
146
  if (!password) {
@@ -1156,408 +222,41 @@ const COMMANDS = {
1156
222
  deviceName,
1157
223
  };
1158
224
  },
1159
-
1160
- async 'show-step-json'(client, args) {
1161
- const step = parseHudStep(args);
1162
- await applyHudUpdate(client, step);
1163
- return { ok: true };
1164
- },
1165
-
1166
- async 'show-step-json-deferred'(client, args) {
1167
- const step = parseHudStep(args);
1168
- if (typeof client.control === 'function') {
1169
- return client.control('hud-update', { step });
1170
- }
1171
- await applyHudUpdate(client, step);
1172
- return { ok: true, status: 'applied' };
1173
- },
1174
-
1175
- async 'hide-step'(client) {
1176
- await applyHudUpdate(client, null);
1177
- return { ok: true };
1178
- },
1179
-
1180
- async 'hide-step-deferred'(client) {
1181
- if (typeof client.control === 'function') {
1182
- return client.control('hud-update', { step: null });
1183
- }
1184
- await applyHudUpdate(client, null);
1185
- return { ok: true, status: 'applied' };
1186
- },
1187
-
1188
- async 'profiler-start'(client) {
1189
- // Hermes CDP exposes the sampling profiler via the Profiler domain.
1190
- // Output of Profiler.stop is a Chrome-compatible .cpuprofile object.
1191
- // Note: Hermes does NOT implement Profiler.enable (returns -32601 Unsupported);
1192
- // Profiler.start/stop work without it.
1193
- await client.send('Profiler.start');
1194
- return { ok: true, started: true };
1195
- },
1196
-
1197
- async 'profiler-stop'(client, args) {
1198
- let outPath = '';
1199
- let label = 'trace';
1200
- for (let i = 0; i < args.length; i++) {
1201
- if (args[i] === '--out' && i + 1 < args.length) {
1202
- outPath = args[++i];
1203
- } else if (args[i] === '--label' && i + 1 < args.length) {
1204
- label = args[++i];
1205
- }
1206
- }
1207
- const res = await client.send('Profiler.stop', {}, 60000);
1208
- const profile = res?.profile;
1209
- if (!profile) {
1210
- return { ok: false, error: 'Profiler.stop returned no profile' };
1211
- }
1212
- const serialized = JSON.stringify(profile);
1213
- if (!outPath) {
1214
- const tracesDir = path.resolve(process.env.APP_ROOT || process.cwd(), 'temp/agentic/traces');
1215
- fs.mkdirSync(tracesDir, { recursive: true });
1216
- outPath = path.join(tracesDir, `trace-${label}.cpuprofile`);
1217
- } else {
1218
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
1219
- }
1220
- fs.writeFileSync(outPath, serialized);
1221
- const nodesCount = Array.isArray(profile.nodes) ? profile.nodes.length : 0;
1222
- const samplesCount = Array.isArray(profile.samples) ? profile.samples.length : 0;
1223
- return {
1224
- ok: true,
1225
- path: outPath,
1226
- label,
1227
- sizeBytes: Buffer.byteLength(serialized, 'utf8'),
1228
- nodesCount,
1229
- samplesCount,
1230
- startTime: profile.startTime ?? null,
1231
- endTime: profile.endTime ?? null,
1232
- };
1233
- },
1234
-
1235
- async 'issues-arm'(client) {
1236
- // Installs console.warn/error and global error/unhandledrejection hooks
1237
- // that push into globalThis.__AGENTIC_ISSUES__ (capped at 500 entries).
1238
- // Idempotent — subsequent calls return { installed: true, reason: 'already-installed' }.
1239
- const result = await cdpEval(client, buildArmSnippet());
1240
- return result || { installed: false };
1241
- },
1242
-
1243
- async 'issues-collect'(client) {
1244
- // Snapshots and clears globalThis.__AGENTIC_ISSUES__.
1245
- // Returns { count, entries } where entries are { t, level, text }.
1246
- const result = await cdpEval(client, buildCollectSnippet());
1247
- return result || { count: 0, entries: [] };
1248
- },
1249
-
1250
- async 'network-capture-start'(client, args) {
1251
- if (typeof client.control !== 'function') {
1252
- throw new Error('Network capture requires the CDP broker');
1253
- }
1254
- const options = JSON.parse(args[0] || '{}');
1255
- return client.control('capture-start', options);
1256
- },
1257
-
1258
- async 'network-capture-end'(client, args) {
1259
- if (typeof client.control !== 'function') {
1260
- throw new Error('Network capture requires the CDP broker');
1261
- }
1262
- const options = JSON.parse(args[0] || '{}');
1263
- return client.control('capture-end', options);
1264
- },
1265
-
1266
225
  };
1267
226
 
1268
- // ---------------------------------------------------------------------------
1269
- // Main
1270
- // ---------------------------------------------------------------------------
1271
-
1272
- // Bridge-priority lock: the Hermes inspector proxy reliably serves one debugger
1273
- // slot, so the console-forwarder yields it while a bridge command runs (it
1274
- // watches this file and backfills from the runtime's console buffer afterward).
1275
- const BRIDGE_LOCK_FILE = path.join(
1276
- process.env.RECIPE_RUNTIME_DIR || path.join('temp', 'recipe', 'runtime'),
1277
- 'cdp-bridge.lock',
1278
- );
1279
- const BRIDGE_LOCK_OWNER_PID = /^\d+$/.test(
1280
- process.env.CDP_BRIDGE_LOCK_OWNER_PID || '',
1281
- )
1282
- ? process.env.CDP_BRIDGE_LOCK_OWNER_PID
1283
- : String(process.pid);
1284
-
1285
- // Ownership semantics: last writer wins. Each bridge stamps its own pid; the
1286
- // forwarder honors the lock while the pid IN THE FILE is alive. Overlapping
1287
- // bridge commands therefore keep the slot guarded as long as the most recent
1288
- // writer runs, and an earlier command that outlives it may lose the slot — one
1289
- // bridge command at a time is the supported concurrency.
1290
- let bridgeLockHeld = false;
1291
-
1292
- function acquireBridgeLock() {
1293
- try {
1294
- fs.writeFileSync(BRIDGE_LOCK_FILE, BRIDGE_LOCK_OWNER_PID);
1295
- bridgeLockHeld = true;
1296
- } catch {
1297
- // Lock is best-effort: a missing runtime dir must not break bridge commands.
1298
- }
1299
- }
1300
-
1301
- process.on('exit', () => {
1302
- // Only the current owner removes the lock: a no-lock invocation (--help,
1303
- // unknown command) or a bridge whose lock was overwritten by a later one
1304
- // must not release the slot under the still-running writer.
1305
- if (!bridgeLockHeld) return;
1306
- try {
1307
- if (fs.readFileSync(BRIDGE_LOCK_FILE, 'utf8').trim() === String(process.pid)) {
1308
- fs.unlinkSync(BRIDGE_LOCK_FILE);
1309
- }
1310
- } catch {
1311
- // Already gone.
1312
- }
1313
- });
1314
-
1315
- // Signal-killed bridges (status-probe kills timed-out probes SIGTERM→SIGKILL)
1316
- // skip 'exit' unless a handler turns the signal into process.exit — without
1317
- // this the lock strands and mutes console forwarding until staleness lapses.
1318
- process.on('SIGTERM', () => process.exit(143));
1319
- process.on('SIGINT', () => process.exit(130));
1320
-
1321
- async function main() {
1322
- const args = process.argv.slice(2);
1323
- const command = args[0];
1324
-
1325
- if (!command || command === '--help' || command === '-h') {
1326
- console.log(`CDP Bridge — interact with the running MetaMask app via Hermes CDP
1327
-
1328
- Usage:
1329
- node adapters/mobile/bridge-runtime/cdp-bridge.cjs <command> [args...]
1330
-
1331
- Commands:
1332
- navigate <RouteName> [params-json] Navigate to a screen
1333
- status Route + selected account snapshot
1334
- get-route Get current route name and params
1335
- get-state [dot.path] Get Redux state (or nav state if no path)
1336
- eval <expression> Evaluate arbitrary JS in app context
1337
- eval-async <expression> Evaluate async/Promise expression
1338
- can-go-back Check if navigation can go back
1339
- go-back Navigate back
1340
- list-accounts List all accounts
1341
- get-selected-account Get the currently selected account
1342
- switch-account <address> Switch to account by address
1343
- press-test-id <testId> Press a component by its testID prop
1344
- long-press-test-id <testId> Invoke a component's long-press gesture by testID
1345
- press-text <text> Press a component containing visible text
1346
- scroll-view [--test-id <id>] [--offset <n>] [--animated]
1347
- Scroll a ScrollView/FlatList
1348
- measure-scroll-transition <json> Scroll and measure a visible target using one CDP session
1349
- set-input <testId> <value> Set text input value by testID (calls onChangeText)
1350
- set-input-file <testId> <value-file> Set text input from a regular file and redact the value
1351
- sentry-debug [enable|disable] Patch Sentry to log errors to console with [SENTRY-DEBUG] prefix
1352
- unlock <password> Unlock wallet (inject password + press login button via fiber tree)
1353
- profiler-start Start Hermes sampling profiler
1354
- profiler-stop [--out <path>] [--label <name>]
1355
- Stop profiler, dump Chrome-compatible
1356
- .cpuprofile to <path> (default:
1357
- temp/agentic/traces/trace-<label>.cpuprofile)
1358
- issues-arm Install console/exception hooks that
1359
- populate globalThis.__AGENTIC_ISSUES__
1360
- issues-collect Snapshot + clear the in-app issue buffer
1361
- network-capture-start <json> Start a broker-owned Network capture
1362
- network-capture-end <json> End capture and return redacted summary
1363
-
1364
- Environment:
1365
- WATCHER_PORT Metro port (default: 8081)
1366
- CDP_TIMEOUT Connection timeout in ms (default: 5000)
1367
- IOS_SIMULATOR Filter targets by iOS simulator name
1368
- ANDROID_DEVICE Filter targets by Android device/emulator serial`);
1369
- process.exit(0);
1370
- }
1371
-
1372
- const handler = COMMANDS[command === 'status-selected' ? 'status' : command];
1373
- if (!handler) {
1374
- console.error(`Unknown command: ${command}`);
1375
- console.error(`Available: ${Object.keys(COMMANDS).join(', ')}`);
1376
- process.exit(1);
1377
- }
1378
-
1379
- const port = loadPort();
1380
- const brokerSocketFile = brokerSocketPath(
1381
- path.dirname(BRIDGE_LOCK_FILE),
1382
- port,
1383
- );
1384
- const timeout = Number.parseInt(process.env.CDP_TIMEOUT || '5000', 10);
1385
- const brokerAvailable = fs.existsSync(brokerSocketFile);
1386
- if (!brokerAvailable) acquireBridgeLock();
1387
-
1388
- function selectedTargetPin() {
1389
- const platform = String(process.env.MM_HARNESS_EXPLICIT_PLATFORM || '').toLowerCase();
1390
- const androidPin =
1391
- process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ANDROID_DEVICE || '';
1392
- const iosPin = process.env.IOS_SIMULATOR || '';
1393
- return String(
1394
- platform === 'android'
1395
- ? androidPin
1396
- : platform === 'ios'
1397
- ? iosPin
1398
- : androidPin || iosPin,
1399
- ).trim();
1400
- }
1401
-
1402
- async function brokerTargets({
1403
- retainIdentity = false,
1404
- includeGeneration = false,
1405
- } = {}) {
1406
- const discoveryClient = await createBrokerClient(
1407
- brokerSocketFile,
1408
- '',
1409
- timeout,
1410
- );
1411
- try {
1412
- const targets = await discoveryClient.control(
1413
- retainIdentity
1414
- ? 'resolve-targets'
1415
- : includeGeneration
1416
- ? 'list-target-identities'
1417
- : 'list-targets',
1418
- retainIdentity ? { nameIncludes: selectedTargetPin() } : {},
1419
- timeout,
1420
- );
1421
- return (Array.isArray(targets) ? targets : []).flatMap((target) => {
1422
- const deviceId = String(target?.deviceId || '');
1423
- return deviceId
1424
- ? [{
1425
- wsUrl: deviceId,
1426
- deviceName: String(target?.name || ''),
1427
- generation: Number(target?.generation) || 0,
1428
- }]
1429
- : [];
1430
- });
1431
- } finally {
1432
- discoveryClient.close();
1433
- }
1434
- }
1435
-
1436
- function selectBrokerTarget(targets) {
1437
- const pin = selectedTargetPin();
1438
- const candidates = pin
1439
- ? targets.filter((target) =>
1440
- target.deviceName.toLowerCase().includes(pin.toLowerCase()),
1441
- )
1442
- : targets;
1443
- if (candidates.length !== 1) {
1444
- const error = new Error(
1445
- `Mobile CDP broker target selection requires one target; found ${candidates.length}${pin ? ` for ${JSON.stringify(pin)}` : ''}.`,
1446
- );
1447
- throw candidates.length === 0
1448
- ? coded(error, BRIDGE_ERROR_CODES.NO_TARGET)
1449
- : error;
1450
- }
1451
- return candidates[0];
1452
- }
1453
-
1454
- if (command === 'target-identity-selected') {
1455
- const target = brokerAvailable
1456
- ? selectBrokerTarget(await brokerTargets({ includeGeneration: true }))
1457
- : await discoverTarget(port, { probe: true });
1458
- const generation = Number(target.generation) || 0;
1459
- console.log(
1460
- JSON.stringify({
1461
- runtimeIdentity: generation
1462
- ? `${target.wsUrl}:${generation}`
1463
- : target.wsUrl,
1464
- }),
1465
- );
1466
- return;
1467
- }
1468
-
1469
- async function clientFor(wsUrl) {
1470
- if (brokerAvailable) {
1471
- return createBrokerClient(
1472
- brokerSocketFile,
1473
- deviceIdFromUrl(wsUrl),
1474
- timeout,
1475
- );
1476
- }
1477
- if (command.startsWith('network-capture-')) {
1478
- throw new Error('Network capture requires a running CDP broker');
1479
- }
1480
- acquireBridgeLock();
1481
- return createWSClient(wsUrl, timeout);
1482
- }
1483
-
1484
- // `status` probes ALL connected targets so both platforms are visible.
1485
- // `status-selected` falls through to the normal pinned discovery path for
1486
- // action code that must survive one device's Hermes runtime rotation.
1487
- if (command === 'status') {
1488
- const allTargets = brokerAvailable
1489
- ? await brokerTargets()
1490
- : await require('./lib/target-discovery.cjs').discoverAllTargets(port);
1491
- const results = [];
1492
- for (const target of allTargets) {
1493
- let client;
1494
- try {
1495
- client = await clientFor(target.wsUrl);
1496
- const platform = await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
1497
- const result = await handler(client, args.slice(1), { deviceName: target.deviceName, platform });
1498
- results.push(result);
1499
- } catch {
1500
- // Recovery is correct: status probes every target advisory; an
1501
- // unresponsive one degrades to absent-from-the-listing rather than
1502
- // failing the whole multi-target command.
1503
- } finally {
1504
- if (client) client.close();
1505
- }
1506
- }
1507
- console.log(JSON.stringify(results.length === 1 ? results[0] : results, null, 2));
1508
- return;
1509
- }
1510
-
1511
- const selectedTarget = brokerAvailable
1512
- ? selectBrokerTarget(await brokerTargets({ retainIdentity: true }))
1513
- : await discoverTarget(port, { probe: true });
1514
- const { wsUrl, deviceName, generation } = selectedTarget;
1515
- if (!wsUrl) throw new Error('No broker-owned Hermes target is available');
1516
- const client = await clientFor(wsUrl);
1517
-
1518
- try {
1519
- const platform = command.endsWith('-deferred')
1520
- ? ''
1521
- : await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
1522
- const runtimeIdentity =
1523
- command === 'status-selected'
1524
- ? generation
1525
- ? `${wsUrl}:${generation}`
1526
- : wsUrl
1527
- : undefined;
1528
- const result = await handler(client, args.slice(1), {
1529
- deviceName,
1530
- platform,
1531
- runtimeIdentity,
1532
- });
1533
- if (brokerAvailable && command === 'status-selected') {
1534
- const currentTarget = selectBrokerTarget(
1535
- await brokerTargets({ includeGeneration: true }),
1536
- );
1537
- if (
1538
- currentTarget.wsUrl !== wsUrl ||
1539
- currentTarget.generation !== generation
1540
- ) {
1541
- throw new Error('Hermes runtime rotated during status-selected');
1542
- }
1543
- }
1544
- console.log(JSON.stringify(result, null, 2));
1545
- } finally {
1546
- client.close();
1547
- }
1548
- }
1549
-
1550
- main().catch((err) => {
1551
- // Typed failure: a code stamped at the throw site wins; otherwise classify the
1552
- // message here (still at the bridge, not by a needle far away in adapters.ts).
1553
- // The marker + teaching go to stderr and the exit code carries the class, so a
1554
- // caller recovers the code from either channel.
1555
- const code = err && err.code ? err.code : classifyBridgeErrorMessage(err && err.message);
1556
- if (code) {
1557
- console.error(formatErrorMarker(code, err.message));
1558
- if (TEACHING_BY_ERROR_CODE[code]) console.error(TEACHING_BY_ERROR_CODE[code]);
1559
- process.exit(EXIT_CODE_BY_ERROR_CODE[code] || 1);
1560
- }
1561
- console.error(`ERROR: ${err.message}`);
1562
- process.exit(1);
227
+ runBridgeCli({
228
+ appLabel: 'MetaMask',
229
+ usagePath: 'adapters/mobile/bridge-runtime/cdp-bridge.cjs',
230
+ commands: METAMASK_COMMANDS,
231
+ commandDocs: {
232
+ status: {
233
+ help: ' status Route + selected account snapshot',
234
+ helpAfter: 'navigate',
235
+ listAfter: 'go-back',
236
+ },
237
+ 'list-accounts': {
238
+ help: ' list-accounts List all accounts',
239
+ helpAfter: 'go-back',
240
+ listAfter: 'status',
241
+ },
242
+ 'get-selected-account': {
243
+ help: ' get-selected-account Get the currently selected account',
244
+ helpAfter: 'list-accounts',
245
+ listAfter: 'list-accounts',
246
+ },
247
+ 'switch-account': {
248
+ help: ' switch-account <address> Switch to account by address',
249
+ helpAfter: 'get-selected-account',
250
+ listAfter: 'get-selected-account',
251
+ },
252
+ unlock: {
253
+ help: ' unlock <password> Unlock wallet (inject password + press login button via fiber tree)',
254
+ helpAfter: 'sentry-debug',
255
+ listAfter: 'sentry-debug',
256
+ },
257
+ },
258
+ routes: { aliases: ROUTE_ALIASES, nestedParents: NESTED_ROUTE_PARENTS },
259
+ teachingByErrorCode: TEACHING_BY_ERROR_CODE,
260
+ perfMarkerPrefixes: ['[PerpsPerf] ', '[HomepagePerf] '],
261
+ readyExpression: WALLET_READY_EXPRESSION,
1563
262
  });