react-native-ai-devtools 1.1.4

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 (147) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +1250 -0
  3. package/build/__tests__/helpers/fake-cdp-server.d.ts +56 -0
  4. package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -0
  5. package/build/__tests__/helpers/fake-cdp-server.js +108 -0
  6. package/build/__tests__/helpers/fake-cdp-server.js.map +1 -0
  7. package/build/__tests__/integration/connection-health.test.d.ts +2 -0
  8. package/build/__tests__/integration/connection-health.test.d.ts.map +1 -0
  9. package/build/__tests__/integration/connection-health.test.js +151 -0
  10. package/build/__tests__/integration/connection-health.test.js.map +1 -0
  11. package/build/__tests__/integration/execute-in-app.test.d.ts +2 -0
  12. package/build/__tests__/integration/execute-in-app.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/execute-in-app.test.js +115 -0
  14. package/build/__tests__/integration/execute-in-app.test.js.map +1 -0
  15. package/build/__tests__/integration/tools.test.d.ts +2 -0
  16. package/build/__tests__/integration/tools.test.d.ts.map +1 -0
  17. package/build/__tests__/integration/tools.test.js +228 -0
  18. package/build/__tests__/integration/tools.test.js.map +1 -0
  19. package/build/__tests__/setup.d.ts +2 -0
  20. package/build/__tests__/setup.d.ts.map +1 -0
  21. package/build/__tests__/setup.js +11 -0
  22. package/build/__tests__/setup.js.map +1 -0
  23. package/build/__tests__/unit/bundle.test.d.ts +2 -0
  24. package/build/__tests__/unit/bundle.test.d.ts.map +1 -0
  25. package/build/__tests__/unit/bundle.test.js +53 -0
  26. package/build/__tests__/unit/bundle.test.js.map +1 -0
  27. package/build/__tests__/unit/connection-health.test.d.ts +2 -0
  28. package/build/__tests__/unit/connection-health.test.d.ts.map +1 -0
  29. package/build/__tests__/unit/connection-health.test.js +28 -0
  30. package/build/__tests__/unit/connection-health.test.js.map +1 -0
  31. package/build/__tests__/unit/executor.test.d.ts +2 -0
  32. package/build/__tests__/unit/executor.test.d.ts.map +1 -0
  33. package/build/__tests__/unit/executor.test.js +79 -0
  34. package/build/__tests__/unit/executor.test.js.map +1 -0
  35. package/build/__tests__/unit/logs.test.d.ts +2 -0
  36. package/build/__tests__/unit/logs.test.d.ts.map +1 -0
  37. package/build/__tests__/unit/logs.test.js +81 -0
  38. package/build/__tests__/unit/logs.test.js.map +1 -0
  39. package/build/__tests__/unit/metro.test.d.ts +2 -0
  40. package/build/__tests__/unit/metro.test.d.ts.map +1 -0
  41. package/build/__tests__/unit/metro.test.js +61 -0
  42. package/build/__tests__/unit/metro.test.js.map +1 -0
  43. package/build/__tests__/unit/network.test.d.ts +2 -0
  44. package/build/__tests__/unit/network.test.d.ts.map +1 -0
  45. package/build/__tests__/unit/network.test.js +102 -0
  46. package/build/__tests__/unit/network.test.js.map +1 -0
  47. package/build/__tests__/unit/tap.test.d.ts +2 -0
  48. package/build/__tests__/unit/tap.test.d.ts.map +1 -0
  49. package/build/__tests__/unit/tap.test.js +157 -0
  50. package/build/__tests__/unit/tap.test.js.map +1 -0
  51. package/build/core/android.d.ts +265 -0
  52. package/build/core/android.d.ts.map +1 -0
  53. package/build/core/android.js +1413 -0
  54. package/build/core/android.js.map +1 -0
  55. package/build/core/bundle.d.ts +49 -0
  56. package/build/core/bundle.d.ts.map +1 -0
  57. package/build/core/bundle.js +368 -0
  58. package/build/core/bundle.js.map +1 -0
  59. package/build/core/connection.d.ts +43 -0
  60. package/build/core/connection.d.ts.map +1 -0
  61. package/build/core/connection.js +963 -0
  62. package/build/core/connection.js.map +1 -0
  63. package/build/core/connectionState.d.ts +108 -0
  64. package/build/core/connectionState.d.ts.map +1 -0
  65. package/build/core/connectionState.js +284 -0
  66. package/build/core/connectionState.js.map +1 -0
  67. package/build/core/errorScreenParser.d.ts +30 -0
  68. package/build/core/errorScreenParser.d.ts.map +1 -0
  69. package/build/core/errorScreenParser.js +198 -0
  70. package/build/core/errorScreenParser.js.map +1 -0
  71. package/build/core/executor.d.ts +113 -0
  72. package/build/core/executor.d.ts.map +1 -0
  73. package/build/core/executor.js +1877 -0
  74. package/build/core/executor.js.map +1 -0
  75. package/build/core/format.d.ts +8 -0
  76. package/build/core/format.d.ts.map +1 -0
  77. package/build/core/format.js +34 -0
  78. package/build/core/format.js.map +1 -0
  79. package/build/core/guides.d.ts +14 -0
  80. package/build/core/guides.d.ts.map +1 -0
  81. package/build/core/guides.js +261 -0
  82. package/build/core/guides.js.map +1 -0
  83. package/build/core/httpServer.d.ts +14 -0
  84. package/build/core/httpServer.d.ts.map +1 -0
  85. package/build/core/httpServer.js +2459 -0
  86. package/build/core/httpServer.js.map +1 -0
  87. package/build/core/httpServerProcess.d.ts +25 -0
  88. package/build/core/httpServerProcess.d.ts.map +1 -0
  89. package/build/core/httpServerProcess.js +153 -0
  90. package/build/core/httpServerProcess.js.map +1 -0
  91. package/build/core/index.d.ts +25 -0
  92. package/build/core/index.d.ts.map +1 -0
  93. package/build/core/index.js +53 -0
  94. package/build/core/index.js.map +1 -0
  95. package/build/core/ios.d.ts +214 -0
  96. package/build/core/ios.d.ts.map +1 -0
  97. package/build/core/ios.js +1232 -0
  98. package/build/core/ios.js.map +1 -0
  99. package/build/core/logs.d.ts +43 -0
  100. package/build/core/logs.d.ts.map +1 -0
  101. package/build/core/logs.js +144 -0
  102. package/build/core/logs.js.map +1 -0
  103. package/build/core/metro.d.ts +23 -0
  104. package/build/core/metro.d.ts.map +1 -0
  105. package/build/core/metro.js +96 -0
  106. package/build/core/metro.js.map +1 -0
  107. package/build/core/network.d.ts +43 -0
  108. package/build/core/network.d.ts.map +1 -0
  109. package/build/core/network.js +217 -0
  110. package/build/core/network.js.map +1 -0
  111. package/build/core/networkInterceptor.d.ts +3 -0
  112. package/build/core/networkInterceptor.d.ts.map +1 -0
  113. package/build/core/networkInterceptor.js +203 -0
  114. package/build/core/networkInterceptor.js.map +1 -0
  115. package/build/core/ocr.d.ts +69 -0
  116. package/build/core/ocr.d.ts.map +1 -0
  117. package/build/core/ocr.js +212 -0
  118. package/build/core/ocr.js.map +1 -0
  119. package/build/core/state.d.ts +17 -0
  120. package/build/core/state.d.ts.map +1 -0
  121. package/build/core/state.js +50 -0
  122. package/build/core/state.js.map +1 -0
  123. package/build/core/tap.d.ts +91 -0
  124. package/build/core/tap.d.ts.map +1 -0
  125. package/build/core/tap.js +542 -0
  126. package/build/core/tap.js.map +1 -0
  127. package/build/core/telemetry.d.ts +4 -0
  128. package/build/core/telemetry.d.ts.map +1 -0
  129. package/build/core/telemetry.js +289 -0
  130. package/build/core/telemetry.js.map +1 -0
  131. package/build/core/types.d.ts +134 -0
  132. package/build/core/types.d.ts.map +1 -0
  133. package/build/core/types.js +2 -0
  134. package/build/core/types.js.map +1 -0
  135. package/build/httpServerStandalone.d.ts +7 -0
  136. package/build/httpServerStandalone.d.ts.map +1 -0
  137. package/build/httpServerStandalone.js +31 -0
  138. package/build/httpServerStandalone.js.map +1 -0
  139. package/build/index.d.ts +3 -0
  140. package/build/index.d.ts.map +1 -0
  141. package/build/index.js +3012 -0
  142. package/build/index.js.map +1 -0
  143. package/build/pro/tap.d.ts +91 -0
  144. package/build/pro/tap.d.ts.map +1 -0
  145. package/build/pro/tap.js +542 -0
  146. package/build/pro/tap.js.map +1 -0
  147. package/package.json +63 -0
