appium-xcuitest-driver 11.12.2 → 11.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [11.13.0](https://github.com/appium/appium-xcuitest-driver/compare/v11.12.2...v11.13.0) (2026-06-20)
2
+
3
+ ### Features
4
+
5
+ * Make Apple TV discovery timeout configurable ([#2882](https://github.com/appium/appium-xcuitest-driver/issues/2882)) ([e9e006b](https://github.com/appium/appium-xcuitest-driver/commit/e9e006b5de99930e41602dc5e540ad0d3a375244))
6
+
1
7
  ## [11.12.2](https://github.com/appium/appium-xcuitest-driver/compare/v11.12.1...v11.12.2) (2026-06-19)
2
8
 
3
9
  ### Bug Fixes
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "appium-xcuitest-driver",
3
- "version": "11.12.2",
3
+ "version": "11.13.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "appium-xcuitest-driver",
9
- "version": "11.12.2",
9
+ "version": "11.13.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@appium/css-locator-to-native": "^1.0.1",
@@ -41,7 +41,7 @@
41
41
  "@semantic-release/changelog": "^6.0.3",
42
42
  "@semantic-release/git": "^10.0.1",
43
43
  "@types/mocha": "^10.0.1",
44
- "@types/node": "^25.0.0",
44
+ "@types/node": "^26.0.0",
45
45
  "@types/portscanner": "^2.1.1",
46
46
  "chai": "^6.0.0",
47
47
  "chai-as-promised": "^8.0.0",
@@ -63,7 +63,7 @@
63
63
  "npm": ">=10"
64
64
  },
65
65
  "optionalDependencies": {
66
- "appium-ios-remotexpc": "^5.1.0"
66
+ "appium-ios-remotexpc": "^5.1.5"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "appium": "^3.0.0-rc.2"
@@ -288,6 +288,18 @@
288
288
  "url": "https://opencollective.com/libvips"
289
289
  }
290
290
  },
291
+ "node_modules/@appium/support/node_modules/semver": {
292
+ "version": "7.8.4",
293
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
294
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
295
+ "license": "ISC",
296
+ "bin": {
297
+ "semver": "bin/semver.js"
298
+ },
299
+ "engines": {
300
+ "node": ">=10"
301
+ }
302
+ },
291
303
  "node_modules/@appium/support/node_modules/sharp": {
292
304
  "version": "0.35.1",
293
305
  "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.1.tgz",
@@ -513,13 +525,13 @@
513
525
  }
514
526
  },
515
527
  "node_modules/appium-ios-remotexpc": {
516
- "version": "5.1.0",
517
- "resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-5.1.0.tgz",
518
- "integrity": "sha512-/q6C2Rwreg69p6R/d29nkdjQewJzjenIAhGPHQiO5PM51JHcPXB6DPwVL5O49iin0eAGgFahp33aGsX+92WANA==",
528
+ "version": "5.1.6",
529
+ "resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-5.1.6.tgz",
530
+ "integrity": "sha512-PNuFaoL5X0bL04RRNqb6EmEtUYRsd6rmMHm9biHmvtBWPeq7WIBdlFkB5Z5qOyiOjb0TXE9WdvGwzw47QcFUJw==",
519
531
  "license": "Apache-2.0",
520
532
  "optional": true,
521
533
  "dependencies": {
522
- "@appium/strongbox": "^1.0.0-rc.1",
534
+ "@appium/strongbox": "^1.1.2",
523
535
  "@appium/support": "^7.2.2",
524
536
  "@xmldom/xmldom": "^0.x",
525
537
  "appium-ios-tuntap": "^1.0.0",
@@ -537,9 +549,9 @@
537
549
  }
538
550
  },
539
551
  "node_modules/appium-ios-simulator": {
540
- "version": "8.2.2",
541
- "resolved": "https://registry.npmjs.org/appium-ios-simulator/-/appium-ios-simulator-8.2.2.tgz",
542
- "integrity": "sha512-NmpbM+CpnSHNoEvCYWXkzvh+fhNd8pcYhpIirw/hvlVbZnG/Gs0Bw6+sU20uOw8O/uzziEgC6odogzpJ4AO70Q==",
552
+ "version": "8.2.3",
553
+ "resolved": "https://registry.npmjs.org/appium-ios-simulator/-/appium-ios-simulator-8.2.3.tgz",
554
+ "integrity": "sha512-naM9JLNT7uZv+IVCMNMtifgiRbiNvRZ0e1SHPo2xPa4dF0vKlqSDdA7WVFguh3V+Sfty3L6HCVY6VRdbyQ+2Ew==",
543
555
  "license": "Apache-2.0",
544
556
  "dependencies": {
545
557
  "@appium/support": "^7.2.2",
@@ -597,9 +609,9 @@
597
609
  }
598
610
  },
