appium-xcuitest-driver 11.3.0 → 11.4.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.4.0](https://github.com/appium/appium-xcuitest-driver/compare/v11.3.1...v11.4.0) (2026-05-13)
2
+
3
+ ### Features
4
+
5
+ * add resign task ([#2836](https://github.com/appium/appium-xcuitest-driver/issues/2836)) ([7dfca62](https://github.com/appium/appium-xcuitest-driver/commit/7dfca6298a0dd17b016d71793e465425bb04e59a))
6
+
7
+ ## [11.3.1](https://github.com/appium/appium-xcuitest-driver/compare/v11.3.0...v11.3.1) (2026-05-12)
8
+
9
+ ### Miscellaneous Chores
10
+
11
+ * bump remotexpc to 1.1.8 ([#2838](https://github.com/appium/appium-xcuitest-driver/issues/2838)) ([46fc046](https://github.com/appium/appium-xcuitest-driver/commit/46fc046c47202b8c17c8d3cd2f6cee75b350e436))
12
+
1
13
  ## [11.3.0](https://github.com/appium/appium-xcuitest-driver/compare/v11.2.4...v11.3.0) (2026-05-09)
2
14
 
3
15
  ### Features
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "appium-xcuitest-driver",
3
- "version": "11.3.0",
3
+ "version": "11.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "appium-xcuitest-driver",
9
- "version": "11.3.0",
9
+ "version": "11.4.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@appium/strongbox": "^1.0.0-rc.1",
@@ -65,7 +65,7 @@
65
65
  "npm": ">=10"
66
66
  },
67
67
  "optionalDependencies": {
68
- "appium-ios-remotexpc": "^1.1.6"
68
+ "appium-ios-remotexpc": "^1.1.8"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "appium": "^3.0.0-rc.2"
@@ -104,6 +104,19 @@
104
104
  "spdy": "4.0.2"
105
105
  }
106
106
  },
107
+ "node_modules/@appium/base-driver/node_modules/asyncbox": {
108
+ "version": "6.2.0",
109
+ "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.2.0.tgz",
110
+ "integrity": "sha512-z1XpHkoT3y+1aXfazEY5d7HN2eOi50fLq7ZTxG0H4WegLxrtEAI5Vsc6OR9dOwoC3SJQLXyV0ZVnPEh6GIgMKQ==",
111
+ "license": "Apache-2.0",
112
+ "dependencies": {
113
+ "p-limit": "^7.2.0"
114
+ },
115
+ "engines": {
116
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
117
+ "npm": ">=10"
118
+ }
119
+ },
107
120
  "node_modules/@appium/base-driver/node_modules/lru-cache": {
108
121
  "version": "11.3.5",
109
122
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
@@ -240,6 +253,19 @@
240
253
  "sharp": "0.34.5"
241
254
  }
242
255
  },
256
+ "node_modules/@appium/support/node_modules/asyncbox": {
257
+ "version": "6.2.0",
258
+ "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.2.0.tgz",
259
+ "integrity": "sha512-z1XpHkoT3y+1aXfazEY5d7HN2eOi50fLq7ZTxG0H4WegLxrtEAI5Vsc6OR9dOwoC3SJQLXyV0ZVnPEh6GIgMKQ==",
260
+ "license": "Apache-2.0",
261
+ "dependencies": {
262
+ "p-limit": "^7.2.0"
263
+ },
264
+ "engines": {
265
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
266
+ "npm": ">=10"
267
+ }
268
+ },
243
269
  "node_modules/@appium/support/node_modules/semver": {
244
270
  "version": "7.7.4",
245
271
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -563,15 +589,14 @@
563
589
  }
564
590
  },
565
591
  "node_modules/appium-ios-device": {
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==",
592
+ "version": "3.1.14",
593
+ "resolved": "https://registry.npmjs.org/appium-ios-device/-/appium-ios-device-3.1.14.tgz",
594
+ "integrity": "sha512-fjqh1oPYQ2C4v96OLhom3HCK6j2NRO0TYOgZXtT9vEEjug/Z25NAjxlLnQLBe7aAHxR2c0V/SyFmLTEBs/Ih5A==",
569
595
  "license": "Apache-2.0",
570
596
  "dependencies": {
571
597
  "@appium/support": "^7.2.2",
572
598
  "asyncbox": "^6.0.1",
573
599
  "axios": "^1.16.0",
574
- "bluebird": "^3.1.1",
575
600
  "bplist-creator": "^0.x",
576
601
  "bplist-parser": "^0.x",
577
602
  "lodash": "^4.17.15",
@@ -583,9 +608,9 @@
583
608
  }
584
609
  },
585
610
  "node_modules/appium-ios-remotexpc": {
586
- "version": "1.1.6",
587
- "resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-1.1.6.tgz",
588
- "integrity": "sha512-jDkqVoP/7WM1g2z9p6aN+Yveek7yQ7WICdoaTnTfpsr5eF5/Luaxlk7okglp4GfdsOK6G4OUBYtCby99WRTczg==",
611
+ "version": "1.1.8",
612
+ "resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-1.1.8.tgz",
613
+ "integrity": "sha512-62j5AqfjgPtZ+7H893z0X/6+LroncahjqvfJsFXafCzy5VLVIqL5OsrMWc1L8babedIuFFX42zHzLkPyiGt4uA==",
589
614
  "license": "Apache-2.0",
590
615
  "optional": true,
591
616
  "dependencies": {
@@ -627,9 +652,9 @@
627
652
  }
628
653
  },
629
654
  "node_modules/appium-ios-tuntap": {
630
- "version": "0.2.2",
631
- "resolved": "https://registry.npmjs.org/appium-ios-tuntap/-/appium-ios-tuntap-0.2.2.tgz",
632
- "integrity": "sha512-AObU1ttH/WCW9wBCpMlyN1SbKqDMFwQkdks5rDN9N2yIn7MCoyN9il9WYa7QIJA5bE6OI1px+pv8QKs9SDTrHA==",
655
+ "version": "0.2.3",
656
+ "resolved": "https://registry.npmjs.org/appium-ios-tuntap/-/appium-ios-tuntap-0.2.3.tgz",
657
+ "integrity": "sha512-rt8sZi66IeOSXyEclMsC+uSA1sO11PMAldztHPC1ipLkSwrahasvILy9AZ9wnPOzcCZ84TOQF3bpz8S4luJ4yA==",
633
658
  "hasInstallScript": true,
634
659
  "license": "Apache-2.0",
635
660
  "optional": true,
@@ -645,16 +670,16 @@
645
670
  }
646
671
  },