@@ -0,0 +1,1232 @@
1
+ import { exec, execFile } from "child_process";
2
+ import { promisify } from "util";
3
+ import { existsSync } from "fs";
4
+ import path from "path";
5
+ import os from "os";
6
+ import sharp from "sharp";
7
+ import { getActiveSimulatorUdid } from "./state.js";
8
+ const execAsync = promisify(exec);
9
+ const execFileAsync = promisify(execFile);
10
+ // simctl command timeout in milliseconds
11
+ const SIMCTL_TIMEOUT = 30000;
12
+ // IDB command timeout in milliseconds
13
+ const IDB_TIMEOUT = 30000;
14
+ // Valid button types for IDB ui button command
15
+ export const IOS_BUTTON_TYPES = ["HOME", "LOCK", "SIDE_BUTTON", "SIRI", "APPLE_PAY"];
16
+ // Track connected IDB simulators to avoid redundant connect calls
17
+ const connectedIdbSimulators = new Set();
18
+ /**
19
+ * Get the IDB executable path
20
+ * Supports IDB_PATH environment variable for custom installations
21
+ */
22
+ function getIdbPath() {
23
+ return process.env.IDB_PATH || "idb";
24
+ }
25
+ /**
26
+ * Run IDB command with execFile (no shell) for proper argument handling
27
+ * This matches the original ios-simulator-mcp implementation
28
+ */
29
+ async function runIdb(...args) {
30
+ const idbPath = getIdbPath();
31
+ const { stdout, stderr } = await execFileAsync(idbPath, args, {
32
+ timeout: IDB_TIMEOUT
33
+ });
34
+ return {
35
+ stdout: stdout.trim(),
36
+ stderr: stderr.trim()
37
+ };
38
+ }
39
+ /**
40
+ * Check if IDB is available
41
+ */
42
+ export async function isIdbAvailable() {
43
+ try {
44
+ await runIdb("--help");
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ /**
52
+ * Ensure IDB is connected to the specified simulator
53
+ * IDB requires `idb connect <UDID>` before any UI commands work
54
+ */
55
+ async function ensureIdbConnected(udid) {
56
+ // Skip if already connected in this session
57
+ if (connectedIdbSimulators.has(udid)) {
58
+ return { success: true };
59
+ }
60
+ try {
61
+ await runIdb("connect", udid);
62
+ connectedIdbSimulators.add(udid);
63
+ return { success: true };
64
+ }
65
+ catch (error) {
66
+ const errorMessage = error instanceof Error ? error.message : String(error);
67
+ // "Already connected" is not an error
68
+ if (errorMessage.includes("already connected") || errorMessage.includes("Connected")) {
69
+ connectedIdbSimulators.add(udid);
70
+ return { success: true };
71
+ }
72
+ return {
73
+ success: false,
74
+ error: `Failed to connect IDB to simulator: ${errorMessage}`
75
+ };
76
+ }
77
+ }
78
+ /**
79
+ * Check if simctl is available
80
+ */
81
+ export async function isSimctlAvailable() {
82
+ try {
83
+ await execAsync("xcrun simctl help", { timeout: 5000 });
84
+ return true;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ /**
91
+ * List iOS simulators
92
+ */
93
+ export async function listIOSSimulators(onlyBooted = false) {
94
+ try {
95
+ const simctlAvailable = await isSimctlAvailable();
96
+ if (!simctlAvailable) {
97
+ return {
98
+ success: false,
99
+ error: "Xcode command line tools not available. Install Xcode from the App Store."
100
+ };
101
+ }
102
+ const { stdout } = await execAsync("xcrun simctl list devices -j", {
103
+ timeout: SIMCTL_TIMEOUT
104
+ });
105
+ const data = JSON.parse(stdout);
106
+ const simulators = [];
107
+ // Parse devices from each runtime
108
+ for (const [runtime, devices] of Object.entries(data.devices)) {
109
+ if (!Array.isArray(devices))
110
+ continue;
111
+ for (const device of devices) {
112
+ if (!device.isAvailable)
113
+ continue;
114
+ if (onlyBooted && device.state !== "Booted")
115
+ continue;
116
+ // Extract iOS version from runtime string
117
+ const runtimeMatch = runtime.match(/iOS[- ](\d+[.-]\d+)/i);
118
+ const runtimeVersion = runtimeMatch ? `iOS ${runtimeMatch[1].replace("-", ".")}` : runtime;
119
+ simulators.push({
120
+ udid: device.udid,
121
+ name: device.name,
122
+ state: device.state,
123
+ runtime: runtimeVersion,
124
+ deviceType: device.deviceTypeIdentifier,
125
+ isAvailable: device.isAvailable
126
+ });
127
+ }
128
+ }
129
+ if (simulators.length === 0) {
130
+ return {
131
+ success: true,
132
+ result: onlyBooted
133
+ ? "No booted iOS simulators. Start a simulator first."
134
+ : "No available iOS simulators found.",
135
+ simulators: []
136
+ };
137
+ }
138
+ // Sort: Booted first, then by name
139
+ simulators.sort((a, b) => {
140
+ if (a.state === "Booted" && b.state !== "Booted")
141
+ return -1;
142
+ if (a.state !== "Booted" && b.state === "Booted")
143
+ return 1;
144
+ return a.name.localeCompare(b.name);
145
+ });
146
+ const formatted = simulators
147
+ .map((s) => {
148
+ const status = s.state === "Booted" ? "🟢 Booted" : "⚪ Shutdown";
149
+ return `${s.name} (${s.runtime}) - ${status}\n UDID: ${s.udid}`;
150
+ })
151
+ .join("\n\n");
152
+ return {
153
+ success: true,
154
+ result: `iOS Simulators:\n\n${formatted}`,
155
+ simulators
156
+ };
157
+ }
158
+ catch (error) {
159
+ return {
160
+ success: false,
161
+ error: `Failed to list simulators: ${error instanceof Error ? error.message : String(error)}`
162
+ };
163
+ }
164
+ }
165
+ /**
166
+ * Get the booted simulator UDID
167
+ */
168
+ export async function getBootedSimulatorUdid() {
169
+ try {
170
+ const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
171
+ timeout: SIMCTL_TIMEOUT
172
+ });
173
+ const data = JSON.parse(stdout);
174
+ for (const devices of Object.values(data.devices)) {
175
+ if (!Array.isArray(devices))
176
+ continue;
177
+ for (const device of devices) {
178
+ if (device.state === "Booted") {
179
+ return device.udid;
180
+ }
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ }
189
+ /**
190
+ * Find a booted simulator's UDID by its device name
191
+ * Matches Metro's deviceName against simulator names from simctl
192
+ */
193
+ export async function findSimulatorByName(deviceName) {
194
+ try {
195
+ const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
196
+ timeout: SIMCTL_TIMEOUT
197
+ });
198
+ const data = JSON.parse(stdout);
199
+ const normalizedDeviceName = deviceName.toLowerCase().trim();
200
+ for (const devices of Object.values(data.devices)) {
201
+ if (!Array.isArray(devices))
202
+ continue;
203
+ for (const device of devices) {
204
+ if (device.state !== "Booted")
205
+ continue;
206
+ const normalizedSimName = device.name.toLowerCase().trim();
207
+ // Exact match
208
+ if (normalizedSimName === normalizedDeviceName) {
209
+ return device.udid;
210
+ }
211
+ // Partial match (deviceName contains simulator name or vice versa)
212
+ if (normalizedSimName.includes(normalizedDeviceName) ||
213
+ normalizedDeviceName.includes(normalizedSimName)) {
214
+ return device.udid;
215
+ }
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ }
224
+ /**
225
+ * Get the active simulator UDID (Metro-connected) or fall back to first booted simulator
226
+ * This enables automatic device scoping based on Metro connection
227
+ */
228
+ export async function getActiveOrBootedSimulatorUdid() {
229
+ // First, check if there's an active Metro-connected simulator
230
+ const activeUdid = getActiveSimulatorUdid();
231
+ if (activeUdid) {
232
+ return activeUdid;
233
+ }
234
+ // Fall back to first booted simulator
235
+ return getBootedSimulatorUdid();
236
+ }
237
+ /**
238
+ * Build device selector for simctl command
239
+ */
240
+ function buildDeviceArg(udid) {
241
+ return udid || "booted";
242
+ }
243
+ /**
244
+ * Take a screenshot from an iOS simulator
245
+ */
246
+ export async function iosScreenshot(outputPath, udid) {
247
+ try {
248
+ const simctlAvailable = await isSimctlAvailable();
249
+ if (!simctlAvailable) {
250
+ return {
251
+ success: false,
252
+ error: "Xcode command line tools not available. Install Xcode from the App Store."
253
+ };
254
+ }
255
+ // Resolve target UDID (prefer Metro-connected simulator)
256
+ const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
257
+ if (!targetUdid) {
258
+ return {
259
+ success: false,
260
+ error: "No iOS simulator is currently running. Start a simulator first."
261
+ };
262
+ }
263
+ // Generate output path if not provided
264
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
265
+ const finalOutputPath = outputPath || path.join(os.tmpdir(), `ios-screenshot-${timestamp}.png`);
266
+ await execAsync(`xcrun simctl io ${targetUdid} screenshot "${finalOutputPath}"`, {
267
+ timeout: SIMCTL_TIMEOUT
268
+ });
269
+ // Resize image if needed (API limit: 2000px max for multi-image requests)
270
+ // Return scale factor so AI can convert image coords to device coords
271
+ const MAX_DIMENSION = 2000;
272
+ const image = sharp(finalOutputPath);
273
+ const metadata = await image.metadata();
274
+ const originalWidth = metadata.width || 0;
275
+ const originalHeight = metadata.height || 0;
276
+ let imageData;
277
+ let scaleFactor = 1;
278
+ if (originalWidth > MAX_DIMENSION || originalHeight > MAX_DIMENSION) {
279
+ // Calculate scale to fit within MAX_DIMENSION
280
+ scaleFactor = Math.max(originalWidth, originalHeight) / MAX_DIMENSION;
281
+ imageData = await image
282
+ .resize(MAX_DIMENSION, MAX_DIMENSION, {
283
+ fit: "inside",
284
+ withoutEnlargement: true
285
+ })
286
+ .jpeg({ quality: 85 })
287
+ .toBuffer();
288
+ }
289
+ else {
290
+ imageData = await image
291
+ .jpeg({ quality: 85 })
292
+ .toBuffer();
293
+ }
294
+ return {
295
+ success: true,
296
+ result: finalOutputPath,
297
+ data: imageData,
298
+ scaleFactor,
299
+ originalWidth,
300
+ originalHeight
301
+ };
302
+ }
303
+ catch (error) {
304
+ return {
305
+ success: false,
306
+ error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`
307
+ };
308
+ }
309
+ }
310
+ /**
311
+ * Install an app on an iOS simulator
312
+ */
313
+ export async function iosInstallApp(appPath, udid) {
314
+ try {
315
+ const simctlAvailable = await isSimctlAvailable();
316
+ if (!simctlAvailable) {
317
+ return {
318
+ success: false,
319
+ error: "Xcode command line tools not available. Install Xcode from the App Store."
320
+ };
321
+ }
322
+ // Verify app exists
323
+ if (!existsSync(appPath)) {
324
+ return {
325
+ success: false,
326
+ error: `App bundle not found: ${appPath}`
327
+ };
328
+ }
329
+ // Resolve target UDID (prefer Metro-connected simulator)
330
+ const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
331
+ if (!targetUdid) {
332
+ return {
333
+ success: false,
334
+ error: "No iOS simulator is currently running. Start a simulator first."
335
+ };
336
+ }
337
+ await execAsync(`xcrun simctl install ${targetUdid} "${appPath}"`, {
338
+ timeout: 120000 // 2 minute timeout for install
339
+ });
340
+ return {
341
+ success: true,
342
+ result: `Successfully installed ${path.basename(appPath)}`
343
+ };
344
+ }
345
+ catch (error) {
346
+ return {
347
+ success: false,
348
+ error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}`
349
+ };
350
+ }
351
+ }
352
+ /**
353
+ * Launch an app on an iOS simulator
354
+ */
355
+ export async function iosLaunchApp(bundleId, udid) {
356
+ try {
357
+ const simctlAvailable = await isSimctlAvailable();
358
+ if (!simctlAvailable) {
359
+ return {
360
+ success: false,
361
+ error: "Xcode command line tools not available. Install Xcode from the App Store."
362
+ };
363
+ }
364
+ // Resolve target UDID (prefer Metro-connected simulator)
365
+ const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
366
+ if (!targetUdid) {
367
+ return {
368
+ success: false,
369
+ error: "No iOS simulator is currently running. Start a simulator first."
370
+ };
371
+ }
372
+ await execAsync(`xcrun simctl launch ${targetUdid} ${bundleId}`, {
373
+ timeout: SIMCTL_TIMEOUT
374
+ });
375
+ return {
376
+ success: true,
377
+ result: `Launched ${bundleId}`
378
+ };
379
+ }
380
+ catch (error) {
381
+ return {
382
+ success: false,
383
+ error: `Failed to launch app: ${error instanceof Error ? error.message : String(error)}`
384
+ };
385
+ }
386
+ }
387
+ /**
388
+ * Open a URL in the iOS simulator
389
+ */
390
+ export async function iosOpenUrl(url, udid) {
391
+ try {
392
+ const simctlAvailable = await isSimctlAvailable();
393
+ if (!simctlAvailable) {
394
+ return {
395
+ success: false,
396
+ error: "Xcode command line tools not available. Install Xcode from the App Store."
397
+ };
398
+ }
399
+ // Resolve target UDID (prefer Metro-connected simulator)
400
+ const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
401
+ if (!targetUdid) {
402
+ return {
403
+ success: false,
404
+ error: "No iOS simulator is currently running. Start a simulator first."
405
+ };
406
+ }
407
+ await execAsync(`xcrun simctl openurl ${targetUdid} "${url}"`, {
408
+ timeout: SIMCTL_TIMEOUT
409
+ });
410
+ return {
411
+ success: true,
412
+ result: `Opened URL: ${url}`
413
+ };
414
+ }
415
+ catch (error) {
416
+ return {
417
+ success: false,
418
+ error: `Failed to open URL: ${error instanceof Error ? error.message : String(error)}`
419
+ };
420
+ }
421
+ }
422
+ /**
423
+ * Terminate an app on an iOS simulator
424
+ */
425
+ export async function iosTerminateApp(bundleId, udid) {
426
+ try {
427
+ const simctlAvailable = await isSimctlAvailable();
428
+ if (!simctlAvailable) {
429
+ return {
430
+ success: false,
431
+ error: "Xcode command line tools not available. Install Xcode from the App Store."
432
+ };
433
+ }
434
+ // Resolve target UDID (prefer Metro-connected simulator)
435
+ const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
436
+ if (!targetUdid) {
437
+ return {
438
+ success: false,
439
+ error: "No iOS simulator is currently running. Start a simulator first."
440
+ };
441
+ }
442
+ await execAsync(`xcrun simctl terminate ${targetUdid} ${bundleId}`, {
443
+ timeout: SIMCTL_TIMEOUT
444
+ });
445
+ return {
446
+ success: true,
447
+ result: `Terminated ${bundleId}`
448
+ };
449
+ }
450
+ catch (error) {
451
+ return {
452
+ success: false,
453
+ error: `Failed to terminate app: ${error instanceof Error ? error.message : String(error)}`
454
+ };
455
+ }
456
+ }
457
+ /**
458
+ * Boot an iOS simulator
459
+ */
460
+ export async function iosBootSimulator(udid) {
461
+ try {
462
+ const simctlAvailable = await isSimctlAvailable();
463
+ if (!simctlAvailable) {
464
+ return {
465
+ success: false,
466
+ error: "Xcode command line tools not available. Install Xcode from the App Store."
467
+ };
468
+ }
469
+ await execAsync(`xcrun simctl boot ${udid}`, {
470
+ timeout: 60000 // 1 minute timeout for boot
471
+ });
472
+ // Open Simulator app
473
+ await execAsync("open -a Simulator", { timeout: 10000 }).catch(() => {
474
+ // Ignore if Simulator app doesn't open
475
+ });
476
+ return {
477
+ success: true,
478
+ result: `Simulator ${udid} is now booting`
479
+ };
480
+ }
481
+ catch (error) {
482
+ const errorMessage = error instanceof Error ? error.message : String(error);
483
+ // Already booted is not an error
484
+ if (errorMessage.includes("Unable to boot device in current state: Booted")) {
485
+ return {
486
+ success: true,
487
+ result: "Simulator is already booted"
488
+ };
489
+ }
490
+ return {
491
+ success: false,
492
+ error: `Failed to boot simulator: ${errorMessage}`
493
+ };
494
+ }
495
+ }
496
+ // ============================================================================
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
500
+ // ============================================================================
501
+ /**
502
+ * Tap at coordinates on an iOS simulator using IDB
503
+ */
504
+ export async function iosTap(x, y, options) {
505
+ 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
+ }
526
+ const xRounded = Math.round(x);
527
+ 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);
537
+ return {
538
+ success: true,
539
+ result: `Tapped at (${xRounded}, ${yRounded})`
540
+ };
541
+ }
542
+ catch (error) {
543
+ return {
544
+ success: false,
545
+ error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`
546
+ };
547
+ }
548
+ }
549
+ /**
550
+ * Swipe gesture on an iOS simulator using IDB
551
+ */
552
+ export async function iosSwipe(startX, startY, endX, endY, options) {
553
+ 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
+ }
573
+ const x1 = Math.round(startX);
574
+ const y1 = Math.round(startY);
575
+ const x2 = Math.round(endX);
576
+ 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);
589
+ return {
590
+ success: true,
591
+ result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})`
592
+ };
593
+ }
594
+ catch (error) {
595
+ return {
596
+ success: false,
597
+ error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`
598
+ };
599
+ }
600
+ }
601
+ /**
602
+ * Input text into the active field on an iOS simulator using IDB
603
+ */
604
+ export async function iosInputText(text, udid) {
605
+ 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
+ };
619
+ }
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 };
624
+ }
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
+ return {
630
+ success: true,
631
+ result: `Typed text: "${text}"`
632
+ };
633
+ }
634
+ catch (error) {
635
+ return {
636
+ success: false,
637
+ error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}`
638
+ };
639
+ }
640
+ }
641
+ /**
642
+ * Press a hardware button on an iOS simulator using IDB
643
+ */
644
+ export async function iosButton(button, options) {
645
+ 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
+ // Validate button type
654
+ if (!IOS_BUTTON_TYPES.includes(button)) {
655
+ return {
656
+ success: false,
657
+ error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}`
658
+ };
659
+ }
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 };
671
+ }
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));
676
+ }
677
+ args.push(button);
678
+ const { stderr } = await runIdb(...args);
679
+ if (stderr)
680
+ throw new Error(stderr);
681
+ return {
682
+ success: true,
683
+ result: `Pressed ${button} button`
684
+ };
685
+ }
686
+ catch (error) {
687
+ return {
688
+ success: false,
689
+ error: `Failed to press button: ${error instanceof Error ? error.message : String(error)}`
690
+ };
691
+ }
692
+ }
693
+ /**
694
+ * Send a key event to an iOS simulator using IDB
695
+ */
696
+ export async function iosKeyEvent(keycode, options) {
697
+ 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 };
716
+ }
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));
721
+ }
722
+ args.push(String(keycode));
723
+ const { stderr } = await runIdb(...args);
724
+ if (stderr)
725
+ throw new Error(stderr);
726
+ return {
727
+ success: true,
728
+ result: `Sent key event: ${keycode}`
729
+ };
730
+ }
731
+ catch (error) {
732
+ return {
733
+ success: false,
734
+ error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}`
735
+ };
736
+ }
737
+ }
738
+ /**
739
+ * Send a sequence of key events to an iOS simulator using IDB
740
+ */
741
+ export async function iosKeySequence(keycodes, udid) {
742
+ 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
+ if (!keycodes || keycodes.length === 0) {
751
+ return {
752
+ success: false,
753
+ error: "At least one keycode is required"
754
+ };
755
+ }
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
+ };
762
+ }
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 };
767
+ }
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
+ return {
774
+ success: true,
775
+ result: `Sent key sequence: ${keycodes.join(", ")}`
776
+ };
777
+ }
778
+ catch (error) {
779
+ return {
780
+ success: false,
781
+ error: `Failed to send key sequence: ${error instanceof Error ? error.message : String(error)}`
782
+ };
783
+ }
784
+ }
785
+ /**
786
+ * Format accessibility tree for human-readable output
787
+ */
788
+ function formatAccessibilityTree(elements, depth = 0) {
789
+ const lines = [];
790
+ const indent = " ".repeat(depth);
791
+ for (const element of elements) {
792
+ const parts = [];
793
+ if (element.type)
794
+ parts.push(`[${element.type}]`);
795
+ if (element.AXLabel)
796
+ parts.push(`"${element.AXLabel}"`);
797
+ if (element.AXValue)
798
+ parts.push(`value="${element.AXValue}"`);
799
+ if (element.frame) {
800
+ const f = element.frame;
801
+ const centerX = Math.round(f.x + f.width / 2);
802
+ const centerY = Math.round(f.y + f.height / 2);
803
+ parts.push(`frame=(${f.x}, ${f.y}, ${f.width}x${f.height}) tap=(${centerX}, ${centerY})`);
804
+ }
805
+ if (parts.length > 0) {
806
+ lines.push(`${indent}${parts.join(" ")}`);
807
+ }
808
+ if (element.children && element.children.length > 0) {
809
+ lines.push(formatAccessibilityTree(element.children, depth + 1));
810
+ }
811
+ }
812
+ return lines.join("\n");
813
+ }
814
+ /**
815
+ * Get accessibility info for the entire screen using IDB
816
+ */
817
+ export async function iosDescribeAll(udid) {
818
+ 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
+ };
832
+ }
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 };
837
+ }
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
+ // Parse JSON response
843
+ const elements = JSON.parse(stdout);
844
+ // Format for human-readable output
845
+ const formatted = formatAccessibilityTree(elements);
846
+ return {
847
+ success: true,
848
+ result: formatted || "No accessibility elements found",
849
+ elements
850
+ };
851
+ }
852
+ catch (error) {
853
+ return {
854
+ success: false,
855
+ error: `Failed to describe screen: ${error instanceof Error ? error.message : String(error)}`
856
+ };
857
+ }
858
+ }
859
+ /**
860
+ * Get accessibility info at a specific point using IDB
861
+ */
862
+ export async function iosDescribePoint(x, y, udid) {
863
+ 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
+ }
883
+ const xRounded = Math.round(x);
884
+ 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);
889
+ // Parse JSON response - may be single element or array
890
+ let element;
891
+ try {
892
+ const parsed = JSON.parse(stdout);
893
+ element = Array.isArray(parsed) ? parsed[0] : parsed;
894
+ }
895
+ catch {
896
+ return {
897
+ success: true,
898
+ result: `No accessibility element found at (${xRounded}, ${yRounded})`,
899
+ elements: []
900
+ };
901
+ }
902
+ // Format for human-readable output
903
+ const parts = [];
904
+ if (element.type)
905
+ parts.push(`Type: ${element.type}`);
906
+ if (element.AXLabel)
907
+ parts.push(`Label: "${element.AXLabel}"`);
908
+ if (element.AXValue)
909
+ parts.push(`Value: "${element.AXValue}"`);
910
+ if (element.frame) {
911
+ const f = element.frame;
912
+ const centerX = Math.round(f.x + f.width / 2);
913
+ const centerY = Math.round(f.y + f.height / 2);
914
+ parts.push(`Frame: (${f.x}, ${f.y}) ${f.width}x${f.height}`);
915
+ parts.push(`Tap: (${centerX}, ${centerY})`);
916
+ }
917
+ return {
918
+ success: true,
919
+ result: parts.length > 0
920
+ ? `Element at (${xRounded}, ${yRounded}):\n${parts.join("\n")}`
921
+ : `No accessibility element found at (${xRounded}, ${yRounded})`,
922
+ elements: element ? [element] : []
923
+ };
924
+ }
925
+ catch (error) {
926
+ return {
927
+ success: false,
928
+ error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`
929
+ };
930
+ }
931
+ }
932
+ /**
933
+ * Helper to flatten nested accessibility elements
934
+ */
935
+ function flattenElements(elements) {
936
+ const result = [];
937
+ for (const el of elements) {
938
+ result.push(el);
939
+ if (el.children && el.children.length > 0) {
940
+ result.push(...flattenElements(el.children));
941
+ }
942
+ }
943
+ return result;
944
+ }
945
+ /**
946
+ * Tap an element by its accessibility label using IDB
947
+ * This simplifies the workflow: no need to manually find coordinates
948
+ */
949
+ export async function iosTapElement(options) {
950
+ try {
951
+ const { label, labelContains, index = 0, duration, udid } = options;
952
+ if (!label && !labelContains) {
953
+ return {
954
+ success: false,
955
+ error: "Either 'label' (exact match) or 'labelContains' (partial match) is required"
956
+ };
957
+ }
958
+ // Get all accessibility elements
959
+ const describeResult = await iosDescribeAll(udid);
960
+ if (!describeResult.success || !describeResult.elements) {
961
+ return {
962
+ success: false,
963
+ error: describeResult.error || "Failed to get accessibility elements"
964
+ };
965
+ }
966
+ // Flatten the tree and find matching elements
967
+ const allElements = flattenElements(describeResult.elements);
968
+ const matches = allElements.filter(el => {
969
+ if (!el.AXLabel)
970
+ return false;
971
+ if (label)
972
+ return el.AXLabel === label;
973
+ if (labelContains)
974
+ return el.AXLabel.toLowerCase().includes(labelContains.toLowerCase());
975
+ return false;
976
+ });
977
+ if (matches.length === 0) {
978
+ const searchTerm = label ? `label="${label}"` : `labelContains="${labelContains}"`;
979
+ return {
980
+ success: false,
981
+ error: `No element found with ${searchTerm}`
982
+ };
983
+ }
984
+ // Select element by index (default 0 = first match)
985
+ if (index >= matches.length) {
986
+ return {
987
+ success: false,
988
+ error: `Index ${index} out of range. Found ${matches.length} matching element(s).`
989
+ };
990
+ }
991
+ const element = matches[index];
992
+ // Check if element has frame coordinates
993
+ if (!element.frame) {
994
+ return {
995
+ success: false,
996
+ error: `Element "${element.AXLabel}" has no frame coordinates`
997
+ };
998
+ }
999
+ // Calculate center
1000
+ const centerX = Math.round(element.frame.x + element.frame.width / 2);
1001
+ const centerY = Math.round(element.frame.y + element.frame.height / 2);
1002
+ // Tap at center
1003
+ const tapResult = await iosTap(centerX, centerY, { duration, udid });
1004
+ if (tapResult.success) {
1005
+ return {
1006
+ success: true,
1007
+ result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})`
1008
+ };
1009
+ }
1010
+ return tapResult;
1011
+ }
1012
+ catch (error) {
1013
+ return {
1014
+ success: false,
1015
+ error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`
1016
+ };
1017
+ }
1018
+ }
1019
+ /**
1020
+ * Parse IDB accessibility output into simplified element array
1021
+ */
1022
+ function parseIdbAccessibilityForFindElement(output) {
1023
+ const elements = [];
1024
+ try {
1025
+ const data = JSON.parse(output);
1026
+ const extractElements = (node) => {
1027
+ const frame = node.frame;
1028
+ if (frame) {
1029
+ const element = {
1030
+ label: node.AXLabel || node.label || "",
1031
+ value: node.AXValue || node.value || "",
1032
+ type: node.type || node.AXType || "",
1033
+ frame: {
1034
+ x: frame.x || 0,
1035
+ y: frame.y || 0,
1036
+ width: frame.width || 0,
1037
+ height: frame.height || 0
1038
+ },
1039
+ center: {
1040
+ x: Math.round((frame.x || 0) + (frame.width || 0) / 2),
1041
+ y: Math.round((frame.y || 0) + (frame.height || 0) / 2)
1042
+ },
1043
+ enabled: node.enabled !== false,
1044
+ traits: node.traits || []
1045
+ };
1046
+ if (element.label || element.value || element.type) {
1047
+ elements.push(element);
1048
+ }
1049
+ }
1050
+ const children = node.children;
1051
+ if (children && Array.isArray(children)) {
1052
+ for (const child of children) {
1053
+ extractElements(child);
1054
+ }
1055
+ }
1056
+ };
1057
+ if (Array.isArray(data)) {
1058
+ for (const item of data) {
1059
+ extractElements(item);
1060
+ }
1061
+ }
1062
+ else {
1063
+ extractElements(data);
1064
+ }
1065
+ }
1066
+ catch {
1067
+ // If JSON parsing fails, return empty array
1068
+ }
1069
+ return elements;
1070
+ }
1071
+ /**
1072
+ * Match iOS element against find options
1073
+ */
1074
+ function matchesIOSFindElement(element, options) {
1075
+ if (options.label !== undefined) {
1076
+ if (element.label !== options.label)
1077
+ return false;
1078
+ }
1079
+ if (options.labelContains !== undefined) {
1080
+ if (!element.label.toLowerCase().includes(options.labelContains.toLowerCase()))
1081
+ return false;
1082
+ }
1083
+ if (options.value !== undefined) {
1084
+ if (element.value !== options.value)
1085
+ return false;
1086
+ }
1087
+ if (options.valueContains !== undefined) {
1088
+ if (!element.value.toLowerCase().includes(options.valueContains.toLowerCase()))
1089
+ return false;
1090
+ }
1091
+ if (options.type !== undefined) {
1092
+ if (!element.type.toLowerCase().includes(options.type.toLowerCase()))
1093
+ return false;
1094
+ }
1095
+ return true;
1096
+ }
1097
+ /**
1098
+ * Get UI accessibility tree from iOS simulator using IDB (for find_element)
1099
+ */
1100
+ export async function iosGetUITree(udid) {
1101
+ 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
+ };
1108
+ }
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 };
1119
+ }
1120
+ const { stdout } = await runIdb("ui", "describe-all", "--udid", targetUdid);
1121
+ const elements = parseIdbAccessibilityForFindElement(stdout);
1122
+ return {
1123
+ success: true,
1124
+ elements,
1125
+ rawOutput: stdout
1126
+ };
1127
+ }
1128
+ catch (error) {
1129
+ return {
1130
+ success: false,
1131
+ error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`
1132
+ };
1133
+ }
1134
+ }
1135
+ /**
1136
+ * Find element(s) in the iOS UI tree matching the given criteria
1137
+ */
1138
+ export async function iosFindElement(options, udid) {
1139
+ try {
1140
+ if (!options.label && !options.labelContains && !options.value &&
1141
+ !options.valueContains && !options.type) {
1142
+ return {
1143
+ success: false,
1144
+ found: false,
1145
+ error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided"
1146
+ };
1147
+ }
1148
+ const treeResult = await iosGetUITree(udid);
1149
+ if (!treeResult.success || !treeResult.elements) {
1150
+ return {
1151
+ success: false,
1152
+ found: false,
1153
+ error: treeResult.error
1154
+ };
1155
+ }
1156
+ const matches = treeResult.elements.filter(el => matchesIOSFindElement(el, options));
1157
+ if (matches.length === 0) {
1158
+ return {
1159
+ success: true,
1160
+ found: false,
1161
+ matchCount: 0
1162
+ };
1163
+ }
1164
+ const index = options.index ?? 0;
1165
+ const selectedElement = matches[index];
1166
+ if (!selectedElement) {
1167
+ return {
1168
+ success: true,
1169
+ found: false,
1170
+ matchCount: matches.length,
1171
+ error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`
1172
+ };
1173
+ }
1174
+ return {
1175
+ success: true,
1176
+ found: true,
1177
+ element: selectedElement,
1178
+ allMatches: matches,
1179
+ matchCount: matches.length
1180
+ };
1181
+ }
1182
+ catch (error) {
1183
+ return {
1184
+ success: false,
1185
+ found: false,
1186
+ error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`
1187
+ };
1188
+ }
1189
+ }
1190
+ /**
1191
+ * Wait for element to appear on iOS screen with polling
1192
+ */
1193
+ export async function iosWaitForElement(options, udid) {
1194
+ const timeoutMs = options.timeoutMs ?? 10000;
1195
+ const pollIntervalMs = options.pollIntervalMs ?? 500;
1196
+ const startTime = Date.now();
1197
+ if (!options.label && !options.labelContains && !options.value &&
1198
+ !options.valueContains && !options.type) {
1199
+ return {
1200
+ success: false,
1201
+ found: false,
1202
+ timedOut: false,
1203
+ error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided"
1204
+ };
1205
+ }
1206
+ while (Date.now() - startTime < timeoutMs) {
1207
+ const result = await iosFindElement(options, udid);
1208
+ if (result.found && result.element) {
1209
+ return {
1210
+ ...result,
1211
+ elapsedMs: Date.now() - startTime,
1212
+ timedOut: false
1213
+ };
1214
+ }
1215
+ if (!result.success) {
1216
+ return {
1217
+ ...result,
1218
+ elapsedMs: Date.now() - startTime,
1219
+ timedOut: false
1220
+ };
1221
+ }
1222
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
1223
+ }
1224
+ return {
1225
+ success: true,
1226
+ found: false,
1227
+ elapsedMs: Date.now() - startTime,
1228
+ timedOut: true,
1229
+ error: `Timed out after ${timeoutMs}ms waiting for element`
1230
+ };
1231
+ }
1232
+ //# sourceMappingURL=ios.js.map