appium-xcuitest-driver 11.4.1 → 11.4.2

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.4.2](https://github.com/appium/appium-xcuitest-driver/compare/v11.4.1...v11.4.2) (2026-05-15)
2
+
3
+ ### Miscellaneous Chores
4
+
5
+ * Refactor wda signer code ([#2839](https://github.com/appium/appium-xcuitest-driver/issues/2839)) ([3aafb36](https://github.com/appium/appium-xcuitest-driver/commit/3aafb3600a103a129d4b9dd4452e4c9c4e1223b1))
6
+
1
7
  ## [11.4.1](https://github.com/appium/appium-xcuitest-driver/compare/v11.4.0...v11.4.1) (2026-05-15)
2
8
 
3
9
  ### Bug Fixes
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "appium-xcuitest-driver",
3
- "version": "11.4.1",
3
+ "version": "11.4.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "appium-xcuitest-driver",
9
- "version": "11.4.1",
9
+ "version": "11.4.2",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@appium/strongbox": "^1.0.0-rc.1",
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "xcuitest",
9
9
  "xctest"
10
10
  ],
11
- "version": "11.4.1",
11
+ "version": "11.4.2",
12
12
  "author": "Appium Contributors",
13
13
  "license": "Apache-2.0",
14
14
  "repository": {
@@ -2,11 +2,12 @@ import {fs, logger} from 'appium/support.js';
2
2
  import {exec} from 'teen_process';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import {pathToFileURL} from 'node:url';
5
+ import {fileURLToPath, pathToFileURL} from 'node:url';
6
6
  import {mkdtemp, rm} from 'node:fs/promises';
7
7
  import {Command} from 'commander';
8
8
 
9
- const SCRIPT_NAME = 'sign-wda';
9
+ const scriptFilePath = fileURLToPath(import.meta.url);
10
+ const SCRIPT_NAME = path.basename(scriptFilePath, path.extname(scriptFilePath));
10
11
  const RESIGNER_BINARY_NAME = 'resigner';
11
12
  const MOBILEPROVISION_EXTENSION = '.mobileprovision';
12
13
  const DEFAULT_PROFILE_DIR_CANDIDATES = [
@@ -21,12 +22,76 @@ const DEFAULT_WDA_BUNDLE_IDS = [
21
22
 
22
23
  const log = logger.getLogger(SCRIPT_NAME);
23
24
 
24
- class RunCmd {
25
+ class Resigner {
26
+ /** @type {string} */
27
+ _wdaPath;
28
+
29
+ /**
30
+ * @param {string} wdaPath Path to the WebDriverAgent `.app` bundle.
31
+ */
32
+ constructor(wdaPath) {
33
+ this._wdaPath = wdaPath;
34
+ }
35
+
36
+ /**
37
+ * @param {SignOptions} options
38
+ * @returns {Promise<void>}
39
+ */
40
+ async signWDA(options) {
41
+ await this._requireBinary();
42
+ const args = this._buildSignArgs(options);
43
+ log.info(`Running resigner to sign ${this._wdaPath}`);
44
+ await exec(RESIGNER_BINARY_NAME, args, {
45
+ env: {
46
+ ...process.env,
47
+ P12_PASSWORD: options.p12Password,
48
+ },
49
+ });
50
+ log.info('WDA signed successfully');
51
+ }
52
+
53
+ /**
54
+ * @returns {Promise<string>}
55
+ */
56
+ async inspectWDA() {
57
+ await this._requireBinary();
58
+ log.info(`Inspecting signed WDA at ${this._wdaPath}`);
59
+ const {stdout} = await exec(RESIGNER_BINARY_NAME, ['--inspect', this._wdaPath]);
60
+ return String(stdout || '').trim();
61
+ }
62
+
25
63
  /**
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() {
64
+ * @param {SignOptions} options
65
+ * @returns {string[]}
66
+ */
67
+ _buildSignArgs(options) {
68
+ const args = [
69
+ '--p12-file', options.p12File,
70
+ '--profile', options.profileDir,
71
+ '--force',
72
+ ];
73
+
74
+ if (options.bundleId) {
75
+ args.push(
76
+ ...[
77
+ // To re-apply the same mapping again for past failure cases for safety.
78
+ options.bundleId,
79
+ ...DEFAULT_WDA_BUNDLE_IDS,
80
+ ].flatMap((bundleId) => [
81
+ '--bundle-id-remap',
82
+ `${bundleId}=${options.bundleId}`,
83
+ ])
84
+ );
85
+ }
86
+
87
+ args.push(this._wdaPath);
88
+ return args;
89
+ }
90
+
91
+ /**
92
+ * @returns {Promise<void>}
93
+ */
94
+ async _requireBinary() {
30
95
  try {
31
96
  await fs.which(RESIGNER_BINARY_NAME);
32
97
  } catch {
@@ -35,275 +100,351 @@ class RunCmd {
35
100
  }
36
101
  }
37
102
 
103
+ class ProvisioningProfilesHelper {
104
+ /** @type {string | undefined} */
105
+ _profileDir;
38
106
 
39
- class RunInspectWDA extends RunCmd {
40
107
  /**
41
- * Run resigner inspect on the signed WDA and return the output.
42
- * @param {InspectWDAOptions} options
43
- * @returns {Promise<void>}
108
+ * @param {string | undefined} profileDir Explicit directory, or `undefined` to auto-discover.
109
+ */
110
+ constructor(profileDir) {
111
+ this._profileDir = profileDir;
112
+ }
113
+
114
+ /**
115
+ * @returns {Promise<string>}
44
116
  */
45
- async inspect(options) {
46
- if (!(await fs.exists(options.wdaPath))) {
47
- throw new Error(`WDA path does not exist: ${options.wdaPath}`);
117
+ async resolveRoot() {
118
+ const profileDir = this._profileDir;
119
+ if (profileDir) {
120
+ return await this._validate(profileDir, 'Provided');
48
121
  }
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.');
122
+
123
+ for (const candidate of DEFAULT_PROFILE_DIR_CANDIDATES) {
124
+ if (!(await fs.exists(candidate))) {
125
+ continue;
126
+ }
127
+ try {
128
+ await this._validate(candidate, 'Discovered');
129
+ log.info(`Using discovered provisioning profile directory: ${candidate}`);
130
+ return candidate;
131
+ } catch {
132
+ continue;
133
+ }
55
134
  }
135
+
136
+ throw new Error(
137
+ `No provisioning profile directory could be discovered. ` +
138
+ `Please provide --profile-dir explicitly. Checked: ${DEFAULT_PROFILE_DIR_CANDIDATES.join(', ')}`
139
+ );
56
140
  }
57
- }
58
141
 
59
- class RunSignWDA extends RunCmd {
60
142
  /**
61
- * Run resigner to sign the WDA.
62
- * @param {SignWDAOptions} options
63
- * @returns {Promise<void>}
143
+ * @param {string} dir
144
+ * @param {string} source
145
+ * @returns {Promise<string>}
64
146
  */
65
- async sign(options) {
66
- if (!(await fs.exists(options.wdaPath))) {
67
- throw new Error(`WDA path does not exist: ${options.wdaPath}`);
147
+ async _validate(dir, source) {
148
+ if (!(await fs.exists(dir))) {
149
+ throw new Error(`${source} provisioning profile directory does not exist: ${dir}`);
68
150
  }
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
151
 
152
+ let entries;
76
153
  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
- }
154
+ entries = await fs.readdir(dir);
155
+ } catch {
156
+ throw new Error(`${source} provisioning profile directory is not a readable directory: ${dir}`);
157
+ }
89
158
 
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
- }
159
+ if (!entries.some((name) => name.toLowerCase().endsWith(MOBILEPROVISION_EXTENSION))) {
160
+ throw new Error(
161
+ `${source} provisioning profile directory does not contain any ${MOBILEPROVISION_EXTENSION} files: ${dir}`
162
+ );
106
163
  }
107
- }
108
- }
109
164
 
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)];
165
+ return dir;
119
166
  }
120
- return password;
121
167
  }
122
168
 
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}`);
169
+ class P12Converter {
170
+ /** @type {string} */
171
+ _certPath;
172
+ /** @type {string} */
173
+ _keyPath;
174
+ /** @type {string} */
175
+ _p12Password;
176
+
177
+ /**
178
+ * @param {string} certPath
179
+ * @param {string} keyPath
180
+ * @param {string} p12Password
181
+ */
182
+ constructor(certPath, keyPath, p12Password) {
183
+ this._certPath = certPath;
184
+ this._keyPath = keyPath;
185
+ this._p12Password = p12Password;
134
186
  }
135
- if (!(await fs.exists(keyPath))) {
136
- throw new Error(`Private key file does not exist: ${keyPath}`);
187
+
188
+ /**
189
+ * @returns {string}
190
+ */
191
+ static generateRandomPassword() {
192
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
193
+ let password = '';
194
+ for (let i = 0; i < 12; i++) {
195
+ password += chars[Math.floor(Math.random() * chars.length)];
196
+ }
197
+ return password;
137
198
  }
138
199
 
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.');
200
+ /**
201
+ * @returns {Promise<{p12File: string, tempDir: string}>}
202
+ */
203
+ async convert() {
204
+ await this._assertCertAndKeyExist();
205
+ await this._requireOpenSsl();
206
+
207
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'wda-sign-'));
208
+ const certPemPath = path.join(tempDir, 'certificate.pem');
209
+ const p12FilePath = path.join(tempDir, 'certificate.p12');
210
+
211
+ try {
212
+ await this._convertCerToPem(certPemPath);
213
+ await this._exportPkcs12(certPemPath, p12FilePath);
214
+ log.info(`Successfully created temporary .p12 file: ${p12FilePath}`);
215
+ return {p12File: p12FilePath, tempDir};
216
+ } catch (err) {
217
+ try {
218
+ await rm(tempDir, {recursive: true, force: true});
219
+ } catch {}
220
+ throw err;
221
+ }
144
222
  }
145
223
 
146
- // Create temp directory
147
- const tempDir = await mkdtemp(path.join(os.tmpdir(), 'wda-sign-'));
224
+ /**
225
+ * @returns {Promise<void>}
226
+ */
227
+ async _assertCertAndKeyExist() {
228
+ const certPath = this._certPath;
229
+ const keyPath = this._keyPath;
230
+ if (!(await fs.exists(certPath))) {
231
+ throw new Error(`Certificate file does not exist: ${certPath}`);
232
+ }
233
+ if (!(await fs.exists(keyPath))) {
234
+ throw new Error(`Private key file does not exist: ${keyPath}`);
235
+ }
236
+ }
148
237
 
149
- try {
150
- const certPem = path.join(tempDir, 'certificate.pem');
151
- const p12File = path.join(tempDir, 'certificate.p12');
238
+ /**
239
+ * @returns {Promise<void>}
240
+ */
241
+ async _requireOpenSsl() {
242
+ try {
243
+ await fs.which('openssl');
244
+ } catch {
245
+ throw new Error(
246
+ 'OpenSSL binary is not available in the PATH. ' +
247
+ 'It is required to convert .cer and .key files to .p12 format.'
248
+ );
249
+ }
250
+ }
152
251
 
153
- // Convert .cer to .pem
252
+ /**
253
+ * @param {string} certPemPath
254
+ * @returns {Promise<void>}
255
+ */
256
+ async _convertCerToPem(certPemPath) {
257
+ const certPath = this._certPath;
154
258
  log.info(`Converting certificate from ${certPath} to PEM format`);
155
259
  await exec('openssl', [
156
260
  'x509',
157
261
  '-in', certPath,
158
262
  '-inform', 'DER',
159
- '-out', certPem,
263
+ '-out', certPemPath,
160
264
  ]);
265
+ }
161
266
 
162
- // Convert to .p12
267
+ /**
268
+ * @param {string} certPemPath
269
+ * @param {string} p12FilePath
270
+ * @returns {Promise<void>}
271
+ */
272
+ async _exportPkcs12(certPemPath, p12FilePath) {
273
+ const keyPath = this._keyPath;
274
+ const p12Password = this._p12Password;
163
275
  log.info(`Creating .p12 file from certificate and key`);
164
276
  await exec('openssl', [
165
277
  'pkcs12',
166
278
  '-export',
167
- '-in', certPem,
279
+ '-in', certPemPath,
168
280
  '-inkey', keyPath,
169
- '-out', p12File,
281
+ '-out', p12FilePath,
170
282
  '-passout', `pass:${p12Password}`,
171
283
  ]);
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
284
  }
182
285
  }
183
286
 
184
287
  /**
185
- * Validate a provisioning profile directory.
186
- * @param {string} dir
187
- * @param {string} source
188
- * @returns {Promise<string>}
288
+ * Shared helpers for workflows that operate on a WDA `.app` bundle path.
189
289
  */
190
- async function validateProfileDir(dir, source) {
191
- if (!(await fs.exists(dir))) {
192
- throw new Error(`${source} provisioning profile directory does not exist: ${dir}`);
290
+ class WdaBundleWorkflow {
291
+ /**
292
+ * @param {string} wdaPath
293
+ * @returns {Promise<void>}
294
+ */
295
+ async _assertWdaExists(wdaPath) {
296
+ if (!(await fs.exists(wdaPath))) {
297
+ throw new Error(`WDA path does not exist: ${wdaPath}`);
298
+ }
193
299
  }
300
+ }
194
301
 
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}`);
302
+ class SignWdaWorkflow extends WdaBundleWorkflow {
303
+ /**
304
+ * @param {object} [deps]
305
+ * @param {(wdaPath: string) => Resigner} [deps.createResigner]
306
+ * @param {(profileDir: string | undefined) => ProvisioningProfilesHelper} [deps.createProvisioning]
307
+ * @param {(certPath: string, keyPath: string, p12Password: string) => P12Converter} [deps.createP12]
308
+ */
309
+ constructor(deps = {}) {
310
+ super();
311
+ this._createResigner = deps.createResigner ?? ((wdaPath) => new Resigner(wdaPath));
312
+ this._createProvisioning =
313
+ deps.createProvisioning ?? ((profileDir) => new ProvisioningProfilesHelper(profileDir));
314
+ this._createP12 =
315
+ deps.createP12 ?? ((certPath, keyPath, p12Password) => new P12Converter(certPath, keyPath, p12Password));
200
316
  }
201
317
 
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
- }
318
+ /**
319
+ * @param {SignWDAOptions} options
320
+ * @returns {Promise<void>}
321
+ */
322
+ async run(options) {
323
+ await this._assertWdaExists(options.wdaPath);
324
+ const resolvedProfileDir = await this._createProvisioning(options.profileDir).resolveRoot();
207
325
 
208
- return dir;
209
- }
326
+ let p12File = options.p12File;
327
+ let tempDir;
328
+ let p12Password = options.p12Password;
210
329
 
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');
330
+ try {
331
+ if (options.p12Cert && options.p12Key) {
332
+ const generatedPassword = P12Converter.generateRandomPassword();
333
+ const result = await this._createP12(
334
+ options.p12Cert,
335
+ options.p12Key,
336
+ generatedPassword
337
+ ).convert();
338
+ p12File = result.p12File;
339
+ tempDir = result.tempDir;
340
+ p12Password = generatedPassword;
341
+ }
342
+
343
+ if (!p12File) {
344
+ throw new Error('No p12 file available for signing');
345
+ }
346
+
347
+ await this._createResigner(options.wdaPath).signWDA({
348
+ p12File,
349
+ p12Password,
350
+ profileDir: resolvedProfileDir,
351
+ bundleId: options.bundleId,
352
+ });
353
+ } finally {
354
+ await this._cleanupTempDir(tempDir);
355
+ }
221
356
  }
222
357
 
223
- for (const candidate of DEFAULT_PROFILE_DIR_CANDIDATES) {
224
- if (!(await fs.exists(candidate))) {
225
- continue;
358
+ /**
359
+ * @param {string | undefined} tempDir
360
+ * @returns {Promise<void>}
361
+ */
362
+ async _cleanupTempDir(tempDir) {
363
+ if (!tempDir) {
364
+ return;
226
365
  }
227
366
  try {
228
- await validateProfileDir(candidate, 'Discovered');
229
- log.info(`Using discovered provisioning profile directory: ${candidate}`);
230
- return candidate;
231
- } catch {
232
- continue;
367
+ await rm(tempDir, {recursive: true, force: true});
368
+ log.info(`Cleaned up temporary directory: ${tempDir}`);
369
+ } catch (err) {
370
+ log.warn(`Failed to clean up temporary directory ${tempDir}: ${err.message}`);
233
371
  }
234
372
  }
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
373
  }
241
374
 
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
- );
375
+ class InspectWdaWorkflow extends WdaBundleWorkflow {
376
+ /**
377
+ * @param {object} [deps]
378
+ * @param {(wdaPath: string) => Resigner} [deps.createResigner]
379
+ */
380
+ constructor(deps = {}) {
381
+ super();
382
+ this._createResigner = deps.createResigner ?? ((wdaPath) => new Resigner(wdaPath));
266
383
  }
267
384
 
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');
385
+ /**
386
+ * @param {InspectWDAOptions} options
387
+ * @returns {Promise<void>}
388
+ */
389
+ async run(options) {
390
+ await this._assertWdaExists(options.wdaPath);
391
+ const inspectResult = await this._createResigner(options.wdaPath).inspectWDA();
392
+ if (inspectResult) {
393
+ log.info(`Resigner inspect result:\n---\n${inspectResult}`);
394
+ } else {
395
+ log.info('Resigner inspect finished, but no output was returned.');
396
+ }
397
+ }
278
398
  }
279
399
 
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
- }
400
+ class SignWdaCli {
401
+ /**
402
+ * @param {object} [deps]
403
+ * @param {SignWdaWorkflow} [deps.signWorkflow]
404
+ * @param {InspectWdaWorkflow} [deps.inspectWorkflow]
405
+ */
406
+ constructor(deps = {}) {
407
+ this._signWorkflow = deps.signWorkflow ?? new SignWdaWorkflow();
408
+ this._inspectWorkflow = deps.inspectWorkflow ?? new InspectWdaWorkflow();
409
+ }
290
410
 
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
- `
411
+ /**
412
+ * @param {string[]} argv
413
+ * @returns {Promise<void>}
414
+ */
415
+ async run(argv) {
416
+ const program = this._createProgram();
417
+ await program.parseAsync(argv);
418
+ }
419
+
420
+ /**
421
+ * @returns {Command}
422
+ */
423
+ _createProgram() {
424
+ const program = new Command();
425
+
426
+ program
427
+ .name('appium driver run xcuitest sign-wda')
428
+ .description('Sign a WebDriverAgentRunner app bundle with code signing certificate')
429
+ .requiredOption('--wda-path <path>', 'Path to the WebDriverAgentRunner.app bundle to sign')
430
+ .option('--inspect', 'Run resigner inspect only (no signing)')
431
+ .option(
432
+ '--p12-file <path>',
433
+ 'Path to the .p12 signing certificate file (requires P12_PASSWORD env var; mutually exclusive with --p12-cert/--p12-key)'
434
+ )
435
+ .option(
436
+ '--p12-cert <path>',
437
+ '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)'
438
+ )
439
+ .option(
440
+ '--p12-key <path>',
441
+ '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)'
442
+ )
443
+ .option('--profile-dir <path>', 'Directory containing provisioning profiles (auto-discovered if omitted)')
444
+ .option('--bundle-id <id>', 'Target bundle ID for remapping (e.g., com.example.wda)')
445
+ .addHelpText(
446
+ 'after',
447
+ `
307
448
  EXAMPLES:
308
449
  # Sign downloaded WDA with .p12 certificate (requires P12_PASSWORD)
309
450
  P12_PASSWORD=mypassword appium driver run xcuitest sign-wda -- --wda-path ./wda-real/WebDriverAgentRunner-Runner.app \
@@ -327,64 +468,79 @@ EXAMPLES:
327
468
 
328
469
  # Inspect a WDA app without signing
329
470
  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;
471
+ )
472
+ .action(async (options) => {
473
+ await this._handleParsedOptions(options);
474
+ });
340
475
 
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);
476
+ return program;
477
+ }
344
478
 
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
- }
479
+ /**
480
+ * @param {object} options
481
+ * @returns {Promise<void>}
482
+ */
483
+ async _handleParsedOptions(options) {
484
+ if (options.inspect) {
485
+ await this._inspectWorkflow.run({
486
+ wdaPath: options.wdaPath,
487
+ });
488
+ return;
489
+ }
350
490
 
351
- if (hasP12File && hasCertAndKey) {
352
- throw new Error(
353
- `Cannot provide both --p12-file and --p12-cert/--p12-key; use one approach`
354
- );
355
- }
491
+ const p12Password = process.env.P12_PASSWORD;
492
+ this._checkSigningOptions(options, p12Password);
493
+
494
+ await this._signWorkflow.run({
495
+ wdaPath: options.wdaPath,
496
+ p12File: options.p12File,
497
+ p12Cert: options.p12Cert,
498
+ p12Key: options.p12Key,
499
+ p12Password: p12Password || '',
500
+ profileDir: options.profileDir,
501
+ bundleId: options.bundleId,
502
+ });
503
+ }
356
504
 
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
- }
505
+ /**
506
+ * @param {object} options
507
+ * @param {string | undefined} p12Password
508
+ * @returns {void}
509
+ */
510
+ _checkSigningOptions(options, p12Password) {
511
+ const hasP12File = !!options.p12File;
512
+ const hasCertAndKey = !!(options.p12Cert && options.p12Key);
513
+
514
+ if (!hasP12File && !hasCertAndKey) {
515
+ throw new Error(
516
+ `Must provide either --p12-file or both --p12-cert and --p12-key for signing mode`
517
+ );
518
+ }
362
519
 
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
- }
520
+ if (hasP12File && hasCertAndKey) {
521
+ throw new Error(
522
+ `Cannot provide both --p12-file and --p12-cert/--p12-key; use one approach`
523
+ );
524
+ }
369
525
 
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
- });
526
+ if ((options.p12Cert && !options.p12Key) || (!options.p12Cert && options.p12Key)) {
527
+ throw new Error(
528
+ `Both --p12-cert and --p12-key must be provided together`
529
+ );
530
+ }
380
531
 
381
- await program.parseAsync(process.argv);
532
+ if (hasP12File && !p12Password) {
533
+ throw new Error(
534
+ `Missing required option for signing mode: P12_PASSWORD env var (required when using --p12-file)`
535
+ );
536
+ }
537
+ }
382
538
  }
383
539
 
384
540
  const isMainModule =
385
541
  Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
386
542
  if (isMainModule) {
387
- await main();
543
+ await new SignWdaCli().run(process.argv);
388
544
  }
389
545
 
390
546
  /**