appium-xcuitest-driver 11.2.3 → 11.3.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,15 @@
1
+ ## [11.3.0](https://github.com/appium/appium-xcuitest-driver/compare/v11.2.4...v11.3.0) (2026-05-09)
2
+
3
+ ### Features
4
+
5
+ * add download-wda command ([#2835](https://github.com/appium/appium-xcuitest-driver/issues/2835)) ([1f568b4](https://github.com/appium/appium-xcuitest-driver/commit/1f568b4823a75ede182afdc7c82b73193f554765))
6
+
7
+ ## [11.2.4](https://github.com/appium/appium-xcuitest-driver/compare/v11.2.3...v11.2.4) (2026-05-08)
8
+
9
+ ### Bug Fixes
10
+
11
+ * Tunnels cleanup ([#2834](https://github.com/appium/appium-xcuitest-driver/issues/2834)) ([e17218c](https://github.com/appium/appium-xcuitest-driver/commit/e17218c49111c500bef98aeee8d3739aafdfeb24))
12
+
1
13
  ## [11.2.3](https://github.com/appium/appium-xcuitest-driver/compare/v11.2.2...v11.2.3) (2026-05-07)
2
14
 
3
15
  ### Miscellaneous Chores
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "appium-xcuitest-driver",
3
- "version": "11.2.3",
3
+ "version": "11.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "appium-xcuitest-driver",
9
- "version": "11.2.3",
9
+ "version": "11.3.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@appium/strongbox": "^1.0.0-rc.1",
@@ -35,7 +35,7 @@
35
35
  "ws": "^8.13.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@appium/docutils": "^2.0.0-rc.1",
38
+ "@appium/docutils": "^2.4.0",
39
39
  "@appium/eslint-config-appium-ts": "^3.0.0",
40
40
  "@appium/tsconfig": "^1.0.0-rc.1",
41
41
  "@appium/types": "^1.0.0-rc.1",
@@ -240,6 +240,18 @@
240
240
  "sharp": "0.34.5"
241
241
  }
242
242
  },
243
+ "node_modules/@appium/support/node_modules/semver": {
244
+ "version": "7.7.4",
245
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
246
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
247
+ "license": "ISC",
248
+ "bin": {
249
+ "semver": "bin/semver.js"
250
+ },
251
+ "engines": {
252
+ "node": ">=10"
253
+ }
254
+ },
243
255
  "node_modules/@appium/tsconfig": {
244
256
  "version": "1.1.2",
245
257
  "resolved": "https://registry.npmjs.org/@appium/tsconfig/-/tsconfig-1.1.2.tgz",
@@ -551,9 +563,9 @@
551
563
  }
552
564
  },
553
565
  "node_modules/appium-ios-device": {
554
- "version": "3.1.12",
555
- "resolved": "https://registry.npmjs.org/appium-ios-device/-/appium-ios-device-3.1.12.tgz",
556
- "integrity": "sha512-2EEeGzWz2uDmUJftxuZ4b5DCptGqvQuND+FDMQ6p4qOpxbQX6E5HmAs0LyeepB/xAxkUP9wXZdBRl1OnrLKqvg==",
566
+ "version": "3.1.13",
567
+ "resolved": "https://registry.npmjs.org/appium-ios-device/-/appium-ios-device-3.1.13.tgz",
568
+ "integrity": "sha512-W10U63ISs2z/OwrkvRkSpGJeLc3qeBfIBhKd33jLaaDZE3VNR3yD7vLVLBfXxVaaM6UFOqx3yL5BtoeYltsmYQ==",
557
569
  "license": "Apache-2.0",
558
570
  "dependencies": {
559
571
  "@appium/support": "^7.2.2",
@@ -595,12 +607,12 @@
595
607
  }
596
608
  },
597
609
  "node_modules/appium-ios-simulator": {
598
- "version": "8.1.2",
599
- "resolved": "https://registry.npmjs.org/appium-ios-simulator/-/appium-ios-simulator-8.1.2.tgz",
600
- "integrity": "sha512-rA5FXFMMPYYNqStHK9k4OBlpqiogXnHASp3QdLtDOEEzjQZMPV1QPHfeoa/W3jmlLGxICkHSdlP2NB5CbJBJmA==",
610
+ "version": "8.1.3",
611
+ "resolved": "https://registry.npmjs.org/appium-ios-simulator/-/appium-ios-simulator-8.1.3.tgz",
612
+ "integrity": "sha512-ED3dNbxTO7YC/8UNKECY2mvePELtTKp9AXj+qwccK0UOMPWLURPszB1vVmZA5UGIykyeOeHBVcKbkSk709R8Gg==",
601
613
  "license": "Apache-2.0",
602
614
  "dependencies": {
603
- "@appium/support": "^7.0.0-rc.1",
615
+ "@appium/support": "^7.2.2",
604
616
  "@xmldom/xmldom": "^0.x",
605
617
  "appium-xcode": "^6.0.0",
606
618
  "async-lock": "^1.0.0",
@@ -655,9 +667,9 @@
655
667
  }
656
668
  },
657
669
  "node_modules/appium-webdriveragent": {
658
- "version": "12.2.1",
659
- "resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-12.2.1.tgz",
660
- "integrity": "sha512-iSY3s4EUiNqY7SZg36ufI3Jt84dBDcFxIUxmusAUJzjxkHPYy+VNTOyIdtuP5RkhFNoSh4VHg8JjusGKqdKWwg==",
670
+ "version": "12.2.2",
671
+ "resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-12.2.2.tgz",
672
+ "integrity": "sha512-/GbzAjEHaaNgBUK8eVnmtf8s2fudjyCi55IBoEzv0sFg/188Bo+wsJfQhECjrk9CdhkpiwjsHAlJGLUy+iEpIQ==",
661
673
  "license": "Apache-2.0",
662
674
  "dependencies": {
663
675
  "@appium/base-driver": "^10.3.0",
@@ -667,7 +679,7 @@
667
679
  "appium-ios-simulator": "^8.0.0",
668
680
  "async-lock": "^1.0.0",
669
681
  "asyncbox": "^6.1.0",
670
- "axios": "^1.4.0",
682
+ "axios": "^1.16.0",
671
683
  "teen_process": "^4.0.7"
672
684
  },
673
685
  "engines": {
@@ -676,12 +688,12 @@
676
688
  }
677
689
  },
678
690
  "node_modules/appium-xcode": {
679
- "version": "6.2.1",
680
- "resolved": "https://registry.npmjs.org/appium-xcode/-/appium-xcode-6.2.1.tgz",
681
- "integrity": "sha512-vY7D7GSm/m9KvRwJjk0AP9U+hT/MRoLN5OEohC2/vvbe5hxGp2H0nhUFYal2/6VwrL5U/P1zQKEIFUe3O8g0Dw==",
691
+ "version": "6.2.2",
692
+ "resolved": "https://registry.npmjs.org/appium-xcode/-/appium-xcode-6.2.2.tgz",
693
+ "integrity": "sha512-THnFNNPW2M680OzSdyHHcvF46CusrvW+/t+zCvVA+MK/sO1AE22GIHoUScbl8HeRxVI9gf1SsXravNSh8OoNaA==",
682
694
  "license": "Apache-2.0",
683
695
  "dependencies": {
684
- "@appium/support": "^7.0.0-rc.1",
696
+ "@appium/support": "^7.2.2",
685
697
  "asyncbox": "^6.0.1",
686
698
  "semver": "^7.0.0",
687
699
  "teen_process": "^4.0.4"
@@ -1062,9 +1074,9 @@
1062
1074
  }
1063
1075
  },
1064
1076
  "node_modules/brace-expansion": {
1065
- "version": "5.0.5",
1066
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
1067
- "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
1077
+ "version": "5.0.6",
1078
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
1079
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
1068
1080
  "license": "MIT",
1069
1081
  "dependencies": {
1070
1082
  "balanced-match": "^4.0.2"
@@ -1953,9 +1965,9 @@
1953
1965
  }
1954
1966
  },
1955
1967
  "node_modules/get-east-asian-width": {
1956
- "version": "1.5.0",
1957
- "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
1958
- "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
1968
+ "version": "1.6.0",
1969
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
1970
+ "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
1959
1971
  "license": "MIT",
1960
1972
  "engines": {
1961
1973
  "node": ">=18"
@@ -2780,9 +2792,9 @@
2780
2792
  }
2781
2793
  },
2782
2794
  "node_modules/node-devicectl": {
2783
- "version": "1.3.0",
2784
- "resolved": "https://registry.npmjs.org/node-devicectl/-/node-devicectl-1.3.0.tgz",
2785
- "integrity": "sha512-BrX8w5XkqeiZHW6wbfQBTnfu9HUAb/dPdNphtmiO4xrQJQBdxB44nyhtsTHMcdLzZLJJGJhyT+YWMNudFx9/dA==",
2795
+ "version": "1.3.1",
2796
+ "resolved": "https://registry.npmjs.org/node-devicectl/-/node-devicectl-1.3.1.tgz",
2797
+ "integrity": "sha512-hjjE/1RuvQ7BnjHqTfi4yhKGVVcTo/rEPdVzj1sIIQk29cvdsa1yXqfM4v4tQNiRcL2lMMqbO1hSkCBVAJ/w1g==",
2786
2798
  "license": "Apache-2.0",
2787
2799
  "dependencies": {
2788
2800
  "@appium/logger": "^2.0.0-rc.1",
@@ -2806,9 +2818,9 @@
2806
2818
  }
2807
2819
  },
2808
2820
  "node_modules/node-simctl": {
2809
- "version": "8.2.2",
2810
- "resolved": "https://registry.npmjs.org/node-simctl/-/node-simctl-8.2.2.tgz",
2811
- "integrity": "sha512-VdEsKHI9r1dih0UvYejNG1sFqb/EYjfr4fp5LiV3ny7PEni3j7IzYIqQzJ1XilTdI7mk8vZG7kTOKqr24OCq9Q==",
2821
+ "version": "8.2.3",
2822
+ "resolved": "https://registry.npmjs.org/node-simctl/-/node-simctl-8.2.3.tgz",
2823
+ "integrity": "sha512-udjYoXRsApFQP0fggKwGWFWUqinJKY11zIUmMxMv8Q4sfMLV0Uu9iavRUfiB3QOmJvL4WAXTaHv+tZvkiT+nyQ==",
2812
2824
  "license": "Apache-2.0",
2813
2825
  "dependencies": {
2814
2826
  "@appium/logger": "^2.0.0-rc.1",
@@ -3450,9 +3462,9 @@
3450
3462
  "optional": true
3451
3463
  },
3452
3464
  "node_modules/semver": {
3453
- "version": "7.7.4",
3454
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
3455
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
3465
+ "version": "7.8.0",
3466
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
3467
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
3456
3468
  "license": "ISC",
3457
3469
  "bin": {
3458
3470
  "semver": "bin/semver.js"
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "xcuitest",
9
9
  "xctest"
10
10
  ],
11
- "version": "11.2.3",
11
+ "version": "11.3.0",
12
12
  "author": "Appium Contributors",
13
13
  "license": "Apache-2.0",
14
14
  "repository": {
@@ -34,6 +34,7 @@
34
34
  "build-wda": "./scripts/build-wda.mjs",
35
35
  "open-wda": "./scripts/open-wda.mjs",
36
36
  "tunnel-creation": "./scripts/tunnel-creation.mjs",
37
+ "download-wda": "./scripts/download-wda.mjs",
37
38
  "download-wda-sim": "./scripts/download-wda-sim.mjs",
38
39
  "image-mounter": "./scripts/image-mounter.mjs",
39
40
  "list-real-devices": "./scripts/list-real-devices.mjs",
@@ -143,7 +144,7 @@
143
144
  "appium": "^3.0.0-rc.2"
144
145
  },
145
146
  "devDependencies": {
146
- "@appium/docutils": "^2.0.0-rc.1",
147
+ "@appium/docutils": "^2.4.0",
147
148
  "@appium/eslint-config-appium-ts": "^3.0.0",
148
149
  "@appium/tsconfig": "^1.0.0-rc.1",
149
150
  "@appium/types": "^1.0.0-rc.1",
@@ -1,80 +1,25 @@
1
- import {fs, logger, zip, net, node} from 'appium/support.js';
2
- import _ from 'lodash';
3
- import os from 'node:os';
4
- import path from 'node:path';
1
+ import {getWDAPrebuiltPackage} from './download-wda.mjs';
5
2
  import {Command} from 'commander';
3
+ import { deprecate } from 'node:util';
6
4
 
7
- const log = logger.getLogger('download-wda-sim');
8
- const wdaUrl = (/** @type {string} */ version, /** @type {string} */ zipFileName) =>
9
- `https://github.com/appium/WebDriverAgent/releases/download/v${version}/${zipFileName}`;
10
- const destZip = (/** @type {string} */ platform) => {
11
- const scheme = `WebDriverAgentRunner${_.toLower(platform) === 'tvos' ? '_tvOS' : ''}`;
12
- return `${scheme}-Build-Sim-${os.arch() === 'arm64' ? 'arm64' : 'x86_64'}.zip`;
13
- };
14
-
15
- /**
16
- * Return installed appium-webdriveragent package version
17
- * @returns {Promise<string>}
18
- */
19
- async function webdriveragentPkgVersion() {
20
- const moduleRoot = node.getModuleRootSync('appium-xcuitest-driver', import.meta.url);
21
- if (!moduleRoot) {
22
- throw new Error('Cannot resolve module root for appium-xcuitest-driver');
23
- }
24
- const pkgPath = path.join(
25
- moduleRoot,
26
- 'node_modules',
27
- 'appium-webdriveragent',
28
- 'package.json'
29
- );
30
- const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
31
- return String(pkg.version);
32
- };
33
-
34
- /**
35
- * Prepare the working root directory.
36
- * @param {string} outdir
37
- * @returns {Promise<string>} Root directory to download and unzip.
38
- */
39
- async function prepareRootDir(outdir) {
40
- const destDir = path.resolve(process.cwd(), outdir);
41
- if (await fs.exists(destDir)) {
42
- throw new Error(`${destDir} already exists`);
43
- }
44
- await fs.mkdir(destDir, {recursive: true});
45
- return destDir;
46
- }
47
-
48
- /**
49
- * @param {DownloadOptions} options
50
- */
51
- async function getWDAPrebuiltPackage(options) {
52
- const destDir = await prepareRootDir(options.outdir);
53
- const zipFileName = destZip(options.platform);
54
- const wdaVersion = await webdriveragentPkgVersion();
55
- const urlToDownload = wdaUrl(wdaVersion, zipFileName);
56
- const downloadedZipFile = path.join(destDir, zipFileName);
57
- try {
58
- log.info(`Downloading ${urlToDownload}`);
59
- await net.downloadFile(urlToDownload, downloadedZipFile);
60
-
61
- log.info(`Unpacking ${downloadedZipFile} into ${destDir}`);
62
- await zip.extractAllTo(downloadedZipFile, destDir);
63
-
64
- log.info(`Deleting ${downloadedZipFile}`);
65
- } finally {
66
- if (await fs.exists(downloadedZipFile)) {
67
- await fs.unlink(downloadedZipFile);
68
- }
69
- }
70
- }
5
+ const DEPRECATION_MESSAGE =
6
+ "[DEPRECATED] 'download-wda-sim' is deprecated. " +
7
+ "Use 'appium driver run xcuitest download-wda -- --kind=sim --platform=<platform> --outdir=<outdir>' instead.";
71
8
 
72
9
  async function main() {
73
10
  const program = new Command();
74
11
 
12
+ const oldHandler = deprecate(
13
+ async (options) => {
14
+ await getWDAPrebuiltPackage({...options, kind: 'sim'});
15
+ },
16
+ DEPRECATION_MESSAGE
17
+ );
18
+
75
19
  program
76
20
  .name('appium driver run xcuitest download-wda-sim')
77
21
  .description('Download a prebuilt WebDriverAgentRunner for iOS/tvOS simulator')
22
+ .addHelpText('beforeAll', `${DEPRECATION_MESSAGE}\n\n`)
78
23
  .requiredOption('--outdir <path>', 'Destination directory to download and unpack into')
79
24
  .requiredOption(
80
25
  '--platform <platform>',
@@ -86,14 +31,12 @@ async function main() {
86
31
  `
87
32
  EXAMPLES:
88
33
  # Download WDA for iOS simulator
89
- appium driver run xcuitest download-wda-sim --outdir ./wda-sim --platform iOS
34
+ appium driver run xcuitest download-wda-sim -- --outdir ./wda-sim --platform iOS
90
35
 
91
36
  # Download WDA for tvOS simulator
92
- appium driver run xcuitest download-wda-sim --outdir ./wda-sim-tvos --platform tvOS`,
37
+ appium driver run xcuitest download-wda-sim -- --outdir ./wda-sim-tvos --platform tvOS`,
93
38
  )
94
- .action(async (options) => {
95
- await getWDAPrebuiltPackage(options);
96
- });
39
+ .action(oldHandler);
97
40
 
98
41
  await program.parseAsync(process.argv);
99
42
  }
@@ -0,0 +1,167 @@
1
+ import {fs, logger, zip, net, node} from 'appium/support.js';
2
+ import {constants as fsConstants, promises as fsPromises} from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import {pathToFileURL} from 'node:url';
6
+ import {Command} from 'commander';
7
+
8
+ const log = logger.getLogger('download-wda');
9
+ const WDA_KIND_REAL = 'real';
10
+ const WDA_KIND_SIM = 'sim';
11
+
12
+ const wdaUrl = (/** @type {string} */ version, /** @type {string} */ zipFileName) =>
13
+ `https://github.com/appium/WebDriverAgent/releases/download/v${version}/${zipFileName}`;
14
+
15
+ /**
16
+ * Download and unpack a prebuilt WDA package for the given platform and kind.
17
+ * @param {DownloadOptions} options
18
+ * @returns {Promise<void>}
19
+ */
20
+ export async function getWDAPrebuiltPackage(options) {
21
+ const kind = normalizeKind(options.kind);
22
+ const destDir = await prepareRootDir(options.outdir);
23
+ const zipFileName = destZip(options.platform, kind);
24
+ const wdaVersion = await getWebdriveragentPkgVersion();
25
+ const urlToDownload = wdaUrl(wdaVersion, zipFileName);
26
+ const downloadedZipFile = path.join(destDir, zipFileName);
27
+ try {
28
+ log.info(`Downloading ${urlToDownload}`);
29
+ await net.downloadFile(urlToDownload, downloadedZipFile);
30
+
31
+ log.info(`Unpacking ${downloadedZipFile} into ${destDir}`);
32
+ await zip.extractAllTo(downloadedZipFile, destDir);
33
+
34
+ log.info(`Deleting ${downloadedZipFile}`);
35
+ } finally {
36
+ if (await fs.exists(downloadedZipFile)) {
37
+ await fs.unlink(downloadedZipFile);
38
+ }
39
+ }
40
+ }
41
+
42
+ const destZip = (/** @type {string} */ platform, /** @type {WDAKind} */ kind) => {
43
+ const scheme = `WebDriverAgentRunner${String(platform).toLowerCase() === 'tvos' ? '_tvOS' : ''}`;
44
+ if (kind === WDA_KIND_SIM) {
45
+ return `${scheme}-Build-Sim-${os.arch() === 'arm64' ? 'arm64' : 'x86_64'}.zip`;
46
+ }
47
+ return `${scheme}-Runner.zip`;
48
+ };
49
+
50
+ /**
51
+ * Normalize the kind value, ensuring it is either 'real' or 'sim'. Default to 'real' if undefined.
52
+ * @param {string | undefined} kind
53
+ * @returns {WDAKind}
54
+ */
55
+ function normalizeKind(kind) {
56
+ const normalized = String(kind || WDA_KIND_REAL).toLowerCase();
57
+ if (![WDA_KIND_REAL, WDA_KIND_SIM].includes(normalized)) {
58
+ throw new Error(`Unsupported kind '${kind}'. Supported values are '${WDA_KIND_REAL}' and '${WDA_KIND_SIM}'`);
59
+ }
60
+ return /** @type {WDAKind} */ (normalized);
61
+ }
62
+
63
+ /**
64
+ * Return installed appium-webdriveragent package version
65
+ * @returns {Promise<string>}
66
+ */
67
+ async function getWebdriveragentPkgVersion() {
68
+ const moduleRoot = node.getModuleRootSync('appium-xcuitest-driver', import.meta.url);
69
+ if (!moduleRoot) {
70
+ throw new Error('Cannot resolve module root for appium-xcuitest-driver');
71
+ }
72
+ const pkgPath = path.join(
73
+ moduleRoot,
74
+ 'node_modules',
75
+ 'appium-webdriveragent',
76
+ 'package.json'
77
+ );
78
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
79
+ if (!pkg.version || typeof pkg.version !== 'string') {
80
+ throw new Error(`Cannot find version in ${pkgPath}`);
81
+ }
82
+ return pkg.version;
83
+ };
84
+
85
+ /**
86
+ * Prepare the working root directory.
87
+ * @param {string} outdir
88
+ * @returns {Promise<string>} Root directory to download and unzip.
89
+ */
90
+ async function prepareRootDir(outdir) {
91
+ const destDir = path.resolve(process.cwd(), outdir);
92
+ if (await fs.exists(destDir)) {
93
+ throw new Error(`${destDir} already exists`);
94
+ }
95
+
96
+ const parentDir = path.dirname(destDir);
97
+ try {
98
+ await fsPromises.access(parentDir, fsConstants.W_OK);
99
+ } catch (err) {
100
+ throw new Error(`Parent directory '${parentDir}' is not writable`, {
101
+ cause: err,
102
+ });
103
+ }
104
+
105
+ try {
106
+ await fs.mkdir(destDir, {recursive: true});
107
+ } catch (err) {
108
+ const message = err instanceof Error ? err.message : String(err);
109
+ throw new Error(`Cannot create directory '${destDir}': ${message}`, {
110
+ cause: err,
111
+ });
112
+ }
113
+ return destDir;
114
+ }
115
+
116
+ async function main() {
117
+ const program = new Command();
118
+
119
+ program
120
+ .name('appium driver run xcuitest download-wda')
121
+ .description('Download a prebuilt WebDriverAgentRunner for iOS/tvOS real devices or simulators')
122
+ .requiredOption('--outdir <path>', 'Destination directory to download and unpack into')
123
+ .requiredOption(
124
+ '--platform <platform>',
125
+ 'Target platform (e.g. iOS or tvOS)',
126
+ (value) => value,
127
+ )
128
+ .option(
129
+ '--kind <kind>',
130
+ `Target package type: ${WDA_KIND_REAL} (real devices) or ${WDA_KIND_SIM} (simulators). Default: ${WDA_KIND_REAL}`,
131
+ )
132
+ .addHelpText(
133
+ 'after',
134
+ `
135
+ EXAMPLES:
136
+ # Download WDA for iOS real device (default)
137
+ appium driver run xcuitest download-wda -- --outdir ./wda-real --platform iOS
138
+
139
+ # Download WDA for tvOS simulator
140
+ appium driver run xcuitest download-wda -- --outdir ./wda-sim-tvos --platform tvOS --kind sim`,
141
+ )
142
+ .action(async (options) => {
143
+ await getWDAPrebuiltPackage({
144
+ ...options,
145
+ kind: options.kind ?? WDA_KIND_REAL,
146
+ });
147
+ });
148
+
149
+ await program.parseAsync(process.argv);
150
+ }
151
+
152
+ const isMainModule =
153
+ Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
154
+ if (isMainModule) {
155
+ await main();
156
+ }
157
+
158
+ /**
159
+ * @typedef {'real' | 'sim'} WDAKind
160
+ */
161
+
162
+ /**
163
+ * @typedef {Object} DownloadOptions
164
+ * @property {string} outdir
165
+ * @property {string} platform
166
+ * @property {WDAKind | undefined} [kind]
167
+ */
@@ -41,6 +41,8 @@ class TunnelCreator {
41
41
  this._tunnelRegistryPort = 42314;
42
42
  /** @type {import('appium-ios-remotexpc').TunnelRegistry | null} */
43
43
  this._registry = null;
44
+ /** @type {import('appium-ios-remotexpc').TunnelRegistryServer | null} */
45
+ this._registryServer = null;
44
46
  /** @type {Map<string, import('appium-ios-remotexpc').UsbmuxDevice>} */
45
47
  this._usbDevices = new Map();
46
48
  /** @type {Map<string, Promise<void>>} */
@@ -75,6 +77,10 @@ class TunnelCreator {
75
77
  return this._registry;
76
78
  }
77
79
 
80
+ get registryServer() {
81
+ return this._registryServer;
82
+ }
83
+
78
84
  set packetStreamBasePort(port) {
79
85
  this._packetStreamBasePort = port;
80
86
  }
@@ -97,6 +103,13 @@ class TunnelCreator {
97
103
  this._registry = value;
98
104
  }
99
105
 
106
+ /**
107
+ * @param {import('appium-ios-remotexpc').TunnelRegistryServer | null} value
108
+ */
109
+ set registryServer(value) {
110
+ this._registryServer = value;
111
+ }
112
+
100
113
  /**
101
114
  * @param {number | null} maxAttempts - null disables retries; 0 means unlimited retries
102
115
  * @param {number} intervalMs
@@ -171,12 +184,29 @@ class TunnelCreator {
171
184
  async cleanup() {
172
185
  this._isCleaningUp = true;
173
186
  log.warn('Cleaning up tunnel resources...');
187
+ /** @type {Error[]} */
188
+ const cleanupErrors = [];
189
+ const recordCleanupError = (message, err) => {
190
+ const wrapped = err instanceof Error ? err : new Error(String(err));
191
+ cleanupErrors.push(new Error(message, {cause: wrapped}));
192
+ log.warn(`${message}: ${wrapped.message}`);
193
+ };
194
+
174
195
  while (this._registryWatcherStops.length > 0) {
175
196
  const stop = this._registryWatcherStops.pop();
176
197
  try {
177
198
  await stop?.();
178
199
  } catch (err) {
179
- log.warn(`Failed to stop tunnel registry watcher: ${err}`);
200
+ recordCleanupError('Failed to stop tunnel registry watcher', err);
201
+ }
202
+ }
203
+ if (this._registryServer) {
204
+ try {
205
+ await this._registryServer.stop();
206
+ } catch (err) {
207
+ recordCleanupError('Failed to stop tunnel registry server', err);
208
+ } finally {
209
+ this._registryServer = null;
180
210
  }
181
211
  }
182
212
 
@@ -194,7 +224,7 @@ class TunnelCreator {
194
224
  await server.stop();
195
225
  log.info(`Closed packet stream server for device ${udid}`);
196
226
  } catch (err) {
197
- log.warn(`Failed to close packet stream server for device ${udid}: ${err}`);
227
+ recordCleanupError(`Failed to close packet stream server for device ${udid}`, err);
198
228
  }
199
229
  }),
200
230
  );
@@ -216,9 +246,18 @@ class TunnelCreator {
216
246
  })();
217
247
 
218
248
  await Promise.allSettled([closeUsbPacketStreamServers, closeAppleTVTunnels]);
249
+ try {
250
+ await TunnelManager.closeAllTunnels();
251
+ } catch (err) {
252
+ recordCleanupError('Failed to close managed tunnel(s)', err);
253
+ }
219
254
  await Promise.allSettled([...this._reconnectTasks.values()]);
220
255
 
221
- log.info('Cleanup completed.');
256
+ if (cleanupErrors.length > 0) {
257
+ throw new AggregateError(cleanupErrors, 'Tunnel cleanup encountered errors');
258
+ } else {
259
+ log.info('Cleanup completed.');
260
+ }
222
261
  }
223
262
 
224
263
  /**
@@ -914,6 +953,9 @@ function setupCleanupHandlers(tunnelCreator) {
914
953
  await tunnelCreator.cleanup();
915
954
  } catch (err) {
916
955
  log.warn(`Error during tunnel cleanup: ${err?.message ?? err}`);
956
+ if (!process.exitCode) {
957
+ process.exitCode = 1;
958
+ }
917
959
  }
918
960
  };
919
961
 
@@ -923,7 +965,8 @@ function setupCleanupHandlers(tunnelCreator) {
923
965
  if (process.exitCode == null) {
924
966
  // Follow conventional POSIX exit codes for signals where possible.
925
967
  if (signal === 'SIGINT') {
926
- process.exitCode = 130;
968
+ // SIGINT is typically sent by Ctrl+C, so we exit with code 0 to indicate success.
969
+ process.exitCode = 0;
927
970
  } else if (signal === 'SIGTERM') {
928
971
  process.exitCode = 143;
929
972
  } else {
@@ -1093,7 +1136,10 @@ async function main() {
1093
1136
  return;
1094
1137
  }
1095
1138
 
1096
- await startTunnelRegistryServer(registry, tunnelCreator.tunnelRegistryPort);
1139
+ tunnelCreator.registryServer = await startTunnelRegistryServer(
1140
+ registry,
1141
+ tunnelCreator.tunnelRegistryPort,
1142
+ );
1097
1143
  tunnelCreator._attachTunnelRegistryLifecycleWatch(watchTunnelRegistrySockets, usbResults, {
1098
1144
  onTunnelDead: async ({udid}) => {
1099
1145
  tunnelCreator._reconnectTunnelByUdid(udid);