react-native-ai-debugger 1.1.6 → 1.4.33

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 (168) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +386 -45
  3. package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
  4. package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
  5. package/build/__tests__/helpers/tap-test-helpers.js +130 -0
  6. package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
  7. package/build/__tests__/integration/connection-health.test.js +12 -6
  8. package/build/__tests__/integration/connection-health.test.js.map +1 -1
  9. package/build/__tests__/integration/execute-in-app.test.js +2 -1
  10. package/build/__tests__/integration/execute-in-app.test.js.map +1 -1
  11. package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
  12. package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/tap-strategies.test.js +329 -0
  14. package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
  15. package/build/__tests__/integration/tools.test.js +9 -10
  16. package/build/__tests__/integration/tools.test.js.map +1 -1
  17. package/build/__tests__/unit/burst.test.d.ts +2 -0
  18. package/build/__tests__/unit/burst.test.d.ts.map +1 -0
  19. package/build/__tests__/unit/burst.test.js +50 -0
  20. package/build/__tests__/unit/burst.test.js.map +1 -0
  21. package/build/__tests__/unit/connection-health.test.js +26 -16
  22. package/build/__tests__/unit/connection-health.test.js.map +1 -1
  23. package/build/__tests__/unit/fiberMatch.test.d.ts +2 -0
  24. package/build/__tests__/unit/fiberMatch.test.d.ts.map +1 -0
  25. package/build/__tests__/unit/fiberMatch.test.js +114 -0
  26. package/build/__tests__/unit/fiberMatch.test.js.map +1 -0
  27. package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
  28. package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
  29. package/build/__tests__/unit/fingerprint.test.js +89 -0
  30. package/build/__tests__/unit/fingerprint.test.js.map +1 -0
  31. package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
  32. package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
  33. package/build/__tests__/unit/imageBuffer.test.js +128 -0
  34. package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
  35. package/build/__tests__/unit/logs.test.js +9 -0
  36. package/build/__tests__/unit/logs.test.js.map +1 -1
  37. package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
  38. package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
  39. package/build/__tests__/unit/networkInterceptor.test.js +104 -0
  40. package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
  41. package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
  42. package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
  43. package/build/__tests__/unit/screenshot-diff.test.js +97 -0
  44. package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
  45. package/build/__tests__/unit/tap.test.js +542 -25
  46. package/build/__tests__/unit/tap.test.js.map +1 -1
  47. package/build/core/android.d.ts.map +1 -1
  48. package/build/core/android.js +104 -113
  49. package/build/core/android.js.map +1 -1
  50. package/build/core/appDetection.d.ts +8 -0
  51. package/build/core/appDetection.d.ts.map +1 -0
  52. package/build/core/appDetection.js +115 -0
  53. package/build/core/appDetection.js.map +1 -0
  54. package/build/core/bundle.d.ts.map +1 -1
  55. package/build/core/bundle.js +15 -23
  56. package/build/core/bundle.js.map +1 -1
  57. package/build/core/config.d.ts +8 -0
  58. package/build/core/config.d.ts.map +1 -0
  59. package/build/core/config.js +27 -0
  60. package/build/core/config.js.map +1 -0
  61. package/build/core/connection.d.ts +47 -3
  62. package/build/core/connection.d.ts.map +1 -1
  63. package/build/core/connection.js +519 -124
  64. package/build/core/connection.js.map +1 -1
  65. package/build/core/executor.d.ts +82 -14
  66. package/build/core/executor.d.ts.map +1 -1
  67. package/build/core/executor.js +1869 -418
  68. package/build/core/executor.js.map +1 -1
  69. package/build/core/feedback.d.ts +18 -0
  70. package/build/core/feedback.d.ts.map +1 -0
  71. package/build/core/feedback.js +73 -0
  72. package/build/core/feedback.js.map +1 -0
  73. package/build/core/fiberMatch.d.ts +25 -0
  74. package/build/core/fiberMatch.d.ts.map +1 -0
  75. package/build/core/fiberMatch.js +35 -0
  76. package/build/core/fiberMatch.js.map +1 -0
  77. package/build/core/fingerprint.d.ts +4 -0
  78. package/build/core/fingerprint.d.ts.map +1 -0
  79. package/build/core/fingerprint.js +47 -0
  80. package/build/core/fingerprint.js.map +1 -0
  81. package/build/core/guides.d.ts.map +1 -1
  82. package/build/core/guides.js +66 -3
  83. package/build/core/guides.js.map +1 -1
  84. package/build/core/imageBuffer.d.ts +37 -0
  85. package/build/core/imageBuffer.d.ts.map +1 -0
  86. package/build/core/imageBuffer.js +58 -0
  87. package/build/core/imageBuffer.js.map +1 -0
  88. package/build/core/index.d.ts +22 -17
  89. package/build/core/index.d.ts.map +1 -1
  90. package/build/core/index.js +23 -19
  91. package/build/core/index.js.map +1 -1
  92. package/build/core/ios.d.ts +36 -7
  93. package/build/core/ios.d.ts.map +1 -1
  94. package/build/core/ios.js +561 -326
  95. package/build/core/ios.js.map +1 -1
  96. package/build/core/license.d.ts +38 -0
  97. package/build/core/license.d.ts.map +1 -0
  98. package/build/core/license.js +243 -0
  99. package/build/core/license.js.map +1 -0
  100. package/build/core/logbox.d.ts +57 -0
  101. package/build/core/logbox.d.ts.map +1 -0
  102. package/build/core/logbox.js +282 -0
  103. package/build/core/logbox.js.map +1 -0
  104. package/build/core/logs.d.ts +1 -0
  105. package/build/core/logs.d.ts.map +1 -1
  106. package/build/core/logs.js +5 -0
  107. package/build/core/logs.js.map +1 -1
  108. package/build/core/metro.d.ts +8 -0
  109. package/build/core/metro.d.ts.map +1 -1
  110. package/build/core/metro.js +28 -0
  111. package/build/core/metro.js.map +1 -1
  112. package/build/core/networkInterceptor.d.ts +29 -2
  113. package/build/core/networkInterceptor.d.ts.map +1 -1
  114. package/build/core/networkInterceptor.js +195 -191
  115. package/build/core/networkInterceptor.js.map +1 -1
  116. package/build/core/ocr.d.ts +8 -3
  117. package/build/core/ocr.d.ts.map +1 -1
  118. package/build/core/ocr.js +116 -15
  119. package/build/core/ocr.js.map +1 -1
  120. package/build/core/posthog.d.ts +5 -0
  121. package/build/core/posthog.d.ts.map +1 -0
  122. package/build/core/posthog.js +46 -0
  123. package/build/core/posthog.js.map +1 -0
  124. package/build/core/sdkBridge.d.ts +67 -0
  125. package/build/core/sdkBridge.d.ts.map +1 -0
  126. package/build/core/sdkBridge.js +148 -0
  127. package/build/core/sdkBridge.js.map +1 -0
  128. package/build/core/state.d.ts +25 -5
  129. package/build/core/state.d.ts.map +1 -1
  130. package/build/core/state.js +79 -10
  131. package/build/core/state.js.map +1 -1
  132. package/build/core/telemetry.d.ts +21 -1
  133. package/build/core/telemetry.d.ts.map +1 -1
  134. package/build/core/telemetry.js +166 -10
  135. package/build/core/telemetry.js.map +1 -1
  136. package/build/core/types.d.ts +11 -0
  137. package/build/core/types.d.ts.map +1 -1
  138. package/build/index.js +1490 -324
  139. package/build/index.js.map +1 -1
  140. package/build/pro/screenshot-diff.d.ts +10 -0
  141. package/build/pro/screenshot-diff.d.ts.map +1 -0
  142. package/build/pro/screenshot-diff.js +38 -0
  143. package/build/pro/screenshot-diff.js.map +1 -0
  144. package/build/pro/tap.d.ts +84 -2
  145. package/build/pro/tap.d.ts.map +1 -1
  146. package/build/pro/tap.js +835 -129
  147. package/build/pro/tap.js.map +1 -1
  148. package/build/tools/accountTools.d.ts +44 -0
  149. package/build/tools/accountTools.d.ts.map +1 -0
  150. package/build/tools/accountTools.js +217 -0
  151. package/build/tools/accountTools.js.map +1 -0
  152. package/package.json +8 -7
  153. package/build/core/httpServer.d.ts +0 -14
  154. package/build/core/httpServer.d.ts.map +0 -1
  155. package/build/core/httpServer.js +0 -2459
  156. package/build/core/httpServer.js.map +0 -1
  157. package/build/core/httpServerProcess.d.ts +0 -25
  158. package/build/core/httpServerProcess.d.ts.map +0 -1
  159. package/build/core/httpServerProcess.js +0 -153
  160. package/build/core/httpServerProcess.js.map +0 -1
  161. package/build/core/tap.d.ts +0 -91
  162. package/build/core/tap.d.ts.map +0 -1
  163. package/build/core/tap.js +0 -542
  164. package/build/core/tap.js.map +0 -1
  165. package/build/httpServerStandalone.d.ts +0 -7
  166. package/build/httpServerStandalone.d.ts.map +0 -1
  167. package/build/httpServerStandalone.js +0 -31
  168. package/build/httpServerStandalone.js.map +0 -1
