react-native-ai-debugger 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/build/core/appDetection.d.ts +7 -0
  2. package/build/core/appDetection.d.ts.map +1 -1
  3. package/build/core/appDetection.js +38 -17
  4. package/build/core/appDetection.js.map +1 -1
  5. package/build/core/componentSearch.d.ts +38 -0
  6. package/build/core/componentSearch.d.ts.map +1 -0
  7. package/build/core/componentSearch.js +419 -0
  8. package/build/core/componentSearch.js.map +1 -0
  9. package/build/core/componentTree.d.ts +12 -0
  10. package/build/core/componentTree.d.ts.map +1 -0
  11. package/build/core/componentTree.js +275 -0
  12. package/build/core/componentTree.js.map +1 -0
  13. package/build/core/connection.d.ts +1 -0
  14. package/build/core/connection.d.ts.map +1 -1
  15. package/build/core/connection.js +97 -88
  16. package/build/core/connection.js.map +1 -1
  17. package/build/core/debugGlobals.d.ts +5 -0
  18. package/build/core/debugGlobals.d.ts.map +1 -0
  19. package/build/core/debugGlobals.js +300 -0
  20. package/build/core/debugGlobals.js.map +1 -0
  21. package/build/core/executor.d.ts +7 -206
  22. package/build/core/executor.d.ts.map +1 -1
  23. package/build/core/executor.js +10 -4333
  24. package/build/core/executor.js.map +1 -1
  25. package/build/core/index.d.ts +3 -2
  26. package/build/core/index.d.ts.map +1 -1
  27. package/build/core/index.js +3 -2
  28. package/build/core/index.js.map +1 -1
  29. package/build/core/inspector.d.ts +50 -0
  30. package/build/core/inspector.d.ts.map +1 -0
  31. package/build/core/inspector.js +604 -0
  32. package/build/core/inspector.js.map +1 -0
  33. package/build/core/jsExecute.d.ts +27 -0
  34. package/build/core/jsExecute.d.ts.map +1 -0
  35. package/build/core/jsExecute.js +453 -0
  36. package/build/core/jsExecute.js.map +1 -0
  37. package/build/core/pressables.d.ts +44 -0
  38. package/build/core/pressables.d.ts.map +1 -0
  39. package/build/core/pressables.js +1480 -0
  40. package/build/core/pressables.js.map +1 -0
  41. package/build/core/register.d.ts +8 -0
  42. package/build/core/register.d.ts.map +1 -0
  43. package/build/core/register.js +216 -0
  44. package/build/core/register.js.map +1 -0
  45. package/build/core/screenLayout.d.ts +52 -0
  46. package/build/core/screenLayout.d.ts.map +1 -0
  47. package/build/core/screenLayout.js +828 -0
  48. package/build/core/screenLayout.js.map +1 -0
  49. package/build/core/toolHelpers.d.ts +24 -0
  50. package/build/core/toolHelpers.d.ts.map +1 -0
  51. package/build/core/toolHelpers.js +118 -0
  52. package/build/core/toolHelpers.js.map +1 -0
  53. package/build/core/types.d.ts +11 -6
  54. package/build/core/types.d.ts.map +1 -1
  55. package/build/index.d.ts +2 -4
  56. package/build/index.d.ts.map +1 -1
  57. package/build/index.js +37 -4440
  58. package/build/index.js.map +1 -1
  59. package/build/tools/accountTools.d.ts +2 -0
  60. package/build/tools/accountTools.d.ts.map +1 -1
  61. package/build/tools/accountTools.js +5 -0
  62. package/build/tools/accountTools.js.map +1 -1
  63. package/build/tools/bundleTools.d.ts +3 -0
  64. package/build/tools/bundleTools.d.ts.map +1 -0
  65. package/build/tools/bundleTools.js +368 -0
  66. package/build/tools/bundleTools.js.map +1 -0
  67. package/build/tools/componentTools.d.ts +3 -0
  68. package/build/tools/componentTools.d.ts.map +1 -0
  69. package/build/tools/componentTools.js +639 -0
  70. package/build/tools/componentTools.js.map +1 -0
  71. package/build/tools/connectionTools.d.ts +3 -0
  72. package/build/tools/connectionTools.d.ts.map +1 -0
  73. package/build/tools/connectionTools.js +564 -0
  74. package/build/tools/connectionTools.js.map +1 -0
  75. package/build/tools/deviceTools.d.ts +3 -0
  76. package/build/tools/deviceTools.d.ts.map +1 -0
  77. package/build/tools/deviceTools.js +296 -0
  78. package/build/tools/deviceTools.js.map +1 -0
  79. package/build/tools/executionTools.d.ts +3 -0
  80. package/build/tools/executionTools.d.ts.map +1 -0
  81. package/build/tools/executionTools.js +173 -0
  82. package/build/tools/executionTools.js.map +1 -0
  83. package/build/tools/interactionTools.d.ts +3 -0
  84. package/build/tools/interactionTools.d.ts.map +1 -0
  85. package/build/tools/interactionTools.js +414 -0
  86. package/build/tools/interactionTools.js.map +1 -0
  87. package/build/tools/logTools.d.ts +3 -0
  88. package/build/tools/logTools.d.ts.map +1 -0
  89. package/build/tools/logTools.js +371 -0
  90. package/build/tools/logTools.js.map +1 -0
  91. package/build/tools/metaTools.d.ts +7 -0
  92. package/build/tools/metaTools.d.ts.map +1 -0
  93. package/build/tools/metaTools.js +205 -0
  94. package/build/tools/metaTools.js.map +1 -0
  95. package/build/tools/networkTools.d.ts +3 -0
  96. package/build/tools/networkTools.d.ts.map +1 -0
  97. package/build/tools/networkTools.js +478 -0
  98. package/build/tools/networkTools.js.map +1 -0
  99. package/build/tools/reduxTools.d.ts +3 -0
  100. package/build/tools/reduxTools.d.ts.map +1 -0
  101. package/build/tools/reduxTools.js +83 -0
  102. package/build/tools/reduxTools.js.map +1 -0
  103. package/build/tools/screenshotTools.d.ts +3 -0
  104. package/build/tools/screenshotTools.d.ts.map +1 -0
  105. package/build/tools/screenshotTools.js +606 -0
  106. package/build/tools/screenshotTools.js.map +1 -0
  107. package/package.json +1 -1
