src/cli/runCli.js
import { isString } from 'lodash';
import isPromise from 'is-promise';
import { appendSettings, getSettings } from '../configuration/manageSettings';
import { setConfig } from '../configuration/manageConfig';
import { setContext } from '../context/helpers/manageContext';
import addRaw from '../configuration/addRaw';
import buildDocumentationObject from '../documentation/buildDocumentationObject';
import execute from '../execute';
import getAbsolutePath from '../helpers/getAbsolutePath';
import getSuggestions from '../helpers/getSuggestions';
import initContext from '../context/initContext';
import log from '../log/default';
import merge from '../helpers/merge';
import runHook from '../hooks/runHook';
import validateSettingsWrapper from '../validation/validateSettingsWrapper';
import extractCommand from './commands/helpers/extractCommand';
import generateAliases from './commands/helpers/generateAliases';
import generateCommandDocumentation from './commands/documentation/generateCommandDocumentation';
import generateCommandsDocumentation from './commands/documentation/generateCommandsDocumentation';
import getMappings from './commands/getMappings';
import parseArguments from './commands/parseArguments';
import parseOptions from './commands/parseOptions';
import processArguments from './processArguments';
/**
* Invokes the Roc cli.
* Returns what the command is returning. If the command is a string command a promise will be
* returned that is resolved when the command has been completed.
*/
export default function runCli({
info = { version: 'Unknown', name: 'Unknown' },
commands: initialCommands,
argv = process.argv,
invoke = true,
}) {
const input = processArguments(argv);
// If version is selected output that and stop
if (input.coreOptions.version || input.coreOptions.v) {
return console.log(info.version);
}
if (input.coreOptions.betterFeedback || input.coreOptions.b) {
require('source-map-support').install(); // eslint-disable-line
require('loud-rejection')(); // eslint-disable-line
}
// Possible to set a command in verbose mode
const verboseMode = !!(input.coreOptions.verbose || input.coreOptions.V);
// Get the project configuration path
const projectConfigPath = input.coreOptions.c || input.coreOptions.config;
// Get the directory path
const dirPath = getAbsolutePath(input.coreOptions.directory || input.coreOptions.d);
// Set temporary context
setContext({
verbose: verboseMode,
directory: dirPath,
});
// Initialize the complete context
const context = initContext({
verbose: verboseMode,
commands: initialCommands,
directory: dirPath,
projectConfigPath,
name: info.name,
});
// If we have no command we will display some help information about all possible commands
if (!input.groupOrCommand) {
return console.log(
generateCommandsDocumentation(context.commands, info.name)
);
}
// Given context commands and user input, extract command data from potentially
// nested group/command structure
const commandData = extractCommand(
context.commands,
input.groupOrCommand,
input.argsWithoutOptions,
info.name
);
// command data as string indicates no match, so print the provided feedback
if (isString(commandData)) {
return console.log(commandData);
}
let commandSuggestions = Object.keys(commandData.commands);
// If there is no direct match we will search through the tree for a match
if (!commandData.commands[commandData.commandName]) {
const commandAliases = generateAliases(
commandData.commands,
commandData.commandName,
commandData.parents
);
if (!commandAliases) {
return undefined;
} else if (commandAliases.commands) {
commandData.commands = commandAliases.commands;
commandData.parents = commandAliases.parents;
}
commandSuggestions = commandSuggestions.concat(commandAliases.mappings);
}
const command = commandData.commands[commandData.commandName];
if (!command) {
log.large.error(
getSuggestions([commandData.commandName], commandSuggestions),
'Invalid command'
);
}
// Show command help information if requested
// Will ignore application configuration
if (input.coreOptions.help || input.coreOptions.h) {
return console.log(
generateCommandDocumentation(
context.extensionConfig.settings,
context.meta.settings,
commandData.commands,
commandData.commandName,
info.name,
commandData.parents
)
);
}
let documentationObject;
// Only parse arguments if the command accepts it
if (command && command.settings) {
// Get config from application and only parse options that this command cares about.
const filter = command.settings === true ? [] : command.settings;
documentationObject = buildDocumentationObject(context.config.settings, context.meta.settings, filter);
}
const { settings, parsedOptions } =
parseOptions(input.extOptions, getMappings(documentationObject), command);
const configToValidate = merge(context.config, {
settings,
});
// Validate configuration
if (command && command.settings) {
validateSettingsWrapper(configToValidate.settings, context.meta.settings, command.settings);
}
// Does this after the validation so that things set by the CLI always will have the highest priority
// We do not want to do validation on RAW configuration
context.config = merge(addRaw(context.config), {
settings,
});
// Set the configuration object
setConfig(context.config);
// Run hook to make it possible for extensions to update the settings before anything other uses them
// This means that they can inspect what has been defined by other extensions, the user through config
// and through command line options
runHook('roc')('update-settings', () => getSettings())(
(newSettings) => appendSettings(newSettings)
);
if (invoke) {
// If string run as shell command
if (isString(command.command)) {
return execute(command.command, {
context: command.__context,
args: input.extraArguments,
cwd: dirPath,
}).catch((error) => {
process.exitCode = error.getCode ? error.getCode() : 1;
log.small.error('An error occurred when running the command');
});
}
const parsedArguments = parseArguments(commandData.commandName, commandData.commands, input.argsWithoutOptions);
// Run the command
try {
const commandResult = command.command({
info,
arguments: parsedArguments,
options: parsedOptions,
extraArguments: input.extraArguments,
// Roc Context
context,
});
if (isPromise(commandResult)) {
return commandResult
.catch((error) => {
log.small.warn('A problem occurred when running the command', error);
});
}
return commandResult;
} catch (error) {
log.small.error('An error occurred when running the command', error);
}
}
return undefined;
}