cbf
Version:
A package for creating scripts to store and run your most commonly used CLI commands for a repo or just in general
308 lines (278 loc) • 8.89 kB
JavaScript
const { spawn } = require('child_process');
const fse = require('fs-extra');
const noop = require('lodash/noop');
const cloneDeep = require('lodash/cloneDeep');
const isEmpty = require('lodash/isEmpty');
const { printMessage, formatMessage } = require('formatted-messages');
const { DEFAULT_SHELL, PROGRAM_NAME, OperatingModes } = require('../../../constants');
const {
isEmptyString,
absolutePath,
throwError,
safeExit,
forceExit,
} = require('../../../utility');
const { CurrentOperatingModes } = require('../../../operating-modes');
const { prompts, inquirerPrompts, InquirerPromptTypes } = require('../../../shims/inquirer');
const messages = require('./messages');
/**
* Handle an error while running a command
*
* @param {object} param - object parameter
* @param {Error|string} param.error - error that occurred while running command
* @param {string[]} param.directives - directives ran that caused error
*/
const handleCommandError = ({ error, directives }) => {
if (directives.length === 1) {
printMessage(formatMessage(messages.errorRunningCommand, { command: directives[0], error }));
} else {
printMessage(formatMessage(messages.errorRunningCommands, { commands: directives, error }));
}
safeExit();
};
class Command {
/**
* Construct a command
*
* @param {object} param - object parameter
* @param {string[]} param.variables - the commands variables
* @param {string[]} param.directives - the commands directives
* @param {string[]} param.hiddenDirectives - the commands hidden directives
* @param {string} param.message - the commands message
*/
constructor({ variables = [], directives = [], hiddenDirectives = [], message = '' } = {}) {
this.variables = variables;
this.directives = directives;
this.hiddenDirectives = hiddenDirectives;
if (!isEmptyString(message)) {
this.message = message;
}
}
/**
* Return a copy of the command
*
* @param {Command} command - command to be copied
*
* @returns {Command} copiedCommand - copied command
*/
static copy(command) {
if (!(command instanceof Command)) {
throwError(
`Command.copy expects a Command instance but instead received a ${command.constructor.name} instance`,
);
}
return cloneDeep(command);
}
/**
* Prompts the user for values for the command variables and saves them to the command directives
*
* @returns {Promise} updatedDirectivesWithVariables - a promise that all the command directives have been updated with command variables
*/
updateDirectivesWithVariables() {
const promises = [];
const variables = this.getVariables();
// Setup subscriber to receive the answers and save the directives
const subscriber = inquirerPrompts.subscribe(
({ name: { variable, resolve }, answer }) => {
const variableRegex = new RegExp(`${variable}`, 'g');
const updatedDirectives = this.getDirectives().map(directive =>
directive.replace(variableRegex, answer),
);
this.updateDirectives(updatedDirectives);
resolve();
},
noop,
noop,
);
Object.keys(variables).forEach(variable => {
// Ask question
promises.push(
new Promise(resolve => {
prompts.next({
type: InquirerPromptTypes.INPUT,
name: {
variable,
resolve,
},
message: variables[variable],
default: '',
});
}),
);
});
return Promise.all(promises).then(() => subscriber.unsubscribe());
}
/**
* Run the command
*
* @param {object} param - object parameter
* @param {string} param.shell - shell to run the command in
* @param {Directory} param.directory - directory to run the command in
*/
run({ shell = DEFAULT_SHELL, directory }) {
const path = absolutePath(directory.getPath());
if (!isEmptyString(path)) {
if (!fse.pathExistsSync(path)) {
printMessage(formatMessage(messages.noSuchDirectory, { path: directory.getPath() }));
forceExit();
}
}
const dryRun = CurrentOperatingModes.includes(OperatingModes.DRY_RUN);
this.updateDirectivesWithVariables().then(() => {
if (this.getMessage()) {
printMessage(formatMessage(messages.commandMessage, { message: this.getMessage() }));
}
const directives = this.getDirectives();
if (directory && !isEmpty(directory.getPath()) && directives.length >= 1) {
if (dryRun) {
printMessage(
formatMessage(messages.runCommandInPathDryRunMode, {
command: directives[0],
path: directory.getPath(),
}),
);
} else {
printMessage(
formatMessage(messages.runCommandInPath, {
command: directives[0],
path: directory.getPath(),
}),
);
}
} else if (directory && !isEmpty(directory.getPath()) && directives.length > 1) {
if (dryRun) {
printMessage(
formatMessage(messages.runCommandsInPathDryRunMode, {
command: directives,
path: directory.getPath(),
}),
);
} else {
printMessage(
formatMessage(messages.runCommandsInPath, {
commands: directives[0],
path: directory.getPath(),
}),
);
}
} else if (
!directory ||
(directory && isEmpty(directory.getPath()) && directives.length === 1)
) {
if (dryRun) {
printMessage(formatMessage(messages.runCommandDryRunMode, { command: directives[0] }));
} else {
printMessage(formatMessage(messages.runCommand, { command: directives[0] }));
}
} else if (
!directory ||
(directory && isEmpty(directory.getPath()) && directives.length > 1)
) {
if (dryRun) {
printMessage(formatMessage(messages.runCommandsDryRunMode, { commands: directives }));
} else {
printMessage(formatMessage(messages.runCommands, { commands: directives }));
}
}
// Join directives
const directive = !isEmpty(this.getHiddenDirectives())
? this.getHiddenDirectives().join(' && ')
: this.getDirectives().join(' && ');
// If the directive will run `cbf` we safe exit the parent running `cbf`
if (directive.indexOf(PROGRAM_NAME) !== -1) {
safeExit();
}
if (dryRun) {
safeExit();
return;
}
const childProcess = spawn(
directive,
{
cwd: path,
shell,
stdio: 'inherit',
detached: true,
},
error => {
if (error) {
handleCommandError({ error, directives });
}
},
);
childProcess.on('exit', safeExit);
childProcess.on('error', error => {
handleCommandError({ error, directives });
});
process.on('SIGINT', () => {
process.kill(-childProcess.pid, 'SIGINT');
});
});
}
/**
* Returns the command variables
*
* @returns {string[]} variables - the command variables
*/
getVariables() {
return this.variables;
}
/**
* Updates the command variables
*
* @param {string[]} variables - variables to update the command variables
*/
updateVariables(variables) {
this.variables = variables;
}
/**
* Returns the command directives
*
* @returns {string[]} directives - the command directives
*/
getDirectives() {
return this.directives;
}
/**
* Updates the command directives
*
* @param {string[]} directives - directives to update the command directives
*/
updateDirectives(directives) {
this.directives = directives;
}
/**
* Returns the command hidden directives used to store directives to be run but not documented
*
* @returns {string[]} hiddenDirectives - directives to be ran but not documented
*/
getHiddenDirectives() {
return this.hiddenDirectives;
}
/**
* Updates the command hidden directives
*
* @param {string[]} hiddenDirectives - directives to be ran but no documented
*/
updateHiddenDirectives(hiddenDirectives) {
this.hiddenDirectives = hiddenDirectives;
}
/**
* Returns message of the command
*
* @returns {string} message - message of the command
*/
getMessage() {
return this.message;
}
/**
* Updates the message of the command
*
* @param {string} message - message to update the command message
*/
updateMessage(message) {
this.message = message;
}
}
module.exports = Command;