appium-geckodriver 2.3.0 → 3.0.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/build/lib/commands/find.d.ts +1 -1
  3. package/build/lib/commands/find.d.ts.map +1 -1
  4. package/build/lib/commands/find.js +3 -6
  5. package/build/lib/commands/find.js.map +1 -1
  6. package/build/lib/constants.js +2 -5
  7. package/build/lib/constants.js.map +1 -1
  8. package/build/lib/desired-caps.js +3 -6
  9. package/build/lib/desired-caps.js.map +1 -1
  10. package/build/lib/doctor/optional-checks.js +18 -22
  11. package/build/lib/doctor/optional-checks.js.map +1 -1
  12. package/build/lib/doctor/required-checks.js +8 -12
  13. package/build/lib/doctor/required-checks.js.map +1 -1
  14. package/build/lib/doctor/utils.js +4 -7
  15. package/build/lib/doctor/utils.js.map +1 -1
  16. package/build/lib/driver.d.ts +4 -4
  17. package/build/lib/driver.d.ts.map +1 -1
  18. package/build/lib/driver.js +18 -55
  19. package/build/lib/driver.js.map +1 -1
  20. package/build/lib/gecko.d.ts +1 -1
  21. package/build/lib/gecko.d.ts.map +1 -1
  22. package/build/lib/gecko.js +26 -34
  23. package/build/lib/gecko.js.map +1 -1
  24. package/build/lib/index.d.ts +1 -1
  25. package/build/lib/index.d.ts.map +1 -1
  26. package/build/lib/index.js +3 -6
  27. package/build/lib/index.js.map +1 -1
  28. package/build/lib/logger.js +3 -6
  29. package/build/lib/logger.js.map +1 -1
  30. package/build/lib/method-map.js +1 -4
  31. package/build/lib/method-map.js.map +1 -1
  32. package/build/lib/utils.js +20 -30
  33. package/build/lib/utils.js.map +1 -1
  34. package/lib/commands/find.ts +2 -2
  35. package/lib/desired-caps.ts +1 -1
  36. package/lib/doctor/required-checks.ts +1 -1
  37. package/lib/driver.ts +7 -7
  38. package/lib/gecko.ts +3 -3
  39. package/lib/index.ts +1 -1
  40. package/lib/logger.ts +1 -1
  41. package/lib/method-map.ts +1 -1
  42. package/lib/utils.ts +2 -2
  43. package/npm-shrinkwrap.json +12 -9
  44. package/package.json +12 -10
  45. package/scripts/install-geckodriver.mjs +382 -216
@@ -1,279 +1,445 @@
1
+ #!/usr/bin/env node
1
2
  import axios from 'axios';
2
3
  import * as semver from 'semver';
3
4
  import path from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import { log } from '../build/lib/logger.js';
5
+ import {homedir, tmpdir} from 'node:os';
6
+ import {constants as fsConstants} from 'node:fs';
7
+ import fs from 'node:fs/promises';
8
+ import {exec} from 'teen_process';
9
+ import {Command} from 'commander';
10
+ import {log} from '../build/lib/logger.js';
6
11
  import {
7
12
  downloadToFile,
8
13
  mkdirp,
9
14
  extractFileFromTarGz,
10
15
  extractFileFromZip,
11
16
  } from '../build/lib/utils.js';
12
- import fs from 'node:fs/promises';
13
- import { exec } from 'teen_process';
14
17
 
15
- const OWNER = 'mozilla';
16
- const REPO = 'geckodriver';
17
- const API_ROOT = `https://api.github.com/repos/${OWNER}/${REPO}`;
18
- const API_VERSION_HEADER = {'X-GitHub-Api-Version': '2022-11-28'};
19
- const API_TIMEOUT_MS = 45 * 1000;
20
18
  const STABLE_VERSION = 'stable';
21
19
  const EXT_TAR_GZ = '.tar.gz';
22
20
  const EXT_ZIP = '.zip';
23
21
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
24
22
  const EXT_REGEXP = new RegExp(`(${escapeRegExp(EXT_TAR_GZ)}|${escapeRegExp(EXT_ZIP)})$`);