599
611
  "node_modules/appium-webdriveragent": {
600
- "version": "14.2.0",
601
- "resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-14.2.0.tgz",
602
- "integrity": "sha512-2x0aN/mpbLfs7CqJvKeaunq3s+I+jN//qqk6jRuf1e32pNv3Z/RSkoO/ibeVpXgIWlOntBnt4pEsQX0pYifCuQ==",
612
+ "version": "14.2.1",
613
+ "resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-14.2.1.tgz",
614
+ "integrity": "sha512-iHFi+ViRzBOeIjSZZ+R94lVUdGCHBplA8GmDhNxd/0PGh0IfuAlfvz8n3/7AvApebpgjMArKcowGXILo0SwE1A==",
603
615
  "license": "Apache-2.0",
604
616
  "dependencies": {
605
617
  "@appium/base-driver": "^10.3.0",
@@ -3063,9 +3075,9 @@
3063
3075
  "optional": true
3064
3076
  },
3065
3077
  "node_modules/semver": {
3066
- "version": "7.8.4",
3067
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
3068
- "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
3078
+ "version": "7.8.5",
3079
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
3080
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
3069
3081
  "license": "ISC",
3070
3082
  "bin": {
3071
3083
  "semver": "bin/semver.js"
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "xcuitest",
9
9
  "xctest"
10
10
  ],
11
- "version": "11.12.2",
11
+ "version": "11.13.0",
12
12
  "author": "Appium Contributors",
13
13
  "license": "Apache-2.0",
14
14
  "repository": {
@@ -107,7 +107,7 @@
107
107
  "ws": "^8.13.0"
108
108
  },
109
109
  "optionalDependencies": {
110
- "appium-ios-remotexpc": "^5.1.0"
110
+ "appium-ios-remotexpc": "^5.1.5"
111
111
  },
112
112
  "scripts": {
113
113
  "build": "tsc -b",
@@ -151,7 +151,7 @@
151
151
  "@semantic-release/changelog": "^6.0.3",
152
152
  "@semantic-release/git": "^10.0.1",
153
153
  "@types/mocha": "^10.0.1",
154
- "@types/node": "^25.0.0",
154
+ "@types/node": "^26.0.0",
155
155
  "@types/portscanner": "^2.1.1",
156
156
  "chai": "^6.0.0",
157
157
  "chai-as-promised": "^8.0.0",
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @param {string} value
3
+ * @param {string} label
4
+ * @returns {number}
5
+ */
6
+ export function parsePositiveIntegerOption(value, label) {
7
+ const num = Number.parseInt(value, 10);
8
+ if (!Number.isFinite(num) || num <= 0) {
9
+ throw new Error(`Invalid ${label}: ${value}. Expected a positive integer.`);
10
+ }
11
+ return num;
12
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Logs a timeout-based progress bar while an operation without native progress callbacks is running.
3
+ *
4
+ * @param {{log: {info: (message: string) => void}, label: string, startedAt: number, timeoutMs: number, barWidth: number, intervalMs: number}} opts
5
+ * @returns {{succeed: (message?: string) => void, fail: (message?: string) => void}}
6
+ */
7
+ export function startTimeoutProgressLogger({log, label, startedAt, timeoutMs, barWidth, intervalMs}) {
8
+ /** @type {NodeJS.Timeout | null} */
9
+ let timer = null;
10
+ let isStopped = false;
11
+
12
+ const logProgress = (status, isComplete = false) => {
13
+ const elapsedMs = performance.now() - startedAt;
14
+ const boundedElapsedMs = Math.min(elapsedMs, timeoutMs);
15
+ const progress = isComplete ? 1 : boundedElapsedMs / timeoutMs;
16
+ const filledWidth = Math.round(progress * barWidth);
17
+ const emptyWidth = barWidth - filledWidth;
18
+ const bar = `${'#'.repeat(filledWidth)}${'-'.repeat(emptyWidth)}`;
19
+ log.info(`${label}: [${bar}]${status && status !== 'waiting' ? ` - ${status}` : ''}`);
20
+ };
21
+
22
+ const stop = (status, isComplete = false) => {
23
+ if (isStopped) {
24
+ return;
25
+ }
26
+ isStopped = true;
27
+ if (timer) {
28
+ clearInterval(timer);
29
+ timer = null;
30
+ }
31
+ logProgress(status, isComplete);
32
+ };
33
+
34
+ logProgress('waiting');
35
+ timer = setInterval(() => {
36
+ logProgress('waiting');
37
+ }, intervalMs);
38
+ timer.unref?.();
39
+
40
+ return {
41
+ succeed: (message = 'done') => stop(message, true),
42
+ fail: (message = 'failed') => stop(message),
43
+ };
44
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Ensures a driver helper script is run with elevated privileges on platforms that expose getuid.
3
+ *
4
+ * @param {string} scriptName
5
+ */
6
+ export function assertRoot(scriptName) {
7
+ if (typeof process.getuid !== 'function') {
8
+ return;
9
+ }
10
+ if (process.getuid() !== 0) {
11
+ throw new Error(
12
+ `This script must be run as root (e.g. sudo appium driver run xcuitest ${scriptName} ...).`,
13
+ );
14
+ }
15
+ }
@@ -16,27 +16,26 @@
16
16
  * - Device name (e.g. "Living Room")
17
17
  * - Device identifier (e.g. "AA:BB:CC:DD:EE:FF")
18
18
  * - Device index (e.g. "0", "1", "2")
19
+ * --discovery-timeout-ms <ms>
20
+ * Apple TV pairing discovery timeout in milliseconds
19
21
  */
20
22
 
21
23
  import {logger} from 'appium/support.js';
22
24
  import {Command} from 'commander';
23
25
  import {AppleTVPairingService, UserInputService} from 'appium-ios-remotexpc';
24
26
 
25
- const log = logger.getLogger('AppleTVPairing');
27
+ import {parsePositiveIntegerOption} from './lib/options.mjs';
28
+ import {startTimeoutProgressLogger} from './lib/progress.mjs';
29
+ import {assertRoot} from './lib/root.mjs';
26
30
 
27
- function assertRoot() {
28
- if (typeof process.getuid !== 'function') {
29
- return;
30
- }
31
- if (process.getuid() !== 0) {
32
- throw new Error(
33
- 'This script must be run as root (e.g. sudo appium driver run xcuitest pair-appletv ...).',
34
- );
35
- }
36
- }
31
+ const log = logger.getLogger('AppleTVPairing');
32
+ const APPLETV_PAIRING_DISCOVERY_PROGRESS_INTERVAL_MS = 1000;
33
+ const DEFAULT_APPLETV_PAIRING_DISCOVERY_TIMEOUT_MS =
34
+ Number(process.env.APPLETV_DISCOVERY_TIMEOUT) || 10_000;
35
+ const APPLETV_PAIRING_DISCOVERY_PROGRESS_BAR_WIDTH = 24;
37
36
 
38
37
  async function main() {
39
- assertRoot();
38
+ assertRoot('pair-appletv');
40
39
  const program = new Command();
41
40
  program
42
41
  .name('appium driver run xcuitest pair-appletv')
@@ -44,6 +43,12 @@ async function main() {
44
43
  .option(
45
44
  '-d, --device <selector>',
46
45
  'Apple TV device selector (name, identifier, or index)',
46
+ )
47
+ .option(
48
+ '--discovery-timeout-ms <ms>',
49
+ 'Apple TV pairing discovery timeout in milliseconds',
50
+ (value) => parsePositiveIntegerOption(value, 'discovery timeout'),
51
+ DEFAULT_APPLETV_PAIRING_DISCOVERY_TIMEOUT_MS,
47
52
  );
48
53
 
49
54
  program.parse(process.argv);
@@ -52,9 +57,25 @@ async function main() {
52
57
  const userInput = new UserInputService();
53
58
  const pairingService = new AppleTVPairingService(userInput);
54
59
 
55
- const result = await pairingService.discoverAndPair(options.device);
60
+ const devices = await discoverAppleTVPairingDevices(
61
+ pairingService,
62
+ options.discoveryTimeoutMs,
63
+ );
64
+ if (devices.length === 0) {
65
+ log.info(getNoAppleTVPairingDevicesMessage());
66
+ return;
67
+ }
68
+
69
+ const result = await pairingService.discoverAndPair(options.device, {
70
+ devices,
71
+ discoveryTimeoutMs: options.discoveryTimeoutMs,
72
+ });
56
73
 
57
74
  if (!result.success) {
75
+ if (isNoAppleTVPairingDevicesFoundError(result.error)) {
76
+ log.info(getNoAppleTVPairingDevicesMessage());
77
+ return;
78
+ }
58
79
  throw result.error ?? new Error('Pairing failed');
59
80
  }
60
81
 
@@ -64,5 +85,57 @@ async function main() {
64
85
  );
65
86
  }
66
87
 
88
+ /**
89
+ * @param {import('appium-ios-remotexpc').AppleTVPairingService} pairingService
90
+ * @param {number} discoveryTimeoutMs
91
+ * @returns {Promise<AppleTVDevice[]>}
92
+ */
93
+ async function discoverAppleTVPairingDevices(pairingService, discoveryTimeoutMs) {
94
+ const startedAt = performance.now();
95
+ const pairingDiscoveryProgress = startTimeoutProgressLogger({
96
+ log,
97
+ label: 'Waiting for Apple TV pairing discovery',
98
+ startedAt,
99
+ timeoutMs: discoveryTimeoutMs,
100
+ barWidth: APPLETV_PAIRING_DISCOVERY_PROGRESS_BAR_WIDTH,
101
+ intervalMs: APPLETV_PAIRING_DISCOVERY_PROGRESS_INTERVAL_MS,
102
+ });
103
+
104
+ try {
105
+ const devices = await pairingService.discoverDevices({
106
+ timeoutMs: discoveryTimeoutMs,
107
+ });
108
+ pairingDiscoveryProgress.succeed(
109
+ `Apple TV pairing discovery completed: ${devices.length} device(s) found`,
110
+ );
111
+ return devices;
112
+ } catch (err) {
113
+ pairingDiscoveryProgress.fail('Apple TV pairing discovery failed');
114
+ throw err;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * @param {unknown} err
120
+ * @returns {boolean}
121
+ */
122
+ function isNoAppleTVPairingDevicesFoundError(err) {
123
+ return (
124
+ err instanceof Error &&
125
+ (err.message === getNoAppleTVPairingDevicesMessage() ||
126
+ ('code' in err && err.code === 'NO_DEVICES'))
127
+ );
128
+ }
129
+
130
+ /**
131
+ * @returns {string}
132
+ */
133
+ function getNoAppleTVPairingDevicesMessage() {
134
+ return 'No Apple TV pairing devices found. Please ensure your Apple TV is on the same network and in pairing mode.';
135
+ }
67
136
 
68
137
  await main();
138
+
139
+ /**
140
+ * @typedef {import('appium-ios-remotexpc').AppleTVDevice} AppleTVDevice
141
+ */
@@ -24,19 +24,28 @@ import {
24
24
  import {strongbox, BaseItem} from '@appium/strongbox';
25
25
  import {Command} from 'commander';
26
26
 
27
+ import {parsePositiveIntegerOption} from './lib/options.mjs';
28
+ import {startTimeoutProgressLogger} from './lib/progress.mjs';
29
+ import {assertRoot} from './lib/root.mjs';
30
+
27
31
  const log = logger.getLogger('TunnelCreation');
28
32
  const TUNNEL_REGISTRY_PORT = 'tunnelRegistryPort';
29
33
  const DEFAULT_TUNNEL_REGISTRY_PORT = 42314;
30
34
  const WIRELESS_APPLETV_DISCOVERY_PROGRESS_INTERVAL_MS = 1000;
31
- const WIRELESS_APPLETV_DISCOVERY_TIMEOUT_MS = 10_000;
35
+ const DEFAULT_WIRELESS_APPLETV_DISCOVERY_TIMEOUT_MS = 10_000;
32
36
  const WIRELESS_APPLETV_DISCOVERY_PROGRESS_BAR_WIDTH = 24;
33
37
 
34
38
  /**
35
39
  * TunnelCreator class for managing tunnel creation and related operations (USB and optional Apple TV over WiFi).
36
40
  */
37
41
  class TunnelCreator {
38
- constructor() {
42
+ /**
43
+ * @param {{appleTVDiscoveryTimeoutMs?: number}} [opts]
44
+ */
45
+ constructor(opts = {}) {
39
46
  this._tunnelRegistryPort = DEFAULT_TUNNEL_REGISTRY_PORT;
47
+ this._appleTVDiscoveryTimeoutMs =
48
+ opts.appleTVDiscoveryTimeoutMs ?? DEFAULT_WIRELESS_APPLETV_DISCOVERY_TIMEOUT_MS;
40
49
  /** @type {import('appium-ios-remotexpc').TunnelRegistryServer | null} */
41
50
  this._registryServer = null;
42
51
  /** @type {import('appium-ios-remotexpc').TunnelReadinessCoordinator} */
@@ -412,13 +421,14 @@ class TunnelCreator {
412
421
 
413
422
  /**
414
423
  * @param {string[] | undefined} specificDeviceIds
415
- * @returns {{startedAt: number, promise: Promise<{devices: AppleTVDevice[] | null, error: unknown | null}>}}
424
+ * @returns {{startedAt: number, timeoutMs: number, promise: Promise<{devices: AppleTVDevice[] | null, error: unknown | null}>}}
416
425
  */
417
426
  prefetchAppleTVDevices(specificDeviceIds) {
418
427
  const startedAt = performance.now();
419
428
  if (specificDeviceIds && specificDeviceIds.length > 0) {
420
429
  return {
421
430
  startedAt,
431
+ timeoutMs: this._appleTVDiscoveryTimeoutMs,
422
432
  promise: Promise.resolve({devices: null, error: null}),
423
433
  };
424
434
  }
@@ -426,7 +436,7 @@ class TunnelCreator {
426
436
  const promise = (async () => {
427
437
  try {
428
438
  const devices = await tunnelService.discoverDevices({
429
- timeoutMs: WIRELESS_APPLETV_DISCOVERY_TIMEOUT_MS,
439
+ timeoutMs: this._appleTVDiscoveryTimeoutMs,
430
440
  });
431
441
  return {devices, error: null};
432
442
  } catch (err) {
@@ -437,7 +447,7 @@ class TunnelCreator {
437
447
  } catch {}
438
448
  }
439
449
  })();
440
- return {startedAt, promise};
450
+ return {startedAt, timeoutMs: this._appleTVDiscoveryTimeoutMs, promise};
441
451
  }
442
452
 
443
453
  /**
@@ -454,7 +464,7 @@ class TunnelCreator {
454
464
 
455
465
  const startResult = await tunnelService.startTunnel(undefined, udid, {
456
466
  devices: prefetchedDevice ? [prefetchedDevice] : undefined,
457
- discoveryTimeoutMs: WIRELESS_APPLETV_DISCOVERY_TIMEOUT_MS,
467
+ discoveryTimeoutMs: this._appleTVDiscoveryTimeoutMs,
458
468
  });
459
469
  if (!startResult.tcpSocket) {
460
470
  throw new Error('Apple TV TCP socket to listener port not established');
@@ -740,14 +750,15 @@ function isNoAppleTVDevicesFoundError(err) {
740
750
  }
741
751
 
742
752
  /**
743
- * @param {{startedAt: number, promise: Promise<{devices: AppleTVDevice[] | null, error: unknown | null}>}} discovery
753
+ * @param {{startedAt: number, timeoutMs: number, promise: Promise<{devices: AppleTVDevice[] | null, error: unknown | null}>}} discovery
744
754
  * @returns {Promise<AppleTVDevice[] | null>}
745
755
  */
746
756
  async function waitForAppleTVDiscovery(discovery) {
747
757
  const wirelessDiscoveryProgress = startTimeoutProgressLogger({
758
+ log,
748
759
  label: 'Waiting for wireless Apple TV discovery',
749
760
  startedAt: discovery.startedAt,
750
- timeoutMs: WIRELESS_APPLETV_DISCOVERY_TIMEOUT_MS,
761
+ timeoutMs: discovery.timeoutMs,
751
762
  barWidth: WIRELESS_APPLETV_DISCOVERY_PROGRESS_BAR_WIDTH,
752
763
  intervalMs: WIRELESS_APPLETV_DISCOVERY_PROGRESS_INTERVAL_MS,
753
764
  });
@@ -775,51 +786,6 @@ async function sleep(ms) {
775
786
  await new Promise((resolve) => setTimeout(resolve, ms));
776
787
  }
777
788
 
778
- /**
779
- * Logs a timeout-based progress bar while an operation without native progress callbacks is running.
780
- *
781
- * @param {{label: string, startedAt: number, timeoutMs: number, barWidth: number, intervalMs: number}} opts
782
- * @returns {{succeed: (message?: string) => void, fail: (message?: string) => void}}
783
- */
784
- function startTimeoutProgressLogger({label, startedAt, timeoutMs, barWidth, intervalMs}) {
785
- /** @type {NodeJS.Timeout | null} */
786
- let timer = null;
787
- let isStopped = false;
788
-
789
- const logProgress = (status, isComplete = false) => {
790
- const elapsedMs = performance.now() - startedAt;
791
- const boundedElapsedMs = Math.min(elapsedMs, timeoutMs);
792
- const progress = isComplete ? 1 : boundedElapsedMs / timeoutMs;
793
- const filledWidth = Math.round(progress * barWidth);
794
- const emptyWidth = barWidth - filledWidth;
795
- const bar = `${'#'.repeat(filledWidth)}${'-'.repeat(emptyWidth)}`;
796
- log.info(`${label}: [${bar}]${status && status !== 'waiting' ? ` - ${status}` : ''}`);
797
- };
798
-
799
- const stop = (status, isComplete = false) => {
800
- if (isStopped) {
801
- return;
802
- }
803
- isStopped = true;
804
- if (timer) {
805
- clearInterval(timer);
806
- timer = null;
807
- }
808
- logProgress(status, isComplete);
809
- };
810
-
811
- logProgress('waiting');
812
- timer = setInterval(() => {
813
- logProgress('waiting');
814
- }, intervalMs);
815
- timer.unref?.();
816
-
817
- return {
818
- succeed: (message = 'done') => stop(message, true),
819
- fail: (message = 'failed') => stop(message),
820
- };
821
- }
822
-
823
789
  /**
824
790
  * @param {string} value
825
791
  * @param {string} label
@@ -846,19 +812,6 @@ function parseNonNegativeIntegerOption(value, label) {
846
812
  return count;
847
813
  }
848
814
 
849
- /**
850
- * @param {string} value
851
- * @param {string} label
852
- * @returns {number}
853
- */
854
- function parsePositiveIntegerOption(value, label) {
855
- const num = Number.parseInt(value, 10);
856
- if (!Number.isFinite(num) || num <= 0) {
857
- throw new Error(`Invalid ${label}: ${value}. Expected a positive integer.`);
858
- }
859
- return num;
860
- }
861
-
862
815
  /**
863
816
  * @param {string} value
864
817
  * @param {string[]} previous
@@ -932,19 +885,8 @@ function setupCleanupHandlers(tunnelCreator) {
932
885
  return cleanupOnce;
933
886
  }
934
887
 
935
- function assertRoot() {
936
- if (typeof process.getuid !== 'function') {
937
- return;
938
- }
939
- if (process.getuid() !== 0) {
940
- throw new Error(
941
- 'This script must be run as root (e.g. sudo appium driver run xcuitest tunnel-creation ...).',
942
- );
943
- }
944
- }
945
-
946
888
  async function main() {
947
- assertRoot();
889
+ assertRoot('tunnel-creation');
948
890
  const program = new Command();
949
891
  program
950
892
  .name('appium driver run xcuitest tunnel-creation')
@@ -968,6 +910,12 @@ async function main() {
968
910
  collectStringValues,
969
911
  [],
970
912
  )
913
+ .option(
914
+ '--appletv-discovery-timeout-ms <ms>',
915
+ 'Apple TV wireless discovery timeout in milliseconds',
916
+ (value) => parsePositiveIntegerOption(value, 'Apple TV discovery timeout'),
917
+ DEFAULT_WIRELESS_APPLETV_DISCOVERY_TIMEOUT_MS,
918
+ )
971
919
  .option(
972
920
  '--disconnect-retry-max-attempts <count>',
973
921
  'Max tunnel recreation attempts after unexpected disconnect: 0 = unlimited; omit to disable retries',
@@ -989,7 +937,9 @@ async function main() {
989
937
  const shouldRunUsbFlow = !hasRequestedAppleTVIds || hasRequestedUdids;
990
938
  const shouldRunAppleTVFlow = !hasRequestedUdids || hasRequestedAppleTVIds;
991
939
 
992
- const tunnelCreator = new TunnelCreator();
940
+ const tunnelCreator = new TunnelCreator({
941
+ appleTVDiscoveryTimeoutMs: options.appletvDiscoveryTimeoutMs,
942
+ });
993
943
  const cleanupOnce = setupCleanupHandlers(tunnelCreator);
994
944
 
995
945
  try {
@@ -1126,7 +1076,7 @@ await main();
1126
1076
  */
1127
1077
 
1128
1078
  /**
1129
- * @typedef {Awaited<ReturnType<import('appium-ios-remotexpc').AppleTVTunnelService['discoverDevices']>>[number]} AppleTVDevice
1079
+ * @typedef {import('appium-ios-remotexpc').AppleTVDevice} AppleTVDevice
1130
1080
  */
1131
1081
 
1132
1082
  /**