@deeeed/metamask-harness 0.79.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.
- package/CHANGELOG.md +47 -0
- package/README.md +1 -1
- package/adapters/extension/ensure-browser.sh +2 -2
- package/adapters/extension/inject.mjs +0 -1
- package/adapters/extension/launch-browser.cjs +39 -613
- package/adapters/extension/readiness.mjs +2 -5
- package/adapters/extension/reattach.sh +2 -2
- package/adapters/extension/stamp-runtime-title.cjs +1 -1
- package/adapters/manifest.json +2 -34
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
- package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
- package/adapters/mobile/inject.sh +16 -0
- package/adapters/mobile/launch-console-forwarder.cjs +1 -2
- package/adapters/mobile/open-device.sh +2 -2
- package/adapters/mobile/reload-app.mjs +4 -4
- package/adapters/mobile/reset-app-data.sh +1 -1
- package/adapters/mobile/start-console-forwarder.sh +3 -2
- package/adapters/mobile/start-metro.sh +17 -12
- package/adapters/mobile/wait-for-bridge.cjs +2 -2
- package/adapters/shared/harness-source-fingerprint.mjs +13 -8
- package/adapters/shared/open-debug.mjs +2 -2
- package/adapters/shared/sync-wallet-fixture.sh +8 -5
- package/adapters/terminal/cleanup.mjs +2 -1
- package/adapters/terminal/inject.mjs +2 -1
- package/bin/mm-harness +9 -28
- package/dist/adapters/checkout-files.js +16 -0
- package/dist/adapters/console-capture.js +14 -1
- package/dist/adapters/core/actions.js +4 -3
- package/dist/adapters/core/overlay.js +1 -1
- package/dist/adapters/core/readiness.js +28 -1
- package/dist/adapters/core/run.js +2 -2
- package/dist/adapters/core/surface.js +16 -1
- package/dist/adapters/extension/browser-pid.js +56 -0
- package/dist/adapters/extension/console-capture.js +1 -1
- package/dist/adapters/extension/doctor-checks.js +1 -1
- package/dist/adapters/extension/ensure-ready.js +5 -4
- package/dist/adapters/extension/extension-id.js +1 -1
- package/dist/adapters/extension/feature-flags.js +1 -1
- package/dist/adapters/extension/fixtures.js +1 -2
- package/dist/adapters/extension/launch.js +4 -5
- package/dist/adapters/extension/launched-browser.js +1 -2
- package/dist/adapters/extension/network-observer.js +1 -1
- package/dist/adapters/extension/overlay.js +1 -1
- package/dist/adapters/extension/product-config.js +1 -1
- package/dist/adapters/extension/readiness.js +1 -1
- package/dist/adapters/extension/reload.js +1 -1
- package/dist/adapters/extension/run.js +24 -10
- package/dist/adapters/extension/runtime-decision.js +2 -1
- package/dist/adapters/extension/runtime.js +9 -5
- package/dist/adapters/extension/surface.js +15 -2
- package/dist/adapters/extension/verify.js +2 -2
- package/dist/adapters/harness-freshness.js +2 -1
- package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
- package/dist/adapters/mobile/app-identity.js +10 -0
- package/dist/adapters/mobile/doctor-checks.js +1 -1
- package/dist/adapters/mobile/fixtures.js +1 -1
- package/dist/adapters/mobile/launch.js +27 -28
- package/dist/adapters/mobile/lifecycle.js +1 -1
- package/dist/adapters/mobile/metro-env.js +9 -59
- package/dist/adapters/mobile/performance-observer.js +1 -1
- package/dist/adapters/mobile/prepare-target.js +1 -1
- package/dist/adapters/mobile/prepare.js +3 -4
- package/dist/adapters/mobile/provision.js +1 -1
- package/dist/adapters/mobile/readiness.js +1 -1
- package/dist/adapters/mobile/recording-target.js +15 -0
- package/dist/adapters/mobile/release-artifact-state.js +1 -1
- package/dist/adapters/mobile/reload.js +1 -1
- package/dist/adapters/mobile/run.js +8 -7
- package/dist/adapters/mobile/runtime-decision.js +2 -4
- package/dist/adapters/mobile/source-freshness.js +22 -126
- package/dist/adapters/mobile/surface.js +26 -3
- package/dist/adapters/mobile/ui.js +16 -5
- package/dist/adapters/mobile/verify.js +2 -3
- package/dist/adapters/performance/cdp-trace.js +1 -1
- package/dist/adapters/performance/device-platform.js +1 -1
- package/dist/adapters/slot-ports.js +1 -1
- package/dist/adapters/surface.js +1 -0
- package/dist/adapters/terminal/console-capture.js +1 -1
- package/dist/adapters/terminal/launch.js +1 -1
- package/dist/adapters/terminal/run.js +25 -2
- package/dist/adapters/terminal/surface.js +7 -1
- package/dist/adapters.js +8 -4
- package/dist/advisor-policy.js +1 -1
- package/dist/checkout-view.js +1 -2
- package/dist/cli.js +28 -15
- package/dist/command-contract.js +1 -26
- package/dist/commands/call.js +10 -24
- package/dist/commands/check.js +3 -4
- package/dist/commands/checklist.js +1 -1
- package/dist/commands/completion-candidates.js +2 -10
- package/dist/commands/config.js +2 -2
- package/dist/commands/core-readiness.js +12 -7
- package/dist/commands/debug.js +3 -3
- package/dist/commands/device-target.js +5 -5
- package/dist/commands/doctor.js +2 -14
- package/dist/commands/domain.js +2 -2
- package/dist/commands/ensure-ready.js +1 -7
- package/dist/commands/fixtures.js +5 -6
- package/dist/commands/help.js +3 -4
- package/dist/commands/launch/device.js +10 -17
- package/dist/commands/launch/extension.js +2 -8
- package/dist/commands/launch/mobile.js +1 -1
- package/dist/commands/list-executables.js +1 -3
- package/dist/commands/logs.js +3 -3
- package/dist/commands/manifest.js +1 -12
- package/dist/commands/mobile-device-view.js +4 -2
- package/dist/commands/overlay.js +110 -0
- package/dist/commands/pin.js +1 -1
- package/dist/commands/pr-body.js +2 -2
- package/dist/commands/prepare.js +4 -9
- package/dist/commands/provision.js +3 -12
- package/dist/commands/recipe-advice.js +1 -2
- package/dist/commands/recipe-quality.js +2 -3
- package/dist/commands/resolve-extension.js +1 -7
- package/dist/commands/review.js +4 -4
- package/dist/commands/run-engine.js +12 -51
- package/dist/commands/run.js +4 -25
- package/dist/commands/runtime-decision.js +1 -6
- package/dist/commands/runtime-health.js +1 -7
- package/dist/commands/runtime-launch.js +1 -13
- package/dist/commands/self-test.js +2 -6
- package/dist/commands/status-watch.js +1 -2
- package/dist/commands/status.js +2 -5
- package/dist/commands/task-init.js +2 -3
- package/dist/commands/tutorial.js +2 -2
- package/dist/commands/update.js +1 -1
- package/dist/completions-cache.js +1 -1
- package/dist/console-classifier.js +19 -0
- package/dist/doctor.js +2 -3
- package/dist/funding-execution-context.js +15 -1
- package/dist/host.js +32 -0
- package/dist/index.js +1 -6
- package/dist/live-tsx.js +49 -0
- package/dist/mm-harness-cli.js +2 -4
- package/dist/network-observation.js +4 -4
- package/dist/paths.js +34 -64
- package/dist/performance-observation.js +2 -2
- package/dist/progress.js +1 -1
- package/dist/recipe-assistance-context.js +1 -3
- package/dist/recipe-security.js +2 -59
- package/dist/review/knowledge.js +2 -2
- package/dist/runner.js +5 -3
- package/dist/runtime-context.js +1 -1
- package/dist/task-view.js +2 -3
- package/docs/CONTRIBUTING.md +1 -1
- package/docs/cli-contract.md +2 -2
- package/docs/feature-guard.md +90 -0
- package/library/README.md +3 -1
- package/library/actions/core/perps/_controller.mjs +153 -130
- package/library/actions/core/perps/quote_fees.mjs +58 -0
- package/library/actions/core/wallet/_fixture-accounts.mjs +69 -0
- package/library/actions/core/wallet/list_accounts.mjs +14 -57
- package/library/actions/extension/feature_flags/state.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +2 -8
- package/library/actions/harness-exports.mjs +1 -6
- package/library/actions/mobile/app/network-control.mjs +6 -8
- package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
- package/library/actions/mobile/platform/bridge.mjs +8 -8
- package/library/actions/mobile/platform/native-session-name.mjs +21 -1
- package/library/actions/mobile/platform/native-session.mjs +3 -4
- package/library/actions/mobile/platform/observe-ui.mjs +12 -6
- package/library/actions/mobile/ui/native-navigation.mjs +4 -3
- package/library/actions/mobile/wallet/native-ui.mjs +3 -2
- package/library/actions/shared/analytics/_adapter.mjs +1 -1
- package/library/actions/shared/console-findings.mjs +1 -1
- package/library/actions/terminal/platform/runtime.mjs +2 -1
- package/library/manifests/core.action-manifest.json +83 -13
- package/package.json +8 -9
- package/adapters/extension/lib/slot-title.cjs +0 -292
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
- package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
- package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
- package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
- package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
- package/adapters/mobile/coalesce-metro-log.cjs +0 -24
- package/adapters/mobile/launch-metro.cjs +0 -77
- package/adapters/mobile/metro-config.cjs +0 -98
- package/adapters/mobile/metro-log-generation.cjs +0 -106
- package/dist/adapters/mobile/frame-metrics.js +0 -118
- package/dist/adapters/mobile/video-recorder.js +0 -289
- package/dist/artifact-files.js +0 -92
- package/dist/checkout-lock.js +0 -149
- package/dist/cli-color.js +0 -98
- package/dist/command-journal.js +0 -308
- package/dist/commands/last.js +0 -60
- package/dist/commands/launch/index.js +0 -109
- package/dist/commands/parse-args.js +0 -279
- package/dist/commands/reload.js +0 -41
- package/dist/commands/run-report.js +0 -241
- package/dist/commands/shared.js +0 -323
- package/dist/commands/stop.js +0 -60
- package/dist/devices.js +0 -100
- package/dist/execution-provenance.js +0 -332
- package/dist/harness.js +0 -451
- package/dist/heal-bounds.js +0 -137
- package/dist/json-stream.js +0 -60
- package/dist/leaf-invoke.js +0 -19
- package/dist/library-provenance.js +0 -33
- package/dist/live-adapter-contract.js +0 -380
- package/dist/recording-target.js +0 -120
- package/dist/run-diagnostics.js +0 -364
- package/dist/run-recording.js +0 -481
- package/dist/runtime-proof.js +0 -90
- 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
|
|
20
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
});
|