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
@@ -1,6 +1,17 @@
1
1
  import B from 'bluebird';
2
2
  import _ from 'lodash';
3
3
  import path from 'node:path';
4
+ import type {AppiumLogger, ExtensionType, IDoctorCheck} from '@appium/types';
5
+ import type {
6
+ ExtInstallReceipt as AppiumExtInstallReceipt,
7
+ ExtManifest as AppiumExtManifest,
8
+ ExtMetadata as AppiumExtMetadata,
9
+ ExtPackageJson as AppiumExtPackageJson,
10
+ ExtRecord as AppiumExtRecord,
11
+ InstallType,
12
+ } from 'appium/types';
13
+ import type {PackageJson} from 'type-fest';
14
+ import type {ExtensionConfig as BaseExtensionConfig} from '../extension/extension-config';
4
15
  import {npm, util, env, console, fs, system} from '@appium/support';
5
16
  import {spinWith, RingBuffer} from './utils';
6
17
  import {
@@ -25,24 +36,73 @@ const MAX_CONCURRENT_REPO_FETCHES = 5;
25
36
  class NotUpdatableError extends Error {}
26
37
  class NoUpdatesAvailableError extends Error {}
27
38
 
39
+ /**
40
+ * Options for the {@linkcode ExtensionCliCommand} constructor
41
+ */
42
+ export type ExtensionCommandOptions<ExtType extends ExtensionType = ExtensionType> = {
43
+ config: ExtensionConfig<ExtType>;
44
+ json: boolean;
45
+ };
46
+
47
+ export type ExtensionConfig<ExtType extends ExtensionType = ExtensionType> = BaseExtensionConfig<ExtType>;
48
+ export type ExtRecord<ExtType extends ExtensionType = ExtensionType> = AppiumExtRecord<ExtType>;
49
+ export type ExtMetadata<ExtType extends ExtensionType = ExtensionType> = AppiumExtMetadata<ExtType>;
50
+ export type ExtManifest<ExtType extends ExtensionType = ExtensionType> = AppiumExtManifest<ExtType>;
51
+ export type ExtPackageJson<ExtType extends ExtensionType = ExtensionType> = AppiumExtPackageJson<ExtType>;
52
+ export type ExtInstallReceipt<ExtType extends ExtensionType = ExtensionType> =
53
+ AppiumExtInstallReceipt<ExtType>;
54
+
55
+ /**
56
+ * Extra stuff about extensions; used indirectly by {@linkcode ExtensionCliCommand.list}.
57
+ */
58
+ export type ExtensionListMetadata = {
59
+ installed: boolean;
60
+ upToDate?: boolean;
61
+ updateVersion?: string | null;
62
+ unsafeUpdateVersion?: string | null;
63
+ updateError?: string;
64
+ devMode?: boolean;
65
+ repositoryUrl?: string;
66
+ };
67
+
68
+ /**
69
+ * Possible return value for {@linkcode ExtensionCliCommand.list}
70
+ */
71
+ export type ExtensionListData<ExtType extends ExtensionType = ExtensionType> = Partial<
72
+ ExtManifest<ExtType>
73
+ > &
74
+ Partial<ExtensionListMetadata>;
75
+
76
+ export type InstalledExtensionListData<ExtType extends ExtensionType = ExtensionType> = ExtManifest<ExtType> &
77
+ ExtensionListMetadata;
78
+
79
+ /**
80
+ * Return value of {@linkcode ExtensionCliCommand.list}.
81
+ */
82
+ export type ExtensionList<ExtType extends ExtensionType = ExtensionType> = Record<
83
+ string,
84
+ ExtensionListData<ExtType>
85
+ >;
86
+
28
87
  /**
29
88
  * Omits `driverName`/`pluginName` props from the receipt to make a {@linkcode ExtManifest}
30
- * @template {ExtensionType} ExtType
31
- * @param {ExtInstallReceipt<ExtType>} receipt
32
- * @returns {ExtManifest<ExtType>}
33
89
  */
34
- function receiptToManifest(receipt) {
35
- return /** @type {ExtManifest<ExtType>} */ (_.omit(receipt, 'driverName', 'pluginName'));
90
+ function receiptToManifest<ExtType extends ExtensionType>(
91
+ receipt: ExtInstallReceipt<ExtType>
92
+ ): ExtManifest<ExtType> {
93
+ return _.omit(receipt, 'driverName', 'pluginName') as ExtManifest<ExtType>;
36
94
  }
37
95
 
38
96
  /**
39
97
  * Fetches the remote extension version requirements
40
98
  *
41
- * @param {string} pkgName Extension name
42
- * @param {string} [pkgVer] Extension version (if not provided then the latest is assumed)
43
- * @returns {Promise<[string, string|null]>}
99
+ * @param pkgName Extension name
100
+ * @param [pkgVer] Extension version (if not provided then the latest is assumed)
44
101
  */
45
- async function getRemoteExtensionVersionReq(pkgName, pkgVer) {
102
+ async function getRemoteExtensionVersionReq(
103
+ pkgName: string,
104
+ pkgVer?: string
105
+ ): Promise<[string, string | null]> {
46
106
  const allDeps = await npm.getPackageInfo(
47
107
  `${pkgName}${pkgVer ? `@${pkgVer}` : ``}`,
48
108
  ['peerDependencies', 'dependencies']
@@ -52,33 +112,29 @@ async function getRemoteExtensionVersionReq(pkgName, pkgVer) {
52
112
  return [npmPackage.version, requiredVersionPair ? requiredVersionPair[1] : null];
53
113
  }
54
114
 
55
- /**
56
- * @template {ExtensionType} ExtType
57
- */
58
- class ExtensionCliCommand {
115
+ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType> {
59
116
  /**
60
117
  * This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
61
- * @type {ExtensionConfig<ExtType>}
62
118
  */
63
- config;
119
+ protected readonly config: ExtensionConfig<ExtType>;
64
120
 
65
121
  /**
66
122
  * {@linkcode Record} of official plugins or drivers.
67
- * @type {KnownExtensions<ExtType>}
68
123
  */
69
- knownExtensions;
124
+ protected knownExtensions: Record<string, string>;
70
125
 
71
126
  /**
72
127
  * If `true`, command output has been requested as JSON.
73
- * @type {boolean}
74
128
  */
75
- isJsonOutput;
129
+ protected readonly isJsonOutput: boolean;
130
+ protected readonly log: any;
76
131
 
77
132
  /**
78
- * Build an ExtensionCommand
79
- * @param {ExtensionCommandOptions<ExtType>} opts
133
+ * Creates an extension command instance.
134
+ *
135
+ * @param opts - constructor options containing extension config and JSON mode
80
136
  */
81
- constructor({config, json}) {
137
+ constructor({config, json}: ExtensionCommandOptions<ExtType>) {
82
138
  this.config = config;
83
139
  this.log = new console.CliConsole({jsonMode: json});
84
140
  this.isJsonOutput = Boolean(json);
@@ -87,32 +143,17 @@ class ExtensionCliCommand {
87
143
  /**
88
144
  * `driver` or `plugin`, depending on the `ExtensionConfig`.
89
145
  */
90
- get type() {
146
+ get type(): ExtensionType {
91
147
  return this.config.extensionType;
92
148
  }
93
149
 
94
150
  /**
95
- * Logs a message and returns an {@linkcode Error} to throw.
96
- *
97
- * For TS to understand that a function throws an exception, it must actually throw an exception--
98
- * in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
99
- * nor is something like `@returns {never}` which does not imply a thrown exception.
100
- *
101
- * @param {string} message
102
- * @protected
103
- * @throws {Error}
104
- */
105
- _createFatalError(message) {
106
- return new Error(this.log.decorate(message, 'error'));
107
- }
108
-
109
- /**
110
- * Take a CLI parse and run an extension command based on its type
151
+ * Executes an extension subcommand from parsed CLI args.
111
152
  *
112
- * @param {object} args - a key/value object with CLI flags and values
113
- * @return {Promise<object>} the result of the specific command which is executed
153
+ * @param args - parsed CLI argument object
154
+ * @returns result of the executed extension subcommand
114
155
  */
115
- async execute(args) {
156
+ async execute(args: Record<string, any>): Promise<unknown> {
116
157
  const cmd = args[`${this.type}Command`];
117
158
  if (!_.isFunction(this[cmd])) {
118
159
  throw this._createFatalError(`Cannot handle ${this.type} command ${cmd}`);
@@ -122,13 +163,12 @@ class ExtensionCliCommand {
122
163
  }
123
164
 
124
165
  /**
125
- * List extensions
166
+ * Lists available/installed extensions and optional update metadata.
126
167
  *
127
- * @template {ExtensionType} ExtType
128
- * @param {ListOptions} opts
129
- * @return {Promise<ExtensionList<ExtType>>} map of extension names to extension data
168
+ * @param opts - list command options
169
+ * @returns map of extension names to list data
130
170
  */
131
- async list({showInstalled, showUpdates, verbose = false}) {
171
+ async list({showInstalled, showUpdates, verbose = false}: ListOptions): Promise<ExtensionList> {
132
172
  const listData = this._buildListData(showInstalled);
133
173
 
134
174
  const lsMsg =
@@ -151,294 +191,36 @@ class ExtensionCliCommand {
151
191
  }
152
192
 
153
193
  /**
154
- * Build the initial list data structure from installed and known extensions
155
- *
156
- * @template {ExtensionType} ExtType
157
- * @param {boolean} showInstalled
158
- * @returns {ExtensionList<ExtType>}
159
- * @private
160
- */
161
- _buildListData(showInstalled) {
162
- const installedNames = Object.keys(this.config.installedExtensions);
163
- const knownNames = Object.keys(this.knownExtensions);
164
- return [...installedNames, ...knownNames].reduce((acc, name) => {
165
- if (!acc[name]) {
166
- if (installedNames.includes(name)) {
167
- acc[name] = {
168
- .../** @type {Partial<ExtManifest<ExtType>>} */ (this.config.installedExtensions[name]),
169
- installed: true,
170
- };
171
- } else if (!showInstalled) {
172
- acc[name] = /** @type {ExtensionListData<ExtType>} */ ({
173
- pkgName: this.knownExtensions[name],
174
- installed: false,
175
- });
176
- }
177
- }
178
- return acc;
179
- }, /** @type {ExtensionList<ExtType>} */ ({}));
180
- }
181
-
182
- /**
183
- * Check for available updates for installed extensions
184
- *
185
- * @template {ExtensionType} ExtType
186
- * @param {ExtensionList<ExtType>} listData
187
- * @param {boolean} showUpdates
188
- * @param {string} lsMsg
189
- * @returns {Promise<void>}
190
- * @private
191
- */
192
- async _checkForUpdates(listData, showUpdates, lsMsg) {
193
- await spinWith(this.isJsonOutput, lsMsg, async () => {
194
- // We'd like to still show lsMsg even if showUpdates is false
195
- if (!showUpdates) {
196
- return;
197
- }
198
-
199
- // Filter to only extensions that need update checks (installed npm packages)
200
- const extensionsToCheck = _.toPairs(listData).filter(
201
- ([, data]) => data.installed && data.installType === INSTALL_TYPE_NPM
202
- );
203
-
204
- await B.map(
205
- extensionsToCheck,
206
- async ([ext, data]) => {
207
- try {
208
- const updates = await this.checkForExtensionUpdate(ext);
209
- data.updateVersion = updates.safeUpdate;
210
- data.unsafeUpdateVersion = updates.unsafeUpdate;
211
- data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
212
- } catch (e) {
213
- data.updateError = e.message;
214
- }
215
- },
216
- {concurrency: MAX_CONCURRENT_REPO_FETCHES}
217
- );
218
- });
219
- }
220
-
221
- /**
222
- * Add repository URLs to list data for all extensions
223
- *
224
- * @template {ExtensionType} ExtType
225
- * @param {ExtensionList<ExtType>} listData
226
- * @returns {Promise<void>}
227
- * @private
228
- */
229
- async _addRepositoryUrlsToListData(listData) {
230
- await spinWith(this.isJsonOutput, 'Fetching repository information', async () => {
231
- await B.map(
232
- _.values(listData),
233
- async (data) => {
234
- const repoUrl = await this._getRepositoryUrl(data);
235
- if (repoUrl) {
236
- data.repositoryUrl = repoUrl;
237
- }
238
- },
239
- {concurrency: MAX_CONCURRENT_REPO_FETCHES}
240
- );
241
- });
242
- }
243
-
244
- /**
245
- * Display normal formatted output
246
- *
247
- * @template {ExtensionType} ExtType
248
- * @param {ExtensionList<ExtType>} listData
249
- * @param {boolean} showUpdates
250
- * @returns {Promise<ExtensionList<ExtType>>}
251
- * @private
252
- */
253
- async _displayNormalListOutput(listData, showUpdates) {
254
- for (const [name, data] of _.toPairs(listData)) {
255
- const line = await this._formatExtensionLine(name, data, showUpdates);
256
- this.log.log(line);
257
- }
258
-
259
- return listData;
260
- }
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
- /**
283
- * Format installation status text
284
- *
285
- * @template {ExtensionType} ExtType
286
- * @param {InstalledExtensionListData<ExtType>} data
287
- * @returns {string}
288
- * @private
289
- */
290
- _formatInstallText(data) {
291
- const {installType, installSpec, version} = data;
292
- let typeTxt;
293
- switch (installType) {
294
- case INSTALL_TYPE_GIT:
295
- case INSTALL_TYPE_GITHUB:
296
- typeTxt = `(cloned from ${installSpec})`.yellow;
297
- break;
298
- case INSTALL_TYPE_LOCAL:
299
- typeTxt = `(linked from ${installSpec})`.magenta;
300
- break;
301
- case INSTALL_TYPE_DEV:
302
- typeTxt = '(dev mode)';
303
- break;
304
- default:
305
- typeTxt = '(npm)';
306
- }
307
- return `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
308
- }
309
-
310
- /**
311
- * Format update information text
312
- *
313
- * @template {ExtensionType} ExtType
314
- * @param {InstalledExtensionListData<ExtType>} data
315
- * @returns {string}
316
- * @private
317
- */
318
- _formatUpdateText(data) {
319
- const {updateVersion, unsafeUpdateVersion, upToDate, updateError} = data;
320
- if (updateError) {
321
- return ` [Cannot check for updates: ${updateError}]`.red;
322
- }
323
- let txt = '';
324
- if (updateVersion) {
325
- txt += ` [${updateVersion} available]`.magenta;
326
- }
327
- if (upToDate) {
328
- txt += ` [Up to date]`.green;
329
- }
330
- if (unsafeUpdateVersion) {
331
- txt += ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
332
- }
333
- return txt;
334
- }
335
-
336
- /**
337
- * Get repository URL from package data
338
- *
339
- * @template {ExtensionType} ExtType
340
- * @param {ExtensionListData<ExtType>} data
341
- * @returns {Promise<string|null>}
342
- * @private
343
- */
344
- async _getRepositoryUrl(data) {
345
- if (data.installed && data.installPath) {
346
- return await this._getRepositoryUrlFromInstalled(
347
- /** @type {InstalledExtensionListData<ExtType>} */ (data)
348
- );
349
- }
350
- if (data.pkgName && !data.installed) {
351
- return await this._getRepositoryUrlFromNpm(data.pkgName);
352
- }
353
- return null;
354
- }
355
-
356
- /**
357
- * Get repository URL from installed extension's package.json
194
+ * For any `package.json` fields which a particular type of extension requires, validate the
195
+ * presence and form of those fields on the `package.json` data, throwing an error if anything is
196
+ * amiss.
358
197
  *
359
- * @template {ExtensionType} ExtType
360
- * @param {InstalledExtensionListData<ExtType>} data
361
- * @returns {Promise<string|null>}
362
- * @private
198
+ * @param extMetadata - the data in the "appium" field of `package.json` for an extension
199
+ * @param installSpec - Extension name/spec
363
200
  */
364
- async _getRepositoryUrlFromInstalled(data) {
365
- try {
366
- const pkgJsonPath = path.join(data.installPath, 'package.json');
367
- if (await fs.exists(pkgJsonPath)) {
368
- const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
369
- if (pkg.repository) {
370
- if (typeof pkg.repository === 'string') {
371
- return pkg.repository;
372
- }
373
- if (pkg.repository.url) {
374
- return pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
375
- }
376
- }
377
- }
378
- } catch {
379
- // Ignore errors reading package.json
380
- }
381
- return null;
382
- }
201
+ protected abstract validateExtensionFields(
202
+ extMetadata: ExtMetadata<ExtType>,
203
+ installSpec: string
204
+ ): void;
383
205
 
384
206
  /**
385
- * Get repository URL from npm for a package name
207
+ * Logs a message and returns an {@linkcode Error} to throw.
386
208
  *
387
- * @param {string} pkgName
388
- * @returns {Promise<string|null>}
389
- * @private
390
- */
391
- async _getRepositoryUrlFromNpm(pkgName) {
392
- try {
393
- const repoInfo = await npm.getPackageInfo(pkgName, ['repository']);
394
- // When requesting only 'repository', npm.getPackageInfo returns the repository object directly
395
- if (repoInfo) {
396
- if (typeof repoInfo === 'string') {
397
- return repoInfo;
398
- }
399
- if (repoInfo.url) {
400
- return repoInfo.url.replace(/^git\+/, '').replace(/\.git$/, '');
401
- }
402
- }
403
- } catch {
404
- // Ignore errors fetching from npm
405
- }
406
- return null;
407
- }
408
-
409
- /**
410
- * Checks whether the given extension is compatible with the currently installed server
209
+ * For TS to understand that a function throws an exception, it must actually throw an exception--
210
+ * in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
211
+ * nor is something like a `never` return annotation, which does not imply a thrown exception.
411
212
  *
412
- * @param {InstallViaNpmArgs} installViaNpmOpts
413
- * @returns {Promise<void>}
213
+ * @throws {Error}
414
214
  */
415
- async _checkInstallCompatibility({installSpec, pkgName, pkgVer, installType}) {
416
- if (INSTALL_TYPE_NPM !== installType) {
417
- return;
418
- }
419
-
420
- await spinWith(this.isJsonOutput, `Checking if '${pkgName}' is compatible`, async () => {
421
- const [serverVersion, extVersionRequirement] = await getRemoteExtensionVersionReq(pkgName, pkgVer);
422
- if (serverVersion && extVersionRequirement && !semver.satisfies(serverVersion, extVersionRequirement)) {
423
- throw this._createFatalError(
424
- `'${installSpec}' cannot be installed because the server version it requires (${extVersionRequirement}) ` +
425
- `does not meet the currently installed one (${serverVersion}). Please install ` +
426
- `a compatible server version first.`
427
- );
428
- }
429
- });
215
+ protected _createFatalError(message: string): Error {
216
+ return new Error(this.log.decorate(message, 'error'));
430
217
  }
431
218
 
432
219
  /**
433
- * Install an extension
220
+ * Build the initial list data structure from installed and known extensions
434
221
  *
435
- * @param {InstallOpts} opts
436
- * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
437
222
  */
438
- async _install({installSpec, installType, packageName}) {
439
- /** @type {ExtInstallReceipt<ExtType>} */
440
- let receipt;
441
-
223
+ protected async _install({installSpec, installType, packageName}: InstallOpts): Promise<Record<string, any>> {
442
224
  if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
443
225
  throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
444
226
  }
@@ -447,16 +229,12 @@ class ExtensionCliCommand {
447
229
  throw this._createFatalError(`When using --source=${installType}, must also use --package`);
448
230
  }
449
231
 
450
- /**
451
- * @type {InstallViaNpmArgs}
452
- */
453
- let installViaNpmOpts;
232
+ let installViaNpmOpts: InstallViaNpmArgs;
454
233
 
455
234
  /**
456
235
  * The probable (?) name of the extension derived from the install spec.
457
236
  *
458
237
  * If using a local install type, this will remain empty.
459
- * @type {string}
460
238
  */
461
239
  let probableExtName = '';
462
240
 
@@ -471,9 +249,9 @@ class ExtensionCliCommand {
471
249
  installViaNpmOpts = {
472
250
  installSpec,
473
251
  installType,
474
- pkgName: /** @type {string} */ (packageName),
252
+ pkgName: packageName as string,
475
253
  };
476
- probableExtName = /** @type {string} */ (packageName);
254
+ probableExtName = packageName as string;
477
255
  } else if (installType === INSTALL_TYPE_GIT) {
478
256
  // git urls can have '.git' at the end, but this is not necessary and would complicate the
479
257
  // way we download and name directories, so we can just remove it
@@ -481,11 +259,12 @@ class ExtensionCliCommand {
481
259
  installViaNpmOpts = {
482
260
  installSpec,
483
261
  installType,
484
- pkgName: /** @type {string} */ (packageName),
262
+ pkgName: packageName as string,
485
263
  };
486
- probableExtName = /** @type {string} */ (packageName);
264
+ probableExtName = packageName as string;
487
265
  } else {
488
- let pkgName, pkgVer;
266
+ let pkgName: string;
267
+ let pkgVer: string | undefined;
489
268
  if (installType === INSTALL_TYPE_LOCAL) {
490
269
  pkgName = path.isAbsolute(installSpec) ? installSpec : path.resolve(installSpec);
491
270
  } else {
@@ -494,7 +273,7 @@ class ExtensionCliCommand {
494
273
  // extensions installed via npm can include versions or tags after the '@'
495
274
  // sign, so check for that. We also need to be careful that package names themselves can
496
275
  // contain the '@' symbol, as in `npm install @appium/fake-driver@1.2.0`
497
- let name;
276
+ let name: string;
498
277
  const splits = installSpec.split('@');
499
278
  if (installSpec.startsWith('@')) {
500
279
  // this is the case where we have an npm org included in the package name
@@ -540,12 +319,11 @@ class ExtensionCliCommand {
540
319
 
541
320
  await this._checkInstallCompatibility(installViaNpmOpts);
542
321
 
543
- receipt = await this.installViaNpm(installViaNpmOpts);
322
+ const receipt = await this.installViaNpm(installViaNpmOpts);
544
323
 
545
324
  // this _should_ be the same as `probablyExtName` as the one derived above unless
546
325
  // install type is local.
547
- /** @type {string} */
548
- const extName = receipt[/** @type {string} */ (`${this.type}Name`)];
326
+ const extName = receipt[`${this.type}Name`];
549
327
 
550
328
  // check _a second time_ with the more-accurate extName
551
329
  if (this.config.isInstalled(extName)) {
@@ -558,12 +336,11 @@ class ExtensionCliCommand {
558
336
 
559
337
  // this field does not exist as such in the manifest (it's used as a property name instead)
560
338
  // so that's why it's being removed here.
561
- /** @type {ExtManifest<ExtType>} */
562
339
  const extManifest = receiptToManifest(receipt);
563
340
 
564
341
  const [errors, warnings] = await B.all([
565
- this.config.getProblems(extName, extManifest),
566
- this.config.getWarnings(extName, extManifest),
342
+ this.config.getProblems(extName, extManifest as any),
343
+ this.config.getWarnings(extName, extManifest as any),
567
344
  ]);
568
345
  const errorMap = new Map([[extName, errors]]);
569
346
  const warningMap = new Map([[extName, warnings]]);
@@ -581,7 +358,7 @@ class ExtensionCliCommand {
581
358
  this.log.warn(warningSummaries.join('\n'));
582
359
  }
583
360
 
584
- await this.config.addExtension(extName, extManifest);
361
+ await this.config.addExtension(extName, extManifest as any);
585
362
 
586
363
  // update the hash if we've changed the local `package.json`
587
364
  if (await env.hasAppiumDependency(this.config.appiumHome)) {
@@ -589,140 +366,19 @@ class ExtensionCliCommand {
589
366
  }
590
367
 
591
368
  // log info for the user
592
- this.log.info(this.getPostInstallText({extName, extData: receipt}));
369
+ this.log.info(
370
+ this.getPostInstallText({extName, extData: receipt as unknown as ExtInstallReceipt<ExtType>})
371
+ );
593
372
 
594
373
  return this.config.installedExtensions;
595
374
  }
596
375
 
597
376
  /**
598
- * Install an extension via NPM
377
+ * Get the text which should be displayed to the user after an extension has been installed. This
378
+ * is designed to be overridden by drivers/plugins with their own particular text.
599
379
  *
600
- * @param {InstallViaNpmArgs} args
601
- * @returns {Promise<ExtInstallReceipt<ExtType>>}
602
380
  */
603
- async installViaNpm({installSpec, pkgName, pkgVer, installType}) {
604
- const installMsg = `Installing '${installSpec}'`;
605
- const validateMsg = `Validating '${installSpec}'`;
606
-
607
- // the string used for installation is either <name>@<ver> in the case of a standard NPM
608
- // package, or whatever the user sent in otherwise.
609
- const installStr = installType === INSTALL_TYPE_NPM ? `${pkgName}${pkgVer ? `@${pkgVer}` : ''}` : installSpec;
610
- const appiumHome = this.config.appiumHome;
611
- try {
612
- const {pkg, installPath} = await spinWith(
613
- this.isJsonOutput,
614
- installMsg,
615
- async () => await npm.installPackage(appiumHome, installStr, {pkgName, installType})
616
- );
617
-
618
- await spinWith(this.isJsonOutput, validateMsg, async () => {
619
- this.validatePackageJson(pkg, installSpec);
620
- });
621
-
622
- return this.getInstallationReceipt({
623
- pkg,
624
- installPath,
625
- installType,
626
- installSpec,
627
- });
628
- } catch (err) {
629
- throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
630
- }
631
- }
632
-
633
- /**
634
- * Get the text which should be displayed to the user after an extension has been installed. This
635
- * is designed to be overridden by drivers/plugins with their own particular text.
636
- *
637
- * @param {ExtensionArgs} args
638
- * @returns {string}
639
- */
640
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
641
- getPostInstallText(args) {
642
- throw this._createFatalError('Must be implemented in final class');
643
- }
644
-
645
- /**
646
- * Once a package is installed on-disk, this gathers some necessary metadata for validation.
647
- *
648
- * @param {GetInstallationReceiptOpts<ExtType>} opts
649
- * @returns {ExtInstallReceipt<ExtType>}
650
- */
651
- getInstallationReceipt({pkg, installPath, installType, installSpec}) {
652
- const {appium, name, version, peerDependencies} = pkg;
653
-
654
- const strVersion = /** @type {string} */ (version);
655
- /** @type {import('appium/types').InternalMetadata} */
656
- const internal = {
657
- pkgName: /** @type {string} */ (name),
658
- version: strVersion,
659
- installType,
660
- installSpec,
661
- installPath,
662
- appiumVersion: peerDependencies?.appium,
663
- };
664
-
665
- /** @type {ExtMetadata<ExtType>} */
666
- const extMetadata = appium;
667
-
668
- return {
669
- ...internal,
670
- ...extMetadata,
671
- };
672
- }
673
-
674
- /**
675
- * Validates the _required_ root fields of an extension's `package.json` file.
676
- *
677
- * These required fields are:
678
- * - `name`
679
- * - `version`
680
- * - `appium`
681
- * @param {import('type-fest').PackageJson} pkg - `package.json` of extension
682
- * @param {string} installSpec - Extension name/spec
683
- * @throws {ReferenceError} If `package.json` has a missing or invalid field
684
- * @returns {pkg is ExtPackageJson<ExtType>}
685
- */
686
- validatePackageJson(pkg, installSpec) {
687
- const {appium, name, version} = /** @type {ExtPackageJson<ExtType>} */ (pkg);
688
-
689
- /**
690
- *
691
- * @param {string} field
692
- * @returns {ReferenceError}
693
- */
694
- const createMissingFieldError = (field) =>
695
- new ReferenceError(
696
- `${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``
697
- );
698
-
699
- if (!name) {
700
- throw createMissingFieldError('name');
701
- }
702
- if (!version) {
703
- throw createMissingFieldError('version');
704
- }
705
- if (!appium) {
706
- throw createMissingFieldError('appium');
707
- }
708
-
709
- this.validateExtensionFields(appium, installSpec);
710
-
711
- return true;
712
- }
713
-
714
- /**
715
- * For any `package.json` fields which a particular type of extension requires, validate the
716
- * presence and form of those fields on the `package.json` data, throwing an error if anything is
717
- * amiss.
718
- *
719
- * @param {ExtMetadata<ExtType>} extMetadata - the data in the "appium" field of `package.json` for an extension
720
- * @param {string} installSpec - Extension name/spec
721
- */
722
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
723
- validateExtensionFields(extMetadata, installSpec) {
724
- throw this._createFatalError('Must be implemented in final class');
725
- }
381
+ protected abstract getPostInstallText(args: ExtensionArgs<ExtType>): PostInstallText;
726
382
 
727
383
  /**
728
384
  * Uninstall an extension.
@@ -731,10 +387,9 @@ class ExtensionCliCommand {
731
387
  *
732
388
  * Will only remove the extension from the manifest if it has been successfully removed.
733
389
  *
734
- * @param {UninstallOpts} opts
735
- * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data (without the extension just uninstalled)
390
+ * @return map of all installed extension names to extension data (without the extension just uninstalled)
736
391
  */
737
- async _uninstall({installSpec}) {
392
+ protected async _uninstall({installSpec}: UninstallOpts): Promise<Record<string, any>> {
738
393
  if (!this.config.isInstalled(installSpec)) {
739
394
  throw this._createFatalError(
740
395
  `Can't uninstall ${this.type} '${installSpec}'; it is not installed`
@@ -757,10 +412,8 @@ class ExtensionCliCommand {
757
412
  /**
758
413
  * Attempt to update one or more drivers using NPM
759
414
  *
760
- * @param {ExtensionUpdateOpts} updateSpec
761
- * @return {Promise<ExtensionUpdateResult>}
762
415
  */
763
- async _update({installSpec, unsafe}) {
416
+ protected async _update({installSpec, unsafe}: ExtensionUpdateOpts): Promise<ExtensionUpdateResult> {
764
417
  const shouldUpdateAll = installSpec === UPDATE_ALL;
765
418
  // if we're specifically requesting an update for an extension, make sure it's installed
766
419
  if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
@@ -773,13 +426,11 @@ class ExtensionCliCommand {
773
426
  : [installSpec];
774
427
 
775
428
  // 'errors' will have ext names as keys and error objects as values
776
- /** @type {Record<string,Error>} */
777
- const errors = {};
429
+ const errors: Record<string, Error> = {};
778
430
 
779
431
  // 'updates' will have ext names as keys and update objects as values, where an update
780
432
  // object is of the form {from: versionString, to: versionString}
781
- /** @type {Record<string,UpdateReport>} */
782
- const updates = {};
433
+ const updates: Record<string, UpdateReport> = {};
783
434
 
784
435
  for (const e of extsToUpdate) {
785
436
  try {
@@ -807,6 +458,9 @@ class ExtensionCliCommand {
807
458
  );
808
459
  }
809
460
  const updateVer = unsafe && update.unsafeUpdate ? update.unsafeUpdate : update.safeUpdate;
461
+ if (!updateVer) {
462
+ throw new NoUpdatesAvailableError();
463
+ }
810
464
  await spinWith(
811
465
  this.isJsonOutput,
812
466
  `Updating ${this.type} '${e}' from ${update.current} to ${updateVer}`,
@@ -850,15 +504,13 @@ class ExtensionCliCommand {
850
504
  * Given an extension name, figure out what its highest possible version upgrade is, and also the
851
505
  * highest possible safe upgrade.
852
506
  *
853
- * @param {string} ext - name of extension
854
- * @return {Promise<PossibleUpdates>}
507
+ * @param ext - name of extension
855
508
  */
856
- async checkForExtensionUpdate(ext) {
509
+ protected async checkForExtensionUpdate(ext: string): Promise<PossibleUpdates> {
857
510
  // TODO decide how we want to handle beta versions?
858
511
  // this is a helper method, 'ext' is assumed to already be installed here, and of the npm
859
512
  // install type
860
513
  const {version, pkgName} = this.config.installedExtensions[ext];
861
- /** @type {string?} */
862
514
  let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
863
515
  let safeUpdate = await npm.getLatestSafeUpgradeVersion(
864
516
  this.config.appiumHome,
@@ -881,53 +533,14 @@ class ExtensionCliCommand {
881
533
  return {current: version, safeUpdate, unsafeUpdate};
882
534
  }
883
535
 
884
- /**
885
- * Actually update an extension installed by NPM, using the NPM cli. And update the installation
886
- * manifest.
887
- *
888
- * @param {string} installSpec - name of extension to update
889
- * @param {string} version - version string identifier to update extension to
890
- * @returns {Promise<void>}
891
- */
892
- async updateExtension(installSpec, version) {
893
- const {pkgName, installType} = this.config.installedExtensions[installSpec];
894
- const extData = await this.installViaNpm({
895
- installSpec,
896
- installType,
897
- pkgName,
898
- pkgVer: version,
899
- });
900
-
901
- delete extData[/** @type {string} */ (`${this.type}Name`)];
902
- await this.config.updateExtension(installSpec, extData);
903
- }
904
-
905
- /**
906
- * Just wraps {@linkcode child_process.spawn} with some default options
907
- *
908
- * @param {string} cwd - CWD
909
- * @param {string} script - Path to script
910
- * @param {string[]} args - Extra args for script
911
- * @param {import('child_process').SpawnOptions} opts - Options
912
- * @returns {import('node:child_process').ChildProcess}
913
- */
914
- _runUnbuffered(cwd, script, args = [], opts = {}) {
915
- return spawn(process.execPath, [script, ...args], {
916
- cwd,
917
- stdio: 'inherit',
918
- ...opts,
919
- });
920
- }
921
-
922
536
  /**
923
537
  * Runs doctor checks for the given extension.
924
538
  *
925
- * @param {DoctorOptions} opts
926
- * @returns {Promise<number>} The amount of Doctor checks that were
539
+ * @returns The amount of Doctor checks that were
927
540
  * successfully loaded and executed for the given extension
928
541
  * @throws {Error} If any of the mandatory Doctor checks fails.
929
542
  */
930
- async _doctor({installSpec}) {
543
+ protected async _doctor({installSpec}: DoctorOptions): Promise<number> {
931
544
  if (!this.config.isInstalled(installSpec)) {
932
545
  throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
933
546
  }
@@ -939,7 +552,7 @@ class ExtensionCliCommand {
939
552
  `No package.json could be found for "${installSpec}" ${this.type}`
940
553
  );
941
554
  }
942
- let doctorSpec;
555
+ let doctorSpec: {checks: string[]} | undefined;
943
556
  try {
944
557
  doctorSpec = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')).appium?.doctor;
945
558
  } catch (e) {
@@ -957,7 +570,8 @@ class ExtensionCliCommand {
957
570
  `containing the 'checks' key with the array of script paths`
958
571
  );
959
572
  }
960
- const paths = doctorSpec.checks.map((/** @type {string} */ p) => {
573
+ const paths: string[] = doctorSpec.checks
574
+ .map((p) => {
961
575
  const scriptPath = path.resolve(moduleRoot, p);
962
576
  if (!path.normalize(scriptPath).startsWith(path.normalize(moduleRoot))) {
963
577
  this.log.error(
@@ -967,9 +581,9 @@ class ExtensionCliCommand {
967
581
  return null;
968
582
  }
969
583
  return scriptPath;
970
- }).filter(Boolean);
971
- /** @type {Promise[]} */
972
- const loadChecksPromises = [];
584
+ })
585
+ .filter((p): p is string => Boolean(p));
586
+ const loadChecksPromises: Promise<unknown>[] = [];
973
587
  for (const p of paths) {
974
588
  const promise = (async () => {
975
589
  // https://github.com/nodejs/node/issues/31710
@@ -982,12 +596,11 @@ class ExtensionCliCommand {
982
596
  })();
983
597
  loadChecksPromises.push(promise);
984
598
  }
985
- const isDoctorCheck = (/** @type {any} */ x) =>
599
+ const isDoctorCheck = (x) =>
986
600
  ['diagnose', 'fix', 'hasAutofix', 'isOptional'].every((method) => _.isFunction(x?.[method]));
987
- /** @type {import('@appium/types').IDoctorCheck[]} */
988
- const checks = _.flatMap((await B.all(loadChecksPromises)).filter(Boolean).map(_.toPairs))
601
+ const checks: IDoctorCheck[] = _.flatMap((await B.all(loadChecksPromises)).filter(Boolean).map(_.toPairs))
989
602
  .map(([, value]) => value)
990
- .filter(isDoctorCheck);
603
+ .filter(isDoctorCheck) as IDoctorCheck[];
991
604
  if (_.isEmpty(checks)) {
992
605
  this.log.info(`The ${this.type} "${installSpec}" exports no valid doctor checks`);
993
606
  return 0;
@@ -1011,10 +624,13 @@ class ExtensionCliCommand {
1011
624
  * `scripts` field is not a plain object, or if the `scriptName` is
1012
625
  * not found within `scripts` object.
1013
626
  *
1014
- * @param {RunOptions} opts
1015
- * @return {Promise<RunOutput>}
1016
627
  */
1017
- async _run({installSpec, scriptName, extraArgs = [], bufferOutput = false}) {
628
+ protected async _run({
629
+ installSpec,
630
+ scriptName,
631
+ extraArgs = [],
632
+ bufferOutput = false,
633
+ }: RunOptions): Promise<RunOutput> {
1018
634
  if (!this.config.isInstalled(installSpec)) {
1019
635
  throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
1020
636
  }
@@ -1038,7 +654,7 @@ class ExtensionCliCommand {
1038
654
  }
1039
655
 
1040
656
  if (!scriptName) {
1041
- const allScripts = _.toPairs(extScripts);
657
+ const allScripts = _.toPairs(extScripts as Record<string, string>);
1042
658
  const root = this.config.getInstallPath(installSpec);
1043
659
  const existingScripts = await B.filter(
1044
660
  allScripts,
@@ -1055,7 +671,7 @@ class ExtensionCliCommand {
1055
671
  return {};
1056
672
  }
1057
673
 
1058
- if (!(scriptName in /** @type {Record<string,string>} */ (extScripts))) {
674
+ if (!(scriptName in extScripts)) {
1059
675
  throw this._createFatalError(
1060
676
  `The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
1061
677
  );
@@ -1118,22 +734,426 @@ class ExtensionCliCommand {
1118
734
  throw this._createFatalError(message);
1119
735
  }
1120
736
  }
737
+
738
+ private _buildListData(showInstalled: boolean): ExtensionList {
739
+ const installedNames = Object.keys(this.config.installedExtensions);
740
+ const knownNames = Object.keys(this.knownExtensions);
741
+ return [...installedNames, ...knownNames].reduce((acc, name) => {
742
+ if (!acc[name]) {
743
+ if (installedNames.includes(name)) {
744
+ acc[name] = {
745
+ ...this.config.installedExtensions[name],
746
+ installed: true,
747
+ };
748
+ } else if (!showInstalled) {
749
+ acc[name] = {
750
+ pkgName: this.knownExtensions[name],
751
+ installed: false,
752
+ };
753
+ }
754
+ }
755
+ return acc;
756
+ }, {});
757
+ }
758
+
759
+ /**
760
+ * Install an extension via NPM
761
+ *
762
+ */
763
+ private async installViaNpm({
764
+ installSpec,
765
+ pkgName,
766
+ pkgVer,
767
+ installType,
768
+ }: InstallViaNpmArgs): Promise<ExtInstallReceipt<ExtType>> {
769
+ const installMsg = `Installing '${installSpec}'`;
770
+ const validateMsg = `Validating '${installSpec}'`;
771
+
772
+ // the string used for installation is either <name>@<ver> in the case of a standard NPM
773
+ // package, or whatever the user sent in otherwise.
774
+ const installStr = installType === INSTALL_TYPE_NPM ? `${pkgName}${pkgVer ? `@${pkgVer}` : ''}` : installSpec;
775
+ const appiumHome = this.config.appiumHome;
776
+ try {
777
+ const {pkg, installPath} = await spinWith(
778
+ this.isJsonOutput,
779
+ installMsg,
780
+ async () => await npm.installPackage(appiumHome, installStr, {pkgName, installType})
781
+ );
782
+
783
+ const validatedPkg = await spinWith(this.isJsonOutput, validateMsg, async () =>
784
+ this.validatePackageJson(pkg, installSpec)
785
+ );
786
+
787
+ return this.getInstallationReceipt({
788
+ pkg: validatedPkg,
789
+ installPath,
790
+ installType,
791
+ installSpec,
792
+ });
793
+ } catch (err) {
794
+ throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
795
+ }
796
+ }
797
+
798
+
799
+ /**
800
+ * Actually update an extension installed by NPM, using the NPM cli. And update the installation
801
+ * manifest.
802
+ *
803
+ * @param installSpec - name of extension to update
804
+ * @param version - version string identifier to update extension to
805
+ */
806
+ private async updateExtension(installSpec: string, version: string): Promise<void> {
807
+ const {pkgName, installType} = this.config.installedExtensions[installSpec];
808
+ const extData = await this.installViaNpm({
809
+ installSpec,
810
+ installType,
811
+ pkgName,
812
+ pkgVer: version,
813
+ });
814
+
815
+ delete extData[`${this.type}Name`];
816
+ await this.config.updateExtension(installSpec, extData as any);
817
+ }
818
+
819
+ /**
820
+ * Just wraps {@linkcode child_process.spawn} with some default options
821
+ *
822
+ * @param cwd - CWD
823
+ * @param script - Path to script
824
+ * @param args - Extra args for script
825
+ * @param opts - Options
826
+ */
827
+ private _runUnbuffered(
828
+ cwd: string,
829
+ script: string,
830
+ args: string[] = [],
831
+ opts: Record<string, any> = {}
832
+ ) {
833
+ return spawn(process.execPath, [script, ...args], {
834
+ cwd,
835
+ stdio: 'inherit',
836
+ ...opts,
837
+ });
838
+ }
839
+
840
+ /**
841
+ * Once a package is installed on-disk, this gathers some necessary metadata for validation.
842
+ *
843
+ */
844
+ private getInstallationReceipt({
845
+ pkg,
846
+ installPath,
847
+ installType,
848
+ installSpec,
849
+ }: GetInstallationReceiptOpts<ExtType>): ExtInstallReceipt<ExtType> {
850
+ const {appium, name, version, peerDependencies} = pkg;
851
+
852
+ const strVersion = version;
853
+ const internal = {
854
+ pkgName: name,
855
+ version: strVersion,
856
+ installType,
857
+ installSpec,
858
+ installPath,
859
+ appiumVersion: peerDependencies?.appium,
860
+ };
861
+
862
+ const extMetadata = appium;
863
+
864
+ return {
865
+ ...internal,
866
+ ...extMetadata,
867
+ } as unknown as ExtInstallReceipt<ExtType>;
868
+ }
869
+
870
+ /**
871
+ * Validates the _required_ root fields of an extension's `package.json` file.
872
+ *
873
+ * These required fields are:
874
+ * - `name`
875
+ * - `version`
876
+ * - `appium`
877
+ * @param pkg - `package.json` of extension
878
+ * @param installSpec - Extension name/spec
879
+ * @throws {ReferenceError} If `package.json` has a missing or invalid field
880
+ */
881
+ private validatePackageJson(pkg: PackageJson, installSpec: string): ExtPackageJson<ExtType> {
882
+ const {appium, name, version} = pkg;
883
+
884
+ const createMissingFieldError = (field: string): ReferenceError =>
885
+ new ReferenceError(
886
+ `${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``
887
+ );
888
+
889
+ if (!name) {
890
+ throw createMissingFieldError('name');
891
+ }
892
+ if (!version) {
893
+ throw createMissingFieldError('version');
894
+ }
895
+ if (!appium) {
896
+ throw createMissingFieldError('appium');
897
+ }
898
+
899
+ this.validateExtensionFields(appium as unknown as ExtMetadata<ExtType>, installSpec);
900
+
901
+ return pkg as unknown as ExtPackageJson<ExtType>;
902
+ }
903
+
904
+ /**
905
+ * Check for available updates for installed extensions
906
+ *
907
+ */
908
+ private async _checkForUpdates(
909
+ listData: ExtensionList,
910
+ showUpdates: boolean,
911
+ lsMsg: string
912
+ ): Promise<void> {
913
+ await spinWith(this.isJsonOutput, lsMsg, async () => {
914
+ // We'd like to still show lsMsg even if showUpdates is false
915
+ if (!showUpdates) {
916
+ return;
917
+ }
918
+
919
+ // Filter to only extensions that need update checks (installed npm packages)
920
+ const extensionsToCheck = _.toPairs(listData as Record<string, any>).filter(
921
+ ([, data]) => data.installed && data.installType === INSTALL_TYPE_NPM
922
+ );
923
+
924
+ await B.map(
925
+ extensionsToCheck,
926
+ async ([ext, data]) => {
927
+ try {
928
+ const updates = await this.checkForExtensionUpdate(ext);
929
+ data.updateVersion = updates.safeUpdate;
930
+ data.unsafeUpdateVersion = updates.unsafeUpdate;
931
+ data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
932
+ } catch (e) {
933
+ data.updateError = (e as Error).message;
934
+ }
935
+ },
936
+ {concurrency: MAX_CONCURRENT_REPO_FETCHES}
937
+ );
938
+ });
939
+ }
940
+
941
+ /**
942
+ * Add repository URLs to list data for all extensions
943
+ *
944
+ */
945
+ private async _addRepositoryUrlsToListData(listData: ExtensionList): Promise<void> {
946
+ await spinWith(this.isJsonOutput, 'Fetching repository information', async () => {
947
+ await B.map(
948
+ _.values(listData),
949
+ async (data) => {
950
+ const repoUrl = await this._getRepositoryUrl(data);
951
+ if (repoUrl) {
952
+ data.repositoryUrl = repoUrl;
953
+ }
954
+ },
955
+ {concurrency: MAX_CONCURRENT_REPO_FETCHES}
956
+ );
957
+ });
958
+ }
959
+
960
+ /**
961
+ * Display normal formatted output
962
+ *
963
+ */
964
+ private async _displayNormalListOutput(
965
+ listData: ExtensionList,
966
+ showUpdates: boolean
967
+ ): Promise<ExtensionList> {
968
+ for (const [name, data] of _.toPairs(listData)) {
969
+ const line = await this._formatExtensionLine(name, data, showUpdates);
970
+ this.log.log(line);
971
+ }
972
+
973
+ return listData;
974
+ }
975
+
976
+ /**
977
+ * Format a single extension line for display
978
+ *
979
+ */
980
+ private async _formatExtensionLine(
981
+ name: string,
982
+ data: ExtensionListData,
983
+ showUpdates: boolean
984
+ ): Promise<string> {
985
+ if (data.installed) {
986
+ const installTxt = this._formatInstallText(data);
987
+ const updateTxt = showUpdates ? this._formatUpdateText(data) : '';
988
+ return `- ${name.yellow}${installTxt}${updateTxt}`;
989
+ }
990
+ const installTxt = ' [not installed]'.grey;
991
+ return `- ${name.yellow}${installTxt}`;
992
+ }
993
+
994
+ /**
995
+ * Format installation status text
996
+ *
997
+ */
998
+ private _formatInstallText(data: ExtensionListData): string {
999
+ const {installType, installSpec, version} = data;
1000
+ let typeTxt;
1001
+ switch (installType) {
1002
+ case INSTALL_TYPE_GIT:
1003
+ case INSTALL_TYPE_GITHUB:
1004
+ typeTxt = `(cloned from ${installSpec})`.yellow;
1005
+ break;
1006
+ case INSTALL_TYPE_LOCAL:
1007
+ typeTxt = `(linked from ${installSpec})`.magenta;
1008
+ break;
1009
+ case INSTALL_TYPE_DEV:
1010
+ typeTxt = '(dev mode)';
1011
+ break;
1012
+ default:
1013
+ typeTxt = '(npm)';
1014
+ }
1015
+ return `@${String(version).yellow} ${('[installed ' + typeTxt + ']').green}`;
1016
+ }
1017
+
1018
+ /**
1019
+ * Format update information text
1020
+ *
1021
+ */
1022
+ private _formatUpdateText(data: ExtensionListData): string {
1023
+ const {updateVersion, unsafeUpdateVersion, upToDate, updateError} = data;
1024
+ if (updateError) {
1025
+ return ` [Cannot check for updates: ${updateError}]`.red;
1026
+ }
1027
+ let txt = '';
1028
+ if (updateVersion) {
1029
+ txt += ` [${updateVersion} available]`.magenta;
1030
+ }
1031
+ if (upToDate) {
1032
+ txt += ` [Up to date]`.green;
1033
+ }
1034
+ if (unsafeUpdateVersion) {
1035
+ txt += ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
1036
+ }
1037
+ return txt;
1038
+ }
1039
+
1040
+ /**
1041
+ * Get repository URL from package data
1042
+ *
1043
+ */
1044
+ private async _getRepositoryUrl(data: ExtensionListData): Promise<string | null> {
1045
+ if (data.installed && data.installPath) {
1046
+ return await this._getRepositoryUrlFromInstalled(
1047
+ data
1048
+ );
1049
+ }
1050
+ if (data.pkgName && !data.installed) {
1051
+ return await this._getRepositoryUrlFromNpm(data.pkgName);
1052
+ }
1053
+ return null;
1054
+ }
1055
+
1056
+ /**
1057
+ * Get repository URL from installed extension's package.json
1058
+ *
1059
+ */
1060
+ private async _getRepositoryUrlFromInstalled(data: ExtensionListData): Promise<string | null> {
1061
+ try {
1062
+ const pkgJsonPath = path.join(String(data.installPath), 'package.json');
1063
+ if (await fs.exists(pkgJsonPath)) {
1064
+ const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
1065
+ if (pkg.repository) {
1066
+ if (typeof pkg.repository === 'string') {
1067
+ return pkg.repository;
1068
+ }
1069
+ if (pkg.repository.url) {
1070
+ return pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
1071
+ }
1072
+ }
1073
+ }
1074
+ } catch {
1075
+ // Ignore errors reading package.json
1076
+ }
1077
+ return null;
1078
+ }
1079
+
1080
+ /**
1081
+ * Get repository URL from npm for a package name
1082
+ *
1083
+ */
1084
+ private async _getRepositoryUrlFromNpm(pkgName: string): Promise<string | null> {
1085
+ try {
1086
+ const repoInfo = await npm.getPackageInfo(pkgName, ['repository']);
1087
+ // When requesting only 'repository', npm.getPackageInfo returns the repository object directly
1088
+ if (repoInfo) {
1089
+ if (typeof repoInfo === 'string') {
1090
+ return repoInfo;
1091
+ }
1092
+ if (repoInfo.url) {
1093
+ return repoInfo.url.replace(/^git\+/, '').replace(/\.git$/, '');
1094
+ }
1095
+ }
1096
+ } catch {
1097
+ // Ignore errors fetching from npm
1098
+ }
1099
+ return null;
1100
+ }
1101
+
1102
+ /**
1103
+ * Checks whether the given extension is compatible with the currently installed server
1104
+ *
1105
+ */
1106
+ private async _checkInstallCompatibility({
1107
+ installSpec,
1108
+ pkgName,
1109
+ pkgVer,
1110
+ installType,
1111
+ }: InstallViaNpmArgs): Promise<void> {
1112
+ if (INSTALL_TYPE_NPM !== installType) {
1113
+ return;
1114
+ }
1115
+
1116
+ await spinWith(this.isJsonOutput, `Checking if '${pkgName}' is compatible`, async () => {
1117
+ const [serverVersion, extVersionRequirement] = await getRemoteExtensionVersionReq(pkgName, pkgVer);
1118
+ if (serverVersion && extVersionRequirement && !semver.satisfies(serverVersion, extVersionRequirement)) {
1119
+ throw this._createFatalError(
1120
+ `'${installSpec}' cannot be installed because the server version it requires (${extVersionRequirement}) ` +
1121
+ `does not meet the currently installed one (${serverVersion}). Please install ` +
1122
+ `a compatible server version first.`
1123
+ );
1124
+ }
1125
+ });
1126
+ }
1121
1127
  }
1122
1128
 
1123
1129
  /**
1124
1130
  * This is needed to ensure proper module resolution for installed extensions,
1125
1131
  * especially ESM ones.
1126
1132
  *
1127
- * @param {ExtensionConfig<ExtensionType>} driverConfig
1128
- * @param {ExtensionConfig<ExtensionType>} pluginConfig
1129
- * @param {import('@appium/types').AppiumLogger} logger
1133
+ * @param driverConfig - active driver extension config
1134
+ * @param pluginConfig - active plugin extension config
1135
+ * @param logger - logger instance used for non-fatal symlink errors
1136
+ * @returns resolves when symlink injection has completed for all extensions
1130
1137
  */
1131
- export async function injectAppiumSymlinks(driverConfig, pluginConfig, logger) {
1132
- const installPaths = _.compact([
1138
+ export async function injectAppiumSymlinks(
1139
+ driverConfig: ExtensionConfig<any>,
1140
+ pluginConfig: ExtensionConfig<any>,
1141
+ logger: AppiumLogger
1142
+ ): Promise<void> {
1143
+ const isNpmInstalledExtension = (
1144
+ details: InstalledExtensionLike
1145
+ ): details is InstalledExtensionLike & {installType: typeof INSTALL_TYPE_NPM; installPath: string} =>
1146
+ details.installType === INSTALL_TYPE_NPM && Boolean(details.installPath);
1147
+
1148
+ const installedExtensions = [
1133
1149
  ...Object.values(driverConfig.installedExtensions || {}),
1134
- ...Object.values(pluginConfig.installedExtensions || {})
1135
- ].filter((details) => details.installType === INSTALL_TYPE_NPM)
1136
- .map((details) => details.installPath));
1150
+ ...Object.values(pluginConfig.installedExtensions || {}),
1151
+ ] as InstalledExtensionLike[];
1152
+
1153
+ const installPaths = _.compact(installedExtensions
1154
+ .filter((details): details is InstalledExtensionLike => Boolean(details))
1155
+ .filter(isNpmInstalledExtension)
1156
+ .map((details) => details.installPath));
1137
1157
  // After the extension is installed, we try to inject the appium module symlink
1138
1158
  // into the extension's node_modules folder if it is not there yet.
1139
1159
  // We also inject the symlink into other installed extensions' node_modules folders
@@ -1148,12 +1168,10 @@ export async function injectAppiumSymlinks(driverConfig, pluginConfig, logger) {
1148
1168
  * This is needed to ensure proper module resolution for installed extensions,
1149
1169
  * especially ESM ones.
1150
1170
  *
1151
- * @param {string} dstFolder The destination folder where the symlink should be created
1152
- * @param {import('@appium/types').AppiumLogger} logger
1153
- * @returns {Promise<void>}
1171
+ * @param dstFolder The destination folder where the symlink should be created
1154
1172
  */
1155
- async function injectAppiumSymlink(dstFolder, logger) {
1156
- let appiumModuleRoot;
1173
+ async function injectAppiumSymlink(dstFolder: string, logger: AppiumLogger): Promise<void> {
1174
+ let appiumModuleRoot = '';
1157
1175
  try {
1158
1176
  appiumModuleRoot = getAppiumModuleRoot();
1159
1177
  const symlinkPath = path.join(dstFolder, path.basename(appiumModuleRoot));
@@ -1169,187 +1187,90 @@ async function injectAppiumSymlink(dstFolder, logger) {
1169
1187
  }
1170
1188
  }
1171
1189
 
1172
- export default ExtensionCliCommand;
1173
- export {ExtensionCliCommand as ExtensionCommand};
1174
-
1175
- /**
1176
- * Options for the {@linkcode ExtensionCliCommand} constructor
1177
- * @template {ExtensionType} ExtType
1178
- * @typedef ExtensionCommandOptions
1179
- * @property {ExtensionConfig<ExtType>} config - the `DriverConfig` or `PluginConfig` instance used for this command
1180
- * @property {boolean} json - whether the output of this command should be JSON or text
1181
- */
1182
-
1183
- /**
1184
- * Extra stuff about extensions; used indirectly by {@linkcode ExtensionCliCommand.list}.
1185
- *
1186
- * @typedef ExtensionListMetadata
1187
- * @property {boolean} installed - If `true`, the extension is installed
1188
- * @property {boolean} upToDate - If the extension is installed and the latest
1189
- * @property {string|null} updateVersion - If the extension is installed, the version it can be updated to
1190
- * @property {string|null} unsafeUpdateVersion - Same as above, but a major version bump
1191
- * @property {string} [updateError] - Update check error message (if present)
1192
- * @property {boolean} [devMode] - If Appium is run from an extension's working copy
1193
- * @property {string} [repositoryUrl] - Repository URL for the extension (if available)
1194
- */
1195
-
1196
- /**
1197
- * @typedef {import('@appium/types').ExtensionType} ExtensionType
1198
- * @typedef {import('@appium/types').DriverType} DriverType
1199
- * @typedef {import('@appium/types').PluginType} PluginType
1200
- */
1201
-
1202
- /**
1203
- * @template {ExtensionType} ExtType
1204
- * @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
1205
- */
1206
-
1207
- /**
1208
- * @template {ExtensionType} ExtType
1209
- * @typedef {import('../extension/extension-config').ExtensionConfig<ExtType>} ExtensionConfig
1210
- */
1211
-
1212
- /**
1213
- * @template {ExtensionType} ExtType
1214
- * @typedef {import('appium/types').ExtMetadata<ExtType>} ExtMetadata
1215
- */
1216
-
1217
- /**
1218
- * @template {ExtensionType} ExtType
1219
- * @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
1220
- */
1221
-
1222
- /**
1223
- * @template {ExtensionType} ExtType
1224
- * @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
1225
- */
1226
-
1227
- /**
1228
- * @template {ExtensionType} ExtType
1229
- * @typedef {import('appium/types').ExtInstallReceipt<ExtType>} ExtInstallReceipt
1230
- */
1231
-
1232
- /**
1233
- * Possible return value for {@linkcode ExtensionCliCommand.list}
1234
- * @template {ExtensionType} ExtType
1235
- * @typedef {Partial<ExtManifest<ExtType>> & Partial<ExtensionListMetadata>} ExtensionListData
1236
- */
1237
-
1238
- /**
1239
- * @template {ExtensionType} ExtType
1240
- * @typedef {ExtManifest<ExtType> & ExtensionListMetadata} InstalledExtensionListData
1241
- */
1242
-
1243
- /**
1244
- * Return value of {@linkcode ExtensionCliCommand.list}.
1245
- * @template {ExtensionType} ExtType
1246
- * @typedef {Record<string,ExtensionListData<ExtType>>} ExtensionList
1247
- */
1248
-
1249
1190
  /**
1250
1191
  * Options for {@linkcode ExtensionCliCommand._run}.
1251
- * @typedef RunOptions
1252
- * @property {string} installSpec - name of the extension to run a script from
1253
- * @property {string} [scriptName] - name of the script to run. If not provided
1254
- * then all available script names will be printed
1255
- * @property {string[]} [extraArgs] - arguments to pass to the script
1256
- * @property {boolean} [bufferOutput] - if true, will buffer the output of the script and return it
1257
1192
  */
1193
+ type RunOptions = {
1194
+ installSpec: string;
1195
+ scriptName?: string;
1196
+ extraArgs?: string[];
1197
+ bufferOutput?: boolean;
1198
+ };
1258
1199
 
1259
1200
  /**
1260
1201
  * Options for {@linkcode ExtensionCliCommand.doctor}.
1261
- * @typedef DoctorOptions
1262
- * @property {string} installSpec - name of the extension to run doctor checks for
1263
1202
  */
1203
+ type DoctorOptions = {installSpec: string};
1264
1204
 
1265
1205
  /**
1266
1206
  * Return value of {@linkcode ExtensionCliCommand._run}
1267
- *
1268
- * @typedef RunOutput
1269
- * @property {string[]} [output] - script output if `bufferOutput` was `true` in {@linkcode RunOptions}
1270
1207
  */
1208
+ export type RunOutput = {output?: string[]};
1271
1209
 
1272
1210
  /**
1273
- * Options for {@linkcode ExtensionCliCommand._update}.
1274
- * @typedef ExtensionUpdateOpts
1275
- * @property {string} installSpec - the name of the extension to update
1276
- * @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
1211
+ * Return type of {@linkcode ExtensionCliCommand.getPostInstallText}.
1277
1212
  */
1213
+ export type PostInstallText = string;
1278
1214
 
1279
1215
  /**
1280
- * Return value of {@linkcode ExtensionCliCommand._update}.
1281
- * @typedef ExtensionUpdateResult
1282
- * @property {Record<string,Error>} errors - map of ext names to error objects
1283
- * @property {Record<string,UpdateReport>} updates - map of ext names to {@linkcode UpdateReport}s
1216
+ * Options for {@linkcode ExtensionCliCommand._update}.
1284
1217
  */
1218
+ type ExtensionUpdateOpts = {installSpec: string; unsafe: boolean};
1285
1219
 
1286
1220
  /**
1287
1221
  * Part of result of {@linkcode ExtensionCliCommand._update}.
1288
- * @typedef UpdateReport
1289
- * @property {string} from - version the extension was updated from
1290
- * @property {string} to - version the extension was updated to
1291
1222
  */
1223
+ type UpdateReport = {from: string; to: string | null};
1224
+
1225
+ /**
1226
+ * Return value of {@linkcode ExtensionCliCommand._update}.
1227
+ */
1228
+ export type ExtensionUpdateResult = {errors: Record<string, Error>; updates: Record<string, UpdateReport>};
1292
1229
 
1293
1230
  /**
1294
1231
  * Options for {@linkcode ExtensionCliCommand._uninstall}.
1295
- * @typedef UninstallOpts
1296
- * @property {string} installSpec - the name or spec of an extension to uninstall
1297
1232
  */
1233
+ type UninstallOpts = {installSpec: string};
1298
1234
 
1299
1235
  /**
1300
1236
  * Used by {@linkcode ExtensionCliCommand.getPostInstallText}
1301
- * @typedef ExtensionArgs
1302
- * @property {string} extName - the name of an extension
1303
- * @property {object} extData - the data for an installed extension
1304
1237
  */
1238
+ export type ExtensionArgs<ExtType extends ExtensionType = ExtensionType> = {
1239
+ extName: string;
1240
+ extData: ExtInstallReceipt<ExtType>;
1241
+ };
1305
1242
 
1306
1243
  /**
1307
1244
  * Options for {@linkcode ExtensionCliCommand.installViaNpm}
1308
- * @typedef InstallViaNpmArgs
1309
- * @property {string} installSpec - the name or spec of an extension to install
1310
- * @property {string} pkgName - the NPM package name of the extension
1311
- * @property {import('appium/types').InstallType} installType - type of install
1312
- * @property {string} [pkgVer] - the specific version of the NPM package
1313
1245
  */
1246
+ type InstallViaNpmArgs = {
1247
+ installSpec: string;
1248
+ pkgName: string;
1249
+ installType: InstallType;
1250
+ pkgVer?: string;
1251
+ };
1314
1252
 
1315
1253
  /**
1316
1254
  * Object returned by {@linkcode ExtensionCliCommand.checkForExtensionUpdate}
1317
- * @typedef PossibleUpdates
1318
- * @property {string} current - current version
1319
- * @property {string?} safeUpdate - version we can safely update to if it exists, or null
1320
- * @property {string?} unsafeUpdate - version we can unsafely update to if it exists, or null
1321
1255
  */
1256
+ type PossibleUpdates = {current: string; safeUpdate: string | null; unsafeUpdate: string | null};
1322
1257
 
1323
1258
  /**
1324
1259
  * Options for {@linkcode ExtensionCliCommand._install}
1325
- * @typedef InstallOpts
1326
- * @property {string} installSpec - the name or spec of an extension to install
1327
- * @property {InstallType} installType - how to install this extension. One of the INSTALL_TYPES
1328
- * @property {string} [packageName] - for git/github installs, the extension node package name
1329
1260
  */
1261
+ type InstallOpts = {installSpec: string; installType: InstallType; packageName?: string};
1330
1262
 
1331
- /**
1332
- * @template {ExtensionType} ExtType
1333
- * @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
1334
- */
1263
+ type ListOptions = {showInstalled: boolean; showUpdates: boolean; verbose?: boolean};
1335
1264
 
1336
- /**
1337
- * @typedef ListOptions
1338
- * @property {boolean} showInstalled - whether should show only installed extensions
1339
- * @property {boolean} showUpdates - whether should show available updates
1340
- * @property {boolean} [verbose] - whether to show additional data from the extension
1341
- */
1265
+ type GetInstallationReceiptOpts<ExtType extends ExtensionType = ExtensionType> = {
1266
+ installPath: string;
1267
+ installSpec: string;
1268
+ pkg: ExtPackageJson<ExtType>;
1269
+ installType: InstallType;
1270
+ };
1342
1271
 
1343
- /**
1344
- * Opts for {@linkcode ExtensionCliCommand.getInstallationReceipt}
1345
- * @template {ExtensionType} ExtType
1346
- * @typedef GetInstallationReceiptOpts
1347
- * @property {string} installPath
1348
- * @property {string} installSpec
1349
- * @property {ExtPackageJson<ExtType>} pkg
1350
- * @property {InstallType} installType
1351
- */
1272
+ type InstalledExtensionLike = {installType?: InstallType; installPath?: string};
1273
+
1274
+ export default ExtensionCliCommand;
1275
+ export {ExtensionCliCommand as ExtensionCommand};
1352
1276
 
1353
- /**
1354
- * @typedef {import('appium/types').InstallType} InstallType
1355
- */