Repository: serverless/serverless (JavaScript)
Date: 2017-06-21 13:36:27+00:00
Task type: Bug Fix

Issue:
DX: Commands should be listed alphabetically in help
# This is a Bug Report

## Description
Commands should be listed alphabetically when running `sls --help`

* What went wrong?
Not all commands are listed in alphabetical order.

<img width="695" alt="hyper" src="https://user-images.githubusercontent.com/1471972/27046538-ade7089e-4f59-11e7-931e-87819bb9d7d9.png">

## Additional Data

* Serverless Framework Version: 1.15.3
* Operating System: OSX 10.12.4




Issue discussion:
Currently the commands are listed by the order of their definition. This leads to the commands being grouped by their defining plugins implicitly. IMO neither an alphabetic nor a plugin based order is right or wrong as both are discussable.
What if we'd add a sort option to the help command, with alphabetic being the default.
I wonder if there are any metrics on which are used the most? Plugins might make that awkward, but I'd rather order a more logical way than alphabetic. 
> Currently the commands are listed by the order of their definition. This leads to the commands being grouped by their defining plugins implicitly.

@HyperBrain good insights as usual 👍 

I'm not sure that the plugin based ordering is immediately obvious to the greater chunk of the user base (mainly because they're unaware that plugins and commands are related until they've really dug in to the code). I've seen this a few times now watching new users of the framework explore the commands. Their comments are "hey, your commands are out of order"

> What if we'd add a sort option to the help command, with alphabetic being the default.

I like this as being an option. Potentially it would be nice to have them displayed under plugin headers. This seems like a feature that power users would get the most use out of, specifically plugin authors and framework maintainers.

Maybe something to this effect? 
```
Commands by plugin

AwsConfigCredentials
  config credentials ............ Configures a new provider profile for the Serverless Framework

Config
  config ........................ Configure Serverless

Create
  create ........................ Create new Serverless service

Deploy
  deploy ........................ Deploy a Serverless service
  deploy function ............... Deploy a single function from the service
  deploy list ................... List deployed version of your Serverless Service
  deploy list functions ......... List all the deployed functions and their versions

Info
  info .......................... Display information about the service

Install
  install ....................... Install a Serverless service from GitHub

Invoke
  invoke ........................ Invoke a deployed function
  invoke local .................. Invoke function locally

Login
  login ......................... Login or sign up for the Serverless Platform
  logout ........................ Logout from the Serverless Platform 

Logs
  logs .......................... Output the logs of a deployed function

Metrics
  metrics ....................... Show metrics for a specific function

Package
  package ....................... Packages a Serverless service

Remove
  remove ........................ Remove Serverless service and all resources

Rollback
  rollback ...................... Rollback the Serverless service to a specific deployment
  rollback function ............. Rollback the function to the previous version

SlStats
  slstats ....................... Enable or disable stats
```
> I wonder if there are any metrics on which are used the most? Plugins might make that awkward, but I'd rather order a more logical way than alphabetic.

@ryanmurakami any thoughts around more useful ways of ordering? I can't think of much else other than alphabetical and grouping by plugin.
I like the idea with the plugin headers, Looks very structured and explanatory to me. This would also reveal quickly which plugins are installed/active.
@brianneisler Yep, love what you did. Plugin ordering makes the a lot of sense.
Also +1 for the headers. Only downside could be that most of our provider plugins are prefixed with the provider name. So e.g. `AwsDeploy` or `AwsMetrics` which could look a little bit ugly.

We could filter them before displaying so that only the "core" plugins are shown (e.g. `Deploy` instead of `AwsDeploy`). But this way we would run into the problem where options which are defined only on the provider plugins won't show up in the help screen.

Definitely smth. we should take a deeper dive into.
@pmuens A pragmatic solution for the names would be that every plugin gets a `displayName` property that is set to a human-readable name and used in the help screen instead of the class names.
> @pmuens A pragmatic solution for the names would be that every plugin gets a displayName property that is set to a human-readable name and used in the help screen instead of the class names.

Thats sounds like a simple and good solution 👍 