25
- const ARCHIVE_NAME_PREFIX = 'geckodriver-v';
26
- const ARCH_MAPPING = Object.freeze({
27
- ia32: '32',
28
- x64: '64',
29
- arm64: 'aarch64',
30
- });
31
- const PLATFORM_MAPPING = Object.freeze({
32
- win32: 'win',
33
- darwin: 'macos',
34
- linux: 'linux',
35
- });
36
23
 
37
24
  /**
38
- *
39
- * @param {string} dstPath
40
- * @returns {Promise<void>}
25
+ * Fetches and selects Geckodriver releases from GitHub.
41
26
  */
42
- async function clearNotarization(dstPath) {
43
- if (process.platform === 'darwin') {
44
- await exec('xattr', ['-cr', dstPath]);
45
- }
46
- }
27
+ class GeckodriverReleaseCatalog {
28
+ static OWNER = 'mozilla';
29
+ static REPO = 'geckodriver';
30
+ static API_ROOT = `https://api.github.com/repos/${GeckodriverReleaseCatalog.OWNER}/${GeckodriverReleaseCatalog.REPO}`;
31
+ static API_VERSION_HEADER = {'X-GitHub-Api-Version': '2022-11-28'};
32
+ static API_TIMEOUT_MS = 45 * 1000;
33
+ static ARCHIVE_NAME_PREFIX = 'geckodriver-v';
34
+ static ARCH_MAPPING = Object.freeze({
35
+ ia32: '32',
36
+ x64: '64',
37
+ arm64: 'aarch64',
38
+ });
39
+ static PLATFORM_MAPPING = Object.freeze({
40
+ win32: 'win',
41
+ darwin: 'macos',
42
+ linux: 'linux',
43
+ });
44
+ static SUPPORTED_PLATFORMS = Object.keys(GeckodriverReleaseCatalog.PLATFORM_MAPPING).join(', ');
45
+ static SUPPORTED_ARCHITECTURES = Object.keys(GeckodriverReleaseCatalog.ARCH_MAPPING).join(', ');
47
46
 
48
- /**
49
- *
50
- * @param {import('axios').AxiosResponseHeaders} headers
51
- * @returns {string|null}
52
- */
53
- function parseNextPageUrl(headers) {
54
- if (!headers.link) {
55
- return null;
47
+ /**
48
+ * https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#list-releases
49
+ *
50
+ * @returns {Promise<ReleaseInfo[]>}
51
+ */
52
+ async listReleases() {
53
+ /** @type {Record<string, any>[]} */
54
+ const allReleases = [];
55
+ let currentUrl = `${GeckodriverReleaseCatalog.API_ROOT}/releases`;
56
+ do {
57
+ const {data, headers} = await axios.get(currentUrl, {
58
+ timeout: GeckodriverReleaseCatalog.API_TIMEOUT_MS,
59
+ headers: {...GeckodriverReleaseCatalog.API_VERSION_HEADER},
60
+ });
61
+ allReleases.push(...data);
62
+ currentUrl = this.#parseNextPageUrl(headers);
63
+ } while (currentUrl);
64
+
65
+ /** @type {ReleaseInfo[]} */
66
+ const result = [];
67
+ for (const releaseInfo of allReleases) {
68
+ const isDraft = !!releaseInfo.draft;
69
+ const isPrerelease = !!releaseInfo.prerelease;
70
+ const version = semver.coerce(releaseInfo.tag_name?.replace(/^v/, ''));
71
+ if (!version) {
72
+ continue;
73
+ }
74
+ /** @type {ReleaseAsset[]} */
75
+ const releaseAssets = [];
76
+ for (const asset of releaseInfo.assets ?? []) {
77
+ const assetName = asset?.name;
78
+ const downloadUrl = asset?.browser_download_url;
79
+ if (
80
+ !assetName?.startsWith(GeckodriverReleaseCatalog.ARCHIVE_NAME_PREFIX)
81
+ || !(assetName?.endsWith(EXT_TAR_GZ) || assetName?.endsWith(EXT_ZIP))
82
+ || !downloadUrl
83
+ ) {
84
+ continue;
85
+ }
86
+ releaseAssets.push({
87
+ name: assetName,
88
+ url: downloadUrl,
89
+ });
90
+ }
91
+ result.push({
92
+ version,
93
+ isDraft,
94
+ isPrerelease,
95
+ assets: releaseAssets,
96
+ });
97
+ }
98
+ return result;
56
99
  }
57
100
 
58
- for (const part of headers.link.split(';')) {
59
- const [rel, pageUrl] = part.split(',').map((item) => item.trim());
60
- if (rel === 'rel="next"' && pageUrl) {
61
- return pageUrl.replace(/^<|>$/g, '');
101
+ /**
102
+ * @param {ReleaseInfo[]} releases
103
+ * @param {string} version
104
+ * @returns {ReleaseInfo}
105
+ */
106
+ selectRelease(releases, version) {
107
+ if (version === STABLE_VERSION) {
108
+ const stableReleasesAsc = releases
109
+ .filter(({isDraft, isPrerelease}) => !isDraft && !isPrerelease)
110
+ .toSorted((a, b) => a.version.compare(b.version));
111
+ const dstRelease = stableReleasesAsc.at(-1);
112
+ if (!dstRelease) {
113
+ throw new Error(`Cannot find any stable GeckoDriver release: ${JSON.stringify(releases)}`);
114
+ }
115
+ return dstRelease;
62
116
  }
117
+ const coercedVersion = semver.coerce(version);
118
+ if (!coercedVersion) {
119
+ throw new Error(
120
+ `The provided version string '${version}' cannot be coerced to a valid SemVer representation`
121
+ );
122
+ }
123
+ const dstRelease = releases.find((r) => r.version.compare(coercedVersion) === 0);
124
+ if (!dstRelease) {
125
+ throw new Error(
126
+ `The provided version string '${version}' cannot be matched to any available GeckoDriver releases: ` +
127
+ JSON.stringify(releases)
128
+ );
129
+ }
130
+ return dstRelease;
63
131
  }
64
- return null;
65
- }
66
132
 
67
- /**
68
- * @returns {Promise<[string, boolean]>}
69
- */
70
- async function prepareDestinationFolder() {
71
- let dstRoot;
72
- switch (process.platform) {
73
- case 'win32':
74
- dstRoot = path.join(process.env.LOCALAPPDATA, 'Mozilla');
75
- break;
76
- case 'linux':
77
- case 'darwin':
78
- dstRoot = path.join('/usr', 'local', 'bin');
79
- break;
80
- default:
133
+ /**
134
+ * @param {ReleaseInfo} release
135
+ * @returns {ReleaseAsset}
136
+ */
137
+ selectAsset(release) {
138
+ if (release.assets.length === 0) {
139
+ throw new Error(`GeckoDriver v${release.version} does not contain any matching releases`);
140
+ }
141
+ const dstPlatform = GeckodriverReleaseCatalog.PLATFORM_MAPPING[process.platform];
142
+ if (!dstPlatform) {
81
143
  throw new Error(
82
144
  `GeckoDriver does not support the ${process.platform} platform. ` +
83
- `Only Linux, Windows and macOS are supported.`
145
+ `Supported platforms: ${GeckodriverReleaseCatalog.SUPPORTED_PLATFORMS}.`
84
146
  );
85
147
  }
86
- await mkdirp(dstRoot);
87
- const pathParts = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
88
- const isInPath = pathParts
89
- .map((pp) => path.normalize(pp))
90
- .some((pp) => pp === path.normalize(dstRoot));
91
- return [dstRoot, isInPath];
92
- }
148
+ const dstArch = GeckodriverReleaseCatalog.ARCH_MAPPING[process.arch];
149
+ if (!dstArch) {
150
+ throw new Error(
151
+ `GeckoDriver does not support the ${process.arch} architecture. ` +
152
+ `Supported architectures: ${GeckodriverReleaseCatalog.SUPPORTED_ARCHITECTURES}.`
153
+ );
154
+ }
155
+ log.info(`Operating system: ${process.platform}@${process.arch}`);
93
156
 
94
- /**
95
- * https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#list-releases
96
- *
97
- * @returns {Promise<ReleaseInfo[]}
98
- */
99
- async function listReleases() {
100
- /** @type {Record<string, any>[]} */
101
- const allReleases = [];
102
- let currentUrl = `${API_ROOT}/releases`;
103
- do {
104
- const {data, headers} = await axios.get(currentUrl, {
105
- timeout: API_TIMEOUT_MS,
106
- headers: { ...API_VERSION_HEADER }
107
- });
108
- allReleases.push(...data);
109
- currentUrl = parseNextPageUrl(headers);
110
- } while (currentUrl);
111
- /** @type {ReleaseInfo[]} */
112
- const result = [];
113
- for (const releaseInfo of allReleases) {
114
- const isDraft = !!releaseInfo.draft;
115
- const isPrerelease = !!releaseInfo.prerelease;
116
- const version = semver.coerce(releaseInfo.tag_name?.replace(/^v/, ''));
117
- if (!version) {
118
- continue;
157
+ /** @type {(filterFunc: (string) => boolean) => null|ReleaseAsset} */
158
+ const findAssetMatch = (filterFunc) => {
159
+ for (const asset of release.assets) {
160
+ if (!asset.name.includes(`-${dstPlatform}`)) {
161
+ continue;
162
+ }
163
+ const nameWoExt = asset.name.replace(EXT_REGEXP, '');
164
+ if (filterFunc(nameWoExt)) {
165
+ return asset;
166
+ }
167
+ }
168
+ return null;
169
+ };
170
+
171
+ const exactMatch = findAssetMatch(
172
+ (nameWoExt) =>
173
+ (dstArch === 'aarch64' && nameWoExt.endsWith(`-${dstArch}`))
174
+ || (['64', '32'].includes(dstArch) && nameWoExt.endsWith(`-${dstPlatform}${dstArch}`))
175
+ );
176
+ if (exactMatch) {
177
+ return exactMatch;
119
178
  }
120
- /** @type {ReleaseAsset[]} */
121
- const releaseAssets = [];
122
- for (const asset of (releaseInfo.assets ?? [])) {
123
- const assetName = asset?.name;
124
- const downloadUrl = asset?.browser_download_url;
125
- if (
126
- !assetName?.startsWith(ARCHIVE_NAME_PREFIX)
127
- || !(assetName?.endsWith(EXT_TAR_GZ) || assetName?.endsWith(EXT_ZIP))
128
- || !downloadUrl
129
- ) {
130
- continue;
179
+ const looseMatch = findAssetMatch(
180
+ (nameWoExt) =>
181
+ nameWoExt.endsWith(`-${dstPlatform}`)
182
+ || (dstArch === '64' && nameWoExt.endsWith(`-${dstPlatform}32`))
183
+ );
184
+ if (looseMatch) {
185
+ return looseMatch;
186
+ }
187
+ throw new Error(
188
+ `GeckoDriver v${release.version} does not contain any release matching the ` +
189
+ `current OS architecture ${process.arch}. Available packages: ${release.assets.map(({name}) => name)}`
190
+ );
191
+ }
192
+
193
+ /**
194
+ * @param {import('axios').AxiosResponseHeaders} headers
195
+ * @returns {string|null}
196
+ */
197
+ #parseNextPageUrl(headers) {
198
+ if (!headers.link) {
199
+ return null;
200
+ }
201
+
202
+ for (const linkPart of headers.link.split(',')) {
203
+ const [pageUrl, rel] = linkPart.split(';').map((item) => item.trim());
204
+ if (rel === 'rel="next"' && pageUrl) {
205
+ return pageUrl.replace(/^<|>$/g, '');
131
206
  }
132
- releaseAssets.push({
133
- name: assetName,
134
- url: downloadUrl,
135
- });
136
207
  }
137
- result.push({
138
- version,
139
- isDraft,
140
- isPrerelease,
141
- assets: releaseAssets,
142
- });
208
+ return null;
143
209
  }
144
- return result;
145
210
  }
146
211
 
147
212
  /**
148
- * @param {ReleaseInfo[]} releases
149
- * @param {string} version
150
- * @returns {ReleaseInfo}
213
+ * Resolves and validates Geckodriver installation paths.
151
214
  */
152
- function selectRelease(releases, version) {
153
- if (version === STABLE_VERSION) {
154
- const stableReleasesAsc = releases
155
- .filter(({isDraft, isPrerelease}) => !isDraft && !isPrerelease)
156
- .toSorted((a, b) => a.version.compare(b.version));
157
- const dstRelease = stableReleasesAsc.at(-1);
158
- if (!dstRelease) {
159
- throw new Error(`Cannot find any stable GeckoDriver release: ${JSON.stringify(releases)}`);
160
- }
161
- return dstRelease;
215
+ class GeckodriverInstallPath {
216
+ /**
217
+ * @returns {string}
218
+ */
219
+ static #getExecutableName() {
220
+ return process.platform === 'win32' ? 'geckodriver.exe' : 'geckodriver';
162
221
  }
163
- const coercedVersion = semver.coerce(version);
164
- if (!coercedVersion) {
165
- throw new Error(`The provided version string '${version}' cannot be coerced to a valid SemVer representation`);
166
- }
167
- const dstRelease = releases.find((r) => r.version.compare(coercedVersion) === 0);
168
- if (!dstRelease) {
169
- throw new Error(
170
- `The provided version string '${version}' cannot be matched to any available GeckoDriver releases: ` +
171
- JSON.stringify(releases)
172
- );
222
+
223
+ /**
224
+ * @param {string} [explicitDestDir]
225
+ * @returns {Promise<{path: string, onPath: boolean}>}
226
+ */
227
+ async resolve(explicitDestDir) {
228
+ const dstRoot = explicitDestDir
229
+ ? path.resolve(explicitDestDir)
230
+ : await this.#resolveDefault();
231
+ if (!(await this.#isInstallable(dstRoot))) {
232
+ throw new Error(
233
+ `The destination directory '${dstRoot}' is not writable or already contains ` +
234
+ `a non-overwritable geckodriver binary`
235
+ );
236
+ }
237
+ return {
238
+ path: dstRoot,
239
+ onPath: this.#isOnPath(dstRoot),
240
+ };
173
241
  }
174
- return dstRelease;
175
- }
176
242
 
177
- /**
178
- *
179
- * @param {ReleaseInfo} release
180
- * @returns {ReleaseAsset}
181
- */
182
- function selectAsset(release) {
183
- if (release.assets.length === 0) {
184
- throw new Error(`GeckoDriver v${release.version} does not contain any matching releases`);
243
+ /**
244
+ * @param {string} dstRoot
245
+ * @returns {boolean}
246
+ */
247
+ #isOnPath(dstRoot) {
248
+ const pathParts = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
249
+ return pathParts
250
+ .map((pp) => path.normalize(pp))
251
+ .some((pp) => pp === path.normalize(dstRoot));
185
252
  }
186
- const dstPlatform = PLATFORM_MAPPING[process.platform];
187
- const dstArch = ARCH_MAPPING[process.arch];
188
- log.info(`Operating system: ${process.platform}@${process.arch}`);
189
- /** @type {(filterFunc: (string) => boolean) => null|ReleaseAsset} */
190
- const findAssetMatch = (filterFunc) => {
191
- for (const asset of release.assets) {
192
- if (!dstPlatform || !asset.name.includes(`-${dstPlatform}`)) {
193
- continue;
253
+
254
+ /**
255
+ * @param {string} dstRoot
256
+ * @returns {Promise<boolean>}
257
+ */
258
+ async #isInstallable(dstRoot) {
259
+ const executablePath = path.join(dstRoot, GeckodriverInstallPath.#getExecutableName());
260
+ try {
261
+ await mkdirp(dstRoot);
262
+ await fs.access(dstRoot, fsConstants.W_OK);
263
+ try {
264
+ const stats = await fs.lstat(executablePath);
265
+ if (!stats.isFile()) {
266
+ return false;
267
+ }
268
+ await fs.access(executablePath, fsConstants.W_OK);
269
+ } catch (err) {
270
+ if (/** @type {NodeJS.ErrnoException} */ (err).code !== 'ENOENT') {
271
+ return false;
272
+ }
273
+ const probePath = path.join(dstRoot, `.geckodriver-install-probe-${process.pid}`);
274
+ await fs.writeFile(probePath, '');
275
+ await fs.unlink(probePath);
194
276
  }
195
- const nameWoExt = asset.name.replace(EXT_REGEXP, '');
196
- if (filterFunc(nameWoExt)) {
197
- return asset;
277
+ return true;
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * @param {string[]} candidates
285
+ * @returns {Promise<string>}
286
+ */
287
+ async #selectFromCandidates(candidates) {
288
+ for (const candidate of candidates) {
289
+ if (await this.#isInstallable(candidate)) {
290
+ return candidate;
198
291
  }
199
292
  }
200
- return null;
201
- };
202
-
203
- // Try to find an exact match
204
- const exactMatch = findAssetMatch(
205
- (nameWoExt) =>
206
- (dstArch === 'aarch64' && nameWoExt.endsWith(`-${dstArch}`))
207
- || (['64', '32'].includes(dstArch) && nameWoExt.endsWith(`-${dstPlatform}${dstArch}`))
208
- );
209
- if (exactMatch) {
210
- return exactMatch;
293
+ throw new Error(
294
+ `Could not find a writable installation directory. Tried: ${candidates.join(', ')}`
295
+ );
211
296
  }
212
- // If no exact match has been been found then try a loose one
213
- const looseMatch = findAssetMatch(
214
- (nameWoExt) =>
215
- nameWoExt.endsWith(`-${dstPlatform}`)
216
- || (dstArch === '64' && nameWoExt.endsWith(`-${dstPlatform}32`))
217
- );
218
- if (looseMatch) {
219
- return looseMatch;
297
+
298
+ /**
299
+ * @returns {Promise<string>}
300
+ */
301
+ async #resolveDefault() {
302
+ switch (process.platform) {
303
+ case 'win32':
304
+ return path.join(process.env.LOCALAPPDATA, 'Mozilla');
305
+ case 'linux':
306
+ case 'darwin':
307
+ return this.#selectFromCandidates([
308
+ path.join('/usr', 'local', 'bin'),
309
+ path.join(homedir(), '.local', 'bin'),
310
+ ]);
311
+ default:
312
+ throw new Error(
313
+ `GeckoDriver does not support the ${process.platform} platform. ` +
314
+ `Supported platforms: ${GeckodriverReleaseCatalog.SUPPORTED_PLATFORMS}.`
315
+ );
316
+ }
220
317
  }
221
- throw new Error(
222
- `GeckoDriver v${release.version} does not contain any release matching the ` +
223
- `current OS architecture ${process.arch}. Available packages: ${release.assets.map(({name}) => name)}`
224
- );
225
318
  }
226
319
 
227
320
  /**
228
- *
229
- * @param {string} version
230
- * @returns {Promise<void>}
321
+ * Downloads and installs Geckodriver binaries.
231
322
  */
232
- async function installGeckodriver(version) {
233
- log.debug(`Retrieving releases from ${API_ROOT}`);
234
- const releases = await listReleases();
235
- if (!releases.length) {
236
- throw new Error(`Cannot retrieve any valid GeckoDriver releases from GitHub`);
323
+ class GeckodriverInstaller {
324
+ /**
325
+ * @param {GeckodriverReleaseCatalog} [catalog]
326
+ * @param {GeckodriverInstallPath} [installPath]
327
+ */
328
+ constructor(
329
+ catalog = new GeckodriverReleaseCatalog(),
330
+ installPath = new GeckodriverInstallPath(),
331
+ ) {
332
+ this.catalog = catalog;
333
+ this.installPath = installPath;
237
334
  }
238
- log.debug(`Retrieved ${releases.length} GitHub releases`);
239
- const release = selectRelease(releases, version);
240
- const asset = selectAsset(release);
241
-
242
- const [dstFolder, isInPath] = await prepareDestinationFolder();
243
- if (!isInPath) {
244
- log.warning(
245
- `The folder '${dstFolder}' is not present in the PATH environment variable. ` +
246
- `Please add it there manually before starting a session.`
335
+
336
+ /**
337
+ * @param {string} version
338
+ * @param {{destDir?: string}} [options]
339
+ * @returns {Promise<void>}
340
+ */
341
+ async install(version, options = {}) {
342
+ log.debug(`Retrieving releases from ${GeckodriverReleaseCatalog.API_ROOT}`);
343
+ const releases = await this.catalog.listReleases();
344
+ if (!releases.length) {
345
+ throw new Error(`Cannot retrieve any valid GeckoDriver releases from GitHub`);
346
+ }
347
+ log.debug(`Retrieved ${releases.length} GitHub releases`);
348
+
349
+ const release = this.catalog.selectRelease(releases, version);
350
+ const asset = this.catalog.selectAsset(release);
351
+ const {path: dstFolder, onPath} = await this.installPath.resolve(options.destDir);
352
+
353
+ if (!onPath) {
354
+ log.warning(
355
+ `The folder '${dstFolder}' is not present in the PATH environment variable. ` +
356
+ `Please add it there manually before starting a session.`
357
+ );
358
+ }
359
+
360
+ const archiveName = asset.name.replace(EXT_REGEXP, '');
361
+ const archivePath = path.join(
362
+ tmpdir(),
363
+ `${archiveName}_${(Math.random() + 1).toString(36).substring(7)}${asset.name.replace(archiveName, '')}`
247
364
  );
365
+ log.info(`Will download and install v${release.version} from ${asset.url}`);
366
+ try {
367
+ await downloadToFile(asset.url, archivePath);
368
+ const executablePath = await this.#deployBinary(archivePath, dstFolder);
369
+ await this.#clearNotarization(executablePath);
370
+ log.info(`The driver is now available at '${executablePath}'`);
371
+ } finally {
372
+ try {
373
+ await fs.unlink(archivePath);
374
+ } catch {}
375
+ }
248
376
  }
249
377
 
250
- const archiveName = asset.name.replace(EXT_REGEXP, '');
251
- const archivePath = path.join(
252
- tmpdir(),
253
- `${archiveName}_${(Math.random() + 1).toString(36).substring(7)}${asset.name.replace(archiveName, '')}`
254
- );
255
- log.info(`Will download and install v${release.version} from ${asset.url}`);
256
- try {
257
- await downloadToFile(asset.url, archivePath);
258
- let executablePath;
378
+ /**
379
+ * @param {string} archivePath
380
+ * @param {string} dstFolder
381
+ * @returns {Promise<string>}
382
+ */
383
+ async #deployBinary(archivePath, dstFolder) {
259
384
  if (archivePath.endsWith(EXT_TAR_GZ)) {
260
- executablePath = path.join(dstFolder, 'geckodriver');
385
+ const executablePath = path.join(dstFolder, 'geckodriver');
261
386
  await extractFileFromTarGz(archivePath, path.basename(executablePath), executablePath);
262
- } else {
263
- // .zip is only used for Windows
264
- executablePath = path.join(dstFolder, 'geckodriver.exe');
265
- await extractFileFromZip(archivePath, path.basename(executablePath), executablePath);
387
+ return executablePath;
266
388
  }
267
- await clearNotarization(executablePath);
268
- log.info(`The driver is now available at '${executablePath}'`);
269
- } finally {
270
- try {
271
- await fs.unlink(archivePath);
272
- } catch {}
389
+ const executablePath = path.join(dstFolder, 'geckodriver.exe');
390
+ await extractFileFromZip(archivePath, path.basename(executablePath), executablePath);
391
+ return executablePath;
273
392
  }
393
+
394
+ /**
395
+ * @param {string} dstPath
396
+ * @returns {Promise<void>}
397
+ */
398
+ async #clearNotarization(dstPath) {
399
+ if (process.platform === 'darwin') {
400
+ await exec('xattr', ['-cr', dstPath]);
401
+ }
402
+ }
403
+ }
404
+
405
+ /**
406
+ * CLI with Commander.js
407
+ */
408
+ async function main() {
409
+ const installer = new GeckodriverInstaller();
410
+ const program = new Command();
411
+
412
+ program
413
+ .name('appium driver run gecko install-geckodriver')
414
+ .description('Download and install Geckodriver from GitHub releases')
415
+ .argument('[version]', 'Geckodriver version to install (default: latest stable)', STABLE_VERSION)
416
+ .option('-d, --dest <path>', 'Destination directory for the geckodriver binary')
417
+ .addHelpText(
418
+ 'after',
419
+ `
420
+ EXAMPLES:
421
+ # Install the latest stable Geckodriver to the default location
422
+ appium driver run gecko install-geckodriver
423
+
424
+ # Install a specific version
425
+ appium driver run gecko install-geckodriver 0.36.0
426
+
427
+ # Install to a custom directory
428
+ appium driver run gecko install-geckodriver --dest ~/.local/bin
429
+
430
+ NOTE:
431
+ On macOS and Linux, the default location is the first writable directory among:
432
+ /usr/local/bin and ~/.local/bin.
433
+ On Windows, the default location is %LOCALAPPDATA%\\Mozilla.`
434
+ )
435
+ .action(async (version, options) => {
436
+ await installer.install(version, {destDir: options.dest});
437
+ });
438
+
439
+ await program.parseAsync(process.argv);
274
440
  }
275
441
 
276
- (async () => await installGeckodriver(process.argv[2] ?? STABLE_VERSION))();
442
+ (async () => await main())();
277
443
 
278
444
  /**
279
445
  * @typedef {Object} ReleaseAsset