appium 3.2.2 → 3.3.1

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