appium 2.0.0-beta.2 → 2.0.0-beta.20

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 (70) hide show
  1. package/README.md +9 -9
  2. package/build/lib/appium-config.schema.json +0 -0
  3. package/build/lib/appium.js +157 -53
  4. package/build/lib/cli/argparse-actions.js +104 -0
  5. package/build/lib/cli/args.js +115 -279
  6. package/build/lib/cli/driver-command.js +11 -1
  7. package/build/lib/cli/extension-command.js +60 -8
  8. package/build/lib/cli/extension.js +30 -7
  9. package/build/lib/cli/npm.js +17 -14
  10. package/build/lib/cli/parser.js +152 -89
  11. package/build/lib/cli/plugin-command.js +11 -1
  12. package/build/lib/cli/utils.js +29 -3
  13. package/build/lib/config-file.js +141 -0
  14. package/build/lib/config.js +76 -61
  15. package/build/lib/driver-config.js +42 -19
  16. package/build/lib/drivers.js +8 -4
  17. package/build/lib/ext-config-io.js +165 -0
  18. package/build/lib/extension-config.js +130 -61
  19. package/build/lib/grid-register.js +22 -24
  20. package/build/lib/logger.js +3 -3
  21. package/build/lib/logsink.js +11 -13
  22. package/build/lib/main.js +197 -77
  23. package/build/lib/plugin-config.js +20 -10
  24. package/build/lib/plugins.js +4 -2
  25. package/build/lib/schema/appium-config-schema.js +252 -0
  26. package/build/lib/schema/arg-spec.js +120 -0
  27. package/build/lib/schema/cli-args.js +173 -0
  28. package/build/lib/schema/cli-transformers.js +76 -0
  29. package/build/lib/schema/index.js +36 -0
  30. package/build/lib/schema/keywords.js +62 -0
  31. package/build/lib/schema/schema.js +357 -0
  32. package/build/lib/utils.js +44 -99
  33. package/lib/appium-config.schema.json +277 -0
  34. package/lib/appium.js +201 -65
  35. package/lib/cli/argparse-actions.js +77 -0
  36. package/lib/cli/args.js +174 -375
  37. package/lib/cli/driver-command.js +4 -0
  38. package/lib/cli/extension-command.js +70 -5
  39. package/lib/cli/extension.js +25 -5
  40. package/lib/cli/npm.js +18 -12
  41. package/lib/cli/parser.js +254 -79
  42. package/lib/cli/plugin-command.js +4 -0
  43. package/lib/cli/utils.js +21 -1
  44. package/lib/config-file.js +227 -0
  45. package/lib/config.js +109 -62
  46. package/lib/driver-config.js +66 -11
  47. package/lib/drivers.js +4 -1
  48. package/lib/ext-config-io.js +287 -0
  49. package/lib/extension-config.js +225 -67
  50. package/lib/grid-register.js +27 -24
  51. package/lib/logger.js +1 -1
  52. package/lib/logsink.js +10 -7
  53. package/lib/main.js +211 -77
  54. package/lib/plugin-config.js +34 -5
  55. package/lib/plugins.js +1 -0
  56. package/lib/schema/appium-config-schema.js +286 -0
  57. package/lib/schema/arg-spec.js +218 -0
  58. package/lib/schema/cli-args.js +273 -0
  59. package/lib/schema/cli-transformers.js +123 -0
  60. package/lib/schema/index.js +2 -0
  61. package/lib/schema/keywords.js +119 -0
  62. package/lib/schema/schema.js +577 -0
  63. package/lib/utils.js +42 -88
  64. package/package.json +55 -80
  65. package/postinstall.js +71 -0
  66. package/types/appium-config.d.ts +197 -0
  67. package/types/types.d.ts +201 -0
  68. package/CHANGELOG.md +0 -3515
  69. package/build/lib/cli/parser-helpers.js +0 -82
  70. package/lib/cli/parser-helpers.js +0 -79
@@ -3,8 +3,9 @@
3
3
  import _ from 'lodash';
4
4
  import NPM from './npm';
5
5
  import path from 'path';