package/build/core/ios.js CHANGED
@@ -5,14 +5,23 @@ import path from "path";
5
5
  import os from "os";
6
6
  import sharp from "sharp";
7
7
  import { getActiveSimulatorUdid } from "./state.js";
8
+ import { notifyDriverMissing } from "./logbox.js";
8
9
  const execAsync = promisify(exec);
9
10
  const execFileAsync = promisify(execFile);
10
11
  // simctl command timeout in milliseconds
11
12
  const SIMCTL_TIMEOUT = 30000;
12
13
  // IDB command timeout in milliseconds
13
14
  const IDB_TIMEOUT = 30000;
15
+ // AXe command timeout in milliseconds
16
+ const AXE_TIMEOUT = 30000;
14
17
  // Valid button types for IDB ui button command
15
- export const IOS_BUTTON_TYPES = ["HOME", "LOCK", "SIDE_BUTTON", "SIRI", "APPLE_PAY"];
18
+ export const IOS_BUTTON_TYPES = [
19
+ "HOME",
20
+ "LOCK",
21
+ "SIDE_BUTTON",
22
+ "SIRI",
23
+ "APPLE_PAY",
24
+ ];
16
25
  // Track connected IDB simulators to avoid redundant connect calls
17
26
  const connectedIdbSimulators = new Set();
