appium 3.2.1 → 3.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.
Files changed (122) hide show
  1. package/build/lib/cli/args.d.ts +16 -12
  2. package/build/lib/cli/args.d.ts.map +1 -1
  3. package/build/lib/cli/args.js +15 -35
  4. package/build/lib/cli/args.js.map +1 -1
  5. package/build/lib/cli/driver-command.d.ts +51 -93
  6. package/build/lib/cli/driver-command.d.ts.map +1 -1
  7. package/build/lib/cli/driver-command.js +11 -66
  8. package/build/lib/cli/driver-command.js.map +1 -1
  9. package/build/lib/cli/extension-command.d.ts +211 -415
  10. package/build/lib/cli/extension-command.d.ts.map +1 -1
  11. package/build/lib/cli/extension-command.js +384 -653
  12. package/build/lib/cli/extension-command.js.map +1 -1
  13. package/build/lib/cli/extension.d.ts +11 -16
  14. package/build/lib/cli/extension.d.ts.map +1 -1
  15. package/build/lib/cli/extension.js +10 -28
  16. package/build/lib/cli/extension.js.map +1 -1
  17. package/build/lib/cli/parser.d.ts +40 -69
  18. package/build/lib/cli/parser.d.ts.map +1 -1
  19. package/build/lib/cli/parser.js +24 -59
  20. package/build/lib/cli/parser.js.map +1 -1
  21. package/build/lib/cli/plugin-command.d.ts +50 -90
  22. package/build/lib/cli/plugin-command.d.ts.map +1 -1
  23. package/build/lib/cli/plugin-command.js +11 -63
  24. package/build/lib/cli/plugin-command.js.map +1 -1
  25. package/build/lib/cli/setup-command.d.ts +21 -26
  26. package/build/lib/cli/setup-command.d.ts.map +1 -1
  27. package/build/lib/cli/setup-command.js +13 -55
  28. package/build/lib/cli/setup-command.js.map +1 -1
  29. package/build/lib/cli/utils.d.ts +27 -29
  30. package/build/lib/cli/utils.d.ts.map +1 -1
  31. package/build/lib/cli/utils.js +29 -31
  32. package/build/lib/cli/utils.js.map +1 -1
  33. package/build/lib/config-file.d.ts +24 -67
  34. package/build/lib/config-file.d.ts.map +1 -1
  35. package/build/lib/config-file.js +56 -115
  36. package/build/lib/config-file.js.map +1 -1
  37. package/build/lib/config.d.ts +42 -44
  38. package/build/lib/config.d.ts.map +1 -1
  39. package/build/lib/config.js +75 -107
  40. package/build/lib/config.js.map +1 -1
  41. package/build/lib/constants.d.ts +23 -23
  42. package/build/lib/constants.d.ts.map +1 -1
  43. package/build/lib/constants.js +10 -15
  44. package/build/lib/constants.js.map +1 -1
  45. package/build/lib/doctor/doctor.d.ts +40 -57
  46. package/build/lib/doctor/doctor.d.ts.map +1 -1
  47. package/build/lib/doctor/doctor.js +29 -60
  48. package/build/lib/doctor/doctor.js.map +1 -1
  49. package/build/lib/grid-register.d.ts +32 -7
  50. package/build/lib/grid-register.d.ts.map +1 -1
  51. package/build/lib/grid-register.js +84 -48
  52. package/build/lib/grid-register.js.map +1 -1
  53. package/build/lib/logsink.d.ts +13 -22
  54. package/build/lib/logsink.d.ts.map +1 -1
  55. package/build/lib/logsink.js +48 -103
  56. package/build/lib/logsink.js.map +1 -1
  57. package/build/lib/main.js +1 -1
  58. package/build/lib/main.js.map +1 -1
  59. package/build/lib/schema/arg-spec.d.ts +32 -107
  60. package/build/lib/schema/arg-spec.d.ts.map +1 -1
  61. package/build/lib/schema/arg-spec.js +11 -107
  62. package/build/lib/schema/arg-spec.js.map +1 -1
  63. package/build/lib/schema/cli-args.d.ts +3 -15
  64. package/build/lib/schema/cli-args.d.ts.map +1 -1
  65. package/build/lib/schema/cli-args.js +15 -105
  66. package/build/lib/schema/cli-args.js.map +1 -1
  67. package/build/lib/schema/cli-transformers.d.ts +15 -12
  68. package/build/lib/schema/cli-transformers.d.ts.map +1 -1
  69. package/build/lib/schema/cli-transformers.js +15 -45
  70. package/build/lib/schema/cli-transformers.js.map +1 -1
  71. package/build/lib/schema/index.d.ts +2 -2
  72. package/build/lib/schema/index.d.ts.map +1 -1
  73. package/build/lib/schema/index.js.map +1 -1
  74. package/build/lib/schema/keywords.d.ts +12 -20
  75. package/build/lib/schema/keywords.d.ts.map +1 -1
  76. package/build/lib/schema/keywords.js +6 -51
  77. package/build/lib/schema/keywords.js.map +1 -1
  78. package/build/lib/schema/schema.d.ts +106 -231
  79. package/build/lib/schema/schema.d.ts.map +1 -1
  80. package/build/lib/schema/schema.js +75 -345
  81. package/build/lib/schema/schema.js.map +1 -1
  82. package/build/lib/utils.d.ts +59 -238
  83. package/build/lib/utils.d.ts.map +1 -1
  84. package/build/lib/utils.js +55 -207
  85. package/build/lib/utils.js.map +1 -1
  86. package/lib/cli/{args.js → args.ts} +40 -51
  87. package/lib/cli/driver-command.ts +122 -0
  88. package/lib/cli/{extension-command.js → extension-command.ts} +610 -689
  89. package/lib/cli/extension.ts +65 -0
  90. package/lib/cli/{parser.js → parser.ts} +48 -71
  91. package/lib/cli/plugin-command.ts +117 -0
  92. package/lib/cli/{setup-command.js → setup-command.ts} +57 -72
  93. package/lib/cli/utils.ts +97 -0
  94. package/lib/config-file.ts +212 -0
  95. package/lib/{config.js → config.ts} +129 -141
  96. package/lib/{constants.js → constants.ts} +30 -41
  97. package/lib/doctor/{doctor.js → doctor.ts} +81 -91
  98. package/lib/grid-register.ts +250 -0
  99. package/lib/{logsink.js → logsink.ts} +91 -137
  100. package/lib/main.js +1 -1
  101. package/lib/schema/arg-spec.ts +131 -0
  102. package/lib/schema/cli-args.ts +171 -0
  103. package/lib/schema/cli-transformers.ts +83 -0
  104. package/lib/schema/keywords.ts +96 -0
  105. package/lib/schema/schema.ts +449 -0
  106. package/lib/utils.ts +404 -0
  107. package/package.json +19 -20
  108. package/tsconfig.json +1 -1
  109. package/build/package.json +0 -99
  110. package/lib/cli/driver-command.js +0 -174
  111. package/lib/cli/extension.js +0 -74
  112. package/lib/cli/plugin-command.js +0 -164
  113. package/lib/cli/utils.js +0 -91
  114. package/lib/config-file.js +0 -228
  115. package/lib/grid-register.js +0 -146
  116. package/lib/schema/arg-spec.js +0 -229
  117. package/lib/schema/cli-args.js +0 -254
  118. package/lib/schema/cli-transformers.js +0 -113
  119. package/lib/schema/keywords.js +0 -136
  120. package/lib/schema/schema.js +0 -725
  121. package/lib/utils.js +0 -512
  122. /package/lib/schema/{index.js → index.ts} +0 -0
@@ -60,19 +60,15 @@ class NoUpdatesAvailableError extends Error {
60
60
  }
61
61
  /**
62
62
  * Omits `driverName`/`pluginName` props from the receipt to make a {@linkcode ExtManifest}
63
- * @template {ExtensionType} ExtType
64
- * @param {ExtInstallReceipt<ExtType>} receipt
65
- * @returns {ExtManifest<ExtType>}
66
63
  */
