appium 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/build/lib/appium.d.ts +4 -64
  2. package/build/lib/appium.d.ts.map +1 -1
  3. package/build/lib/appium.js +46 -316
  4. package/build/lib/appium.js.map +1 -1
  5. package/build/lib/bidi.d.ts +32 -0
  6. package/build/lib/bidi.d.ts.map +1 -0
  7. package/build/lib/bidi.js +348 -0
  8. package/build/lib/bidi.js.map +1 -0
  9. package/build/lib/cli/args.d.ts.map +1 -1
  10. package/build/lib/cli/args.js +21 -31
  11. package/build/lib/cli/args.js.map +1 -1
  12. package/build/lib/cli/extension-command.d.ts.map +1 -1
  13. package/build/lib/cli/extension-command.js +38 -5
  14. package/build/lib/cli/extension-command.js.map +1 -1
  15. package/build/lib/cli/parser.d.ts.map +1 -1
  16. package/build/lib/cli/parser.js +26 -15
  17. package/build/lib/cli/parser.js.map +1 -1
  18. package/build/lib/cli/utils.d.ts.map +1 -1
  19. package/build/lib/cli/utils.js +3 -1
  20. package/build/lib/cli/utils.js.map +1 -1
  21. package/build/lib/config.js +42 -9
  22. package/build/lib/config.js.map +1 -1
  23. package/build/lib/extension/extension-config.d.ts +7 -0
  24. package/build/lib/extension/extension-config.d.ts.map +1 -1
  25. package/build/lib/extension/extension-config.js +42 -11
  26. package/build/lib/extension/extension-config.js.map +1 -1
  27. package/build/lib/extension/manifest.js +2 -2
  28. package/build/lib/extension/manifest.js.map +1 -1
  29. package/build/lib/schema/schema.js +3 -3
  30. package/build/lib/schema/schema.js.map +1 -1
  31. package/build/lib/utils.js +1 -2
  32. package/build/lib/utils.js.map +1 -1
  33. package/lib/appium.js +16 -377
  34. package/lib/bidi.ts +436 -0
  35. package/lib/cli/args.js +22 -32
  36. package/lib/cli/extension-command.js +4 -4
  37. package/lib/cli/parser.js +33 -17
  38. package/lib/cli/utils.js +3 -1
  39. package/lib/config.js +7 -7
  40. package/lib/extension/extension-config.js +49 -11
  41. package/lib/extension/manifest.js +2 -2
  42. package/lib/schema/schema.js +2 -2
  43. package/lib/utils.js +2 -2
  44. package/package.json +13 -13
  45. package/scripts/autoinstall-extensions.js +5 -5
  46. package/LICENSE +0 -201
