appium 2.0.0-beta.4 → 2.0.0-beta.40

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 (151) hide show
  1. package/README.md +10 -11
  2. package/build/lib/appium.d.ts +204 -0
  3. package/build/lib/appium.d.ts.map +1 -0
  4. package/build/lib/appium.js +257 -131
  5. package/build/lib/cli/args.d.ts +20 -0
  6. package/build/lib/cli/args.d.ts.map +1 -0
  7. package/build/lib/cli/args.js +96 -282
  8. package/build/lib/cli/driver-command.d.ts +36 -0
  9. package/build/lib/cli/driver-command.d.ts.map +1 -0
  10. package/build/lib/cli/driver-command.js +25 -18
  11. package/build/lib/cli/extension-command.d.ts +372 -0
  12. package/build/lib/cli/extension-command.d.ts.map +1 -0
  13. package/build/lib/cli/extension-command.js +286 -156
  14. package/build/lib/cli/extension.d.ts +18 -0
  15. package/build/lib/cli/extension.d.ts.map +1 -0
  16. package/build/lib/cli/extension.js +30 -17
  17. package/build/lib/cli/parser.d.ts +80 -0
  18. package/build/lib/cli/parser.d.ts.map +1 -0
  19. package/build/lib/cli/parser.js +152 -95
  20. package/build/lib/cli/plugin-command.d.ts +33 -0
  21. package/build/lib/cli/plugin-command.d.ts.map +1 -0
  22. package/build/lib/cli/plugin-command.js +24 -19
  23. package/build/lib/cli/utils.d.ts +29 -0
  24. package/build/lib/cli/utils.d.ts.map +1 -0
  25. package/build/lib/cli/utils.js +27 -3
  26. package/build/lib/config-file.d.ts +100 -0
  27. package/build/lib/config-file.d.ts.map +1 -0
  28. package/build/lib/config-file.js +136 -0
  29. package/build/lib/config.d.ts +41 -0
  30. package/build/lib/config.d.ts.map +1 -0
  31. package/build/lib/config.js +92 -67
  32. package/build/lib/constants.d.ts +48 -0
  33. package/build/lib/constants.d.ts.map +1 -0
  34. package/build/lib/constants.js +60 -0
  35. package/build/lib/extension/driver-config.d.ts +81 -0
  36. package/build/lib/extension/driver-config.d.ts.map +1 -0
  37. package/build/lib/extension/driver-config.js +177 -0
  38. package/build/lib/extension/extension-config.d.ts +242 -0
  39. package/build/lib/extension/extension-config.d.ts.map +1 -0
  40. package/build/lib/extension/extension-config.js +436 -0
  41. package/build/lib/extension/index.d.ts +48 -0
  42. package/build/lib/extension/index.d.ts.map +1 -0
  43. package/build/lib/extension/index.js +74 -0
  44. package/build/lib/extension/manifest.d.ts +174 -0
  45. package/build/lib/extension/manifest.d.ts.map +1 -0
  46. package/build/lib/extension/manifest.js +256 -0
  47. package/build/lib/extension/package-changed.d.ts +11 -0
  48. package/build/lib/extension/package-changed.d.ts.map +1 -0
  49. package/build/lib/extension/package-changed.js +68 -0
  50. package/build/lib/extension/plugin-config.d.ts +57 -0
  51. package/build/lib/extension/plugin-config.d.ts.map +1 -0
  52. package/build/lib/extension/plugin-config.js +78 -0
  53. package/build/lib/grid-register.d.ts +10 -0
  54. package/build/lib/grid-register.d.ts.map +1 -0
  55. package/build/lib/grid-register.js +21 -25
  56. package/build/lib/logger.d.ts +3 -0
  57. package/build/lib/logger.d.ts.map +1 -0
  58. package/build/lib/logger.js +4 -6
  59. package/build/lib/logsink.d.ts +4 -0
  60. package/build/lib/logsink.d.ts.map +1 -0
  61. package/build/lib/logsink.js +14 -17
  62. package/build/lib/main.d.ts +55 -0
  63. package/build/lib/main.d.ts.map +1 -0
  64. package/build/lib/main.js +189 -90
  65. package/build/lib/schema/arg-spec.d.ts +143 -0
  66. package/build/lib/schema/arg-spec.d.ts.map +1 -0
  67. package/build/lib/schema/arg-spec.js +119 -0
  68. package/build/lib/schema/cli-args.d.ts +19 -0
  69. package/build/lib/schema/cli-args.d.ts.map +1 -0
  70. package/build/lib/schema/cli-args.js +180 -0
  71. package/build/lib/schema/cli-transformers.d.ts +5 -0
  72. package/build/lib/schema/cli-transformers.d.ts.map +1 -0
  73. package/build/lib/schema/cli-transformers.js +74 -0
  74. package/build/lib/schema/index.d.ts +3 -0
  75. package/build/lib/schema/index.d.ts.map +1 -0
  76. package/build/lib/schema/index.js +34 -0
  77. package/build/lib/schema/keywords.d.ts +24 -0
  78. package/build/lib/schema/keywords.d.ts.map +1 -0
  79. package/build/lib/schema/keywords.js +70 -0
  80. package/build/lib/schema/schema.d.ts +259 -0
  81. package/build/lib/schema/schema.d.ts.map +1 -0
  82. package/build/lib/schema/schema.js +452 -0
  83. package/build/lib/utils.d.ts +66 -0
  84. package/build/lib/utils.d.ts.map +1 -0
  85. package/build/lib/utils.js +35 -139
  86. package/build/tsconfig.tsbuildinfo +1 -0
  87. package/build/types/appium-manifest.d.ts +59 -0
  88. package/build/types/appium-manifest.d.ts.map +1 -0
  89. package/build/types/cli.d.ts +112 -0
  90. package/build/types/cli.d.ts.map +1 -0
  91. package/build/types/extension-manifest.d.ts +55 -0
  92. package/build/types/extension-manifest.d.ts.map +1 -0
  93. package/build/types/index.d.ts +16 -0
  94. package/build/types/index.d.ts.map +1 -0
  95. package/driver.d.ts +1 -0
  96. package/driver.js +14 -0
  97. package/index.js +11 -0
  98. package/lib/appium.js +517 -186
  99. package/lib/cli/args.js +269 -422
  100. package/lib/cli/driver-command.js +58 -23
  101. package/lib/cli/extension-command.js +612 -260
  102. package/lib/cli/extension.js +34 -16
  103. package/lib/cli/parser.js +241 -83
  104. package/lib/cli/plugin-command.js +48 -20
  105. package/lib/cli/utils.js +24 -10
  106. package/lib/config-file.js +219 -0
  107. package/lib/config.js +210 -91
  108. package/lib/constants.js +69 -0
  109. package/lib/extension/driver-config.js +249 -0
  110. package/lib/extension/extension-config.js +679 -0
  111. package/lib/extension/index.js +116 -0
  112. package/lib/extension/manifest.js +475 -0
  113. package/lib/extension/package-changed.js +64 -0
  114. package/lib/extension/plugin-config.js +113 -0
  115. package/lib/grid-register.js +49 -35
  116. package/lib/logger.js +1 -2
  117. package/lib/logsink.js +38 -33
  118. package/lib/main.js +303 -100
  119. package/lib/schema/arg-spec.js +229 -0
  120. package/lib/schema/cli-args.js +238 -0
  121. package/lib/schema/cli-transformers.js +115 -0
  122. package/lib/schema/index.js +2 -0
  123. package/lib/schema/keywords.js +136 -0
  124. package/lib/schema/schema.js +717 -0
  125. package/lib/utils.js +121 -140
  126. package/package.json +75 -85
  127. package/plugin.d.ts +1 -0
  128. package/plugin.js +13 -0
  129. package/scripts/autoinstall-extensions.js +177 -0
  130. package/support.d.ts +1 -0
  131. package/support.js +13 -0
  132. package/types/appium-manifest.ts +73 -0
  133. package/types/cli.ts +146 -0
  134. package/types/extension-manifest.ts +64 -0
  135. package/types/index.ts +21 -0
  136. package/CHANGELOG.md +0 -3515
  137. package/bin/ios-webkit-debug-proxy-launcher.js +0 -71
  138. package/build/lib/cli/npm.js +0 -206
  139. package/build/lib/cli/parser-helpers.js +0 -82
  140. package/build/lib/driver-config.js +0 -77
  141. package/build/lib/drivers.js +0 -96
  142. package/build/lib/extension-config.js +0 -253
  143. package/build/lib/plugin-config.js +0 -59
  144. package/build/lib/plugins.js +0 -14
  145. package/lib/cli/npm.js +0 -183
  146. package/lib/cli/parser-helpers.js +0 -79
  147. package/lib/driver-config.js +0 -46
  148. package/lib/drivers.js +0 -81
  149. package/lib/extension-config.js +0 -209
  150. package/lib/plugin-config.js +0 -34
  151. package/lib/plugins.js +0 -10