67
64
  function receiptToManifest(receipt) {
68
- return /** @type {ExtManifest<ExtType>} */ (lodash_1.default.omit(receipt, 'driverName', 'pluginName'));
65
+ return lodash_1.default.omit(receipt, 'driverName', 'pluginName');
69
66
  }
70
67
  /**
71
68
  * Fetches the remote extension version requirements
72
69
  *
73
- * @param {string} pkgName Extension name
74
- * @param {string} [pkgVer] Extension version (if not provided then the latest is assumed)
75
- * @returns {Promise<[string, string|null]>}
70
+ * @param pkgName Extension name
71
+ * @param [pkgVer] Extension version (if not provided then the latest is assumed)
76
72
  */
77
73
  async function getRemoteExtensionVersionReq(pkgName, pkgVer) {
78
74
  const allDeps = await support_1.npm.getPackageInfo(`${pkgName}${pkgVer ? `@${pkgVer}` : ``}`, ['peerDependencies', 'dependencies']);
@@ -80,28 +76,24 @@ async function getRemoteExtensionVersionReq(pkgName, pkgVer) {
80
76
  .find(([name]) => name === 'appium');
81
77
  return [utils_2.npmPackage.version, requiredVersionPair ? requiredVersionPair[1] : null];
82
78
  }
83
- /**
84
- * @template {ExtensionType} ExtType
85
- */
86
79
  class ExtensionCliCommand {
87
80
  /**
88
81
  * This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
89
- * @type {ExtensionConfig<ExtType>}
90
82
  */
91
83
  config;
92
84
  /**
93
85
  * {@linkcode Record} of official plugins or drivers.
94
- * @type {KnownExtensions<ExtType>}
95
86
  */
96
87
  knownExtensions;
97
88
  /**
98
89
  * If `true`, command output has been requested as JSON.
99
- * @type {boolean}
100
90
  */
101
91
  isJsonOutput;
92
+ log;
102
93
  /**
103
- * Build an ExtensionCommand
104
- * @param {ExtensionCommandOptions<ExtType>} opts
94
+ * Creates an extension command instance.
95
+ *
96
+ * @param opts - constructor options containing extension config and JSON mode
105
97
  */
106
98
  constructor({ config, json }) {
107
99
  this.config = config;
@@ -115,24 +107,10 @@ class ExtensionCliCommand {
115
107
  return this.config.extensionType;
116
108
  }
117
109
  /**
118
- * Logs a message and returns an {@linkcode Error} to throw.
119
- *
120
- * For TS to understand that a function throws an exception, it must actually throw an exception--
121
- * in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
122
- * nor is something like `@returns {never}` which does not imply a thrown exception.
123
- *
124
- * @param {string} message
125
- * @protected
126
- * @throws {Error}
127
- */
128
- _createFatalError(message) {
129
- return new Error(this.log.decorate(message, 'error'));
130
- }
131
- /**
132
- * Take a CLI parse and run an extension command based on its type
110
+ * Executes an extension subcommand from parsed CLI args.
133
111
  *
134
- * @param {object} args - a key/value object with CLI flags and values
135
- * @return {Promise<object>} the result of the specific command which is executed
112
+ * @param args - parsed CLI argument object
113
+ * @returns result of the executed extension subcommand
136
114
  */
137
115
  async execute(args) {
138
116
  const cmd = args[`${this.type}Command`];
@@ -143,11 +121,10 @@ class ExtensionCliCommand {
143
121
  return await executeCmd(args);
144
122
  }
145
123
  /**
146
- * List extensions
124
+ * Lists available/installed extensions and optional update metadata.
147
125
  *
148
- * @template {ExtensionType} ExtType
149
- * @param {ListOptions} opts
150
- * @return {Promise<ExtensionList<ExtType>>} map of extension names to extension data
126
+ * @param opts - list command options
127
+ * @returns map of extension names to list data
151
128
  */
152
129
  async list({ showInstalled, showUpdates, verbose = false }) {
153
130
  const listData = this._buildListData(showInstalled);
@@ -166,284 +143,33 @@ class ExtensionCliCommand {
166
143
  return await this._displayNormalListOutput(listData, showUpdates);
167
144
  }
168
145
  /**
169
- * Build the initial list data structure from installed and known extensions
170
- *
171
- * @template {ExtensionType} ExtType
172
- * @param {boolean} showInstalled
173
- * @returns {ExtensionList<ExtType>}
174
- * @private
175
- */
176
- _buildListData(showInstalled) {
177
- const installedNames = Object.keys(this.config.installedExtensions);
178
- const knownNames = Object.keys(this.knownExtensions);
179
- return [...installedNames, ...knownNames].reduce((acc, name) => {
180
- if (!acc[name]) {
181
- if (installedNames.includes(name)) {
182
- acc[name] = {
183
- ... /** @type {Partial<ExtManifest<ExtType>>} */(this.config.installedExtensions[name]),
184
- installed: true,
185
- };
186
- }
187
- else if (!showInstalled) {
188
- acc[name] = /** @type {ExtensionListData<ExtType>} */ ({
189
- pkgName: this.knownExtensions[name],
190
- installed: false,
191
- });
192
- }
193
- }
194
- return acc;
195
- }, /** @type {ExtensionList<ExtType>} */ ({}));
196
- }
197
- /**
198
- * Check for available updates for installed extensions
199
- *
200
- * @template {ExtensionType} ExtType
201
- * @param {ExtensionList<ExtType>} listData
202
- * @param {boolean} showUpdates
203
- * @param {string} lsMsg
204
- * @returns {Promise<void>}
205
- * @private
206
- */
207
- async _checkForUpdates(listData, showUpdates, lsMsg) {
208
- await (0, utils_1.spinWith)(this.isJsonOutput, lsMsg, async () => {
209
- // We'd like to still show lsMsg even if showUpdates is false
210
- if (!showUpdates) {
211
- return;
212
- }
213
- // Filter to only extensions that need update checks (installed npm packages)
214
- const extensionsToCheck = lodash_1.default.toPairs(listData).filter(([, data]) => data.installed && data.installType === extension_config_1.INSTALL_TYPE_NPM);
215
- await bluebird_1.default.map(extensionsToCheck, async ([ext, data]) => {
216
- try {
217
- const updates = await this.checkForExtensionUpdate(ext);
218
- data.updateVersion = updates.safeUpdate;
219
- data.unsafeUpdateVersion = updates.unsafeUpdate;
220
- data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
221
- }
222
- catch (e) {
223
- data.updateError = e.message;
224
- }
225
- }, { concurrency: MAX_CONCURRENT_REPO_FETCHES });
226
- });
227
- }
228
- /**
229
- * Add repository URLs to list data for all extensions
230
- *
231
- * @template {ExtensionType} ExtType
232
- * @param {ExtensionList<ExtType>} listData
233
- * @returns {Promise<void>}
234
- * @private
235
- */
236
- async _addRepositoryUrlsToListData(listData) {
237
- await (0, utils_1.spinWith)(this.isJsonOutput, 'Fetching repository information', async () => {
238
- await bluebird_1.default.map(lodash_1.default.values(listData), async (data) => {
239
- const repoUrl = await this._getRepositoryUrl(data);
240
- if (repoUrl) {
241
- data.repositoryUrl = repoUrl;
242
- }
243
- }, { concurrency: MAX_CONCURRENT_REPO_FETCHES });
244
- });
245
- }
246
- /**
247
- * Display normal formatted output
248
- *
249
- * @template {ExtensionType} ExtType
250
- * @param {ExtensionList<ExtType>} listData
251
- * @param {boolean} showUpdates
252
- * @returns {Promise<ExtensionList<ExtType>>}
253
- * @private
254
- */
255
- async _displayNormalListOutput(listData, showUpdates) {
256
- for (const [name, data] of lodash_1.default.toPairs(listData)) {
257
- const line = await this._formatExtensionLine(name, data, showUpdates);
258
- this.log.log(line);
259
- }
260
- return listData;
261
- }
262
- /**
263
- * Format a single extension line for display
264
- *
265
- * @template {ExtensionType} ExtType
266
- * @param {string} name
267
- * @param {ExtensionListData<ExtType>} data
268
- * @param {boolean} showUpdates
269
- * @returns {Promise<string>}
270
- * @private
271
- */
272
- async _formatExtensionLine(name, data, showUpdates) {
273
- if (data.installed) {
274
- const installTxt = this._formatInstallText(/** @type {InstalledExtensionListData<ExtType>} */ (data));
275
- const updateTxt = showUpdates ? this._formatUpdateText(/** @type {InstalledExtensionListData<ExtType>} */ (data)) : '';
276
- return `- ${name.yellow}${installTxt}${updateTxt}`;
277
- }
278
- const installTxt = ' [not installed]'.grey;
279
- return `- ${name.yellow}${installTxt}`;
280
- }
281
- /**
282
- * Format installation status text
283
- *
284
- * @template {ExtensionType} ExtType
285
- * @param {InstalledExtensionListData<ExtType>} data
286
- * @returns {string}
287
- * @private
288
- */
289
- _formatInstallText(data) {
290
- const { installType, installSpec, version } = data;
291
- let typeTxt;
292
- switch (installType) {
293
- case extension_config_1.INSTALL_TYPE_GIT:
294
- case extension_config_1.INSTALL_TYPE_GITHUB:
295
- typeTxt = `(cloned from ${installSpec})`.yellow;
296
- break;
297
- case extension_config_1.INSTALL_TYPE_LOCAL:
298
- typeTxt = `(linked from ${installSpec})`.magenta;
299
- break;
300
- case extension_config_1.INSTALL_TYPE_DEV:
301
- typeTxt = '(dev mode)';
302
- break;
303
- default:
304
- typeTxt = '(npm)';
305
- }
306
- return `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
307
- }
308
- /**
309
- * Format update information text
310
- *
311
- * @template {ExtensionType} ExtType
312
- * @param {InstalledExtensionListData<ExtType>} data
313
- * @returns {string}
314
- * @private
315
- */
316
- _formatUpdateText(data) {
317
- const { updateVersion, unsafeUpdateVersion, upToDate, updateError } = data;
318
- if (updateError) {
319
- return ` [Cannot check for updates: ${updateError}]`.red;
320
- }
321
- let txt = '';
322
- if (updateVersion) {
323
- txt += ` [${updateVersion} available]`.magenta;
324
- }
325
- if (upToDate) {
326
- txt += ` [Up to date]`.green;
327
- }
328
- if (unsafeUpdateVersion) {
329
- txt += ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
330
- }
331
- return txt;
332
- }
333
- /**
334
- * Get repository URL from package data
335
- *
336
- * @template {ExtensionType} ExtType
337
- * @param {ExtensionListData<ExtType>} data
338
- * @returns {Promise<string|null>}
339
- * @private
340
- */
341
- async _getRepositoryUrl(data) {
342
- if (data.installed && data.installPath) {
343
- return await this._getRepositoryUrlFromInstalled(
344
- /** @type {InstalledExtensionListData<ExtType>} */ (data));
345
- }
346
- if (data.pkgName && !data.installed) {
347
- return await this._getRepositoryUrlFromNpm(data.pkgName);
348
- }
349
- return null;
350
- }
351
- /**
352
- * Get repository URL from installed extension's package.json
353
- *
354
- * @template {ExtensionType} ExtType
355
- * @param {InstalledExtensionListData<ExtType>} data
356
- * @returns {Promise<string|null>}
357
- * @private
358
- */
359
- async _getRepositoryUrlFromInstalled(data) {
360
- try {
361
- const pkgJsonPath = node_path_1.default.join(data.installPath, 'package.json');
362
- if (await support_1.fs.exists(pkgJsonPath)) {
363
- const pkg = JSON.parse(await support_1.fs.readFile(pkgJsonPath, 'utf8'));
364
- if (pkg.repository) {
365
- if (typeof pkg.repository === 'string') {
366
- return pkg.repository;
367
- }
368
- if (pkg.repository.url) {
369
- return pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
370
- }
371
- }
372
- }
373
- }
374
- catch {
375
- // Ignore errors reading package.json
376
- }
377
- return null;
378
- }
379
- /**
380
- * Get repository URL from npm for a package name
146
+ * Logs a message and returns an {@linkcode Error} to throw.
381
147
  *
382
- * @param {string} pkgName
383
- * @returns {Promise<string|null>}
384
- * @private
385
- */
386
- async _getRepositoryUrlFromNpm(pkgName) {
387
- try {
388
- const repoInfo = await support_1.npm.getPackageInfo(pkgName, ['repository']);
389
- // When requesting only 'repository', npm.getPackageInfo returns the repository object directly
390
- if (repoInfo) {
391
- if (typeof repoInfo === 'string') {
392
- return repoInfo;
393
- }
394
- if (repoInfo.url) {
395
- return repoInfo.url.replace(/^git\+/, '').replace(/\.git$/, '');
396
- }
397
- }
398
- }
399
- catch {
400
- // Ignore errors fetching from npm
401
- }
402
- return null;
403
- }
404
- /**
405
- * Checks whether the given extension is compatible with the currently installed server
148
+ * For TS to understand that a function throws an exception, it must actually throw an exception--
149
+ * in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
150
+ * nor is something like a `never` return annotation, which does not imply a thrown exception.
406
151
  *
407
- * @param {InstallViaNpmArgs} installViaNpmOpts
408
- * @returns {Promise<void>}
152
+ * @throws {Error}
409
153
  */
410
- async _checkInstallCompatibility({ installSpec, pkgName, pkgVer, installType }) {
411
- if (extension_config_1.INSTALL_TYPE_NPM !== installType) {
412
- return;
413
- }
414
- await (0, utils_1.spinWith)(this.isJsonOutput, `Checking if '${pkgName}' is compatible`, async () => {
415
- const [serverVersion, extVersionRequirement] = await getRemoteExtensionVersionReq(pkgName, pkgVer);
416
- if (serverVersion && extVersionRequirement && !semver.satisfies(serverVersion, extVersionRequirement)) {
417
- throw this._createFatalError(`'${installSpec}' cannot be installed because the server version it requires (${extVersionRequirement}) ` +
418
- `does not meet the currently installed one (${serverVersion}). Please install ` +
419
- `a compatible server version first.`);
420
- }
421
- });
154
+ _createFatalError(message) {
155
+ return new Error(this.log.decorate(message, 'error'));
422
156
  }
423
157
  /**
424
- * Install an extension
158
+ * Build the initial list data structure from installed and known extensions
425
159
  *
426
- * @param {InstallOpts} opts
427
- * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
428
160
  */
429
161
  async _install({ installSpec, installType, packageName }) {
430
- /** @type {ExtInstallReceipt<ExtType>} */
431
- let receipt;
432
162
  if (packageName && [extension_config_1.INSTALL_TYPE_LOCAL, extension_config_1.INSTALL_TYPE_NPM].includes(installType)) {
433
163
  throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
434
164
  }
435
165
  if (!packageName && [extension_config_1.INSTALL_TYPE_GIT, extension_config_1.INSTALL_TYPE_GITHUB].includes(installType)) {
436
166
  throw this._createFatalError(`When using --source=${installType}, must also use --package`);
437
167
  }
438
- /**
439
- * @type {InstallViaNpmArgs}
440
- */
441
168
  let installViaNpmOpts;
442
169
  /**
443
170
  * The probable (?) name of the extension derived from the install spec.
444
171
  *
445
172
  * If using a local install type, this will remain empty.
446
- * @type {string}
447
173
  */
448
174
  let probableExtName = '';
449
175
  // depending on `installType`, build the options to pass into `installViaNpm`
@@ -455,9 +181,9 @@ class ExtensionCliCommand {
455
181
  installViaNpmOpts = {
456
182
  installSpec,
457
183
  installType,
458
- pkgName: /** @type {string} */ (packageName),
184
+ pkgName: packageName,
459
185
  };
460
- probableExtName = /** @type {string} */ (packageName);
186
+ probableExtName = packageName;
461
187
  }
462
188
  else if (installType === extension_config_1.INSTALL_TYPE_GIT) {
463
189
  // git urls can have '.git' at the end, but this is not necessary and would complicate the
@@ -466,12 +192,13 @@ class ExtensionCliCommand {
466
192
  installViaNpmOpts = {
467
193
  installSpec,
468
194
  installType,
469
- pkgName: /** @type {string} */ (packageName),
195
+ pkgName: packageName,
470
196
  };
471
- probableExtName = /** @type {string} */ (packageName);
197
+ probableExtName = packageName;
472
198
  }
473
199
  else {
474
- let pkgName, pkgVer;
200
+ let pkgName;
201
+ let pkgVer;
475
202
  if (installType === extension_config_1.INSTALL_TYPE_LOCAL) {
476
203
  pkgName = node_path_1.default.isAbsolute(installSpec) ? installSpec : node_path_1.default.resolve(installSpec);
477
204
  }
@@ -522,11 +249,10 @@ class ExtensionCliCommand {
522
249
  `installed ${this.type}s with "appium ${this.type} list --installed".`);
523
250
  }
524
251
  await this._checkInstallCompatibility(installViaNpmOpts);
525
- receipt = await this.installViaNpm(installViaNpmOpts);
252
+ const receipt = await this.installViaNpm(installViaNpmOpts);
526
253
  // this _should_ be the same as `probablyExtName` as the one derived above unless
527
254
  // install type is local.
528
- /** @type {string} */
529
- const extName = receipt[ /** @type {string} */(`${this.type}Name`)];
255
+ const extName = receipt[`${this.type}Name`];
530
256
  // check _a second time_ with the more-accurate extName
531
257
  if (this.config.isInstalled(extName)) {
532
258
  throw this._createFatalError(`A ${this.type} named "${extName}" is already installed. ` +
@@ -535,7 +261,6 @@ class ExtensionCliCommand {
535
261
  }
536
262
  // this field does not exist as such in the manifest (it's used as a property name instead)
537
263
  // so that's why it's being removed here.
538
- /** @type {ExtManifest<ExtType>} */
539
264
  const extManifest = receiptToManifest(receipt);
540
265
  const [errors, warnings] = await bluebird_1.default.all([
541
266
  this.config.getProblems(extName, extManifest),
@@ -561,146 +286,34 @@ class ExtensionCliCommand {
561
286
  return this.config.installedExtensions;
562
287
  }
563
288
  /**
564
- * Install an extension via NPM
289
+ * Uninstall an extension.
290
+ *
291
+ * First tries to do this via `npm uninstall`, but if that fails, just `rm -rf`'s the extension dir.
565
292
  *
566
- * @param {InstallViaNpmArgs} args
567
- * @returns {Promise<ExtInstallReceipt<ExtType>>}
293
+ * Will only remove the extension from the manifest if it has been successfully removed.
294
+ *
295
+ * @return map of all installed extension names to extension data (without the extension just uninstalled)
568
296
  */
569
- async installViaNpm({ installSpec, pkgName, pkgVer, installType }) {
570
- const installMsg = `Installing '${installSpec}'`;
571
- const validateMsg = `Validating '${installSpec}'`;
572
- // the string used for installation is either <name>@<ver> in the case of a standard NPM
573
- // package, or whatever the user sent in otherwise.
574
- const installStr = installType === extension_config_1.INSTALL_TYPE_NPM ? `${pkgName}${pkgVer ? `@${pkgVer}` : ''}` : installSpec;
575
- const appiumHome = this.config.appiumHome;
576
- try {
577
- const { pkg, installPath } = await (0, utils_1.spinWith)(this.isJsonOutput, installMsg, async () => await support_1.npm.installPackage(appiumHome, installStr, { pkgName, installType }));
578
- await (0, utils_1.spinWith)(this.isJsonOutput, validateMsg, async () => {
579
- this.validatePackageJson(pkg, installSpec);
580
- });
581
- return this.getInstallationReceipt({
582
- pkg,
583
- installPath,
584
- installType,
585
- installSpec,
586
- });
297
+ async _uninstall({ installSpec }) {
298
+ if (!this.config.isInstalled(installSpec)) {
299
+ throw this._createFatalError(`Can't uninstall ${this.type} '${installSpec}'; it is not installed`);
587
300
  }
588
- catch (err) {
589
- throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
301
+ const extRecord = this.config.installedExtensions[installSpec];
302
+ if (extRecord.installType === extension_config_1.INSTALL_TYPE_DEV) {
303
+ this.log.warn(`Cannot uninstall ${this.type} "${installSpec}" because it is in development!`);
304
+ return this.config.installedExtensions;
590
305
  }
306
+ const pkgName = extRecord.pkgName;
307
+ await (0, utils_1.spinWith)(this.isJsonOutput, `Uninstalling ${this.type} '${installSpec}'`, async () => {
308
+ await support_1.npm.uninstallPackage(this.config.appiumHome, pkgName);
309
+ });
310
+ await this.config.removeExtension(installSpec);
311
+ this.log.ok(`Successfully uninstalled ${this.type} '${installSpec}'`.green);
312
+ return this.config.installedExtensions;
591
313
  }
592
314
  /**
593
- * Get the text which should be displayed to the user after an extension has been installed. This
594
- * is designed to be overridden by drivers/plugins with their own particular text.
315
+ * Attempt to update one or more drivers using NPM
595
316
  *
596
- * @param {ExtensionArgs} args
597
- * @returns {string}
598
- */
599
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
600
- getPostInstallText(args) {
601
- throw this._createFatalError('Must be implemented in final class');
602
- }
603
- /**
604
- * Once a package is installed on-disk, this gathers some necessary metadata for validation.
605
- *
606
- * @param {GetInstallationReceiptOpts<ExtType>} opts
607
- * @returns {ExtInstallReceipt<ExtType>}
608
- */
609
- getInstallationReceipt({ pkg, installPath, installType, installSpec }) {
610
- const { appium, name, version, peerDependencies } = pkg;
611
- const strVersion = /** @type {string} */ (version);
612
- /** @type {import('appium/types').InternalMetadata} */
613
- const internal = {
614
- pkgName: /** @type {string} */ (name),
615
- version: strVersion,
616
- installType,
617
- installSpec,
618
- installPath,
619
- appiumVersion: peerDependencies?.appium,
620
- };
621
- /** @type {ExtMetadata<ExtType>} */
622
- const extMetadata = appium;
623
- return {
624
- ...internal,
625
- ...extMetadata,
626
- };
627
- }
628
- /**
629
- * Validates the _required_ root fields of an extension's `package.json` file.
630
- *
631
- * These required fields are:
632
- * - `name`
633
- * - `version`
634
- * - `appium`
635
- * @param {import('type-fest').PackageJson} pkg - `package.json` of extension
636
- * @param {string} installSpec - Extension name/spec
637
- * @throws {ReferenceError} If `package.json` has a missing or invalid field
638
- * @returns {pkg is ExtPackageJson<ExtType>}
639
- */
640
- validatePackageJson(pkg, installSpec) {
641
- const { appium, name, version } = /** @type {ExtPackageJson<ExtType>} */ (pkg);
642
- /**
643
- *
644
- * @param {string} field
645
- * @returns {ReferenceError}
646
- */
647
- const createMissingFieldError = (field) => new ReferenceError(`${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``);
648
- if (!name) {
649
- throw createMissingFieldError('name');
650
- }
651
- if (!version) {
652
- throw createMissingFieldError('version');
653
- }
654
- if (!appium) {
655
- throw createMissingFieldError('appium');
656
- }
657
- this.validateExtensionFields(appium, installSpec);
658
- return true;
659
- }
660
- /**
661
- * For any `package.json` fields which a particular type of extension requires, validate the
662
- * presence and form of those fields on the `package.json` data, throwing an error if anything is
663
- * amiss.
664
- *
665
- * @param {ExtMetadata<ExtType>} extMetadata - the data in the "appium" field of `package.json` for an extension
666
- * @param {string} installSpec - Extension name/spec
667
- */
668
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
669
- validateExtensionFields(extMetadata, installSpec) {
670
- throw this._createFatalError('Must be implemented in final class');
671
- }
672
- /**
673
- * Uninstall an extension.
674
- *
675
- * First tries to do this via `npm uninstall`, but if that fails, just `rm -rf`'s the extension dir.
676
- *
677
- * Will only remove the extension from the manifest if it has been successfully removed.
678
- *
679
- * @param {UninstallOpts} opts
680
- * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data (without the extension just uninstalled)
681
- */
682
- async _uninstall({ installSpec }) {
683
- if (!this.config.isInstalled(installSpec)) {
684
- throw this._createFatalError(`Can't uninstall ${this.type} '${installSpec}'; it is not installed`);
685
- }
686
- const extRecord = this.config.installedExtensions[installSpec];
687
- if (extRecord.installType === extension_config_1.INSTALL_TYPE_DEV) {
688
- this.log.warn(`Cannot uninstall ${this.type} "${installSpec}" because it is in development!`);
689
- return this.config.installedExtensions;
690
- }
691
- const pkgName = extRecord.pkgName;
692
- await (0, utils_1.spinWith)(this.isJsonOutput, `Uninstalling ${this.type} '${installSpec}'`, async () => {
693
- await support_1.npm.uninstallPackage(this.config.appiumHome, pkgName);
694
- });
695
- await this.config.removeExtension(installSpec);
696
- this.log.ok(`Successfully uninstalled ${this.type} '${installSpec}'`.green);
697
- return this.config.installedExtensions;
698
- }
699
- /**
700
- * Attempt to update one or more drivers using NPM
701
- *
702
- * @param {ExtensionUpdateOpts} updateSpec
703
- * @return {Promise<ExtensionUpdateResult>}
704
317
  */
705
318
  async _update({ installSpec, unsafe }) {
706
319
  const shouldUpdateAll = installSpec === UPDATE_ALL;
@@ -712,11 +325,9 @@ class ExtensionCliCommand {
712
325
  ? Object.keys(this.config.installedExtensions)
713
326
  : [installSpec];
714
327
  // 'errors' will have ext names as keys and error objects as values
715
- /** @type {Record<string,Error>} */
716
328
  const errors = {};
717
329
  // 'updates' will have ext names as keys and update objects as values, where an update
718
330
  // object is of the form {from: versionString, to: versionString}
719
- /** @type {Record<string,UpdateReport>} */
720
331
  const updates = {};
721
332
  for (const e of extsToUpdate) {
722
333
  try {
@@ -738,6 +349,9 @@ class ExtensionCliCommand {
738
349
  `breaking changes. If you want to apply this update, re-run with --unsafe`);
739
350
  }
740
351
  const updateVer = unsafe && update.unsafeUpdate ? update.unsafeUpdate : update.safeUpdate;
352
+ if (!updateVer) {
353
+ throw new NoUpdatesAvailableError();
354
+ }
741
355
  await (0, utils_1.spinWith)(this.isJsonOutput, `Updating ${this.type} '${e}' from ${update.current} to ${updateVer}`, async () => await this.updateExtension(e, updateVer));
742
356
  // if we're doing a safe update, but an unsafe update is also available, let the user know
743
357
  if (!unsafe && update.unsafeUpdate) {
@@ -774,15 +388,13 @@ class ExtensionCliCommand {
774
388
  * Given an extension name, figure out what its highest possible version upgrade is, and also the
775
389
  * highest possible safe upgrade.
776
390
  *
777
- * @param {string} ext - name of extension
778
- * @return {Promise<PossibleUpdates>}
391
+ * @param ext - name of extension
779
392
  */
780
393
  async checkForExtensionUpdate(ext) {
781
394
  // TODO decide how we want to handle beta versions?
782
395
  // this is a helper method, 'ext' is assumed to already be installed here, and of the npm
783
396
  // install type
784
397
  const { version, pkgName } = this.config.installedExtensions[ext];
785
- /** @type {string?} */
786
398
  let unsafeUpdate = await support_1.npm.getLatestVersion(this.config.appiumHome, pkgName);
787
399
  let safeUpdate = await support_1.npm.getLatestSafeUpgradeVersion(this.config.appiumHome, pkgName, version);
788
400
  if (unsafeUpdate !== null && !support_1.util.compareVersions(unsafeUpdate, '>', version)) {
@@ -800,46 +412,10 @@ class ExtensionCliCommand {
800
412
  }
801
413
  return { current: version, safeUpdate, unsafeUpdate };
802
414
  }
803
- /**
804
- * Actually update an extension installed by NPM, using the NPM cli. And update the installation
805
- * manifest.
806
- *
807
- * @param {string} installSpec - name of extension to update
808
- * @param {string} version - version string identifier to update extension to
809
- * @returns {Promise<void>}
810
- */
811
- async updateExtension(installSpec, version) {
812
- const { pkgName, installType } = this.config.installedExtensions[installSpec];
813
- const extData = await this.installViaNpm({
814
- installSpec,
815
- installType,
816
- pkgName,
817
- pkgVer: version,
818
- });
819
- delete extData[ /** @type {string} */(`${this.type}Name`)];
820
- await this.config.updateExtension(installSpec, extData);
821
- }
822
- /**
823
- * Just wraps {@linkcode child_process.spawn} with some default options
824
- *
825
- * @param {string} cwd - CWD
826
- * @param {string} script - Path to script
827
- * @param {string[]} args - Extra args for script
828
- * @param {import('child_process').SpawnOptions} opts - Options
829
- * @returns {import('node:child_process').ChildProcess}
830
- */
831
- _runUnbuffered(cwd, script, args = [], opts = {}) {
832
- return (0, node_child_process_1.spawn)(process.execPath, [script, ...args], {
833
- cwd,
834
- stdio: 'inherit',
835
- ...opts,
836
- });
837
- }
838
415
  /**
839
416
  * Runs doctor checks for the given extension.
840
417
  *
841
- * @param {DoctorOptions} opts
842
- * @returns {Promise<number>} The amount of Doctor checks that were
418
+ * @returns The amount of Doctor checks that were
843
419
  * successfully loaded and executed for the given extension
844
420
  * @throws {Error} If any of the mandatory Doctor checks fails.
845
421
  */
@@ -867,7 +443,8 @@ class ExtensionCliCommand {
867
443
  throw this._createFatalError(`The 'doctor' entry in the package manifest '${packageJsonPath}' must be a proper object ` +
868
444
  `containing the 'checks' key with the array of script paths`);
869
445
  }
870
- const paths = doctorSpec.checks.map((/** @type {string} */ p) => {
446
+ const paths = doctorSpec.checks
447
+ .map((p) => {
871
448
  const scriptPath = node_path_1.default.resolve(moduleRoot, p);
872
449
  if (!node_path_1.default.normalize(scriptPath).startsWith(node_path_1.default.normalize(moduleRoot))) {
873
450
  this.log.error(`The doctor check script '${p}' from the package manifest '${packageJsonPath}' must be located ` +
@@ -875,8 +452,8 @@ class ExtensionCliCommand {
875
452
  return null;
876
453
  }
877
454
  return scriptPath;
878
- }).filter(Boolean);
879
- /** @type {Promise[]} */
455
+ })
456
+ .filter((p) => Boolean(p));
880
457
  const loadChecksPromises = [];
881
458
  for (const p of paths) {
882
459
  const promise = (async () => {
@@ -891,8 +468,7 @@ class ExtensionCliCommand {
891
468
  })();
892
469
  loadChecksPromises.push(promise);
893
470
  }
894
- const isDoctorCheck = (/** @type {any} */ x) => ['diagnose', 'fix', 'hasAutofix', 'isOptional'].every((method) => lodash_1.default.isFunction(x?.[method]));
895
- /** @type {import('@appium/types').IDoctorCheck[]} */
471
+ const isDoctorCheck = (x) => ['diagnose', 'fix', 'hasAutofix', 'isOptional'].every((method) => lodash_1.default.isFunction(x?.[method]));
896
472
  const checks = lodash_1.default.flatMap((await bluebird_1.default.all(loadChecksPromises)).filter(Boolean).map(lodash_1.default.toPairs))
897
473
  .map(([, value]) => value)
898
474
  .filter(isDoctorCheck);
@@ -916,10 +492,8 @@ class ExtensionCliCommand {
916
492
  * `scripts` field is not a plain object, or if the `scriptName` is
917
493
  * not found within `scripts` object.
918
494
  *
919
- * @param {RunOptions} opts
920
- * @return {Promise<RunOutput>}
921
495
  */
922
- async _run({ installSpec, scriptName, extraArgs = [], bufferOutput = false }) {
496
+ async _run({ installSpec, scriptName, extraArgs = [], bufferOutput = false, }) {
923
497
  if (!this.config.isInstalled(installSpec)) {
924
498
  throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
925
499
  }
@@ -948,7 +522,7 @@ class ExtensionCliCommand {
948
522
  this.log.ok(`Successfully retrieved the list of scripts`.green);
949
523
  return {};
950
524
  }
951
- if (!(scriptName in /** @type {Record<string,string>} */ (extScripts))) {
525
+ if (!(scriptName in extScripts)) {
952
526
  throw this._createFatalError(`The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`);
953
527
  }
954
528
  const scriptPath = extScripts[scriptName];
@@ -1003,21 +577,335 @@ class ExtensionCliCommand {
1003
577
  throw this._createFatalError(message);
1004
578
  }
1005
579
  }
580
+ _buildListData(showInstalled) {
581
+ const installedNames = Object.keys(this.config.installedExtensions);
582
+ const knownNames = Object.keys(this.knownExtensions);
583
+ return [...installedNames, ...knownNames].reduce((acc, name) => {
584
+ if (!acc[name]) {
585
+ if (installedNames.includes(name)) {
586
+ acc[name] = {
587
+ ...this.config.installedExtensions[name],
588
+ installed: true,
589
+ };
590
+ }
591
+ else if (!showInstalled) {
592
+ acc[name] = {
593
+ pkgName: this.knownExtensions[name],
594
+ installed: false,
595
+ };
596
+ }
597
+ }
598
+ return acc;
599
+ }, {});
600
+ }
601
+ /**
602
+ * Install an extension via NPM
603
+ *
604
+ */
605
+ async installViaNpm({ installSpec, pkgName, pkgVer, installType, }) {
606
+ const installMsg = `Installing '${installSpec}'`;
607
+ const validateMsg = `Validating '${installSpec}'`;
608
+ // the string used for installation is either <name>@<ver> in the case of a standard NPM
609
+ // package, or whatever the user sent in otherwise.
610
+ const installStr = installType === extension_config_1.INSTALL_TYPE_NPM ? `${pkgName}${pkgVer ? `@${pkgVer}` : ''}` : installSpec;
611
+ const appiumHome = this.config.appiumHome;
612
+ try {
613
+ const { pkg, installPath } = await (0, utils_1.spinWith)(this.isJsonOutput, installMsg, async () => await support_1.npm.installPackage(appiumHome, installStr, { pkgName, installType }));
614
+ const validatedPkg = await (0, utils_1.spinWith)(this.isJsonOutput, validateMsg, async () => this.validatePackageJson(pkg, installSpec));
615
+ return this.getInstallationReceipt({
616
+ pkg: validatedPkg,
617
+ installPath,
618
+ installType,
619
+ installSpec,
620
+ });
621
+ }
622
+ catch (err) {
623
+ throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
624
+ }
625
+ }
626
+ /**
627
+ * Actually update an extension installed by NPM, using the NPM cli. And update the installation
628
+ * manifest.
629
+ *
630
+ * @param installSpec - name of extension to update
631
+ * @param version - version string identifier to update extension to
632
+ */
633
+ async updateExtension(installSpec, version) {
634
+ const { pkgName, installType } = this.config.installedExtensions[installSpec];
635
+ const extData = await this.installViaNpm({
636
+ installSpec,
637
+ installType,
638
+ pkgName,
639
+ pkgVer: version,
640
+ });
641
+ delete extData[`${this.type}Name`];
642
+ await this.config.updateExtension(installSpec, extData);
643
+ }
644
+ /**
645
+ * Just wraps {@linkcode child_process.spawn} with some default options
646
+ *
647
+ * @param cwd - CWD
648
+ * @param script - Path to script
649
+ * @param args - Extra args for script
650
+ * @param opts - Options
651
+ */
652
+ _runUnbuffered(cwd, script, args = [], opts = {}) {
653
+ return (0, node_child_process_1.spawn)(process.execPath, [script, ...args], {
654
+ cwd,
655
+ stdio: 'inherit',
656
+ ...opts,
657
+ });
658
+ }
659
+ /**
660
+ * Once a package is installed on-disk, this gathers some necessary metadata for validation.
661
+ *
662
+ */
663
+ getInstallationReceipt({ pkg, installPath, installType, installSpec, }) {
664
+ const { appium, name, version, peerDependencies } = pkg;
665
+ const strVersion = version;
666
+ const internal = {
667
+ pkgName: name,
668
+ version: strVersion,
669
+ installType,
670
+ installSpec,
671
+ installPath,
672
+ appiumVersion: peerDependencies?.appium,
673
+ };
674
+ const extMetadata = appium;
675
+ return {
676
+ ...internal,
677
+ ...extMetadata,
678
+ };
679
+ }
680
+ /**
681
+ * Validates the _required_ root fields of an extension's `package.json` file.
682
+ *
683
+ * These required fields are:
684
+ * - `name`
685
+ * - `version`
686
+ * - `appium`
687
+ * @param pkg - `package.json` of extension
688
+ * @param installSpec - Extension name/spec
689
+ * @throws {ReferenceError} If `package.json` has a missing or invalid field
690
+ */
691
+ validatePackageJson(pkg, installSpec) {
692
+ const { appium, name, version } = pkg;
693
+ const createMissingFieldError = (field) => new ReferenceError(`${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``);
694
+ if (!name) {
695
+ throw createMissingFieldError('name');
696
+ }
697
+ if (!version) {
698
+ throw createMissingFieldError('version');
699
+ }
700
+ if (!appium) {
701
+ throw createMissingFieldError('appium');
702
+ }
703
+ this.validateExtensionFields(appium, installSpec);
704
+ return pkg;
705
+ }
706
+ /**
707
+ * Check for available updates for installed extensions
708
+ *
709
+ */
710
+ async _checkForUpdates(listData, showUpdates, lsMsg) {
711
+ await (0, utils_1.spinWith)(this.isJsonOutput, lsMsg, async () => {
712
+ // We'd like to still show lsMsg even if showUpdates is false
713
+ if (!showUpdates) {
714
+ return;
715
+ }
716
+ // Filter to only extensions that need update checks (installed npm packages)
717
+ const extensionsToCheck = lodash_1.default.toPairs(listData).filter(([, data]) => data.installed && data.installType === extension_config_1.INSTALL_TYPE_NPM);
718
+ await bluebird_1.default.map(extensionsToCheck, async ([ext, data]) => {
719
+ try {
720
+ const updates = await this.checkForExtensionUpdate(ext);
721
+ data.updateVersion = updates.safeUpdate;
722
+ data.unsafeUpdateVersion = updates.unsafeUpdate;
723
+ data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
724
+ }
725
+ catch (e) {
726
+ data.updateError = e.message;
727
+ }
728
+ }, { concurrency: MAX_CONCURRENT_REPO_FETCHES });
729
+ });
730
+ }
731
+ /**
732
+ * Add repository URLs to list data for all extensions
733
+ *
734
+ */
735
+ async _addRepositoryUrlsToListData(listData) {
736
+ await (0, utils_1.spinWith)(this.isJsonOutput, 'Fetching repository information', async () => {
737
+ await bluebird_1.default.map(lodash_1.default.values(listData), async (data) => {
738
+ const repoUrl = await this._getRepositoryUrl(data);
739
+ if (repoUrl) {
740
+ data.repositoryUrl = repoUrl;
741
+ }
742
+ }, { concurrency: MAX_CONCURRENT_REPO_FETCHES });
743
+ });
744
+ }
745
+ /**
746
+ * Display normal formatted output
747
+ *
748
+ */
749
+ async _displayNormalListOutput(listData, showUpdates) {
750
+ for (const [name, data] of lodash_1.default.toPairs(listData)) {
751
+ const line = await this._formatExtensionLine(name, data, showUpdates);
752
+ this.log.log(line);
753
+ }
754
+ return listData;
755
+ }
756
+ /**
757
+ * Format a single extension line for display
758
+ *
759
+ */
760
+ async _formatExtensionLine(name, data, showUpdates) {
761
+ if (data.installed) {
762
+ const installTxt = this._formatInstallText(data);
763
+ const updateTxt = showUpdates ? this._formatUpdateText(data) : '';
764
+ return `- ${name.yellow}${installTxt}${updateTxt}`;
765
+ }
766
+ const installTxt = ' [not installed]'.grey;
767
+ return `- ${name.yellow}${installTxt}`;
768
+ }
769
+ /**
770
+ * Format installation status text
771
+ *
772
+ */
773
+ _formatInstallText(data) {
774
+ const { installType, installSpec, version } = data;
775
+ let typeTxt;
776
+ switch (installType) {
777
+ case extension_config_1.INSTALL_TYPE_GIT:
778
+ case extension_config_1.INSTALL_TYPE_GITHUB:
779
+ typeTxt = `(cloned from ${installSpec})`.yellow;
780
+ break;
781
+ case extension_config_1.INSTALL_TYPE_LOCAL:
782
+ typeTxt = `(linked from ${installSpec})`.magenta;
783
+ break;
784
+ case extension_config_1.INSTALL_TYPE_DEV:
785
+ typeTxt = '(dev mode)';
786
+ break;
787
+ default:
788
+ typeTxt = '(npm)';
789
+ }
790
+ return `@${String(version).yellow} ${('[installed ' + typeTxt + ']').green}`;
791
+ }
792
+ /**
793
+ * Format update information text
794
+ *
795
+ */
796
+ _formatUpdateText(data) {
797
+ const { updateVersion, unsafeUpdateVersion, upToDate, updateError } = data;
798
+ if (updateError) {
799
+ return ` [Cannot check for updates: ${updateError}]`.red;
800
+ }
801
+ let txt = '';
802
+ if (updateVersion) {
803
+ txt += ` [${updateVersion} available]`.magenta;
804
+ }
805
+ if (upToDate) {
806
+ txt += ` [Up to date]`.green;
807
+ }
808
+ if (unsafeUpdateVersion) {
809
+ txt += ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
810
+ }
811
+ return txt;
812
+ }
813
+ /**
814
+ * Get repository URL from package data
815
+ *
816
+ */
817
+ async _getRepositoryUrl(data) {
818
+ if (data.installed && data.installPath) {
819
+ return await this._getRepositoryUrlFromInstalled(data);
820
+ }
821
+ if (data.pkgName && !data.installed) {
822
+ return await this._getRepositoryUrlFromNpm(data.pkgName);
823
+ }
824
+ return null;
825
+ }
826
+ /**
827
+ * Get repository URL from installed extension's package.json
828
+ *
829
+ */
830
+ async _getRepositoryUrlFromInstalled(data) {
831
+ try {
832
+ const pkgJsonPath = node_path_1.default.join(String(data.installPath), 'package.json');
833
+ if (await support_1.fs.exists(pkgJsonPath)) {
834
+ const pkg = JSON.parse(await support_1.fs.readFile(pkgJsonPath, 'utf8'));
835
+ if (pkg.repository) {
836
+ if (typeof pkg.repository === 'string') {
837
+ return pkg.repository;
838
+ }
839
+ if (pkg.repository.url) {
840
+ return pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
841
+ }
842
+ }
843
+ }
844
+ }
845
+ catch {
846
+ // Ignore errors reading package.json
847
+ }
848
+ return null;
849
+ }
850
+ /**
851
+ * Get repository URL from npm for a package name
852
+ *
853
+ */
854
+ async _getRepositoryUrlFromNpm(pkgName) {
855
+ try {
856
+ const repoInfo = await support_1.npm.getPackageInfo(pkgName, ['repository']);
857
+ // When requesting only 'repository', npm.getPackageInfo returns the repository object directly
858
+ if (repoInfo) {
859
+ if (typeof repoInfo === 'string') {
860
+ return repoInfo;
861
+ }
862
+ if (repoInfo.url) {
863
+ return repoInfo.url.replace(/^git\+/, '').replace(/\.git$/, '');
864
+ }
865
+ }
866
+ }
867
+ catch {
868
+ // Ignore errors fetching from npm
869
+ }
870
+ return null;
871
+ }
872
+ /**
873
+ * Checks whether the given extension is compatible with the currently installed server
874
+ *
875
+ */
876
+ async _checkInstallCompatibility({ installSpec, pkgName, pkgVer, installType, }) {
877
+ if (extension_config_1.INSTALL_TYPE_NPM !== installType) {
878
+ return;
879
+ }
880
+ await (0, utils_1.spinWith)(this.isJsonOutput, `Checking if '${pkgName}' is compatible`, async () => {
881
+ const [serverVersion, extVersionRequirement] = await getRemoteExtensionVersionReq(pkgName, pkgVer);
882
+ if (serverVersion && extVersionRequirement && !semver.satisfies(serverVersion, extVersionRequirement)) {
883
+ throw this._createFatalError(`'${installSpec}' cannot be installed because the server version it requires (${extVersionRequirement}) ` +
884
+ `does not meet the currently installed one (${serverVersion}). Please install ` +
885
+ `a compatible server version first.`);
886
+ }
887
+ });
888
+ }
1006
889
  }
1007
890
  exports.ExtensionCommand = ExtensionCliCommand;
1008
891
  /**
1009
892
  * This is needed to ensure proper module resolution for installed extensions,
1010
893
  * especially ESM ones.
1011
894
  *
1012
- * @param {ExtensionConfig<ExtensionType>} driverConfig
1013
- * @param {ExtensionConfig<ExtensionType>} pluginConfig
1014
- * @param {import('@appium/types').AppiumLogger} logger
895
+ * @param driverConfig - active driver extension config
896
+ * @param pluginConfig - active plugin extension config
897
+ * @param logger - logger instance used for non-fatal symlink errors
898
+ * @returns resolves when symlink injection has completed for all extensions
1015
899
  */
1016
900
  async function injectAppiumSymlinks(driverConfig, pluginConfig, logger) {
1017
- const installPaths = lodash_1.default.compact([
901
+ const isNpmInstalledExtension = (details) => details.installType === extension_config_1.INSTALL_TYPE_NPM && Boolean(details.installPath);
902
+ const installedExtensions = [
1018
903
  ...Object.values(driverConfig.installedExtensions || {}),
1019
- ...Object.values(pluginConfig.installedExtensions || {})
1020
- ].filter((details) => details.installType === extension_config_1.INSTALL_TYPE_NPM)
904
+ ...Object.values(pluginConfig.installedExtensions || {}),
905
+ ];
906
+ const installPaths = lodash_1.default.compact(installedExtensions
907
+ .filter((details) => Boolean(details))
908
+ .filter(isNpmInstalledExtension)
1021
909
  .map((details) => details.installPath));
1022
910
  // After the extension is installed, we try to inject the appium module symlink
1023
911
  // into the extension's node_modules folder if it is not there yet.
@@ -1030,12 +918,10 @@ async function injectAppiumSymlinks(driverConfig, pluginConfig, logger) {
1030
918
  * This is needed to ensure proper module resolution for installed extensions,
1031
919
  * especially ESM ones.
1032
920
  *
1033
- * @param {string} dstFolder The destination folder where the symlink should be created
1034
- * @param {import('@appium/types').AppiumLogger} logger
1035
- * @returns {Promise<void>}
921
+ * @param dstFolder The destination folder where the symlink should be created
1036
922
  */
1037
923
  async function injectAppiumSymlink(dstFolder, logger) {
1038
- let appiumModuleRoot;
924
+ let appiumModuleRoot = '';
1039
925
  try {
1040
926
  appiumModuleRoot = (0, utils_2.getAppiumModuleRoot)();
1041
927
  const symlinkPath = node_path_1.default.join(dstFolder, node_path_1.default.basename(appiumModuleRoot));
@@ -1050,159 +936,4 @@ async function injectAppiumSymlink(dstFolder, logger) {
1050
936
  }
1051
937
  }
1052
938
  exports.default = ExtensionCliCommand;
1053
- /**
1054
- * Options for the {@linkcode ExtensionCliCommand} constructor
1055
- * @template {ExtensionType} ExtType
1056
- * @typedef ExtensionCommandOptions
1057
- * @property {ExtensionConfig<ExtType>} config - the `DriverConfig` or `PluginConfig` instance used for this command
1058
- * @property {boolean} json - whether the output of this command should be JSON or text
1059
- */
1060
- /**
1061
- * Extra stuff about extensions; used indirectly by {@linkcode ExtensionCliCommand.list}.
1062
- *
1063
- * @typedef ExtensionListMetadata
1064
- * @property {boolean} installed - If `true`, the extension is installed
1065
- * @property {boolean} upToDate - If the extension is installed and the latest
1066
- * @property {string|null} updateVersion - If the extension is installed, the version it can be updated to
1067
- * @property {string|null} unsafeUpdateVersion - Same as above, but a major version bump
1068
- * @property {string} [updateError] - Update check error message (if present)
1069
- * @property {boolean} [devMode] - If Appium is run from an extension's working copy
1070
- * @property {string} [repositoryUrl] - Repository URL for the extension (if available)
1071
- */
1072
- /**
1073
- * @typedef {import('@appium/types').ExtensionType} ExtensionType
1074
- * @typedef {import('@appium/types').DriverType} DriverType
1075
- * @typedef {import('@appium/types').PluginType} PluginType
1076
- */
1077
- /**
1078
- * @template {ExtensionType} ExtType
1079
- * @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
1080
- */
1081
- /**
1082
- * @template {ExtensionType} ExtType
1083
- * @typedef {import('../extension/extension-config').ExtensionConfig<ExtType>} ExtensionConfig
1084
- */
1085
- /**
1086
- * @template {ExtensionType} ExtType
1087
- * @typedef {import('appium/types').ExtMetadata<ExtType>} ExtMetadata
1088
- */
1089
- /**
1090
- * @template {ExtensionType} ExtType
1091
- * @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
1092
- */
1093
- /**
1094
- * @template {ExtensionType} ExtType
1095
- * @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
1096
- */
1097
- /**
1098
- * @template {ExtensionType} ExtType
1099
- * @typedef {import('appium/types').ExtInstallReceipt<ExtType>} ExtInstallReceipt
1100
- */
1101
- /**
1102
- * Possible return value for {@linkcode ExtensionCliCommand.list}
1103
- * @template {ExtensionType} ExtType
1104
- * @typedef {Partial<ExtManifest<ExtType>> & Partial<ExtensionListMetadata>} ExtensionListData
1105
- */
1106
- /**
1107
- * @template {ExtensionType} ExtType
1108
- * @typedef {ExtManifest<ExtType> & ExtensionListMetadata} InstalledExtensionListData
1109
- */
1110
- /**
1111
- * Return value of {@linkcode ExtensionCliCommand.list}.
1112
- * @template {ExtensionType} ExtType
1113
- * @typedef {Record<string,ExtensionListData<ExtType>>} ExtensionList
1114
- */
1115
- /**
1116
- * Options for {@linkcode ExtensionCliCommand._run}.
1117
- * @typedef RunOptions
1118
- * @property {string} installSpec - name of the extension to run a script from
1119
- * @property {string} [scriptName] - name of the script to run. If not provided
1120
- * then all available script names will be printed
1121
- * @property {string[]} [extraArgs] - arguments to pass to the script
1122
- * @property {boolean} [bufferOutput] - if true, will buffer the output of the script and return it
1123
- */
1124
- /**
1125
- * Options for {@linkcode ExtensionCliCommand.doctor}.
1126
- * @typedef DoctorOptions
1127
- * @property {string} installSpec - name of the extension to run doctor checks for
1128
- */
1129
- /**
1130
- * Return value of {@linkcode ExtensionCliCommand._run}
1131
- *
1132
- * @typedef RunOutput
1133
- * @property {string[]} [output] - script output if `bufferOutput` was `true` in {@linkcode RunOptions}
1134
- */
1135
- /**
1136
- * Options for {@linkcode ExtensionCliCommand._update}.
1137
- * @typedef ExtensionUpdateOpts
1138
- * @property {string} installSpec - the name of the extension to update
1139
- * @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
1140
- */
1141
- /**
1142
- * Return value of {@linkcode ExtensionCliCommand._update}.
1143
- * @typedef ExtensionUpdateResult
1144
- * @property {Record<string,Error>} errors - map of ext names to error objects
1145
- * @property {Record<string,UpdateReport>} updates - map of ext names to {@linkcode UpdateReport}s
1146
- */
1147
- /**
1148
- * Part of result of {@linkcode ExtensionCliCommand._update}.
1149
- * @typedef UpdateReport
1150
- * @property {string} from - version the extension was updated from
1151
- * @property {string} to - version the extension was updated to
1152
- */
1153
- /**
1154
- * Options for {@linkcode ExtensionCliCommand._uninstall}.
1155
- * @typedef UninstallOpts
1156
- * @property {string} installSpec - the name or spec of an extension to uninstall
1157
- */
1158
- /**
1159
- * Used by {@linkcode ExtensionCliCommand.getPostInstallText}
1160
- * @typedef ExtensionArgs
1161
- * @property {string} extName - the name of an extension
1162
- * @property {object} extData - the data for an installed extension
1163
- */
1164
- /**
1165
- * Options for {@linkcode ExtensionCliCommand.installViaNpm}
1166
- * @typedef InstallViaNpmArgs
1167
- * @property {string} installSpec - the name or spec of an extension to install
1168
- * @property {string} pkgName - the NPM package name of the extension
1169
- * @property {import('appium/types').InstallType} installType - type of install
1170
- * @property {string} [pkgVer] - the specific version of the NPM package
1171
- */
1172
- /**
1173
- * Object returned by {@linkcode ExtensionCliCommand.checkForExtensionUpdate}
1174
- * @typedef PossibleUpdates
1175
- * @property {string} current - current version
1176
- * @property {string?} safeUpdate - version we can safely update to if it exists, or null
1177
- * @property {string?} unsafeUpdate - version we can unsafely update to if it exists, or null
1178
- */
1179
- /**
1180
- * Options for {@linkcode ExtensionCliCommand._install}
1181
- * @typedef InstallOpts
1182
- * @property {string} installSpec - the name or spec of an extension to install
1183
- * @property {InstallType} installType - how to install this extension. One of the INSTALL_TYPES
1184
- * @property {string} [packageName] - for git/github installs, the extension node package name
1185
- */
1186
- /**
1187
- * @template {ExtensionType} ExtType
1188
- * @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
1189
- */
1190
- /**
1191
- * @typedef ListOptions
1192
- * @property {boolean} showInstalled - whether should show only installed extensions
1193
- * @property {boolean} showUpdates - whether should show available updates
1194
- * @property {boolean} [verbose] - whether to show additional data from the extension
1195
- */
1196
- /**
1197
- * Opts for {@linkcode ExtensionCliCommand.getInstallationReceipt}
1198
- * @template {ExtensionType} ExtType
1199
- * @typedef GetInstallationReceiptOpts
1200
- * @property {string} installPath
1201
- * @property {string} installSpec
1202
- * @property {ExtPackageJson<ExtType>} pkg
1203
- * @property {InstallType} installType
1204
- */
1205
- /**
1206
- * @typedef {import('appium/types').InstallType} InstallType
1207
- */
1208
939
  //# sourceMappingURL=extension-command.js.map