6
- import { fs, util } from 'appium-support';
7
- import { log, spinWith } from './utils';
6
+ import { fs, util } from '@appium/support';
7
+ import { log, spinWith, RingBuffer } from './utils';
8
+ import { SubProcess} from 'teen_process';
8
9
  import { INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB,
9
10
  INSTALL_TYPE_LOCAL } from '../extension-config';
10
11
 
@@ -482,8 +483,72 @@ export default class ExtensionCommand {
482
483
  */
483
484
  async updateExtension (ext, version) {
484
485
  const {pkgName} = this.config.installedExtensions[ext];
485
- await this.installViaNpm({ext, pkgName, pkgVer: version});
486
- this.config.installedExtensions[ext].version = version;
487
- await this.config.write();
486
+ await fs.rimraf(this.config.getInstallPath(ext));
487
+ const extData = await this.installViaNpm({ext, pkgName, pkgVer: version});
488
+ delete extData[`${this.type}Name`];
489
+ await this.config.updateExtension(ext, extData);
490
+ }
491
+
492
+ /**
493
+ * Runs a script cached inside the "scripts" field under "appium"
494
+ * inside of the driver/plugins "package.json" file. Will throw
495
+ * an error if the driver/plugin does not contain a "scripts" field
496
+ * underneath the "appium" field in its package.json, if the
497
+ * "scripts" field is not a plain object, or if the scriptName is
498
+ * not found within "scripts" object.
499
+ *
500
+ * @param {string} ext - name of the extension to run a script from
501
+ * @param {string} scriptName - name of the script to run
502
+ * @return {RunOutput}
503
+ */
504
+ async run ({ext, scriptName}) {
505
+ if (!_.has(this.config.installedExtensions, ext)) {
506
+ throw new Error(`please install the ${this.type} first`);
507
+ }
508
+
509
+ const extConfig = this.config.installedExtensions[ext];
510
+
511
+ if (!_.has(extConfig, 'scripts')) {
512
+ throw new Error(`The ${this.type} named '${ext}' does not contain the ` +
513
+ `"scripts" field underneath the "appium" field in its package.json`);
514
+ }
515
+
516
+ const extScripts = extConfig.scripts;
517
+
518
+ if (!_.isPlainObject(extScripts)) {
519
+ throw new Error(`The ${this.type} named '${ext}' "scripts" field must be a plain object`);
520
+ }
521
+
522
+ if (!_.has(extScripts, scriptName)) {
523
+ throw new Error(`The ${this.type} named '${ext}' does not support the script: '${scriptName}'`);
524
+ }
525
+
526
+ const runner = new SubProcess(process.execPath, [extScripts[scriptName]], {
527
+ cwd: this.config.getExtensionRequirePath(ext)
528
+ });
529
+
530
+ const output = new RingBuffer(50);
531
+
532
+ runner.on('stream-line', (line) => {
533
+ output.enqueue(line);
534
+ log(this.isJsonOutput, line);
535
+ });
536
+
537
+ await runner.start(0);
538
+
539
+ try {
540
+ await runner.join();
541
+ log(this.isJsonOutput, `${scriptName} successfully ran`.green);
542
+ return {output: output.getBuff()};
543
+ } catch (err) {
544
+ log(this.isJsonOutput, `Encountered an error when running '${scriptName}': ${err.message}`.red);
545
+ return {error: err.message, output: output.getBuff()};
546
+ }
488
547
  }
489
548
  }
549
+
550
+ /**
551
+ * @typedef {Object} RunOutput
552
+ * @property {string|undefined} error - error message if script ran unsuccessfully, otherwise undefined
553
+ * @property {string[]} output - script output
554
+ */
@@ -6,6 +6,7 @@ import DriverConfig from '../driver-config';
6
6
  import PluginConfig from '../plugin-config';
7
7
  import { DRIVER_TYPE } from '../extension-config';
8
8
  import { errAndQuit, log, JSON_SPACES } from './utils';
9
+ import { APPIUM_HOME } from './args';
9
10
 