647
672
  "node_modules/appium-remote-debugger": {
648
- "version": "15.9.1",
649
- "resolved": "https://registry.npmjs.org/appium-remote-debugger/-/appium-remote-debugger-15.9.1.tgz",
650
- "integrity": "sha512-+ms2NsTfu+L1JN9UbFUQo4ZiJV1FFgcm3PSWRgJziI7TasOtShYgcNt2d+V9o4fkf9+BESOAhQ6Fm0FVNdISLA==",
673
+ "version": "15.10.1",
674
+ "resolved": "https://registry.npmjs.org/appium-remote-debugger/-/appium-remote-debugger-15.10.1.tgz",
675
+ "integrity": "sha512-IcEXCuUNUIZVmOLiC6gmpBarh4ZM1ESQkSLD8Yb6v3oC6ksUHUFPRDdpdH48G38AgseGSnTFD15RSv4FqWwasw==",
651
676
  "license": "Apache-2.0",
652
677
  "dependencies": {
653
678
  "@appium/base-driver": "^10.0.0-rc.1",
654
- "@appium/support": "^7.0.0-rc.1",
679
+ "@appium/support": "^7.2.2",
655
680
  "appium-ios-device": "^3.0.0",
656
681
  "async-lock": "^1.4.1",
657
- "asyncbox": "^6.1.0",
682
+ "asyncbox": "^6.3.0",
658
683
  "glob": "^13.0.0",
659
684
  "teen_process": "^4.0.4"
660
685
  },
@@ -832,9 +857,9 @@
832
857
  "license": "MIT"
833
858
  },
