Repository: appium/appium (js)

Issue:
[Feat]: alias for `driver list`
I find myself often wanting to do `appium driver ls`, it should be easy enough to create that as an alias?

Pull request description:
feat(appium): make "ls" alias of "list"
This makes `ls` an alias of `list`; e.g., `appium driver ls`

While doing this I noticed a limitation of `argparser`, which is that an option (not a command) can only have a single (1) alias.  I have noted this behavior in a comment.

I also notice that we need to rewrite `ls` to be `list` ourselves, or we will have a bunch of conditionals checking for both values scattered around.

We need to use yargs...

* * *

Closes #18448

Patch:
diff --git a/packages/appium/lib/cli/args.js b/packages/appium/lib/cli/args.js
index c3e7ab733..db12c1374 100644
--- a/packages/appium/lib/cli/args.js
+++ b/packages/appium/lib/cli/args.js
@@ -294,5 +294,5 @@ export {getServerArgs, getExtensionArgs};
 
 /**
  * A tuple of argument aliases and argument options
- * @typedef {Map<string[],import('argparse').ArgumentOptions>} ArgumentDefinitions
+ * @typedef {Map<[name: string]|[name: string, alias: string],import('argparse').ArgumentOptions>} ArgumentDefinitions
  */
diff --git a/packages/appium/lib/cli/parser.js b/packages/appium/lib/cli/parser.js
index a547464e5..f605da837 100644
--- a/packages/appium/lib/cli/parser.js
+++ b/packages/appium/lib/cli/parser.js
@@ -101,6 +101,13 @@ class ArgParser {
     try {
       const parsed = this.parser.parse_known_args(args);
       const [knownArgs, unknownArgs] = parsed;
+      // XXX: you'd think that argparse, when given an alias for a subcommand,
+      // would set this value to the original subcommand name, but it doesn't.
+      if (knownArgs?.driverCommand === 'ls') {
+        knownArgs.driverCommand = 'list';
+      } else if (knownArgs?.pluginCommand === 'ls') {
+        knownArgs.pluginCommand = 'list';
+      }
       if (
         unknownArgs?.length &&
         (knownArgs.driverCommand === 'run' || knownArgs.pluginCommand === 'run')
@@ -210,13 +217,14 @@ class ArgParser {
       });
       const extensionArgs = getExtensionArgs();
       /**
-       * @type { {command: import('appium/types').CliExtensionSubcommand, args: import('./args').ArgumentDefinitions, help: string}[] }
+       * @type { {command: import('appium/types').CliExtensionSubcommand, args: import('./args').ArgumentDefinitions, help: string, aliases?: import('argparse').SubArgumentParserOptions['aliases']}[] }
        */
       const parserSpecs = [
         {
           command: 'list',
           args: extensionArgs[type].list,
           help: `List available and installed ${type}s`,
+          aliases: ['ls'],
         },
         {
           command: 'install',
@@ -242,15 +250,18 @@ class ArgParser {
         },
       ];
 
-      for (const {command, args, help} of parserSpecs) {
-        const parser = extSubParsers.add_parser(command, {help});
+      for (const {command, args, help, aliases} of parserSpecs) {
+        const parser = extSubParsers.add_parser(command, {help, aliases: aliases ?? []});
 
         ArgParser._patchExit(parser);
 
         for (const [flagsOrNames, opts] of args) {
           // add_argument mutates params so make sure to send in copies instead
-          // @ts-ignore
-          parser.add_argument(...flagsOrNames, {...opts});
+          if (flagsOrNames.length === 2) {
+            parser.add_argument(flagsOrNames[0], flagsOrNames[1], {...opts});
+          } else {
+            parser.add_argument(flagsOrNames[0], {...opts});
+          }
         }
       }
     }
diff --git a/packages/appium/lib/schema/cli-args.js b/packages/appium/lib/schema/cli-args.js
index ce70f1f41..609d8acb7 100644
--- a/packages/appium/lib/schema/cli-args.js
+++ b/packages/appium/lib/schema/cli-args.js
@@ -94,7 +94,7 @@ function makeDescription(schema) {
  * as understood by `argparse`.
  * @param {AppiumJSONSchema} subSchema - JSON schema for the option
  * @param {ArgSpec} argSpec - Argument spec tuple
- * @returns {[string[], import('argparse').ArgumentOptions]} Tuple of flag and options
+ * @returns {[[string]|[string, string], import('argparse').ArgumentOptions]} Tuple of flag and options
  */
 function subSchemaToArgDef(subSchema, argSpec) {
   let {type, appiumCliAliases, appiumCliTransformer, enum: enumValues} = subSchema;
@@ -207,7 +207,10 @@ function subSchemaToArgDef(subSchema, argSpec) {
     }
   }
 
-  return [aliases, argOpts];
+  // TODO: argparse only accepts the command name and a single alias; any extra aliases
+  // will be silently discarded.
+  const finalAliases = /** @type {[string]|[string, string]} */ (aliases);
+  return [finalAliases, argOpts];
 }
 
 /**


Test patch:
diff --git a/packages/appium/test/unit/parser.spec.js b/packages/appium/test/unit/parser.spec.js
index ca1406fa2..7ee1456c2 100644
--- a/packages/appium/test/unit/parser.spec.js
+++ b/packages/appium/test/unit/parser.spec.js
@@ -264,6 +264,14 @@ describe('parser', function () {
         const args = p.parseArgs([DRIVER_TYPE, 'list', '--updates']);
         args.showUpdates.should.eql(true);
       });
+      it('should allow "ls" as an alias for "list"', function () {
+        const args = p.parseArgs([DRIVER_TYPE, 'ls']);
+        args.subcommand.should.eql(DRIVER_TYPE);
+        args.driverCommand.should.eql('list');
+        args.showInstalled.should.eql(false);
+        args.showUpdates.should.eql(false);
+        args.json.should.eql(false);
+      });
     });
     describe('install', function () {
       it('should not allow an empty argument list', function () {