18
27
  /**
@@ -22,6 +31,36 @@ const connectedIdbSimulators = new Set();
22
31
  function getIdbPath() {
23
32
  return process.env.IDB_PATH || "idb";
24
33
  }
34
+ /**
35
+ * Get the active iOS UI driver.
36
+ * Set IOS_DRIVER=axe in MCP server env to use AXe CLI instead of IDB.
37
+ */
38
+ function getIosDriver() {
39
+ const driver = process.env.IOS_DRIVER?.toLowerCase();
40
+ if (driver === "axe")
41
+ return "axe";
42
+ return "idb";
43
+ }
44
+ /**
45
+ * Get the AXe executable path.
46
+ * Supports AXE_PATH environment variable for custom installations.
47
+ */
48
+ function getAxePath() {
49
+ return process.env.AXE_PATH || "axe";
50
+ }
51
+ /**
52
+ * Run AXe command with execFile (no shell) for proper argument handling.
53
+ */
54
+ async function runAxe(...args) {
55
+ const axePath = getAxePath();
56
+ const { stdout, stderr } = await execFileAsync(axePath, args, {
57
+ timeout: AXE_TIMEOUT,
58
+ });
59
+ return {
60
+ stdout: stdout.trim(),
61
+ stderr: stderr.trim(),
62
+ };
63
+ }
25
64
  /**
26
65
  * Run IDB command with execFile (no shell) for proper argument handling
27
66
  * This matches the original ios-simulator-mcp implementation
@@ -29,11 +68,11 @@ function getIdbPath() {
29
68
  async function runIdb(...args) {
30
69
  const idbPath = getIdbPath();
31
70
  const { stdout, stderr } = await execFileAsync(idbPath, args, {
32
- timeout: IDB_TIMEOUT
71
+ timeout: IDB_TIMEOUT,
33
72
  });
34
73
  return {
35
74
  stdout: stdout.trim(),
36
- stderr: stderr.trim()
75
+ stderr: stderr.trim(),
37
76
  };
38
77
  }
39
78
  /**
@@ -48,6 +87,49 @@ export async function isIdbAvailable() {
48
87
  return false;
49
88
  }
50
89
  }
90
+ /**
91
+ * Check if AXe CLI is available
92
+ */
93
+ export async function isAxeAvailable() {
94
+ try {
95
+ await runAxe("--version");
96
+ return true;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ /**
103
+ * Check if the active iOS UI driver is available.
104
+ * Used by tap.ts for early fail-fast checks.
105
+ */
106
+ export async function isUiDriverAvailable() {
107
+ const driver = getIosDriver();
108
+ return driver === "axe" ? isAxeAvailable() : isIdbAvailable();
109
+ }
110
+ /**
111
+ * Get install instructions for the active iOS UI driver.
112
+ */
113
+ export function getUiDriverInstallHint() {
114
+ const driver = getIosDriver();
115
+ if (driver === "axe") {
116
+ return "AXe is not installed. Install with: brew install cameroncooke/axe/axe\nAXe is the recommended iOS UI driver. See: https://github.com/cameroncooke/AXe";
117
+ }
118
+ return "IDB is not installed. Install with: brew install idb-companion\nTip: Consider switching to AXe (recommended) — brew install cameroncooke/axe/axe and set IOS_DRIVER=axe";
119
+ }
120
+ /**
121
+ * Map IDB button names (uppercase/underscores) to AXe button names (lowercase/hyphens).
122
+ */
123
+ function toAxeButtonName(idbButton) {
124
+ const map = {
125
+ HOME: "home",
126
+ LOCK: "lock",
127
+ SIDE_BUTTON: "side-button",
128
+ SIRI: "siri",
129
+ APPLE_PAY: "apple-pay",
130
+ };
131
+ return map[idbButton];
132
+ }
51
133
  /**
52
134
  * Ensure IDB is connected to the specified simulator
53
135
  * IDB requires `idb connect <UDID>` before any UI commands work
@@ -65,15 +147,63 @@ async function ensureIdbConnected(udid) {
65
147
  catch (error) {
66
148
  const errorMessage = error instanceof Error ? error.message : String(error);
67
149
  // "Already connected" is not an error
68
- if (errorMessage.includes("already connected") || errorMessage.includes("Connected")) {
150
+ if (errorMessage.includes("already connected") ||
151
+ errorMessage.includes("Connected")) {
69
152
  connectedIdbSimulators.add(udid);
70
153
  return { success: true };
71
154
  }
72
155
  return {
73
156
  success: false,
74
- error: `Failed to connect IDB to simulator: ${errorMessage}`
157
+ error: `Failed to connect IDB to simulator: ${errorMessage}`,
158
+ };
159
+ }
160
+ }
161
+ /**
162
+ * Shared pre-flight check for all UI driver functions:
163
+ * 1. Check driver availability
164
+ * 2. Resolve target simulator UDID
165
+ * 3. Ensure IDB connection (IDB only)
166
+ */
167
+ async function ensureUiDriverReady(udid) {
168
+ const driver = getIosDriver();
169
+ if (driver === "axe") {
170
+ if (!(await isAxeAvailable())) {
171
+ notifyDriverMissing("ios");
172
+ return {
173
+ ready: false,
174
+ error: { success: false, error: getUiDriverInstallHint() },
175
+ };
176
+ }
177
+ }
178
+ else {
179
+ if (!(await isIdbAvailable())) {
180
+ notifyDriverMissing("ios");
181
+ return {
182
+ ready: false,
183
+ error: { success: false, error: getUiDriverInstallHint() },
184
+ };
185
+ }
186
+ }
187
+ const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
188
+ if (!targetUdid) {
189
+ return {
190
+ ready: false,
191
+ error: {
192
+ success: false,
193
+ error: "No iOS simulator is currently running. Start a simulator first.",
194
+ },
75
195
  };
76
196
  }
197
+ if (driver === "idb") {
198
+ const connectResult = await ensureIdbConnected(targetUdid);
199
+ if (!connectResult.success) {
200
+ return {
201
+ ready: false,
202
+ error: { success: false, error: connectResult.error },
203
+ };
204
+ }
205
+ }
206
+ return { ready: true, driver, targetUdid };
77
207
  }
78
208
  /**
79
209
  * Check if simctl is available
@@ -96,11 +226,11 @@ export async function listIOSSimulators(onlyBooted = false) {
96
226
  if (!simctlAvailable) {
97
227
  return {
98
228
  success: false,
99
- error: "Xcode command line tools not available. Install Xcode from the App Store."
229
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
100
230
  };
101
231
  }
102
232
  const { stdout } = await execAsync("xcrun simctl list devices -j", {
103
- timeout: SIMCTL_TIMEOUT
233
+ timeout: SIMCTL_TIMEOUT,
104
234
  });
105
235
  const data = JSON.parse(stdout);
106
236
  const simulators = [];
@@ -115,14 +245,16 @@ export async function listIOSSimulators(onlyBooted = false) {
115
245
  continue;
116
246
  // Extract iOS version from runtime string
117
247
  const runtimeMatch = runtime.match(/iOS[- ](\d+[.-]\d+)/i);
118
- const runtimeVersion = runtimeMatch ? `iOS ${runtimeMatch[1].replace("-", ".")}` : runtime;
248
+ const runtimeVersion = runtimeMatch
249
+ ? `iOS ${runtimeMatch[1].replace("-", ".")}`
250
+ : runtime;
119
251
  simulators.push({
120
252
  udid: device.udid,
121
253
  name: device.name,
122
254
  state: device.state,
123
255
  runtime: runtimeVersion,
124
256
  deviceType: device.deviceTypeIdentifier,
125
- isAvailable: device.isAvailable
257
+ isAvailable: device.isAvailable,
126
258
  });
127
259
  }
128
260
  }
@@ -132,7 +264,7 @@ export async function listIOSSimulators(onlyBooted = false) {
132
264
  result: onlyBooted
133
265
  ? "No booted iOS simulators. Start a simulator first."
134
266
  : "No available iOS simulators found.",
135
- simulators: []
267
+ simulators: [],
136
268
  };
137
269
  }
138
270
  // Sort: Booted first, then by name
@@ -152,13 +284,13 @@ export async function listIOSSimulators(onlyBooted = false) {
152
284
  return {
153
285
  success: true,
154
286
  result: `iOS Simulators:\n\n${formatted}`,
155
- simulators
287
+ simulators,
156
288
  };
157
289
  }
158
290
  catch (error) {
159
291
  return {
160
292
  success: false,
161
- error: `Failed to list simulators: ${error instanceof Error ? error.message : String(error)}`
293
+ error: `Failed to list simulators: ${error instanceof Error ? error.message : String(error)}`,
162
294
  };
163
295
  }
164
296
  }
@@ -168,7 +300,7 @@ export async function listIOSSimulators(onlyBooted = false) {
168
300
  export async function getBootedSimulatorUdid() {
169
301
  try {
170
302
  const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
171
- timeout: SIMCTL_TIMEOUT
303
+ timeout: SIMCTL_TIMEOUT,
172
304
  });
173
305
  const data = JSON.parse(stdout);
174
306
  for (const devices of Object.values(data.devices)) {
@@ -193,7 +325,7 @@ export async function getBootedSimulatorUdid() {
193
325
  export async function findSimulatorByName(deviceName) {
194
326
  try {
195
327
  const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
196
- timeout: SIMCTL_TIMEOUT
328
+ timeout: SIMCTL_TIMEOUT,
197
329
  });
198
330
  const data = JSON.parse(stdout);
199
331
  const normalizedDeviceName = deviceName.toLowerCase().trim();
@@ -249,7 +381,7 @@ export async function iosScreenshot(outputPath, udid) {
249
381
  if (!simctlAvailable) {
250
382
  return {
251
383
  success: false,
252
- error: "Xcode command line tools not available. Install Xcode from the App Store."
384
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
253
385
  };
254
386
  }
255
387
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -257,14 +389,14 @@ export async function iosScreenshot(outputPath, udid) {
257
389
  if (!targetUdid) {
258
390
  return {
259
391
  success: false,
260
- error: "No iOS simulator is currently running. Start a simulator first."
392
+ error: "No iOS simulator is currently running. Start a simulator first.",
261
393
  };
262
394
  }
263
395
  // Generate output path if not provided
264
396
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
265
397
  const finalOutputPath = outputPath || path.join(os.tmpdir(), `ios-screenshot-${timestamp}.png`);
266
398
  await execAsync(`xcrun simctl io ${targetUdid} screenshot "${finalOutputPath}"`, {
267
- timeout: SIMCTL_TIMEOUT
399
+ timeout: SIMCTL_TIMEOUT,
268
400
  });
269
401
  // Resize image if needed (API limit: 2000px max for multi-image requests)
270
402
  // Return scale factor so AI can convert image coords to device coords
@@ -281,15 +413,13 @@ export async function iosScreenshot(outputPath, udid) {
281
413
  imageData = await image
282
414
  .resize(MAX_DIMENSION, MAX_DIMENSION, {
283
415
  fit: "inside",
284
- withoutEnlargement: true
416
+ withoutEnlargement: true,
285
417
  })
286
418
  .jpeg({ quality: 85 })
287
419
  .toBuffer();
288
420
  }
289
421
  else {
290
- imageData = await image
291
- .jpeg({ quality: 85 })
292
- .toBuffer();
422
+ imageData = await image.jpeg({ quality: 85 }).toBuffer();
293
423
  }
294
424
  return {
295
425
  success: true,
@@ -297,13 +427,13 @@ export async function iosScreenshot(outputPath, udid) {
297
427
  data: imageData,
298
428
  scaleFactor,
299
429
  originalWidth,
300
- originalHeight
430
+ originalHeight,
301
431
  };
302
432
  }
303
433
  catch (error) {
304
434
  return {
305
435
  success: false,
306
- error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`
436
+ error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`,
307
437
  };
308
438
  }
309
439
  }
@@ -316,14 +446,14 @@ export async function iosInstallApp(appPath, udid) {
316
446
  if (!simctlAvailable) {
317
447
  return {
318
448
  success: false,
319
- error: "Xcode command line tools not available. Install Xcode from the App Store."
449
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
320
450
  };
321
451
  }
322
452
  // Verify app exists
323
453
  if (!existsSync(appPath)) {
324
454
  return {
325
455
  success: false,
326
- error: `App bundle not found: ${appPath}`
456
+ error: `App bundle not found: ${appPath}`,
327
457
  };
328
458
  }
329
459
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -331,21 +461,21 @@ export async function iosInstallApp(appPath, udid) {
331
461
  if (!targetUdid) {
332
462
  return {
333
463
  success: false,
334
- error: "No iOS simulator is currently running. Start a simulator first."
464
+ error: "No iOS simulator is currently running. Start a simulator first.",
335
465
  };
336
466
  }
337
467
  await execAsync(`xcrun simctl install ${targetUdid} "${appPath}"`, {
338
- timeout: 120000 // 2 minute timeout for install
468
+ timeout: 120000, // 2 minute timeout for install
339
469
  });
340
470
  return {
341
471
  success: true,
342
- result: `Successfully installed ${path.basename(appPath)}`
472
+ result: `Successfully installed ${path.basename(appPath)}`,
343
473
  };
344
474
  }
345
475
  catch (error) {
346
476
  return {
347
477
  success: false,
348
- error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}`
478
+ error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}`,
349
479
  };
350
480
  }
351
481
  }
@@ -358,7 +488,7 @@ export async function iosLaunchApp(bundleId, udid) {
358
488
  if (!simctlAvailable) {
359
489
  return {
360
490
  success: false,
361
- error: "Xcode command line tools not available. Install Xcode from the App Store."
491
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
362
492
  };
363
493
  }
364
494
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -366,21 +496,27 @@ export async function iosLaunchApp(bundleId, udid) {
366
496
  if (!targetUdid) {
367
497
  return {
368
498
  success: false,
369
- error: "No iOS simulator is currently running. Start a simulator first."
499
+ error: "No iOS simulator is currently running. Start a simulator first.",
370
500
  };
371
501
  }
372
502
  await execAsync(`xcrun simctl launch ${targetUdid} ${bundleId}`, {
373
- timeout: SIMCTL_TIMEOUT
503
+ timeout: SIMCTL_TIMEOUT,
374
504
  });
375
505
  return {
376
506
  success: true,
377
- result: `Launched ${bundleId}`
507
+ result: `Launched ${bundleId}`,
378
508
  };
379
509
  }
380
510
  catch (error) {
511
+ const msg = error instanceof Error ? error.message : String(error);
512
+ let hint = "";
513
+ if (msg.includes("FBSOpenApplicationErrorDomain")) {
514
+ hint =
515
+ "\n\nCommon causes:\n- App is not installed on this simulator (install it first with ios_install_app)\n- Bundle ID is incorrect (check with: xcrun simctl listapps booted)";
516
+ }
381
517
  return {
382
518
  success: false,
383
- error: `Failed to launch app: ${error instanceof Error ? error.message : String(error)}`
519
+ error: `Failed to launch app: ${msg}${hint}`,
384
520
  };
385
521
  }
386
522
  }
@@ -393,7 +529,7 @@ export async function iosOpenUrl(url, udid) {
393
529
  if (!simctlAvailable) {
394
530
  return {
395
531
  success: false,
396
- error: "Xcode command line tools not available. Install Xcode from the App Store."
532
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
397
533
  };
398
534
  }
399
535
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -401,21 +537,28 @@ export async function iosOpenUrl(url, udid) {
401
537
  if (!targetUdid) {
402
538
  return {
403
539
  success: false,
404
- error: "No iOS simulator is currently running. Start a simulator first."
540
+ error: "No iOS simulator is currently running. Start a simulator first.",
405
541
  };
406
542
  }
407
543
  await execAsync(`xcrun simctl openurl ${targetUdid} "${url}"`, {
408
- timeout: SIMCTL_TIMEOUT
544
+ timeout: SIMCTL_TIMEOUT,
409
545
  });
410
546
  return {
411
547
  success: true,
412
- result: `Opened URL: ${url}`
548
+ result: `Opened URL: ${url}`,
413
549
  };
414
550
  }
415
551
  catch (error) {
552
+ const msg = error instanceof Error ? error.message : String(error);
553
+ let hint = "";
554
+ if (msg.includes("LSApplicationWorkspaceError") ||
555
+ msg.includes("OpenApplicationErrorDomain")) {
556
+ hint =
557
+ "\n\nCommon causes:\n- Custom URL scheme not registered in the app's Info.plist\n- App that handles this URL scheme is not installed\n- For Expo apps, use the exp:// URL format";
558
+ }
416
559
  return {
417
560
  success: false,
418
- error: `Failed to open URL: ${error instanceof Error ? error.message : String(error)}`
561
+ error: `Failed to open URL: ${msg}${hint}`,
419
562
  };
420
563
  }
421
564
  }
@@ -428,7 +571,7 @@ export async function iosTerminateApp(bundleId, udid) {
428
571
  if (!simctlAvailable) {
429
572
  return {
430
573
  success: false,
431
- error: "Xcode command line tools not available. Install Xcode from the App Store."
574
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
432
575
  };
433
576
  }
434
577
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -436,21 +579,21 @@ export async function iosTerminateApp(bundleId, udid) {
436
579
  if (!targetUdid) {
437
580
  return {
438
581
  success: false,
439
- error: "No iOS simulator is currently running. Start a simulator first."
582
+ error: "No iOS simulator is currently running. Start a simulator first.",
440
583
  };
441
584
  }
442
585
  await execAsync(`xcrun simctl terminate ${targetUdid} ${bundleId}`, {
443
- timeout: SIMCTL_TIMEOUT
586
+ timeout: SIMCTL_TIMEOUT,
444
587
  });
445
588
  return {
446
589
  success: true,
447
- result: `Terminated ${bundleId}`
590
+ result: `Terminated ${bundleId}`,
448
591
  };
449
592
  }
450
593
  catch (error) {
451
594
  return {
452
595
  success: false,
453
- error: `Failed to terminate app: ${error instanceof Error ? error.message : String(error)}`
596
+ error: `Failed to terminate app: ${error instanceof Error ? error.message : String(error)}`,
454
597
  };
455
598
  }
456
599
  }
@@ -463,11 +606,11 @@ export async function iosBootSimulator(udid) {
463
606
  if (!simctlAvailable) {
464
607
  return {
465
608
  success: false,
466
- error: "Xcode command line tools not available. Install Xcode from the App Store."
609
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
467
610
  };
468
611
  }
469
612
  await execAsync(`xcrun simctl boot ${udid}`, {
470
- timeout: 60000 // 1 minute timeout for boot
613
+ timeout: 60000, // 1 minute timeout for boot
471
614
  });
472
615
  // Open Simulator app
473
616
  await execAsync("open -a Simulator", { timeout: 10000 }).catch(() => {
@@ -475,7 +618,7 @@ export async function iosBootSimulator(udid) {
475
618
  });
476
619
  return {
477
620
  success: true,
478
- result: `Simulator ${udid} is now booting`
621
+ result: `Simulator ${udid} is now booting`,
479
622
  };
480
623
  }
481
624
  catch (error) {
@@ -484,301 +627,277 @@ export async function iosBootSimulator(udid) {
484
627
  if (errorMessage.includes("Unable to boot device in current state: Booted")) {
485
628
  return {
486
629
  success: true,
487
- result: "Simulator is already booted"
630
+ result: "Simulator is already booted",
488
631
  };
489
632
  }
490
633
  return {
491
634
  success: false,
492
- error: `Failed to boot simulator: ${errorMessage}`
635
+ error: `Failed to boot simulator: ${errorMessage}`,
493
636
  };
494
637
  }
495
638
  }
496
639
  // ============================================================================
497
- // IDB-Based UI Interaction Tools
498
- // These tools require Facebook IDB (iOS Development Bridge) to be installed
499
- // Install with: brew install idb-companion
640
+ // iOS UI Interaction Tools
641
+ // These tools require an iOS UI driver to be installed:
642
+ // Recommended: AXe — brew install cameroncooke/axe/axe
643
+ // Alternative: IDB — brew install idb-companion
644
+ // Set IOS_DRIVER=axe env var to use AXe (default is idb)
500
645
  // ============================================================================
501
646
  /**
502
647
  * Tap at coordinates on an iOS simulator using IDB
503
648
  */
504
649
  export async function iosTap(x, y, options) {
505
650
  try {
506
- const idbAvailable = await isIdbAvailable();
507
- if (!idbAvailable) {
508
- return {
509
- success: false,
510
- error: "IDB is not installed. Install with: brew install idb-companion"
511
- };
512
- }
513
- // Get simulator UDID (prefer Metro-connected, then fall back to booted)
514
- const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
515
- if (!targetUdid) {
516
- return {
517
- success: false,
518
- error: "No iOS simulator is currently running. Start a simulator first."
519
- };
520
- }
521
- // Ensure IDB is connected to the simulator
522
- const connectResult = await ensureIdbConnected(targetUdid);
523
- if (!connectResult.success) {
524
- return { success: false, error: connectResult.error };
525
- }
651
+ const preflight = await ensureUiDriverReady(options?.udid);
652
+ if (!preflight.ready)
653
+ return preflight.error;
654
+ const { driver, targetUdid } = preflight;
526
655
  const xRounded = Math.round(x);
527
656
  const yRounded = Math.round(y);
528
- // Build args array for execFile (no shell)
529
- const args = ["ui", "tap", "--udid", targetUdid];
530
- if (options?.duration !== undefined) {
531
- args.push("--duration", String(options.duration));
532
- }
533
- args.push("--json", "--", String(xRounded), String(yRounded));
534
- const { stderr } = await runIdb(...args);
535
- if (stderr)
536
- throw new Error(stderr);
657
+ if (driver === "axe") {
658
+ if (options?.duration !== undefined) {
659
+ // Long press: use axe touch with delay (convert ms to seconds)
660
+ const delaySecs = String(options.duration / 1000);
661
+ await runAxe("touch", "-x", String(xRounded), "-y", String(yRounded), "--down", "--up", "--delay", delaySecs, "--udid", targetUdid);
662
+ }
663
+ else {
664
+ await runAxe("tap", "-x", String(xRounded), "-y", String(yRounded), "--udid", targetUdid);
665
+ }
666
+ }
667
+ else {
668
+ const args = ["ui", "tap", "--udid", targetUdid];
669
+ if (options?.duration !== undefined) {
670
+ args.push("--duration", String(options.duration));
671
+ }
672
+ args.push("--json", "--", String(xRounded), String(yRounded));
673
+ const { stderr } = await runIdb(...args);
674
+ if (stderr)
675
+ throw new Error(stderr);
676
+ }
537
677
  return {
538
678
  success: true,
539
- result: `Tapped at (${xRounded}, ${yRounded})`
679
+ result: `Tapped at (${xRounded}, ${yRounded})`,
540
680
  };
541
681
  }
542
682
  catch (error) {
543
683
  return {
544
684
  success: false,
545
- error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`
685
+ error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`,
546
686
  };
547
687
  }
548
688
  }
549
689
  /**
550
- * Swipe gesture on an iOS simulator using IDB
690
+ * Swipe gesture on an iOS simulator
551
691
  */
552
692
  export async function iosSwipe(startX, startY, endX, endY, options) {
553
693
  try {
554
- const idbAvailable = await isIdbAvailable();
555
- if (!idbAvailable) {
556
- return {
557
- success: false,
558
- error: "IDB is not installed. Install with: brew install idb-companion"
559
- };
560
- }
561
- const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
562
- if (!targetUdid) {
563
- return {
564
- success: false,
565
- error: "No iOS simulator is currently running. Start a simulator first."
566
- };
567
- }
568
- // Ensure IDB is connected to the simulator
569
- const connectResult = await ensureIdbConnected(targetUdid);
570
- if (!connectResult.success) {
571
- return { success: false, error: connectResult.error };
572
- }
694
+ const preflight = await ensureUiDriverReady(options?.udid);
695
+ if (!preflight.ready)
696
+ return preflight.error;
697
+ const { driver, targetUdid } = preflight;
573
698
  const x1 = Math.round(startX);
574
699
  const y1 = Math.round(startY);
575
700
  const x2 = Math.round(endX);
576
701
  const y2 = Math.round(endY);
577
- // Build args array for execFile (no shell)
578
- const args = ["ui", "swipe", "--udid", targetUdid];
579
- if (options?.duration !== undefined) {
580
- args.push("--duration", String(options.duration));
581
- }
582
- if (options?.delta !== undefined) {
583
- args.push("--delta", String(options.delta));
584
- }
585
- args.push("--json", "--", String(x1), String(y1), String(x2), String(y2));
586
- const { stderr } = await runIdb(...args);
587
- if (stderr)
588
- throw new Error(stderr);
702
+ if (driver === "axe") {
703
+ const args = [
704
+ "swipe",
705
+ "--start-x",
706
+ String(x1),
707
+ "--start-y",
708
+ String(y1),
709
+ "--end-x",
710
+ String(x2),
711
+ "--end-y",
712
+ String(y2),
713
+ ];
714
+ if (options?.duration !== undefined) {
715
+ args.push("--duration", String(options.duration / 1000));
716
+ }
717
+ if (options?.delta !== undefined) {
718
+ args.push("--delta", String(options.delta));
719
+ }
720
+ args.push("--udid", targetUdid);
721
+ await runAxe(...args);
722
+ }
723
+ else {
724
+ const args = ["ui", "swipe", "--udid", targetUdid];
725
+ if (options?.duration !== undefined) {
726
+ args.push("--duration", String(options.duration));
727
+ }
728
+ if (options?.delta !== undefined) {
729
+ args.push("--delta", String(options.delta));
730
+ }
731
+ args.push("--json", "--", String(x1), String(y1), String(x2), String(y2));
732
+ const { stderr } = await runIdb(...args);
733
+ if (stderr)
734
+ throw new Error(stderr);
735
+ }
589
736
  return {
590
737
  success: true,
591
- result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})`
738
+ result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})`,
592
739
  };
593
740
  }
594
741
  catch (error) {
595
742
  return {
596
743
  success: false,
597
- error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`
744
+ error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`,
598
745
  };
599
746
  }
600
747
  }