@@ -0,0 +1,275 @@
1
+ import { executeInApp } from "./jsExecute.js";
2
+ function formatTreeToTonl(node, indent = 0) {
3
+ const prefix = " ".repeat(indent);
4
+ let result = `${prefix}${node.component}`;
5
+ // Add props inline if present
6
+ if (node.props && Object.keys(node.props).length > 0) {
7
+ const propsStr = Object.entries(node.props)
8
+ .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
9
+ .join(",");
10
+ result += ` (${propsStr})`;
11
+ }
12
+ // Add layout inline if present
13
+ if (node.layout && Object.keys(node.layout).length > 0) {
14
+ const layoutStr = Object.entries(node.layout)
15
+ .map(([k, v]) => `${k}:${v}`)
16
+ .join(",");
17
+ result += ` [${layoutStr}]`;
18
+ }
19
+ result += "\n";
20
+ // Recurse children
21
+ if (node.children && node.children.length > 0) {
22
+ for (const child of node.children) {
23
+ result += formatTreeToTonl(child, indent + 1);
24
+ }
25
+ }
26
+ return result;
27
+ }
28
+ // Ultra-compact structure-only tree format (just component names, indented)
29
+ function formatTreeStructureOnly(node, indent = 0) {
30
+ const prefix = " ".repeat(indent);
31
+ let result = `${prefix}${node.component}\n`;
32
+ if (node.children && node.children.length > 0) {
33
+ for (const child of node.children) {
34
+ result += formatTreeStructureOnly(child, indent + 1);
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ export async function getComponentTree(options = {}) {
40
+ const { includeProps = false, includeStyles = false, hideInternals = true, format = "tonl", structureOnly = false, focusedOnly = false, device } = options;
41
+ // Use lower default depth for structureOnly to keep output compact (~2-5KB)
42
+ // Full mode uses higher depth since TONL format handles it better
43
+ // focusedOnly mode uses moderate depth since we're already filtering to active screen
44
+ const maxDepth = options.maxDepth ?? 5000;
45
+ const expression = `
46
+ (function() {
47
+ const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
48
+ if (!hook) return { error: 'React DevTools hook not found. Make sure you are running a development build.' };
49
+
50
+ // Try to get fiber roots (renderer ID is usually 1)
51
+ let roots = [];
52
+ if (hook.getFiberRoots) {
53
+ roots = [...(hook.getFiberRoots(1) || [])];
54
+ }
55
+ if (roots.length === 0 && hook.renderers) {
56
+ // Try all renderers
57
+ for (const [id] of hook.renderers) {
58
+ const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
59
+ if (r.length > 0) {
60
+ roots = r;
61
+ break;
62
+ }
63
+ }
64
+ }
65
+ if (roots.length === 0) return { error: 'No fiber roots found. The app may not have rendered yet.' };
66
+
67
+ const maxDepth = ${maxDepth};
68
+ const includeProps = ${includeProps};
69
+ const includeStyles = ${includeStyles};
70
+ const hideInternals = ${hideInternals};
71
+ const focusedOnly = ${focusedOnly};
72
+
73
+ // Internal RN components to hide
74
+ const internalPatterns = /^(RCT|RNS|Animated\\(|AnimatedComponent|VirtualizedList|CellRenderer|ScrollViewContext|PerformanceLoggerContext|RootTagContext|HeaderShownContext|HeaderHeightContext|HeaderBackContext|SafeAreaFrameContext|SafeAreaInsetsContext|VirtualizedListContext|VirtualizedListCellContextProvider|StaticContainer|DelayedFreeze|Freeze|Suspender|DebugContainer|MaybeNestedStack|SceneView|NavigationContent|PreventRemoveProvider|EnsureSingleNavigator)/;
75
+
76
+ // Screen component patterns - user's actual screens (strict matching)
77
+ // Only match *Screen and *Page to avoid false positives like BottomTabView
78
+ const screenPatterns = /^[A-Z][a-zA-Z0-9]*(Screen|Page)$/;
79
+
80
+ // Navigation/internal screen patterns to SKIP (these look like screens but are framework components)
81
+ const internalScreenPatterns = /^(MaybeScreen|Screen$|ScreenContainer|ScreenStack|SceneView|Background$)/;
82
+
83
+ // Provider/wrapper patterns to skip when finding focused screen
84
+ const wrapperPatterns = /^(App|AppContainer|Provider|Context|SafeArea|Gesture|Theme|Redux|Root|Navigator|Stack|Tab|Drawer|Navigation|Container|Wrapper|Layout|ErrorBoundary|Suspense|PersistGate|LinkingContext|AppState|View|Fragment|NativeStack|BottomTab|Screen$)/i;
85
+
86
+ // Global overlay patterns - stop traversing into these subtrees
87
+ // Be specific to avoid blocking BottomSheetDrawer, PortalProvider, etc.
88
+ const overlayPatterns = /^(BottomSheet$|BottomSheetGlobal|Modal$|Toast$|Snackbar$|Dialog$|Overlay$|Popup$|MyToast$|PaywallModal$|FullScreenBannerModal$)/i;
89
+
90
+ // Navigation container patterns - skip traversing into these (screens inside are nav screens, not focused content)
91
+ const navContainerPatterns = /^(RootNavigation|NativeStackNavigator|BottomTabNavigator|DrawerNavigator|TabNavigator|StackNavigator)/;
92
+
93
+ function getComponentName(fiber) {
94
+ if (!fiber || !fiber.type) return null;
95
+ if (typeof fiber.type === 'string') return fiber.type; // Host component (View, Text, etc.)
96
+ return fiber.type.displayName || fiber.type.name || null;
97
+ }
98
+
99
+ function shouldHide(name) {
100
+ if (!hideInternals || !name) return false;
101
+ return internalPatterns.test(name);
102
+ }
103
+
104
+ function extractLayoutStyles(style) {
105
+ try {
106
+ if (!style) return null;
107
+ const merged = Array.isArray(style)
108
+ ? Object.assign({}, ...style.filter(Boolean).map(s => {
109
+ try { return typeof s === 'object' ? s : {}; }
110
+ catch { return {}; }
111
+ }))
112
+ : (typeof style === 'object' ? style : {});
113
+
114
+ const layout = {};
115
+ const layoutKeys = [
116
+ 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
117
+ 'paddingHorizontal', 'paddingVertical',
118
+ 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
119
+ 'marginHorizontal', 'marginVertical',
120
+ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
121
+ 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
122
+ 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
123
+ 'position', 'top', 'bottom', 'left', 'right',
124
+ 'gap', 'rowGap', 'columnGap',
125
+ 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth'
126
+ ];
127
+
128
+ for (const key of layoutKeys) {
129
+ if (merged[key] !== undefined) layout[key] = merged[key];
130
+ }
131
+ return Object.keys(layout).length > 0 ? layout : null;
132
+ } catch { return null; }
133
+ }
134
+
135
+ function walkFiber(fiber, depth) {
136
+ if (!fiber || depth > maxDepth) return null;
137
+
138
+ const name = getComponentName(fiber);
139
+
140
+ // Skip anonymous/internal components unless they have meaningful children
141
+ if (!name || shouldHide(name)) {
142
+ // Still traverse children
143
+ let child = fiber.child;
144
+ const children = [];
145
+ while (child) {
146
+ const childResult = walkFiber(child, depth);
147
+ if (childResult) children.push(childResult);
148
+ child = child.sibling;
149
+ }
150
+ // Return first meaningful child or null
151
+ return children.length === 1 ? children[0] : (children.length > 1 ? { component: '(Fragment)', children } : null);
152
+ }
153
+
154
+ const node = { component: name };
155
+
156
+ // Include props if requested (excluding children and style for cleaner output)
157
+ if (includeProps && fiber.memoizedProps) {
158
+ const props = {};
159
+ for (const key of Object.keys(fiber.memoizedProps)) {
160
+ if (key === 'children' || key === 'style') continue;
161
+ try {
162
+ const val = fiber.memoizedProps[key];
163
+ if (typeof val === 'function') {
164
+ props[key] = '[Function]';
165
+ } else if (typeof val === 'object' && val !== null) {
166
+ props[key] = Array.isArray(val) ? '[Array]' : '[Object]';
167
+ } else {
168
+ props[key] = val;
169
+ }
170
+ } catch {
171
+ props[key] = '[Animated Value]';
172
+ }
173
+ }
174
+ if (Object.keys(props).length > 0) node.props = props;
175
+ }
176
+
177
+ // Include layout styles if requested
178
+ try {
179
+ if (includeStyles && fiber.memoizedProps?.style) {
180
+ const layout = extractLayoutStyles(fiber.memoizedProps.style);
181
+ if (layout) node.layout = layout;
182
+ }
183
+ } catch { /* animated style — skip */ }
184
+
185
+ // Traverse children
186
+ let child = fiber.child;
187
+ const children = [];
188
+ while (child) {
189
+ const childResult = walkFiber(child, depth + 1);
190
+ if (childResult) children.push(childResult);
191
+ child = child.sibling;
192
+ }
193
+ if (children.length > 0) node.children = children;
194
+
195
+ return node;
196
+ }
197
+
198
+ // Find focused screen if requested
199
+ function findFocusedScreen(fiber, depth = 0) {
200
+ if (!fiber || depth > 5000) return null;
201
+
202
+ const name = getComponentName(fiber);
203
+
204
+ // Skip overlays (BottomSheet, Modal, Toast, etc.) - don't traverse into them
205
+ if (name && overlayPatterns.test(name)) {
206
+ return null;
207
+ }
208
+
209
+ // Skip navigation containers - screens inside are nav screens, not focused content
210
+ if (name && navContainerPatterns.test(name)) {
211
+ return null;
212
+ }
213
+
214
+ // Check if this is a user's screen component (not framework internals)
215
+ if (name && screenPatterns.test(name) && !wrapperPatterns.test(name) && !internalScreenPatterns.test(name)) {
216
+ return fiber;
217
+ }
218
+
219
+ // Search children
220
+ let child = fiber.child;
221
+ while (child) {
222
+ const found = findFocusedScreen(child, depth + 1);
223
+ if (found) return found;
224
+ child = child.sibling;
225
+ }
226
+
227
+ return null;
228
+ }
229
+
230
+ let startFiber = roots[0].current;
231
+ let focusedScreenName = null;
232
+
233
+ if (focusedOnly) {
234
+ const focused = findFocusedScreen(roots[0].current);
235
+ if (focused) {
236
+ startFiber = focused;
237
+ focusedScreenName = getComponentName(focused);
238
+ }
239
+ }
240
+
241
+ const tree = walkFiber(startFiber, 0);
242
+
243
+ if (focusedOnly && focusedScreenName) {
244
+ return { focusedScreen: focusedScreenName, tree };
245
+ }
246
+ return { tree };
247
+ })()
248
+ `;
249
+ // Use a longer timeout for component tree traversal — large apps can exceed 10s
250
+ const result = await executeInApp(expression, false, { timeoutMs: 30000 }, device);
251
+ // Apply formatting if requested
252
+ if (result.success && result.result) {
253
+ try {
254
+ const parsed = JSON.parse(result.result);
255
+ if (parsed.tree) {
256
+ const prefix = parsed.focusedScreen ? `Focused: ${parsed.focusedScreen}\n\n` : "";
257
+ // Structure-only mode: ultra-compact format with just component names
258
+ if (structureOnly) {
259
+ const structure = formatTreeStructureOnly(parsed.tree);
260
+ return { success: true, result: prefix + structure };
261
+ }
262
+ // TONL format: compact with props/layout
263
+ if (format === "tonl") {
264
+ const tonl = formatTreeToTonl(parsed.tree);
265
+ return { success: true, result: prefix + tonl };
266
+ }
267
+ }
268
+ }
269
+ catch {
270
+ // If parsing fails, return original result
271
+ }
272
+ }
273
+ return result;
274
+ }
275
+ //# sourceMappingURL=componentTree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"componentTree.js","sourceRoot":"","sources":["../../src/core/componentTree.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,7 @@ export declare function clearReconnectionSuppression(): void;
23
23
  * Tries with Origin first, falls back to without on quick failure.
24
24
  */
25
25
  export declare function createWebSocketWithOriginFallback(url: string, timeoutMs?: number): Promise<WebSocket>;
26
+ export declare function getWebSocketStateName(state: number): string;
26
27
  /**
27
28
  * Purge stale connections for scanned ports.
28
29
  * When Metro restarts with a different app, /json returns new device IDs.
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/core/connection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/core/connection.ts"],"names":[],"mappings":"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"}
@@ -167,7 +167,7 @@ function findAppKeyForDevice(device) {
167
167
  return null;
168
168
  }
169
169
  // Helper to convert WebSocket readyState to readable name
170
- function getWebSocketStateName(state) {
170
+ export function getWebSocketStateName(state) {
171
171
  switch (state) {
172
172
  case WebSocket.CONNECTING: return "CONNECTING";
173
173
  case WebSocket.OPEN: return "OPEN";
@@ -1132,8 +1132,11 @@ export function getConnectedAppByDevice(device) {
1132
1132
  const names = matches.map(a => a.deviceInfo.deviceName || a.deviceInfo.title).join(", ");
1133
1133
  throw new UserInputError(`Multiple devices match "${device}": ${names}. Be more specific (use the full device name for an exact match).`);
1134
1134
  }
1135
- const available = allOpenNames.length > 0 ? ` Available: ${allOpenNames.join(", ")}.` : "";
1136
- throw new UserInputError(`No connected device matches "${device}".${available} Run get_apps to see available devices.`);
1135
+ if (allOpenNames.length === 0) {
1136
+ throw new UserInputError(`No connected device matches "${device}". No devices are currently connected — run scan_metro to discover and connect to Metro servers.`);
1137
+ }
1138
+ const quoted = allOpenNames.map(n => `"${n}"`).join(", ");
1139
+ throw new UserInputError(`No connected device matches "${device}". Connected devices: ${quoted}. Retry with one of these names (substring match, case-insensitive).`);
1137
1140
  }
1138
1141
  // Check if any app is connected with an OPEN WebSocket
1139
1142
  export function hasConnectedApp() {
@@ -1309,30 +1312,32 @@ async function findFirstMetroPort() {
1309
1312
  */
1310
1313
  export async function ensureConnection(options = {}) {
1311
1314
  const { port, healthCheck = true, forceRefresh = false } = options;
1312
- let app = getFirstConnectedApp();
1313
1315
  let wasReconnected = false;
1314
- // Force refresh if requested - close existing connection
1315
- if (forceRefresh && app) {
1316
- const appKey = `${app.port}-${app.deviceInfo.id}`;
1317
- cancelReconnectionTimer(appKey);
1318
- try {
1319
- app.ws.close();
1320
- }
1321
- catch {
1322
- // Ignore close errors
1316
+ // forceRefresh: close every currently-connected app, then fall through to
1317
+ // single-device fresh connect. Full multi-device re-discovery is scan_metro's
1318
+ // job — ensureConnection only fresh-connects one when nothing is open.
1319
+ if (forceRefresh) {
1320
+ for (const { key, app } of getConnectedApps()) {
1321
+ cancelReconnectionTimer(key);
1322
+ try {
1323
+ app.ws.close();
1324
+ }
1325
+ catch { /* ignore */ }
1326
+ connectedApps.delete(key);
1323
1327
  }
1324
- connectedApps.delete(appKey);
1325
- app = null;
1326
1328
  }
1327
- // Attempt connection if not connected
1328
- if (!app) {
1329
+ let openApps = getConnectedApps()
1330
+ .filter(a => a.isConnected)
1331
+ .map(a => a.app);
1332
+ // Nothing connected — establish a single-device connection from scratch.
1333
+ if (openApps.length === 0) {
1329
1334
  const targetPort = port ?? await findFirstMetroPort();
1330
1335
  if (!targetPort) {
1331
1336
  return {
1332
1337
  connected: false,
1333
1338
  wasReconnected: false,
1334
1339
  healthCheckPassed: false,
1335
- connectionInfo: null,
1340
+ connectionInfos: [],
1336
1341
  error: "No Metro server found. Make sure Metro bundler is running.",
1337
1342
  };
1338
1343
  }
@@ -1343,7 +1348,7 @@ export async function ensureConnection(options = {}) {
1343
1348
  connected: false,
1344
1349
  wasReconnected: false,
1345
1350
  healthCheckPassed: false,
1346
- connectionInfo: null,
1351
+ connectionInfos: [],
1347
1352
  error: `No debuggable devices found on port ${targetPort}. Make sure the app is running.`,
1348
1353
  };
1349
1354
  }
@@ -1355,32 +1360,23 @@ export async function ensureConnection(options = {}) {
1355
1360
  clearReconnectionSuppression();
1356
1361
  const connectResult = await connectToDevice(mainDevice, targetPort);
1357
1362
  wasReconnected = true;
1358
- // connectToDevice resolves with a status string for non-throw
1359
- // outcomes including "Skipped X (stale CDP target …)". Surface
1360
- // that explicitly instead of falling through to the generic
1361
- // "Connection succeeded but app is not available" below.
1362
1363
  if (connectResult.includes("stale CDP target")) {
1363
1364
  return {
1364
1365
  connected: false,
1365
1366
  wasReconnected: false,
1366
1367
  healthCheckPassed: false,
1367
- connectionInfo: null,
1368
+ connectionInfos: [],
1368
1369
  error: `${connectResult}. The CDP page advertised by Metro is no longer responsive — restart the React Native app, then retry.`,
1369
1370
  };
1370
1371
  }
1371
- // Race window: connectToDevice may resolve with the WS open and
1372
- // connectedApps populated, but Metro can immediately close the
1373
- // socket (single-connection limit, transient hiccup). The close
1374
- // handler then deletes the entry and getFirstConnectedApp returns
1375
- // null. Settle briefly and retry a few times before giving up.
1376
- app = getFirstConnectedApp();
1377
- for (let i = 0; !app && i < 5; i++) {
1378
- await new Promise(resolve => setTimeout(resolve, 100));
1379
- app = getFirstConnectedApp();
1372
+ // Race window: settle briefly and retry getting the open app list.
1373
+ for (let i = 0; i < 5 && openApps.length === 0; i++) {
1374
+ if (i > 0)
1375
+ await new Promise(resolve => setTimeout(resolve, 100));
1376
+ openApps = getConnectedApps().filter(a => a.isConnected).map(a => a.app);
1380
1377
  }
1381
1378
  }
1382
1379
  catch (error) {
1383
- // Ensure we always have a meaningful error message
1384
1380
  let errorMessage;
1385
1381
  if (error instanceof Error) {
1386
1382
  errorMessage = error.message;
@@ -1395,82 +1391,95 @@ export async function ensureConnection(options = {}) {
1395
1391
  connected: false,
1396
1392
  wasReconnected: false,
1397
1393
  healthCheckPassed: false,
1398
- connectionInfo: null,
1394
+ connectionInfos: [],
1399
1395
  error: `Connection failed: ${errorMessage}`,
1400
1396
  };
1401
1397
  }
1402
1398
  }
1403
- if (!app) {
1399
+ if (openApps.length === 0) {
1404
1400
  return {
1405
1401
  connected: false,
1406
1402
  wasReconnected: false,
1407
1403
  healthCheckPassed: false,
1408
- connectionInfo: null,
1404
+ connectionInfos: [],
1409
1405
  error: "Connection succeeded but app is not available",
1410
1406
  };
1411
1407
  }
1412
- // Run health check if requested
1413
- let healthCheckPassed = true;
1414
- if (healthCheck) {
1415
- healthCheckPassed = await runQuickHealthCheck(app);
1416
- // If health check failed and we haven't just reconnected, try reconnecting
1417
- if (!healthCheckPassed && !wasReconnected) {
1418
- console.error(`[execbro] Health check failed, attempting reconnection...`);
1419
- // Close and reconnect
1420
- const appKey = `${app.port}-${app.deviceInfo.id}`;
1421
- const targetPort = app.port;
1422
- cancelReconnectionTimer(appKey);
1423
- try {
1424
- app.ws.close();
1425
- }
1426
- catch {
1427
- // Ignore
1428
- }
1429
- connectedApps.delete(appKey);
1430
- // Re-fetch devices and reconnect
1431
- const devices = await fetchDevices(targetPort);
1432
- const mainDevice = selectMainDevice(devices);
1433
- if (mainDevice) {
1408
+ // Per-device health-check; reconnect failed devices individually so a dead
1409
+ // Android doesn't take down a healthy iOS report and vice versa.
1410
+ const perApp = [];
1411
+ for (const candidate of openApps) {
1412
+ let app = candidate;
1413
+ let healthy = true;
1414
+ if (healthCheck) {
1415
+ healthy = await runQuickHealthCheck(app);
1416
+ if (!healthy && !wasReconnected) {
1417
+ const failedTitle = app.deviceInfo.title;
1418
+ console.error(`[execbro] Health check failed for ${failedTitle}, attempting reconnection...`);
1419
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
1420
+ const appPort = app.port;
1421
+ cancelReconnectionTimer(appKey);
1434
1422
  try {
1435
- await connectToDevice(mainDevice, targetPort);
1436
- app = getFirstConnectedApp();
1437
- wasReconnected = true;
1438
- // Re-run health check after reconnection
1439
- if (app) {
1440
- healthCheckPassed = await runQuickHealthCheck(app);
1423
+ app.ws.close();
1424
+ }
1425
+ catch { /* ignore */ }
1426
+ connectedApps.delete(appKey);
1427
+ const devices = await fetchDevices(appPort);
1428
+ const mainDevice = selectMainDevice(devices);
1429
+ if (mainDevice) {
1430
+ try {
1431
+ await connectToDevice(mainDevice, appPort);
1432
+ wasReconnected = true;
1433
+ const fresh = getConnectedApps()
1434
+ .filter(a => a.isConnected)
1435
+ .map(a => a.app);
1436
+ const replacement = fresh.find(a => a.deviceInfo.id === mainDevice.id);
1437
+ if (replacement) {
1438
+ app = replacement;
1439
+ healthy = await runQuickHealthCheck(app);
1440
+ }
1441
+ else {
1442
+ healthy = false;
1443
+ }
1444
+ }
1445
+ catch {
1446
+ healthy = false;
1441
1447
  }
1442
1448
  }
1443
- catch {
1444
- // Failed to reconnect
1445
- healthCheckPassed = false;
1449
+ else {
1450
+ healthy = false;
1446
1451
  }
1447
1452
  }
1448
1453
  }
1454
+ perApp.push({ app, healthy });
1449
1455
  }
1450
- // Build connection info
1451
- const appKey = app ? `${app.port}-${app.deviceInfo.id}` : null;
1452
- const connectionState = appKey ? getConnectionState(appKey) : null;
1453
- const contextHealth = appKey ? getContextHealth(appKey) : null;
1454
- let uptime = "unknown";
1455
- if (connectionState?.lastConnectedTime) {
1456
- const uptimeMs = Date.now() - connectionState.lastConnectedTime.getTime();
1457
- uptime = formatDuration(uptimeMs);
1458
- }
1459
- const isConnected = app !== null && app.ws.readyState === WebSocket.OPEN;
1460
- return {
1461
- connected: isConnected,
1462
- wasReconnected,
1463
- healthCheckPassed,
1464
- connectionInfo: app ? {
1456
+ const connectionInfos = perApp.map(({ app, healthy }) => {
1457
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
1458
+ const connectionState = getConnectionState(appKey);
1459
+ const contextHealth = getContextHealth(appKey);
1460
+ let uptime = "unknown";
1461
+ if (connectionState?.lastConnectedTime) {
1462
+ uptime = formatDuration(Date.now() - connectionState.lastConnectedTime.getTime());
1463
+ }
1464
+ return {
1465
+ deviceName: app.deviceInfo.deviceName || app.deviceInfo.title,
1465
1466
  deviceTitle: app.deviceInfo.title,
1467
+ platform: app.platform,
1466
1468
  port: app.port,
1467
1469
  uptime,
1468
1470
  contextId: contextHealth?.contextId ?? null,
1469
- } : null,
1470
- ...(!isConnected && {
1471
- error: app
1472
- ? `Connection lost: WebSocket is in ${["CONNECTING", "OPEN", "CLOSING", "CLOSED"][app.ws.readyState] || "unknown"} state (expected OPEN). Health check ${healthCheckPassed ? "passed" : "failed"}. Try ensure_connection with forceRefresh=true.`
1473
- : "App became unavailable after reconnection attempt. Try running scan_metro then ensure_connection."
1471
+ healthCheckPassed: healthy,
1472
+ };
1473
+ });
1474
+ const anyOpen = perApp.some(r => r.app.ws.readyState === WebSocket.OPEN);
1475
+ const allHealthy = perApp.every(r => r.healthy);
1476
+ return {
1477
+ connected: anyOpen,
1478
+ wasReconnected,
1479
+ healthCheckPassed: allHealthy,
1480
+ connectionInfos,
1481
+ ...(!anyOpen && {
1482
+ error: "App became unavailable after reconnection attempt. Try running scan_metro then ensure_connection."
1474
1483
  }),
1475
1484
  };
1476
1485
  }