react-native-ai-devtools 1.4.17 → 1.4.18

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