package/lib/bidi.ts ADDED
@@ -0,0 +1,436 @@
1
+ import _ from 'lodash';
2
+ import B from 'bluebird';
3
+ import {
4
+ errors,
5
+ } from '@appium/base-driver';
6
+ import {BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
7
+ import WebSocket from 'ws';
8
+ import os from 'node:os';
9
+ import {
10
+ isBroadcastIp,
11
+ fetchInterfaces,
12
+ V4_BROADCAST_IP,
13
+ } from './utils';
14
+ import type {IncomingMessage} from 'node:http';
15
+ import type {AppiumDriver} from './appium';
16
+ import type {
17
+ ErrorBiDiCommandResponse,
18
+ SuccessBiDiCommandResponse,
19
+ ExternalDriver,
20
+ StringRecord
21
+ } from '@appium/types';
22
+
23
+ type AnyDriver = ExternalDriver | AppiumDriver;
24
+ type SendData = (data: string | Buffer) => Promise<void>;
25
+ type LogSocketError = (err: Error) => void;
26
+ interface InitBiDiSocketResult {
27
+ bidiHandlerDriver: AnyDriver;
28
+ proxyClient: WebSocket | null;
29
+ send: SendData;
30
+ sendToProxy: SendData | null;
31
+ logSocketErr: LogSocketError;
32
+ }
33
+
34
+
35
+ const MIN_WS_CODE_VAL = 1000;
36
+ const MAX_WS_CODE_VAL = 1015;
37
+ const WS_FALLBACK_CODE = 1011; // server encountered an error while fulfilling request
38
+ const BIDI_EVENTS_MAP: WeakMap<AnyDriver, Record<string, number>> = new WeakMap();
39
+ const MAX_LOGGED_DATA_LENGTH = 300;
40
+
41
+ /**
42
+ * Clients cannot use broadcast addresses, like 0.0.0.0 or ::
43
+ * to create connections. Thus we prefer a hostname if such
44
+ * address is provided or the actual address of a non-local interface,
45
+ * in case the host only has one such interface.
46
+ *
47
+ * @param address
48
+ */
49
+ export function determineBiDiHost(address: string): string {
50
+ if (!isBroadcastIp(address)) {
51
+ return address;
52
+ }
53
+
54
+ const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6)
55
+ .filter((iface) => !iface.internal);
56
+ return nonLocalInterfaces.length === 1 ? nonLocalInterfaces[0].address : os.hostname();
57
+ }
58
+
59
+ /**
60
+ * Initialize a new bidi connection and set up handlers
61
+ * @param ws The websocket connection object
62
+ * @param req The connection pathname, which might include the session id
63
+ */
64
+ export function onBidiConnection(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): void {
65
+ try {
66
+ const initBiDiSocketFunc: OmitThisParameter<typeof initBidiSocket> = initBidiSocket.bind(this);
67
+ const {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr} = initBiDiSocketFunc(
68
+ ws,
69
+ req,
70
+ );
71
+
72
+ const initBidiSocketHandlersFunc: OmitThisParameter<typeof initBidiSocketHandlers> = initBidiSocketHandlers
73
+ .bind(this);
74
+ initBidiSocketHandlersFunc(
75
+ ws,
76
+ proxyClient,
77
+ send,
78
+ sendToProxy,
79
+ bidiHandlerDriver,
80
+ logSocketErr,
81
+ );
82
+ if (proxyClient) {
83
+ const initBidiProxyHandlersFunc: OmitThisParameter<typeof initBidiProxyHandlers> = initBidiProxyHandlers
84
+ .bind(bidiHandlerDriver);
85
+ initBidiProxyHandlersFunc(proxyClient, ws, send);
86
+ }
87
+ const initBidiEventListenersFunc: OmitThisParameter<typeof initBidiEventListeners> = initBidiEventListeners
88
+ .bind(this);
89
+ initBidiEventListenersFunc(ws, bidiHandlerDriver, send);
90
+ } catch (err) {
91
+ this.log.error(err);
92
+ try {
93
+ ws.close();
94
+ } catch {}
95
+ }
96
+ }
97
+
98
+ /**
99
+ * @param data
100
+ * @param driver
101
+ */
102
+ export async function onBidiMessage(
103
+ this: AppiumDriver,
104
+ data: Buffer,
105
+ driver: AnyDriver
106
+ ): Promise<SuccessBiDiCommandResponse | ErrorBiDiCommandResponse> {
107
+ let resMessage: SuccessBiDiCommandResponse | ErrorBiDiCommandResponse;
108
+ let id: number = 0;
109
+ const driverLog = driver.log;
110
+ const dataTruncated = _.truncate(data.toString(), {length: MAX_LOGGED_DATA_LENGTH});
111
+ try {
112
+ let method: string;
113
+ let params: StringRecord;
114
+ try {
115
+ ({id, method, params} = JSON.parse(data.toString('utf8')));
116
+ } catch (err) {
117
+ throw new errors.InvalidArgumentError(
118
+ `Could not parse Bidi command '${dataTruncated}': ${err.message}`,
119
+ );
120
+ }
121
+ driverLog.info(`--> BIDI message #${id}`);
122
+ if (!method) {
123
+ throw new errors.InvalidArgumentError(
124
+ `Missing method for BiDi operation in '${dataTruncated}'`,
125
+ );
126
+ }
127
+ if (!params) {
128
+ throw new errors.InvalidArgumentError(
129
+ `Missing params for BiDi operation in '${dataTruncated}`,
130
+ );
131
+ }
132
+ const result = await driver.executeBidiCommand(method, params);
133
+ resMessage = {
134
+ id,
135
+ type: 'success',
136
+ result,
137
+ };
138
+ } catch (err) {
139
+ resMessage = _.has(err, 'bidiErrObject')
140
+ ? err.bidiErrObject(id)
141
+ : {
142
+ id,
143
+ type: 'error',
144
+ error: errors.UnknownError.error(),
145
+ message: (err as Error).message,
146
+ stacktrace: (err as Error).stack,
147
+ };
148
+ }
149
+ driverLog.info(`<-- BIDI message #${id}`);
150
+ return resMessage;
151
+ }
152
+
153
+ /**
154
+ * Log a bidi server error
155
+ * @param err
156
+ */
157
+ export function onBidiServerError(this: AppiumDriver, err: Error): void {
158
+ this.log.warn(`Error from bidi websocket server: ${err}`);
159
+ }
160
+
161
+ /**
162
+ * Initialize a new bidi connection
163
+ * @param ws The websocket connection object
164
+ * @param req The connection pathname, which might include the session id
165
+ */
166
+ function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): InitBiDiSocketResult {
167
+ const pathname = req.url;
168
+ if (!pathname) {
169
+ throw new Error('Invalid connection request: pathname missing from request');
170
+ }
171
+ const bidiSessionRe = new RegExp(`${BIDI_BASE_PATH}/([^/]+)$`);
172
+ const bidiNoSessionRe = new RegExp(`${BIDI_BASE_PATH}/?$`);
173
+ const sessionMatch = bidiSessionRe.exec(pathname);
174
+ const noSessionMatch = bidiNoSessionRe.exec(pathname);
175
+
176
+ if (!sessionMatch && !noSessionMatch) {
177
+ throw new Error(
178
+ `Got websocket connection for path ${pathname} but didn't know what to do with it. ` +
179
+ `Ignoring and will close the connection`,
180
+ );
181
+ }
182
+
183
+ // Let's figure out which driver is going to handle this socket connection. It's either going
184
+ // to be a driver matching a session id appended to the bidi base path, or this umbrella driver
185
+ // (if no session id is included in the bidi connection request)
186
+
187
+ let bidiHandlerDriver: AnyDriver;
188
+ let proxyClient: WebSocket | null = null;
189
+ if (sessionMatch) {
190
+ // If we found a session id, see if it matches an active session
191
+ const sessionId = sessionMatch[1];
192
+ bidiHandlerDriver = this.sessions[sessionId];
193
+ if (!bidiHandlerDriver) {
194
+ // The session ID sent in doesn't match an active session; just ignore this socket
195
+ // connection in that case
196
+ throw new Error(
197
+ `Got bidi connection request for session with id ${sessionId} which is closed ` +
198
+ `or does not exist. Closing the socket connection.`,
199
+ );
200
+ }
201
+ const driverName = bidiHandlerDriver.constructor.name;
202
+ this.log.info(`Bidi websocket connection made for session ${sessionId}`);
203
+ // store this socket connection for later removal on session deletion. theoretically there
204
+ // can be multiple sockets per session
205
+ if (!this.bidiSockets[sessionId]) {
206
+ this.bidiSockets[sessionId] = [];
207
+ }
208
+ this.bidiSockets[sessionId].push(ws);
209
+
210
+ const bidiProxyUrl = bidiHandlerDriver.bidiProxyUrl;
211
+ if (bidiProxyUrl) {
212
+ try {
213
+ new URL(bidiProxyUrl);
214
+ } catch {
215
+ throw new Error(
216
+ `Got request for ${driverName} to proxy bidi connections to upstream socket with ` +
217
+ `url ${bidiProxyUrl}, but this was not a valid url`,
218
+ );
219
+ }
220
+ this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}`);
221
+ proxyClient = new WebSocket(bidiProxyUrl);
222
+ this.bidiProxyClients[sessionId] = proxyClient;
223
+ }
224
+ } else {
225
+ this.log.info('Bidi websocket connection made to main server');
226
+ // no need to store the socket connection if it's to the main server since it will just
227
+ // stay open as long as the server itself is and will close when the server closes.
228
+ bidiHandlerDriver = this; // eslint-disable-line @typescript-eslint/no-this-alias
229
+ }
230
+
231
+ const driverLog = bidiHandlerDriver.log;
232
+ const logSocketErr: LogSocketError = (err: Error) => {
233
+ driverLog.warn(err.message);
234
+ };
235
+
236
+ // This is a function which wraps the 'send' method on a web socket for two reasons:
237
+ // 1. Make it async-await friendly
238
+ // 2. Do some logging if there's a send error
239
+ const sendFactory = (socket: WebSocket) => {
240
+ const socketSend = B.promisify(socket.send, {context: socket});
241
+ return async (data: string | Buffer) => {
242
+ try {
243
+ await socketSend(data);
244
+ } catch (err) {
245
+ logSocketErr(err);
246
+ }
247
+ };
248
+ };
249
+
250
+ // Construct our send method for sending messages to the client
251
+ const send: SendData = sendFactory(ws);
252
+
253
+ // Construct a conditional send method for proxying messages from the client to an upstream
254
+ // bidi socket server (e.g. on a browser)
255
+ const sendToProxy: SendData | null = proxyClient ? sendFactory(proxyClient) : null;
256
+
257
+ return {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr};
258
+ }
259
+
260
+ /**
261
+ * Set up handlers on upstream bidi socket we are proxying to/from
262
+ *
263
+ * @param proxyClient - the websocket connection to/from the
264
+ * upstream socket (the one we're proxying to/from)
265
+ * @param ws - the websocket connection to/from the client
266
+ * @param send - a method used to send data to the
267
+ * client
268
+ */
269
+ function initBidiProxyHandlers(
270
+ this: AnyDriver,
271
+ proxyClient: WebSocket,
272
+ ws: WebSocket,
273
+ send: SendData,
274
+ ): void {
275
+ // Set up handlers for events that might come from the upstream bidi socket connection if
276
+ // we're in proxy mode
277
+ const driverLog = this.log;
278
+
279
+ // Here we're receiving a message from the upstream socket server. We want to pass it on to
280
+ // the client
281
+ proxyClient.on('message', send);
282
+
283
+ // If the upstream socket server closes the connection, should close the connection to the
284
+ // client as well
285
+ proxyClient.on('close', (code, reason) => {
286
+ driverLog.debug(
287
+ `Upstream bidi socket closed connection (code ${code}, reason: '${reason}'). ` +
288
+ `Closing proxy connection to client`,
289
+ );
290
+ const intCode: number = _.isNumber(code) ? (code as number) : parseInt(code, 10);
291
+ if (_.isNaN(intCode) || intCode < MIN_WS_CODE_VAL || intCode > MAX_WS_CODE_VAL) {
292
+ driverLog.warn(
293
+ `Received code ${code} from upstream socket, but this is not a valid ` +
294
+ `websocket code. Rewriting to ${WS_FALLBACK_CODE} for ws compatibility`,
295
+ );
296
+ code = WS_FALLBACK_CODE;
297
+ }
298
+ ws.close(code, reason);
299
+ });
300
+
301
+ proxyClient.on('error', (err) => {
302
+ driverLog.warn(`Got error on upstream bidi socket connection: ${err.message}`);
303
+ });
304
+ }
305
+
306
+ /**
307
+ * Set up handlers on the bidi socket connection to the client
308
+ *
309
+ * @param ws - the websocket connection to/from the client
310
+ * @param proxyClient - the websocket connection to/from the
311
+ * upstream socket (the one we're proxying to/from, if we're proxying)
312
+ * @param send - a method used to send data to the
313
+ * client
314
+ * @param sendToProxy - a method used to send data to the
315
+ * upstream socket
316
+ * @param bidiHandlerDriver - the driver
317
+ * handling the bidi commands
318
+ * @param logSocketErr - a special prefixed logger
319
+ */
320
+ function initBidiSocketHandlers(
321
+ this: AppiumDriver,
322
+ ws: WebSocket,
323
+ proxyClient: WebSocket | null,
324
+ send: SendData,
325
+ sendToProxy: SendData | null,
326
+ bidiHandlerDriver: AnyDriver,
327
+ logSocketErr: LogSocketError,
328
+ ): void {
329
+ const driverLog = bidiHandlerDriver.log;
330
+ // Can't do much with random errors on the connection other than log them
331
+ ws.on('error', logSocketErr);
332
+
333
+ ws.on('open', () => {
334
+ driverLog.info('BiDi websocket connection is now open');
335
+ });
336
+
337
+ // Now set up handlers for the various events that might happen on the websocket connection
338
+ // coming from the client
339
+ // First is incoming messages from the client
340
+ ws.on('message', async (data: Buffer) => {
341
+ if (proxyClient && sendToProxy) {
342
+ // if we're meant to proxy to an upstream bidi socket, just do that
343
+ await sendToProxy(data.toString('utf8'));
344
+ } else {
345
+ const res = await this.onBidiMessage(data, bidiHandlerDriver);
346
+ await send(JSON.stringify(res));
347
+ }
348
+ });
349
+
350
+ // Next consider if the client closes the socket connection on us
351
+ ws.on('close', (code, reason) => {
352
+ // Not sure if we need to do anything here if the client closes the websocket connection.
353
+ // Probably if a session was started via the socket, and the socket closes, we should end the
354
+ // associated session to free up resources. But otherwise, for sockets attached to existing
355
+ // sessions, doing nothing is probably right.
356
+ driverLog.debug(`BiDi socket connection closed (code ${code}, reason: '${reason}')`);
357
+
358
+ // If we're proxying, might as well close the upstream connection and clean it up
359
+ if (proxyClient) {
360
+ driverLog.debug('Also closing BiDi proxy socket connection');
361
+ proxyClient.close(code, reason);
362
+ }
363
+
364
+ const eventLogCounts = BIDI_EVENTS_MAP.get(bidiHandlerDriver);
365
+ if (!_.isEmpty(eventLogCounts)) {
366
+ driverLog.debug(`BiDi events statistics: ${JSON.stringify(eventLogCounts, null, 2)}`);
367
+ }
368
+ });
369
+ }
370
+
371
+ /**
372
+ * Set up bidi event listeners
373
+ *
374
+ * @param ws - the websocket connection to/from the client
375
+ * @param bidiHandlerDriver - the driver
376
+ * handling the bidi commands
377
+ * @param send - a method used to send data to the
378
+ * client
379
+ */
380
+ function initBidiEventListeners(
381
+ this: AppiumDriver,
382
+ ws: WebSocket,
383
+ bidiHandlerDriver: AnyDriver,
384
+ send: SendData,
385
+ ): void {
386
+ // If the driver emits a bidi event that should maybe get sent to the client, check to make
387
+ // sure the client is subscribed and then pass it on
388
+ const driverLog = bidiHandlerDriver.log;
389
+ const driverEe = bidiHandlerDriver.eventEmitter;
390
+ const eventLogCounts: Record<string, number> = BIDI_EVENTS_MAP.get(bidiHandlerDriver) ?? {};
391
+ BIDI_EVENTS_MAP.set(bidiHandlerDriver, eventLogCounts);
392
+ const eventListener = async ({context, method, params = {}}) => {
393
+ // if the driver didn't specify a context, use the empty context
394
+ if (!context) {
395
+ context = '';
396
+ }
397
+ if (!method || !params) {
398
+ driverLog.warn(
399
+ `Driver emitted a bidi event that was malformed. Require method and params keys ` +
400
+ `(with optional context). But instead received: ${_.truncate(JSON.stringify({
401
+ context,
402
+ method,
403
+ params,
404
+ }), {length: MAX_LOGGED_DATA_LENGTH})}`,
405
+ );
406
+ return;
407
+ }
408
+ if (ws.readyState !== WebSocket.OPEN) {
409
+ // if the websocket is not still 'open', then we can ignore sending these events
410
+ if (ws.readyState > WebSocket.OPEN) {
411
+ // if the websocket is closed or closing, we can remove this listener as well to avoid
412
+ // leaks
413
+ driverEe.removeListener(BIDI_EVENT_NAME, eventListener);
414
+ }
415
+ return;
416
+ }
417
+
418
+ const eventSubs = bidiHandlerDriver.bidiEventSubs[method];
419
+ if (_.isArray(eventSubs) && eventSubs.includes(context)) {
420
+ if (method in eventLogCounts) {
421
+ ++eventLogCounts[method];
422
+ } else {
423
+ driverLog.info(
424
+ `<-- BIDI EVENT ${method} (context: '${context}', ` +
425
+ `params: ${_.truncate(JSON.stringify(params), {length: MAX_LOGGED_DATA_LENGTH})}). ` +
426
+ `All further similar events won't be logged.`,
427
+ );
428
+ eventLogCounts[method] = 1;
429
+ }
430
+ // now we can send the event onto the socket
431
+ const ev = {type: 'event', context, method, params};
432
+ await send(JSON.stringify(ev));
433
+ }
434
+ };
435
+ driverEe.on(BIDI_EVENT_NAME, eventListener);
436
+ }
package/lib/cli/args.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  import {INSTALL_TYPES} from '../extension/extension-config';
13
13
  import {toParserArgs} from '../schema/cli-args';