@@ -1,82 +1,121 @@
1
1
  /* eslint-disable no-console */
2
-
2
+ import B from 'bluebird';
3
3
  import _ from 'lodash';
4
- import NPM from './npm';
5
4
  import path from 'path';
6
- import { fs, util } from 'appium-support';
7
- import { log, spinWith } from './utils';
8
- import { INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB,
9
- INSTALL_TYPE_LOCAL } from '../extension-config';
5
+ import {npm, util, env, console} from '@appium/support';
6
+ import {spinWith, RingBuffer} from './utils';
7
+ import {SubProcess} from 'teen_process';
8
+ import {
9
+ INSTALL_TYPE_NPM,
10
+ INSTALL_TYPE_GIT,
11
+ INSTALL_TYPE_GITHUB,
12
+ INSTALL_TYPE_LOCAL,
13
+ } from '../extension/extension-config';
14
+ import {packageDidChange} from '../extension/package-changed';
10
15
 
11
16
  const UPDATE_ALL = 'installed';
12
17
 
13
18
  class NotUpdatableError extends Error {}
14
19
  class NoUpdatesAvailableError extends Error {}
15
20
 
16
- export default class ExtensionCommand {
21
+ /**
22
+ * @template {ExtensionType} ExtType
23
+ */
24
+ class ExtensionCommand {
25
+ /**
26
+ * This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
27
+ * @type {ExtensionConfig<ExtType>}
28
+ */
29
+ config;
30
+
31
+ /**
32
+ * {@linkcode Record} of official plugins or drivers.
33
+ * @type {KnownExtensions<ExtType>}
34
+ */
35
+ knownExtensions;
17
36
 
18
37
  /**
19
- * @typedef {Object} ExtensionCommandConstructor
20
- * @property {Object} config - the DriverConfig or PluginConfig object used for this command
21
- * @property {boolean} json - whether the output of this command should be JSON or text
22
- * @property {string} type - DRIVER_TYPE or PLUGIN_TYPE
38
+ * If `true`, command output has been requested as JSON.
39
+ * @type {boolean}
23
40
  */
41
+ isJsonOutput;
24
42
 
25
43
  /**
26
44
  * Build an ExtensionCommand
27
- *
28
- * @param {ExtensionCommandConstructor} opts
29
- * @return {ExtensionCommand}
45
+ * @param {ExtensionCommandOptions<ExtType>} opts
30
46
  */
31
- constructor ({config, json, type}) {
47
+ constructor({config, json}) {
32
48
  this.config = config;
33
- this.type = type;
34
- this.isJsonOutput = json;
35
- this.npm = new NPM(this.config.appiumHome);
36
- this.knownExtensions = {}; // this needs to be overridden in final class
49
+ this.log = new console.CliConsole({jsonMode: json});
50
+ this.isJsonOutput = Boolean(json);
51
+ }
52
+
53
+ /**
54
+ * `driver` or `plugin`, depending on the `ExtensionConfig`.
55
+ */
56
+ get type() {
57
+ return this.config.extensionType;
58
+ }
59
+
60
+ /**
61
+ * Logs a message and returns an {@linkcode Error} to throw.
62
+ *
63
+ * For TS to understand that a function throws an exception, it must actually throw an exception--
64
+ * in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
65
+ * nor is something like `@returns {never}` which does not imply a thrown exception.
66
+ * @param {string} message
67
+ * @protected
68
+ * @returns {Error}
69
+ */
70
+ _createFatalError(message) {
71
+ return new Error(this.log.decorate(message, 'error'));
37
72
  }
38
73
 
39
74
  /**
40
75
  * Take a CLI parse and run an extension command based on its type
41
76
  *
42
77
  * @param {object} args - a key/value object with CLI flags and values
43
- * @return {object} the result of the specific command which is executed
78
+ * @return {Promise<object>} the result of the specific command which is executed
44
79
  */
45
- async execute (args) {
80
+ async execute(args) {
46
81
  const cmd = args[`${this.type}Command`];
47
- if (!_.isFunction(ExtensionCommand.prototype[cmd])) {
48
- throw new Error(`Cannot handle ${this.type} command ${cmd}`);
82
+ if (!_.isFunction(this[cmd])) {
83
+ throw this._createFatalError(`Cannot handle ${this.type} command ${cmd}`);
49
84
  }
50
85
  const executeCmd = this[cmd].bind(this);
51
86
  return await executeCmd(args);
52
87
  }
53
88
 
54
- /**
55
- * @typedef {Object} ListArgs
56
- * @property {boolean} showInstalled - whether should show only installed extensions
57
- * @property {boolean} showUpdates - whether should show available updates
58
- */
59
-
60
89
  /**
61
90
  * List extensions
62
91
  *
63
- * @param {ListArgs} args
64
- * @return {object} map of extension names to extension data
92
+ * @param {ListOptions} opts
93
+ * @return {Promise<ExtensionListData>} map of extension names to extension data
65
94
  */
66
- async list ({showInstalled, showUpdates}) {
95
+ async list({showInstalled, showUpdates}) {
67
96
  const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
68
97
  const installedNames = Object.keys(this.config.installedExtensions);
69
98
  const knownNames = Object.keys(this.knownExtensions);
70
- const exts = [...installedNames, ...knownNames].reduce((acc, name) => {
71
- if (!acc[name]) {
72
- if (installedNames.includes(name)) {
73
- acc[name] = {...this.config.installedExtensions[name], installed: true};
74
- } else if (!showInstalled) {
75
- acc[name] = {pkgName: this.knownExtensions[name], installed: false};
99
+ const exts = [...installedNames, ...knownNames].reduce(
100
+ (acc, name) => {
101
+ if (!acc[name]) {
102
+ if (installedNames.includes(name)) {
103
+ acc[name] = {
104
+ ...this.config.installedExtensions[name],
105
+ installed: true,
106
+ };
107
+ } else if (!showInstalled) {
108
+ acc[name] = {pkgName: this.knownExtensions[name], installed: false};
109
+ }
76
110
  }
77
- }
78
- return acc;
79
- }, {});
111
+ return acc;
112
+ },
113
+ /**
114
+ * This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
115
+ * {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
116
+ * @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
117
+ */ ({})
118
+ );
80
119
 
81
120
  // if we want to show whether updates are available, put that behind a spinner
82
121
  await spinWith(this.isJsonOutput, lsMsg, async () => {
@@ -84,8 +123,7 @@ export default class ExtensionCommand {
84
123
  return;
85
124
  }
86
125
  for (const [ext, data] of _.toPairs(exts)) {
87
- const {installed, installType} = data;
88
- if (!installed || installType !== INSTALL_TYPE_NPM) {
126
+ if (!data.installed || data.installType !== INSTALL_TYPE_NPM) {
89
127
  // don't need to check for updates on exts that aren't installed
90
128
  // also don't need to check for updates on non-npm exts
91
129
  continue;
@@ -97,197 +135,250 @@ export default class ExtensionCommand {
97
135
  }
98
136
  });
99
137
 
138
+ const listData = /** @type {ExtensionListData} */ (exts);
139
+
100
140
  // if we're just getting the data, short circuit return here since we don't need to do any
101
141
  // formatting logic
102
142
  if (this.isJsonOutput) {
103
- return exts;
143
+ return listData;
104
144
  }
105
145
 
106
- for (const [
107
- name,
108
- {installType, installSpec, installed, updateVersion, unsafeUpdateVersion, version, upToDate}
109
- ] of _.toPairs(exts)) {
110
- let typeTxt;
111
- switch (installType) {
112
- case INSTALL_TYPE_GIT:
113
- case INSTALL_TYPE_GITHUB:
114
- typeTxt = `(cloned from ${installSpec})`.yellow;
115
- break;
116
- case INSTALL_TYPE_LOCAL:
117
- typeTxt = `(linked from ${installSpec})`.magenta;
118
- break;
119
- default:
120
- typeTxt = '(NPM)';
146
+ for (const [name, data] of _.toPairs(listData)) {
147
+ let installTxt = ' [not installed]'.grey;
148
+ let updateTxt = '';
149
+ let upToDateTxt = '';
150
+ let unsafeUpdateTxt = '';
151
+ if (data.installed) {
152
+ const {installType, installSpec, updateVersion, unsafeUpdateVersion, version, upToDate} =
153
+ data;
154
+ let typeTxt;
155
+ switch (installType) {
156
+ case INSTALL_TYPE_GIT:
157
+ case INSTALL_TYPE_GITHUB:
158
+ typeTxt = `(cloned from ${installSpec})`.yellow;
159
+ break;
160
+ case INSTALL_TYPE_LOCAL:
161
+ typeTxt = `(linked from ${installSpec})`.magenta;
162
+ break;
163
+ default:
164
+ typeTxt = '(NPM)';
165
+ }
166
+ installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
167
+
168
+ if (showUpdates) {
169
+ if (updateVersion) {
170
+ updateTxt = ` [${updateVersion} available]`.magenta;
171
+ }
172
+ if (upToDate) {
173
+ upToDateTxt = ` [Up to date]`.green;
174
+ }
175
+ if (unsafeUpdateVersion) {
176
+ unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
177
+ }
178
+ }
121
179
  }
122
- const installTxt = installed ?
123
- `@${version.yellow} ${('[installed ' + typeTxt + ']').green}` :
124
- ' [not installed]'.grey;
125
- const updateTxt = showUpdates && updateVersion ?
126
- ` [${updateVersion} available]`.magenta :
127
- '';
128
- const upToDateTxt = showUpdates && upToDate ?
129
- ` [Up to date]`.green :
130
- '';
131
- const unsafeUpdateTxt = showUpdates && unsafeUpdateVersion ?
132
- ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan :
133
- '';
134
-
135
- console.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
180
+
181
+ this.log.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
136
182
  }
137
183
 
138
- return exts;
184
+ return listData;
139
185
  }
140
186
 
141
- /**
142
- * @typedef {Object} InstallArgs
143
- * @property {string} ext - the name or spec of an extension to install
144
- * @property {string} installType - how to install this extension. One of the INSTALL_TYPES
145
- * @property {string} [packageName] - for git/github installs, the extension node package name
146
- */
147
-
148
187
  /**
149
188
  * Install an extension
150
189
  *
151
190
  * @param {InstallArgs} args
152
- * @return {object} map of all installed extension names to extension data
191
+ * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
153
192
  */
154
- async install ({ext, installType, packageName}) {
155
- log(this.isJsonOutput, `Attempting to find and install ${this.type} '${ext}'`);
156
-
193
+ async _install({installSpec, installType, packageName}) {
194
+ /** @type {ExtensionFields<ExtType>} */
157
195
  let extData;
158
- let installSpec = ext;
159
196
 
160
197
  if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
161
- throw new Error(`When using --source=${installType}, cannot also use --package`);
198
+ throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
162
199
  }
163
200
 
164
201
  if (!packageName && [INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB].includes(installType)) {
165
- throw new Error(`When using --source=${installType}, must also use --package`);
202
+ throw this._createFatalError(`When using --source=${installType}, must also use --package`);
166
203
  }
167
204
 
168
- if (installType === INSTALL_TYPE_LOCAL) {
169
- const msg = `Linking ${this.type} from local path`;
170
- const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => (
171
- await this.npm.linkPackage(installSpec))
172
- );
173
- extData = this.getExtensionFields(pkgJsonData);
174
- extData.installPath = extData.pkgName;
175
- } else if (installType === INSTALL_TYPE_GITHUB) {
205
+ /**
206
+ * @type {InstallViaNpmArgs}
207
+ */
208
+ let installOpts;
209
+
210
+ /**
211
+ * The probable (?) name of the extension derived from the install spec.
212
+ *
213
+ * If using a local install type, this will remain empty.
214
+ * @type {string}
215
+ */
216
+ let probableExtName = '';
217
+
218
+ // depending on `installType`, build the options to pass into `installViaNpm`
219
+ if (installType === INSTALL_TYPE_GITHUB) {
176
220
  if (installSpec.split('/').length !== 2) {
177
- throw new Error(`Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
178
- 'it should be of the form <org>/<repo>');
221
+ throw this._createFatalError(
222
+ `Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
223
+ 'it should be of the form <org>/<repo>'
224
+ );
179
225
  }
180
- extData = await this.installViaNpm({ext: installSpec, pkgName: packageName});
226
+ installOpts = {
227
+ installSpec,
228
+ pkgName: /** @type {string} */ (packageName),
229
+ };
230
+ probableExtName = installSpec;
181
231
  } else if (installType === INSTALL_TYPE_GIT) {
182
232
  // git urls can have '.git' at the end, but this is not necessary and would complicate the
183
233
  // way we download and name directories, so we can just remove it
184
234
  installSpec = installSpec.replace(/\.git$/, '');
185
- extData = await this.installViaNpm({ext: installSpec, pkgName: packageName});
235
+ installOpts = {
236
+ installSpec,
237
+ pkgName: /** @type {string} */ (packageName),
238
+ };
239
+ probableExtName = installSpec;
186
240
  } else {
187
- // at this point we have either an npm package or an appium verified extension
188
- // name. both of which will be installed via npm.
189
- // extensions installed via npm can include versions or tags after the '@'
190
- // sign, so check for that. We also need to be careful that package names themselves can
191
- // contain the '@' symbol, as in `npm install @appium/fake-driver@1.2.0`
192
- let name, pkgVer;
193
- const splits = installSpec.split('@');
194
- if (installSpec[0] === '@') {
195
- // this is the case where we have an npm org included in the package name
196
- [name, pkgVer] = [`@${splits[1]}`, splits[2]];
241
+ let pkgName, pkgVer;
242
+ if (installType === INSTALL_TYPE_LOCAL) {
243
+ pkgName = path.isAbsolute(installSpec) ? installSpec : path.resolve(installSpec);
197
244
  } else {
198
- // this is the case without an npm org
199
- [name, pkgVer] = splits;
200
- }
201
- let pkgName;
245
+ // at this point we have either an npm package or an appium verified extension
246
+ // name or a local path. both of which will be installed via npm.
247
+ // extensions installed via npm can include versions or tags after the '@'
248
+ // sign, so check for that. We also need to be careful that package names themselves can
249
+ // contain the '@' symbol, as in `npm install @appium/fake-driver@1.2.0`
250
+ let name;
251
+ const splits = installSpec.split('@');
252
+ if (installSpec[0] === '@') {
253
+ // this is the case where we have an npm org included in the package name
254
+ [name, pkgVer] = [`@${splits[1]}`, splits[2]];
255
+ } else {
256
+ // this is the case without an npm org
257
+ [name, pkgVer] = splits;
258
+ }
202
259
 
203
- if (installType === INSTALL_TYPE_NPM) {
204
- // if we're installing a named package from npm, we don't need to check
205
- // against the appium extension list; just use the installSpec as is
206
- pkgName = name;
207
- } else {
208
- // if we're installing a named appium driver (like 'xcuitest') we need to
209
- // dereference the actual npm package ('appiupm-xcuitest-driver'), so
210
- // check it exists and get the correct package
211
- const knownNames = Object.keys(this.knownExtensions);
212
- if (!_.includes(knownNames, name)) {
213
- const msg = `Could not resolve ${this.type}; are you sure it's in the list ` +
214
- `of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
215
- throw new Error(msg);
260
+ if (installType === INSTALL_TYPE_NPM) {
261
+ // if we're installing a named package from npm, we don't need to check
262
+ // against the appium extension list; just use the installSpec as is
263
+ pkgName = name;
264
+ } else {
265
+ // if we're installing a named appium driver (like 'xcuitest') we need to
266
+ // dereference the actual npm package ('appiupm-xcuitest-driver'), so
267
+ // check it exists and get the correct package
268
+ const knownNames = Object.keys(this.knownExtensions);
269
+ if (!_.includes(knownNames, name)) {
270
+ const msg =
271
+ `Could not resolve ${this.type}; are you sure it's in the list ` +
272
+ `of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
273
+ throw this._createFatalError(msg);
274
+ }
275
+ probableExtName = name;
276
+ pkgName = this.knownExtensions[name];
277
+ // given that we'll use the install type in the driver json, store it as
278
+ // 'npm' now
279
+ installType = INSTALL_TYPE_NPM;
216
280
  }
217
- pkgName = this.knownExtensions[name];
218
- // given that we'll use the install type in the driver json, store it as
219
- // 'npm' now
220
- installType = INSTALL_TYPE_NPM;
221
281
  }
282
+ installOpts = {installSpec, pkgName, pkgVer};
283
+ }
222
284
 
223
- extData = await this.installViaNpm({ext, pkgName, pkgVer});
285
+ // fail fast here if we can
286
+ if (probableExtName && this.config.isInstalled(probableExtName)) {
287
+ throw this._createFatalError(
288
+ `A ${this.type} named "${probableExtName}" is already installed. ` +
289
+ `Did you mean to update? Run "appium ${this.type} update". See ` +
290
+ `installed ${this.type}s with "appium ${this.type} list --installed".`
291
+ );
224
292
  }
225
293
 
226
- const extName = extData[`${this.type}Name`];
227
- delete extData[`${this.type}Name`];
294
+ extData = await this.installViaNpm(installOpts);
228
295
 
296
+ // this _should_ be the same as `probablyExtName` as the one derived above unless
297
+ // install type is local.
298
+ const extName = extData[/** @type {string} */ (`${this.type}Name`)];
299
+
300
+ // check _a second time_ with the more-accurate extName
229
301
  if (this.config.isInstalled(extName)) {
230
- throw new Error(`A ${this.type} named '${extName}' is already installed. ` +
231
- `Did you mean to update? 'appium ${this.type} update'. See ` +
232
- `installed ${this.type}s with 'appium ${this.type} list --installed'.`);
302
+ throw this._createFatalError(
303
+ `A ${this.type} named "${extName}" is already installed. ` +
304
+ `Did you mean to update? Run "appium ${this.type} update". See ` +
305
+ `installed ${this.type}s with "appium ${this.type} list --installed".`
306
+ );
307
+ }
308
+
309
+ // this field does not exist as such in the manifest (it's used as a property name instead)
310
+ // so that's why it's being removed here.
311
+ delete extData[/** @type {string} */ (`${this.type}Name`)];
312
+
313
+ /** @type {ExtManifest<ExtType>} */
314
+ const extManifest = {...extData, installType, installSpec};
315
+ const [errors, warnings] = await B.all([
316
+ this.config.getProblems(extName, extManifest),
317
+ this.config.getWarnings(extName, extManifest),
318
+ ]);
319
+ const errorMap = new Map([[extName, errors]]);
320
+ const warningMap = new Map([[extName, warnings]]);
321
+ const {errorSummaries, warningSummaries} = this.config.getValidationResultSummaries(
322
+ errorMap,
323
+ warningMap
324
+ );
325
+
326
+ if (!_.isEmpty(errorSummaries)) {
327
+ throw this._createFatalError(errorSummaries.join('\n'));
328
+ }
329
+
330
+ // note that we won't show any warnings if there were errors.
331
+ if (!_.isEmpty(warningSummaries)) {
332
+ this.log.warn(warningSummaries.join('\n'));
233
333
  }
234
334
 
235
- extData.installType = installType;
236
- extData.installSpec = installSpec;
237
- await this.config.addExtension(extName, extData);
335
+ await this.config.addExtension(extName, extManifest);
336
+
337
+ // update the if we've changed the local `package.json`
338
+ if (await env.hasAppiumDependency(this.config.appiumHome)) {
339
+ await packageDidChange(this.config.appiumHome);
340
+ }
238
341
 
239
342
  // log info for the user
240
- log(this.isJsonOutput, this.getPostInstallText({extName, extData}));
343
+ this.log.info(this.getPostInstallText({extName, extData}));
241
344
 
242
345
  return this.config.installedExtensions;
243
346
  }
244
347
 
245
- /**
246
- * @typedef {Object} InstallViaNpmArgs
247
- * @property {string} ext - the name or spec of an extension to install
248
- * @property {string} pkgName - the NPM package name of the extension
249
- * @property {string} [pkgVer] - the specific version of the NPM package
250
- */
251
-
252
348
  /**
253
349
  * Install an extension via NPM
254
350
  *
255
351
  * @param {InstallViaNpmArgs} args
256
352
  */
257
- async installViaNpm ({ext, pkgName, pkgVer}) {
353
+ async installViaNpm({installSpec, pkgName, pkgVer}) {
258
354
  const npmSpec = `${pkgName}${pkgVer ? '@' + pkgVer : ''}`;
259
- const specMsg = npmSpec === ext ? '' : ` using NPM install spec '${npmSpec}'`;
260
- const msg = `Installing '${ext}'${specMsg}`;
355
+ const specMsg = npmSpec === installSpec ? '' : ` using NPM install spec '${npmSpec}'`;
356
+ const msg = `Installing '${installSpec}'${specMsg}`;
261
357
  try {
262
- const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => (
263
- await this.npm.installPackage({
264
- pkgDir: path.resolve(this.config.appiumHome, pkgName),
265
- pkgName,
266
- pkgVer
267
- })
268
- ));
269
- const extData = this.getExtensionFields(pkgJsonData);
270
- extData.installPath = pkgName;
271
- return extData;
358
+ const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => {
359
+ const pkgJsonData = await npm.installPackage(this.config.appiumHome, pkgName, {
360
+ pkgVer,
361
+ });
362
+ this.validatePackageJson(pkgJsonData, installSpec);
363
+ return pkgJsonData;
364
+ });
365
+
366
+ return this.getExtensionFields(pkgJsonData);
272
367
  } catch (err) {
273
- throw new Error(`Encountered an error when installing package: ${err.message}`);
368
+ throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
274
369
  }
275
370
  }
276
371
 
277
- /**
278
- * @typedef {Object} ExtensionArgs
279
- * @property {string} extName - the name of an extension
280
- * @property {object} extData - the data for an installed extension
281
- */
282
-
283
372
  /**
284
373
  * Get the text which should be displayed to the user after an extension has been installed. This
285
374
  * is designed to be overridden by drivers/plugins with their own particular text.
286
375
  *
287
376
  * @param {ExtensionArgs} args
377
+ * @returns {string}
288
378
  */
289
- getPostInstallText (/*{extName, extData}*/) {
290
- throw new Error('Must be implemented in final class');
379
+ // eslint-disable-next-line no-unused-vars
380
+ getPostInstallText(args) {
381
+ throw this._createFatalError('Must be implemented in final class');
291
382
  }
292
383
 
293
384
  /**
@@ -296,94 +387,123 @@ export default class ExtensionCommand {
296
387
  * load as the main driver class, or to be able to detect incompatibilities between driver and
297
388
  * appium versions.
298
389
  *
299
- * @param {object} pkgJsonData - the package.json data for a driver module, as if it had been
300
- * straightforwardly 'require'd
390
+ * @param {ExtPackageJson<ExtType>} pkgJson - the package.json data for a driver module, as if it had been straightforwardly 'require'd
391
+ * @returns {ExtensionFields<ExtType>}
301
392
  */
302
- getExtensionFields (pkgJsonData) {
303
- if (!pkgJsonData.appium) {
304
- throw new Error(`Installed driver did not have an 'appium' section in its ` +
305
- `package.json file as expected`);
306
- }
307
- const {appium, name, version} = pkgJsonData;
308
- this.validateExtensionFields(appium);
309
-
310
- return {...appium, pkgName: name, version};
393
+ getExtensionFields(pkgJson) {
394
+ const {appium, name, version, peerDependencies} = pkgJson;
395
+
396
+ /** @type {unknown} */
397
+ const result = {
398
+ ...appium,
399
+ pkgName: name,
400
+ version,
401
+ appiumVersion: peerDependencies?.appium,
402
+ };
403
+ return /** @type {ExtensionFields<ExtType>} */ (result);
311
404
  }
312
405
 
313
406
  /**
314
- * For any package.json fields which a particular type of extension requires, validate the
315
- * presence and form of those fields on the package.json data, throwing an error if anything is
316
- * amiss.
407
+ * Validates the _required_ root fields of an extension's `package.json` file.
317
408
  *
318
- * @param {object} appiumPkgData - the data in the "appium" field of package.json for an
319
- * extension
409
+ * These required fields are:
410
+ * - `name`
411
+ * - `version`
412
+ * - `appium`
413
+ * @param {import('type-fest').PackageJson} pkgJson - `package.json` of extension
414
+ * @param {string} installSpec - Extension name/spec
415
+ * @throws {ReferenceError} If `package.json` has a missing or invalid field
416
+ * @returns {pkgJson is ExtPackageJson<ExtType>}
320
417
  */
321
- validateExtensionFields (/*appiumPkgData*/) {
322
- throw new Error('Must be implemented in final class');
418
+ validatePackageJson(pkgJson, installSpec) {
419
+ const {appium, name, version} = /** @type {ExtPackageJson<ExtType>} */ (pkgJson);
420
+
421
+ /**
422
+ *
423
+ * @param {string} field
424
+ * @returns {ReferenceError}
425
+ */
426
+ const createMissingFieldError = (field) =>
427
+ new ReferenceError(
428
+ `${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``
429
+ );
430
+
431
+ if (!name) {
432
+ throw createMissingFieldError('name');
433
+ }
434
+ if (!version) {
435
+ throw createMissingFieldError('version');
436
+ }
437
+ if (!appium) {
438
+ throw createMissingFieldError('appium');
439
+ }
440
+
441
+ this.validateExtensionFields(appium, installSpec);
442
+
443
+ return true;
323
444
  }
324
445
 
325
446
  /**
326
- * @typedef {Object} UninstallArgs
327
- * @property {string} ext - the name or spec of an extension to uninstall
447
+ * For any `package.json` fields which a particular type of extension requires, validate the
448
+ * presence and form of those fields on the `package.json` data, throwing an error if anything is
449
+ * amiss.
450
+ *
451
+ * @param {ExtMetadata<ExtType>} extMetadata - the data in the "appium" field of `package.json` for an extension
452
+ * @param {string} installSpec - Extension name/spec
328
453
  */
454
+ // eslint-disable-next-line no-unused-vars
455
+ validateExtensionFields(extMetadata, installSpec) {
456
+ throw this._createFatalError('Must be implemented in final class');
457
+ }
329
458
 
330
459
  /**
331
- * Uninstall an extension
460
+ * Uninstall an extension.
332
461
  *
333
- * @param {UninstallArgs} args
334
- * @return {object} map of all installed extension names to extension data
462
+ * First tries to do this via `npm uninstall`, but if that fails, just `rm -rf`'s the extension dir.
463
+ *
464
+ * Will only remove the extension from the manifest if it has been successfully removed.
465
+ *
466
+ * @param {UninstallOpts} opts
467
+ * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data (without the extension just uninstalled)
335
468
  */
336
- async uninstall ({ext}) {
337
- if (!this.config.isInstalled(ext)) {
338
- throw new Error(`Can't uninstall ${this.type} '${ext}'; it is not installed`);
339
- }
340
- try {
341
- await fs.rimraf(this.config.getInstallPath(ext));
342
- } finally {
343
- await this.config.removeExtension(ext);
469
+ async _uninstall({installSpec}) {
470
+ if (!this.config.isInstalled(installSpec)) {
471
+ throw this._createFatalError(
472
+ `Can't uninstall ${this.type} '${installSpec}'; it is not installed`
473
+ );
344
474
  }
345
- log(this.isJsonOutput, `Successfully uninstalled ${this.type} '${ext}'`.green);
475
+ const pkgName = this.config.installedExtensions[installSpec].pkgName;
476
+ await npm.uninstallPackage(this.config.appiumHome, pkgName);
477
+ await this.config.removeExtension(installSpec);
478
+ this.log.ok(`Successfully uninstalled ${this.type} '${installSpec}'`.green);
346
479
  return this.config.installedExtensions;
347
480
  }
348
481
 
349
- /**
350
- * @typedef {Object} ExtensionUpdateOpts
351
- * @property {string} ext - the name of the extension to update
352
- * @property {boolean} unsafe - if true, will perform unsafe updates past major revision
353
- * boundaries
354
- */
355
-
356
- /**
357
- * @typedef {Object} UpdateReport
358
- * @property {string} from - version updated from
359
- * @property {string} to - version updated to
360
- */
361
-
362
- /**
363
- * @typedef {Object} ExtensionUpdateResult
364
- * @property {Object} errors - map of ext names to error objects
365
- * @property {Object} updates - map of ext names to {@link UpdateReport}s
366
- */
367
-
368
482
  /**
369
483
  * Attempt to update one or more drivers using NPM
370
484
  *
371
485
  * @param {ExtensionUpdateOpts} updateSpec
372
- * @return {ExtensionUpdateResult}
486
+ * @return {Promise<ExtensionUpdateResult>}
373
487
  */
374
- async update ({ext, unsafe}) {
375
- const shouldUpdateAll = ext === UPDATE_ALL;
488
+ async _update({installSpec, unsafe}) {
489
+ const shouldUpdateAll = installSpec === UPDATE_ALL;
376
490
  // if we're specifically requesting an update for an extension, make sure it's installed
377
- if (!shouldUpdateAll && !this.config.isInstalled(ext)) {
378
- throw new Error(`The ${this.type} '${ext}' was not installed, so can't be updated`);
491
+ if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
492
+ throw this._createFatalError(
493
+ `The ${this.type} "${installSpec}" was not installed, so can't be updated`
494
+ );
379
495
  }
380
- const extsToUpdate = shouldUpdateAll ? Object.keys(this.config.installedExtensions) : [ext];
496
+ const extsToUpdate = shouldUpdateAll
497
+ ? Object.keys(this.config.installedExtensions)
498
+ : [installSpec];
381
499
 
382
500
  // 'errors' will have ext names as keys and error objects as values
501
+ /** @type {Record<string,Error>} */
383
502
  const errors = {};
384
503
 
385
504
  // 'updates' will have ext names as keys and update objects as values, where an update
386
505
  // object is of the form {from: versionString, to: versionString}
506
+ /** @type {Record<string,UpdateReport>} */
387
507
  const updates = {};
388
508
 
389
509
  for (const e of extsToUpdate) {
@@ -393,17 +513,23 @@ export default class ExtensionCommand {
393
513
  throw new NotUpdatableError();
394
514
  }
395
515
  });
396
- const update = await spinWith(this.isJsonOutput, `Checking if ${this.type} '${e}' needs an update`, async () => {
397
- const update = await this.checkForExtensionUpdate(e);
398
- if (!(update.safeUpdate || update.unsafeUpdate)) {
399
- throw new NoUpdatesAvailableError();
516
+ const update = await spinWith(
517
+ this.isJsonOutput,
518
+ `Checking if ${this.type} '${e}' needs an update`,
519
+ async () => {
520
+ const update = await this.checkForExtensionUpdate(e);
521
+ if (!(update.safeUpdate || update.unsafeUpdate)) {
522
+ throw new NoUpdatesAvailableError();
523
+ }
524
+ return update;
400
525
  }
401
- return update;
402
- });
526
+ );
403
527
  if (!unsafe && !update.safeUpdate) {
404
- throw new Error(`The ${this.type} '${e}' has a major revision update ` +
405
- `(${update.current} => ${update.unsafeUpdate}), which could include ` +
406
- `breaking changes. If you want to apply this update, re-run with --unsafe`);
528
+ throw this._createFatalError(
529
+ `The ${this.type} '${e}' has a major revision update ` +
530
+ `(${update.current} => ${update.unsafeUpdate}), which could include ` +
531
+ `breaking changes. If you want to apply this update, re-run with --unsafe`
532
+ );
407
533
  }
408
534
  const updateVer = unsafe && update.unsafeUpdate ? update.unsafeUpdate : update.safeUpdate;
409
535
  await spinWith(
@@ -417,46 +543,45 @@ export default class ExtensionCommand {
417
543
  }
418
544
  }
419
545
 
420
- log(this.isJsonOutput, 'Update report:');
546
+ this.log.info('Update report:');
547
+
421
548
  for (const [e, update] of _.toPairs(updates)) {
422
- log(this.isJsonOutput, `- ${this.type} ${e} updated: ${update.from} => ${update.to}`.green);
549
+ this.log.ok(` - ${this.type} ${e} updated: ${update.from} => ${update.to}`.green);
423
550
  }
551
+
424
552
  for (const [e, err] of _.toPairs(errors)) {
425
553
  if (err instanceof NotUpdatableError) {
426
- log(this.isJsonOutput, `- '${e}' was not installed via npm, so we could not check ` +
427
- `for updates`.yellow);
554
+ this.log.warn(
555
+ ` - '${e}' was not installed via npm, so we could not check ` + `for updates`.yellow
556
+ );
428
557
  } else if (err instanceof NoUpdatesAvailableError) {
429
- log(this.isJsonOutput, `- '${e}' had no updates available`.yellow);
558
+ this.log.info(` - '${e}' had no updates available`.yellow);
430
559
  } else {
431
560
  // otherwise, make it pop with red!
432
- log(this.isJsonOutput, `- '${e}' failed to update: ${err}`.red);
561
+ this.log.error(` - '${e}' failed to update: ${err}`.red);
433
562
  }
434
563
  }
435
-
436
564
  return {updates, errors};
437
565
  }
438
566
 
439
- /**
440
- * @typedef PossibleUpdates
441
- * @property {string} current - current version
442
- * @property {string|null} safeUpdate - version we can safely update to if it exists, or null
443
- * @property {string|null} unsafeUpdate - version we can unsafely update to if it exists, or null
444
- */
445
-
446
567
  /**
447
568
  * Given an extension name, figure out what its highest possible version upgrade is, and also the
448
569
  * highest possible safe upgrade.
449
570
  *
450
571
  * @param {string} ext - name of extension
451
- * @return {PossibleUpdates}
572
+ * @return {Promise<PossibleUpdates>}
452
573
  */
453
- async checkForExtensionUpdate (ext) {
574
+ async checkForExtensionUpdate(ext) {
454
575
  // TODO decide how we want to handle beta versions?
455
576
  // this is a helper method, 'ext' is assumed to already be installed here, and of the npm
456
577
  // install type
457
578
  const {version, pkgName} = this.config.installedExtensions[ext];
458
- let unsafeUpdate = await this.npm.getLatestVersion(pkgName);
459
- let safeUpdate = await this.npm.getLatestSafeUpgradeVersion(pkgName, version);
579
+ let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
580
+ let safeUpdate = await npm.getLatestSafeUpgradeVersion(
581
+ this.config.appiumHome,
582
+ pkgName,
583
+ version
584
+ );
460
585
  if (!util.compareVersions(unsafeUpdate, '>', version)) {
461
586
  // the latest version is not greater than the current version, so there's no possible update
462
587
  unsafeUpdate = null;
@@ -477,13 +602,240 @@ export default class ExtensionCommand {
477
602
  * Actually update an extension installed by NPM, using the NPM cli. And update the installation
478
603
  * manifest.
479
604
  *
480
- * @param {string} ext - name of extension to update
605
+ * @param {string} installSpec - name of extension to update
481
606
  * @param {string} version - version string identifier to update extension to
607
+ * @returns {Promise<void>}
482
608
  */
483
- async updateExtension (ext, version) {
484
- const {pkgName} = this.config.installedExtensions[ext];
485
- await this.installViaNpm({ext, pkgName, pkgVer: version});
486
- this.config.installedExtensions[ext].version = version;
487
- await this.config.write();
609
+ async updateExtension(installSpec, version) {
610
+ const {pkgName} = this.config.installedExtensions[installSpec];
611
+ const extData = await this.installViaNpm({
612
+ installSpec,
613
+ pkgName,
614
+ pkgVer: version,
615
+ });
616
+ delete extData[/** @type {string} */ (`${this.type}Name`)];
617
+ await this.config.updateExtension(installSpec, extData);
618
+ }
619
+
620
+ /**
621
+ * Runs a script cached inside the "scripts" field under "appium"
622
+ * inside of the driver/plugins "package.json" file. Will throw
623
+ * an error if the driver/plugin does not contain a "scripts" field
624
+ * underneath the "appium" field in its package.json, if the
625
+ * "scripts" field is not a plain object, or if the scriptName is
626
+ * not found within "scripts" object.
627
+ *
628
+ * @param {RunOptions} opts
629
+ * @return {Promise<RunOutput>}
630
+ */
631
+ async _run({installSpec, scriptName}) {
632
+ if (!this.config.isInstalled(installSpec)) {
633
+ throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
634
+ }
635
+
636
+ const extConfig = this.config.installedExtensions[installSpec];
637
+
638
+ // note: TS cannot understand that _.has() is a type guard
639
+ if (!extConfig.scripts) {
640
+ throw this._createFatalError(
641
+ `The ${this.type} named '${installSpec}' does not contain the ` +
642
+ `"scripts" field underneath the "appium" field in its package.json`
643
+ );
644
+ }
645
+
646
+ const extScripts = extConfig.scripts;
647
+
648
+ if (!_.isPlainObject(extScripts)) {
649
+ throw this._createFatalError(
650
+ `The ${this.type} named '${installSpec}' "scripts" field must be a plain object`
651
+ );
652
+ }
653
+
654
+ if (!_.has(extScripts, scriptName)) {
655
+ throw this._createFatalError(
656
+ `The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
657
+ );
658
+ }
659
+
660
+ const runner = new SubProcess(process.execPath, [extScripts[scriptName]], {
661
+ cwd: this.config.getInstallPath(installSpec),
662
+ });
663
+
664
+ const output = new RingBuffer(50);
665
+
666
+ runner.on('stream-line', (line) => {
667
+ output.enqueue(line);
668
+ this.log.log(line);
669
+ });
670
+
671
+ await runner.start(0);
672
+
673
+ try {
674
+ await runner.join();
675
+ this.log.ok(`${scriptName} successfully ran`.green);
676
+ return {output: output.getBuff()};
677
+ } catch (err) {
678
+ this.log.error(`Encountered an error when running '${scriptName}': ${err.message}`.red);
679
+ return {error: err.message, output: output.getBuff()};
680
+ }
488
681
  }
489
682
  }
683
+
684
+ export default ExtensionCommand;
685
+ export {ExtensionCommand};
686
+
687
+ /**
688
+ * Options for the {@linkcode ExtensionCommand} constructor
689
+ * @template {ExtensionType} ExtType
690
+ * @typedef ExtensionCommandOptions
691
+ * @property {ExtensionConfig<ExtType>} config - the `DriverConfig` or `PluginConfig` instance used for this command
692
+ * @property {boolean} json - whether the output of this command should be JSON or text
693
+ */
694
+
695
+ /**
696
+ * Extra stuff about extensions; used indirectly by {@linkcode ExtensionCommand.list}.
697
+ *
698
+ * @typedef ExtensionMetadata
699
+ * @property {boolean} installed - If `true`, the extension is installed
700
+ * @property {string?} updateVersion - If the extension is installed, the version it can be updated to
701
+ * @property {string?} unsafeUpdateVersion - Same as above, but a major version bump
702
+ * @property {boolean} upToDate - If the extension is installed and the latest
703
+ */
704
+
705
+ /**
706
+ * @typedef {import('@appium/types').ExtensionType} ExtensionType
707
+ * @typedef {import('@appium/types').DriverType} DriverType
708
+ * @typedef {import('@appium/types').PluginType} PluginType
709
+ */
710
+
711
+ /**
712
+ * @template {ExtensionType} ExtType
713
+ * @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
714
+ */
715
+
716
+ /**
717
+ * @template {ExtensionType} ExtType
718
+ * @typedef {import('../extension/extension-config').ExtensionConfig<ExtType>} ExtensionConfig
719
+ */
720
+
721
+ /**
722
+ * @template {ExtensionType} ExtType
723
+ * @typedef {import('appium/types').ExtMetadata<ExtType>} ExtMetadata
724
+ */
725
+
726
+ /**
727
+ * @template {ExtensionType} ExtType
728
+ * @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
729
+ */
730
+
731
+ /**
732
+ * @template {ExtensionType} ExtType
733
+ * @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
734
+ */
735
+
736
+ /**
737
+ * Possible return value for {@linkcode ExtensionCommand.list}
738
+ * @typedef UninstalledExtensionListData
739
+ * @property {string} pkgName
740
+ * @property {false} installed
741
+ */
742
+
743
+ /**
744
+ * Possible return value for {@linkcode ExtensionCommand.list}
745
+ * @typedef {import('appium/types').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
746
+ */
747
+
748
+ /**
749
+ * Return value of {@linkcode ExtensionCommand.list}.
750
+ * @typedef {Record<string,InstalledExtensionListData|UninstalledExtensionListData>} ExtensionListData
751
+ */
752
+
753
+ /**
754
+ * Options for {@linkcode ExtensionCommand._run}.
755
+ * @typedef RunOptions
756
+ * @property {string} installSpec - name of the extension to run a script from
757
+ * @property {string} scriptName - name of the script to run
758
+ */
759
+
760
+ /**
761
+ * Return value of {@linkcode ExtensionCommand._run}
762
+ *
763
+ * @typedef RunOutput
764
+ * @property {string} [error] - error message if script ran unsuccessfully, otherwise undefined
765
+ * @property {string[]} output - script output
766
+ */
767
+
768
+ /**
769
+ * Options for {@linkcode ExtensionCommand._update}.
770
+ * @typedef ExtensionUpdateOpts
771
+ * @property {string} installSpec - the name of the extension to update
772
+ * @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
773
+ */
774
+
775
+ /**
776
+ * Return value of {@linkcode ExtensionCommand._update}.
777
+ * @typedef ExtensionUpdateResult
778
+ * @property {Record<string,Error>} errors - map of ext names to error objects
779
+ * @property {Record<string,UpdateReport>} updates - map of ext names to {@linkcode UpdateReport}s
780
+ */
781
+
782
+ /**
783
+ * Part of result of {@linkcode ExtensionCommand._update}.
784
+ * @typedef UpdateReport
785
+ * @property {string} from - version the extension was updated from
786
+ * @property {string} to - version the extension was updated to
787
+ */
788
+
789
+ /**
790
+ * Options for {@linkcode ExtensionCommand._uninstall}.
791
+ * @typedef UninstallOpts
792
+ * @property {string} installSpec - the name or spec of an extension to uninstall
793
+ */
794
+
795
+ /**
796
+ * Used by {@linkcode ExtensionCommand.getPostInstallText}
797
+ * @typedef ExtensionArgs
798
+ * @property {string} extName - the name of an extension
799
+ * @property {object} extData - the data for an installed extension
800
+ */
801
+
802
+ /**
803
+ * Options for {@linkcode ExtensionCommand.installViaNpm}
804
+ * @typedef InstallViaNpmArgs
805
+ * @property {string} installSpec - the name or spec of an extension to install
806
+ * @property {string} pkgName - the NPM package name of the extension
807
+ * @property {string} [pkgVer] - the specific version of the NPM package
808
+ */
809
+
810
+ /**
811
+ * Object returned by {@linkcode ExtensionCommand.checkForExtensionUpdate}
812
+ * @typedef PossibleUpdates
813
+ * @property {string} current - current version
814
+ * @property {string?} safeUpdate - version we can safely update to if it exists, or null
815
+ * @property {string?} unsafeUpdate - version we can unsafely update to if it exists, or null
816
+ */
817
+
818
+ /**
819
+ * Options for {@linkcode ExtensionCommand._install}
820
+ * @typedef InstallArgs
821
+ * @property {string} installSpec - the name or spec of an extension to install
822
+ * @property {import('appium/types').InstallType} installType - how to install this extension. One of the INSTALL_TYPES
823
+ * @property {string} [packageName] - for git/github installs, the extension node package name
824
+ */
825
+
826
+ /**
827
+ * Returned by {@linkcode ExtensionCommand.getExtensionFields}
828
+ * @template {ExtensionType} ExtType
829
+ * @typedef {ExtMetadata<ExtType> & { pkgName: string, version: string, appiumVersion: string } & import('appium/types').CommonExtMetadata} ExtensionFields
830
+ */
831
+
832
+ /**
833
+ * @template {ExtensionType} ExtType
834
+ * @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
835
+ */
836
+
837
+ /**
838
+ * @typedef ListOptions
839
+ * @property {boolean} showInstalled - whether should show only installed extensions
840
+ * @property {boolean} showUpdates - whether should show available updates
841
+ */