10
11
  /**
11
12
  * Run a subcommand of the 'appium driver' type. Each subcommand has its own set of arguments which
@@ -13,25 +14,44 @@ import { errAndQuit, log, JSON_SPACES } from './utils';
13
14
  *
14
15
  * @param {Object} args - JS object where the key is the parameter name (as defined in
15
16
  * driver-parser.js)
17
+ * @param {import('../ext-config-io').ExtensionType} type - Extension type
18
+ * @oaram {ExtensionConfig} [configObject] - Extension config object, if we have one
16
19
  */
17
- async function runExtensionCommand (args, type) {
20
+ async function runExtensionCommand (args, type, configObject) {
18
21
  // TODO driver config file should be locked while any of these commands are
19
22
  // running to prevent weird situations
20
23
  let jsonResult = null;
21
- const {json, appiumHome} = args;
24
+ const extCmd = args[`${type}Command`];
25
+ if (!extCmd) {
26
+ throw new TypeError(`Cannot call ${type} command without a subcommand like 'install'`);
27
+ }
28
+ let {json, suppressOutput} = args;
29
+ if (suppressOutput) {
30
+ json = true;
31
+ }
22
32
  const logFn = (msg) => log(json, msg);
23
- const ConfigClass = type === DRIVER_TYPE ? DriverConfig : PluginConfig;
33
+ let config;
34
+ if (configObject) {
35
+ config = configObject;
36
+ config.log = logFn;
37
+ } else {
38
+ config = (type === DRIVER_TYPE ? DriverConfig : PluginConfig).getInstance(APPIUM_HOME, logFn);
39
+ }
24
40
  const CommandClass = type === DRIVER_TYPE ? DriverCommand : PluginCommand;
25
- const config = new ConfigClass(appiumHome, logFn);
26
41
  const cmd = new CommandClass({config, json});
27
42
  try {
28
43
  await config.read();
29
44
  jsonResult = await cmd.execute(args);
30
45
  } catch (err) {
46
+ // in the suppress output case, we are calling this function internally and should
47
+ // just throw instead of printing an error and ending the process
48
+ if (suppressOutput) {
49
+ throw err;
50
+ }
31
51
  errAndQuit(json, err);
32
52
  }
33
53
 
34
- if (json) {
54
+ if (json && !suppressOutput) {
35
55
  console.log(JSON.stringify(jsonResult, null, JSON_SPACES));
36
56
  }
37
57
 
package/lib/cli/npm.js CHANGED
@@ -1,8 +1,7 @@
1
- import _ from 'lodash';
2
1
  import path from 'path';
3
2
  import semver from 'semver';
4
3
  import { exec } from 'teen_process';
5
- import { mkdirp, util } from 'appium-support';
4
+ import { fs, mkdirp, util, system } from '@appium/support';
6
5
 
7
6
  const INSTALL_LOCKFILE = '.appium.install.lock';
8
7
  const LINK_LOCKFILE = '.appium.link.lock';
@@ -21,6 +20,14 @@ export default class NPM {
21
20
  // ensure the directory we want to install inside of exists
22
21
  await mkdirp(cwd);
23
22
 
23
+ // not only this, this directory needs a 'package.json' inside of it, otherwise, if any
24
+ // directory in the filesystem tree ABOVE cwd happens to have a package.json or a node_modules
25
+ // dir in it, NPM will install the module up there instead (silly NPM)
26
+ const dummyPkgJson = path.resolve(cwd, 'package.json');
27
+ if (!await fs.exists(dummyPkgJson)) {
28
+ await fs.writeFile(dummyPkgJson, '{}');
29
+ }
30
+
24
31
  // make sure we perform the current operation in cwd
25
32
  execOpts = {...execOpts, cwd};
26
33
 
@@ -28,7 +35,8 @@ export default class NPM {
28
35
  if (json) {
29
36
  args.push('-json');
30
37
  }
31
- let runner = async () => await exec('npm', args, execOpts);
38
+ const npmCmd = system.isWindows() ? 'npm.cmd' : 'npm';
39
+ let runner = async () => await exec(npmCmd, args, execOpts);
32
40
  if (lockFile) {
33
41
  const acquireLock = util.getLockFileGuard(path.resolve(cwd, lockFile));
34
42
  const _runner = runner;
@@ -124,14 +132,6 @@ export default class NPM {
124
132
  if (res.json.error) {
125
133
  throw new Error(res.json.error);
126
134
  }
127
-
128
- // if we tried to install a package via git/github, we might not have been
129
- // certain of the package name, so if we can retrieve it unambiguously from
130
- // the json report of the install, do so
131
- const names = _.uniq([...res.json.added, ...res.json.updated].map((x) => x.name));
132
- if (names.length === 1) {
133
- pkgName = names[0];
134
- }
135
135
  }
136
136
 
137
137
  // Now read package data from the installed package to return, and make sure
@@ -159,8 +159,14 @@ export default class NPM {
159
159
  `provided: ${pkgPath}`);
160
160
  }
161
161
 
162
+ // this is added to handle commands with relative paths
163
+ // ie: "node . driver install --source=local ../fake-driver"
164
+ pkgPath = path.resolve(process.cwd(), pkgPath);
165
+
162
166
  const pkgHome = path.resolve(this.appiumHome, pkgName);
163
- const res = await this.exec('link', [pkgPath], {cwd: pkgHome, lockFile: LINK_LOCKFILE});
167
+
168
+ // call link with --no-package-lock to ensure no corruption while installing local packages
169
+ const res = await this.exec('link', ['--no-package-lock', pkgPath], {cwd: pkgHome, lockFile: LINK_LOCKFILE});
164
170
  if (res.json && res.json.error) {
165
171
  throw new Error(res.json.error);
166
172
  }
package/lib/cli/parser.js CHANGED
@@ -1,98 +1,273 @@
1
- import path from 'path';
2
- import _ from 'lodash';
1
+ // @ts-check
2
+
3
+ import { fs } from '@appium/support';
3
4
  import { ArgumentParser } from 'argparse';
4
- import { sharedArgs, serverArgs, extensionArgs } from './args';
5
+ import B from 'bluebird';
6
+ import _ from 'lodash';
7
+ import path from 'path';
5
8
  import { DRIVER_TYPE, PLUGIN_TYPE } from '../extension-config';
9
+ import { finalizeSchema, getArgSpec, hasArgSpec } from '../schema';
6
10
  import { rootDir } from '../utils';
11
+ import {
12
+ driverConfig,
13
+ getExtensionArgs,
14
+ getServerArgs,
15
+ pluginConfig
16
+ } from './args';
7
17
 
8
- function getParser (debug = false) {
9
- const parser = new ArgumentParser({
10
- version: require(path.resolve(rootDir, 'package.json')).version,
11
- addHelp: true,
12
- description: 'A webdriver-compatible server for use with native and hybrid iOS and Android applications.',
13
- prog: process.argv[1] ? path.basename(process.argv[1]) : 'appium',
14
- debug
15
- });
16
- const subParsers = parser.addSubparsers({dest: 'subcommand', debug});
17
-
18
- // add the 'server' subcommand, and store the raw arguments on the parser
19
- // object as a way for other parts of the code to work with the arguments
20
- // conceptually rather than just through argparse
21
- const serverArgs = addServerToParser(sharedArgs, subParsers, debug);
22
- parser.rawArgs = serverArgs;
23
-
24
- // add the 'driver' and 'plugin' subcommands
25
- addExtensionsToParser(sharedArgs, subParsers, debug);
26
-
27
- // modify the parseArgs function to insert the 'server' subcommand if the
28
- // user hasn't specified a subcommand or the global help command
29
- parser._parseArgs = parser.parseArgs;
30
- parser.parseArgs = function (args, namespace) {
31
- if (_.isUndefined(args)) {
32
- args = [...process.argv.slice(2)];
33
- }
34
- if (!_.includes([DRIVER_TYPE, PLUGIN_TYPE, 'server', '-h'], args[0])) {
35
- args.splice(0, 0, 'server');
18
+ export const SERVER_SUBCOMMAND = 'server';
19
+
20
+ /**
21
+ * If the parsed args do not contain any of these values, then we
22
+ * will automatially inject the `server` subcommand.
23
+ */
24
+ const NON_SERVER_ARGS = Object.freeze(
25
+ new Set([
26
+ DRIVER_TYPE,
27
+ PLUGIN_TYPE,
28
+ SERVER_SUBCOMMAND,
29
+ '-h',
30
+ '--help',
31
+ '-v',
32
+ '--version'
33
+ ])
34
+ );
35
+
36
+ const version = fs.readPackageJsonFrom(rootDir).version;
37
+
38
+ /**
39
+ * A wrapper around `argparse`
40
+ *
41
+ * - Handles instantiation, configuration, and monkeypatching of an
42
+ * `ArgumentParser` instance for Appium server and its extensions
43
+ * - Handles error conditions, messages, and exit behavior
44
+ */
45
+ class ArgParser {
46
+ /**
47
+ * @param {boolean} [debug] - If true, throw instead of exit on error.
48
+ */
49
+ constructor (debug = false) {
50
+ const prog = process.argv[1] ? path.basename(process.argv[1]) : 'appium';
51
+ const parser = new ArgumentParser({
52
+ add_help: true,
53
+ description:
54
+ 'A webdriver-compatible server that facilitates automation of web, mobile, and other types of apps across various platforms.',
55
+ prog,
56
+ });
57
+
58
+ ArgParser._patchExit(parser);
59
+
60
+ /**
61
+ * Program name (typically `appium`)
62
+ * @type {string}
63
+ */
64
+ this.prog = prog;
65
+
66
+ /**
67
+ * If `true`, throw an error on parse failure instead of printing help
68
+ * @type {boolean}
69
+ */
70
+ this.debug = debug;
71
+
72
+ /**
73
+ * Wrapped `ArgumentParser` instance
74
+ * @type {ArgumentParser}
75
+ */
76
+ this.parser = parser;
77
+
78
+ parser.add_argument('-v', '--version', {
79
+ action: 'version',
80
+ version,
81
+ });
82
+
83
+ const subParsers = parser.add_subparsers({dest: 'subcommand'});
84
+
85
+ // add the 'server' subcommand, and store the raw arguments on the parser
86
+ // object as a way for other parts of the code to work with the arguments
87
+ // conceptually rather than just through argparse
88
+ const serverArgs = ArgParser._addServerToParser(subParsers);
89
+
90
+ this.rawArgs = serverArgs;
91
+
92
+ // add the 'driver' and 'plugin' subcommands
93
+ ArgParser._addExtensionCommandsToParser(subParsers);
94
+
95
+ // backwards compatibility / drop-in wrapper
96
+ /**
97
+ * @type {ArgParser['parseArgs']}
98
+ */
99
+ this.parse_args = this.parseArgs;
100
+ }
101
+
102
+ /**
103
+ * Parse arguments from the command line.
104
+ *
105
+ * If no subcommand is passed in, this method will inject the `server` subcommand.
106
+ *
107
+ * `ArgParser.prototype.parse_args` is an alias of this method.
108
+ * @param {string[]} [args] - Array of arguments, ostensibly from `process.argv`. Gathers args from `process.argv` if not provided.
109
+ * @returns {import('../../types/types').ParsedArgs} - The parsed arguments
110
+ */
111
+ parseArgs (args = process.argv.slice(2)) {
112
+ if (!NON_SERVER_ARGS.has(args[0])) {
113
+ args.unshift(SERVER_SUBCOMMAND);
36
114
  }
37
- return this._parseArgs(args, namespace);
38
- }.bind(parser);
39
- return parser;
40
- }
41
115
 
42
- function addServerToParser (sharedArgs, subParsers, debug) {
43
- const serverParser = subParsers.addParser('server', {
44
- addHelp: true,
45
- help: 'Run an Appium server',
46
- debug
47
- });
116
+ try {
117
+ const parsed = this.parser.parse_args(args);
118
+ return ArgParser._transformParsedArgs(parsed);
119
+ } catch (err) {
120
+ if (this.debug) {
121
+ throw err;
122
+ }
123
+ // this isn't tested via unit tests (we use `debug: true`) so may escape coverage.
48
124
 
49
- for (const [flags, opts] of [...sharedArgs, ...serverArgs]) {
50
- // addArgument mutates arguments so make copies
51
- serverParser.addArgument([...flags], {...opts});
125
+ /* istanbul ignore next */
126
+ {
127
+ // eslint-disable-next-line no-console
128
+ console.error(); // need an extra space since argparse prints usage.
129
+ // eslint-disable-next-line no-console
130
+ console.error(err.message);
131
+ process.exit(1);
132
+ }
133
+ }
52
134
  }
53
135
 
54
- return serverArgs;
55
- }
136
+ /**
137
+ * Given an object full of arguments as returned by `argparser.parse_args`,
138
+ * expand the ones for extensions into a nested object structure and rename
139
+ * keys to match the intended destination.
140
+ *
141
+ * E.g., `{'driver-foo-bar': baz}` becomes `{driver: {foo: {bar: 'baz'}}}`
142
+ * @param {object} args
143
+ * @returns {object}
144
+ */
145
+ static _transformParsedArgs (args) {
146
+ return _.reduce(
147
+ args,
148
+ (unpacked, value, key) => {
149
+ if (hasArgSpec(key)) {
150
+ const {dest} = /** @type {import('../schema/arg-spec').ArgSpec} */(getArgSpec(key));
151
+ _.set(unpacked, dest, value);
152
+ } else {
153
+ // this could be anything that _isn't_ a server arg
154
+ unpacked[key] = value;
155
+ }
156
+ return unpacked;
157
+ },
158
+ {},
159
+ );
160
+ }
56
161
 
57
- function getDefaultServerArgs () {
58
- let defaults = {};
59
- for (let [, arg] of serverArgs) {
60
- defaults[arg.dest] = arg.defaultValue;
162
+ /**
163
+ * Patches the `exit()` method of the parser to throw an error, so we can handle it manually.
164
+ * @param {ArgumentParser} parser
165
+ */
166
+ static _patchExit (parser) {
167
+ parser.exit = (code, msg) => {
168
+ throw new Error(msg);
169
+ };
61
170
  }
62
- return defaults;
63
- }
64
171
 
65
- function addExtensionsToParser (sharedArgs, subParsers, debug) {
66
- for (const type of [DRIVER_TYPE, PLUGIN_TYPE]) {
67
- const extParser = subParsers.addParser(type, {
68
- addHelp: true,
69
- help: `Access the ${type} management CLI commands`,
70
- debug
71
- });
72
- const extSubParsers = extParser.addSubparsers({
73
- dest: `${type}Command`,
74
- debug
172
+ /**
173
+ *
174
+ * @param {import('argparse').SubParser} subParser
175
+ * @returns {import('./args').ArgumentDefinitions}
176
+ */
177
+ static _addServerToParser (subParser) {
178
+ const serverParser = subParser.add_parser('server', {
179
+ add_help: true,
180
+ help: 'Run an Appium server',
75
181
  });
76
- const parserSpecs = [
77
- {command: 'list', args: extensionArgs[type].list,
78
- help: `List available and installed ${type}s`},
79
- {command: 'install', args: extensionArgs[type].install,
80
- help: `Install a ${type}`},
81
- {command: 'uninstall', args: extensionArgs[type].uninstall,
82
- help: `Uninstall a ${type}`},
83
- {command: 'update', args: extensionArgs[type].update,
84
- help: `Update installed ${type}s to the latest version`},
85
- ];
86
-
87
- for (const {command, args, help} of parserSpecs) {
88
- const parser = extSubParsers.addParser(command, {help, debug});
89
- for (const [flags, opts] of [...sharedArgs, ...args]) {
90
- // addArgument mutates params so make sure to send in copies instead
91
- parser.addArgument([...flags], {...opts});
182
+
183
+ ArgParser._patchExit(serverParser);
184
+
185
+ const serverArgs = getServerArgs();
186
+ for (const [flagsOrNames, opts] of serverArgs) {
187
+ // TS doesn't like the spread operator here.
188
+ // @ts-ignore
189
+ serverParser.add_argument(...flagsOrNames, {...opts});
190
+ }
191
+
192
+ return serverArgs;
193
+ }
194
+
195
+ /**
196
+ * Adds extension sub-sub-commands to `driver`/`plugin` subcommands
197
+ * @param {import('argparse').SubParser} subParsers
198
+ */
199
+ static _addExtensionCommandsToParser (subParsers) {
200
+ for (const type of [DRIVER_TYPE, PLUGIN_TYPE]) {
201
+ const extParser = subParsers.add_parser(type, {
202
+ add_help: true,
203
+ help: `Access the ${type} management CLI commands`,
204
+ });
205
+
206
+ ArgParser._patchExit(extParser);
207
+
208
+ const extSubParsers = extParser.add_subparsers({
209
+ dest: `${type}Command`,
210
+ });
211
+ const extensionArgs = getExtensionArgs();
212
+ const parserSpecs = [
213
+ {
214
+ command: 'list',
215
+ args: extensionArgs[type].list,
216
+ help: `List available and installed ${type}s`,
217
+ },
218
+ {
219
+ command: 'install',
220
+ args: extensionArgs[type].install,
221
+ help: `Install a ${type}`,
222
+ },
223
+ {
224
+ command: 'uninstall',
225
+ args: extensionArgs[type].uninstall,
226
+ help: `Uninstall a ${type}`,
227
+ },
228
+ {
229
+ command: 'update',
230
+ args: extensionArgs[type].update,
231
+ help: `Update installed ${type}s to the latest version`,
232
+ },
233
+ {
234
+ command: 'run',
235
+ args: extensionArgs[type].run,
236
+ help:
237
+ `Run a script (defined inside the ${type}'s package.json under the ` +
238
+ `“scripts” field inside the “appium” field) from an installed ${type}`,
239
+ },
240
+ ];
241
+
242
+ for (const {command, args, help} of parserSpecs) {
243
+ const parser = extSubParsers.add_parser(command, {help});
244
+
245
+ ArgParser._patchExit(parser);
246
+
247
+ for (const [flagsOrNames, opts] of args) {
248
+ // add_argument mutates params so make sure to send in copies instead
249
+ // @ts-ignore
250
+ parser.add_argument(...flagsOrNames, {...opts});
251
+ }
92
252
  }
93
253
  }
94
254
  }
95
255
  }
96
256
 
257
+ /**
258
+ * Creates a {@link ArgParser} instance. Necessarily reads extension configuration
259
+ * beforehand, and finalizes the config schema.
260
+ *
261
+ * @constructs ArgParser
262
+ * @param {boolean} [debug] - If `true`, throw instead of exit upon parsing error
263
+ * @returns {Promise<ArgParser>}
264
+ */
265
+ async function getParser (debug = false) {
266
+ await B.all([driverConfig.read(), pluginConfig.read()]);
267
+ finalizeSchema();
268
+
269
+ return new ArgParser(debug);
270
+ }
271
+
97
272
  export default getParser;
98
- export { getParser, getDefaultServerArgs };
273
+ export { getParser, ArgParser };
@@ -24,6 +24,10 @@ export default class PluginCommand extends ExtensionCommand {
24
24
  return await super.update({ext: plugin, unsafe});
25
25
  }
26
26
 
27
+ async run ({plugin, scriptName}) {
28
+ return await super.run({ext: plugin, scriptName});
29
+ }
30
+
27
31
  getPostInstallText ({extName, extData}) {
28
32
  return `Plugin ${extName}@${extData.version} successfully installed`.green;
29
33
  }
package/lib/cli/utils.js CHANGED
@@ -52,9 +52,29 @@ async function spinWith (json, msg, fn) {
52
52
  }
53
53
  }
54
54
 
55
+ class RingBuffer {
56
+ constructor (size = 50) {
57
+ this.size = size;
58
+ this.buffer = [];
59
+ }
60
+ getBuff () {
61
+ return this.buffer;
62
+ }
63
+ dequeue () {
64
+ this.buffer.shift();
65
+ }
66
+ enqueue (item) {
67
+ if (this.buffer.length >= this.size) {
68
+ this.dequeue();
69
+ }
70
+ this.buffer.push(item);
71
+ }
72
+ }
73
+
55
74
  export {
56
75
  errAndQuit,
57
76
  log,
58
77
  spinWith,
59
- JSON_SPACES
78
+ JSON_SPACES,
79
+ RingBuffer
60
80
  };