@deeeed/metamask-harness 0.2.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 +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-undef, import-x/no-extraneous-dependencies */
|
|
3
|
+
/**
|
|
4
|
+
* CDP Bridge — Connect to the running MetaMask app via Hermes Chrome DevTools Protocol.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node library/actions/mobile/bridge-runtime/cdp-bridge.cjs navigate PerpsMarketListView
|
|
8
|
+
* node library/actions/mobile/bridge-runtime/cdp-bridge.cjs get-route
|
|
9
|
+
* node library/actions/mobile/bridge-runtime/cdp-bridge.cjs get-state "engine.backgroundState.NetworkController"
|
|
10
|
+
* node library/actions/mobile/bridge-runtime/cdp-bridge.cjs eval "1+1"
|
|
11
|
+
*
|
|
12
|
+
* Environment:
|
|
13
|
+
* WATCHER_PORT Metro port (default: 8081, read from .js.env if present)
|
|
14
|
+
* CDP_TIMEOUT Connection timeout in ms (default: 5000)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const { loadPort } = require('./lib/config.cjs');
|
|
22
|
+
const { discoverTarget } = require('./lib/target-discovery.cjs');
|
|
23
|
+
const { createWSClient } = require('./lib/ws-client.cjs');
|
|
24
|
+
const { cdpEval, cdpEvalAsync } = require('./lib/cdp-eval.cjs');
|
|
25
|
+
const { buildArmSnippet, buildCollectSnippet } = require('./lib/issue-capture.cjs');
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Commands
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
// Map of nested route names to their parent navigator.
|
|
32
|
+
// Friendly aliases → actual route names (for developer convenience)
|
|
33
|
+
const ROUTE_ALIASES = {
|
|
34
|
+
PerpsHomeView: 'PerpsMarketListView',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// When navigating to a nested route, we need: navigate('Parent', { screen: 'Child', params })
|
|
38
|
+
// Routes not in this map are assumed to be root-level and navigated to directly.
|
|
39
|
+
const NESTED_ROUTE_PARENTS = {
|
|
40
|
+
// Perps
|
|
41
|
+
PerpsMarketListView: 'Perps', PerpsMarketDetails: 'Perps', PerpsPositions: 'Perps',
|
|
42
|
+
PerpsTrendingView: 'Perps', PerpsWithdraw: 'Perps', PerpsTutorial: 'Perps', PerpsOrderRedirect: 'Perps',
|
|
43
|
+
PerpsClosePosition: 'Perps', PerpsTPSL: 'Perps', PerpsAdjustMargin: 'Perps',
|
|
44
|
+
PerpsOrderDetailsView: 'Perps', PerpsActivity: 'Perps', PerpsOrderBook: 'Perps',
|
|
45
|
+
PerpsPnlHeroCard: 'Perps', PerpsHIP3Debug: 'Perps',
|
|
46
|
+
PerpsSelectModifyAction: 'Perps', PerpsSelectAdjustMarginAction: 'Perps',
|
|
47
|
+
PerpsSelectOrderType: 'Perps', PerpsTradingView: 'Perps',
|
|
48
|
+
// Predict
|
|
49
|
+
PredictMarketList: 'Predict', PredictMarketDetails: 'Predict',
|
|
50
|
+
PredictActivityDetail: 'Predict',
|
|
51
|
+
// Bridge
|
|
52
|
+
BridgeView: 'Bridge', BridgeTokenSelector: 'Bridge',
|
|
53
|
+
BridgeTransactionDetails: 'Bridge',
|
|
54
|
+
// Staking
|
|
55
|
+
Stake: 'Staking', StakeConfirmation: 'Staking', Unstake: 'Staking',
|
|
56
|
+
UnstakeConfirmation: 'Staking', EarningsHistory: 'Staking', Claim: 'Staking',
|
|
57
|
+
// Earn
|
|
58
|
+
EarnLendingDepositConfirmation: 'EarnScreens',
|
|
59
|
+
EarnLendingWithdrawalConfirmation: 'EarnScreens',
|
|
60
|
+
// Card
|
|
61
|
+
CardHome: 'CardScreens', CardWelcome: 'CardScreens', CardAuthentication: 'CardScreens',
|
|
62
|
+
// Settings (nested under Settings root)
|
|
63
|
+
ContactsSettings: 'Settings', SecuritySettings: 'Settings',
|
|
64
|
+
AdvancedSettings: 'Settings', ExperimentalSettings: 'Settings',
|
|
65
|
+
DeveloperOptions: 'Settings', NotificationsSettings: 'Settings',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const COMMANDS = {
|
|
69
|
+
async navigate(client, args, { deviceName, platform } = {}) {
|
|
70
|
+
const routeName = ROUTE_ALIASES[args[0]] || args[0];
|
|
71
|
+
if (!routeName) {
|
|
72
|
+
throw new Error('Usage: navigate <RouteName> [params-json]');
|
|
73
|
+
}
|
|
74
|
+
let params = {};
|
|
75
|
+
if (args[1]) {
|
|
76
|
+
try {
|
|
77
|
+
params = JSON.parse(args[1]);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
throw new Error(`Invalid params JSON: ${e.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Capture route before navigation
|
|
84
|
+
const previousRoute = await cdpEval(client, 'globalThis.__AGENTIC__?.getRoute()');
|
|
85
|
+
|
|
86
|
+
let expr;
|
|
87
|
+
const parent = NESTED_ROUTE_PARENTS[routeName];
|
|
88
|
+
if (parent) {
|
|
89
|
+
// Nested route: navigate('Parent', { screen: 'Child', params: {...} })
|
|
90
|
+
const navParams = Object.keys(params).length > 0
|
|
91
|
+
? JSON.stringify({ screen: routeName, params })
|
|
92
|
+
: JSON.stringify({ screen: routeName });
|
|
93
|
+
expr = `globalThis.__AGENTIC__?.navigate(${JSON.stringify(parent)}, ${navParams})`;
|
|
94
|
+
} else {
|
|
95
|
+
// Root-level route: navigate('Route', params)
|
|
96
|
+
const paramsStr = JSON.stringify(params);
|
|
97
|
+
expr = `globalThis.__AGENTIC__?.navigate(${JSON.stringify(routeName)}, ${paramsStr})`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await cdpEval(client, expr);
|
|
101
|
+
// Small delay for navigation to settle, then return current route
|
|
102
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
103
|
+
const currentRoute = await cdpEval(
|
|
104
|
+
client,
|
|
105
|
+
'globalThis.__AGENTIC__?.getRoute()',
|
|
106
|
+
);
|
|
107
|
+
return { navigated: routeName, params, previousRoute, currentRoute, deviceName, platform };
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async 'get-route'(client) {
|
|
111
|
+
const route = await cdpEval(
|
|
112
|
+
client,
|
|
113
|
+
'globalThis.__AGENTIC__?.getRoute()',
|
|
114
|
+
);
|
|
115
|
+
return route;
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async 'get-state'(client, args) {
|
|
119
|
+
const path = args[0] || '';
|
|
120
|
+
if (!path) {
|
|
121
|
+
// Return navigation state
|
|
122
|
+
const state = await cdpEval(
|
|
123
|
+
client,
|
|
124
|
+
'globalThis?.__AGENTIC__?.getState()',
|
|
125
|
+
);
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
// Access Redux store at the given dot-path
|
|
129
|
+
const expr = `(() => {
|
|
130
|
+
const store = globalThis?.store;
|
|
131
|
+
if (!store) return { error: 'Redux store not found on globalThis.store' };
|
|
132
|
+
const state = store.getState();
|
|
133
|
+
const parts = ${JSON.stringify(path)}.split('.');
|
|
134
|
+
let current = state;
|
|
135
|
+
for (const p of parts) {
|
|
136
|
+
if (current == null) return undefined;
|
|
137
|
+
current = current[p];
|
|
138
|
+
}
|
|
139
|
+
return current;
|
|
140
|
+
})()`;
|
|
141
|
+
return await cdpEval(client, expr);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async eval(client, args) {
|
|
145
|
+
const expression = args.join(' ');
|
|
146
|
+
if (!expression) {
|
|
147
|
+
throw new Error('Usage: eval <expression>');
|
|
148
|
+
}
|
|
149
|
+
return await cdpEval(client, expression);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async 'eval-async'(client, args) {
|
|
153
|
+
const expression = args.join(' ');
|
|
154
|
+
if (!expression) {
|
|
155
|
+
throw new Error('Usage: eval-async <expression>');
|
|
156
|
+
}
|
|
157
|
+
return await cdpEvalAsync(client, expression);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
async 'can-go-back'(client) {
|
|
161
|
+
return await cdpEval(client, 'globalThis.__AGENTIC__?.canGoBack()');
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async 'go-back'(client, _args, { deviceName, platform } = {}) {
|
|
165
|
+
await cdpEval(client, 'globalThis.__AGENTIC__?.goBack()');
|
|
166
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
167
|
+
const route = await cdpEval(
|
|
168
|
+
client,
|
|
169
|
+
'globalThis.__AGENTIC__?.getRoute()',
|
|
170
|
+
);
|
|
171
|
+
return { currentRoute: route, deviceName, platform };
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async status(client, _args, { deviceName, platform } = {}) {
|
|
175
|
+
const expr = `(function() {
|
|
176
|
+
var route = globalThis.__AGENTIC__?.getRoute() || null;
|
|
177
|
+
var account = null;
|
|
178
|
+
try { account = globalThis.__AGENTIC__?.getSelectedAccount() || null; } catch(e) {}
|
|
179
|
+
return { route: route, account: account };
|
|
180
|
+
})()`;
|
|
181
|
+
const snapshot = await cdpEval(client, expr);
|
|
182
|
+
return { ...snapshot, deviceName: deviceName || '', platform: platform || '' };
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async 'list-accounts'(client) {
|
|
186
|
+
return await cdpEval(client, 'globalThis.__AGENTIC__?.listAccounts()');
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async 'get-selected-account'(client) {
|
|
190
|
+
return await cdpEval(client, 'globalThis.__AGENTIC__?.getSelectedAccount()');
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async 'switch-account'(client, args) {
|
|
194
|
+
const address = args[0];
|
|
195
|
+
if (!address) {
|
|
196
|
+
throw new Error('Usage: switch-account <address>');
|
|
197
|
+
}
|
|
198
|
+
return await cdpEval(client, `globalThis.__AGENTIC__?.switchAccount(${JSON.stringify(address)})`);
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async 'press-test-id'(client, args, { deviceName } = {}) {
|
|
202
|
+
const testId = args[0];
|
|
203
|
+
if (!testId) {
|
|
204
|
+
throw new Error('Usage: press-test-id <testId>');
|
|
205
|
+
}
|
|
206
|
+
// Try __AGENTIC__ bridge first, fall back to inline fiber walking
|
|
207
|
+
const expr = `(function() {
|
|
208
|
+
if (globalThis.__AGENTIC__?.pressTestId) return globalThis.__AGENTIC__.pressTestId(${JSON.stringify(testId)});
|
|
209
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
210
|
+
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
211
|
+
var renderers = hook.renderers;
|
|
212
|
+
if (!renderers) return { ok: false, error: 'No renderers' };
|
|
213
|
+
var getFiberRoots = hook.getFiberRoots;
|
|
214
|
+
function walk(fiber) {
|
|
215
|
+
if (!fiber) return false;
|
|
216
|
+
var props = fiber.memoizedProps;
|
|
217
|
+
if (props && props.testID === ${JSON.stringify(testId)}) {
|
|
218
|
+
if (typeof props.onPress === 'function') { props.onPress(); return true; }
|
|
219
|
+
}
|
|
220
|
+
return walk(fiber.child) || walk(fiber.sibling);
|
|
221
|
+
}
|
|
222
|
+
for (var [id] of renderers) {
|
|
223
|
+
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
224
|
+
if (!roots) continue;
|
|
225
|
+
var found = false;
|
|
226
|
+
roots.forEach(function(r) { if (!found) found = walk(r.current); });
|
|
227
|
+
if (found) return { ok: true, testId: ${JSON.stringify(testId)} };
|
|
228
|
+
}
|
|
229
|
+
return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onPress' };
|
|
230
|
+
})()`;
|
|
231
|
+
const result = await cdpEval(client, expr);
|
|
232
|
+
return { ...result, testId, deviceName };
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
async 'scroll-view'(client, args, { deviceName } = {}) {
|
|
236
|
+
let testId;
|
|
237
|
+
let offset = 300;
|
|
238
|
+
let animated = false;
|
|
239
|
+
for (let i = 0; i < args.length; i++) {
|
|
240
|
+
if (args[i] === '--test-id' && i + 1 < args.length) {
|
|
241
|
+
testId = args[++i];
|
|
242
|
+
} else if (args[i] === '--offset' && i + 1 < args.length) {
|
|
243
|
+
offset = Number(args[++i]);
|
|
244
|
+
} else if (args[i] === '--animated') {
|
|
245
|
+
animated = true;
|
|
246
|
+
} else if (args[i] === '--no-animated') {
|
|
247
|
+
animated = false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const optsJson = JSON.stringify({ testId, offset, animated });
|
|
251
|
+
// Try __AGENTIC__ bridge first, fall back to inline fiber walking
|
|
252
|
+
const expr = `(function() {
|
|
253
|
+
if (globalThis.__AGENTIC__?.scrollView) return globalThis.__AGENTIC__.scrollView(${optsJson});
|
|
254
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
255
|
+
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
256
|
+
var renderers = hook.renderers;
|
|
257
|
+
if (!renderers) return { ok: false, error: 'No renderers' };
|
|
258
|
+
var getFiberRoots = hook.getFiberRoots;
|
|
259
|
+
var opts = ${optsJson};
|
|
260
|
+
function tryScroll(fiber, walkSiblings) {
|
|
261
|
+
if (walkSiblings === undefined) walkSiblings = true;
|
|
262
|
+
var current = fiber;
|
|
263
|
+
while (current) {
|
|
264
|
+
var sn = current.stateNode;
|
|
265
|
+
if (sn) {
|
|
266
|
+
if (typeof sn.scrollTo === 'function') { sn.scrollTo({ y: opts.offset, animated: opts.animated }); return true; }
|
|
267
|
+
if (typeof sn.scrollToOffset === 'function') { sn.scrollToOffset({ offset: opts.offset, animated: opts.animated }); return true; }
|
|
268
|
+
}
|
|
269
|
+
if (tryScroll(current.child)) return true;
|
|
270
|
+
current = walkSiblings ? current.sibling : null;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
function findTestId(fiber) {
|
|
275
|
+
if (!fiber) return null;
|
|
276
|
+
var props = fiber.memoizedProps;
|
|
277
|
+
if (props && props.testID === opts.testId) return fiber;
|
|
278
|
+
return findTestId(fiber.child) || findTestId(fiber.sibling);
|
|
279
|
+
}
|
|
280
|
+
for (var [id] of renderers) {
|
|
281
|
+
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
282
|
+
if (!roots) continue;
|
|
283
|
+
var scrolled = false;
|
|
284
|
+
roots.forEach(function(r) {
|
|
285
|
+
if (scrolled) return;
|
|
286
|
+
if (opts.testId) {
|
|
287
|
+
var anchor = findTestId(r.current);
|
|
288
|
+
if (anchor) scrolled = tryScroll(anchor, false);
|
|
289
|
+
} else {
|
|
290
|
+
scrolled = tryScroll(r.current);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
if (scrolled) return { ok: true, testId: opts.testId, offset: opts.offset, animated: opts.animated };
|
|
294
|
+
}
|
|
295
|
+
return { ok: false, error: opts.testId ? 'No scrollable near testID=' + opts.testId : 'No scrollable found' };
|
|
296
|
+
})()`;
|
|
297
|
+
const result = await cdpEval(client, expr);
|
|
298
|
+
return { ...result, deviceName };
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
async 'set-input'(client, args, { deviceName } = {}) {
|
|
302
|
+
const testId = args[0];
|
|
303
|
+
const value = args.slice(1).join(' ');
|
|
304
|
+
if (!testId) {
|
|
305
|
+
throw new Error('Usage: set-input <testId> <value>');
|
|
306
|
+
}
|
|
307
|
+
// Try __AGENTIC__ bridge first, fall back to inline fiber walking
|
|
308
|
+
const expr = `(function() {
|
|
309
|
+
if (globalThis.__AGENTIC__?.setInput) return globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)});
|
|
310
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
311
|
+
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
312
|
+
var renderers = hook.renderers;
|
|
313
|
+
if (!renderers) return { ok: false, error: 'No renderers' };
|
|
314
|
+
var getFiberRoots = hook.getFiberRoots;
|
|
315
|
+
function findByTestId(fiber) {
|
|
316
|
+
if (!fiber) return null;
|
|
317
|
+
var props = fiber.memoizedProps;
|
|
318
|
+
if (props && props.testID === ${JSON.stringify(testId)}) return fiber;
|
|
319
|
+
return findByTestId(fiber.child) || findByTestId(fiber.sibling);
|
|
320
|
+
}
|
|
321
|
+
for (var [id] of renderers) {
|
|
322
|
+
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
323
|
+
if (!roots) continue;
|
|
324
|
+
var result = null;
|
|
325
|
+
roots.forEach(function(r) {
|
|
326
|
+
if (result) return;
|
|
327
|
+
var fiber = findByTestId(r.current);
|
|
328
|
+
if (!fiber) return;
|
|
329
|
+
var cur = fiber;
|
|
330
|
+
while (cur) {
|
|
331
|
+
if (cur.memoizedProps && typeof cur.memoizedProps.onChangeText === 'function') {
|
|
332
|
+
cur.memoizedProps.onChangeText(${JSON.stringify(value)});
|
|
333
|
+
result = { ok: true, testId: ${JSON.stringify(testId)}, value: ${JSON.stringify(value)} };
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
cur = cur.return || null;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
if (result) return result;
|
|
340
|
+
}
|
|
341
|
+
return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onChangeText' };
|
|
342
|
+
})()`;
|
|
343
|
+
const result = await cdpEval(client, expr);
|
|
344
|
+
return { ...result, testId, value, deviceName };
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
async 'sentry-debug'(client, args, { deviceName } = {}) {
|
|
348
|
+
const action = args[0] || 'enable';
|
|
349
|
+
if (action === 'enable') {
|
|
350
|
+
const expr = `(function() {
|
|
351
|
+
try {
|
|
352
|
+
var sentryHub = globalThis.__SENTRY__;
|
|
353
|
+
if (!sentryHub) return { ok: false, error: 'globalThis.__SENTRY__ not found' };
|
|
354
|
+
var ver = Object.keys(sentryHub).filter(function(k) { return /^[0-9]/.test(k); })[0];
|
|
355
|
+
if (!ver) return { ok: false, error: 'No Sentry version key found in __SENTRY__' };
|
|
356
|
+
var hub = sentryHub[ver];
|
|
357
|
+
var scope = hub.defaultCurrentScope;
|
|
358
|
+
if (!scope) return { ok: false, error: 'No defaultCurrentScope in Sentry hub' };
|
|
359
|
+
var sentryClient = scope.getClient();
|
|
360
|
+
if (!sentryClient) return { ok: false, error: 'No Sentry client found via scope.getClient()' };
|
|
361
|
+
|
|
362
|
+
if (sentryClient.__agenticPatched) return { ok: true, alreadyPatched: true, version: ver };
|
|
363
|
+
|
|
364
|
+
var origException = sentryClient.captureException.bind(sentryClient);
|
|
365
|
+
var origMessage = sentryClient.captureMessage.bind(sentryClient);
|
|
366
|
+
|
|
367
|
+
sentryClient.captureException = function(err, hint, currentScope) {
|
|
368
|
+
var msg = err && err.message ? err.message : String(err);
|
|
369
|
+
console.warn('[SENTRY-DEBUG] captureException:', msg);
|
|
370
|
+
if (err && err.stack) console.warn('[SENTRY-DEBUG] stack:', err.stack);
|
|
371
|
+
return origException(err, hint, currentScope);
|
|
372
|
+
};
|
|
373
|
+
sentryClient.captureMessage = function(msg, level, hint, currentScope) {
|
|
374
|
+
console.warn('[SENTRY-DEBUG] captureMessage:', msg);
|
|
375
|
+
return origMessage(msg, level, hint, currentScope);
|
|
376
|
+
};
|
|
377
|
+
sentryClient.__agenticPatched = true;
|
|
378
|
+
sentryClient.__agenticOrigException = origException;
|
|
379
|
+
sentryClient.__agenticOrigMessage = origMessage;
|
|
380
|
+
return { ok: true, patched: true, version: ver };
|
|
381
|
+
} catch(e) {
|
|
382
|
+
return { ok: false, error: String(e) };
|
|
383
|
+
}
|
|
384
|
+
})()`;
|
|
385
|
+
return await cdpEval(client, expr);
|
|
386
|
+
} else if (action === 'disable') {
|
|
387
|
+
const expr = `(function() {
|
|
388
|
+
try {
|
|
389
|
+
var sentryHub = globalThis.__SENTRY__;
|
|
390
|
+
if (!sentryHub) return { ok: true, wasNotPatched: true };
|
|
391
|
+
var ver = Object.keys(sentryHub).filter(function(k) { return /^[0-9]/.test(k); })[0];
|
|
392
|
+
if (!ver) return { ok: true, wasNotPatched: true };
|
|
393
|
+
var hub = sentryHub[ver];
|
|
394
|
+
var scope = hub.defaultCurrentScope;
|
|
395
|
+
if (!scope) return { ok: true, wasNotPatched: true };
|
|
396
|
+
var sentryClient = scope.getClient();
|
|
397
|
+
if (!sentryClient || !sentryClient.__agenticPatched) return { ok: true, wasNotPatched: true };
|
|
398
|
+
sentryClient.captureException = sentryClient.__agenticOrigException;
|
|
399
|
+
sentryClient.captureMessage = sentryClient.__agenticOrigMessage;
|
|
400
|
+
delete sentryClient.__agenticPatched;
|
|
401
|
+
delete sentryClient.__agenticOrigException;
|
|
402
|
+
delete sentryClient.__agenticOrigMessage;
|
|
403
|
+
return { ok: true, unpatched: true, version: ver };
|
|
404
|
+
} catch(e) {
|
|
405
|
+
return { ok: false, error: String(e) };
|
|
406
|
+
}
|
|
407
|
+
})()`;
|
|
408
|
+
return await cdpEval(client, expr);
|
|
409
|
+
}
|
|
410
|
+
throw new Error('Usage: sentry-debug [enable|disable]');
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
async unlock(client, args, { deviceName } = {}) {
|
|
414
|
+
const password = args[0];
|
|
415
|
+
if (!password) {
|
|
416
|
+
throw new Error('Usage: unlock <password>');
|
|
417
|
+
}
|
|
418
|
+
const escapedPw = JSON.stringify(password);
|
|
419
|
+
|
|
420
|
+
// Fiber-tree walker snippet reused in both phases
|
|
421
|
+
const fiberWalker = `
|
|
422
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
423
|
+
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
424
|
+
var renderers = hook.renderers;
|
|
425
|
+
if (!renderers) return { ok: false, error: 'No renderers' };
|
|
426
|
+
var getFiberRoots = hook.getFiberRoots;
|
|
427
|
+
function walkFiber(fiber, testId) {
|
|
428
|
+
if (!fiber) return null;
|
|
429
|
+
var props = fiber.memoizedProps;
|
|
430
|
+
if (props && props.testID === testId) return fiber;
|
|
431
|
+
var found = walkFiber(fiber.child, testId);
|
|
432
|
+
if (found) return found;
|
|
433
|
+
return walkFiber(fiber.sibling, testId);
|
|
434
|
+
}`;
|
|
435
|
+
|
|
436
|
+
// Phase 1: inject password via onChangeText
|
|
437
|
+
const injectExpr = `(function() {
|
|
438
|
+
${fiberWalker}
|
|
439
|
+
var password = ${escapedPw};
|
|
440
|
+
for (var [id] of renderers) {
|
|
441
|
+
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
442
|
+
if (!roots) continue;
|
|
443
|
+
var injected = false;
|
|
444
|
+
roots.forEach(function(root) {
|
|
445
|
+
if (injected) return;
|
|
446
|
+
var input = walkFiber(root.current, 'login-password-input');
|
|
447
|
+
if (input && input.memoizedProps && typeof input.memoizedProps.onChangeText === 'function') {
|
|
448
|
+
input.memoizedProps.onChangeText(password);
|
|
449
|
+
injected = true;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
if (injected) return { ok: true, phase: 'inject' };
|
|
453
|
+
}
|
|
454
|
+
return { ok: false, error: 'login-password-input not found' };
|
|
455
|
+
})()`;
|
|
456
|
+
|
|
457
|
+
const injectResult = await cdpEval(client, injectExpr);
|
|
458
|
+
if (!injectResult?.ok) {
|
|
459
|
+
return { ok: false, error: injectResult?.error || 'Password injection failed', deviceName };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Wait for React to re-render so useCallback picks up the new password
|
|
463
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
464
|
+
|
|
465
|
+
// Phase 2: walk the fiber tree AGAIN to get the updated onPress callback
|
|
466
|
+
const pressExpr = `(function() {
|
|
467
|
+
${fiberWalker}
|
|
468
|
+
for (var [id] of renderers) {
|
|
469
|
+
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
470
|
+
if (!roots) continue;
|
|
471
|
+
var pressed = false;
|
|
472
|
+
roots.forEach(function(root) {
|
|
473
|
+
if (pressed) return;
|
|
474
|
+
var btn = walkFiber(root.current, 'log-in-button');
|
|
475
|
+
if (btn && btn.memoizedProps && typeof btn.memoizedProps.onPress === 'function') {
|
|
476
|
+
btn.memoizedProps.onPress();
|
|
477
|
+
pressed = true;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
if (pressed) return { ok: true, phase: 'press' };
|
|
481
|
+
}
|
|
482
|
+
return { ok: false, error: 'log-in-button not found' };
|
|
483
|
+
})()`;
|
|
484
|
+
|
|
485
|
+
const pressResult = await cdpEval(client, pressExpr);
|
|
486
|
+
return {
|
|
487
|
+
ok: pressResult?.ok ?? false,
|
|
488
|
+
injected: true,
|
|
489
|
+
pressed: pressResult?.ok ?? false,
|
|
490
|
+
error: pressResult?.error,
|
|
491
|
+
deviceName,
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
async 'show-step-json'(client, args) {
|
|
496
|
+
const raw = args.join(' ');
|
|
497
|
+
let step;
|
|
498
|
+
try {
|
|
499
|
+
step = JSON.parse(raw);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
throw new Error(`show-step-json requires a JSON step payload: ${error.message}`);
|
|
502
|
+
}
|
|
503
|
+
if (!step || typeof step !== 'object' || Array.isArray(step)) {
|
|
504
|
+
throw new Error('show-step-json requires a JSON object step payload');
|
|
505
|
+
}
|
|
506
|
+
if (typeof step.intent !== 'string' || !step.intent.trim()) {
|
|
507
|
+
throw new Error('show-step-json requires step.intent');
|
|
508
|
+
}
|
|
509
|
+
// HUD derives status/progress from step.id when status is omitted; a
|
|
510
|
+
// non-string id makes statusForStep throw during render. Reject at the boundary.
|
|
511
|
+
if (step.id !== undefined && typeof step.id !== 'string') {
|
|
512
|
+
throw new Error('show-step-json step.id must be a string when provided');
|
|
513
|
+
}
|
|
514
|
+
const payload = JSON.stringify(step);
|
|
515
|
+
await cdpEval(client, `globalThis.__AGENTIC__?.showStep && globalThis.__AGENTIC__.showStep(${payload})`);
|
|
516
|
+
return { ok: true };
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
async 'hide-step'(client) {
|
|
520
|
+
await cdpEval(client, `globalThis.__AGENTIC__?.hideStep && globalThis.__AGENTIC__.hideStep()`);
|
|
521
|
+
return { ok: true };
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
async 'profiler-start'(client) {
|
|
525
|
+
// Hermes CDP exposes the sampling profiler via the Profiler domain.
|
|
526
|
+
// Output of Profiler.stop is a Chrome-compatible .cpuprofile object.
|
|
527
|
+
// Note: Hermes does NOT implement Profiler.enable (returns -32601 Unsupported);
|
|
528
|
+
// Profiler.start/stop work without it.
|
|
529
|
+
await client.send('Profiler.start');
|
|
530
|
+
return { ok: true, started: true };
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
async 'profiler-stop'(client, args) {
|
|
534
|
+
let outPath = '';
|
|
535
|
+
let label = 'trace';
|
|
536
|
+
for (let i = 0; i < args.length; i++) {
|
|
537
|
+
if (args[i] === '--out' && i + 1 < args.length) {
|
|
538
|
+
outPath = args[++i];
|
|
539
|
+
} else if (args[i] === '--label' && i + 1 < args.length) {
|
|
540
|
+
label = args[++i];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const res = await client.send('Profiler.stop', {}, 60000);
|
|
544
|
+
const profile = res?.profile;
|
|
545
|
+
if (!profile) {
|
|
546
|
+
return { ok: false, error: 'Profiler.stop returned no profile' };
|
|
547
|
+
}
|
|
548
|
+
const serialized = JSON.stringify(profile);
|
|
549
|
+
if (!outPath) {
|
|
550
|
+
const tracesDir = path.resolve(process.env.APP_ROOT || process.cwd(), 'temp/agentic/traces');
|
|
551
|
+
fs.mkdirSync(tracesDir, { recursive: true });
|
|
552
|
+
outPath = path.join(tracesDir, `trace-${label}.cpuprofile`);
|
|
553
|
+
} else {
|
|
554
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
555
|
+
}
|
|
556
|
+
fs.writeFileSync(outPath, serialized);
|
|
557
|
+
const nodesCount = Array.isArray(profile.nodes) ? profile.nodes.length : 0;
|
|
558
|
+
const samplesCount = Array.isArray(profile.samples) ? profile.samples.length : 0;
|
|
559
|
+
return {
|
|
560
|
+
ok: true,
|
|
561
|
+
path: outPath,
|
|
562
|
+
label,
|
|
563
|
+
sizeBytes: Buffer.byteLength(serialized, 'utf8'),
|
|
564
|
+
nodesCount,
|
|
565
|
+
samplesCount,
|
|
566
|
+
startTime: profile.startTime ?? null,
|
|
567
|
+
endTime: profile.endTime ?? null,
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
async 'issues-arm'(client) {
|
|
572
|
+
// Installs console.warn/error and global error/unhandledrejection hooks
|
|
573
|
+
// that push into globalThis.__AGENTIC_ISSUES__ (capped at 500 entries).
|
|
574
|
+
// Idempotent — subsequent calls return { installed: true, reason: 'already-installed' }.
|
|
575
|
+
const result = await cdpEval(client, buildArmSnippet());
|
|
576
|
+
return result || { installed: false };
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
async 'issues-collect'(client) {
|
|
580
|
+
// Snapshots and clears globalThis.__AGENTIC_ISSUES__.
|
|
581
|
+
// Returns { count, entries } where entries are { t, level, text }.
|
|
582
|
+
const result = await cdpEval(client, buildCollectSnippet());
|
|
583
|
+
return result || { count: 0, entries: [] };
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
// Main
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
|
|
592
|
+
async function main() {
|
|
593
|
+
const args = process.argv.slice(2);
|
|
594
|
+
const command = args[0];
|
|
595
|
+
|
|
596
|
+
if (!command || command === '--help' || command === '-h') {
|
|
597
|
+
console.log(`CDP Bridge — interact with the running MetaMask app via Hermes CDP
|
|
598
|
+
|
|
599
|
+
Usage:
|
|
600
|
+
node library/actions/mobile/bridge-runtime/cdp-bridge.cjs <command> [args...]
|
|
601
|
+
|
|
602
|
+
Commands:
|
|
603
|
+
navigate <RouteName> [params-json] Navigate to a screen
|
|
604
|
+
status Route + selected account snapshot
|
|
605
|
+
get-route Get current route name and params
|
|
606
|
+
get-state [dot.path] Get Redux state (or nav state if no path)
|
|
607
|
+
eval <expression> Evaluate arbitrary JS in app context
|
|
608
|
+
eval-async <expression> Evaluate async/Promise expression
|
|
609
|
+
can-go-back Check if navigation can go back
|
|
610
|
+
go-back Navigate back
|
|
611
|
+
list-accounts List all accounts
|
|
612
|
+
get-selected-account Get the currently selected account
|
|
613
|
+
switch-account <address> Switch to account by address
|
|
614
|
+
press-test-id <testId> Press a component by its testID prop
|
|
615
|
+
scroll-view [--test-id <id>] [--offset <n>] [--animated]
|
|
616
|
+
Scroll a ScrollView/FlatList
|
|
617
|
+
set-input <testId> <value> Set text input value by testID (calls onChangeText)
|
|
618
|
+
sentry-debug [enable|disable] Patch Sentry to log errors to console with [SENTRY-DEBUG] prefix
|
|
619
|
+
unlock <password> Unlock wallet (inject password + press login button via fiber tree)
|
|
620
|
+
profiler-start Start Hermes sampling profiler
|
|
621
|
+
profiler-stop [--out <path>] [--label <name>]
|
|
622
|
+
Stop profiler, dump Chrome-compatible
|
|
623
|
+
.cpuprofile to <path> (default:
|
|
624
|
+
temp/agentic/traces/trace-<label>.cpuprofile)
|
|
625
|
+
issues-arm Install console/exception hooks that
|
|
626
|
+
populate globalThis.__AGENTIC_ISSUES__
|
|
627
|
+
issues-collect Snapshot + clear the in-app issue buffer
|
|
628
|
+
|
|
629
|
+
Environment:
|
|
630
|
+
WATCHER_PORT Metro port (default: 8081)
|
|
631
|
+
CDP_TIMEOUT Connection timeout in ms (default: 5000)
|
|
632
|
+
IOS_SIMULATOR Filter targets by iOS simulator name
|
|
633
|
+
ANDROID_DEVICE Filter targets by Android device/emulator serial`);
|
|
634
|
+
process.exit(0);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const handler = COMMANDS[command];
|
|
638
|
+
if (!handler) {
|
|
639
|
+
console.error(`Unknown command: ${command}`);
|
|
640
|
+
console.error(`Available: ${Object.keys(COMMANDS).join(', ')}`);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
const port = loadPort();
|
|
646
|
+
const timeout = Number.parseInt(process.env.CDP_TIMEOUT || '5000', 10);
|
|
647
|
+
|
|
648
|
+
// `status` probes ALL connected targets so both platforms are visible.
|
|
649
|
+
if (command === 'status') {
|
|
650
|
+
const { discoverAllTargets } = require('./lib/target-discovery.cjs');
|
|
651
|
+
const allTargets = await discoverAllTargets(port);
|
|
652
|
+
const results = [];
|
|
653
|
+
for (const target of allTargets) {
|
|
654
|
+
let client;
|
|
655
|
+
try {
|
|
656
|
+
client = await createWSClient(target.wsUrl, timeout);
|
|
657
|
+
const platform = await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
|
|
658
|
+
const result = await handler(client, args.slice(1), { deviceName: target.deviceName, platform });
|
|
659
|
+
results.push(result);
|
|
660
|
+
} catch {
|
|
661
|
+
// Target not responsive — skip
|
|
662
|
+
} finally {
|
|
663
|
+
if (client) client.close();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
console.log(JSON.stringify(results.length === 1 ? results[0] : results, null, 2));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const { wsUrl, deviceName } = await discoverTarget(port);
|
|
671
|
+
const client = await createWSClient(wsUrl, timeout);
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
// Detect platform from the running app (exposed by __AGENTIC__ bridge as Platform.OS)
|
|
675
|
+
const platform = await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
|
|
676
|
+
const result = await handler(client, args.slice(1), { deviceName, platform });
|
|
677
|
+
console.log(JSON.stringify(result, null, 2));
|
|
678
|
+
} finally {
|
|
679
|
+
client.close();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
main().catch((err) => {
|
|
684
|
+
console.error(`ERROR: ${err.message}`);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
});
|