834
859
  "node_modules/asyncbox": {
835
- "version": "6.2.0",
836
- "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.2.0.tgz",
837
- "integrity": "sha512-z1XpHkoT3y+1aXfazEY5d7HN2eOi50fLq7ZTxG0H4WegLxrtEAI5Vsc6OR9dOwoC3SJQLXyV0ZVnPEh6GIgMKQ==",
860
+ "version": "6.3.0",
861
+ "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-6.3.0.tgz",
862
+ "integrity": "sha512-7IFpnQDltd5rYQjhIJIpyismJtdWmw/pOABZKJfv2WVo0a6iYh2ZzUuCJJclae5mBtK0H/EychxXg91GB7rGdQ==",
838
863
  "license": "Apache-2.0",
839
864
  "dependencies": {
840
865
  "p-limit": "^7.2.0"
@@ -4410,9 +4435,9 @@
4410
4435
  "license": "ISC"
4411
4436
  },
4412
4437
  "node_modules/ws": {
4413
- "version": "8.20.0",
4414
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
4415
- "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
4438
+ "version": "8.20.1",
4439
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
4440
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
4416
4441
  "license": "MIT",
4417
4442
  "engines": {
4418
4443
  "node": ">=10.0.0"
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "xcuitest",
9
9
  "xctest"
10
10
  ],
11
- "version": "11.3.0",
11
+ "version": "11.4.0",
12
12
  "author": "Appium Contributors",
13
13
  "license": "Apache-2.0",
14
14
  "repository": {
@@ -36,6 +36,7 @@
36
36
  "tunnel-creation": "./scripts/tunnel-creation.mjs",
37
37
  "download-wda": "./scripts/download-wda.mjs",
38
38
  "download-wda-sim": "./scripts/download-wda-sim.mjs",
39
+ "sign-wda": "./scripts/sign-wda.mjs",
39
40
  "image-mounter": "./scripts/image-mounter.mjs",
40
41
  "list-real-devices": "./scripts/list-real-devices.mjs",
41
42
  "cleanup-videos": "./scripts/cleanup-videos.mjs",
@@ -107,7 +108,7 @@
107
108
  "ws": "^8.13.0"
108
109
  },
109
110
  "optionalDependencies": {
110
- "appium-ios-remotexpc": "^1.1.6"
111
+ "appium-ios-remotexpc": "^1.1.8"
111
112
  },
112
113
  "scripts": {
113
114
  "build": "tsc -b",
@@ -0,0 +1,412 @@
1
+ import {fs, logger} from 'appium/support.js';
2
+ import {exec} from 'teen_process';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import {pathToFileURL} from 'node:url';
6
+ import {mkdtemp, rm} from 'node:fs/promises';
7
+ import {Command} from 'commander';
8
+
9
+ const SCRIPT_NAME = 'sign-wda';
10
+ const RESIGNER_BINARY_NAME = 'resigner';
11
+ const MOBILEPROVISION_EXTENSION = '.mobileprovision';
12
+ const DEFAULT_PROFILE_DIR_CANDIDATES = [
13
+ path.join(os.homedir(), 'Library', 'Developer', 'Xcode', 'UserData', 'Provisioning Profiles'),
14
+ path.join(os.homedir(), 'Library', 'MobileDevice', 'Provisioning Profiles'),
15
+ ];
16
+ const DEFAULT_WDA_BUNDLE_IDS = [
17
+ 'com.facebook.WebDriverAgentRunner',
18
+ 'com.facebook.WebDriverAgentRunner.xctrunner',
19
+ 'com.facebook.WebDriverAgentLib',
20
+ ];
21
+
22
+ const log = logger.getLogger(SCRIPT_NAME);
23
+
24
+ class RunCmd {
25
+ /**
26
+ * Check if the resginer binary is available in the PATH.
27
+ * @returns {Promise<void>} Whether the resigner binary is available in the local environment
28
+ */
29
+ async requireResignerBinary() {
30
+ try {
31
+ await fs.which(RESIGNER_BINARY_NAME);
32
+ } catch {
33
+ throw new Error('Resigner binary is not available in the PATH.');
34
+ }
35
+ }
36
+ }
37
+
38
+
39
+ class RunInspectWDA extends RunCmd {
40
+ /**
41
+ * Run resigner inspect on the signed WDA and return the output.
42
+ * @param {InspectWDAOptions} options
43
+ * @returns {Promise<void>}
44
+ */
45
+ async inspect(options) {
46
+ if (!(await fs.exists(options.wdaPath))) {
47
+ throw new Error(`WDA path does not exist: ${options.wdaPath}`);
48
+ }
49
+ await this.requireResignerBinary();
50
+ const inspectResult = await inspectWDAWithResigner(options.wdaPath);
51
+ if (inspectResult) {
52
+ log.info(`Resigner inspect result:\n---\n${inspectResult}`);
53
+ } else {
54
+ log.info('Resigner inspect finished, but no output was returned.');
55
+ }
56
+ }
57
+ }
58
+
59
+ class RunSignWDA extends RunCmd {
60
+ /**
61
+ * Run resigner to sign the WDA.
62
+ * @param {SignWDAOptions} options
63
+ * @returns {Promise<void>}
64
+ */
65
+ async sign(options) {
66
+ if (!(await fs.exists(options.wdaPath))) {
67
+ throw new Error(`WDA path does not exist: ${options.wdaPath}`);
68
+ }
69
+ await this.requireResignerBinary();
70
+ const resolvedProfileDir = await resolveProfileDir(options.profileDir);
71
+
72
+ let p12File = options.p12File;
73
+ let tempDir;
74
+ let p12Password = options.p12Password;
75
+
76
+ try {
77
+ // If cert and key provided, convert to p12 with auto-generated password
78
+ if (options.p12Cert && options.p12Key) {
79
+ const generatedPassword = generateRandomPassword();
80
+ const result = await convertCertAndKeyToP12(options.p12Cert, options.p12Key, generatedPassword);
81
+ p12File = result.p12File;
82
+ tempDir = result.tempDir;
83
+ p12Password = generatedPassword;
84
+ }
85
+
86
+ if (!p12File) {
87
+ throw new Error('No p12 file available for signing');
88
+ }
89
+
90
+ await signWDAWithResigner(options.wdaPath, {
91
+ p12File,
92
+ p12Password,
93
+ profileDir: resolvedProfileDir,
94
+ bundleId: options.bundleId,
95
+ });
96
+ } finally {
97
+ // Clean up temp directory if it was created
98
+ if (tempDir) {
99
+ try {
100
+ await rm(tempDir, {recursive: true, force: true});
101
+ log.info(`Cleaned up temporary directory: ${tempDir}`);
102
+ } catch (err) {
103
+ log.warn(`Failed to clean up temporary directory ${tempDir}: ${err.message}`);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Generate a random password for temporary .p12 files.
112
+ * @returns {string} A random 12-character alphanumeric password
113
+ */
114
+ function generateRandomPassword() {
115
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
116
+ let password = '';
117
+ for (let i = 0; i < 12; i++) {
118
+ password += chars[Math.floor(Math.random() * chars.length)];
119
+ }
120
+ return password;
121
+ }
122
+
123
+ /**
124
+ * Convert .cer and .key files to a .p12 file.
125
+ * @param {string} certPath - Path to the .cer certificate file
126
+ * @param {string} keyPath - Path to the .key private key file
127
+ * @param {string} p12Password - Password to protect the .p12 file
128
+ * @returns {Promise<{p12File: string, tempDir: string}>} Path to the generated .p12 and temp directory
129
+ */
130
+ async function convertCertAndKeyToP12(certPath, keyPath, p12Password) {
131
+ // Validate input files exist
132
+ if (!(await fs.exists(certPath))) {
133
+ throw new Error(`Certificate file does not exist: ${certPath}`);
134
+ }
135
+ if (!(await fs.exists(keyPath))) {
136
+ throw new Error(`Private key file does not exist: ${keyPath}`);
137
+ }
138
+
139
+ try {
140
+ await fs.which('openssl');
141
+ } catch {
142
+ throw new Error('OpenSSL binary is not available in the PATH. ' +
143
+ 'It is required to convert .cer and .key files to .p12 format.');
144
+ }
145
+
146
+ // Create temp directory
147
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'wda-sign-'));
148
+
149
+ try {
150
+ const certPem = path.join(tempDir, 'certificate.pem');
151
+ const p12File = path.join(tempDir, 'certificate.p12');
152
+
153
+ // Convert .cer to .pem
154
+ log.info(`Converting certificate from ${certPath} to PEM format`);
155
+ await exec('openssl', [
156
+ 'x509',
157
+ '-in', certPath,
158
+ '-inform', 'DER',
159
+ '-out', certPem,
160
+ ]);
161
+
162
+ // Convert to .p12
163
+ log.info(`Creating .p12 file from certificate and key`);
164
+ await exec('openssl', [
165
+ 'pkcs12',
166
+ '-export',
167
+ '-in', certPem,
168
+ '-inkey', keyPath,
169
+ '-out', p12File,
170
+ '-passout', `pass:${p12Password}`,
171
+ ]);
172
+
173
+ log.info(`Successfully created temporary .p12 file: ${p12File}`);
174
+ return {p12File, tempDir};
175
+ } catch (err) {
176
+ // Clean up temp dir on error
177
+ try {
178
+ await rm(tempDir, {recursive: true, force: true});
179
+ } catch {}
180
+ throw err;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Validate a provisioning profile directory.
186
+ * @param {string} dir
187
+ * @param {string} source
188
+ * @returns {Promise<string>}
189
+ */
190
+ async function validateProfileDir(dir, source) {
191
+ if (!(await fs.exists(dir))) {
192
+ throw new Error(`${source} provisioning profile directory does not exist: ${dir}`);
193
+ }
194
+
195
+ let entries;
196
+ try {
197
+ entries = await fs.readdir(dir);
198
+ } catch {
199
+ throw new Error(`${source} provisioning profile directory is not a readable directory: ${dir}`);
200
+ }
201
+
202
+ if (!entries.some((name) => name.toLowerCase().endsWith(MOBILEPROVISION_EXTENSION))) {
203
+ throw new Error(
204
+ `${source} provisioning profile directory does not contain any ${MOBILEPROVISION_EXTENSION} files: ${dir}`
205
+ );
206
+ }
207
+
208
+ return dir;
209
+ }
210
+
211
+ /**
212
+ * Resolve the provisioning profile directory.
213
+ * If user provided --profile-dir, validate and use it.
214
+ * Otherwise discover from known defaults in priority order.
215
+ * @param {string | undefined} profileDir
216
+ * @returns {Promise<string>}
217
+ */
218
+ async function resolveProfileDir(profileDir) {
219
+ if (profileDir) {
220
+ return await validateProfileDir(profileDir, 'Provided');
221
+ }
222
+
223
+ for (const candidate of DEFAULT_PROFILE_DIR_CANDIDATES) {
224
+ if (!(await fs.exists(candidate))) {
225
+ continue;
226
+ }
227
+ try {
228
+ await validateProfileDir(candidate, 'Discovered');
229
+ log.info(`Using discovered provisioning profile directory: ${candidate}`);
230
+ return candidate;
231
+ } catch {
232
+ continue;
233
+ }
234
+ }
235
+
236
+ throw new Error(
237
+ `No provisioning profile directory could be discovered. ` +
238
+ `Please provide --profile-dir explicitly. Checked: ${DEFAULT_PROFILE_DIR_CANDIDATES.join(', ')}`
239
+ );
240
+ }
241
+
242
+ /**
243
+ * Run resigner to sign WDA
244
+ * @param {string} wdaPath
245
+ * @param {SignOptions} options
246
+ * @returns {Promise<void>}
247
+ */
248
+ async function signWDAWithResigner(wdaPath, options) {
249
+ const args = [
250
+ '--p12-file', options.p12File,
251
+ '--profile', options.profileDir,
252
+ '--force',
253
+ ];
254
+
255
+ if (options.bundleId) {
256
+ args.push(
257
+ ...[
258
+ // To re-apply the same mapping again for past failure cases for safe.
259
+ options.bundleId,
260
+ ...DEFAULT_WDA_BUNDLE_IDS,
261
+ ].flatMap((bundleId) => [
262
+ '--bundle-id-remap',
263
+ `${bundleId}=${options.bundleId}`,
264
+ ])
265
+ );
266
+ }
267
+
268
+ args.push(wdaPath);
269
+
270
+ log.info(`Running resigner to sign ${wdaPath}`);
271
+ await exec(RESIGNER_BINARY_NAME, args, {
272
+ env: {
273
+ ...process.env,
274
+ P12_PASSWORD: options.p12Password,
275
+ },
276
+ });
277
+ log.info('WDA signed successfully');
278
+ }
279
+
280
+ /**
281
+ * Run resigner inspect on the signed WDA and return the output.
282
+ * @param {string} wdaPath
283
+ * @returns {Promise<string>}
284
+ */
285
+ async function inspectWDAWithResigner(wdaPath) {
286
+ log.info(`Inspecting signed WDA at ${wdaPath}`);
287
+ const {stdout} = await exec(RESIGNER_BINARY_NAME, ['--inspect', wdaPath]);
288
+ return String(stdout || '').trim();
289
+ }
290
+
291
+ async function main() {
292
+ const program = new Command();
293
+
294
+ program
295
+ .name('appium driver run xcuitest sign-wda')
296
+ .description('Sign a WebDriverAgentRunner app bundle with code signing certificate')
297
+ .requiredOption('--wda-path <path>', 'Path to the WebDriverAgentRunner.app bundle to sign')
298
+ .option('--inspect', 'Run resigner inspect only (no signing)')
299
+ .option('--p12-file <path>', 'Path to the .p12 signing certificate file (requires P12_PASSWORD env var; mutually exclusive with --p12-cert/--p12-key)')
300
+ .option('--p12-cert <path>', 'Path to the .cer certificate file from Apple Developer portal (auto-converted to .p12 with generated password; mutually exclusive with --p12-file; must use with --p12-key)')
301
+ .option('--p12-key <path>', 'Path to the .key private key file from Apple Developer portal (auto-converted to .p12 with generated password; mutually exclusive with --p12-file; must use with --p12-cert)')
302
+ .option('--profile-dir <path>', 'Directory containing provisioning profiles (auto-discovered if omitted)')
303
+ .option('--bundle-id <id>', 'Target bundle ID for remapping (e.g., com.example.wda)')
304
+ .addHelpText(
305
+ 'after',
306
+ `
307
+ EXAMPLES:
308
+ # Sign downloaded WDA with .p12 certificate (requires P12_PASSWORD)
309
+ P12_PASSWORD=mypassword appium driver run xcuitest sign-wda -- --wda-path ./wda-real/WebDriverAgentRunner-Runner.app \
310
+ --p12-file ~/sign.p12
311
+
312
+ # Sign WDA with .cer and .key files (auto-converted, no password needed!)
313
+ appium driver run xcuitest sign-wda -- --wda-path ./wda-real/WebDriverAgentRunner-Runner.app \
314
+ --p12-cert ~/certificate.cer \
315
+ --p12-key ~/private.key
316
+
317
+ # Sign WDA and remap bundle ID with .p12 certificate (requires P12_PASSWORD)
318
+ P12_PASSWORD=mypassword appium driver run xcuitest sign-wda -- --wda-path ./wda-real/WebDriverAgentRunner-Runner.app \
319
+ --p12-file ~/sign.p12 \
320
+ --bundle-id com.example.wda
321
+
322
+ # Sign WDA with specified provisioning profile directory (cert+key approach)
323
+ appium driver run xcuitest sign-wda -- --wda-path ./wda-real/WebDriverAgentRunner-Runner.app \
324
+ --p12-cert ~/certificate.cer \
325
+ --p12-key ~/private.key \
326
+ --profile-dir /path/to/your/provisioning/profiles
327
+
328
+ # Inspect a WDA app without signing
329
+ appium driver run xcuitest sign-wda -- --wda-path ./wda-real/WebDriverAgentRunner-Runner.app --inspect`,
330
+ )
331
+ .action(async (options) => {
332
+ if (options.inspect) {
333
+ new RunInspectWDA().inspect({
334
+ wdaPath: options.wdaPath,
335
+ });
336
+ return;
337
+ }
338
+
339
+ const p12Password = process.env.P12_PASSWORD;
340
+
341
+ // Validate that either --p12-file OR (--p12-cert AND --p12-key) is provided
342
+ const hasP12File = !!options.p12File;
343
+ const hasCertAndKey = !!(options.p12Cert && options.p12Key);
344
+
345
+ if (!hasP12File && !hasCertAndKey) {
346
+ throw new Error(
347
+ `Must provide either --p12-file or both --p12-cert and --p12-key for signing mode`
348
+ );
349
+ }
350
+
351
+ if (hasP12File && hasCertAndKey) {
352
+ throw new Error(
353
+ `Cannot provide both --p12-file and --p12-cert/--p12-key; use one approach`
354
+ );
355
+ }
356
+
357
+ if ((options.p12Cert && !options.p12Key) || (!options.p12Cert && options.p12Key)) {
358
+ throw new Error(
359
+ `Both --p12-cert and --p12-key must be provided together`
360
+ );
361
+ }
362
+
363
+ // P12_PASSWORD is only required when using --p12-file
364
+ if (hasP12File && !p12Password) {
365
+ throw new Error(
366
+ `Missing required option for signing mode: P12_PASSWORD env var (required when using --p12-file)`
367
+ );
368
+ }
369
+
370
+ await new RunSignWDA().sign({
371
+ wdaPath: options.wdaPath,
372
+ p12File: options.p12File,
373
+ p12Cert: options.p12Cert,
374
+ p12Key: options.p12Key,
375
+ p12Password: p12Password || '',
376
+ profileDir: options.profileDir,
377
+ bundleId: options.bundleId,
378
+ });
379
+ });
380
+
381
+ await program.parseAsync(process.argv);
382
+ }
383
+
384
+ const isMainModule =
385
+ Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
386
+ if (isMainModule) {
387
+ await main();
388
+ }
389
+
390
+ /**
391
+ * @typedef {Object} SignOptions
392
+ * @property {string} p12File
393
+ * @property {string} p12Password
394
+ * @property {string} profileDir
395
+ * @property {string | undefined} [bundleId]
396
+ */
397
+
398
+ /**
399
+ * @typedef {Object} SignWDAOptions
400
+ * @property {string} wdaPath
401
+ * @property {string | undefined} [p12File]
402
+ * @property {string | undefined} [p12Cert]
403
+ * @property {string | undefined} [p12Key]
404
+ * @property {string} p12Password
405
+ * @property {string | undefined} [profileDir]
406
+ * @property {string | undefined} [bundleId]
407
+ */
408
+
409
+ /**
410
+ * @typedef {Object} InspectWDAOptions
411
+ * @property {string} wdaPath
412
+ */