UNPKG

rapido

Version:

Rápido is an extensible command line interface that enables rapid development for any technology stack. Rápido is written in JavaScript and is built on top of Node.js and npm.

933 lines (740 loc) 29.7 kB
var optimist = require('optimist'); var path = require('path'); var colors = require('colors'); var forEachEntry = require('raptor-util').forEachEntry; var extend = require('raptor-util').extend; var createError = require('raptor-util').createError; for (var i=0, len=process.argv.length; i<len; i++) { if (process.argv[i] === '--no-colors') { colors.mode = 'none'; } } require('raptor-logging').configureLoggers({ ROOT: 'WARN' }); var logger = require('raptor-logging').logger(module); var CommandRegistry = require('./CommandRegistry'); var File = require('raptor-files/File'); // logger.warn('rapido dependencies loaded in ' + (Date.now() - rapidoStartTime) + 'ms'); colors.setTheme({ silly: 'rainbow', input: 'grey', verbose: 'cyan', prompt: 'white', data: 'grey', info: 'cyan', help: 'cyan', warn: 'yellow', debug: 'blue', error: 'red' }); function getUserHome() { return new File(process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']); } function createPaths() { var paths = [], foundPaths = {}; return { add: function(dir) { if (typeof dir === 'string') { dir = new File(dir); } var path = dir.getAbsolutePath(); if (!foundPaths[path]) { foundPaths[path] = true; if (dir.exists() && dir.isDirectory()) { paths.push(dir); } } }, paths: paths }; } function buildStackConfigPaths(rapido) { var paths = createPaths(); if (rapido.stackDirs) { rapido.stackDirs.forEach(paths.add); return paths.paths; } // Add this directory since we have a rapido-stack.json file here paths.add(new File(__dirname)); rapido.additionalStackDirs.forEach(paths.add); var foundNodeModulesDir = {}; function addFromNodeModules(nodeModulesDir) { // console.error("addFromNodeModules: " + nodeModulesDir.getAbsolutePath()); if (typeof nodeModulesDir === 'string') { nodeModulesDir = new File(nodeModulesDir); } if (!nodeModulesDir || !nodeModulesDir.exists()) { return; } var path = nodeModulesDir.getAbsolutePath(); if (foundNodeModulesDir[path]) { return; } foundNodeModulesDir[path] = true; var moduleDirs = nodeModulesDir.listFiles(); if (moduleDirs && moduleDirs.length) { moduleDirs.forEach(paths.add); } } rapido.additionalNodeModulesDirs.forEach(function(dir) { addFromNodeModules(dir); }); // Now discover all of the Rapido stacks/commands // in first-level modules in available node_modules directories: var curDir = new File(process.cwd()); // Add all of the node_modules starting with CWD up to root while(true) { addFromNodeModules(new File(curDir, "node_modules")); curDir = curDir.getParentFile(); if (!curDir || !curDir.exists()) { break; } } addFromNodeModules(rapido.globalNodeModulesDir); return paths.paths; } function buildDefaultUserConfigPaths() { var paths = createPaths(); var curDir = new File(process.cwd()); while(true) { paths.add(curDir); curDir = curDir.getParentFile(); if (!curDir || !curDir.exists()) { break; } } paths.add(getUserHome()); return paths.paths; } var $0 = path.basename(require('path').basename(process.argv[1])); exports.run = function(argv, options) { var instance = exports.create(options); instance.run(argv); }; exports.create = function(options) { var _exclusiveStackCommands = []; var _hiddenCommands = {}; var rapido = { initialized: false, config: null, commands: new CommandRegistry(), optimist: optimist, globalNodeModulesDir: null, log: function(message) { var args = Array.prototype.slice.call(arguments, 0); for (var i=0, len=args.length; i<len; i++) { var arg = arguments[i]; if (typeof arg === 'string') { args[i] = arg.replace(/\$0/g, $0); } } console.log.apply(console, args); }, scaffold: function(config) { require('./scaffolding').generate(config, rapido); }, $0: $0, configFilename: 'rapido.json', stackConfigFilename: 'rapido-stack.json', stackDirs: null, additionalStackDirs: [], additionalNodeModulesDirs: [], additionalEnabledStacks: [], exclusiveStackCommands: function(stackName) { _exclusiveStackCommands.push(stackName); }, hideCommand: function(stackName, commandName) { _hiddenCommands[stackName + ':' + commandName] = { stackName: stackName, commandName: commandName }; }, init: function() { if (this.initialized) { return; } this.initialized = true; var startTime = Date.now(); var configFilename = rapido.configFilename; var stackConfigFilename = rapido.stackConfigFilename; var config = rapido.config; if (!config) { config = rapido.config = {}; require('./config-loader').loadConfigs( config, buildDefaultUserConfigPaths(rapido), configFilename, rapido); var stacks = require('./config-loader').loadStackConfigs( config, buildStackConfigPaths(rapido), stackConfigFilename, rapido); if (_exclusiveStackCommands) { _exclusiveStackCommands.forEach(function(stackName) { var stack = stacks[stackName]; if (stack) { if (stack.commands) { forEachEntry(stack.commands, function(commandName, commandConfig) { rapido.commands.exclusiveStackCommand(stackName, commandName); }); } } }); } forEachEntry(stacks, function(stackName, stackConfig) { rapido.registerStack(stackConfig); }); forEachEntry(_hiddenCommands, function(name, value) { var stackName = value.stackName; var commandName = value.commandName; var stack = rapido.getStack(stackName); if (!stack) { throw new Error('Stack not found with name "' + stackName + '"'); } rapido.getStack(stackName).hideCommand(commandName); }); } var enabledStackNames = rapido.config['use'] || []; this.disableAllStacks(); enabledStackNames = enabledStackNames.concat(rapido.additionalEnabledStacks); enabledStackNames.push('default'); // Always enable the default stack enabledStackNames.forEach(function(enabledStackName) { if (!rapido.stackExists(enabledStackName)) { rapido.log.warn('WARN', "Enabled stack not found: " + enabledStackName); } else { this.enableStack(enabledStackName); } }, this); logger.debug('rapido configuration loaded in ' + (Date.now() - startTime) + 'ms'); }, disableAllStacks: function() { this.commands.disableAllStacks(); }, getStack: function(stackName) { return this.commands.getStack(stackName); }, enableStack: function(stackName) { if (!this.initialized) { rapido.additionalEnabledStacks.push(stackName); return; } var stack = this.commands.getStack(stackName); if (!stack) { throw new Error("Unable to enable stack. Invalid stack: " + stackName); } this.commands.enableStack(stackName); }, isStackEnabled: function(stackName) { return this.commands.isStackEnabled(stackName); }, getStacks: function() { return this.commands.getStacks(); }, stackExists: function(stackName) { return this.commands.getStack(stackName) != null; }, getEnabledStacks: function(stackName) { return this.commands.getEnabledStacks(); }, registerStack: function(stackConfig) { this.commands.registerStack(stackConfig); }, registerCommand: function(stackName, commandName, commandConfig) { this.commands.registerCommand(stackName, commandName, commandConfig); }, getCommandModule: function(stackName, commandName) { var command = rapido.commands.getCommand(stackName, commandName); return command.getModule(); }, runCommand: function(stackName, commandName, options) { var command; if (typeof arguments[0] !== 'string') { options = arguments[1]; command = arguments[0]; commandName = command.name; stackName = command.stack.name; } if (!stackName) { stackName = "default"; } if (typeof stackName !== 'string') { throw new Error('Unable to run command. Invalid stack name: ' + stackName); } if (typeof commandName !== 'string') { throw new Error('Unable to run command. Invalid command name: ' + commandName); } command = rapido.getCommandModule(stackName, commandName); try { return command.run(options || {}, rapido.config, rapido); } catch(e) { if (e === '') { throw e; } throw createError( new Error('Error while executing command "' + commandName + '" (' + stackName + '). Error: ' + (e.stack || e)), e); } }, padding: function(str, targetLen) { var padding = ''; for (var i=0, len=targetLen-str.length; i<len; i++) { padding += ' '; } return padding; }, rightPad: function(str, len) { while(str.length<len) { str += ' '; } return str; }, leftPad: function(str, len) { while(str.length<len) { str = ' ' + str; } return str; }, writeConfig: function(jsonFile, config) { if (typeof jsonFile === 'string') { jsonFile = new File(jsonFile); } jsonFile.writeAsString(JSON.stringify(config, null, " ")); }, findClosestConfigFile: function(paths, configFilename) { if (!paths) { paths = buildDefaultUserConfigPaths(); } if (!configFilename) { configFilename = rapido.configFilename; } for (var i=0, len=paths.length; i<len; i++) { var configFile = new File(paths[i], configFilename); if (configFile.exists()) { return configFile; } } // Config file not found... create it as a child of the first path return new File(paths[0], configFilename); }, findClosestConfig: function(paths, configFilename) { var file = rapido.findClosestConfigFile(paths, configFilename); var config; if (file.exists()) { config = JSON.parse(file.readAsString()); } else { config = {}; } return { file: file, exists: file.exists(), config: config, path: this.relativePath(file) }; }, updateConfig: function(jsonFile, newProps) { if (arguments.length === 1) { newProps = arguments[0]; jsonFile = rapido.findClosestConfigFile(); } var config, updated = false; if (jsonFile.exists()) { updated = true; config = JSON.parse(jsonFile.readAsString()); } else { config = {}; } var newPropsArray = []; forEachEntry(newProps, function(name, value) { if (config[name] !== value) { newPropsArray.push(' ' + name + ': ' + value); } }); var path = this.relativePath(jsonFile); if (newPropsArray.length) { extend(config, newProps); jsonFile.writeAsString(JSON.stringify(config, null, " ")); rapido.log.info('update', 'Updated the following properties in "' + path + '":'); newPropsArray.forEach(function(newPropStr) { rapido.log.info('update', newPropStr); }); } else { rapido.log.info('skip', 'No changes to "' + path + '"'); } return { config: config, file: jsonFile, path: this.relativePath(jsonFile), updated: updated }; }, isCommandEnabled: function(stackName, commandName) { return this.commands.isCommandEnabled(stackName, commandName); }, _parseArgs: function(args) { this.init(); var commandParts = [], optionArgs = [], matchingCommands = [], optionsStarted = false; //console.error('CONFIG: ', config); /* * Separate out option args from the command */ args.forEach(function(arg) { if (arg.startsWith('-') || optionsStarted) { optionsStarted = true; optionArgs.push(arg); } else { commandParts.push(arg); } }); if (commandParts.length === 0) { commandParts = ['default']; } var enabledStacks = this.getEnabledStacks(); var commandName; for (var i=commandParts.length-1; i>=0; i--) { commandName = commandParts.slice(0, i+1).join(' '); enabledStacks.forEach(function(stack) { var command = stack.getCommand(commandName); if (command && this.commands.isCommandEnabled(stack.name, commandName)) { matchingCommands.push(command); } }, this); if (matchingCommands.length) { if (i<commandParts.length-1) { optionArgs = commandParts.slice(i+1).concat(optionArgs); } break; } } if (!matchingCommands.length) { commandName = commandParts.join(' '); } function forEachCommand(callback, thisObj) { matchingCommands.forEach(callback, thisObj); } return { matchingCommands: matchingCommands, optionArgs: optionArgs, commandName: commandName, hasMatchingCommands: matchingCommands.length > 0, forEachCommand: forEachCommand }; }, /** * This method is used by the Bash shell to support * command line auto-completion for command names * and command options. * * See: * http://www.gnu.org/software/bash/manual/bash.html#Programmable-Completion-Builtins * * @param {String} line The auto-completion line (i.e. COMP_LINE) * @param {int} cursorPos The index into the line where the cursor is * @return {void} */ outputCompletions: function(line, cursorPos) { this.init(); if (cursorPos == null) { cursorPos = line.length; } /* Use cases: 1) Completion on option value: rap some command --options opt Expected: (no completions) 2) Completion on option name rap some comm Expected: Complete last words of any matching commands 3) Completion on rap rap Expected: Output first word for all enabled commends */ // We only care about the string up to the cursor line = line.substring(0, cursorPos); // Chop off the shell command name used to invoke Rapido var firstWordRegExp = /^([^\s]+)\s*/g; var firstWordMatches = firstWordRegExp.exec(line); if (firstWordMatches) { line = line.substring(firstWordRegExp.lastIndex); } // First find where the options start: var optionsStartMatches = /\s-/.exec(line); var optionsStart = -1; if (optionsStartMatches) { optionsStart = optionsStartMatches.index; } // Find where the current word is based on the cursor position var currentWord; var currentWordStart = -1; var currentWordRegExp = /([^\s^]+)$/g; var currentWordMatches = currentWordRegExp.exec(line); if (currentWordMatches) { currentWord = currentWordMatches[1]; currentWordStart = currentWordMatches.index; } else { currentWord = ''; currentWordStart = line.length; } var completions = []; var foundCompletions = {}; function add(completion) { if (!foundCompletions[completion]) { foundCompletions[completion] = true; completions.push(completion); } } if (optionsStart !== -1) { if (/^--?.*$/.test(currentWord)) { // The current word is an option name so we are expected to // have a complete command var parseInfo = this._parseArgs(line.split(/\s+/)); if (!parseInfo.hasMatchingCommands) { // Invalid command...we cannot complete the option names return; } // Loop over all the matching commands to look for matching options parseInfo.forEachCommand(function(command) { // Get a list of allowed options for the current command as an array // NOTE: The "-" and "--" prefixes will be included for each option var allowedOptions = command.getAllowedOptions(); allowedOptions.forEach(function(allowedOption) { if (allowedOption.startsWith(currentWord)) { add(allowedOption); } }); }, this); } else { // We are after the options, but the current word is not an option // name so we can't auto-complete the current word return; } } else { var enabledStacks = this.getEnabledStacks(); enabledStacks.forEach(function(stack) { stack.forEachCommand(function(command) { if (!this.isCommandEnabled(stack.name, command.name)) { return; } if (!command.listed) { return; } // Find the start of the last word if (command.name.startsWith(line)) { add(command.name.substring(currentWordStart)); } }, this); }, this); } if (completions.length) { completions.sort(function(a, b) { a = a.toLowerCase(); b = b.toLowerCase(); return a < b ? -1 : (a === b ? 0 : 1); }); require('util').print(completions.join('\n')); } }, _doRun: function(args) { this.init(); if (process.env.COMP_LINE) { this.outputCompletions(process.env.COMP_LINE, process.env.COMP_POINT); return; } args = args.slice(2); function onError(err) { if (err.message === 'canceled') { rapido.log.error('ABORT', 'Command canceled'); return; } rapido.log.error('ERROR', '' + (err.stack || err)); process.exit(1); } var parseInfo = this._parseArgs(args); var commandName = parseInfo.commandName; var matchingCommands = parseInfo.matchingCommands; var optionArgs = parseInfo.optionArgs; if (!matchingCommands.length) { rapido.log.error('Command not found: ' + commandName); return process.exit(1); } function invokeCommand(command) { // console.log('Running command "' + command.name + '" (stack: ' + command.stack.name + ')...\n'); try { var commandFilePath = command.file.getAbsolutePath(); var commandModule = require(commandFilePath); var commandArgs; var options = command.getOptions(); if (options) { var optimistCommand = optimist(optionArgs); if (commandModule.usage) { optimistCommand.usage(commandModule.usage.replace(/\$0/g, $0)); } optimistCommand.options(options); commandArgs = optimistCommand.argv; if (commandArgs.help) { command.showHelp(null, optimistCommand); return; } if (commandModule.validate) { try { var newCommandArgs = commandModule.validate(commandArgs, rapido); if (newCommandArgs) { commandArgs = newCommandArgs; } } catch(e) { optimistCommand.showHelp(console.log); rapido.log('Invalid args: ' + (e.message || e)); return; } } } else if (commandModule.parseOptions) { commandArgs = commandModule.parseOptions(optionArgs, rapido); } var output = rapido.runCommand(command, commandArgs); if (output && typeof output.fail === 'function') { output.fail(function(err) { if (err.message === 'canceled') { rapido.log.error('ABORT', 'Command canceled'); return; } onError(err); }); } // Time not valid for async commands... // logger.debug('Command completed in ' + (Date.now() - startTime) + 'ms'); } catch(e) { if (e !== '') { onError(e); } } } if (matchingCommands.length > 1) { console.log('Multiple matching commands found. Choose a command:'); matchingCommands.forEach(function(command, i) { console.log('[' + (i+1) + '] ' + command.name + ' (' + command.stack.name + ')'); }); var prompt = rapido.prompt; prompt.start(); prompt.get( { name: "index", description: 'Command number [default: 1]', // Prompt displayed to the user. If not supplied name will be used. //type: 'number', // Specify the type of input to expect. pattern: /^[0-9]+$/, // Regular expression that input must be valid against. message: 'Choose a valid command', // Warning message to display if validation fails. conform: function(value) { var index = parseInt(value, 10); return index >= 1 && index <= matchingCommands.length; }, before: function(value) { return value === '' ? '1' : value; } }, function (err, result) { if (err) { return onError(err); } var index = parseInt(result.index, 10)-1; invokeCommand(matchingCommands[index]); }); } else { invokeCommand(matchingCommands[0]); } }, run: function(args) { exports.instance = rapido; var globalNodeModulesDir = new File(path.dirname(process.execPath), '../lib/node_modules'); if (globalNodeModulesDir.exists()) { rapido.globalNodeModulesDir = globalNodeModulesDir; rapido._doRun(args); } else { var npm = require("npm"); npm.load({}, function (er) { rapido.globalNodeModulesDir = new File(npm.globalDir); rapido._doRun(args); }); } }, relativePath: function(filePath) { if (filePath instanceof File) { filePath = filePath.getAbsolutePath(); } return path.relative(process.cwd(), filePath); }, addNodeModulesDir: function(path) { rapido.additionalNodeModulesDirs.push(path); }, addStackDir: function(path) { rapido.additionalStackDirs.push(path); }, prompt: require('./prompt') }; Object.defineProperty(rapido, 'util', { get: function() { return require('./util/util'); } }); Object.defineProperty(rapido, 'projectManager', { get: function() { return require('./util/project-manager'); } }); function logColor(color, args) { if (args.length === 2) { var label = '[' + args[0] + ']', message = args[1]; rapido.log((label[color]) + ' ' + message); } else { var output = args[0] ? args[0][color] : null; rapido.log(output); } } function logSuccess(message) { logColor('green', arguments); } function logDebug(message) { logColor('cyan', arguments); } function logInfo(message) { logColor('cyan', arguments); } function logError(message) { logColor('red', arguments); } function logWarn(message) { logColor('yellow', arguments); } extend(rapido.log, { success: logSuccess, debug: logDebug, info: logInfo, error: logError, warn: logWarn }); if (options) { require('./options-loader').load(rapido, options); } return rapido; };