14
14
  const DRIVER_EXAMPLE = 'xcuitest';
15
- const PLUGIN_EXAMPLE = 'find_by_image';
15
+ const PLUGIN_EXAMPLE = 'images';
16
16
 
17
17
  /**
18
18
  * This is necessary because we pass the array into `argparse`. `argparse` is bad and mutates things. We don't want that.
@@ -32,7 +32,7 @@ const globalExtensionArgs = new Map([
32
32
  required: false,
33
33
  default: false,
34
34
  action: 'store_true',
35
- help: 'Use JSON for output format',
35
+ help: 'Return output in JSON format',
36
36
  dest: 'json',
37
37
  },
38
38
  ],
@@ -82,7 +82,7 @@ function makeListArgs(type) {
82
82
  required: false,
83
83
  default: false,
84
84
  action: 'store_true',
85
- help: 'Show information about newer versions',
85
+ help: `Show information about available ${type} updates`,
86
86
  dest: 'showUpdates',
87
87
  },
88
88
  ],
@@ -92,7 +92,7 @@ function makeListArgs(type) {
92
92
  required: false,
93
93
  default: false,
94
94
  action: 'store_true',
95
- help: 'Show more information about each extension',
95
+ help: `Show more information about each ${type}`,
96
96
  dest: 'verbose',
97
97
  },
98
98
  ],
@@ -112,9 +112,8 @@ function makeInstallArgs(type) {
112
112
  {
113
113
  type: 'str',
114
114
  help:
115
- `Name of the ${type} to install, for example: ` + type === DRIVER_TYPE
116
- ? DRIVER_EXAMPLE
117
- : PLUGIN_EXAMPLE,
115
+ `Name of the ${type} to install, for example: ` +
116
+ (type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
118
117
  },
119
118
  ],
120
119
  [
@@ -124,7 +123,7 @@ function makeInstallArgs(type) {
124
123
  default: null,
125
124
  choices: INSTALL_TYPES_ARRAY,
126
125
  help:
127
- `Where to look for the ${type} if it is not one of Appium's verified ` +
126
+ `Where to look for the ${type} if it is not one of Appium's official ` +
128
127
  `${type}s. Possible values: ${INSTALL_TYPES_ARRAY.join(', ')}`,
129
128
  dest: 'installType',
130
129
  },
@@ -136,9 +135,8 @@ function makeInstallArgs(type) {
136
135
  default: null,
137
136
  type: 'str',
138
137
  help:
139
- `If installing from Git or GitHub, the package name, as defined in the plugin's ` +
140
- `package.json file in the "name" field, cannot be determined automatically, and ` +
141
- `should be reported here, otherwise the install will probably fail.`,
138
+ `The Node.js package name of the ${type}. ` +
139
+ `Required if "source" is set to "git" or "github".`,
142
140
  dest: 'packageName',
143
141
  },
144
142
  ],
@@ -158,9 +156,8 @@ function makeUninstallArgs(type) {
158
156
  {
159
157
  type: 'str',
160
158
  help:
161
- `Name of the ${type} to uninstall, for example: ` + type === DRIVER_TYPE
162
- ? DRIVER_EXAMPLE
163
- : PLUGIN_EXAMPLE,
159
+ `Name of the ${type} to uninstall, for example: ` +
160
+ (type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
164
161
  },
165
162
  ],
166
163
  ]);
@@ -179,9 +176,8 @@ function makeDoctorArgs(type) {
179
176
  {
180
177
  type: 'str',
181
178
  help:
182
- `Name of the ${type} to run doctor checks for, for example: ` + type === DRIVER_TYPE
183
- ? DRIVER_EXAMPLE
184
- : PLUGIN_EXAMPLE,
179
+ `Name of the ${type} to run doctor checks for, for example: ` +
180
+ (type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
185
181
  },
186
182
  ],
187
183
  ]);
@@ -200,13 +196,10 @@ function makeUpdateArgs(type) {
200
196
  {
201
197
  type: 'str',
202
198
  help:
203
- `Name of the ${type} to update, or the word "installed" to update all installed ` +
204
- `${type}s. To see available updates, run "appium ${type} list --installed --updates". ` +
205
- 'For example: ' +
206
- type ===
207
- DRIVER_TYPE
208
- ? DRIVER_EXAMPLE
209
- : PLUGIN_EXAMPLE,
199
+ `Name of the ${type} to update, or "installed" to update all installed ${type}s. ` +
200
+ `To see available ${type} updates, run "appium ${type} list --installed --updates". ` +
201
+ 'For example: ' +
202
+ (type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
210
203
  },
211
204
  ],
212
205
  [
@@ -216,8 +209,7 @@ function makeUpdateArgs(type) {
216
209
  default: false,
217
210
  action: 'store_true',
218
211
  help:
219
- `Include updates that might have a new major revision, and potentially include ` +
220
- `breaking changes`,
212
+ 'Include any available major revision updates, which may have breaking changes',
221
213
  },
222
214
  ],
223
215
  ]);
@@ -236,9 +228,8 @@ function makeRunArgs(type) {
236
228
  {
237
229
  type: 'str',
238
230
  help:
239
- `Name of the ${type} to run a script from, for example: ` + type === DRIVER_TYPE
240
- ? DRIVER_EXAMPLE
241
- : PLUGIN_EXAMPLE,
231
+ `Name of the ${type} to run a script from, for example: ` +
232
+ (type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
242
233
  },
243
234
  ],
244
235
  [
@@ -248,9 +239,8 @@ function makeRunArgs(type) {
248
239
  nargs: '?',
249
240
  type: 'str',
250
241
  help:
251
- `Name of the script to run from the ${type}. The script name must be a key ` +
252
- `inside the "appium.scripts" field inside the ${type}'s "package.json" file. ` +
253
- `If not provided then all available script names are going to be listed.`,
242
+ `Name of the ${type} script to run. If not provided, return a list ` +
243
+ `of available scripts for this ${type}.`,
254
244
  },
255
245
  ],
256
246
  ]);
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-console */
2
1
  import B from 'bluebird';
3
2
  import _ from 'lodash';
4
3
  import path from 'path';
@@ -18,7 +17,7 @@ import {inspect} from 'node:util';
18
17
  import {pathToFileURL} from 'url';
19
18
  import {Doctor, EXIT_CODE as DOCTOR_EXIT_CODE} from '../doctor/doctor';
20
19
  import {npmPackage} from '../utils';
21
- import semver from 'semver';
20
+ import * as semver from 'semver';
22
21
 
23
22
  const UPDATE_ALL = 'installed';
24
23
 
@@ -486,10 +485,11 @@ class ExtensionCliCommand {
486
485
  getInstallationReceipt({pkg, installPath, installType, installSpec}) {
487
486
  const {appium, name, version, peerDependencies} = pkg;
488
487
 
488
+ const strVersion = /** @type {string} */ (version);
489
489
  /** @type {import('appium/types').InternalMetadata} */
490
490
  const internal = {
491
- pkgName: name,
492
- version,
491
+ pkgName: /** @type {string} */ (name),
492
+ version: strVersion,
493
493
  installType,
494
494
  installSpec,
495
495
  installPath,