Solution patch:
diff --git a/lib/classes/CLI.js b/lib/classes/CLI.js
index 05698c36da2..b23d1559947 100644
--- a/lib/classes/CLI.js
+++ b/lib/classes/CLI.js
@@ -56,7 +56,11 @@ class CLI {
     if ((commands.length === 0) ||
         (commands.length === 0 && (options.help || options.h)) ||
         (commands.length === 1 && (commands.indexOf('help') > -1))) {
-      this.generateMainHelp();
+      if (options.verbose || options.v) {
+        this.generateVerboseHelp();
+      } else {
+        this.generateMainHelp();
+      }
       return true;
     }
 
@@ -68,18 +72,19 @@ class CLI {
     return false;
   }
 
-  displayCommandUsage(commandObject, command) {
+  displayCommandUsage(commandObject, command, indents) {
     const dotsLength = 30;
 
     // check if command has lifecycleEvents (can be executed)
     if (commandObject.lifecycleEvents) {
       const usage = commandObject.usage;
       const dots = _.repeat('.', dotsLength - command.length);
-      this.consoleLog(`${chalk.yellow(command)} ${chalk.dim(dots)} ${usage}`);
+      const indent = _.repeat('  ', indents || 0);
+      this.consoleLog(`${indent}${chalk.yellow(command)} ${chalk.dim(dots)} ${usage}`);
     }
 
     _.forEach(commandObject.commands, (subcommandObject, subcommand) => {
-      this.displayCommandUsage(subcommandObject, `${command} ${subcommand}`);
+      this.displayCommandUsage(subcommandObject, `${command} ${subcommand}`, indents);
     });
   }
 
@@ -120,13 +125,24 @@ class CLI {
     this.consoleLog(chalk.yellow.underline('Commands'));
     this.consoleLog(chalk.dim('* Serverless documentation: http://docs.serverless.com'));
     this.consoleLog(chalk.dim('* You can run commands with "serverless" or the shortcut "sls"'));
+    this.consoleLog(chalk.dim('* Pass "--verbose" to this command to get in-depth plugin info'));
     this.consoleLog(chalk.dim('* Pass "--help" after any <command> for contextual help'));
 
     this.consoleLog('');
 
-    _.forEach(this.loadedCommands, (details, command) => {
-      this.displayCommandUsage(details, command);
-    });
+    if (this.loadedCommands) {
+      const commandKeys = Object.keys(this.loadedCommands);
+      const sortedCommandKeys = _.sortBy(commandKeys);
+      const sortedCommands = _.fromPairs(
+        _.map(sortedCommandKeys, key => [key, this.loadedCommands[key]])
+      );
+
+      _.forEach(sortedCommands, (details, command) => {
+        this.displayCommandUsage(details, command);
+      });
+    } else {
+      this.consoleLog('No commands found');
+    }
 
     this.consoleLog('');
 
@@ -134,17 +150,62 @@ class CLI {
     this.consoleLog(chalk.yellow.underline('Plugins'));
 
     if (this.loadedPlugins.length) {
-      const sortedPlugins = _.sortBy(
-        this.loadedPlugins,
-        (plugin) => plugin.constructor.name
-      );
-
+      const sortedPlugins = _.sortBy(this.loadedPlugins, (plugin) => plugin.constructor.name);
       this.consoleLog(sortedPlugins.map((plugin) => plugin.constructor.name).join(', '));
     } else {
       this.consoleLog('No plugins added yet');
     }
   }
 
+  generateVerboseHelp() {
+    this.consoleLog('');
+    this.consoleLog(chalk.yellow.underline('Commands by plugin'));
+    this.consoleLog('');
+
+    let pluginCommands = {};
+
+    // add commands to pluginCommands based on command's plugin
+    const addToPluginCommands = (cmd) => {
+      const pcmd = _.clone(cmd);
+
+      // remove subcommand from clone
+      delete pcmd.commands;
+
+      // check if a plugin entry is alreay present in pluginCommands. Use the
+      // existing one or create a new plugin entry.
+      if (_.has(pluginCommands, pcmd.pluginName)) {
+        pluginCommands[pcmd.pluginName] = pluginCommands[pcmd.pluginName].concat(pcmd);
+      } else {
+        pluginCommands[pcmd.pluginName] = [pcmd];
+      }
+
+      // check for subcommands
+      if ('commands' in cmd) {
+        _.forEach(cmd.commands, (d) => {
+          addToPluginCommands(d);
+        });
+      }
+    };
+
+    // fill up pluginCommands with commands in loadedCommands
+    _.forEach(this.loadedCommands, (details) => {
+      addToPluginCommands(details);
+    });
+
+    // sort plugins alphabetically
+    pluginCommands = _(pluginCommands).toPairs().sortBy(0).fromPairs()
+      .value();
+
+    _.forEach(pluginCommands, (details, plugin) => {
+      this.consoleLog(plugin);
+      _.forEach(details, (cmd) => {
+        // display command usage with single(1) indent
+        this.displayCommandUsage(cmd, cmd.key.split(':').join(' '), 1);
+      });
+      this.consoleLog('');
+    });
+  }
+
   generateCommandsHelp(commandsArray) {
     const command = this.serverless.pluginManager.getCommand(commandsArray);
     const commandName = commandsArray.join(' ');


Test patch:
diff --git a/lib/classes/CLI.test.js b/lib/classes/CLI.test.js
index 3a1d6b326ab..1e5022bdbe4 100644
--- a/lib/classes/CLI.test.js
+++ b/lib/classes/CLI.test.js
@@ -1,9 +1,5 @@
 'use strict';
 
-/**
- * Test: CLI Class
- */
-
 const expect = require('chai').expect;
 const CLI = require('../../lib/classes/CLI');
 const os = require('os');
@@ -352,5 +348,17 @@ describe('CLI', () => {
         done();
       });
     });
+
+    it('should print help --verbose to stdout', (done) => {
+      exec(`${this.serverlessExec} help --verbose`, (err, stdout) => {
+        if (err) {
+          done(err);
+          return;
+        }
+
+        expect(stdout).to.contain('Commands by plugin');
+        done();
+      });
+    });
   });
 });