601
748
  /**
602
- * Input text into the active field on an iOS simulator using IDB
749
+ * Input text into the active field on an iOS simulator
603
750
  */
604
751
  export async function iosInputText(text, udid) {
605
752
  try {
606
- const idbAvailable = await isIdbAvailable();
607
- if (!idbAvailable) {
608
- return {
609
- success: false,
610
- error: "IDB is not installed. Install with: brew install idb-companion"
611
- };
612
- }
613
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
614
- if (!targetUdid) {
615
- return {
616
- success: false,
617
- error: "No iOS simulator is currently running. Start a simulator first."
618
- };
753
+ const preflight = await ensureUiDriverReady(udid);
754
+ if (!preflight.ready)
755
+ return preflight.error;
756
+ const { driver, targetUdid } = preflight;
757
+ if (driver === "axe") {
758
+ await runAxe("type", text, "--udid", targetUdid);
619
759
  }
620
- // Ensure IDB is connected to the simulator
621
- const connectResult = await ensureIdbConnected(targetUdid);
622
- if (!connectResult.success) {
623
- return { success: false, error: connectResult.error };
760
+ else {
761
+ const { stderr } = await runIdb("ui", "text", "--udid", targetUdid, text);
762
+ if (stderr)
763
+ throw new Error(stderr);
624
764
  }
625
- // Use execFile with args array (no shell escaping needed)
626
- const { stderr } = await runIdb("ui", "text", "--udid", targetUdid, text);
627
- if (stderr)
628
- throw new Error(stderr);
629
765
  return {
630
766
  success: true,
631
- result: `Typed text: "${text}"`
767
+ result: `Typed text: "${text}"`,
632
768
  };
633
769
  }
634
770
  catch (error) {
635
771
  return {
636
772
  success: false,
637
- error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}`
773
+ error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}`,
638
774
  };
639
775
  }
640
776
  }
641
777
  /**
642
- * Press a hardware button on an iOS simulator using IDB
778
+ * Press a hardware button on an iOS simulator
643
779
  */
644
780
  export async function iosButton(button, options) {
645
781
  try {
646
- const idbAvailable = await isIdbAvailable();
647
- if (!idbAvailable) {
648
- return {
649
- success: false,
650
- error: "IDB is not installed. Install with: brew install idb-companion"
651
- };
652
- }
653
782
  // Validate button type
654
783
  if (!IOS_BUTTON_TYPES.includes(button)) {
655
784
  return {
656
785
  success: false,
657
- error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}`
786
+ error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}`,
658
787
  };
659
788
  }
660
- const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
661
- if (!targetUdid) {
662
- return {
663
- success: false,
664
- error: "No iOS simulator is currently running. Start a simulator first."
665
- };
666
- }
667
- // Ensure IDB is connected to the simulator
668
- const connectResult = await ensureIdbConnected(targetUdid);
669
- if (!connectResult.success) {
670
- return { success: false, error: connectResult.error };
789
+ const preflight = await ensureUiDriverReady(options?.udid);
790
+ if (!preflight.ready)
791
+ return preflight.error;
792
+ const { driver, targetUdid } = preflight;
793
+ if (driver === "axe") {
794
+ const args = ["button", toAxeButtonName(button)];
795
+ if (options?.duration !== undefined) {
796
+ args.push("--duration", String(options.duration));
797
+ }
798
+ args.push("--udid", targetUdid);
799
+ await runAxe(...args);
671
800
  }
672
- // Build args array for execFile (no shell)
673
- const args = ["ui", "button", "--udid", targetUdid];
674
- if (options?.duration !== undefined) {
675
- args.push("--duration", String(options.duration));
801
+ else {
802
+ const args = ["ui", "button", "--udid", targetUdid];
803
+ if (options?.duration !== undefined) {
804
+ args.push("--duration", String(options.duration));
805
+ }
806
+ args.push(button);
807
+ const { stderr } = await runIdb(...args);
808
+ if (stderr)
809
+ throw new Error(stderr);
676
810
  }
677
- args.push(button);
678
- const { stderr } = await runIdb(...args);
679
- if (stderr)
680
- throw new Error(stderr);
681
811
  return {
682
812
  success: true,
683
- result: `Pressed ${button} button`
813
+ result: `Pressed ${button} button`,
684
814
  };
685
815
  }
686
816
  catch (error) {
687
817
  return {
688
818
  success: false,
689
- error: `Failed to press button: ${error instanceof Error ? error.message : String(error)}`
819
+ error: `Failed to press button: ${error instanceof Error ? error.message : String(error)}`,
690
820
  };
691
821
  }
692
822
  }
693
823
  /**
694
- * Send a key event to an iOS simulator using IDB
824
+ * Send a key event to an iOS simulator
695
825
  */
696
826
  export async function iosKeyEvent(keycode, options) {
697
827
  try {
698
- const idbAvailable = await isIdbAvailable();
699
- if (!idbAvailable) {
700
- return {
701
- success: false,
702
- error: "IDB is not installed. Install with: brew install idb-companion"
703
- };
704
- }
705
- const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
706
- if (!targetUdid) {
707
- return {
708
- success: false,
709
- error: "No iOS simulator is currently running. Start a simulator first."
710
- };
711
- }
712
- // Ensure IDB is connected to the simulator
713
- const connectResult = await ensureIdbConnected(targetUdid);
714
- if (!connectResult.success) {
715
- return { success: false, error: connectResult.error };
828
+ const preflight = await ensureUiDriverReady(options?.udid);
829
+ if (!preflight.ready)
830
+ return preflight.error;
831
+ const { driver, targetUdid } = preflight;
832
+ if (driver === "axe") {
833
+ const args = ["key", String(keycode)];
834
+ if (options?.duration !== undefined) {
835
+ args.push("--duration", String(options.duration));
836
+ }
837
+ args.push("--udid", targetUdid);
838
+ await runAxe(...args);
716
839
  }
717
- // Build args array for execFile (no shell)
718
- const args = ["ui", "key", "--udid", targetUdid];
719
- if (options?.duration !== undefined) {
720
- args.push("--duration", String(options.duration));
840
+ else {
841
+ const args = ["ui", "key", "--udid", targetUdid];
842
+ if (options?.duration !== undefined) {
843
+ args.push("--duration", String(options.duration));
844
+ }
845
+ args.push(String(keycode));
846
+ const { stderr } = await runIdb(...args);
847
+ if (stderr)
848
+ throw new Error(stderr);
721
849
  }
722
- args.push(String(keycode));
723
- const { stderr } = await runIdb(...args);
724
- if (stderr)
725
- throw new Error(stderr);
726
850
  return {
727
851
  success: true,
728
- result: `Sent key event: ${keycode}`
852
+ result: `Sent key event: ${keycode}`,
729
853
  };
730
854
  }
731
855
  catch (error) {
732
856
  return {
733
857
  success: false,
734
- error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}`
858
+ error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}`,
735
859
  };
736
860
  }
737
861
  }
738
862
  /**
739
- * Send a sequence of key events to an iOS simulator using IDB
863
+ * Send a sequence of key events to an iOS simulator
740
864
  */
741
865
  export async function iosKeySequence(keycodes, udid) {
742
866
  try {
743
- const idbAvailable = await isIdbAvailable();
744
- if (!idbAvailable) {
745
- return {
746
- success: false,
747
- error: "IDB is not installed. Install with: brew install idb-companion"
748
- };
749
- }
750
867
  if (!keycodes || keycodes.length === 0) {
751
868
  return {
752
869
  success: false,
753
- error: "At least one keycode is required"
870
+ error: "At least one keycode is required",
754
871
  };
755
872
  }
756
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
757
- if (!targetUdid) {
758
- return {
759
- success: false,
760
- error: "No iOS simulator is currently running. Start a simulator first."
761
- };
873
+ const preflight = await ensureUiDriverReady(udid);
874
+ if (!preflight.ready)
875
+ return preflight.error;
876
+ const { driver, targetUdid } = preflight;
877
+ if (driver === "axe") {
878
+ await runAxe("key-sequence", "--keycodes", keycodes.join(","), "--udid", targetUdid);
762
879
  }
763
- // Ensure IDB is connected to the simulator
764
- const connectResult = await ensureIdbConnected(targetUdid);
765
- if (!connectResult.success) {
766
- return { success: false, error: connectResult.error };
880
+ else {
881
+ const args = [
882
+ "ui",
883
+ "key-sequence",
884
+ "--udid",
885
+ targetUdid,
886
+ ...keycodes.map(String),
887
+ ];
888
+ const { stderr } = await runIdb(...args);
889
+ if (stderr)
890
+ throw new Error(stderr);
767
891
  }
768
- // Build args array for execFile (no shell)
769
- const args = ["ui", "key-sequence", "--udid", targetUdid, ...keycodes.map(String)];
770
- const { stderr } = await runIdb(...args);
771
- if (stderr)
772
- throw new Error(stderr);
773
892
  return {
774
893
  success: true,
775
- result: `Sent key sequence: ${keycodes.join(", ")}`
894
+ result: `Sent key sequence: ${keycodes.join(", ")}`,
776
895
  };
777
896
  }
778
897
  catch (error) {
779
898
  return {
780
899
  success: false,
781
- error: `Failed to send key sequence: ${error instanceof Error ? error.message : String(error)}`
900
+ error: `Failed to send key sequence: ${error instanceof Error ? error.message : String(error)}`,
782
901
  };
783
902
  }
784
903
  }
@@ -812,80 +931,65 @@ function formatAccessibilityTree(elements, depth = 0) {
812
931
  return lines.join("\n");
813
932
  }
814
933
  /**
815
- * Get accessibility info for the entire screen using IDB
934
+ * Get accessibility info for the entire screen
816
935
  */
817
936
  export async function iosDescribeAll(udid) {
818
937
  try {
819
- const idbAvailable = await isIdbAvailable();
820
- if (!idbAvailable) {
821
- return {
822
- success: false,
823
- error: "IDB is not installed. Install with: brew install idb-companion"
824
- };
825
- }
826
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
827
- if (!targetUdid) {
828
- return {
829
- success: false,
830
- error: "No iOS simulator is currently running. Start a simulator first."
831
- };
938
+ const preflight = await ensureUiDriverReady(udid);
939
+ if (!preflight.ready)
940
+ return preflight.error;
941
+ const { driver, targetUdid } = preflight;
942
+ let stdout;
943
+ if (driver === "axe") {
944
+ const result = await runAxe("describe-ui", "--udid", targetUdid);
945
+ stdout = result.stdout;
832
946
  }
833
- // Ensure IDB is connected to the simulator
834
- const connectResult = await ensureIdbConnected(targetUdid);
835
- if (!connectResult.success) {
836
- return { success: false, error: connectResult.error };
947
+ else {
948
+ const result = await runIdb("ui", "describe-all", "--udid", targetUdid, "--json", "--nested");
949
+ if (result.stderr)
950
+ throw new Error(result.stderr);
951
+ stdout = result.stdout;
837
952
  }
838
- // Use execFile with args array (no shell)
839
- const { stdout, stderr } = await runIdb("ui", "describe-all", "--udid", targetUdid, "--json", "--nested");
840
- if (stderr)
841
- throw new Error(stderr);
842
953
  // Parse JSON response
843
954
  const elements = JSON.parse(stdout);
844
955
  // Format for human-readable output
845
956
  const formatted = formatAccessibilityTree(elements);
957
+ const header = 'Note: All coordinates are in iOS points. Use tap(text="...") for tapping — do not pass these coordinates to tap(x, y) directly.\n\n';
846
958
  return {
847
959
  success: true,
848
- result: formatted || "No accessibility elements found",
849
- elements
960
+ result: header + (formatted || "No accessibility elements found"),
961
+ elements,
850
962
  };
851
963
  }
852
964
  catch (error) {
853
965
  return {
854
966
  success: false,
855
- error: `Failed to describe screen: ${error instanceof Error ? error.message : String(error)}`
967
+ error: `Failed to describe screen: ${error instanceof Error ? error.message : String(error)}`,
856
968
  };
857
969
  }
858
970
  }
859
971
  /**
860
- * Get accessibility info at a specific point using IDB
972
+ * Get accessibility info at a specific point
861
973
  */
862
974
  export async function iosDescribePoint(x, y, udid) {
863
975
  try {
864
- const idbAvailable = await isIdbAvailable();
865
- if (!idbAvailable) {
866
- return {
867
- success: false,
868
- error: "IDB is not installed. Install with: brew install idb-companion"
869
- };
870
- }
871
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
872
- if (!targetUdid) {
873
- return {
874
- success: false,
875
- error: "No iOS simulator is currently running. Start a simulator first."
876
- };
877
- }
878
- // Ensure IDB is connected to the simulator
879
- const connectResult = await ensureIdbConnected(targetUdid);
880
- if (!connectResult.success) {
881
- return { success: false, error: connectResult.error };
882
- }
976
+ const preflight = await ensureUiDriverReady(udid);
977
+ if (!preflight.ready)
978
+ return preflight.error;
979
+ const { driver, targetUdid } = preflight;
883
980
  const xRounded = Math.round(x);
884
981
  const yRounded = Math.round(y);
885
- // Use execFile with args array (no shell)
886
- const { stdout, stderr } = await runIdb("ui", "describe-point", "--udid", targetUdid, "--json", "--", String(xRounded), String(yRounded));
887
- if (stderr)
888
- throw new Error(stderr);
982
+ let stdout;
983
+ if (driver === "axe") {
984
+ const result = await runAxe("describe-ui", "--point", `${xRounded},${yRounded}`, "--udid", targetUdid);
985
+ stdout = result.stdout;
986
+ }
987
+ else {
988
+ const result = await runIdb("ui", "describe-point", "--udid", targetUdid, "--json", "--", String(xRounded), String(yRounded));
989
+ if (result.stderr)
990
+ throw new Error(result.stderr);
991
+ stdout = result.stdout;
992
+ }
889
993
  // Parse JSON response - may be single element or array
890
994
  let element;
891
995
  try {
@@ -896,7 +1000,7 @@ export async function iosDescribePoint(x, y, udid) {
896
1000
  return {
897
1001
  success: true,
898
1002
  result: `No accessibility element found at (${xRounded}, ${yRounded})`,
899
- elements: []
1003
+ elements: [],
900
1004
  };
901
1005
  }
902
1006
  // Format for human-readable output
@@ -919,13 +1023,13 @@ export async function iosDescribePoint(x, y, udid) {
919
1023
  result: parts.length > 0
920
1024
  ? `Element at (${xRounded}, ${yRounded}):\n${parts.join("\n")}`
921
1025
  : `No accessibility element found at (${xRounded}, ${yRounded})`,
922
- elements: element ? [element] : []
1026
+ elements: element ? [element] : [],
923
1027
  };
924
1028
  }
925
1029
  catch (error) {
926
1030
  return {
927
1031
  success: false,
928
- error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`
1032
+ error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`,
929
1033
  };
930
1034
  }
931
1035
  }
@@ -952,7 +1056,7 @@ export async function iosTapElement(options) {
952
1056
  if (!label && !labelContains) {
953
1057
  return {
954
1058
  success: false,
955
- error: "Either 'label' (exact match) or 'labelContains' (partial match) is required"
1059
+ error: "Either 'label' (exact match) or 'labelContains' (partial match) is required",
956
1060
  };
957
1061
  }
958
1062
  // Get all accessibility elements
@@ -960,12 +1064,12 @@ export async function iosTapElement(options) {
960
1064
  if (!describeResult.success || !describeResult.elements) {
961
1065
  return {
962
1066
  success: false,
963
- error: describeResult.error || "Failed to get accessibility elements"
1067
+ error: describeResult.error || "Failed to get accessibility elements",
964
1068
  };
965
1069
  }
966
1070
  // Flatten the tree and find matching elements
967
1071
  const allElements = flattenElements(describeResult.elements);
968
- const matches = allElements.filter(el => {
1072
+ const matches = allElements.filter((el) => {
969
1073
  if (!el.AXLabel)
970
1074
  return false;
971
1075
  if (label)
@@ -975,17 +1079,19 @@ export async function iosTapElement(options) {
975
1079
  return false;
976
1080
  });
977
1081
  if (matches.length === 0) {
978
- const searchTerm = label ? `label="${label}"` : `labelContains="${labelContains}"`;
1082
+ const searchTerm = label
1083
+ ? `label="${label}"`
1084
+ : `labelContains="${labelContains}"`;
979
1085
  return {
980
1086
  success: false,
981
- error: `No element found with ${searchTerm}`
1087
+ error: `No element found with ${searchTerm}`,
982
1088
  };
983
1089
  }
984
1090
  // Select element by index (default 0 = first match)
985
1091
  if (index >= matches.length) {
986
1092
  return {
987
1093
  success: false,
988
- error: `Index ${index} out of range. Found ${matches.length} matching element(s).`
1094
+ error: `Index ${index} out of range. Found ${matches.length} matching element(s).`,
989
1095
  };
990
1096
  }
991
1097
  const element = matches[index];
@@ -993,7 +1099,7 @@ export async function iosTapElement(options) {
993
1099
  if (!element.frame) {
994
1100
  return {
995
1101
  success: false,
996
- error: `Element "${element.AXLabel}" has no frame coordinates`
1102
+ error: `Element "${element.AXLabel}" has no frame coordinates`,
997
1103
  };
998
1104
  }
999
1105
  // Calculate center
@@ -1004,7 +1110,7 @@ export async function iosTapElement(options) {
1004
1110
  if (tapResult.success) {
1005
1111
  return {
1006
1112
  success: true,
1007
- result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})`
1113
+ result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})`,
1008
1114
  };
1009
1115
  }
1010
1116
  return tapResult;
@@ -1012,7 +1118,7 @@ export async function iosTapElement(options) {
1012
1118
  catch (error) {
1013
1119
  return {
1014
1120
  success: false,
1015
- error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`
1121
+ error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`,
1016
1122
  };
1017
1123
  }
1018
1124
  }
@@ -1030,20 +1136,27 @@ function parseIdbAccessibilityForFindElement(output) {
1030
1136
  label: node.AXLabel || node.label || "",
1031
1137
  value: node.AXValue || node.value || "",
1032
1138
  type: node.type || node.AXType || "",
1139
+ identifier: node.AXIdentifier ||
1140
+ node.identifier ||
1141
+ node.AXUniqueId ||
1142
+ "",
1033
1143
  frame: {
1034
1144
  x: frame.x || 0,
1035
1145
  y: frame.y || 0,
1036
1146
  width: frame.width || 0,
1037
- height: frame.height || 0
1147
+ height: frame.height || 0,
1038
1148
  },
1039
1149
  center: {
1040
1150
  x: Math.round((frame.x || 0) + (frame.width || 0) / 2),
1041
- y: Math.round((frame.y || 0) + (frame.height || 0) / 2)
1151
+ y: Math.round((frame.y || 0) + (frame.height || 0) / 2),
1042
1152
  },
1043
1153
  enabled: node.enabled !== false,
1044
- traits: node.traits || []
1154
+ traits: node.traits || [],
1045
1155
  };
1046
- if (element.label || element.value || element.type) {
1156
+ if (element.label ||
1157
+ element.value ||
1158
+ element.type ||
1159
+ element.identifier) {
1047
1160
  elements.push(element);
1048
1161
  }
1049
1162
  }
@@ -1068,6 +1181,35 @@ function parseIdbAccessibilityForFindElement(output) {
1068
1181
  }
1069
1182
  return elements;
1070
1183
  }
1184
+ /**
1185
+ * Check if an element's center is within the screen viewport.
1186
+ * Filters out off-screen elements in scroll views, horizontal pagers, etc.
1187
+ */
1188
+ function isIOSElementInViewport(element, screenWidth, screenHeight) {
1189
+ const { center } = element;
1190
+ return (center.x >= 0 &&
1191
+ center.x <= screenWidth &&
1192
+ center.y >= 0 &&
1193
+ center.y <= screenHeight);
1194
+ }
1195
+ /**
1196
+ * Extract screen dimensions from the parsed UI tree.
1197
+ * The root Application element's frame represents the screen size.
1198
+ */
1199
+ function extractIOSScreenSize(rawOutput) {
1200
+ try {
1201
+ const data = JSON.parse(rawOutput);
1202
+ const root = Array.isArray(data) ? data[0] : data;
1203
+ const frame = root?.frame;
1204
+ if (frame?.width && frame?.height) {
1205
+ return { width: frame.width, height: frame.height };
1206
+ }
1207
+ }
1208
+ catch {
1209
+ // Fall through
1210
+ }
1211
+ return null;
1212
+ }
1071
1213
  /**
1072
1214
  * Match iOS element against find options
1073
1215
  */
@@ -1092,6 +1234,16 @@ function matchesIOSFindElement(element, options) {
1092
1234
  if (!element.type.toLowerCase().includes(options.type.toLowerCase()))
1093
1235
  return false;
1094
1236
  }
1237
+ if (options.identifier !== undefined) {
1238
+ if (element.identifier !== options.identifier)
1239
+ return false;
1240
+ }
1241
+ if (options.identifierContains !== undefined) {
1242
+ if (!element.identifier
1243
+ .toLowerCase()
1244
+ .includes(options.identifierContains.toLowerCase()))
1245
+ return false;
1246
+ }
1095
1247
  return true;
1096
1248
  }
1097
1249
  /**
@@ -1099,36 +1251,30 @@ function matchesIOSFindElement(element, options) {
1099
1251
  */
1100
1252
  export async function iosGetUITree(udid) {
1101
1253
  try {
1102
- const idbAvailable = await isIdbAvailable();
1103
- if (!idbAvailable) {
1104
- return {
1105
- success: false,
1106
- error: "IDB is not installed. Install with: brew install idb-companion"
1107
- };
1254
+ const preflight = await ensureUiDriverReady(udid);
1255
+ if (!preflight.ready)
1256
+ return preflight.error;
1257
+ const { driver, targetUdid } = preflight;
1258
+ let stdout;
1259
+ if (driver === "axe") {
1260
+ const result = await runAxe("describe-ui", "--udid", targetUdid);
1261
+ stdout = result.stdout;
1108
1262
  }
1109
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
1110
- if (!targetUdid) {
1111
- return {
1112
- success: false,
1113
- error: "No iOS simulator is currently running. Start a simulator first."
1114
- };
1115
- }
1116
- const connectResult = await ensureIdbConnected(targetUdid);
1117
- if (!connectResult.success) {
1118
- return { success: false, error: connectResult.error };
1263
+ else {
1264
+ const result = await runIdb("ui", "describe-all", "--udid", targetUdid);
1265
+ stdout = result.stdout;
1119
1266
  }
1120
- const { stdout } = await runIdb("ui", "describe-all", "--udid", targetUdid);
1121
1267
  const elements = parseIdbAccessibilityForFindElement(stdout);
1122
1268
  return {
1123
1269
  success: true,
1124
1270
  elements,
1125
- rawOutput: stdout
1271
+ rawOutput: stdout,
1126
1272
  };
1127
1273
  }
1128
1274
  catch (error) {
1129
1275
  return {
1130
1276
  success: false,
1131
- error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`
1277
+ error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`,
1132
1278
  };
1133
1279
  }
1134
1280
  }
@@ -1137,12 +1283,17 @@ export async function iosGetUITree(udid) {
1137
1283
  */
1138
1284
  export async function iosFindElement(options, udid) {
1139
1285
  try {
1140
- if (!options.label && !options.labelContains && !options.value &&
1141
- !options.valueContains && !options.type) {
1286
+ if (!options.label &&
1287
+ !options.labelContains &&
1288
+ !options.value &&
1289
+ !options.valueContains &&
1290
+ !options.type &&
1291
+ !options.identifier &&
1292
+ !options.identifierContains) {
1142
1293
  return {
1143
1294
  success: false,
1144
1295
  found: false,
1145
- error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided"
1296
+ error: "At least one search criteria (label, labelContains, value, valueContains, identifier, identifierContains, or type) must be provided",
1146
1297
  };
1147
1298
  }
1148
1299
  const treeResult = await iosGetUITree(udid);
@@ -1150,15 +1301,28 @@ export async function iosFindElement(options, udid) {
1150
1301
  return {
1151
1302
  success: false,
1152
1303
  found: false,
1153
- error: treeResult.error
1304
+ error: treeResult.error,
1154
1305
  };
1155
1306
  }
1156
- const matches = treeResult.elements.filter(el => matchesIOSFindElement(el, options));
1307
+ // Filter to viewport-visible elements first, then match criteria.
1308
+ // This prevents selecting off-screen duplicates in scroll views / pagers.
1309
+ const screenSize = treeResult.rawOutput
1310
+ ? extractIOSScreenSize(treeResult.rawOutput)
1311
+ : null;
1312
+ const visibleElements = screenSize
1313
+ ? treeResult.elements.filter((el) => isIOSElementInViewport(el, screenSize.width, screenSize.height))
1314
+ : treeResult.elements;
1315
+ let matches = visibleElements.filter((el) => matchesIOSFindElement(el, options));
1316
+ // If no visible matches, fall back to all elements (element may be partially visible
1317
+ // or the screen size extraction failed)
1318
+ if (matches.length === 0) {
1319
+ matches = treeResult.elements.filter((el) => matchesIOSFindElement(el, options));
1320
+ }
1157
1321
  if (matches.length === 0) {
1158
1322
  return {
1159
1323
  success: true,
1160
1324
  found: false,
1161
- matchCount: 0
1325
+ matchCount: 0,
1162
1326
  };
1163
1327
  }
1164
1328
  const index = options.index ?? 0;
@@ -1168,7 +1332,7 @@ export async function iosFindElement(options, udid) {
1168
1332
  success: true,
1169
1333
  found: false,
1170
1334
  matchCount: matches.length,
1171
- error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`
1335
+ error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`,
1172
1336
  };
1173
1337
  }
1174
1338
  return {
@@ -1176,17 +1340,85 @@ export async function iosFindElement(options, udid) {
1176
1340
  found: true,
1177
1341
  element: selectedElement,
1178
1342
  allMatches: matches,
1179
- matchCount: matches.length
1343
+ matchCount: matches.length,
1180
1344
  };
1181
1345
  }
1182
1346
  catch (error) {
1183
1347
  return {
1184
1348
  success: false,
1185
1349
  found: false,
1186
- error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`
1350
+ error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`,
1187
1351
  };
1188
1352
  }
1189
1353
  }
1354
+ // DPR cache: UDID → device pixel ratio (never changes during session)
1355
+ const dprCache = new Map();
1356
+ /**
1357
+ * Pure calculation: DPR = screenshotPixelWidth / screenPointWidth.
1358
+ * Exported for unit testing.
1359
+ */
1360
+ export function calculateDPR(screenshotPixelWidth, screenPointWidth) {
1361
+ return Math.round(screenshotPixelWidth / screenPointWidth);
1362
+ }
1363
+ /**
1364
+ * Query the actual device pixel ratio for an iOS simulator.
1365
+ * Uses screenshot pixel width vs accessibility root frame width.
1366
+ * Result is cached per UDID for the session.
1367
+ */
1368
+ export async function getDevicePixelRatio(udid) {
1369
+ const cacheKey = udid || "__default__";
1370
+ const cached = dprCache.get(cacheKey);
1371
+ if (cached)
1372
+ return cached;
1373
+ const resolvedUdid = udid || (await getActiveOrBootedSimulatorUdid());
1374
+ if (!resolvedUdid) {
1375
+ throw new Error("No iOS simulator is currently running. Start a simulator first.");
1376
+ }
1377
+ // Take screenshot to get pixel dimensions
1378
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1379
+ const screenshotPath = path.join(os.tmpdir(), `ios-dpr-screenshot-${timestamp}.png`);
1380
+ await execAsync(`xcrun simctl io ${resolvedUdid} screenshot "${screenshotPath}"`, {
1381
+ timeout: SIMCTL_TIMEOUT,
1382
+ });
1383
+ const metadata = await sharp(screenshotPath).metadata();
1384
+ const pixelWidth = metadata.width;
1385
+ const pixelHeight = metadata.height;
1386
+ // Try accessibility tree for exact DPR, fall back to inference
1387
+ const driver = getIosDriver();
1388
+ const driverOk = driver === "axe" ? await isAxeAvailable() : await isIdbAvailable();
1389
+ if (driverOk) {
1390
+ try {
1391
+ let stdout;
1392
+ if (driver === "axe") {
1393
+ const result = await runAxe("describe-ui", "--udid", resolvedUdid);
1394
+ stdout = result.stdout;
1395
+ }
1396
+ else {
1397
+ await ensureIdbConnected(resolvedUdid);
1398
+ const result = await runIdb("ui", "describe-all", "--udid", resolvedUdid, "--json", "--nested");
1399
+ stdout = result.stdout;
1400
+ }
1401
+ const screenSize = extractIOSScreenSize(stdout);
1402
+ if (screenSize) {
1403
+ const dpr = calculateDPR(pixelWidth, screenSize.width);
1404
+ dprCache.set(cacheKey, dpr);
1405
+ return dpr;
1406
+ }
1407
+ }
1408
+ catch {
1409
+ // driver failed — fall through to inference
1410
+ }
1411
+ }
1412
+ // Fallback: infer DPR from screenshot pixel dimensions
1413
+ const { inferIOSDevicePixelRatio } = await import("./ocr.js");
1414
+ const dpr = inferIOSDevicePixelRatio(pixelWidth, pixelHeight);
1415
+ dprCache.set(cacheKey, dpr);
1416
+ return dpr;
1417
+ }
1418
+ /** Clear DPR cache (for testing or when devices change) */
1419
+ export function clearDPRCache() {
1420
+ dprCache.clear();
1421
+ }
1190
1422
  /**
1191
1423
  * Wait for element to appear on iOS screen with polling
1192
1424
  */
@@ -1194,13 +1426,16 @@ export async function iosWaitForElement(options, udid) {
1194
1426
  const timeoutMs = options.timeoutMs ?? 10000;
1195
1427
  const pollIntervalMs = options.pollIntervalMs ?? 500;
1196
1428
  const startTime = Date.now();
1197
- if (!options.label && !options.labelContains && !options.value &&
1198
- !options.valueContains && !options.type) {
1429
+ if (!options.label &&
1430
+ !options.labelContains &&
1431
+ !options.value &&
1432
+ !options.valueContains &&
1433
+ !options.type) {
1199
1434
  return {
1200
1435
  success: false,
1201
1436
  found: false,
1202
1437
  timedOut: false,
1203
- error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided"
1438
+ error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided",
1204
1439
  };
1205
1440
  }
1206
1441
  while (Date.now() - startTime < timeoutMs) {
@@ -1209,24 +1444,24 @@ export async function iosWaitForElement(options, udid) {
1209
1444
  return {
1210
1445
  ...result,
1211
1446
  elapsedMs: Date.now() - startTime,
1212
- timedOut: false
1447
+ timedOut: false,
1213
1448
  };
1214
1449
  }
1215
1450
  if (!result.success) {
1216
1451
  return {
1217
1452
  ...result,
1218
1453
  elapsedMs: Date.now() - startTime,
1219
- timedOut: false
1454
+ timedOut: false,
1220
1455
  };
1221
1456
  }
1222
- await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
1457
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1223
1458
  }
1224
1459
  return {
1225
1460
  success: true,
1226
1461
  found: false,
1227
1462
  elapsedMs: Date.now() - startTime,
1228
1463
  timedOut: true,
1229
- error: `Timed out after ${timeoutMs}ms waiting for element`
1464
+ error: `Timed out after ${timeoutMs}ms waiting for element`,
1230
1465
  };
1231
1466
  }
1232
1467
  //# sourceMappingURL=ios.js.map