Jerzy Kozera efa6963242 Fix issues with PR #5631 causing base64 errors
- Some of values were not converted back from base64, which resulted in
  spurious errors with base64 strings.
  (This was caused by nested arrays, like commands, not being converted
   back, combined with the wrong assumption that commands always occur
   before options. It is not always true, as the added test case
   demonstrates.)
- Some other values were converted *from* base64, but they weren't
  base64 in the first place, which resulted in junk binary data.
  (This was due to the invalid assumption that only odd-numbered values
   need to be converted.)
2018-11-15 17:39:35 +01:00

342 lines
11 KiB
JavaScript

'use strict';
const version = require('../../package.json').version;
const minimist = require('minimist');
const _ = require('lodash');
const os = require('os');
const chalk = require('chalk');
const getCommandSuggestion = require('../utils/getCommandSuggestion');
class CLI {
constructor(serverless, inputArray) {
this.serverless = serverless;
this.inputArray = inputArray || null;
this.loadedPlugins = [];
this.loadedCommands = {};
}
setLoadedPlugins(plugins) {
this.loadedPlugins = plugins;
}
setLoadedCommands(commands) {
this.loadedCommands = commands;
}
processInput() {
let inputArray;
// check if commands are passed externally (e.g. used by tests)
// otherwise use process.argv to receive the commands
if (this.inputArray !== null) {
inputArray = this.inputArray;
} else {
inputArray = process.argv.slice(2);
}
const base64Encode = (valueStr) =>
new Buffer(valueStr).toString('base64');
const toBase64Helper = (value) => {
const valueStr = value.toString();
if (valueStr.startsWith('-')) {
if (valueStr.indexOf('=') !== -1) {
// do not encode argument names, since those are parsed by
// minimist, and thus need to be there unconverted:
const splitted = valueStr.split('=', 2);
// splitted[1] values, however, need to be encoded, since we
// decode them later back to utf8
const encodedValue = base64Encode(splitted[1]);
return `${splitted[0]}=${encodedValue}`;
}
// do not encode plain flags, for the same reason as above
return valueStr;
}
return base64Encode(valueStr);
};
const decodedArgsHelper = (arg) => {
if (_.isString(arg)) {
return new Buffer(arg, 'base64').toString();
} else if (_.isArray(arg)) {
return _.map(arg, decodedArgsHelper);
}
return arg;
};
// encode all the options values to base64
const valuesToParse = _.map(inputArray, toBase64Helper);
// parse the options with minimist
const argvToParse = minimist(valuesToParse);
// decode all values back to utf8 strings
const argv = _.mapValues(argvToParse, decodedArgsHelper);
const commands = [].concat(argv._);
const options = _.omit(argv, ['_']);
return { commands, options };
}
displayHelp(processedInput) {
const commands = processedInput.commands;
const options = processedInput.options;
// if only "version" or "v" was entered
if ((commands.length === 0 && (options.version || options.v)) ||
(commands.length === 1 && (commands.indexOf('version') > -1))) {
this.getVersionNumber();
return true;
}
// if only "help" or "h" was entered
if ((commands.length === 0) ||
(commands.length === 0 && (options.help || options.h)) ||
(commands.length === 1 && (commands.indexOf('help') > -1))) {
if (options.verbose || options.v) {
this.generateVerboseHelp();
} else {
this.generateMainHelp();
}
return true;
}
// if "help" was entered in combination with commands (or one command)
if (commands.length >= 1 && (options.help || options.h)) {
this.generateCommandsHelp(commands);
return true;
}
return false;
}
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);
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}`, indents);
});
}
displayCommandOptions(commandObject) {
const dotsLength = 40;
_.forEach(commandObject.options, (optionsObject, option) => {
let optionsDots = _.repeat('.', dotsLength - option.length);
const optionsUsage = optionsObject.usage;
if (optionsObject.required) {
optionsDots = optionsDots.slice(0, optionsDots.length - 18);
} else {
optionsDots = optionsDots.slice(0, optionsDots.length - 7);
}
if (optionsObject.shortcut) {
optionsDots = optionsDots.slice(0, optionsDots.length - 5);
}
const optionInfo = ` --${option}`;
let shortcutInfo = '';
let requiredInfo = '';
if (optionsObject.shortcut) {
shortcutInfo = ` / -${optionsObject.shortcut}`;
}
if (optionsObject.required) {
requiredInfo = ' (required)';
}
const thingsToLog = `${optionInfo}${shortcutInfo}${requiredInfo} ${
chalk.dim(optionsDots)} ${optionsUsage}`;
this.consoleLog(chalk.yellow(thingsToLog));
});
}
generateMainHelp() {
let platformCommands;
let frameworkCommands;
if (this.loadedCommands) {
const commandKeys = Object.keys(this.loadedCommands);
const sortedCommandKeys = _.sortBy(commandKeys);
const partitionedCommandKeys = _.partition(sortedCommandKeys,
(key) => this.loadedCommands[key].platform);
platformCommands = _.fromPairs(
_.map(partitionedCommandKeys[0], key => [key, this.loadedCommands[key]])
);
frameworkCommands = _.fromPairs(
_.map(partitionedCommandKeys[1], key => [key, this.loadedCommands[key]])
);
}
this.consoleLog('');
this.consoleLog(chalk.yellow.underline('Commands'));
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 "--no-color" to disable CLI colors'));
this.consoleLog(chalk.dim('* Pass "--help" after any <command> for contextual help'));
this.consoleLog('');
this.consoleLog(chalk.yellow.underline('Framework'));
this.consoleLog(chalk.dim('* Documentation: https://serverless.com/framework/docs/'));
this.consoleLog('');
if (!_.isEmpty(frameworkCommands)) {
_.forEach(frameworkCommands, (details, command) => {
this.displayCommandUsage(details, command);
});
} else {
this.consoleLog('No commands found');
}
this.consoleLog('');
this.consoleLog(chalk.yellow.underline('Platform (Beta)'));
// eslint-disable-next-line max-len
this.consoleLog(chalk.dim('* The Serverless Platform is currently in experimental beta. Follow the docs below to get started.'));
this.consoleLog(chalk.dim('* Documentation: https://serverless.com/platform/docs/'));
this.consoleLog('');
if (!_.isEmpty(platformCommands)) {
_.forEach(platformCommands, (details, command) => {
this.displayCommandUsage(details, command);
});
} else {
this.consoleLog('No commands found');
}
this.consoleLog('');
// print all the installed plugins
this.consoleLog(chalk.yellow.underline('Plugins'));
if (this.loadedPlugins.length) {
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 commandName = commandsArray.join(' ');
// Get all the commands using getCommands() with filtered entrypoint
// commands and reduce to the required command.
const allCommands = this.serverless.pluginManager.getCommands();
const command = _.reduce(commandsArray, (currentCmd, cmd) => {
if (currentCmd.commands && cmd in currentCmd.commands) {
return currentCmd.commands[cmd];
}
return null;
}, { commands: allCommands });
// Throw error if command not found.
if (!command) {
const suggestedCommand = getCommandSuggestion(commandName, allCommands);
const errorMessage = [
`Serverless command "${commandName}" not found. Did you mean "${suggestedCommand}"?`,
' Run "serverless help" for a list of all available commands.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
// print the name of the plugin
this.consoleLog(chalk.yellow.underline(`Plugin: ${command.pluginName}`));
this.displayCommandUsage(command, commandName);
this.displayCommandOptions(command);
this.consoleLog('');
return null;
}
getVersionNumber() {
this.consoleLog(version);
}
asciiGreeting() {
let art = '';
art = `${art} _______ __${os.EOL}`;
art = `${art}| _ .-----.----.--.--.-----.----| .-----.-----.-----.${os.EOL}`;
art = `${art}| |___| -__| _| | | -__| _| | -__|__ --|__ --|${os.EOL}`;
art = `${art}|____ |_____|__| \\___/|_____|__| |__|_____|_____|_____|${os.EOL}`;
art = `${art}| | | The Serverless Application Framework${os.EOL}`;
art = `${art}| | serverless.com, v${version}${os.EOL}`;
art = `${art} -------'`;
this.consoleLog(chalk.yellow(art));
this.consoleLog('');
}
printDot() {
process.stdout.write(chalk.yellow('.'));
}
log(message) {
this.consoleLog(`Serverless: ${chalk.yellow(`${message}`)}`);
}
consoleLog(message) {
console.log(message); // eslint-disable-line no-console
}
}
module.exports = CLI;