├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── autocomplete.js ├── command-instance.js ├── command.js ├── history.js ├── intercept.js ├── local-storage.js ├── logger.js ├── option.js ├── session.js ├── ui.js ├── util.js ├── vorpal-commons.js └── vorpal.js ├── examples ├── README.md ├── calculator │ ├── README.md │ └── calculator.js ├── descriptors │ └── descriptors.js ├── mode │ ├── README.md │ └── mode.js └── prompt │ └── prompt.js ├── gulpfile.babel.js ├── lib ├── autocomplete.js ├── command-instance.js ├── command.js ├── history.js ├── intercept.js ├── local-storage.js ├── logger.js ├── option.js ├── session.js ├── ui.js ├── util.js ├── vorpal-commons.js └── vorpal.js ├── package.json └── test ├── autocomplete.js ├── index.js ├── integration.js ├── util ├── server.js └── util.js └── vorpal.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | pids 3 | *.pid 4 | *.seed 5 | *sublime* 6 | lib-cov 7 | coverage 8 | .grunt 9 | dist/.local_storage* 10 | build/Release 11 | node_modules 12 | test/util/playground.js 13 | .idea/ 14 | electron* 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dthree/vorpal/51f5e2b545631b6a86c9781c274a1b0916a67ee8/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "10" 6 | sudo: false 7 | cache: 8 | directories: 9 | - "node_modules" 10 | branches: 11 | only: 12 | - master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015-2016 DC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vorpal 2 | 3 | 4 | [![Build Status](https://travis-ci.org/dthree/vorpal.svg)](https://travis-ci.org/dthree/vorpal/) 5 | 6 | NPM Downloads 7 | 8 | [![Package Quality](http://npm.packagequality.com/shield/vorpal.svg)](http://packagequality.com/#?package=vorpal) 9 | 10 | NPM Version 11 | 12 | [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 13 | 14 | > Conquer the command-line. 15 | 16 | ```text 17 | (O) 18 | 22 | \| ^^^^^^ \:W/------------------------------------------------'''''' 23 | o Build a popular framework based on the [Jabberwocky](https://en.wikipedia.org/wiki/Jabberwocky) poem. 218 | 219 | 220 | ## License 221 | 222 | MIT © [David Caccavella](https://github.com/dthree) 223 | -------------------------------------------------------------------------------- /dist/autocomplete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var strip = require('strip-ansi'); 5 | 6 | var autocomplete = { 7 | 8 | /** 9 | * Handles tabbed autocompletion. 10 | * 11 | * - Initial tabbing lists all registered commands. 12 | * - Completes a command halfway typed. 13 | * - Recognizes options and lists all possible options. 14 | * - Recognizes option arguments and lists them. 15 | * - Supports cursor positions anywhere in the string. 16 | * - Supports piping. 17 | * 18 | * @param {String} str 19 | * @return {String} cb 20 | * @api public 21 | */ 22 | 23 | exec: function exec(str, cb) { 24 | var self = this; 25 | var input = parseInput(str, this.parent.ui._activePrompt.screen.rl.cursor); 26 | var commands = getCommandNames(this.parent.commands); 27 | var vorpalMatch = getMatch(input.context, commands, { ignoreSlashes: true }); 28 | var freezeTabs = false; 29 | 30 | function end(str) { 31 | var res = handleTabCounts.call(self, str, freezeTabs); 32 | cb(undefined, res); 33 | } 34 | 35 | function evaluateTabs(input) { 36 | if (input.context && input.context[input.context.length - 1] === '/') { 37 | freezeTabs = true; 38 | } 39 | } 40 | 41 | if (vorpalMatch) { 42 | input.context = vorpalMatch; 43 | evaluateTabs(input); 44 | end(assembleInput(input)); 45 | return; 46 | } 47 | 48 | input = getMatchObject.call(this, input, commands); 49 | if (input.match) { 50 | input = parseMatchSection.call(this, input); 51 | getMatchData.call(self, input, function (data) { 52 | var dataMatch = getMatch(input.context, data); 53 | if (dataMatch) { 54 | input.context = dataMatch; 55 | evaluateTabs(input); 56 | end(assembleInput(input)); 57 | return; 58 | } 59 | end(filterData(input.context, data)); 60 | }); 61 | return; 62 | } 63 | end(filterData(input.context, commands)); 64 | }, 65 | 66 | /** 67 | * Independent / stateless auto-complete function. 68 | * Parses an array of strings for the best match. 69 | * 70 | * @param {String} str 71 | * @param {Array} arr 72 | * @return {String} 73 | * @api private 74 | */ 75 | 76 | match: function match(str, arr, options) { 77 | arr = arr || []; 78 | options = options || {}; 79 | arr.sort(); 80 | var arrX = _.clone(arr); 81 | var strX = String(str); 82 | 83 | var prefix = ''; 84 | 85 | if (options.ignoreSlashes !== true) { 86 | var parts = strX.split('/'); 87 | strX = parts.pop(); 88 | prefix = parts.join('/'); 89 | prefix = parts.length > 0 ? prefix + '/' : prefix; 90 | } 91 | 92 | var matches = []; 93 | for (var i = 0; i < arrX.length; i++) { 94 | if (strip(arrX[i]).slice(0, strX.length) === strX) { 95 | matches.push(arrX[i]); 96 | } 97 | } 98 | if (matches.length === 1) { 99 | // If we have a slash, don't add a space after match. 100 | var space = String(strip(matches[0])).slice(strip(matches[0]).length - 1) === '/' ? '' : ' '; 101 | return prefix + matches[0] + space; 102 | } else if (matches.length === 0) { 103 | return undefined; 104 | } else if (strX.length === 0) { 105 | return matches; 106 | } 107 | 108 | var longestMatchLength = matches.reduce(function (previous, current) { 109 | for (var i = 0; i < current.length; i++) { 110 | if (previous[i] && current[i] !== previous[i]) { 111 | return current.substr(0, i); 112 | } 113 | } 114 | return previous; 115 | }).length; 116 | 117 | // couldn't resolve any further, return all matches 118 | if (longestMatchLength === strX.length) { 119 | return matches; 120 | } 121 | 122 | // return the longest matching portion along with the prefix 123 | return prefix + matches[0].substr(0, longestMatchLength); 124 | } 125 | }; 126 | 127 | /** 128 | * Tracks how many times tab was pressed 129 | * based on whether the UI changed. 130 | * 131 | * @param {String} str 132 | * @return {String} result 133 | * @api private 134 | */ 135 | 136 | function handleTabCounts(str, freezeTabs) { 137 | var result; 138 | if (_.isArray(str)) { 139 | this._tabCtr += 1; 140 | if (this._tabCtr > 1) { 141 | result = str.length === 0 ? undefined : str; 142 | } 143 | } else { 144 | this._tabCtr = freezeTabs === true ? this._tabCtr + 1 : 0; 145 | result = str; 146 | } 147 | return result; 148 | } 149 | 150 | /** 151 | * Looks for a potential exact match 152 | * based on given data. 153 | * 154 | * @param {String} ctx 155 | * @param {Array} data 156 | * @return {String} 157 | * @api private 158 | */ 159 | 160 | function getMatch(ctx, data, options) { 161 | // Look for a command match, eliminating and then 162 | // re-introducing leading spaces. 163 | var len = ctx.length; 164 | var trimmed = ctx.replace(/^\s+/g, ''); 165 | var match = autocomplete.match(trimmed, data.slice(), options); 166 | if (_.isArray(match)) { 167 | return match; 168 | } 169 | var prefix = new Array(len - trimmed.length + 1).join(' '); 170 | // If we get an autocomplete match on a command, finish it. 171 | if (match) { 172 | // Put the leading spaces back in. 173 | match = prefix + match; 174 | return match; 175 | } 176 | return undefined; 177 | } 178 | 179 | /** 180 | * Takes the input object and assembles 181 | * the final result to display on the screen. 182 | * 183 | * @param {Object} input 184 | * @return {String} 185 | * @api private 186 | */ 187 | 188 | function assembleInput(input) { 189 | if (_.isArray(input.context)) { 190 | return input.context; 191 | } 192 | var result = (input.prefix || '') + (input.context || '') + (input.suffix || ''); 193 | return strip(result); 194 | } 195 | 196 | /** 197 | * Reduces an array of possible 198 | * matches to list based on a given 199 | * string. 200 | * 201 | * @param {String} str 202 | * @param {Array} data 203 | * @return {Array} 204 | * @api private 205 | */ 206 | 207 | function filterData(str, data) { 208 | data = data || []; 209 | var ctx = String(str || '').trim(); 210 | var slashParts = ctx.split('/'); 211 | ctx = slashParts.pop(); 212 | var wordParts = String(ctx).trim().split(' '); 213 | var res = data.filter(function (item) { 214 | return strip(item).slice(0, ctx.length) === ctx; 215 | }); 216 | res = res.map(function (item) { 217 | var parts = String(item).trim().split(' '); 218 | if (parts.length > 1) { 219 | parts = parts.slice(wordParts.length); 220 | return parts.join(' '); 221 | } 222 | return item; 223 | }); 224 | return res; 225 | } 226 | 227 | /** 228 | * Takes the user's current prompt 229 | * string and breaks it into its 230 | * integral parts for analysis and 231 | * modification. 232 | * 233 | * @param {String} str 234 | * @param {Integer} idx 235 | * @return {Object} 236 | * @api private 237 | */ 238 | 239 | function parseInput(str, idx) { 240 | var raw = String(str || ''); 241 | var sliced = raw.slice(0, idx); 242 | var sections = sliced.split('|'); 243 | var prefix = sections.slice(0, sections.length - 1) || []; 244 | prefix.push(''); 245 | prefix = prefix.join('|'); 246 | var suffix = getSuffix(raw.slice(idx)); 247 | var context = sections[sections.length - 1]; 248 | return { 249 | raw: raw, 250 | prefix: prefix, 251 | suffix: suffix, 252 | context: context 253 | }; 254 | } 255 | 256 | /** 257 | * Takes the context after a 258 | * matched command and figures 259 | * out the applicable context, 260 | * including assigning its role 261 | * such as being an option 262 | * parameter, etc. 263 | * 264 | * @param {Object} input 265 | * @return {Object} 266 | * @api private 267 | */ 268 | 269 | function parseMatchSection(input) { 270 | var parts = (input.context || '').split(' '); 271 | var last = parts.pop(); 272 | var beforeLast = strip(parts[parts.length - 1] || '').trim(); 273 | if (beforeLast.slice(0, 1) === '-') { 274 | input.option = beforeLast; 275 | } 276 | input.context = last; 277 | input.prefix = (input.prefix || '') + parts.join(' ') + ' '; 278 | return input; 279 | } 280 | 281 | /** 282 | * Returns a cleaned up version of the 283 | * remaining text to the right of the cursor. 284 | * 285 | * @param {String} suffix 286 | * @return {String} 287 | * @api private 288 | */ 289 | 290 | function getSuffix(suffix) { 291 | suffix = suffix.slice(0, 1) === ' ' ? suffix : suffix.replace(/.+?(?=\s)/, ''); 292 | suffix = suffix.slice(1, suffix.length); 293 | return suffix; 294 | } 295 | 296 | /** 297 | * Compile all available commands and aliases 298 | * in alphabetical order. 299 | * 300 | * @param {Array} cmds 301 | * @return {Array} 302 | * @api private 303 | */ 304 | 305 | function getCommandNames(cmds) { 306 | var commands = _.map(cmds, '_name'); 307 | commands = commands.concat.apply(commands, _.map(cmds, '_aliases')); 308 | commands.sort(); 309 | return commands; 310 | } 311 | 312 | /** 313 | * When we know that we've 314 | * exceeded a known command, grab 315 | * on to that command and return it, 316 | * fixing the overall input context 317 | * at the same time. 318 | * 319 | * @param {Object} input 320 | * @param {Array} commands 321 | * @return {Object} 322 | * @api private 323 | */ 324 | 325 | function getMatchObject(input, commands) { 326 | var len = input.context.length; 327 | var trimmed = String(input.context).replace(/^\s+/g, ''); 328 | var prefix = new Array(len - trimmed.length + 1).join(' '); 329 | var match; 330 | var suffix; 331 | commands.forEach(function (cmd) { 332 | var nextChar = trimmed.substr(cmd.length, 1); 333 | if (trimmed.substr(0, cmd.length) === cmd && String(cmd).trim() !== '' && nextChar === ' ') { 334 | match = cmd; 335 | suffix = trimmed.substr(cmd.length); 336 | prefix += trimmed.substr(0, cmd.length); 337 | } 338 | }); 339 | 340 | var matchObject = match ? _.find(this.parent.commands, { _name: String(match).trim() }) : undefined; 341 | 342 | if (!matchObject) { 343 | this.parent.commands.forEach(function (cmd) { 344 | if ((cmd._aliases || []).indexOf(String(match).trim()) > -1) { 345 | matchObject = cmd; 346 | } 347 | return; 348 | }); 349 | } 350 | 351 | if (!matchObject) { 352 | matchObject = _.find(this.parent.commands, { _catch: true }); 353 | if (matchObject) { 354 | suffix = input.context; 355 | } 356 | } 357 | 358 | if (!matchObject) { 359 | prefix = input.context; 360 | suffix = ''; 361 | } 362 | 363 | if (matchObject) { 364 | input.match = matchObject; 365 | input.prefix += prefix; 366 | input.context = suffix; 367 | } 368 | return input; 369 | } 370 | 371 | /** 372 | * Takes a known matched command, and reads 373 | * the applicable data by calling its autocompletion 374 | * instructions, whether it is the command's 375 | * autocompletion or one of its options. 376 | * 377 | * @param {Object} input 378 | * @param {Function} cb 379 | * @return {Array} 380 | * @api private 381 | */ 382 | 383 | function getMatchData(input, cb) { 384 | var string = input.context; 385 | var cmd = input.match; 386 | var midOption = String(string).trim().slice(0, 1) === '-'; 387 | var afterOption = input.option !== undefined; 388 | if (midOption === true && !cmd._allowUnknownOptions) { 389 | var results = []; 390 | for (var i = 0; i < cmd.options.length; ++i) { 391 | var long = cmd.options[i].long; 392 | var short = cmd.options[i].short; 393 | if (!long && short) { 394 | results.push(short); 395 | } else if (long) { 396 | results.push(long); 397 | } 398 | } 399 | cb(results); 400 | return; 401 | } 402 | 403 | function handleDataFormat(str, config, callback) { 404 | var data = []; 405 | if (_.isArray(config)) { 406 | data = config; 407 | } else if (_.isFunction(config)) { 408 | var cbk = config.length < 2 ? function () {} : function (res) { 409 | callback(res || []); 410 | }; 411 | var res = config(str, cbk); 412 | if (res && _.isFunction(res.then)) { 413 | res.then(function (resp) { 414 | callback(resp); 415 | }).catch(function (err) { 416 | callback(err); 417 | }); 418 | } else if (config.length < 2) { 419 | callback(res); 420 | } 421 | return; 422 | } 423 | callback(data); 424 | return; 425 | } 426 | 427 | if (afterOption === true) { 428 | var opt = strip(input.option).trim(); 429 | var shortMatch = _.find(cmd.options, { short: opt }); 430 | var longMatch = _.find(cmd.options, { long: opt }); 431 | var match = longMatch || shortMatch; 432 | if (match) { 433 | var config = match.autocomplete; 434 | handleDataFormat(string, config, cb); 435 | return; 436 | } 437 | } 438 | 439 | var conf = cmd._autocomplete; 440 | conf = conf && conf.data ? conf.data : conf; 441 | handleDataFormat(string, conf, cb); 442 | return; 443 | } 444 | 445 | module.exports = autocomplete; -------------------------------------------------------------------------------- /dist/command-instance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | var util = require('./util'); 12 | var _ = require('lodash'); 13 | 14 | var CommandInstance = function () { 15 | 16 | /** 17 | * Initialize a new `CommandInstance` instance. 18 | * 19 | * @param {Object} params 20 | * @return {CommandInstance} 21 | * @api public 22 | */ 23 | 24 | function CommandInstance() { 25 | var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, 26 | command = _ref.command, 27 | commandObject = _ref.commandObject, 28 | args = _ref.args, 29 | commandWrapper = _ref.commandWrapper, 30 | callback = _ref.callback, 31 | downstream = _ref.downstream; 32 | 33 | _classCallCheck(this, CommandInstance); 34 | 35 | this.command = command; 36 | this.commandObject = commandObject; 37 | this.args = args; 38 | this.commandWrapper = commandWrapper; 39 | this.session = commandWrapper.session; 40 | this.parent = this.session.parent; 41 | this.callback = callback; 42 | this.downstream = downstream; 43 | } 44 | 45 | /** 46 | * Cancel running command. 47 | */ 48 | 49 | _createClass(CommandInstance, [{ 50 | key: 'cancel', 51 | value: function cancel() { 52 | this.session.emit('vorpal_command_cancel'); 53 | } 54 | 55 | /** 56 | * Route stdout either through a piped command, or the session's stdout. 57 | */ 58 | 59 | }, { 60 | key: 'log', 61 | value: function log() { 62 | var _this = this; 63 | 64 | var args = util.fixArgsForApply(arguments); 65 | if (this.downstream) { 66 | var fn = this.downstream.commandObject._fn || function () {}; 67 | this.session.registerCommand(); 68 | this.downstream.args.stdin = args; 69 | var onComplete = function onComplete(err) { 70 | if (_this.session.isLocal() && err) { 71 | _this.session.log(err.stack || err); 72 | _this.session.parent.emit('client_command_error', { command: _this.downstream.command, error: err }); 73 | } 74 | _this.session.completeCommand(); 75 | }; 76 | 77 | var validate = this.downstream.commandObject._validate; 78 | if (_.isFunction(validate)) { 79 | try { 80 | validate.call(this.downstream, this.downstream.args); 81 | } catch (e) { 82 | // Log error without piping to downstream on validation error. 83 | this.session.log(e.toString()); 84 | onComplete(); 85 | return; 86 | } 87 | } 88 | 89 | var res = fn.call(this.downstream, this.downstream.args, onComplete); 90 | if (res && _.isFunction(res.then)) { 91 | res.then(onComplete, onComplete); 92 | } 93 | } else { 94 | this.session.log.apply(this.session, args); 95 | } 96 | } 97 | }, { 98 | key: 'prompt', 99 | value: function prompt(a, b, c) { 100 | return this.session.prompt(a, b, c); 101 | } 102 | }, { 103 | key: 'delimiter', 104 | value: function delimiter(a, b, c) { 105 | return this.session.delimiter(a, b, c); 106 | } 107 | }, { 108 | key: 'help', 109 | value: function help(a, b, c) { 110 | return this.session.help(a, b, c); 111 | } 112 | }, { 113 | key: 'match', 114 | value: function match(a, b, c) { 115 | return this.session.match(a, b, c); 116 | } 117 | }]); 118 | 119 | return CommandInstance; 120 | }(); 121 | 122 | module.exports = CommandInstance; -------------------------------------------------------------------------------- /dist/command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var EventEmitter = require('events').EventEmitter; 8 | var Option = require('./option'); 9 | var VorpalUtil = require('./util'); 10 | var _ = require('lodash'); 11 | 12 | /** 13 | * Command prototype. 14 | */ 15 | 16 | var command = Command.prototype; 17 | 18 | /** 19 | * Expose `Command`. 20 | */ 21 | 22 | module.exports = exports = Command; 23 | 24 | /** 25 | * Initialize a new `Command` instance. 26 | * 27 | * @param {String} name 28 | * @param {Vorpal} parent 29 | * @return {Command} 30 | * @api public 31 | */ 32 | 33 | function Command(name, parent) { 34 | if (!(this instanceof Command)) { 35 | return new Command(); 36 | } 37 | this.commands = []; 38 | this.options = []; 39 | this._args = []; 40 | this._aliases = []; 41 | this._name = name; 42 | this._relay = false; 43 | this._hidden = false; 44 | this._parent = parent; 45 | this._mode = false; 46 | this._catch = false; 47 | this._help = undefined; 48 | this._init = undefined; 49 | this._after = undefined; 50 | this._allowUnknownOptions = false; 51 | } 52 | 53 | /** 54 | * Registers an option for given command. 55 | * 56 | * @param {String} flags 57 | * @param {String} description 58 | * @param {Function} fn 59 | * @param {String} defaultValue 60 | * @return {Command} 61 | * @api public 62 | */ 63 | 64 | command.option = function (flags, description, autocomplete) { 65 | var self = this; 66 | var option = new Option(flags, description, autocomplete); 67 | var oname = option.name(); 68 | var name = _camelcase(oname); 69 | var defaultValue; 70 | 71 | // preassign default value only for --no-*, [optional], or 72 | if (option.bool === false || option.optional || option.required) { 73 | // when --no-* we make sure default is true 74 | if (option.bool === false) { 75 | defaultValue = true; 76 | } 77 | // preassign only if we have a default 78 | if (defaultValue !== undefined) { 79 | self[name] = defaultValue; 80 | } 81 | } 82 | 83 | // register the option 84 | this.options.push(option); 85 | 86 | // when it's passed assign the value 87 | // and conditionally invoke the callback 88 | this.on(oname, function (val) { 89 | // unassigned or bool 90 | if (typeof self[name] === 'boolean' || typeof self[name] === 'undefined') { 91 | // if no value, bool true, and we have a default, then use it! 92 | if (val === null) { 93 | self[name] = option.bool ? defaultValue || true : false; 94 | } else { 95 | self[name] = val; 96 | } 97 | } else if (val !== null) { 98 | // reassign 99 | self[name] = val; 100 | } 101 | }); 102 | 103 | return this; 104 | }; 105 | 106 | /** 107 | * Defines an action for a given command. 108 | * 109 | * @param {Function} fn 110 | * @return {Command} 111 | * @api public 112 | */ 113 | 114 | command.action = function (fn) { 115 | var self = this; 116 | self._fn = fn; 117 | return this; 118 | }; 119 | 120 | /** 121 | * Let's you compose other funtions to extend the command. 122 | * 123 | * @param {Function} fn 124 | * @return {Command} 125 | * @api public 126 | */ 127 | 128 | command.use = function (fn) { 129 | return fn(this); 130 | }; 131 | 132 | /** 133 | * Defines a function to validate arguments 134 | * before action is performed. Arguments 135 | * are valid if no errors are thrown from 136 | * the function. 137 | * 138 | * @param fn 139 | * @returns {Command} 140 | * @api public 141 | */ 142 | command.validate = function (fn) { 143 | var self = this; 144 | self._validate = fn; 145 | return this; 146 | }; 147 | 148 | /** 149 | * Defines a function to be called when the 150 | * command is canceled. 151 | * 152 | * @param fn 153 | * @returns {Command} 154 | * @api public 155 | */ 156 | command.cancel = function (fn) { 157 | this._cancel = fn; 158 | return this; 159 | }; 160 | 161 | /** 162 | * Defines a method to be called when 163 | * the command set has completed. 164 | * 165 | * @param {Function} fn 166 | * @return {Command} 167 | * @api public 168 | */ 169 | 170 | command.done = function (fn) { 171 | this._done = fn; 172 | return this; 173 | }; 174 | 175 | /** 176 | * Defines tabbed auto-completion 177 | * for the given command. Favored over 178 | * deprecated command.autocompletion. 179 | * 180 | * @param {Function} fn 181 | * @return {Command} 182 | * @api public 183 | */ 184 | 185 | command.autocomplete = function (obj) { 186 | this._autocomplete = obj; 187 | return this; 188 | }; 189 | 190 | /** 191 | * Defines tabbed auto-completion rules 192 | * for the given command. 193 | * 194 | * @param {Function} fn 195 | * @return {Command} 196 | * @api public 197 | */ 198 | 199 | command.autocompletion = function (param) { 200 | this._parent._useDeprecatedAutocompletion = true; 201 | if (!_.isFunction(param) && !_.isObject(param)) { 202 | throw new Error('An invalid object type was passed into the first parameter of command.autocompletion: function expected.'); 203 | } 204 | 205 | this._autocompletion = param; 206 | return this; 207 | }; 208 | 209 | /** 210 | * Defines an init action for a mode command. 211 | * 212 | * @param {Function} fn 213 | * @return {Command} 214 | * @api public 215 | */ 216 | 217 | command.init = function (fn) { 218 | var self = this; 219 | if (self._mode !== true) { 220 | throw Error('Cannot call init from a non-mode action.'); 221 | } 222 | self._init = fn; 223 | return this; 224 | }; 225 | 226 | /** 227 | * Defines a prompt delimiter for a 228 | * mode once entered. 229 | * 230 | * @param {String} delimiter 231 | * @return {Command} 232 | * @api public 233 | */ 234 | 235 | command.delimiter = function (delimiter) { 236 | this._delimiter = delimiter; 237 | return this; 238 | }; 239 | 240 | /** 241 | * Sets args for static typing of options 242 | * using minimist. 243 | * 244 | * @param {Object} types 245 | * @return {Command} 246 | * @api public 247 | */ 248 | 249 | command.types = function (types) { 250 | var supported = ['string', 'boolean']; 251 | for (var item in types) { 252 | if (supported.indexOf(item) === -1) { 253 | throw new Error('An invalid type was passed into command.types(): ' + item); 254 | } 255 | types[item] = !_.isArray(types[item]) ? [types[item]] : types[item]; 256 | } 257 | this._types = types; 258 | return this; 259 | }; 260 | 261 | /** 262 | * Defines an alias for a given command. 263 | * 264 | * @param {String} alias 265 | * @return {Command} 266 | * @api public 267 | */ 268 | 269 | command.alias = function () { 270 | var self = this; 271 | for (var i = 0; i < arguments.length; ++i) { 272 | var alias = arguments[i]; 273 | if (_.isArray(alias)) { 274 | for (var j = 0; j < alias.length; ++j) { 275 | this.alias(alias[j]); 276 | } 277 | return this; 278 | } 279 | this._parent.commands.forEach(function (cmd) { 280 | if (!_.isEmpty(cmd._aliases)) { 281 | if (_.includes(cmd._aliases, alias)) { 282 | var msg = 'Duplicate alias "' + alias + '" for command "' + self._name + '" detected. Was first reserved by command "' + cmd._name + '".'; 283 | throw new Error(msg); 284 | } 285 | } 286 | }); 287 | this._aliases.push(alias); 288 | } 289 | return this; 290 | }; 291 | 292 | /** 293 | * Defines description for given command. 294 | * 295 | * @param {String} str 296 | * @return {Command} 297 | * @api public 298 | */ 299 | 300 | command.description = function (str) { 301 | if (arguments.length === 0) { 302 | return this._description; 303 | } 304 | this._description = str; 305 | return this; 306 | }; 307 | 308 | /** 309 | * Removes self from Vorpal instance. 310 | * 311 | * @return {Command} 312 | * @api public 313 | */ 314 | 315 | command.remove = function () { 316 | var self = this; 317 | this._parent.commands = _.reject(this._parent.commands, function (command) { 318 | if (command._name === self._name) { 319 | return true; 320 | } 321 | }); 322 | return this; 323 | }; 324 | 325 | /** 326 | * Returns the commands arguments as string. 327 | * 328 | * @param {String} desc 329 | * @return {String} 330 | * @api public 331 | */ 332 | 333 | command.arguments = function (desc) { 334 | return this._parseExpectedArgs(desc.split(/ +/)); 335 | }; 336 | 337 | /** 338 | * Returns the help info for given command. 339 | * 340 | * @return {String} 341 | * @api public 342 | */ 343 | 344 | command.helpInformation = function () { 345 | var desc = []; 346 | var cmdName = this._name; 347 | var alias = ''; 348 | 349 | if (this._description) { 350 | desc = [' ' + this._description, '']; 351 | } 352 | 353 | if (this._aliases.length > 0) { 354 | alias = ' Alias: ' + this._aliases.join(' | ') + '\n'; 355 | } 356 | var usage = ['', ' Usage: ' + cmdName + ' ' + this.usage(), '']; 357 | 358 | var cmds = []; 359 | 360 | var help = String(this.optionHelp().replace(/^/gm, ' ')); 361 | var options = [' Options:', '', help, '']; 362 | 363 | var res = usage.concat(cmds).concat(alias).concat(desc).concat(options).join('\n'); 364 | 365 | res = res.replace(/\n\n\n/g, '\n\n'); 366 | 367 | return res; 368 | }; 369 | 370 | /** 371 | * Doesn't show command in the help menu. 372 | * 373 | * @return {Command} 374 | * @api public 375 | */ 376 | 377 | command.hidden = function () { 378 | this._hidden = true; 379 | return this; 380 | }; 381 | 382 | /** 383 | * Allows undeclared options to be passed in with the command. 384 | * 385 | * @param {Boolean} [allowUnknownOptions=true] 386 | * @return {Command} 387 | * @api public 388 | */ 389 | 390 | command.allowUnknownOptions = function () { 391 | var allowUnknownOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; 392 | 393 | allowUnknownOptions = allowUnknownOptions === "false" ? false : allowUnknownOptions; 394 | 395 | this._allowUnknownOptions = !!allowUnknownOptions; 396 | return this; 397 | }; 398 | 399 | /** 400 | * Returns the command usage string for help. 401 | * 402 | * @param {String} str 403 | * @return {String} 404 | * @api public 405 | */ 406 | 407 | command.usage = function (str) { 408 | var args = this._args.map(function (arg) { 409 | return VorpalUtil.humanReadableArgName(arg); 410 | }); 411 | 412 | var usage = '[options]' + (this.commands.length ? ' [command]' : '') + (this._args.length ? ' ' + args.join(' ') : ''); 413 | 414 | if (arguments.length === 0) { 415 | return this._usage || usage; 416 | } 417 | 418 | this._usage = str; 419 | 420 | return this; 421 | }; 422 | 423 | /** 424 | * Returns the help string for the command's options. 425 | * 426 | * @return {String} 427 | * @api public 428 | */ 429 | 430 | command.optionHelp = function () { 431 | var width = this._largestOptionLength(); 432 | 433 | // Prepend the help information 434 | return [VorpalUtil.pad('--help', width) + ' output usage information'].concat(this.options.map(function (option) { 435 | return VorpalUtil.pad(option.flags, width) + ' ' + option.description; 436 | })).join('\n'); 437 | }; 438 | 439 | /** 440 | * Returns the length of the longest option. 441 | * 442 | * @return {Integer} 443 | * @api private 444 | */ 445 | 446 | command._largestOptionLength = function () { 447 | return this.options.reduce(function (max, option) { 448 | return Math.max(max, option.flags.length); 449 | }, 0); 450 | }; 451 | 452 | /** 453 | * Adds a custom handling for the --help flag. 454 | * 455 | * @param {Function} fn 456 | * @return {Command} 457 | * @api public 458 | */ 459 | 460 | command.help = function (fn) { 461 | if (_.isFunction(fn)) { 462 | this._help = fn; 463 | } 464 | return this; 465 | }; 466 | 467 | /** 468 | * Edits the raw command string before it 469 | * is executed. 470 | * 471 | * @param {String} str 472 | * @return {String} str 473 | * @api public 474 | */ 475 | 476 | command.parse = function (fn) { 477 | if (_.isFunction(fn)) { 478 | this._parse = fn; 479 | } 480 | return this; 481 | }; 482 | 483 | /** 484 | * Adds a command to be executed after command completion. 485 | * 486 | * @param {Function} fn 487 | * @return {Command} 488 | * @api public 489 | */ 490 | 491 | command.after = function (fn) { 492 | if (_.isFunction(fn)) { 493 | this._after = fn; 494 | } 495 | return this; 496 | }; 497 | 498 | /** 499 | * Parses and returns expected command arguments. 500 | * 501 | * @param {String} args 502 | * @return {Array} 503 | * @api private 504 | */ 505 | 506 | command._parseExpectedArgs = function (args) { 507 | if (!args.length) { 508 | return; 509 | } 510 | var self = this; 511 | args.forEach(function (arg) { 512 | var argDetails = { 513 | required: false, 514 | name: '', 515 | variadic: false 516 | }; 517 | 518 | switch (arg[0]) { 519 | case '<': 520 | argDetails.required = true; 521 | argDetails.name = arg.slice(1, -1); 522 | break; 523 | case '[': 524 | argDetails.name = arg.slice(1, -1); 525 | break; 526 | default: 527 | break; 528 | } 529 | 530 | if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') { 531 | argDetails.variadic = true; 532 | argDetails.name = argDetails.name.slice(0, -3); 533 | } 534 | if (argDetails.name) { 535 | self._args.push(argDetails); 536 | } 537 | }); 538 | 539 | // If the user entered args in a weird order, 540 | // properly sequence them. 541 | if (self._args.length > 1) { 542 | self._args = self._args.sort(function (argu1, argu2) { 543 | if (argu1.required && !argu2.required) { 544 | return -1; 545 | } else if (argu2.required && !argu1.required) { 546 | return 1; 547 | } else if (argu1.variadic && !argu2.variadic) { 548 | return 1; 549 | } else if (argu2.variadic && !argu1.variadic) { 550 | return -1; 551 | } 552 | return 0; 553 | }); 554 | } 555 | 556 | return; 557 | }; 558 | 559 | /** 560 | * Converts string to camel case. 561 | * 562 | * @param {String} flag 563 | * @return {String} 564 | * @api private 565 | */ 566 | 567 | function _camelcase(flag) { 568 | return flag.split('-').reduce(function (str, word) { 569 | return str + word[0].toUpperCase() + word.slice(1); 570 | }); 571 | } 572 | 573 | /** 574 | * Make command an EventEmitter. 575 | */ 576 | 577 | command.__proto__ = EventEmitter.prototype; -------------------------------------------------------------------------------- /dist/history.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var LocalStorage = require('node-localstorage').LocalStorage; 5 | var path = require('path'); 6 | var os = require('os'); 7 | 8 | // Number of command histories kept in persistent storage 9 | var HISTORY_SIZE = 500; 10 | 11 | var temp = path.normalize(path.join(os.tmpdir(), '/.local_storage')); 12 | var DEFAULT_STORAGE_PATH = temp; 13 | 14 | var History = function History() { 15 | this._storageKey = undefined; 16 | 17 | // Prompt Command History 18 | // Histctr moves based on number of times 'up' (+= ctr) 19 | // or 'down' (-= ctr) was pressed in traversing 20 | // command history. 21 | this._hist = []; 22 | this._histCtr = 0; 23 | 24 | // When in a 'mode', we reset the 25 | // history and store it in a cache until 26 | // exiting the 'mode', at which point we 27 | // resume the original history. 28 | this._histCache = []; 29 | this._histCtrCache = 0; 30 | }; 31 | 32 | /** 33 | * Initialize the history with local storage data 34 | * Called from setId when history id is set 35 | */ 36 | 37 | History.prototype._init = function () { 38 | if (!this._storageKey) { 39 | return; 40 | } 41 | 42 | // Load history from local storage 43 | var persistedHistory = JSON.parse(this._localStorage.getItem(this._storageKey)); 44 | if (_.isArray(persistedHistory)) { 45 | Array.prototype.push.apply(this._hist, persistedHistory); 46 | } 47 | }; 48 | 49 | /** 50 | * Set id for this history instance. 51 | * Calls init internally to initialize 52 | * the history with the id. 53 | */ 54 | 55 | History.prototype.setId = function (id) { 56 | // Initialize a localStorage instance with default 57 | // path if it is not initialized 58 | if (!this._localStorage) { 59 | this._localStorage = new LocalStorage(DEFAULT_STORAGE_PATH); 60 | } 61 | this._storageKey = 'cmd_history_' + id; 62 | this._init(); 63 | }; 64 | 65 | /** 66 | * Initialize a local storage instance with 67 | * the path if not already initialized. 68 | * 69 | * @param path 70 | */ 71 | 72 | History.prototype.setStoragePath = function (path) { 73 | if (!this._localStorage) { 74 | this._localStorage = new LocalStorage(path); 75 | } 76 | }; 77 | 78 | /** 79 | * Get previous history. Called when up is pressed. 80 | * 81 | * @return {String} 82 | */ 83 | 84 | History.prototype.getPreviousHistory = function () { 85 | this._histCtr++; 86 | this._histCtr = this._histCtr > this._hist.length ? this._hist.length : this._histCtr; 87 | return this._hist[this._hist.length - this._histCtr]; 88 | }; 89 | 90 | /** 91 | * Get next history. Called when down is pressed. 92 | * 93 | * @return {String} 94 | */ 95 | 96 | History.prototype.getNextHistory = function () { 97 | this._histCtr--; 98 | 99 | // Return empty prompt if the we dont have any history to show 100 | if (this._histCtr < 1) { 101 | this._histCtr = 0; 102 | return ''; 103 | } 104 | 105 | return this._hist[this._hist.length - this._histCtr]; 106 | }; 107 | 108 | /** 109 | * Peek into history, without changing state 110 | * 111 | * @return {String} 112 | */ 113 | 114 | History.prototype.peek = function (depth) { 115 | depth = depth || 0; 116 | return this._hist[this._hist.length - 1 - depth]; 117 | }; 118 | 119 | /** 120 | * A new command was submitted. Called when enter is pressed and the prompt is not empty. 121 | * 122 | * @param cmd 123 | */ 124 | 125 | History.prototype.newCommand = function (cmd) { 126 | // Always reset history when new command is executed. 127 | this._histCtr = 0; 128 | 129 | // Don't store command in history if it's a duplicate. 130 | if (this._hist[this._hist.length - 1] === cmd) { 131 | return; 132 | } 133 | 134 | // Push into history. 135 | this._hist.push(cmd); 136 | 137 | // Only persist history when not in mode 138 | if (this._storageKey && !this._inMode) { 139 | var persistedHistory = this._hist; 140 | var historyLen = this._hist.length; 141 | if (historyLen > HISTORY_SIZE) { 142 | persistedHistory = this._hist.slice(historyLen - HISTORY_SIZE - 1, historyLen - 1); 143 | } 144 | 145 | // Add to local storage 146 | this._localStorage.setItem(this._storageKey, JSON.stringify(persistedHistory)); 147 | } 148 | }; 149 | 150 | /** 151 | * Called when entering a mode 152 | */ 153 | 154 | History.prototype.enterMode = function () { 155 | // Reassign the command history to a 156 | // cache, replacing it with a blank 157 | // history for the mode. 158 | this._histCache = _.clone(this._hist); 159 | this._histCtrCache = parseFloat(this._histCtr); 160 | this._hist = []; 161 | this._histCtr = 0; 162 | this._inMode = true; 163 | }; 164 | 165 | /** 166 | * Called when exiting a mode 167 | */ 168 | 169 | History.prototype.exitMode = function () { 170 | this._hist = this._histCache; 171 | this._histCtr = this._histCtrCache; 172 | this._histCache = []; 173 | this._histCtrCache = 0; 174 | this._inMode = false; 175 | }; 176 | 177 | /** 178 | * Clears the command history 179 | * (Currently only used in unit test) 180 | */ 181 | 182 | History.prototype.clear = function () { 183 | if (this._storageKey) { 184 | this._localStorage.removeItem(this._storageKey); 185 | } 186 | }; 187 | 188 | module.exports = History; -------------------------------------------------------------------------------- /dist/intercept.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var _ = require('lodash'); 8 | 9 | /** 10 | * Intercepts stdout, passes thru callback 11 | * also pass console.error thru stdout so it goes to callback too 12 | * (stdout.write and stderr.write are both refs to the same stream.write function) 13 | * returns an unhook() function, call when done intercepting 14 | * 15 | * @param {Function} callback 16 | * @return {Function} 17 | */ 18 | 19 | module.exports = function (callback) { 20 | var oldStdoutWrite = process.stdout.write; 21 | var oldConsoleError = console.error; 22 | process.stdout.write = function (write) { 23 | return function (string) { 24 | var args = _.toArray(arguments); 25 | args[0] = interceptor(string); 26 | write.apply(process.stdout, args); 27 | }; 28 | }(process.stdout.write); 29 | 30 | console.error = function () { 31 | return function () { 32 | var args = _.toArray(arguments); 33 | args.unshift('\x1b[31m[ERROR]\x1b[0m'); 34 | console.log.apply(console.log, args); 35 | }; 36 | }(console.error); 37 | 38 | function interceptor(string) { 39 | // only intercept the string 40 | var result = callback(string); 41 | if (typeof result === 'string') { 42 | string = result.replace(/\n$/, '') + (result && /\n$/.test(string) ? '\n' : ''); 43 | } 44 | return string; 45 | } 46 | // puts back to original 47 | return function unhook() { 48 | process.stdout.write = oldStdoutWrite; 49 | console.error = oldConsoleError; 50 | }; 51 | }; -------------------------------------------------------------------------------- /dist/local-storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LocalStorageO = require('node-localstorage').LocalStorage; 4 | var path = require('path'); 5 | var os = require('os'); 6 | var temp = path.normalize(path.join(os.tmpdir(), '/.local_storage_')); 7 | var DEFAULT_STORAGE_PATH = temp; 8 | 9 | var LocalStorage = { 10 | setId: function setId(id) { 11 | if (id === undefined) { 12 | throw new Error('vorpal.localStorage() requires a unique key to be passed in.'); 13 | } 14 | if (!this._localStorage) { 15 | this._localStorage = new LocalStorageO(DEFAULT_STORAGE_PATH + id); 16 | } 17 | }, 18 | validate: function validate() { 19 | if (this._localStorage === undefined) { 20 | throw new Error('Vorpal.localStorage() was not initialized before writing data.'); 21 | } 22 | }, 23 | getItem: function getItem(key, value) { 24 | this.validate(); 25 | return this._localStorage.getItem(key, value); 26 | }, 27 | setItem: function setItem(key, value) { 28 | this.validate(); 29 | return this._localStorage.setItem(key, value); 30 | }, 31 | removeItem: function removeItem(key) { 32 | this.validate(); 33 | return this._localStorage.removeItem(key); 34 | } 35 | }; 36 | 37 | module.exports = LocalStorage; -------------------------------------------------------------------------------- /dist/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var _ = require('lodash'); 8 | var util = require('./util'); 9 | var ut = require('util'); 10 | 11 | /** 12 | * Initialize a new `Logger` instance. 13 | * 14 | * @return {Logger} 15 | * @api public 16 | */ 17 | 18 | function viewed(str) { 19 | var re = /\u001b\[\d+m/gm; 20 | return String(str).replace(re, ''); 21 | } 22 | 23 | function trimTo(str, amt) { 24 | var raw = ''; 25 | var visual = viewed(str).slice(0, amt); 26 | var result = ''; 27 | for (var i = 0; i < str.length; ++i) { 28 | raw += str[i]; 29 | if (viewed(raw) === visual) { 30 | result = raw;break; 31 | } 32 | } 33 | 34 | if (result.length < amt - 10) { 35 | return result; 36 | } 37 | 38 | var newResult = result;var found = false; 39 | for (var j = result.length; j > 0; --j) { 40 | if (result[j] === ' ') { 41 | found = true; 42 | break; 43 | } else { 44 | newResult = newResult.slice(0, newResult.length - 1); 45 | } 46 | } 47 | 48 | if (found === true) { 49 | return newResult; 50 | } 51 | 52 | return result; 53 | } 54 | 55 | function Logger(cons) { 56 | var logger = cons || console; 57 | log = function log() { 58 | logger.log.apply(logger, arguments); 59 | }; 60 | 61 | log.cols = function () { 62 | var width = process.stdout.columns; 63 | var pads = 0; 64 | var padsWidth = 0; 65 | var cols = 0; 66 | var colsWidth = 0; 67 | var input = arguments; 68 | 69 | for (var h = 0; h < arguments.length; ++h) { 70 | if (typeof arguments[h] === 'number') { 71 | padsWidth += arguments[h]; 72 | pads++; 73 | } 74 | if (_.isArray(arguments[h]) && typeof arguments[h][0] === 'number') { 75 | padsWidth += arguments[h][0]; 76 | pads++; 77 | } 78 | } 79 | 80 | cols = arguments.length - pads; 81 | colsWidth = Math.floor((width - padsWidth) / cols); 82 | 83 | var lines = []; 84 | 85 | var go = function go() { 86 | var str = ''; 87 | var done = true; 88 | for (var i = 0; i < input.length; ++i) { 89 | if (typeof input[i] === 'number') { 90 | str += util.pad('', input[i], ' '); 91 | } else if (_.isArray(input[i]) && typeof input[i][0] === 'number') { 92 | str += util.pad('', input[i][0], input[i][1]); 93 | } else { 94 | var chosenWidth = colsWidth + 0; 95 | var trimmed = trimTo(input[i], colsWidth); 96 | var trimmedLength = trimmed.length; 97 | var re = /\\u001b\[\d+m/gm; 98 | var matches = ut.inspect(trimmed).match(re); 99 | var color = ''; 100 | // Ugh. We're chopping a line, so we have to look for unfinished 101 | // color assignments and throw them on the next line. 102 | if (matches && matches[matches.length - 1] !== '\\u001b[39m') { 103 | trimmed += '\x1B[39m'; 104 | var number = String(matches[matches.length - 1]).slice(7, 9); 105 | color = '\x1B[' + number + 'm'; 106 | } 107 | input[i] = color + String(input[i].slice(trimmedLength, input[i].length)).trim(); 108 | str += util.pad(String(trimmed).trim(), chosenWidth, ' '); 109 | if (viewed(input[i]).trim() !== '') { 110 | done = false; 111 | } 112 | } 113 | } 114 | lines.push(str); 115 | if (!done) { 116 | go(); 117 | } 118 | }; 119 | go(); 120 | for (var i = 0; i < lines.length; ++i) { 121 | logger.log(lines[i]); 122 | } 123 | return this; 124 | }; 125 | 126 | log.br = function () { 127 | logger.log(' '); 128 | return this; 129 | }; 130 | 131 | return this.log; 132 | } 133 | 134 | /** 135 | * Expose `logger`. 136 | */ 137 | 138 | module.exports = exports = Logger; -------------------------------------------------------------------------------- /dist/option.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | var Option = function () { 8 | 9 | /** 10 | * Initialize a new `Option` instance. 11 | * 12 | * @param {String} flags 13 | * @param {String} description 14 | * @param {Autocomplete} autocomplete 15 | * @return {Option} 16 | * @api public 17 | */ 18 | 19 | function Option(flags, description, autocomplete) { 20 | _classCallCheck(this, Option); 21 | 22 | this.flags = flags; 23 | this.required = ~flags.indexOf('<'); 24 | this.optional = ~flags.indexOf('['); 25 | this.bool = !~flags.indexOf('-no-'); 26 | this.autocomplete = autocomplete; 27 | flags = flags.split(/[ ,|]+/); 28 | if (flags.length > 1 && !/^[[<]/.test(flags[1])) { 29 | this.assignFlag(flags.shift()); 30 | } 31 | this.assignFlag(flags.shift()); 32 | this.description = description || ''; 33 | } 34 | 35 | /** 36 | * Return option name. 37 | * 38 | * @return {String} 39 | * @api private 40 | */ 41 | 42 | _createClass(Option, [{ 43 | key: 'name', 44 | value: function name() { 45 | if (this.long !== undefined) { 46 | return this.long.replace('--', '').replace('no-', ''); 47 | } 48 | return this.short.replace('-', ''); 49 | } 50 | 51 | /** 52 | * Check if `arg` matches the short or long flag. 53 | * 54 | * @param {String} arg 55 | * @return {Boolean} 56 | * @api private 57 | */ 58 | 59 | }, { 60 | key: 'is', 61 | value: function is(arg) { 62 | return arg === this.short || arg === this.long; 63 | } 64 | 65 | /** 66 | * Assigned flag to either long or short. 67 | * 68 | * @param {String} flag 69 | * @api private 70 | */ 71 | 72 | }, { 73 | key: 'assignFlag', 74 | value: function assignFlag(flag) { 75 | if (flag.startsWith('--')) { 76 | this.long = flag; 77 | } else { 78 | this.short = flag; 79 | } 80 | } 81 | }]); 82 | 83 | return Option; 84 | }(); 85 | 86 | /** 87 | * Expose `Option`. 88 | */ 89 | 90 | module.exports = exports = Option; -------------------------------------------------------------------------------- /dist/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var EventEmitter = require('events').EventEmitter; 8 | var os = require('os'); 9 | var _ = require('lodash'); 10 | var util = require('./util'); 11 | var autocomplete = require('./autocomplete'); 12 | var CommandInstance = require('./command-instance'); 13 | 14 | /** 15 | * Initialize a new `Session` instance. 16 | * 17 | * @param {String} name 18 | * @return {Session} 19 | * @api public 20 | */ 21 | 22 | function Session(options) { 23 | options = options || {}; 24 | this.id = options.id || this._guid(); 25 | this.parent = options.parent || undefined; 26 | this.authenticating = options.authenticating || false; 27 | this.authenticated = options.authenticated || undefined; 28 | this.user = options.user || 'guest'; 29 | this.host = options.host; 30 | this.address = options.address || undefined; 31 | this._isLocal = options.local || undefined; 32 | this._delimiter = options.delimiter || String(os.hostname()).split('.')[0] + '~$'; 33 | this._modeDelimiter = undefined; 34 | 35 | // Keeps history of how many times in a row `tab` was 36 | // pressed on the keyboard. 37 | this._tabCtr = 0; 38 | 39 | this.cmdHistory = this.parent.cmdHistory; 40 | 41 | // Special command mode vorpal is in at the moment, 42 | // such as REPL. See mode documentation. 43 | this._mode = undefined; 44 | 45 | return this; 46 | } 47 | 48 | /** 49 | * Extend Session prototype as an event emitter. 50 | */ 51 | 52 | Session.prototype = Object.create(EventEmitter.prototype); 53 | 54 | /** 55 | * Session prototype. 56 | */ 57 | 58 | var session = Session.prototype; 59 | 60 | /** 61 | * Expose `Session`. 62 | */ 63 | 64 | module.exports = exports = Session; 65 | 66 | /** 67 | * Pipes logging data through any piped 68 | * commands, and then sends it to ._log for 69 | * actual logging. 70 | * 71 | * @param {String} [... arguments] 72 | * @return {Session} 73 | * @api public 74 | */ 75 | 76 | session.log = function () { 77 | var args = util.fixArgsForApply(arguments); 78 | return this._log.apply(this, args); 79 | }; 80 | 81 | /** 82 | * Routes logging for a given session. 83 | * is on a local TTY, or remote. 84 | * 85 | * @param {String} [... arguments] 86 | * @return {Session} 87 | * @api public 88 | */ 89 | 90 | session._log = function () { 91 | var self = this; 92 | if (this.isLocal()) { 93 | this.parent.ui.log.apply(this.parent.ui, arguments); 94 | } else { 95 | // If it's an error, expose the stack. Otherwise 96 | // we get a helpful '{}'. 97 | var args = []; 98 | for (var i = 0; i < arguments.length; ++i) { 99 | var str = arguments[i]; 100 | str = str && str.stack ? 'Error: ' + str.message : str; 101 | args.push(str); 102 | } 103 | self.parent._send('vantage-ssn-stdout-downstream', 'downstream', { sessionId: self.id, value: args }); 104 | } 105 | return this; 106 | }; 107 | 108 | /** 109 | * Returns whether given session 110 | * is on a local TTY, or remote. 111 | * 112 | * @return {Boolean} 113 | * @api public 114 | */ 115 | 116 | session.isLocal = function () { 117 | return this._isLocal; 118 | }; 119 | 120 | /** 121 | * Maps to vorpal.prompt for a session 122 | * context. 123 | * 124 | * @param {Object} options 125 | * @param {Function} cb 126 | * @api public 127 | */ 128 | 129 | session.prompt = function (options, cb) { 130 | options = options || {}; 131 | options.sessionId = this.id; 132 | return this.parent.prompt(options, cb); 133 | }; 134 | 135 | /** 136 | * Gets the full (normal + mode) delimiter 137 | * for this session. 138 | * 139 | * @return {String} 140 | * @api public 141 | */ 142 | 143 | session.fullDelimiter = function () { 144 | var result = this._delimiter + (this._modeDelimiter !== undefined ? this._modeDelimiter : ''); 145 | return result; 146 | }; 147 | 148 | /** 149 | * Sets the delimiter for this session. 150 | * 151 | * @param {String} str 152 | * @return {Session} 153 | * @api public 154 | */ 155 | 156 | session.delimiter = function (str) { 157 | if (str === undefined) { 158 | return this._delimiter; 159 | } 160 | this._delimiter = String(str).trim() + ' '; 161 | if (this.isLocal()) { 162 | this.parent.ui.refresh(); 163 | } else { 164 | this.parent._send('vantage-delimiter-downstream', 'downstream', { value: str, sessionId: this.id }); 165 | } 166 | return this; 167 | }; 168 | 169 | /** 170 | * Sets the mode delimiter for this session. 171 | * 172 | * @param {String} str 173 | * @return {Session} 174 | * @api public 175 | */ 176 | 177 | session.modeDelimiter = function (str) { 178 | var self = this; 179 | if (str === undefined) { 180 | return this._modeDelimiter; 181 | } 182 | if (!this.isLocal()) { 183 | self.parent._send('vantage-mode-delimiter-downstream', 'downstream', { value: str, sessionId: self.id }); 184 | } else { 185 | if (str === false || str === 'false') { 186 | this._modeDelimiter = undefined; 187 | } else { 188 | this._modeDelimiter = String(str).trim() + ' '; 189 | } 190 | this.parent.ui.refresh(); 191 | } 192 | return this; 193 | }; 194 | 195 | /** 196 | * Returns the result of a keypress 197 | * string, depending on the type. 198 | * 199 | * @param {String} key 200 | * @param {String} value 201 | * @return {Function} 202 | * @api private 203 | */ 204 | 205 | session.getKeypressResult = function (key, value, cb) { 206 | cb = cb || function () {}; 207 | var keyMatch = ['up', 'down', 'tab'].indexOf(key) > -1; 208 | if (key !== 'tab') { 209 | this._tabCtr = 0; 210 | } 211 | if (keyMatch) { 212 | if (['up', 'down'].indexOf(key) > -1) { 213 | cb(undefined, this.getHistory(key)); 214 | } else if (key === 'tab') { 215 | // If the Vorpal user has any commands that use 216 | // command.autocompletion, defer to the deprecated 217 | // version of autocompletion. Otherwise, default 218 | // to the new version. 219 | var fn = this.parent._useDeprecatedAutocompletion ? 'getAutocompleteDeprecated' : 'getAutocomplete'; 220 | this[fn](value, function (err, data) { 221 | cb(err, data); 222 | }); 223 | } 224 | } else { 225 | this._histCtr = 0; 226 | } 227 | }; 228 | 229 | session.history = function (str) { 230 | var exceptions = []; 231 | if (str && exceptions.indexOf(String(str).toLowerCase()) === -1) { 232 | this.cmdHistory.newCommand(str); 233 | } 234 | }; 235 | 236 | /** 237 | * New autocomplete. 238 | * 239 | * @param {String} str 240 | * @param {Function} cb 241 | * @api private 242 | */ 243 | 244 | session.getAutocomplete = function (str, cb) { 245 | return autocomplete.exec.call(this, str, cb); 246 | }; 247 | 248 | /** 249 | * Deprecated autocomplete - being deleted 250 | * in Vorpal 2.0. 251 | * 252 | * @param {String} str 253 | * @param {Function} cb 254 | * @api private 255 | */ 256 | 257 | session.getAutocompleteDeprecated = function (str, cb) { 258 | cb = cb || function () {}; 259 | 260 | // Entire command string 261 | var cursor = this.parent.ui._activePrompt.screen.rl.cursor; 262 | var trimmed = String(str).trim(); 263 | var cut = String(trimmed).slice(0, cursor); 264 | var remainder = String(trimmed).slice(cursor, trimmed.length).replace(/ +$/, ''); 265 | trimmed = cut; 266 | 267 | // Set "trimmed" to command string after pipe 268 | // Set "pre" to command string, pipe, and a space 269 | var pre = ''; 270 | var lastPipeIndex = trimmed.lastIndexOf('|'); 271 | if (lastPipeIndex !== -1) { 272 | pre = trimmed.substr(0, lastPipeIndex + 1) + ' '; 273 | trimmed = trimmed.substr(lastPipeIndex + 1).trim(); 274 | } 275 | 276 | // Complete command 277 | var names = _.map(this.parent.commands, '_name'); 278 | names = names.concat.apply(names, _.map(this.parent.commands, '_aliases')); 279 | var result = this._autocomplete(trimmed, names); 280 | if (result && trimmed.length < String(result).trim().length) { 281 | cb(undefined, pre + result + remainder); 282 | return; 283 | } 284 | 285 | // Find custom autocompletion 286 | var match; 287 | var extra; 288 | 289 | names.forEach(function (name) { 290 | if (trimmed.substr(0, name.length) === name && String(name).trim() !== '') { 291 | match = name; 292 | extra = trimmed.substr(name.length).trim(); 293 | } 294 | }); 295 | 296 | var command = match ? _.find(this.parent.commands, { _name: match }) : undefined; 297 | 298 | if (!command) { 299 | command = _.find(this.parent.commands, { _catch: true }); 300 | if (command) { 301 | extra = trimmed; 302 | } 303 | } 304 | 305 | if (command && _.isFunction(command._autocompletion)) { 306 | this._tabCtr++; 307 | command._autocompletion.call(this, extra, this._tabCtr, function (err, autocomplete) { 308 | if (err) { 309 | return cb(err); 310 | } 311 | if (_.isArray(autocomplete)) { 312 | return cb(undefined, autocomplete); 313 | } else if (autocomplete === undefined) { 314 | return cb(undefined, undefined); 315 | } 316 | return cb(undefined, pre + autocomplete + remainder); 317 | }); 318 | } else { 319 | cb(undefined, undefined); 320 | } 321 | }; 322 | 323 | session._autocomplete = function (str, arr) { 324 | return autocomplete.match.call(this, str, arr); 325 | }; 326 | 327 | /** 328 | * Public facing autocomplete helper. 329 | * 330 | * @param {String} str 331 | * @param {Array} arr 332 | * @return {String} 333 | * @api public 334 | */ 335 | 336 | session.help = function (command) { 337 | this.log(this.parent._commandHelp(command || '')); 338 | }; 339 | 340 | /** 341 | * Public facing autocomplete helper. 342 | * 343 | * @param {String} str 344 | * @param {Array} arr 345 | * @return {String} 346 | * @api public 347 | */ 348 | 349 | session.match = function (str, arr) { 350 | return this._autocomplete(str, arr); 351 | }; 352 | 353 | /** 354 | * Gets a new command set ready. 355 | * 356 | * @return {session} 357 | * @api public 358 | */ 359 | 360 | session.execCommandSet = function (wrapper, callback) { 361 | var self = this; 362 | var response = {}; 363 | var res; 364 | var cbk = callback; 365 | this._registeredCommands = 1; 366 | this._completedCommands = 0; 367 | 368 | // Create the command instance for the first 369 | // command and hook it up to the pipe chain. 370 | var commandInstance = new CommandInstance({ 371 | downstream: wrapper.pipes[0], 372 | commandObject: wrapper.commandObject, 373 | commandWrapper: wrapper 374 | }); 375 | 376 | wrapper.commandInstance = commandInstance; 377 | 378 | function sendDones(itm) { 379 | if (itm.commandObject && itm.commandObject._done) { 380 | itm.commandObject._done.call(itm); 381 | } 382 | if (itm.downstream) { 383 | sendDones(itm.downstream); 384 | } 385 | } 386 | 387 | // Called when command is cancelled 388 | this.cancelCommands = function () { 389 | var callCancel = function callCancel(commandInstance) { 390 | if (_.isFunction(commandInstance.commandObject._cancel)) { 391 | commandInstance.commandObject._cancel.call(commandInstance); 392 | } 393 | 394 | if (commandInstance.downstream) { 395 | callCancel(commandInstance.downstream); 396 | } 397 | }; 398 | 399 | callCancel(wrapper.commandInstance); 400 | 401 | // Check if there is a cancel method on the promise 402 | if (res && _.isFunction(res.cancel)) { 403 | res.cancel(wrapper.commandInstance); 404 | } 405 | 406 | self.removeListener('vorpal_command_cancel', self.cancelCommands); 407 | self.cancelCommands = undefined; 408 | self._commandSetCallback = undefined; 409 | self._registeredCommands = 0; 410 | self._completedCommands = 0; 411 | self.parent.emit('client_command_cancelled', { command: wrapper.command }); 412 | 413 | cbk(wrapper); 414 | }; 415 | 416 | this.on('vorpal_command_cancel', self.cancelCommands); 417 | 418 | // Gracefully handles all instances of the command completing. 419 | this._commandSetCallback = function () { 420 | var err = response.error; 421 | var data = response.data; 422 | var argus = response.args; 423 | if (self.isLocal() && err) { 424 | var stack; 425 | if (data && data.stack) { 426 | stack = data.stack; 427 | } else if (err && err.stack) { 428 | stack = err.stack; 429 | } else { 430 | stack = err; 431 | } 432 | self.log(stack); 433 | self.parent.emit('client_command_error', { command: wrapper.command, error: err }); 434 | } else if (self.isLocal()) { 435 | self.parent.emit('client_command_executed', { command: wrapper.command }); 436 | } 437 | 438 | self.removeListener('vorpal_command_cancel', self.cancelCommands); 439 | self.cancelCommands = undefined; 440 | cbk(wrapper, err, data, argus); 441 | sendDones(commandInstance); 442 | }; 443 | 444 | function onCompletion(wrapper, err, data, argus) { 445 | response = { 446 | error: err, 447 | data: data, 448 | args: argus 449 | }; 450 | self.completeCommand(); 451 | } 452 | 453 | var valid; 454 | if (_.isFunction(wrapper.validate)) { 455 | try { 456 | valid = wrapper.validate.call(commandInstance, wrapper.args); 457 | } catch (e) { 458 | // Complete with error on validation error 459 | onCompletion(wrapper, e); 460 | return this; 461 | } 462 | } 463 | 464 | if (valid !== true && valid !== undefined) { 465 | onCompletion(wrapper, valid || null); 466 | return this; 467 | } 468 | 469 | // Call the root command. 470 | res = wrapper.fn.call(commandInstance, wrapper.args, function () { 471 | var argus = util.fixArgsForApply(arguments); 472 | onCompletion(wrapper, argus[0], argus[1], argus); 473 | }); 474 | 475 | // If the command as declared by the user 476 | // returns a promise, handle accordingly. 477 | if (res && _.isFunction(res.then)) { 478 | res.then(function (data) { 479 | onCompletion(wrapper, undefined, data); 480 | }).catch(function (err) { 481 | onCompletion(wrapper, true, err); 482 | }); 483 | } 484 | 485 | return this; 486 | }; 487 | 488 | /** 489 | * Adds on a command or sub-command in progress. 490 | * Session keeps tracked of commands, 491 | * and as soon as all commands have been 492 | * compelted, the session returns the entire 493 | * command set as complete. 494 | * 495 | * @return {session} 496 | * @api public 497 | */ 498 | 499 | session.registerCommand = function () { 500 | this._registeredCommands = this._registeredCommands || 0; 501 | this._registeredCommands++; 502 | return this; 503 | }; 504 | 505 | /** 506 | * Marks a command or subcommand as having completed. 507 | * If all commands have completed, calls back 508 | * to the root command as being done. 509 | * 510 | * @return {session} 511 | * @api public 512 | */ 513 | 514 | session.completeCommand = function () { 515 | this._completedCommands++; 516 | if (this._registeredCommands <= this._completedCommands) { 517 | this._registeredCommands = 0; 518 | this._completedCommands = 0; 519 | if (this._commandSetCallback) { 520 | this._commandSetCallback(); 521 | } 522 | this._commandSetCallback = undefined; 523 | } 524 | return this; 525 | }; 526 | 527 | /** 528 | * Returns the appropriate command history 529 | * string based on an 'Up' or 'Down' arrow 530 | * key pressed by the user. 531 | * 532 | * @param {String} direction 533 | * @return {String} 534 | * @api private 535 | */ 536 | 537 | session.getHistory = function (direction) { 538 | var history; 539 | if (direction === 'up') { 540 | history = this.cmdHistory.getPreviousHistory(); 541 | } else if (direction === 'down') { 542 | history = this.cmdHistory.getNextHistory(); 543 | } 544 | return history; 545 | }; 546 | 547 | /** 548 | * Generates random GUID for Session ID. 549 | * 550 | * @return {GUID} 551 | * @api private 552 | */ 553 | 554 | session._guid = function () { 555 | function s4() { 556 | return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 557 | } 558 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); 559 | }; -------------------------------------------------------------------------------- /dist/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | var _ = require('lodash'); 10 | var minimist = require('minimist'); 11 | var strip = require('strip-ansi'); 12 | 13 | var util = { 14 | /** 15 | * Parses command arguments from multiple 16 | * sources. 17 | * 18 | * @param {String} str 19 | * @param {Object} opts 20 | * @return {Array} 21 | * @api private 22 | */ 23 | 24 | parseArgs: function parseArgs(str, opts) { 25 | var reg = /"(.*?)"|'(.*?)'|`(.*?)`|([^\s"]+)/gi; 26 | var arr = []; 27 | var match = void 0; 28 | do { 29 | match = reg.exec(str); 30 | if (match !== null) { 31 | arr.push(match[1] || match[2] || match[3] || match[4]); 32 | } 33 | } while (match !== null); 34 | 35 | arr = minimist(arr, opts); 36 | arr._ = arr._ || []; 37 | return arr; 38 | }, 39 | 40 | /** 41 | * Prepares a command and all its parts for execution. 42 | * 43 | * @param {String} command 44 | * @param {Array} commands 45 | * @return {Object} 46 | * @api public 47 | */ 48 | 49 | parseCommand: function parseCommand(command, commands) { 50 | var self = this; 51 | var pipes = []; 52 | var match = void 0; 53 | var matchArgs = void 0; 54 | var matchParts = void 0; 55 | 56 | function parsePipes() { 57 | // First, split the command by pipes naively. 58 | // This will split command arguments in half when the argument contains a pipe character. 59 | // For example, say "(Vorpal|vorpal)" will be split into ['say "(Vorpal', 'vorpal)'] which isn't good. 60 | var naivePipes = String(command).trim().split('|'); 61 | 62 | // Contruct empty array to place correctly split commands into. 63 | var newPipes = []; 64 | 65 | // We will look for pipe characters within these quotes to rejoin together. 66 | var quoteChars = ['"', '\'', '`']; 67 | 68 | // This will expand to contain one boolean key for each type of quote. 69 | // The value keyed by the quote is toggled off and on as quote type is opened and closed. 70 | // Example { "`": true, "'": false } would mean that there is an open angle quote. 71 | var quoteTracker = {}; 72 | 73 | // The current command piece before being rejoined with it's over half. 74 | // Since it's not common for pipes to occur in commands, this is usually the entire command pipe. 75 | var commandPart = ''; 76 | 77 | // Loop through each naive pipe. 78 | for (var key in naivePipes) { 79 | // It's possible/likely that this naive pipe is the whole pipe if it doesn't contain an unfinished quote. 80 | var possiblePipe = naivePipes[key]; 81 | commandPart += possiblePipe; 82 | 83 | // Loop through each individual character in the possible pipe tracking the opening and closing of quotes. 84 | for (var i = 0; i < possiblePipe.length; i++) { 85 | var char = possiblePipe[i]; 86 | if (quoteChars.indexOf(char) !== -1) { 87 | quoteTracker[char] = !quoteTracker[char]; 88 | } 89 | } 90 | 91 | // Does the pipe end on an unfinished quote? 92 | var inQuote = _.some(quoteChars, function (quoteChar) { 93 | return quoteTracker[quoteChar]; 94 | }); 95 | 96 | // If the quotes have all been closed or this is the last possible pipe in the array, add as pipe. 97 | if (!inQuote || key * 1 === naivePipes.length - 1) { 98 | newPipes.push(commandPart.trim()); 99 | commandPart = ''; 100 | } else { 101 | // Quote was left open. The pipe character was previously removed when the array was split. 102 | commandPart += '|'; 103 | } 104 | } 105 | 106 | // Set the first pipe to command and the rest to pipes. 107 | command = newPipes.shift(); 108 | pipes = pipes.concat(newPipes); 109 | } 110 | 111 | function parseMatch() { 112 | matchParts = self.matchCommand(command, commands); 113 | match = matchParts.command; 114 | matchArgs = matchParts.args; 115 | } 116 | 117 | parsePipes(); 118 | parseMatch(); 119 | 120 | if (match && _.isFunction(match._parse)) { 121 | command = match._parse(command, matchParts.args); 122 | parsePipes(); 123 | parseMatch(); 124 | } 125 | 126 | return { 127 | command: command, 128 | match: match, 129 | matchArgs: matchArgs, 130 | pipes: pipes 131 | }; 132 | }, 133 | 134 | /** 135 | * Run a raw command string, e.g. foo -bar 136 | * against a given list of commands, 137 | * and if there is a match, parse the 138 | * results. 139 | * 140 | * @param {String} cmd 141 | * @param {Array} cmds 142 | * @return {Object} 143 | * @api public 144 | */ 145 | 146 | matchCommand: function matchCommand(cmd, cmds) { 147 | var parts = String(cmd).trim().split(' '); 148 | 149 | var match = void 0; 150 | var matchArgs = void 0; 151 | for (var i = 0; i < parts.length; ++i) { 152 | var subcommand = String(parts.slice(0, parts.length - i).join(' ')).trim(); 153 | match = _.find(cmds, { _name: subcommand }) || match; 154 | if (!match) { 155 | for (var key in cmds) { 156 | var _cmd = cmds[key]; 157 | var idx = _cmd._aliases.indexOf(subcommand); 158 | match = idx > -1 ? _cmd : match; 159 | } 160 | } 161 | if (match) { 162 | matchArgs = parts.slice(parts.length - i, parts.length).join(' '); 163 | break; 164 | } 165 | } 166 | // If there's no command match, check if the 167 | // there's a `catch` command, which catches all 168 | // missed commands. 169 | if (!match) { 170 | match = _.find(cmds, { _catch: true }); 171 | // If there is one, we still need to make sure we aren't 172 | // partially matching command groups, such as `do things` when 173 | // there is a command `do things well`. If we match partially, 174 | // we still want to show the help menu for that command group. 175 | if (match) { 176 | var allCommands = _.map(cmds, '_name'); 177 | var wordMatch = false; 178 | for (var _key in allCommands) { 179 | var _cmd2 = allCommands[_key]; 180 | var parts2 = String(_cmd2).split(' '); 181 | var cmdParts = String(match.command).split(' '); 182 | var matchAll = true; 183 | for (var k = 0; k < cmdParts.length; ++k) { 184 | if (parts2[k] !== cmdParts[k]) { 185 | matchAll = false; 186 | break; 187 | } 188 | } 189 | if (matchAll) { 190 | wordMatch = true; 191 | break; 192 | } 193 | } 194 | if (wordMatch) { 195 | match = undefined; 196 | } else { 197 | matchArgs = cmd; 198 | } 199 | } 200 | } 201 | 202 | return { 203 | command: match, 204 | args: matchArgs 205 | }; 206 | }, 207 | 208 | buildCommandArgs: function buildCommandArgs(passedArgs, cmd, execCommand, isCommandArgKeyPairNormalized) { 209 | var args = { options: {} }; 210 | 211 | if (isCommandArgKeyPairNormalized) { 212 | // Normalize all foo="bar" with "foo='bar'" 213 | // This helps implement unix-like key value pairs. 214 | var reg = /(['"]?)(\w+)=(?:(['"])((?:(?!\3).)*)\3|(\S+))\1/g; 215 | passedArgs = passedArgs.replace(reg, '"$2=\'$4$5\'"'); 216 | } 217 | 218 | // Types are custom arg types passed 219 | // into `minimist` as per its docs. 220 | var types = cmd._types || {}; 221 | 222 | // Make a list of all boolean options 223 | // registered for this command. These are 224 | // simply commands that don't have required 225 | // or optional args. 226 | var booleans = []; 227 | cmd.options.forEach(function (opt) { 228 | if (opt.required === 0 && opt.optional === 0) { 229 | if (opt.short) { 230 | booleans.push(opt.short); 231 | } 232 | if (opt.long) { 233 | booleans.push(opt.long); 234 | } 235 | } 236 | }); 237 | 238 | // Review the args passed into the command, 239 | // and filter out the boolean list to only those 240 | // options passed in. 241 | // This returns a boolean list of all options 242 | // passed in by the caller, which don't have 243 | // required or optional args. 244 | var passedArgParts = passedArgs.split(' '); 245 | types.boolean = booleans.map(function (str) { 246 | return String(str).replace(/^-*/, ''); 247 | }).filter(function (str) { 248 | var match = false; 249 | var strings = ['-' + str, '--' + str, '--no-' + str]; 250 | for (var i = 0; i < passedArgParts.length; ++i) { 251 | if (strings.indexOf(passedArgParts[i]) > -1) { 252 | match = true; 253 | break; 254 | } 255 | } 256 | return match; 257 | }); 258 | 259 | // Use minimist to parse the args. 260 | var parsedArgs = this.parseArgs(passedArgs, types); 261 | 262 | function validateArg(arg, cmdArg) { 263 | return !(arg === undefined && cmdArg.required === true); 264 | } 265 | 266 | // Builds varidiac args and options. 267 | var valid = true; 268 | var remainingArgs = _.clone(parsedArgs._); 269 | for (var l = 0; l < 10; ++l) { 270 | var matchArg = cmd._args[l]; 271 | var passedArg = parsedArgs._[l]; 272 | if (matchArg !== undefined) { 273 | valid = !valid ? false : validateArg(parsedArgs._[l], matchArg); 274 | if (!valid) { 275 | break; 276 | } 277 | if (passedArg !== undefined) { 278 | if (matchArg.variadic === true) { 279 | args[matchArg.name] = remainingArgs; 280 | } else { 281 | args[matchArg.name] = passedArg; 282 | remainingArgs.shift(); 283 | } 284 | } 285 | } 286 | } 287 | 288 | if (!valid) { 289 | return '\n Missing required argument. Showing Help:'; 290 | } 291 | 292 | // Looks for ommitted required options and throws help. 293 | for (var m = 0; m < cmd.options.length; ++m) { 294 | var o = cmd.options[m]; 295 | var short = String(o.short || '').replace(/-/g, ''); 296 | var long = String(o.long || '').replace(/--no-/g, '').replace(/^-*/g, ''); 297 | var exist = parsedArgs[short] !== undefined ? parsedArgs[short] : undefined; 298 | exist = exist === undefined && parsedArgs[long] !== undefined ? parsedArgs[long] : exist; 299 | var existsNotSet = exist === true || exist === false; 300 | if (existsNotSet && o.required !== 0) { 301 | return '\n Missing required value for option ' + (o.long || o.short) + '. Showing Help:'; 302 | } 303 | if (exist !== undefined) { 304 | args.options[long || short] = exist; 305 | } 306 | } 307 | 308 | // Looks for supplied options that don't 309 | // exist in the options list. 310 | // If the command allows unknown options, 311 | // adds it, otherwise throws help. 312 | var passedOpts = _.chain(parsedArgs).keys().pull('_').pull('help').value(); 313 | 314 | var _loop = function _loop(key) { 315 | var opt = passedOpts[key]; 316 | var optionFound = _.find(cmd.options, function (expected) { 317 | if ('--' + opt === expected.long || '--no-' + opt === expected.long || '-' + opt === expected.short) { 318 | return true; 319 | } 320 | return false; 321 | }); 322 | if (optionFound === undefined) { 323 | if (cmd._allowUnknownOptions) { 324 | args.options[opt] = parsedArgs[opt]; 325 | } else { 326 | return { 327 | v: '\n Invalid option: \'' + opt + '\'. Showing Help:' 328 | }; 329 | } 330 | } 331 | }; 332 | 333 | for (var key in passedOpts) { 334 | var _ret = _loop(key); 335 | 336 | if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; 337 | } 338 | 339 | // If args were passed into the programmatic 340 | // `vorpal.exec(cmd, args, callback)`, merge 341 | // them here. 342 | if (execCommand && execCommand.args && _.isObject(execCommand.args)) { 343 | args = _.extend(args, execCommand.args); 344 | } 345 | 346 | // Looks for a help arg and throws help if any. 347 | if (parsedArgs.help || parsedArgs._.indexOf('/?') > -1) { 348 | args.options.help = true; 349 | } 350 | 351 | return args; 352 | }, 353 | 354 | /** 355 | * Makes an argument name pretty for help. 356 | * 357 | * @param {String} arg 358 | * @return {String} 359 | * @api private 360 | */ 361 | 362 | humanReadableArgName: function humanReadableArgName(arg) { 363 | var nameOutput = arg.name + (arg.variadic === true ? '...' : ''); 364 | return arg.required ? '<' + nameOutput + '>' : '[' + nameOutput + ']'; 365 | }, 366 | 367 | /** 368 | * Formats an array to display in a TTY 369 | * in a pretty fashion. 370 | * 371 | * @param {Array} arr 372 | * @return {String} 373 | * @api public 374 | */ 375 | 376 | prettifyArray: function prettifyArray(arr) { 377 | arr = arr || []; 378 | var arrClone = _.clone(arr); 379 | var width = process.stdout.columns; 380 | var longest = strip(arrClone.sort(function (a, b) { 381 | return strip(b).length - strip(a).length; 382 | })[0] || '').length + 2; 383 | var fullWidth = strip(String(arr.join(''))).length; 384 | var fitsOneLine = fullWidth + arr.length * 2 <= width; 385 | var cols = Math.floor(width / longest); 386 | cols = cols < 1 ? 1 : cols; 387 | if (fitsOneLine) { 388 | return arr.join(' '); 389 | } 390 | var col = 0; 391 | var lines = []; 392 | var line = ''; 393 | for (var key in arr) { 394 | var arrEl = arr[key]; 395 | if (col < cols) { 396 | col++; 397 | } else { 398 | lines.push(line); 399 | line = ''; 400 | col = 1; 401 | } 402 | line += this.pad(arrEl, longest, ' '); 403 | } 404 | if (line !== '') { 405 | lines.push(line); 406 | } 407 | return lines.join('\n'); 408 | }, 409 | 410 | /** 411 | * Pads a value with with space or 412 | * a specified delimiter to match a 413 | * given width. 414 | * 415 | * @param {String} str 416 | * @param {Integer} width 417 | * @param {String} delimiter 418 | * @return {String} 419 | * @api private 420 | */ 421 | 422 | pad: function pad(str, width, delimiter) { 423 | width = Math.floor(width); 424 | delimiter = delimiter || ' '; 425 | var len = Math.max(0, width - strip(str).length); 426 | return str + Array(len + 1).join(delimiter); 427 | }, 428 | 429 | /** 430 | * Pad a row on the start and end with spaces. 431 | * 432 | * @param {String} str 433 | * @return {String} 434 | */ 435 | padRow: function padRow(str) { 436 | return str.split('\n').map(function (row) { 437 | return ' ' + row + ' '; 438 | }).join('\n'); 439 | }, 440 | 441 | // When passing down applied args, we need to turn 442 | // them from `{ '0': 'foo', '1': 'bar' }` into ['foo', 'bar'] 443 | // instead. 444 | fixArgsForApply: function fixArgsForApply(obj) { 445 | if (!_.isObject(obj)) { 446 | if (!_.isArray(obj)) { 447 | return [obj]; 448 | } 449 | return obj; 450 | } 451 | var argArray = []; 452 | for (var key in obj) { 453 | var aarg = obj[key]; 454 | argArray.push(aarg); 455 | } 456 | return argArray; 457 | } 458 | }; 459 | 460 | /** 461 | * Expose `util`. 462 | */ 463 | 464 | module.exports = exports = util; -------------------------------------------------------------------------------- /dist/vorpal-commons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Function library for Vorpal's out-of-the-box 5 | * API commands. Imported into a Vorpal server 6 | * through vorpal.use(module). 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var _ = require('lodash'); 14 | 15 | module.exports = function (vorpal) { 16 | /** 17 | * Help for a particular command. 18 | */ 19 | 20 | vorpal.command('help [command...]').description('Provides help for a given command.').action(function (args, cb) { 21 | var self = this; 22 | if (args.command) { 23 | args.command = args.command.join(' '); 24 | var name = _.find(this.parent.commands, { _name: String(args.command).trim() }); 25 | if (name && !name._hidden) { 26 | if (_.isFunction(name._help)) { 27 | name._help(args.command, function (str) { 28 | self.log(str); 29 | cb(); 30 | }); 31 | return; 32 | } 33 | this.log(name.helpInformation()); 34 | } else { 35 | this.log(this.parent._commandHelp(args.command)); 36 | } 37 | } else { 38 | this.log(this.parent._commandHelp(args.command)); 39 | } 40 | cb(); 41 | }); 42 | 43 | /** 44 | * Exits Vorpal. 45 | */ 46 | 47 | vorpal.command('exit').alias('quit').description('Exits application.').action(function (args) { 48 | args.options = args.options || {}; 49 | args.options.sessionId = this.session.id; 50 | this.parent.exit(args.options); 51 | }); 52 | }; -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Calculator 4 | 5 | A simple calculator CLI app made with Vorpal. 6 | 7 | ## Mode 8 | 9 | Using Vorpal's "mode" function. 10 | 11 | -------------------------------------------------------------------------------- /examples/calculator/README.md: -------------------------------------------------------------------------------- 1 | # A simple calculator app 2 | 3 | This is an example of using Vorpal to make a simple calculator immersive app. 4 | 5 | ### Running the file 6 | 7 | ```bash 8 | $ git clone git://github.com/dthree/vorpal.git vorpal 9 | $ cd ./vorpal 10 | $ npm install 11 | $ node ./examples/calculator/calculator.js 12 | ``` 13 | 14 | -------------------------------------------------------------------------------- /examples/calculator/calculator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var vorpal = require('./../../')(); 4 | var less = require('vorpal-less'); 5 | var repl = require('vorpal-repl'); 6 | vorpal.use(less).use(repl); 7 | 8 | vorpal.command('add [numbers...]', 'Adds numbers together') 9 | .alias('addition') 10 | .alias('plus') 11 | .action(function (args, cb) { 12 | var numbers = args.numbers; 13 | var sum = 0; 14 | for (var i = 0; i < numbers.length; ++i) { 15 | sum += parseFloat(numbers[i]); 16 | } 17 | this.log(sum); 18 | cb(undefined, sum); 19 | }); 20 | 21 | vorpal.command('double [values...]', 'Doubles a value on each tab press') 22 | .autocompletion(function (text, iteration, cb) { 23 | if (iteration > 1000000) { 24 | cb(undefined, ['cows', 'hogs', 'horses']); 25 | } else { 26 | var number = String(text).trim(); 27 | if (!isNaN(number)) { 28 | number = (number < 1) ? 1 : number; 29 | cb(undefined, 'double ' + number * 2); 30 | } else { 31 | cb(undefined, 'double 2'); 32 | } 33 | } 34 | }) 35 | .action(function (args, cb) { 36 | cb(); 37 | }); 38 | 39 | vorpal.command('args [items...]', 'Shows args.') 40 | .option('-d') 41 | .option('-a') 42 | .option('--save') 43 | .action(function (args, cb) { 44 | this.log(args); 45 | cb(); 46 | }); 47 | 48 | vorpal 49 | .delimiter('calc:') 50 | .show() 51 | .parse(process.argv); 52 | -------------------------------------------------------------------------------- /examples/descriptors/descriptors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var vorpal = require('./../../')(); 4 | var chalk = vorpal.chalk; 5 | 6 | vorpal 7 | .title(chalk.magenta('Vorpal')) 8 | .version('1.4.0') 9 | .description(chalk.cyan('Conquer the command-line.')) 10 | .banner(chalk.gray(` (O) 11 | 15 | \\| ^^^^^^ \\:W/------------------------------------------------'''''' 16 | o { 9 | return gulp.src(paths.src) 10 | .pipe(_gulp.xo()); 11 | }); 12 | 13 | gulp.task('build', () => { 14 | return gulp.src(paths.src) 15 | .pipe(_gulp.babel()) 16 | .pipe(gulp.dest(paths.dist)); 17 | }); 18 | 19 | gulp.task('watch', ['build'], () => { 20 | gulp.watch(paths.src, ['build']); 21 | }); 22 | 23 | gulp.task('default', ['watch']); 24 | -------------------------------------------------------------------------------- /lib/autocomplete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var strip = require('strip-ansi'); 5 | 6 | var autocomplete = { 7 | 8 | /** 9 | * Handles tabbed autocompletion. 10 | * 11 | * - Initial tabbing lists all registered commands. 12 | * - Completes a command halfway typed. 13 | * - Recognizes options and lists all possible options. 14 | * - Recognizes option arguments and lists them. 15 | * - Supports cursor positions anywhere in the string. 16 | * - Supports piping. 17 | * 18 | * @param {String} str 19 | * @return {String} cb 20 | * @api public 21 | */ 22 | 23 | exec: function (str, cb) { 24 | var self = this; 25 | var input = parseInput(str, this.parent.ui._activePrompt.screen.rl.cursor); 26 | var commands = getCommandNames(this.parent.commands); 27 | var vorpalMatch = getMatch(input.context, commands, {ignoreSlashes: true}); 28 | var freezeTabs = false; 29 | 30 | function end(str) { 31 | var res = handleTabCounts.call(self, str, freezeTabs); 32 | cb(undefined, res); 33 | } 34 | 35 | function evaluateTabs(input) { 36 | if (input.context && input.context[input.context.length - 1] === '/') { 37 | freezeTabs = true; 38 | } 39 | } 40 | 41 | if (vorpalMatch) { 42 | input.context = vorpalMatch; 43 | evaluateTabs(input); 44 | end(assembleInput(input)); 45 | return; 46 | } 47 | 48 | input = getMatchObject.call(this, input, commands); 49 | if (input.match) { 50 | input = parseMatchSection.call(this, input); 51 | getMatchData.call(self, input, function (data) { 52 | var dataMatch = getMatch(input.context, data); 53 | if (dataMatch) { 54 | input.context = dataMatch; 55 | evaluateTabs(input); 56 | end(assembleInput(input)); 57 | return; 58 | } 59 | end(filterData(input.context, data)); 60 | }); 61 | return; 62 | } 63 | end(filterData(input.context, commands)); 64 | }, 65 | 66 | /** 67 | * Independent / stateless auto-complete function. 68 | * Parses an array of strings for the best match. 69 | * 70 | * @param {String} str 71 | * @param {Array} arr 72 | * @return {String} 73 | * @api private 74 | */ 75 | 76 | match: function (str, arr, options) { 77 | arr = arr || []; 78 | options = options || {}; 79 | arr.sort(); 80 | var arrX = _.clone(arr); 81 | var strX = String(str); 82 | 83 | var prefix = ''; 84 | 85 | if (options.ignoreSlashes !== true) { 86 | var parts = strX.split('/'); 87 | strX = parts.pop(); 88 | prefix = parts.join('/'); 89 | prefix = parts.length > 0 ? prefix + '/' : prefix; 90 | } 91 | 92 | var matches = []; 93 | for (var i = 0; i < arrX.length; i++) { 94 | if (strip(arrX[i]).slice(0, strX.length) === strX) { 95 | matches.push(arrX[i]); 96 | } 97 | } 98 | if (matches.length === 1) { 99 | // If we have a slash, don't add a space after match. 100 | var space = (String(strip(matches[0])).slice(strip(matches[0]).length - 1) === '/') ? '' : ' '; 101 | return prefix + matches[0] + space; 102 | } else if (matches.length === 0) { 103 | return undefined; 104 | } else if (strX.length === 0) { 105 | return matches; 106 | } 107 | 108 | var longestMatchLength = matches 109 | .reduce(function (previous, current) { 110 | for (var i = 0; i < current.length; i++) { 111 | if (previous[i] && current[i] !== previous[i]) { 112 | return current.substr(0, i); 113 | } 114 | } 115 | return previous; 116 | }).length; 117 | 118 | // couldn't resolve any further, return all matches 119 | if (longestMatchLength === strX.length) { 120 | return matches; 121 | } 122 | 123 | // return the longest matching portion along with the prefix 124 | return prefix + matches[0].substr(0, longestMatchLength); 125 | } 126 | }; 127 | 128 | /** 129 | * Tracks how many times tab was pressed 130 | * based on whether the UI changed. 131 | * 132 | * @param {String} str 133 | * @return {String} result 134 | * @api private 135 | */ 136 | 137 | function handleTabCounts(str, freezeTabs) { 138 | var result; 139 | if (_.isArray(str)) { 140 | this._tabCtr += 1; 141 | if (this._tabCtr > 1) { 142 | result = ((str.length === 0) ? undefined : str); 143 | } 144 | } else { 145 | this._tabCtr = (freezeTabs === true) ? this._tabCtr + 1 : 0; 146 | result = str; 147 | } 148 | return result; 149 | } 150 | 151 | /** 152 | * Looks for a potential exact match 153 | * based on given data. 154 | * 155 | * @param {String} ctx 156 | * @param {Array} data 157 | * @return {String} 158 | * @api private 159 | */ 160 | 161 | function getMatch(ctx, data, options) { 162 | // Look for a command match, eliminating and then 163 | // re-introducing leading spaces. 164 | var len = ctx.length; 165 | var trimmed = ctx.replace(/^\s+/g, ''); 166 | var match = autocomplete.match(trimmed, data.slice(), options); 167 | if (_.isArray(match)) { 168 | return match; 169 | } 170 | var prefix = new Array((len - trimmed.length) + 1).join(' '); 171 | // If we get an autocomplete match on a command, finish it. 172 | if (match) { 173 | // Put the leading spaces back in. 174 | match = prefix + match; 175 | return match; 176 | } 177 | return undefined; 178 | } 179 | 180 | /** 181 | * Takes the input object and assembles 182 | * the final result to display on the screen. 183 | * 184 | * @param {Object} input 185 | * @return {String} 186 | * @api private 187 | */ 188 | 189 | function assembleInput(input) { 190 | if (_.isArray(input.context)) { 191 | return input.context; 192 | } 193 | var result = 194 | (input.prefix || '') + 195 | (input.context || '') + 196 | (input.suffix || ''); 197 | return strip(result); 198 | } 199 | 200 | /** 201 | * Reduces an array of possible 202 | * matches to list based on a given 203 | * string. 204 | * 205 | * @param {String} str 206 | * @param {Array} data 207 | * @return {Array} 208 | * @api private 209 | */ 210 | 211 | function filterData(str, data) { 212 | data = data || []; 213 | var ctx = String(str || '').trim(); 214 | var slashParts = ctx.split('/'); 215 | ctx = slashParts.pop(); 216 | var wordParts = String(ctx).trim().split(' '); 217 | var res = data.filter(function (item) { 218 | return (strip(item).slice(0, ctx.length) === ctx); 219 | }); 220 | res = res.map(function (item) { 221 | var parts = String(item).trim().split(' '); 222 | if (parts.length > 1) { 223 | parts = parts.slice(wordParts.length); 224 | return parts.join(' '); 225 | } 226 | return item; 227 | }); 228 | return res; 229 | } 230 | 231 | /** 232 | * Takes the user's current prompt 233 | * string and breaks it into its 234 | * integral parts for analysis and 235 | * modification. 236 | * 237 | * @param {String} str 238 | * @param {Integer} idx 239 | * @return {Object} 240 | * @api private 241 | */ 242 | 243 | function parseInput(str, idx) { 244 | var raw = String(str || ''); 245 | var sliced = raw.slice(0, idx); 246 | var sections = sliced.split('|'); 247 | var prefix = (sections.slice(0, sections.length - 1) || []); 248 | prefix.push(''); 249 | prefix = prefix.join('|'); 250 | var suffix = getSuffix(raw.slice(idx)); 251 | var context = sections[sections.length - 1]; 252 | return ({ 253 | raw: raw, 254 | prefix: prefix, 255 | suffix: suffix, 256 | context: context 257 | }); 258 | } 259 | 260 | /** 261 | * Takes the context after a 262 | * matched command and figures 263 | * out the applicable context, 264 | * including assigning its role 265 | * such as being an option 266 | * parameter, etc. 267 | * 268 | * @param {Object} input 269 | * @return {Object} 270 | * @api private 271 | */ 272 | 273 | function parseMatchSection(input) { 274 | var parts = (input.context || '').split(' '); 275 | var last = parts.pop(); 276 | var beforeLast = strip(parts[parts.length - 1] || '').trim(); 277 | if (beforeLast.slice(0, 1) === '-') { 278 | input.option = beforeLast; 279 | } 280 | input.context = last; 281 | input.prefix = (input.prefix || '') + parts.join(' ') + ' '; 282 | return input; 283 | } 284 | 285 | /** 286 | * Returns a cleaned up version of the 287 | * remaining text to the right of the cursor. 288 | * 289 | * @param {String} suffix 290 | * @return {String} 291 | * @api private 292 | */ 293 | 294 | function getSuffix(suffix) { 295 | suffix = (suffix.slice(0, 1) === ' ') ? 296 | suffix : 297 | suffix.replace(/.+?(?=\s)/, ''); 298 | suffix = suffix.slice(1, suffix.length); 299 | return suffix; 300 | } 301 | 302 | /** 303 | * Compile all available commands and aliases 304 | * in alphabetical order. 305 | * 306 | * @param {Array} cmds 307 | * @return {Array} 308 | * @api private 309 | */ 310 | 311 | function getCommandNames(cmds) { 312 | var commands = _.map(cmds, '_name'); 313 | commands = commands.concat.apply(commands, _.map(cmds, '_aliases')); 314 | commands.sort(); 315 | return commands; 316 | } 317 | 318 | /** 319 | * When we know that we've 320 | * exceeded a known command, grab 321 | * on to that command and return it, 322 | * fixing the overall input context 323 | * at the same time. 324 | * 325 | * @param {Object} input 326 | * @param {Array} commands 327 | * @return {Object} 328 | * @api private 329 | */ 330 | 331 | function getMatchObject(input, commands) { 332 | var len = input.context.length; 333 | var trimmed = String(input.context).replace(/^\s+/g, ''); 334 | var prefix = new Array((len - trimmed.length) + 1).join(' '); 335 | var match; 336 | var suffix; 337 | commands.forEach(function (cmd) { 338 | var nextChar = trimmed.substr(cmd.length, 1); 339 | if (trimmed.substr(0, cmd.length) === cmd && String(cmd).trim() !== '' && nextChar === ' ') { 340 | match = cmd; 341 | suffix = trimmed.substr(cmd.length); 342 | prefix += trimmed.substr(0, cmd.length); 343 | } 344 | }); 345 | 346 | var matchObject = (match) ? 347 | _.find(this.parent.commands, {_name: String(match).trim()}) : 348 | undefined; 349 | 350 | if (!matchObject) { 351 | this.parent.commands.forEach(function (cmd) { 352 | if ((cmd._aliases || []).indexOf(String(match).trim()) > -1) { 353 | matchObject = cmd; 354 | } 355 | return; 356 | }); 357 | } 358 | 359 | if (!matchObject) { 360 | matchObject = _.find(this.parent.commands, {_catch: true}); 361 | if (matchObject) { 362 | suffix = input.context; 363 | } 364 | } 365 | 366 | if (!matchObject) { 367 | prefix = input.context; 368 | suffix = ''; 369 | } 370 | 371 | if (matchObject) { 372 | input.match = matchObject; 373 | input.prefix += prefix; 374 | input.context = suffix; 375 | } 376 | return input; 377 | } 378 | 379 | /** 380 | * Takes a known matched command, and reads 381 | * the applicable data by calling its autocompletion 382 | * instructions, whether it is the command's 383 | * autocompletion or one of its options. 384 | * 385 | * @param {Object} input 386 | * @param {Function} cb 387 | * @return {Array} 388 | * @api private 389 | */ 390 | 391 | function getMatchData(input, cb) { 392 | var string = input.context; 393 | var cmd = input.match; 394 | var midOption = (String(string).trim().slice(0, 1) === '-'); 395 | var afterOption = (input.option !== undefined); 396 | if (midOption === true && (!cmd._allowUnknownOptions)) { 397 | var results = []; 398 | for (var i = 0; i < cmd.options.length; ++i) { 399 | var long = cmd.options[i].long; 400 | var short = cmd.options[i].short; 401 | if (!long && short) { 402 | results.push(short); 403 | } else if (long) { 404 | results.push(long); 405 | } 406 | } 407 | cb(results); 408 | return; 409 | } 410 | 411 | function handleDataFormat(str, config, callback) { 412 | var data = []; 413 | if (_.isArray(config)) { 414 | data = config; 415 | } else if (_.isFunction(config)) { 416 | var cbk = (config.length < 2) ? (function () {}) : (function (res) { 417 | callback(res || []); 418 | }); 419 | var res = config(str, cbk); 420 | if (res && _.isFunction(res.then)) { 421 | res.then(function (resp) { 422 | callback(resp); 423 | }).catch(function (err) { 424 | callback(err); 425 | }); 426 | } else if (config.length < 2) { 427 | callback(res); 428 | } 429 | return; 430 | } 431 | callback(data); 432 | return; 433 | } 434 | 435 | if (afterOption === true) { 436 | var opt = strip(input.option).trim(); 437 | var shortMatch = _.find(cmd.options, {short: opt}); 438 | var longMatch = _.find(cmd.options, {long: opt}); 439 | var match = longMatch || shortMatch; 440 | if (match) { 441 | var config = match.autocomplete; 442 | handleDataFormat(string, config, cb); 443 | return; 444 | } 445 | } 446 | 447 | var conf = cmd._autocomplete; 448 | conf = (conf && conf.data) ? conf.data : conf; 449 | handleDataFormat(string, conf, cb); 450 | return; 451 | } 452 | 453 | module.exports = autocomplete; 454 | -------------------------------------------------------------------------------- /lib/command-instance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const util = require('./util'); 8 | const _ = require('lodash'); 9 | 10 | class CommandInstance { 11 | 12 | /** 13 | * Initialize a new `CommandInstance` instance. 14 | * 15 | * @param {Object} params 16 | * @return {CommandInstance} 17 | * @api public 18 | */ 19 | 20 | constructor({command, commandObject, args, commandWrapper, callback, downstream} = {}) { 21 | this.command = command; 22 | this.commandObject = commandObject; 23 | this.args = args; 24 | this.commandWrapper = commandWrapper; 25 | this.session = commandWrapper.session; 26 | this.parent = this.session.parent; 27 | this.callback = callback; 28 | this.downstream = downstream; 29 | } 30 | 31 | /** 32 | * Cancel running command. 33 | */ 34 | 35 | cancel() { 36 | this.session.emit('vorpal_command_cancel'); 37 | } 38 | 39 | /** 40 | * Route stdout either through a piped command, or the session's stdout. 41 | */ 42 | 43 | log() { 44 | const args = util.fixArgsForApply(arguments); 45 | if (this.downstream) { 46 | const fn = this.downstream.commandObject._fn || function () {}; 47 | this.session.registerCommand(); 48 | this.downstream.args.stdin = args; 49 | const onComplete = (err) => { 50 | if (this.session.isLocal() && err) { 51 | this.session.log(err.stack || err); 52 | this.session.parent.emit('client_command_error', {command: this.downstream.command, error: err}); 53 | } 54 | this.session.completeCommand(); 55 | }; 56 | 57 | const validate = this.downstream.commandObject._validate; 58 | if (_.isFunction(validate)) { 59 | try { 60 | validate.call(this.downstream, this.downstream.args); 61 | } catch (e) { 62 | // Log error without piping to downstream on validation error. 63 | this.session.log(e.toString()); 64 | onComplete(); 65 | return; 66 | } 67 | } 68 | 69 | const res = fn.call(this.downstream, this.downstream.args, onComplete); 70 | if (res && _.isFunction(res.then)) { 71 | res.then(onComplete, onComplete); 72 | } 73 | } else { 74 | this.session.log.apply(this.session, args); 75 | } 76 | } 77 | 78 | prompt(a, b, c) { 79 | return this.session.prompt(a, b, c); 80 | } 81 | 82 | delimiter(a, b, c) { 83 | return this.session.delimiter(a, b, c); 84 | } 85 | 86 | help(a, b, c) { 87 | return this.session.help(a, b, c); 88 | } 89 | 90 | match(a, b, c) { 91 | return this.session.match(a, b, c); 92 | } 93 | } 94 | 95 | module.exports = CommandInstance; 96 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var EventEmitter = require('events').EventEmitter; 8 | var Option = require('./option'); 9 | var VorpalUtil = require('./util'); 10 | var _ = require('lodash'); 11 | 12 | /** 13 | * Command prototype. 14 | */ 15 | 16 | var command = Command.prototype; 17 | 18 | /** 19 | * Expose `Command`. 20 | */ 21 | 22 | module.exports = exports = Command; 23 | 24 | /** 25 | * Initialize a new `Command` instance. 26 | * 27 | * @param {String} name 28 | * @param {Vorpal} parent 29 | * @return {Command} 30 | * @api public 31 | */ 32 | 33 | function Command(name, parent) { 34 | if (!(this instanceof Command)) { 35 | return new Command(); 36 | } 37 | this.commands = []; 38 | this.options = []; 39 | this._args = []; 40 | this._aliases = []; 41 | this._name = name; 42 | this._relay = false; 43 | this._hidden = false; 44 | this._parent = parent; 45 | this._mode = false; 46 | this._catch = false; 47 | this._help = undefined; 48 | this._init = undefined; 49 | this._after = undefined; 50 | this._allowUnknownOptions = false; 51 | } 52 | 53 | /** 54 | * Registers an option for given command. 55 | * 56 | * @param {String} flags 57 | * @param {String} description 58 | * @param {Function} fn 59 | * @param {String} defaultValue 60 | * @return {Command} 61 | * @api public 62 | */ 63 | 64 | command.option = function (flags, description, autocomplete) { 65 | var self = this; 66 | var option = new Option(flags, description, autocomplete); 67 | var oname = option.name(); 68 | var name = _camelcase(oname); 69 | var defaultValue; 70 | 71 | // preassign default value only for --no-*, [optional], or 72 | if (option.bool === false || option.optional || option.required) { 73 | // when --no-* we make sure default is true 74 | if (option.bool === false) { 75 | defaultValue = true; 76 | } 77 | // preassign only if we have a default 78 | if (defaultValue !== undefined) { 79 | self[name] = defaultValue; 80 | } 81 | } 82 | 83 | // register the option 84 | this.options.push(option); 85 | 86 | // when it's passed assign the value 87 | // and conditionally invoke the callback 88 | this.on(oname, function (val) { 89 | // unassigned or bool 90 | if (typeof self[name] === 'boolean' || typeof self[name] === 'undefined') { 91 | // if no value, bool true, and we have a default, then use it! 92 | if (val === null) { 93 | self[name] = option.bool ? 94 | defaultValue || true : 95 | false; 96 | } else { 97 | self[name] = val; 98 | } 99 | } else if (val !== null) { 100 | // reassign 101 | self[name] = val; 102 | } 103 | }); 104 | 105 | return this; 106 | }; 107 | 108 | /** 109 | * Defines an action for a given command. 110 | * 111 | * @param {Function} fn 112 | * @return {Command} 113 | * @api public 114 | */ 115 | 116 | command.action = function (fn) { 117 | var self = this; 118 | self._fn = fn; 119 | return this; 120 | }; 121 | 122 | /** 123 | * Let's you compose other funtions to extend the command. 124 | * 125 | * @param {Function} fn 126 | * @return {Command} 127 | * @api public 128 | */ 129 | 130 | command.use = function (fn) { 131 | return fn(this); 132 | }; 133 | 134 | /** 135 | * Defines a function to validate arguments 136 | * before action is performed. Arguments 137 | * are valid if no errors are thrown from 138 | * the function. 139 | * 140 | * @param fn 141 | * @returns {Command} 142 | * @api public 143 | */ 144 | command.validate = function (fn) { 145 | var self = this; 146 | self._validate = fn; 147 | return this; 148 | }; 149 | 150 | /** 151 | * Defines a function to be called when the 152 | * command is canceled. 153 | * 154 | * @param fn 155 | * @returns {Command} 156 | * @api public 157 | */ 158 | command.cancel = function (fn) { 159 | this._cancel = fn; 160 | return this; 161 | }; 162 | 163 | /** 164 | * Defines a method to be called when 165 | * the command set has completed. 166 | * 167 | * @param {Function} fn 168 | * @return {Command} 169 | * @api public 170 | */ 171 | 172 | command.done = function (fn) { 173 | this._done = fn; 174 | return this; 175 | }; 176 | 177 | /** 178 | * Defines tabbed auto-completion 179 | * for the given command. Favored over 180 | * deprecated command.autocompletion. 181 | * 182 | * @param {Function} fn 183 | * @return {Command} 184 | * @api public 185 | */ 186 | 187 | command.autocomplete = function (obj) { 188 | this._autocomplete = obj; 189 | return this; 190 | }; 191 | 192 | /** 193 | * Defines tabbed auto-completion rules 194 | * for the given command. 195 | * 196 | * @param {Function} fn 197 | * @return {Command} 198 | * @api public 199 | */ 200 | 201 | command.autocompletion = function (param) { 202 | this._parent._useDeprecatedAutocompletion = true; 203 | if (!_.isFunction(param) && !_.isObject(param)) { 204 | throw new Error('An invalid object type was passed into the first parameter of command.autocompletion: function expected.'); 205 | } 206 | 207 | this._autocompletion = param; 208 | return this; 209 | }; 210 | 211 | /** 212 | * Defines an init action for a mode command. 213 | * 214 | * @param {Function} fn 215 | * @return {Command} 216 | * @api public 217 | */ 218 | 219 | command.init = function (fn) { 220 | var self = this; 221 | if (self._mode !== true) { 222 | throw Error('Cannot call init from a non-mode action.'); 223 | } 224 | self._init = fn; 225 | return this; 226 | }; 227 | 228 | /** 229 | * Defines a prompt delimiter for a 230 | * mode once entered. 231 | * 232 | * @param {String} delimiter 233 | * @return {Command} 234 | * @api public 235 | */ 236 | 237 | command.delimiter = function (delimiter) { 238 | this._delimiter = delimiter; 239 | return this; 240 | }; 241 | 242 | /** 243 | * Sets args for static typing of options 244 | * using minimist. 245 | * 246 | * @param {Object} types 247 | * @return {Command} 248 | * @api public 249 | */ 250 | 251 | command.types = function (types) { 252 | var supported = ['string', 'boolean']; 253 | for (var item in types) { 254 | if (supported.indexOf(item) === -1) { 255 | throw new Error('An invalid type was passed into command.types(): ' + item); 256 | } 257 | types[item] = (!_.isArray(types[item])) ? [types[item]] : types[item]; 258 | } 259 | this._types = types; 260 | return this; 261 | }; 262 | 263 | /** 264 | * Defines an alias for a given command. 265 | * 266 | * @param {String} alias 267 | * @return {Command} 268 | * @api public 269 | */ 270 | 271 | command.alias = function () { 272 | var self = this; 273 | for (var i = 0; i < arguments.length; ++i) { 274 | var alias = arguments[i]; 275 | if (_.isArray(alias)) { 276 | for (var j = 0; j < alias.length; ++j) { 277 | this.alias(alias[j]); 278 | } 279 | return this; 280 | } 281 | this._parent.commands.forEach(function (cmd) { 282 | if (!_.isEmpty(cmd._aliases)) { 283 | if (_.includes(cmd._aliases, alias)) { 284 | var msg = 'Duplicate alias "' + alias + '" for command "' + self._name + '" detected. Was first reserved by command "' + cmd._name + '".'; 285 | throw new Error(msg); 286 | } 287 | } 288 | }); 289 | this._aliases.push(alias); 290 | } 291 | return this; 292 | }; 293 | 294 | /** 295 | * Defines description for given command. 296 | * 297 | * @param {String} str 298 | * @return {Command} 299 | * @api public 300 | */ 301 | 302 | command.description = function (str) { 303 | if (arguments.length === 0) { 304 | return this._description; 305 | } 306 | this._description = str; 307 | return this; 308 | }; 309 | 310 | /** 311 | * Removes self from Vorpal instance. 312 | * 313 | * @return {Command} 314 | * @api public 315 | */ 316 | 317 | command.remove = function () { 318 | var self = this; 319 | this._parent.commands = _.reject(this._parent.commands, function (command) { 320 | if (command._name === self._name) { 321 | return true; 322 | } 323 | }); 324 | return this; 325 | }; 326 | 327 | /** 328 | * Returns the commands arguments as string. 329 | * 330 | * @param {String} desc 331 | * @return {String} 332 | * @api public 333 | */ 334 | 335 | command.arguments = function (desc) { 336 | return this._parseExpectedArgs(desc.split(/ +/)); 337 | }; 338 | 339 | /** 340 | * Returns the help info for given command. 341 | * 342 | * @return {String} 343 | * @api public 344 | */ 345 | 346 | command.helpInformation = function () { 347 | var desc = []; 348 | var cmdName = this._name; 349 | var alias = ''; 350 | 351 | if (this._description) { 352 | desc = [ 353 | ' ' + this._description, 354 | '' 355 | ]; 356 | } 357 | 358 | if (this._aliases.length > 0) { 359 | alias = ' Alias: ' + this._aliases.join(' | ') + '\n'; 360 | } 361 | var usage = [ 362 | '', 363 | ' Usage: ' + cmdName + ' ' + this.usage(), 364 | '' 365 | ]; 366 | 367 | var cmds = []; 368 | 369 | var help = String(this.optionHelp().replace(/^/gm, ' ')); 370 | var options = [ 371 | ' Options:', 372 | '', 373 | help, 374 | '' 375 | ]; 376 | 377 | var res = usage 378 | .concat(cmds) 379 | .concat(alias) 380 | .concat(desc) 381 | .concat(options) 382 | .join('\n'); 383 | 384 | res = res.replace(/\n\n\n/g, '\n\n'); 385 | 386 | return res; 387 | }; 388 | 389 | /** 390 | * Doesn't show command in the help menu. 391 | * 392 | * @return {Command} 393 | * @api public 394 | */ 395 | 396 | command.hidden = function () { 397 | this._hidden = true; 398 | return this; 399 | }; 400 | 401 | /** 402 | * Allows undeclared options to be passed in with the command. 403 | * 404 | * @param {Boolean} [allowUnknownOptions=true] 405 | * @return {Command} 406 | * @api public 407 | */ 408 | 409 | command.allowUnknownOptions = function (allowUnknownOptions = true) { 410 | allowUnknownOptions = allowUnknownOptions === "false" ? false : allowUnknownOptions; 411 | 412 | this._allowUnknownOptions = !!allowUnknownOptions; 413 | return this; 414 | }; 415 | 416 | /** 417 | * Returns the command usage string for help. 418 | * 419 | * @param {String} str 420 | * @return {String} 421 | * @api public 422 | */ 423 | 424 | command.usage = function (str) { 425 | var args = this._args.map(function (arg) { 426 | return VorpalUtil.humanReadableArgName(arg); 427 | }); 428 | 429 | var usage = '[options]' + 430 | (this.commands.length ? ' [command]' : '') + 431 | (this._args.length ? ' ' + args.join(' ') : ''); 432 | 433 | if (arguments.length === 0) { 434 | return (this._usage || usage); 435 | } 436 | 437 | this._usage = str; 438 | 439 | return this; 440 | }; 441 | 442 | /** 443 | * Returns the help string for the command's options. 444 | * 445 | * @return {String} 446 | * @api public 447 | */ 448 | 449 | command.optionHelp = function () { 450 | var width = this._largestOptionLength(); 451 | 452 | // Prepend the help information 453 | return [VorpalUtil.pad('--help', width) + ' output usage information'] 454 | .concat(this.options.map(function (option) { 455 | return VorpalUtil.pad(option.flags, width) + ' ' + option.description; 456 | })) 457 | .join('\n'); 458 | }; 459 | 460 | /** 461 | * Returns the length of the longest option. 462 | * 463 | * @return {Integer} 464 | * @api private 465 | */ 466 | 467 | command._largestOptionLength = function () { 468 | return this.options.reduce(function (max, option) { 469 | return Math.max(max, option.flags.length); 470 | }, 0); 471 | }; 472 | 473 | /** 474 | * Adds a custom handling for the --help flag. 475 | * 476 | * @param {Function} fn 477 | * @return {Command} 478 | * @api public 479 | */ 480 | 481 | command.help = function (fn) { 482 | if (_.isFunction(fn)) { 483 | this._help = fn; 484 | } 485 | return this; 486 | }; 487 | 488 | /** 489 | * Edits the raw command string before it 490 | * is executed. 491 | * 492 | * @param {String} str 493 | * @return {String} str 494 | * @api public 495 | */ 496 | 497 | command.parse = function (fn) { 498 | if (_.isFunction(fn)) { 499 | this._parse = fn; 500 | } 501 | return this; 502 | }; 503 | 504 | /** 505 | * Adds a command to be executed after command completion. 506 | * 507 | * @param {Function} fn 508 | * @return {Command} 509 | * @api public 510 | */ 511 | 512 | command.after = function (fn) { 513 | if (_.isFunction(fn)) { 514 | this._after = fn; 515 | } 516 | return this; 517 | }; 518 | 519 | /** 520 | * Parses and returns expected command arguments. 521 | * 522 | * @param {String} args 523 | * @return {Array} 524 | * @api private 525 | */ 526 | 527 | command._parseExpectedArgs = function (args) { 528 | if (!args.length) { 529 | return; 530 | } 531 | var self = this; 532 | args.forEach(function (arg) { 533 | var argDetails = { 534 | required: false, 535 | name: '', 536 | variadic: false 537 | }; 538 | 539 | switch (arg[0]) { 540 | case '<': 541 | argDetails.required = true; 542 | argDetails.name = arg.slice(1, -1); 543 | break; 544 | case '[': 545 | argDetails.name = arg.slice(1, -1); 546 | break; 547 | default: 548 | break; 549 | } 550 | 551 | if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') { 552 | argDetails.variadic = true; 553 | argDetails.name = argDetails.name.slice(0, -3); 554 | } 555 | if (argDetails.name) { 556 | self._args.push(argDetails); 557 | } 558 | }); 559 | 560 | // If the user entered args in a weird order, 561 | // properly sequence them. 562 | if (self._args.length > 1) { 563 | self._args = self._args.sort(function (argu1, argu2) { 564 | if (argu1.required && !argu2.required) { 565 | return -1; 566 | } else if (argu2.required && !argu1.required) { 567 | return 1; 568 | } else if (argu1.variadic && !argu2.variadic) { 569 | return 1; 570 | } else if (argu2.variadic && !argu1.variadic) { 571 | return -1; 572 | } 573 | return 0; 574 | }); 575 | } 576 | 577 | return; 578 | }; 579 | 580 | /** 581 | * Converts string to camel case. 582 | * 583 | * @param {String} flag 584 | * @return {String} 585 | * @api private 586 | */ 587 | 588 | function _camelcase(flag) { 589 | return flag.split('-').reduce(function (str, word) { 590 | return str + word[0].toUpperCase() + word.slice(1); 591 | }); 592 | } 593 | 594 | /** 595 | * Make command an EventEmitter. 596 | */ 597 | 598 | command.__proto__ = EventEmitter.prototype; 599 | -------------------------------------------------------------------------------- /lib/history.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var LocalStorage = require('node-localstorage').LocalStorage; 5 | var path = require('path'); 6 | const os = require('os'); 7 | 8 | // Number of command histories kept in persistent storage 9 | var HISTORY_SIZE = 500; 10 | 11 | const temp = path.normalize(path.join(os.tmpdir(), '/.local_storage')); 12 | const DEFAULT_STORAGE_PATH = temp; 13 | 14 | var History = function () { 15 | this._storageKey = undefined; 16 | 17 | // Prompt Command History 18 | // Histctr moves based on number of times 'up' (+= ctr) 19 | // or 'down' (-= ctr) was pressed in traversing 20 | // command history. 21 | this._hist = []; 22 | this._histCtr = 0; 23 | 24 | // When in a 'mode', we reset the 25 | // history and store it in a cache until 26 | // exiting the 'mode', at which point we 27 | // resume the original history. 28 | this._histCache = []; 29 | this._histCtrCache = 0; 30 | }; 31 | 32 | /** 33 | * Initialize the history with local storage data 34 | * Called from setId when history id is set 35 | */ 36 | 37 | History.prototype._init = function () { 38 | if (!this._storageKey) { 39 | return; 40 | } 41 | 42 | // Load history from local storage 43 | var persistedHistory = JSON.parse(this._localStorage.getItem(this._storageKey)); 44 | if (_.isArray(persistedHistory)) { 45 | Array.prototype.push.apply(this._hist, persistedHistory); 46 | } 47 | }; 48 | 49 | /** 50 | * Set id for this history instance. 51 | * Calls init internally to initialize 52 | * the history with the id. 53 | */ 54 | 55 | History.prototype.setId = function (id) { 56 | // Initialize a localStorage instance with default 57 | // path if it is not initialized 58 | if (!this._localStorage) { 59 | this._localStorage = new LocalStorage(DEFAULT_STORAGE_PATH); 60 | } 61 | this._storageKey = 'cmd_history_' + id; 62 | this._init(); 63 | }; 64 | 65 | /** 66 | * Initialize a local storage instance with 67 | * the path if not already initialized. 68 | * 69 | * @param path 70 | */ 71 | 72 | History.prototype.setStoragePath = function (path) { 73 | if (!this._localStorage) { 74 | this._localStorage = new LocalStorage(path); 75 | } 76 | }; 77 | 78 | /** 79 | * Get previous history. Called when up is pressed. 80 | * 81 | * @return {String} 82 | */ 83 | 84 | History.prototype.getPreviousHistory = function () { 85 | this._histCtr++; 86 | this._histCtr = (this._histCtr > this._hist.length) ? 87 | this._hist.length : 88 | this._histCtr; 89 | return this._hist[this._hist.length - (this._histCtr)]; 90 | }; 91 | 92 | /** 93 | * Get next history. Called when down is pressed. 94 | * 95 | * @return {String} 96 | */ 97 | 98 | History.prototype.getNextHistory = function () { 99 | this._histCtr--; 100 | 101 | // Return empty prompt if the we dont have any history to show 102 | if (this._histCtr < 1) { 103 | this._histCtr = 0; 104 | return ''; 105 | } 106 | 107 | return this._hist[this._hist.length - this._histCtr]; 108 | }; 109 | 110 | /** 111 | * Peek into history, without changing state 112 | * 113 | * @return {String} 114 | */ 115 | 116 | History.prototype.peek = function (depth) { 117 | depth = depth || 0; 118 | return this._hist[this._hist.length - 1 - depth]; 119 | }; 120 | 121 | /** 122 | * A new command was submitted. Called when enter is pressed and the prompt is not empty. 123 | * 124 | * @param cmd 125 | */ 126 | 127 | History.prototype.newCommand = function (cmd) { 128 | // Always reset history when new command is executed. 129 | this._histCtr = 0; 130 | 131 | // Don't store command in history if it's a duplicate. 132 | if (this._hist[this._hist.length - 1] === cmd) { 133 | return; 134 | } 135 | 136 | // Push into history. 137 | this._hist.push(cmd); 138 | 139 | // Only persist history when not in mode 140 | if (this._storageKey && !this._inMode) { 141 | var persistedHistory = this._hist; 142 | var historyLen = this._hist.length; 143 | if (historyLen > HISTORY_SIZE) { 144 | persistedHistory = this._hist.slice(historyLen - HISTORY_SIZE - 1, historyLen - 1); 145 | } 146 | 147 | // Add to local storage 148 | this._localStorage.setItem(this._storageKey, JSON.stringify(persistedHistory)); 149 | } 150 | }; 151 | 152 | /** 153 | * Called when entering a mode 154 | */ 155 | 156 | History.prototype.enterMode = function () { 157 | // Reassign the command history to a 158 | // cache, replacing it with a blank 159 | // history for the mode. 160 | this._histCache = _.clone(this._hist); 161 | this._histCtrCache = parseFloat(this._histCtr); 162 | this._hist = []; 163 | this._histCtr = 0; 164 | this._inMode = true; 165 | }; 166 | 167 | /** 168 | * Called when exiting a mode 169 | */ 170 | 171 | History.prototype.exitMode = function () { 172 | this._hist = this._histCache; 173 | this._histCtr = this._histCtrCache; 174 | this._histCache = []; 175 | this._histCtrCache = 0; 176 | this._inMode = false; 177 | }; 178 | 179 | /** 180 | * Clears the command history 181 | * (Currently only used in unit test) 182 | */ 183 | 184 | History.prototype.clear = function () { 185 | if (this._storageKey) { 186 | this._localStorage.removeItem(this._storageKey); 187 | } 188 | }; 189 | 190 | module.exports = History; 191 | -------------------------------------------------------------------------------- /lib/intercept.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var _ = require('lodash'); 8 | 9 | /** 10 | * Intercepts stdout, passes thru callback 11 | * also pass console.error thru stdout so it goes to callback too 12 | * (stdout.write and stderr.write are both refs to the same stream.write function) 13 | * returns an unhook() function, call when done intercepting 14 | * 15 | * @param {Function} callback 16 | * @return {Function} 17 | */ 18 | 19 | module.exports = function (callback) { 20 | var oldStdoutWrite = process.stdout.write; 21 | var oldConsoleError = console.error; 22 | process.stdout.write = (function (write) { 23 | return function (string) { 24 | var args = _.toArray(arguments); 25 | args[0] = interceptor(string); 26 | write.apply(process.stdout, args); 27 | }; 28 | }(process.stdout.write)); 29 | 30 | console.error = (function () { 31 | return function () { 32 | var args = _.toArray(arguments); 33 | args.unshift('\x1b[31m[ERROR]\x1b[0m'); 34 | console.log.apply(console.log, args); 35 | }; 36 | }(console.error)); 37 | 38 | function interceptor(string) { 39 | // only intercept the string 40 | var result = callback(string); 41 | if (typeof result === 'string') { 42 | string = result.replace(/\n$/, '') + (result && (/\n$/).test(string) ? '\n' : ''); 43 | } 44 | return string; 45 | } 46 | // puts back to original 47 | return function unhook() { 48 | process.stdout.write = oldStdoutWrite; 49 | console.error = oldConsoleError; 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/local-storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LocalStorageO = require('node-localstorage').LocalStorage; 4 | var path = require('path'); 5 | const os = require('os'); 6 | const temp = path.normalize(path.join(os.tmpdir(), '/.local_storage_')); 7 | const DEFAULT_STORAGE_PATH = temp; 8 | 9 | var LocalStorage = { 10 | 11 | setId(id) { 12 | if (id === undefined) { 13 | throw new Error('vorpal.localStorage() requires a unique key to be passed in.'); 14 | } 15 | if (!this._localStorage) { 16 | this._localStorage = new LocalStorageO(DEFAULT_STORAGE_PATH + id); 17 | } 18 | }, 19 | 20 | validate() { 21 | if (this._localStorage === undefined) { 22 | throw new Error('Vorpal.localStorage() was not initialized before writing data.'); 23 | } 24 | }, 25 | 26 | getItem(key, value) { 27 | this.validate(); 28 | return this._localStorage.getItem(key, value); 29 | }, 30 | 31 | setItem(key, value) { 32 | this.validate(); 33 | return this._localStorage.setItem(key, value); 34 | }, 35 | 36 | removeItem(key) { 37 | this.validate(); 38 | return this._localStorage.removeItem(key); 39 | } 40 | }; 41 | 42 | module.exports = LocalStorage; 43 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var _ = require('lodash'); 6 | var util = require('./util'); 7 | var ut = require('util'); 8 | 9 | /** 10 | * Initialize a new `Logger` instance. 11 | * 12 | * @return {Logger} 13 | * @api public 14 | */ 15 | 16 | function viewed(str) { 17 | var re = /\u001b\[\d+m/gm; 18 | return String(str).replace(re, ''); 19 | } 20 | 21 | function trimTo(str, amt) { 22 | var raw = ''; 23 | var visual = viewed(str).slice(0, amt); 24 | var result = ''; 25 | for (var i = 0; i < str.length; ++i) { 26 | raw += str[i]; 27 | if (viewed(raw) === visual) { 28 | result = raw; break; 29 | } 30 | } 31 | 32 | if (result.length < amt - 10) { 33 | return result; 34 | } 35 | 36 | var newResult = result; var found = false; 37 | for (var j = result.length; j > 0; --j) { 38 | if (result[j] === ' ') { 39 | found = true; 40 | break; 41 | } else { 42 | newResult = newResult.slice(0, newResult.length - 1); 43 | } 44 | } 45 | 46 | if (found === true) { 47 | return newResult; 48 | } 49 | 50 | return result; 51 | } 52 | 53 | function Logger(cons) { 54 | var logger = cons || console; 55 | log = function () { 56 | logger.log.apply(logger, arguments); 57 | }; 58 | 59 | log.cols = function () { 60 | var width = process.stdout.columns; 61 | var pads = 0; 62 | var padsWidth = 0; 63 | var cols = 0; 64 | var colsWidth = 0; 65 | var input = arguments; 66 | 67 | for (var h = 0; h < arguments.length; ++h) { 68 | if (typeof arguments[h] === 'number') { 69 | padsWidth += arguments[h]; 70 | pads++; 71 | } 72 | if (_.isArray(arguments[h]) && typeof arguments[h][0] === 'number') { 73 | padsWidth += arguments[h][0]; 74 | pads++; 75 | } 76 | } 77 | 78 | cols = arguments.length - pads; 79 | colsWidth = Math.floor((width - padsWidth) / cols); 80 | 81 | var lines = []; 82 | 83 | var go = function () { 84 | var str = ''; 85 | var done = true; 86 | for (var i = 0; i < input.length; ++i) { 87 | if (typeof input[i] === 'number') { 88 | str += util.pad('', input[i], ' '); 89 | } else if (_.isArray(input[i]) && typeof input[i][0] === 'number') { 90 | str += util.pad('', input[i][0], input[i][1]); 91 | } else { 92 | var chosenWidth = colsWidth + 0; 93 | var trimmed = trimTo(input[i], colsWidth); 94 | var trimmedLength = trimmed.length; 95 | var re = /\\u001b\[\d+m/gm; 96 | var matches = ut.inspect(trimmed).match(re); 97 | var color = ''; 98 | // Ugh. We're chopping a line, so we have to look for unfinished 99 | // color assignments and throw them on the next line. 100 | if (matches && matches[matches.length - 1] !== '\\u001b[39m') { 101 | trimmed += '\u001b[39m'; 102 | var number = String(matches[matches.length - 1]).slice(7, 9); 103 | color = '\x1B[' + number + 'm'; 104 | } 105 | input[i] = color + String(input[i].slice(trimmedLength, input[i].length)).trim(); 106 | str += util.pad(String(trimmed).trim(), chosenWidth, ' '); 107 | if (viewed(input[i]).trim() !== '') { 108 | done = false; 109 | } 110 | } 111 | } 112 | lines.push(str); 113 | if (!done) { 114 | go(); 115 | } 116 | }; 117 | go(); 118 | for (var i = 0; i < lines.length; ++i) { 119 | logger.log(lines[i]); 120 | } 121 | return this; 122 | }; 123 | 124 | log.br = function () { 125 | logger.log(' '); 126 | return this; 127 | }; 128 | 129 | return this.log; 130 | } 131 | 132 | /** 133 | * Expose `logger`. 134 | */ 135 | 136 | module.exports = exports = Logger; 137 | -------------------------------------------------------------------------------- /lib/option.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Option { 4 | 5 | /** 6 | * Initialize a new `Option` instance. 7 | * 8 | * @param {String} flags 9 | * @param {String} description 10 | * @param {Autocomplete} autocomplete 11 | * @return {Option} 12 | * @api public 13 | */ 14 | 15 | constructor(flags, description, autocomplete) { 16 | this.flags = flags; 17 | this.required = ~flags.indexOf('<'); 18 | this.optional = ~flags.indexOf('['); 19 | this.bool = !~flags.indexOf('-no-'); 20 | this.autocomplete = autocomplete; 21 | flags = flags.split(/[ ,|]+/); 22 | if (flags.length > 1 && !/^[[<]/.test(flags[1])) { 23 | this.assignFlag(flags.shift()); 24 | } 25 | this.assignFlag(flags.shift()); 26 | this.description = description || ''; 27 | } 28 | 29 | /** 30 | * Return option name. 31 | * 32 | * @return {String} 33 | * @api private 34 | */ 35 | 36 | name() { 37 | if (this.long !== undefined) { 38 | return this.long 39 | .replace('--', '') 40 | .replace('no-', ''); 41 | } 42 | return this.short 43 | .replace('-', ''); 44 | } 45 | 46 | /** 47 | * Check if `arg` matches the short or long flag. 48 | * 49 | * @param {String} arg 50 | * @return {Boolean} 51 | * @api private 52 | */ 53 | 54 | is(arg) { 55 | return (arg === this.short || arg === this.long); 56 | } 57 | 58 | /** 59 | * Assigned flag to either long or short. 60 | * 61 | * @param {String} flag 62 | * @api private 63 | */ 64 | 65 | assignFlag(flag) { 66 | if (flag.startsWith('--')) { 67 | this.long = flag; 68 | } else { 69 | this.short = flag; 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Expose `Option`. 76 | */ 77 | 78 | module.exports = exports = Option; 79 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var EventEmitter = require('events').EventEmitter; 8 | var os = require('os'); 9 | var _ = require('lodash'); 10 | var util = require('./util'); 11 | var autocomplete = require('./autocomplete'); 12 | var CommandInstance = require('./command-instance'); 13 | 14 | /** 15 | * Initialize a new `Session` instance. 16 | * 17 | * @param {String} name 18 | * @return {Session} 19 | * @api public 20 | */ 21 | 22 | function Session(options) { 23 | options = options || {}; 24 | this.id = options.id || this._guid(); 25 | this.parent = options.parent || undefined; 26 | this.authenticating = options.authenticating || false; 27 | this.authenticated = options.authenticated || undefined; 28 | this.user = options.user || 'guest'; 29 | this.host = options.host; 30 | this.address = options.address || undefined; 31 | this._isLocal = options.local || undefined; 32 | this._delimiter = options.delimiter || String(os.hostname()).split('.')[0] + '~$'; 33 | this._modeDelimiter = undefined; 34 | 35 | // Keeps history of how many times in a row `tab` was 36 | // pressed on the keyboard. 37 | this._tabCtr = 0; 38 | 39 | this.cmdHistory = this.parent.cmdHistory; 40 | 41 | // Special command mode vorpal is in at the moment, 42 | // such as REPL. See mode documentation. 43 | this._mode = undefined; 44 | 45 | return this; 46 | } 47 | 48 | /** 49 | * Extend Session prototype as an event emitter. 50 | */ 51 | 52 | Session.prototype = Object.create(EventEmitter.prototype); 53 | 54 | /** 55 | * Session prototype. 56 | */ 57 | 58 | var session = Session.prototype; 59 | 60 | /** 61 | * Expose `Session`. 62 | */ 63 | 64 | module.exports = exports = Session; 65 | 66 | /** 67 | * Pipes logging data through any piped 68 | * commands, and then sends it to ._log for 69 | * actual logging. 70 | * 71 | * @param {String} [... arguments] 72 | * @return {Session} 73 | * @api public 74 | */ 75 | 76 | session.log = function () { 77 | var args = util.fixArgsForApply(arguments); 78 | return this._log.apply(this, args); 79 | }; 80 | 81 | /** 82 | * Routes logging for a given session. 83 | * is on a local TTY, or remote. 84 | * 85 | * @param {String} [... arguments] 86 | * @return {Session} 87 | * @api public 88 | */ 89 | 90 | session._log = function () { 91 | var self = this; 92 | if (this.isLocal()) { 93 | this.parent.ui.log.apply(this.parent.ui, arguments); 94 | } else { 95 | // If it's an error, expose the stack. Otherwise 96 | // we get a helpful '{}'. 97 | var args = []; 98 | for (var i = 0; i < arguments.length; ++i) { 99 | var str = arguments[i]; 100 | str = (str && str.stack) ? 'Error: ' + str.message : str; 101 | args.push(str); 102 | } 103 | self.parent._send('vantage-ssn-stdout-downstream', 'downstream', {sessionId: self.id, value: args}); 104 | } 105 | return this; 106 | }; 107 | 108 | /** 109 | * Returns whether given session 110 | * is on a local TTY, or remote. 111 | * 112 | * @return {Boolean} 113 | * @api public 114 | */ 115 | 116 | session.isLocal = function () { 117 | return this._isLocal; 118 | }; 119 | 120 | /** 121 | * Maps to vorpal.prompt for a session 122 | * context. 123 | * 124 | * @param {Object} options 125 | * @param {Function} cb 126 | * @api public 127 | */ 128 | 129 | session.prompt = function (options, cb) { 130 | options = options || {}; 131 | options.sessionId = this.id; 132 | return this.parent.prompt(options, cb); 133 | }; 134 | 135 | /** 136 | * Gets the full (normal + mode) delimiter 137 | * for this session. 138 | * 139 | * @return {String} 140 | * @api public 141 | */ 142 | 143 | session.fullDelimiter = function () { 144 | var result = this._delimiter + 145 | ((this._modeDelimiter !== undefined) ? this._modeDelimiter : ''); 146 | return result; 147 | }; 148 | 149 | /** 150 | * Sets the delimiter for this session. 151 | * 152 | * @param {String} str 153 | * @return {Session} 154 | * @api public 155 | */ 156 | 157 | session.delimiter = function (str) { 158 | if (str === undefined) { 159 | return this._delimiter; 160 | } 161 | this._delimiter = String(str).trim() + ' '; 162 | if (this.isLocal()) { 163 | this.parent.ui.refresh(); 164 | } else { 165 | this.parent._send('vantage-delimiter-downstream', 'downstream', {value: str, sessionId: this.id}); 166 | } 167 | return this; 168 | }; 169 | 170 | /** 171 | * Sets the mode delimiter for this session. 172 | * 173 | * @param {String} str 174 | * @return {Session} 175 | * @api public 176 | */ 177 | 178 | session.modeDelimiter = function (str) { 179 | var self = this; 180 | if (str === undefined) { 181 | return this._modeDelimiter; 182 | } 183 | if (!this.isLocal()) { 184 | self.parent._send('vantage-mode-delimiter-downstream', 'downstream', {value: str, sessionId: self.id}); 185 | } else { 186 | if (str === false || str === 'false') { 187 | this._modeDelimiter = undefined; 188 | } else { 189 | this._modeDelimiter = String(str).trim() + ' '; 190 | } 191 | this.parent.ui.refresh(); 192 | } 193 | return this; 194 | }; 195 | 196 | /** 197 | * Returns the result of a keypress 198 | * string, depending on the type. 199 | * 200 | * @param {String} key 201 | * @param {String} value 202 | * @return {Function} 203 | * @api private 204 | */ 205 | 206 | session.getKeypressResult = function (key, value, cb) { 207 | cb = cb || function () {}; 208 | var keyMatch = (['up', 'down', 'tab'].indexOf(key) > -1); 209 | if (key !== 'tab') { 210 | this._tabCtr = 0; 211 | } 212 | if (keyMatch) { 213 | if (['up', 'down'].indexOf(key) > -1) { 214 | cb(undefined, this.getHistory(key)); 215 | } else if (key === 'tab') { 216 | // If the Vorpal user has any commands that use 217 | // command.autocompletion, defer to the deprecated 218 | // version of autocompletion. Otherwise, default 219 | // to the new version. 220 | var fn = (this.parent._useDeprecatedAutocompletion) ? 221 | 'getAutocompleteDeprecated' : 222 | 'getAutocomplete'; 223 | this[fn](value, function (err, data) { 224 | cb(err, data); 225 | }); 226 | } 227 | } else { 228 | this._histCtr = 0; 229 | } 230 | }; 231 | 232 | session.history = function (str) { 233 | var exceptions = []; 234 | if (str && exceptions.indexOf(String(str).toLowerCase()) === -1) { 235 | this.cmdHistory.newCommand(str); 236 | } 237 | }; 238 | 239 | /** 240 | * New autocomplete. 241 | * 242 | * @param {String} str 243 | * @param {Function} cb 244 | * @api private 245 | */ 246 | 247 | session.getAutocomplete = function (str, cb) { 248 | return autocomplete.exec.call(this, str, cb); 249 | }; 250 | 251 | /** 252 | * Deprecated autocomplete - being deleted 253 | * in Vorpal 2.0. 254 | * 255 | * @param {String} str 256 | * @param {Function} cb 257 | * @api private 258 | */ 259 | 260 | session.getAutocompleteDeprecated = function (str, cb) { 261 | cb = cb || function () {}; 262 | 263 | // Entire command string 264 | var cursor = this.parent.ui._activePrompt.screen.rl.cursor; 265 | var trimmed = String(str).trim(); 266 | var cut = String(trimmed).slice(0, cursor); 267 | var remainder = String(trimmed).slice(cursor, trimmed.length).replace(/ +$/, ''); 268 | trimmed = cut; 269 | 270 | // Set "trimmed" to command string after pipe 271 | // Set "pre" to command string, pipe, and a space 272 | var pre = ''; 273 | var lastPipeIndex = trimmed.lastIndexOf('|'); 274 | if (lastPipeIndex !== -1) { 275 | pre = trimmed.substr(0, lastPipeIndex + 1) + ' '; 276 | trimmed = trimmed.substr(lastPipeIndex + 1).trim(); 277 | } 278 | 279 | // Complete command 280 | var names = _.map(this.parent.commands, '_name'); 281 | names = names.concat.apply(names, _.map(this.parent.commands, '_aliases')); 282 | var result = this._autocomplete(trimmed, names); 283 | if (result && trimmed.length < String(result).trim().length) { 284 | cb(undefined, pre + result + remainder); 285 | return; 286 | } 287 | 288 | // Find custom autocompletion 289 | var match; 290 | var extra; 291 | 292 | names.forEach(function (name) { 293 | if (trimmed.substr(0, name.length) === name && String(name).trim() !== '') { 294 | match = name; 295 | extra = trimmed.substr(name.length).trim(); 296 | } 297 | }); 298 | 299 | var command = (match) ? 300 | _.find(this.parent.commands, {_name: match}) : 301 | undefined; 302 | 303 | if (!command) { 304 | command = _.find(this.parent.commands, {_catch: true}); 305 | if (command) { 306 | extra = trimmed; 307 | } 308 | } 309 | 310 | if (command && _.isFunction(command._autocompletion)) { 311 | this._tabCtr++; 312 | command._autocompletion.call(this, extra, this._tabCtr, function (err, autocomplete) { 313 | if (err) { 314 | return cb(err); 315 | } 316 | if (_.isArray(autocomplete)) { 317 | return cb(undefined, autocomplete); 318 | } else if (autocomplete === undefined) { 319 | return cb(undefined, undefined); 320 | } 321 | return cb(undefined, pre + autocomplete + remainder); 322 | }); 323 | } else { 324 | cb(undefined, undefined); 325 | } 326 | }; 327 | 328 | session._autocomplete = function (str, arr) { 329 | return autocomplete.match.call(this, str, arr); 330 | }; 331 | 332 | /** 333 | * Public facing autocomplete helper. 334 | * 335 | * @param {String} str 336 | * @param {Array} arr 337 | * @return {String} 338 | * @api public 339 | */ 340 | 341 | session.help = function (command) { 342 | this.log(this.parent._commandHelp(command || '')); 343 | }; 344 | 345 | /** 346 | * Public facing autocomplete helper. 347 | * 348 | * @param {String} str 349 | * @param {Array} arr 350 | * @return {String} 351 | * @api public 352 | */ 353 | 354 | session.match = function (str, arr) { 355 | return this._autocomplete(str, arr); 356 | }; 357 | 358 | /** 359 | * Gets a new command set ready. 360 | * 361 | * @return {session} 362 | * @api public 363 | */ 364 | 365 | session.execCommandSet = function (wrapper, callback) { 366 | var self = this; 367 | var response = {}; 368 | var res; 369 | var cbk = callback; 370 | this._registeredCommands = 1; 371 | this._completedCommands = 0; 372 | 373 | // Create the command instance for the first 374 | // command and hook it up to the pipe chain. 375 | var commandInstance = new CommandInstance({ 376 | downstream: wrapper.pipes[0], 377 | commandObject: wrapper.commandObject, 378 | commandWrapper: wrapper 379 | }); 380 | 381 | wrapper.commandInstance = commandInstance; 382 | 383 | function sendDones(itm) { 384 | if (itm.commandObject && itm.commandObject._done) { 385 | itm.commandObject._done.call(itm); 386 | } 387 | if (itm.downstream) { 388 | sendDones(itm.downstream); 389 | } 390 | } 391 | 392 | // Called when command is cancelled 393 | this.cancelCommands = function () { 394 | var callCancel = function (commandInstance) { 395 | if (_.isFunction(commandInstance.commandObject._cancel)) { 396 | commandInstance.commandObject._cancel.call(commandInstance); 397 | } 398 | 399 | if (commandInstance.downstream) { 400 | callCancel(commandInstance.downstream); 401 | } 402 | }; 403 | 404 | callCancel(wrapper.commandInstance); 405 | 406 | // Check if there is a cancel method on the promise 407 | if (res && _.isFunction(res.cancel)) { 408 | res.cancel(wrapper.commandInstance); 409 | } 410 | 411 | self.removeListener('vorpal_command_cancel', self.cancelCommands); 412 | self.cancelCommands = undefined; 413 | self._commandSetCallback = undefined; 414 | self._registeredCommands = 0; 415 | self._completedCommands = 0; 416 | self.parent.emit('client_command_cancelled', {command: wrapper.command}); 417 | 418 | cbk(wrapper); 419 | }; 420 | 421 | this.on('vorpal_command_cancel', self.cancelCommands); 422 | 423 | // Gracefully handles all instances of the command completing. 424 | this._commandSetCallback = function () { 425 | var err = response.error; 426 | var data = response.data; 427 | var argus = response.args; 428 | if (self.isLocal() && err) { 429 | var stack; 430 | if (data && data.stack) { 431 | stack = data.stack; 432 | } else if (err && err.stack) { 433 | stack = err.stack; 434 | } else { 435 | stack = err; 436 | } 437 | self.log(stack); 438 | self.parent.emit('client_command_error', {command: wrapper.command, error: err}); 439 | } else if (self.isLocal()) { 440 | self.parent.emit('client_command_executed', {command: wrapper.command}); 441 | } 442 | 443 | self.removeListener('vorpal_command_cancel', self.cancelCommands); 444 | self.cancelCommands = undefined; 445 | cbk(wrapper, err, data, argus); 446 | sendDones(commandInstance); 447 | }; 448 | 449 | function onCompletion(wrapper, err, data, argus) { 450 | response = { 451 | error: err, 452 | data: data, 453 | args: argus 454 | }; 455 | self.completeCommand(); 456 | } 457 | 458 | var valid; 459 | if (_.isFunction(wrapper.validate)) { 460 | try { 461 | valid = wrapper.validate.call(commandInstance, wrapper.args); 462 | } catch (e) { 463 | // Complete with error on validation error 464 | onCompletion(wrapper, e); 465 | return this; 466 | } 467 | } 468 | 469 | if (valid !== true && valid !== undefined) { 470 | onCompletion(wrapper, valid || null); 471 | return this; 472 | } 473 | 474 | if(wrapper.args && typeof wrapper.args === 'object'){ 475 | wrapper.args.rawCommand = wrapper.command; 476 | } 477 | 478 | // Call the root command. 479 | res = wrapper.fn.call(commandInstance, wrapper.args, function () { 480 | var argus = util.fixArgsForApply(arguments); 481 | onCompletion(wrapper, argus[0], argus[1], argus); 482 | }); 483 | 484 | // If the command as declared by the user 485 | // returns a promise, handle accordingly. 486 | if (res && _.isFunction(res.then)) { 487 | res.then(function (data) { 488 | onCompletion(wrapper, undefined, data); 489 | }).catch(function (err) { 490 | onCompletion(wrapper, true, err); 491 | }); 492 | } 493 | 494 | return this; 495 | }; 496 | 497 | /** 498 | * Adds on a command or sub-command in progress. 499 | * Session keeps tracked of commands, 500 | * and as soon as all commands have been 501 | * compelted, the session returns the entire 502 | * command set as complete. 503 | * 504 | * @return {session} 505 | * @api public 506 | */ 507 | 508 | session.registerCommand = function () { 509 | this._registeredCommands = this._registeredCommands || 0; 510 | this._registeredCommands++; 511 | return this; 512 | }; 513 | 514 | /** 515 | * Marks a command or subcommand as having completed. 516 | * If all commands have completed, calls back 517 | * to the root command as being done. 518 | * 519 | * @return {session} 520 | * @api public 521 | */ 522 | 523 | session.completeCommand = function () { 524 | this._completedCommands++; 525 | if (this._registeredCommands <= this._completedCommands) { 526 | this._registeredCommands = 0; 527 | this._completedCommands = 0; 528 | if (this._commandSetCallback) { 529 | this._commandSetCallback(); 530 | } 531 | this._commandSetCallback = undefined; 532 | } 533 | return this; 534 | }; 535 | 536 | /** 537 | * Returns the appropriate command history 538 | * string based on an 'Up' or 'Down' arrow 539 | * key pressed by the user. 540 | * 541 | * @param {String} direction 542 | * @return {String} 543 | * @api private 544 | */ 545 | 546 | session.getHistory = function (direction) { 547 | var history; 548 | if (direction === 'up') { 549 | history = this.cmdHistory.getPreviousHistory(); 550 | } else if (direction === 'down') { 551 | history = this.cmdHistory.getNextHistory(); 552 | } 553 | return history; 554 | }; 555 | 556 | /** 557 | * Generates random GUID for Session ID. 558 | * 559 | * @return {GUID} 560 | * @api private 561 | */ 562 | 563 | session._guid = function () { 564 | function s4() { 565 | return Math.floor((1 + Math.random()) * 0x10000) 566 | .toString(16) 567 | .substring(1); 568 | } 569 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 570 | s4() + '-' + s4() + s4() + s4(); 571 | }; 572 | -------------------------------------------------------------------------------- /lib/ui.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const _ = require('lodash'); 8 | const inquirer = require('inquirer'); 9 | const EventEmitter = require('events').EventEmitter; 10 | const chalk = require('chalk'); 11 | const util = require('./util'); 12 | const logUpdate = require('log-update'); 13 | 14 | class UI extends EventEmitter { 15 | 16 | /** 17 | * Sets intial variables and registers 18 | * listeners. This is called once in a 19 | * process thread regardless of how many 20 | * instances of Vorpal have been generated. 21 | * 22 | * @api private 23 | */ 24 | 25 | constructor() { 26 | super(); 27 | const self = this; 28 | 29 | // Attached vorpal instance. The UI can 30 | // only attach to one instance of Vorpal 31 | // at a time, and directs all events to that 32 | // instance. 33 | this.parent = undefined; 34 | 35 | // Hook to reference active inquirer prompt. 36 | this._activePrompt = undefined; 37 | 38 | // Fail-safe to ensure there is no double 39 | // prompt in odd situations. 40 | this._midPrompt = false; 41 | 42 | // Handle for inquirer's prompt. 43 | this.inquirer = inquirer; 44 | 45 | // prompt history from inquirer 46 | this.inquirerStdout = []; 47 | 48 | // Whether a prompt is currently in cancel mode. 49 | this._cancelled = false; 50 | 51 | // Middleware for piping stdout through. 52 | this._pipeFn = undefined; 53 | 54 | // Custom function on sigint event. 55 | this._sigintCalled = false; 56 | this._sigintCount = 0; 57 | this._sigint = () => { 58 | if (this._sigintCount > 1) { 59 | this.parent.emit('vorpal_exit'); 60 | process.exit(0); 61 | } else { 62 | const text = this.input(); 63 | if (!this.parent) { 64 | // If Vorpal isn't shown, just exit. 65 | process.exit(0); 66 | } else if (this.parent.session.cancelCommands) { 67 | // There are commands running if 68 | // cancelCommands function is available. 69 | this.imprint(); 70 | this.submit(''); 71 | this._sigintCalled = false; 72 | this._sigintCount = 0; 73 | this.parent.session.emit('vorpal_command_cancel'); 74 | } else if (String(text).trim() !== '') { 75 | this.imprint(); 76 | this.submit(''); 77 | this._sigintCalled = false; 78 | this._sigintCount = 0; 79 | } else { 80 | this._sigintCalled = false; 81 | this.delimiter(' '); 82 | this.submit(''); 83 | this.log('(^C again to quit)'); 84 | } 85 | } 86 | }; 87 | 88 | process.stdin.on('keypress', (letter, key) => { 89 | key = key || {}; 90 | if (key.ctrl === true && key.shift === false && key.meta === false && ['c', 'C'].indexOf(key.name) > -1) { 91 | this._sigintCount++; 92 | if (this._sigint !== undefined && !this._sigintCalled) { 93 | this._sigintCalled = true; 94 | this._sigint.call(self.parent); 95 | this._sigintCalled = false; 96 | } 97 | } else { 98 | this._sigintCalled = false; 99 | this._sigintCount = 0; 100 | } 101 | }); 102 | 103 | // Extend the render function to steal the active prompt object, 104 | // as inquirer doesn't expose it and we need it. 105 | const prompts = ['input', 'checkbox', 'confirm', 'expand', 'list', 'password', 'rawlist']; 106 | 107 | for (const key in prompts) { 108 | const promptType = prompts[key]; 109 | 110 | // Add method to Inquirer to get type of prompt. 111 | inquirer.prompt.prompts[promptType].prototype.getType = function () { 112 | return promptType; 113 | }; 114 | 115 | // Hook in to steal Inquirer's keypress. 116 | inquirer.prompt.prompts[promptType].prototype.onKeypress = function (e) { 117 | // Inquirer seems to have a bug with release v0.10.1 118 | // (not 0.10.0 though) that triggers keypresses for 119 | // the previous prompt in addition to the current one. 120 | // So if the prompt is answered, shut it up. 121 | if (this.status && this.status === 'answered') { 122 | return; 123 | } 124 | self._activePrompt = this; 125 | self.parent.emit('client_keypress', e); 126 | self._keypressHandler(e, this); 127 | }; 128 | 129 | // Add hook to render method. 130 | const render = inquirer.prompt.prompts[promptType].prototype.render; 131 | inquirer.prompt.prompts[promptType].prototype.render = function () { 132 | self._activePrompt = this; 133 | return render.apply(this, arguments); 134 | }; 135 | } 136 | 137 | // Sigint handling - make it more graceful. 138 | const onSigInt = () => { 139 | if (_.isFunction(this._sigint) && !this._sigintCalled) { 140 | this._sigintCalled = true; 141 | this._sigint.call(this.parent); 142 | } 143 | }; 144 | process.on('SIGINT', onSigInt); 145 | process.on('SIGTERM', onSigInt); 146 | } 147 | 148 | /** 149 | * Hook for sigint event. 150 | * 151 | * @param {Object} options 152 | * @param {Function} cb 153 | * @api public 154 | */ 155 | 156 | sigint(fn) { 157 | if (_.isFunction(fn)) { 158 | this._sigint = fn; 159 | } else { 160 | throw new Error('vorpal.ui.sigint must be passed in a valid function.'); 161 | } 162 | return this; 163 | } 164 | 165 | /** 166 | * Creates an inquirer prompt on the TTY. 167 | * 168 | * @param {Object} options 169 | * @param {Function} cb 170 | * @api public 171 | */ 172 | 173 | prompt(options, cb) { 174 | let prompt; 175 | options = options || {}; 176 | if (!this.parent) { 177 | return prompt; 178 | } 179 | if (options.delimiter) { 180 | this.setDelimiter(options.delimiter); 181 | } 182 | if (options.message) { 183 | this.setDelimiter(options.message); 184 | } 185 | if (this._midPrompt) { 186 | console.log('Prompt called when mid prompt...'); 187 | throw new Error('UI Prompt called when already mid prompt.'); 188 | } 189 | this._midPrompt = true; 190 | try { 191 | prompt = inquirer.prompt(options, (result) => { 192 | this.inquirerStdout = []; 193 | this._midPrompt = false; 194 | if (this._cancel === true) { 195 | this._cancel = false; 196 | } else { 197 | cb(result); 198 | } 199 | }); 200 | 201 | // Temporary hack. We need to pull the active 202 | // prompt from inquirer as soon as possible, 203 | // however we can't just assign it sync, as 204 | // the prompt isn't ready yet. 205 | // I am trying to get inquirer extended to 206 | // fire an event instead. 207 | setTimeout(() => { 208 | // this._activePrompt = prompt._activePrompt; 209 | }, 100); 210 | } catch (e) { 211 | console.log('Vorpal Prompt error:', e); 212 | } 213 | return prompt; 214 | } 215 | 216 | /** 217 | * Returns a boolean as to whether user 218 | * is mid another pr ompt. 219 | * 220 | * @return {Boolean} 221 | * @api public 222 | */ 223 | 224 | midPrompt() { 225 | const mid = (this._midPrompt === true && this.parent !== undefined); 226 | return mid; 227 | } 228 | 229 | setDelimiter(str) { 230 | const self = this; 231 | if (!this.parent) { 232 | return; 233 | } 234 | str = String(str).trim() + ' '; 235 | this._lastDelimiter = str; 236 | inquirer.prompt.prompts.password.prototype.getQuestion = function () { 237 | self._activePrompt = this; 238 | return this.opt.message; 239 | }; 240 | inquirer.prompt.prompts.input.prototype.getQuestion = function () { 241 | self._activePrompt = this; 242 | let message = this.opt.message; 243 | if ((this.opt.default || this.opt.default === false) 244 | && this.status !== 'answered') { 245 | message += chalk.dim('(' + this.opt.default + ') '); 246 | } 247 | self.inquirerStdout.push(message); 248 | return message; 249 | }; 250 | } 251 | 252 | /** 253 | * Event handler for keypresses - deals with command history 254 | * and tabbed auto-completion. 255 | * 256 | * @param {Event} e 257 | * @param {Prompt} prompt 258 | * @api private 259 | */ 260 | 261 | _keypressHandler(e, prompt) { 262 | // Remove tab characters from user input. 263 | prompt.rl.line = prompt.rl.line.replace(/\t+/, ''); 264 | 265 | // Mask passwords. 266 | const line = prompt.getType() !== 'password' ? 267 | prompt.rl.line : 268 | '*'.repeat(prompt.rl.line.length); 269 | 270 | // Re-write render function. 271 | const width = prompt.rl.line.length; 272 | const newWidth = prompt.rl.line.length; 273 | const diff = newWidth - width; 274 | prompt.rl.cursor += diff; 275 | const cursor = 0; 276 | let message = prompt.getQuestion(); 277 | const addition = (prompt.status === 'answered') ? 278 | chalk.cyan(prompt.answer) : 279 | line; 280 | message += addition; 281 | prompt.screen.render(message, {cursor: cursor}); 282 | 283 | const key = (e.key || {}).name; 284 | const value = (prompt) ? String(line) : undefined; 285 | this.emit('vorpal_ui_keypress', {key: key, value: value, e: e}); 286 | } 287 | 288 | /** 289 | * Pauses active prompt, returning 290 | * the value of what had been typed so far. 291 | * 292 | * @return {String} val 293 | * @api public 294 | */ 295 | 296 | pause() { 297 | if (!this.parent) { 298 | return false; 299 | } 300 | if (!this._activePrompt) { 301 | return false; 302 | } 303 | if (!this._midPrompt) { 304 | return false; 305 | } 306 | const val = this._lastDelimiter + this._activePrompt.rl.line; 307 | this._midPrompt = false; 308 | const rl = this._activePrompt.screen.rl; 309 | const screen = this._activePrompt.screen; 310 | rl.output.unmute(); 311 | screen.clean(); 312 | rl.output.write(''); 313 | return val; 314 | } 315 | 316 | /** 317 | * Resumes active prompt, accepting 318 | * a string, which will fill the prompt 319 | * with that text and put the cursor at 320 | * the end. 321 | * 322 | * @param {String} val 323 | * @api public 324 | */ 325 | 326 | resume(val) { 327 | if (!this.parent) { 328 | return this; 329 | } 330 | val = val || ''; 331 | if (!this._activePrompt) { 332 | return this; 333 | } 334 | if (this._midPrompt) { 335 | return this; 336 | } 337 | const rl = this._activePrompt.screen.rl; 338 | rl.output.write(val); 339 | this._midPrompt = true; 340 | return this; 341 | } 342 | 343 | /** 344 | * Cancels the active prompt, essentially 345 | * but cutting out of the inquirer loop. 346 | * 347 | * @api public 348 | */ 349 | 350 | cancel() { 351 | if (this.midPrompt()) { 352 | this._cancel = true; 353 | this.submit(''); 354 | this._midPrompt = false; 355 | } 356 | return this; 357 | } 358 | 359 | /** 360 | * Attaches TTY prompt to a given Vorpal instance. 361 | * 362 | * @param {Vorpal} vorpal 363 | * @return {UI} 364 | * @api public 365 | */ 366 | 367 | attach(vorpal) { 368 | this.parent = vorpal; 369 | this.refresh(); 370 | this.parent._prompt(); 371 | return this; 372 | } 373 | 374 | /** 375 | * Detaches UI from a given Vorpal instance. 376 | * 377 | * @param {Vorpal} vorpal 378 | * @return {UI} 379 | * @api public 380 | */ 381 | 382 | detach(vorpal) { 383 | if (vorpal === this.parent) { 384 | this.parent = undefined; 385 | } 386 | return this; 387 | } 388 | 389 | /** 390 | * Receives and runs logging through 391 | * a piped function is one is provided 392 | * through ui.pipe(). Pauses any active 393 | * prompts, logs the data and then if 394 | * paused, resumes the prompt. 395 | * 396 | * @return {UI} 397 | * @api public 398 | */ 399 | 400 | log() { 401 | let args = util.fixArgsForApply(arguments); 402 | args = (_.isFunction(this._pipeFn)) ? 403 | this._pipeFn(args) : 404 | args; 405 | if (args === '') { 406 | return this; 407 | } 408 | args = util.fixArgsForApply(args); 409 | if (this.midPrompt()) { 410 | const data = this.pause(); 411 | console.log.apply(console.log, args); 412 | if (typeof data !== 'undefined' && data !== false) { 413 | this.resume(data); 414 | } else { 415 | console.log('Log got back \'false\' as data. This shouldn\'t happen.', data); 416 | } 417 | } else { 418 | console.log.apply(console.log, args); 419 | } 420 | return this; 421 | } 422 | 423 | /** 424 | * Submits a given prompt. 425 | * 426 | * @param {String} value 427 | * @return {UI} 428 | * @api public 429 | */ 430 | 431 | submit() { 432 | if (this._activePrompt) { 433 | // this._activePrompt.screen.onClose(); 434 | this._activePrompt.rl.emit('line'); 435 | // this._activePrompt.onEnd({isValid: true, value: value}); 436 | // to do - I don't know a good way to do this. 437 | } 438 | return this; 439 | } 440 | 441 | /** 442 | * Does a literal, one-time write to the 443 | * *current* prompt delimiter. 444 | * 445 | * @param {String} str 446 | * @return {UI} 447 | * @api public 448 | */ 449 | 450 | delimiter(str) { 451 | if (!this._activePrompt) { 452 | return this; 453 | } 454 | const prompt = this._activePrompt; 455 | if (str === undefined) { 456 | return prompt.opt.message; 457 | } 458 | prompt.opt.message = str; 459 | this.refresh(); 460 | return this; 461 | } 462 | 463 | /** 464 | * Re-writes the input of an Inquirer prompt. 465 | * If no string is passed, it gets the current 466 | * input. 467 | * 468 | * @param {String} str 469 | * @return {String} 470 | * @api public 471 | */ 472 | 473 | input(str) { 474 | if (!this._activePrompt) { 475 | return undefined; 476 | } 477 | const prompt = this._activePrompt; 478 | if (str === undefined) { 479 | return prompt.rl.line; 480 | } 481 | const width = prompt.rl.line.length; 482 | prompt.rl.line = str; 483 | const newWidth = prompt.rl.line.length; 484 | const diff = newWidth - width; 485 | prompt.rl.cursor += diff; 486 | const cursor = 0; 487 | let message = prompt.getQuestion(); 488 | const addition = (prompt.status === 'answered') ? 489 | chalk.cyan(prompt.answer) : 490 | prompt.rl.line; 491 | message += addition; 492 | prompt.screen.render(message, {cursor: cursor}); 493 | return this; 494 | } 495 | 496 | /** 497 | * Logs the current delimiter and typed data. 498 | * 499 | * @return {UI} 500 | * @api public 501 | */ 502 | 503 | imprint() { 504 | if (!this.parent) { 505 | return this; 506 | } 507 | const val = this._activePrompt.rl.line; 508 | const delimiter = this._lastDelimiter || this.delimiter() || ''; 509 | this.log(delimiter + val); 510 | return this; 511 | } 512 | 513 | /** 514 | * Redraws the inquirer prompt with a new string. 515 | * 516 | * @param {String} str 517 | * @return {UI} 518 | * @api private 519 | */ 520 | 521 | refresh() { 522 | if (!this.parent || !this._activePrompt) { 523 | return this; 524 | } 525 | this._activePrompt.screen.clean(); 526 | this._activePrompt.render(); 527 | this._activePrompt.rl.output.write(this._activePrompt.rl.line); 528 | return this; 529 | } 530 | 531 | /** 532 | * Writes over existing logging. 533 | * 534 | * @param {String} str 535 | * @return {UI} 536 | * @api public 537 | */ 538 | 539 | redraw(str) { 540 | logUpdate(str); 541 | return this; 542 | } 543 | 544 | } 545 | 546 | /** 547 | * Initialize singleton. 548 | */ 549 | 550 | const ui = new UI(); 551 | 552 | /** 553 | * Clears logging from `ui.redraw` 554 | * permanently. 555 | * 556 | * @return {UI} 557 | * @api public 558 | */ 559 | 560 | ui.redraw.clear = function () { 561 | logUpdate.clear(); 562 | return ui; 563 | }; 564 | 565 | /** 566 | * Prints logging from `ui.redraw` 567 | * permanently. 568 | * 569 | * @return {UI} 570 | * @api public 571 | */ 572 | 573 | ui.redraw.done = function () { 574 | logUpdate.done(); 575 | ui.refresh(); 576 | return ui; 577 | }; 578 | 579 | /** 580 | * Expose `ui`. 581 | * 582 | * Modifying global? WTF?!? Yes. It is evil. 583 | * However node.js prompts are also quite 584 | * evil in a way. Nothing prevents dual prompts 585 | * between applications in the same terminal, 586 | * and inquirer doesn't catch or deal with this, so 587 | * if you want to start two independent instances of 588 | * vorpal, you need to know that prompt listeners 589 | * have already been initiated, and that you can 590 | * only attach the tty to one vorpal instance 591 | * at a time. 592 | * When you fire inqurier twice, you get a double-prompt, 593 | * where every keypress fires twice and it's just a 594 | * total mess. So forgive me. 595 | */ 596 | 597 | global.__vorpal = global.__vorpal || {}; 598 | global.__vorpal.ui = global.__vorpal.ui || { 599 | exists: false, 600 | exports: undefined 601 | }; 602 | 603 | if (!global.__vorpal.ui.exists) { 604 | global.__vorpal.ui.exists = true; 605 | global.__vorpal.ui.exports = ui; 606 | module.exports = exports = global.__vorpal.ui.exports; 607 | } else { 608 | module.exports = global.__vorpal.ui.exports; 609 | } 610 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const _ = require('lodash'); 8 | const minimist = require('minimist'); 9 | const strip = require('strip-ansi'); 10 | 11 | const util = { 12 | /** 13 | * Parses command arguments from multiple 14 | * sources. 15 | * 16 | * @param {String} str 17 | * @param {Object} opts 18 | * @return {Array} 19 | * @api private 20 | */ 21 | 22 | parseArgs: function (str, opts) { 23 | const reg = /"(.*?)"|'(.*?)'|`(.*?)`|([^\s"]+)/gi; 24 | let arr = []; 25 | let match; 26 | do { 27 | match = reg.exec(str); 28 | if (match !== null) { 29 | arr.push(match[1] || match[2] || match[3] || match[4]); 30 | } 31 | } while (match !== null); 32 | 33 | arr = minimist(arr, opts); 34 | arr._ = arr._ || []; 35 | return arr; 36 | }, 37 | 38 | /** 39 | * Prepares a command and all its parts for execution. 40 | * 41 | * @param {String} command 42 | * @param {Array} commands 43 | * @return {Object} 44 | * @api public 45 | */ 46 | 47 | parseCommand: function (command, commands) { 48 | const self = this; 49 | let pipes = []; 50 | let match; 51 | let matchArgs; 52 | let matchParts; 53 | 54 | function parsePipes() { 55 | // First, split the command by pipes naively. 56 | // This will split command arguments in half when the argument contains a pipe character. 57 | // For example, say "(Vorpal|vorpal)" will be split into ['say "(Vorpal', 'vorpal)'] which isn't good. 58 | const naivePipes = String(command).trim().split('|'); 59 | 60 | // Contruct empty array to place correctly split commands into. 61 | const newPipes = []; 62 | 63 | // We will look for pipe characters within these quotes to rejoin together. 64 | const quoteChars = ['"', '\'', '`']; 65 | 66 | // This will expand to contain one boolean key for each type of quote. 67 | // The value keyed by the quote is toggled off and on as quote type is opened and closed. 68 | // Example { "`": true, "'": false } would mean that there is an open angle quote. 69 | const quoteTracker = {}; 70 | 71 | // The current command piece before being rejoined with it's over half. 72 | // Since it's not common for pipes to occur in commands, this is usually the entire command pipe. 73 | let commandPart = ''; 74 | 75 | // Loop through each naive pipe. 76 | for (const key in naivePipes) { 77 | // It's possible/likely that this naive pipe is the whole pipe if it doesn't contain an unfinished quote. 78 | const possiblePipe = naivePipes[key]; 79 | commandPart += possiblePipe; 80 | 81 | // Loop through each individual character in the possible pipe tracking the opening and closing of quotes. 82 | for (let i = 0; i < possiblePipe.length; i++) { 83 | const char = possiblePipe[i]; 84 | if (quoteChars.indexOf(char) !== -1) { 85 | quoteTracker[char] = !quoteTracker[char]; 86 | } 87 | } 88 | 89 | // Does the pipe end on an unfinished quote? 90 | const inQuote = _.some(quoteChars, (quoteChar) => quoteTracker[quoteChar]); 91 | 92 | // If the quotes have all been closed or this is the last possible pipe in the array, add as pipe. 93 | if (!inQuote || key * 1 === naivePipes.length - 1) { 94 | newPipes.push(commandPart.trim()); 95 | commandPart = ''; 96 | } else { 97 | // Quote was left open. The pipe character was previously removed when the array was split. 98 | commandPart += '|'; 99 | } 100 | } 101 | 102 | // Set the first pipe to command and the rest to pipes. 103 | command = newPipes.shift(); 104 | pipes = pipes.concat(newPipes); 105 | } 106 | 107 | function parseMatch() { 108 | matchParts = self.matchCommand(command, commands); 109 | match = matchParts.command; 110 | matchArgs = matchParts.args; 111 | } 112 | 113 | parsePipes(); 114 | parseMatch(); 115 | 116 | if (match && _.isFunction(match._parse)) { 117 | command = match._parse(command, matchParts.args); 118 | parsePipes(); 119 | parseMatch(); 120 | } 121 | 122 | return ({ 123 | command: command, 124 | match: match, 125 | matchArgs: matchArgs, 126 | pipes: pipes 127 | }); 128 | }, 129 | 130 | /** 131 | * Run a raw command string, e.g. foo -bar 132 | * against a given list of commands, 133 | * and if there is a match, parse the 134 | * results. 135 | * 136 | * @param {String} cmd 137 | * @param {Array} cmds 138 | * @return {Object} 139 | * @api public 140 | */ 141 | 142 | matchCommand: function (cmd, cmds) { 143 | const parts = String(cmd).trim().split(' '); 144 | 145 | let match; 146 | let matchArgs; 147 | for (let i = 0; i < parts.length; ++i) { 148 | const subcommand = String(parts.slice(0, parts.length - i).join(' ')).trim(); 149 | match = _.find(cmds, {_name: subcommand}) || match; 150 | if (!match) { 151 | for (const key in cmds) { 152 | const cmd = cmds[key]; 153 | const idx = cmd._aliases.indexOf(subcommand); 154 | match = (idx > -1) ? cmd : match; 155 | } 156 | } 157 | if (match) { 158 | matchArgs = parts.slice(parts.length - i, parts.length).join(' '); 159 | break; 160 | } 161 | } 162 | // If there's no command match, check if the 163 | // there's a `catch` command, which catches all 164 | // missed commands. 165 | if (!match) { 166 | match = _.find(cmds, {_catch: true}); 167 | // If there is one, we still need to make sure we aren't 168 | // partially matching command groups, such as `do things` when 169 | // there is a command `do things well`. If we match partially, 170 | // we still want to show the help menu for that command group. 171 | if (match) { 172 | const allCommands = _.map(cmds, '_name'); 173 | let wordMatch = false; 174 | for (const key in allCommands) { 175 | const cmd = allCommands[key]; 176 | const parts2 = String(cmd).split(' '); 177 | const cmdParts = String(match.command).split(' '); 178 | let matchAll = true; 179 | for (let k = 0; k < cmdParts.length; ++k) { 180 | if (parts2[k] !== cmdParts[k]) { 181 | matchAll = false; 182 | break; 183 | } 184 | } 185 | if (matchAll) { 186 | wordMatch = true; 187 | break; 188 | } 189 | } 190 | if (wordMatch) { 191 | match = undefined; 192 | } else { 193 | matchArgs = cmd; 194 | } 195 | } 196 | } 197 | 198 | return ({ 199 | command: match, 200 | args: matchArgs 201 | }); 202 | }, 203 | 204 | buildCommandArgs: function (passedArgs, cmd, execCommand, isCommandArgKeyPairNormalized) { 205 | let args = {options: {}}; 206 | 207 | if(isCommandArgKeyPairNormalized) { 208 | // Normalize all foo="bar" with "foo='bar'" 209 | // This helps implement unix-like key value pairs. 210 | const reg = /(['"]?)(\w+)=(?:(['"])((?:(?!\3).)*)\3|(\S+))\1/g; 211 | passedArgs = passedArgs.replace(reg, `"$2='$4$5'"`); 212 | } 213 | 214 | // Types are custom arg types passed 215 | // into `minimist` as per its docs. 216 | const types = cmd._types || {}; 217 | 218 | // Make a list of all boolean options 219 | // registered for this command. These are 220 | // simply commands that don't have required 221 | // or optional args. 222 | const booleans = []; 223 | cmd.options.forEach(function (opt) { 224 | if (opt.required === 0 && opt.optional === 0) { 225 | if (opt.short) { 226 | booleans.push(opt.short); 227 | } 228 | if (opt.long) { 229 | booleans.push(opt.long); 230 | } 231 | } 232 | }); 233 | 234 | // Review the args passed into the command, 235 | // and filter out the boolean list to only those 236 | // options passed in. 237 | // This returns a boolean list of all options 238 | // passed in by the caller, which don't have 239 | // required or optional args. 240 | const passedArgParts = passedArgs.split(' '); 241 | types.boolean = booleans.map(function (str) { 242 | return String(str).replace(/^-*/, ''); 243 | }).filter(function (str) { 244 | let match = false; 245 | let strings = [`-${str}`, `--${str}`, `--no-${str}`]; 246 | for (let i = 0; i < passedArgParts.length; ++i) { 247 | if (strings.indexOf(passedArgParts[i]) > -1) { 248 | match = true; 249 | break; 250 | } 251 | } 252 | return match; 253 | }); 254 | 255 | // Use minimist to parse the args. 256 | const parsedArgs = this.parseArgs(passedArgs, types); 257 | 258 | function validateArg(arg, cmdArg) { 259 | return !(arg === undefined && cmdArg.required === true); 260 | } 261 | 262 | // Builds varidiac args and options. 263 | let valid = true; 264 | const remainingArgs = _.clone(parsedArgs._); 265 | for (let l = 0; l < 10; ++l) { 266 | const matchArg = cmd._args[l]; 267 | const passedArg = parsedArgs._[l]; 268 | if (matchArg !== undefined) { 269 | valid = (!valid) ? false : validateArg(parsedArgs._[l], matchArg); 270 | if (!valid) { 271 | break; 272 | } 273 | if (passedArg !== undefined) { 274 | if (matchArg.variadic === true) { 275 | args[matchArg.name] = remainingArgs; 276 | } else { 277 | args[matchArg.name] = passedArg; 278 | remainingArgs.shift(); 279 | } 280 | } 281 | } 282 | } 283 | 284 | if (!valid) { 285 | return '\n Missing required argument. Showing Help:'; 286 | } 287 | 288 | // Looks for ommitted required options and throws help. 289 | for (let m = 0; m < cmd.options.length; ++m) { 290 | const o = cmd.options[m]; 291 | const short = String(o.short || '').replace(/-/g, ''); 292 | const long = String(o.long || '').replace(/--no-/g, '').replace(/^-*/g, ''); 293 | let exist = (parsedArgs[short] !== undefined) ? parsedArgs[short] : undefined; 294 | exist = (exist === undefined && parsedArgs[long] !== undefined) ? parsedArgs[long] : exist; 295 | const existsNotSet = (exist === true || exist === false); 296 | if (existsNotSet && o.required !== 0) { 297 | return `\n Missing required value for option ${(o.long || o.short)}. Showing Help:`; 298 | } 299 | if (exist !== undefined) { 300 | args.options[long || short] = exist; 301 | } 302 | } 303 | 304 | // Looks for supplied options that don't 305 | // exist in the options list. 306 | // If the command allows unknown options, 307 | // adds it, otherwise throws help. 308 | const passedOpts = _.chain(parsedArgs) 309 | .keys() 310 | .pull('_') 311 | .pull('help') 312 | .value(); 313 | for (const key in passedOpts) { 314 | const opt = passedOpts[key]; 315 | const optionFound = _.find(cmd.options, function (expected) { 316 | if ('--' + opt === expected.long || 317 | '--no-' + opt === expected.long || 318 | '-' + opt === expected.short) { 319 | return true; 320 | } 321 | return false; 322 | }); 323 | if (optionFound === undefined) { 324 | if (cmd._allowUnknownOptions) { 325 | args.options[opt] = parsedArgs[opt]; 326 | } else { 327 | return `\n Invalid option: '${opt}'. Showing Help:`; 328 | } 329 | } 330 | } 331 | 332 | // If args were passed into the programmatic 333 | // `vorpal.exec(cmd, args, callback)`, merge 334 | // them here. 335 | if (execCommand && execCommand.args && _.isObject(execCommand.args)) { 336 | args = _.extend(args, execCommand.args); 337 | } 338 | 339 | // Looks for a help arg and throws help if any. 340 | if (parsedArgs.help || parsedArgs._.indexOf('/?') > -1) { 341 | args.options.help = true; 342 | } 343 | 344 | return args; 345 | }, 346 | 347 | /** 348 | * Makes an argument name pretty for help. 349 | * 350 | * @param {String} arg 351 | * @return {String} 352 | * @api private 353 | */ 354 | 355 | humanReadableArgName: function (arg) { 356 | const nameOutput = arg.name + (arg.variadic === true ? '...' : ''); 357 | return arg.required ? 358 | `<${nameOutput}>` : 359 | `[${nameOutput}]`; 360 | }, 361 | 362 | /** 363 | * Formats an array to display in a TTY 364 | * in a pretty fashion. 365 | * 366 | * @param {Array} arr 367 | * @return {String} 368 | * @api public 369 | */ 370 | 371 | prettifyArray: function (arr) { 372 | arr = arr || []; 373 | const arrClone = _.clone(arr); 374 | const width = process.stdout.columns; 375 | const longest = strip((arrClone.sort(function (a, b) { 376 | return strip(b).length - strip(a).length; 377 | })[0] || '')).length + 2; 378 | const fullWidth = strip(String(arr.join(''))).length; 379 | const fitsOneLine = ((fullWidth + (arr.length * 2)) <= width); 380 | let cols = Math.floor(width / longest); 381 | cols = (cols < 1) ? 1 : cols; 382 | if (fitsOneLine) { 383 | return arr.join(' '); 384 | } 385 | let col = 0; 386 | const lines = []; 387 | let line = ''; 388 | for (const key in arr) { 389 | const arrEl = arr[key]; 390 | if (col < cols) { 391 | col++; 392 | } else { 393 | lines.push(line); 394 | line = ''; 395 | col = 1; 396 | } 397 | line += this.pad(arrEl, longest, ' '); 398 | } 399 | if (line !== '') { 400 | lines.push(line); 401 | } 402 | return lines.join('\n'); 403 | }, 404 | 405 | /** 406 | * Pads a value with with space or 407 | * a specified delimiter to match a 408 | * given width. 409 | * 410 | * @param {String} str 411 | * @param {Integer} width 412 | * @param {String} delimiter 413 | * @return {String} 414 | * @api private 415 | */ 416 | 417 | pad: function (str, width, delimiter) { 418 | width = Math.floor(width); 419 | delimiter = delimiter || ' '; 420 | const len = Math.max(0, width - strip(str).length); 421 | return str + Array(len + 1).join(delimiter); 422 | }, 423 | 424 | /** 425 | * Pad a row on the start and end with spaces. 426 | * 427 | * @param {String} str 428 | * @return {String} 429 | */ 430 | padRow: function (str) { 431 | return str.split('\n').map(function (row) { 432 | return ' ' + row + ' '; 433 | }).join('\n'); 434 | }, 435 | 436 | // When passing down applied args, we need to turn 437 | // them from `{ '0': 'foo', '1': 'bar' }` into ['foo', 'bar'] 438 | // instead. 439 | fixArgsForApply: function (obj) { 440 | if (!_.isObject(obj)) { 441 | if (!_.isArray(obj)) { 442 | return [obj]; 443 | } 444 | return obj; 445 | } 446 | const argArray = []; 447 | for (const key in obj) { 448 | const aarg = obj[key]; 449 | argArray.push(aarg); 450 | } 451 | return argArray; 452 | } 453 | }; 454 | 455 | /** 456 | * Expose `util`. 457 | */ 458 | 459 | module.exports = exports = util; 460 | -------------------------------------------------------------------------------- /lib/vorpal-commons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Function library for Vorpal's out-of-the-box 5 | * API commands. Imported into a Vorpal server 6 | * through vorpal.use(module). 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var _ = require('lodash'); 14 | 15 | module.exports = function (vorpal) { 16 | /** 17 | * Help for a particular command. 18 | */ 19 | 20 | vorpal 21 | .command('help [command...]') 22 | .description('Provides help for a given command.') 23 | .action(function (args, cb) { 24 | const self = this; 25 | if (args.command) { 26 | args.command = args.command.join(' '); 27 | var name = _.find(this.parent.commands, {_name: String(args.command).trim()}); 28 | if (name && !name._hidden) { 29 | if (_.isFunction(name._help)) { 30 | name._help(args.command, function (str) { 31 | self.log(str); 32 | cb(); 33 | }); 34 | return; 35 | } 36 | this.log(name.helpInformation()); 37 | } else { 38 | this.log(this.parent._commandHelp(args.command)); 39 | } 40 | } else { 41 | this.log(this.parent._commandHelp(args.command)); 42 | } 43 | cb(); 44 | }); 45 | 46 | /** 47 | * Exits Vorpal. 48 | */ 49 | 50 | vorpal 51 | .command('exit') 52 | .alias('quit') 53 | .description('Exits application.') 54 | .action(function (args) { 55 | args.options = args.options || {}; 56 | args.options.sessionId = this.session.id; 57 | this.parent.exit(args.options); 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vorpal", 3 | "version": "1.11.4", 4 | "description": "Node's first framework for building immersive CLI apps.", 5 | "main": "./dist/vorpal.js", 6 | "scripts": { 7 | "test": "gulp build; mocha;", 8 | "prepublish": "in-publish && gulp build || not-in-publish" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/dthree/vorpal.git" 13 | }, 14 | "keywords": [ 15 | "api", 16 | "cli", 17 | "repl", 18 | "shell", 19 | "immersive", 20 | "framework", 21 | "app", 22 | "application", 23 | "command", 24 | "commander", 25 | "automated", 26 | "prompt", 27 | "inquirer" 28 | ], 29 | "author": "dthree", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/dthree/vorpal/issues" 33 | }, 34 | "homepage": "https://github.com/dthree/vorpal#readme", 35 | "devDependencies": { 36 | "babel": "^6.3.26", 37 | "babel-core": "^6.4.5", 38 | "babel-preset-es2015": "^6.3.13", 39 | "bluebird": "^3.1.1", 40 | "gulp": "^3.9.0", 41 | "gulp-babel": "^6.1.2", 42 | "gulp-changed": "^1.3.0", 43 | "gulp-eslint": "^1.1.1", 44 | "gulp-xo": "^0.7.0", 45 | "load-plugins": "^2.1.0", 46 | "mocha": "^2.2.5", 47 | "moment": "^2.10.3", 48 | "request": "^2.58.0", 49 | "should": "^6.0.3", 50 | "vorpal-less": "0.0.4", 51 | "vorpal-repl": "^1.1.8", 52 | "xo": "^0.9.0" 53 | }, 54 | "dependencies": { 55 | "babel-polyfill": "^6.3.14", 56 | "chalk": "^1.1.0", 57 | "in-publish": "^2.0.0", 58 | "inquirer": "0.11.0", 59 | "lodash": "^4.5.1", 60 | "log-update": "^1.0.2", 61 | "minimist": "^1.2.0", 62 | "node-localstorage": "^0.6.0", 63 | "strip-ansi": "^3.0.0", 64 | "wrap-ansi": "^2.0.0" 65 | }, 66 | "engines": { 67 | "node": ">= 0.10.0", 68 | "iojs": ">= 1.0.0" 69 | }, 70 | "xo": { 71 | "space": true, 72 | "rules": { 73 | "no-eval": 0, 74 | "no-unused-expressions": 0, 75 | "max-nested-callbacks": 0, 76 | "no-proto": 0, 77 | "wrap-iife": 0, 78 | "global-require": 0, 79 | "no-negated-condition": 0, 80 | "no-loop-func": 0, 81 | "no-implicit-coercion": 0, 82 | "no-use-extend-native/no-use-extend-native": 0, 83 | "no-undef": 0 84 | } 85 | }, 86 | "files": [ 87 | "dist" 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /test/autocomplete.js: -------------------------------------------------------------------------------- 1 | var Vorpal = require('../'); 2 | var assert = require('assert'); 3 | var _ = require('lodash'); 4 | var vorpal = new Vorpal(); 5 | 6 | describe('session._autocomplete', function () { 7 | it('should return longest possible match', function () { 8 | var result = vorpal.session._autocomplete('c', ['cmd', 'cme', 'def']); 9 | assert.equal(result, 'cm'); 10 | }); 11 | 12 | it('should return list of matches when there are no more common characters', function () { 13 | var result = vorpal.session._autocomplete('c', ['cmd', 'ced']); 14 | assert.equal(result.length, 2); 15 | assert.equal(result[0], 'ced'); 16 | assert.equal(result[1], 'cmd'); 17 | }); 18 | 19 | it('should return list of matches even if we have a complete match', function () { 20 | var result = vorpal.session._autocomplete('cmd', ['cmd', 'cmd2']); 21 | assert.equal(result.length, 2); 22 | assert.equal(result[0], 'cmd'); 23 | assert.equal(result[1], 'cmd2'); 24 | }); 25 | 26 | it('should return undefined if no match', function () { 27 | var result = vorpal.session._autocomplete('cmd', ['def', 'xyz']); 28 | assert.equal(result, undefined); 29 | }); 30 | 31 | it('should return the match if only a single possible match exists', function () { 32 | var result = vorpal.session._autocomplete('d', ['def', 'xyz']); 33 | assert.equal(result, 'def '); 34 | }); 35 | 36 | 37 | it('should return the prefix along with the partial match when supplied with a prefix input', function() { 38 | var result = vorpal.session._autocomplete('foo/de', ['dally','definitive', 'definitop', 'bob']); 39 | assert.equal(result, "foo/definit"); 40 | }); 41 | 42 | it("should return a list of matches when supplied with a prefix but no value post prefix", function() { 43 | var result = vorpal.session._autocomplete('foo/', ['dally','definitive', 'definitop', 'bob']); 44 | assert.equal(result.length, 4); 45 | assert.equal(result[0], "bob"); 46 | assert.equal(result[1], "dally"); 47 | assert.equal(result[2], "definitive"); 48 | assert.equal(result[3], "definitop"); 49 | }); 50 | }); -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var Vorpal = require('../'); 2 | var should = require('should'); 3 | 4 | var vorpal = new Vorpal(); 5 | 6 | require('assert'); 7 | 8 | describe('vorpal', function () { 9 | describe('constructor', function () { 10 | it('should exist and be a function', function () { 11 | should.exist(Vorpal); 12 | Vorpal.should.be.type('function'); 13 | }); 14 | }); 15 | 16 | describe('.parse', function () { 17 | it('should exist and be a function', function () { 18 | should.exist(vorpal.parse); 19 | vorpal.parse.should.be.type('function'); 20 | }); 21 | 22 | it('should expose minimist', function () { 23 | var result = vorpal.parse(['a', 'b', 'foo', 'bar', '-r'], {use: 'minimist'}); 24 | result.r.should.be.true; 25 | (result._.indexOf('foo') > -1).should.be.true; 26 | (result._.indexOf('bar') > -1).should.be.true; 27 | result._.length.should.equal(2); 28 | }); 29 | }); 30 | 31 | describe('mode context', function () { 32 | it('parent should have the same context in init and action', function (done) { 33 | var vorpal = Vorpal(); 34 | var initCtx; 35 | vorpal 36 | .mode('ooga') 37 | .init(function (args, cb) { 38 | initCtx = this.parent; 39 | cb() 40 | }) 41 | .action(function (args, cb) { 42 | this.parent.should.equal(initCtx) 43 | cb() 44 | done() 45 | }); 46 | vorpal.exec('ooga') 47 | .then(function () { 48 | vorpal.exec('booga') 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/util/server.js: -------------------------------------------------------------------------------- 1 | require('assert'); 2 | require('should'); 3 | 4 | module.exports = function (vorpal) { 5 | vorpal 6 | .mode('repl', 'Enters REPL Mode.') 7 | .init(function (args, cb) { 8 | this.log('Entering REPL Mode.'); 9 | cb(); 10 | }) 11 | .action(function (command, cb) { 12 | var res = eval(command); 13 | this.log(res); 14 | cb(res); 15 | }); 16 | 17 | vorpal 18 | .command('foo') 19 | .description('Should return \'bar\'.') 20 | .action(function () { 21 | var self = this; 22 | return new Promise(function (resolve) { 23 | self.log('bar'); 24 | resolve(); 25 | }); 26 | }); 27 | 28 | vorpal.command('say ', 'say something') 29 | .action(function (args, cb) { 30 | this.log(args.words); 31 | cb(); 32 | }); 33 | 34 | vorpal.command('prompt default ', 'action prompt') 35 | .action(function(args, cb) { 36 | 37 | return this.prompt([ 38 | { 39 | type: 'input', 40 | name: 'project', 41 | message: 'Project: ', 42 | default: args.defaultValue 43 | } 44 | ]); 45 | 46 | }); 47 | 48 | vorpal.command('parse me ', 'Takes input and adds a reverse pipe to it.') 49 | .parse(function (str) { 50 | return str + ' | reverse'; 51 | }) 52 | .action(function (args, cb) { 53 | this.log(args.words); 54 | cb(); 55 | }); 56 | 57 | vorpal.command('custom-help', 'Outputs custom help.') 58 | .help(function (args, cb) { 59 | this.log('This is a custom help output.'); 60 | cb(); 61 | }) 62 | .action(function (args, cb) { 63 | cb(); 64 | }); 65 | 66 | vorpal.command('reverse [words]', 'append bar to stdin') 67 | .alias('r') 68 | .action(function (args, cb) { 69 | var stdin = args.stdin || args.words; 70 | stdin = String(stdin).split('').reverse().join(''); 71 | this.log(stdin); 72 | cb(); 73 | }); 74 | 75 | vorpal.command('sync [word]', 'run sync') 76 | .action(function (args) { 77 | if (args.word === undefined) { 78 | return 'no args were passed'; 79 | } 80 | if (args.word === 'throwme') { 81 | throw new Error('You said so...'); 82 | } 83 | return 'you said ' + args.word; 84 | }); 85 | 86 | vorpal.command('array [string]', 'convert string to an array.') 87 | .action(function (args, cb) { 88 | var stdin = args.stdin || args.string; 89 | stdin = String(stdin).split(''); 90 | this.log(stdin); 91 | cb(); 92 | }); 93 | 94 | vorpal 95 | .command('fuzzy') 96 | .description('Should return \'wuzzy\'.') 97 | .action(function () { 98 | var self = this; 99 | return new Promise(function (resolve) { 100 | self.log('wuzzy'); 101 | resolve(); 102 | }); 103 | }); 104 | 105 | vorpal 106 | .command('optional [arg]') 107 | .description('Should optionally return an arg.') 108 | .action(function (args) { 109 | var self = this; 110 | return new Promise(function (resolve) { 111 | self.log(args.arg || ''); 112 | resolve(); 113 | }); 114 | }); 115 | 116 | vorpal 117 | .command('variadic [pizza] [ingredients...]') 118 | .description('Should optionally return an arg.') 119 | .option('-e, --extra', 'Extra complexity on the place.') 120 | .action(function (args, cb) { 121 | cb(undefined, args); 122 | }); 123 | 124 | vorpal 125 | .command('typehappy') 126 | .option('-n, --numberify ', 'Should be a number') 127 | .option('-s, --stringify ', 'Should be a string') 128 | .types({ 129 | string: ['s', 'stringify'] 130 | }) 131 | .action(function (args, cb) { 132 | cb(undefined, args); 133 | }); 134 | 135 | vorpal 136 | .command('cmd [with] [one] [million] [arguments] [in] [it]') 137 | .description('Should deal with many args.') 138 | .option('-e, --extra', 'Extra complexity on the place.') 139 | .action(function (args, cb) { 140 | cb(undefined, args); 141 | }); 142 | 143 | vorpal 144 | .command('variadic-pizza [ingredients...]') 145 | .description('Should optionally return an arg.') 146 | .option('-e, --extra', 'Extra complexity on the place.') 147 | .action(function (args, cb) { 148 | cb(undefined, args); 149 | }); 150 | 151 | vorpal 152 | .command('port') 153 | .description('Returns port.') 154 | .action(function (args, cb) { 155 | this.log(this.server._port); 156 | cb(undefined, this.parent.server._port); 157 | }); 158 | 159 | vorpal 160 | .command('i want') 161 | .description('Negative args.') 162 | .option('-N, --no-cheese', 'No chease please.') 163 | .action(function (args, cb) { 164 | this.log(args.options.cheese); 165 | cb(); 166 | }); 167 | 168 | vorpal 169 | .command('hyphenated-option') 170 | .description('Negative args.') 171 | .option('--dry-run', 'Perform dry run only.') 172 | .action(function (args, cb) { 173 | this.log(args.options['dry-run']); 174 | cb(); 175 | }); 176 | 177 | vorpal 178 | .command('required ') 179 | .description('Must return an arg.') 180 | .action(function (args, cb) { 181 | this.log(args.arg); 182 | cb(undefined, args); 183 | }); 184 | 185 | vorpal 186 | .command('required-option') 187 | .description('Must return an arg.') 188 | .option('--arg ', 'Arg to return.') 189 | .action(function (args, cb) { 190 | this.log(args.options.arg); 191 | cb(undefined, args); 192 | }); 193 | 194 | vorpal 195 | .command('unknown-option') 196 | .description('shows help if we pass any unknown option.') 197 | .action(function (args, cb) { 198 | this.log('should never see this'); 199 | cb(undefined, args); 200 | }); 201 | 202 | vorpal 203 | .command('fail me ') 204 | .description('Must return an arg.') 205 | .action(function (args) { 206 | return new Promise(function (resolve, reject) { 207 | if (args.arg === 'not') { 208 | resolve('we are happy'); 209 | } else { 210 | reject('we are not happy.'); 211 | } 212 | }); 213 | }); 214 | 215 | vorpal 216 | .command('deep command [arg]') 217 | .description('Tests execution of deep command.') 218 | .action(function (args) { 219 | var self = this; 220 | return new Promise(function (resolve) { 221 | self.log(args.arg); 222 | resolve(); 223 | }); 224 | }); 225 | 226 | vorpal 227 | .command('very deep command [arg]') 228 | .description('Tests execution of three-deep command.') 229 | .action(function (args) { 230 | var self = this; 231 | return new Promise(function (resolve) { 232 | self.log(args.arg); 233 | resolve(); 234 | }); 235 | }); 236 | 237 | vorpal 238 | .command('count ') 239 | .description('Tests execution of three-deep command.') 240 | .action(function (args) { 241 | var self = this; 242 | return new Promise(function (resolve) { 243 | self.log(args.number); 244 | resolve(); 245 | }); 246 | }); 247 | 248 | vorpal 249 | .command('very complicated deep command [arg]') 250 | .option('-r', 'Test Option.') 251 | .option('-a', 'Test Option.') 252 | .option('-d', 'Test Option.') 253 | .option('-s, --sleep', 'Test Option.') 254 | .option('-t', 'Test Option.') 255 | .option('-i [param]', 'Test Option.') 256 | .description('Tests execution of three-deep command.') 257 | .action(function (args) { 258 | var self = this; 259 | return new Promise(function (resolve) { 260 | var str = ''; 261 | str = (args.options.r === true) ? str + 'r' : str; 262 | str = (args.options.a === true) ? str + 'a' : str; 263 | str = (args.options.d === true) ? str + 'd' : str; 264 | str = (args.options.t === true) ? str + 't' : str; 265 | str = (args.options.i === 'j') ? str + args.options.i : str; 266 | str = (args.options.sleep === 'well') ? str + args.options.sleep : str; 267 | str += (args.arg || ''); 268 | self.log(str); 269 | resolve(); 270 | }); 271 | }); 272 | }; 273 | -------------------------------------------------------------------------------- /test/util/util.js: -------------------------------------------------------------------------------- 1 | var Vantage = require('../../'); 2 | var _ = require('lodash'); 3 | var path = require('path'); 4 | 5 | module.exports = { 6 | 7 | instances: [], 8 | 9 | spawn: function (options, cb) { 10 | options = options || {}; 11 | options = _.defaults(options, { 12 | ports: [], 13 | ssl: false 14 | }); 15 | 16 | for (var i = 0; i < options.ports.length; ++i) { 17 | var vorpal = new Vantage(); 18 | var port = options.ports[i]; 19 | vorpal 20 | .delimiter(port + ':') 21 | .use(path.join(__dirname, '/server')) 22 | .listen(port); 23 | module.exports.instances.push(vorpal); 24 | } 25 | 26 | cb(undefined, module.exports.instances); 27 | return; 28 | }, 29 | 30 | kill: function (what, cb) { 31 | cb = cb || function () {}; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /test/vorpal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the new testing file, as 3 | * the current one totally sucks. 4 | * eventually move all tests over to 5 | * this one. 6 | */ 7 | 8 | var Vorpal = require('../'); 9 | var should = require('should'); 10 | var assert = require('assert'); 11 | var intercept = require('../dist/intercept'); 12 | 13 | var vorpal; 14 | 15 | // Normalize inputs to objects. 16 | function obj(inp) { 17 | if (typeof inp === 'String') { 18 | return JSON.stringify(JSON.parse('(' + inp + ')')); 19 | } else { 20 | return JSON.stringify(inp); 21 | } 22 | } 23 | 24 | var stdout = ''; 25 | var umute; 26 | var mute = function () { 27 | unmute = intercept(function (str) { 28 | stdout += str; 29 | return ''; 30 | }); 31 | } 32 | 33 | vorpal = Vorpal(); 34 | vorpal 35 | .command('foo [args...]') 36 | .option('-b, --bool') 37 | .option('-r, --required ') 38 | .option('-o, --optional [str]') 39 | .action(function (args, cb) { 40 | return args; 41 | }); 42 | 43 | vorpal 44 | .command('bar') 45 | .allowUnknownOptions(true) 46 | .action(function (args, cb) { 47 | return args; 48 | }); 49 | 50 | vorpal 51 | .command('baz') 52 | .allowUnknownOptions(true) 53 | .allowUnknownOptions(false) 54 | .action(function (args, cb) { 55 | return args; 56 | }); 57 | 58 | vorpal 59 | .command('optional [str]') 60 | .action(function (args, cb) { 61 | return args; 62 | }); 63 | 64 | vorpal 65 | .command('required ') 66 | .action(function (args, cb) { 67 | return args; 68 | }); 69 | 70 | vorpal 71 | .command('multiple [opt] [variadic...]') 72 | .action(function (args, cb) { 73 | return args; 74 | }); 75 | 76 | vorpal 77 | .command('wrong-sequence [opt] [variadic...]') 78 | .action(function (args, cb) { 79 | return args; 80 | }); 81 | 82 | vorpal 83 | .command('multi word command [variadic...]') 84 | .action(function (args, cb) { 85 | return args; 86 | }); 87 | 88 | require('assert'); 89 | 90 | describe('argument parsing', function () { 91 | it('should execute a command with no args', function () { 92 | var fixture = obj({ options: {} }); 93 | obj(vorpal.execSync('foo')).should.equal(fixture); 94 | }); 95 | 96 | it('should execute a command without an optional arg', function () { 97 | var fixture = obj({ options: {} }); 98 | obj(vorpal.execSync('optional')).should.equal(fixture); 99 | }); 100 | 101 | it('should execute a command with an optional arg', function () { 102 | var fixture = obj({ options: {}, str: 'bar' }); 103 | obj(vorpal.execSync('optional bar')).should.equal(fixture); 104 | }); 105 | 106 | it('should execute a command with a required arg', function () { 107 | var fixture = obj({ options: {}, str: 'bar' }); 108 | obj(vorpal.execSync('required bar')).should.equal(fixture); 109 | }); 110 | 111 | it('should throw help when not passed a required arg', function () { 112 | mute(); 113 | var fixture = '\n Missing required argument. Showing Help:'; 114 | vorpal.execSync('required').should.equal(fixture); 115 | unmute(); 116 | }); 117 | 118 | it('should execute a command with multiple arg types', function () { 119 | var fixture = obj({ options: {}, req: 'foo', opt: 'bar', variadic: ['joe', 'smith'] }); 120 | obj(vorpal.execSync('multiple foo bar joe smith')).should.equal(fixture); 121 | }); 122 | 123 | it('should correct a command with wrong arg sequences declared', function () { 124 | var fixture = obj({ options: {}, req: 'foo', opt: 'bar', variadic: ['joe', 'smith'] }); 125 | obj(vorpal.execSync('multiple foo bar joe smith')).should.equal(fixture); 126 | }); 127 | 128 | it('should normalize key=value pairs', function () { 129 | var fixture = obj({ options: {}, 130 | req: "a='b'", 131 | opt: "c='d and e'", 132 | variadic: ["wombat='true'","a","fizz='buzz'","hello='goodbye'"] }); 133 | obj(vorpal.execSync('multiple a=\'b\' c="d and e" wombat=true a fizz=\'buzz\' "hello=\'goodbye\'"')).should.equal(fixture); 134 | }); 135 | 136 | it('should NOT normalize key=value pairs when isCommandArgKeyPairNormalized is false', function () { 137 | var fixture = obj({ options: {}, 138 | req: "hello=world", 139 | opt: 'hello="world"', 140 | variadic: ['hello=`world`'] 141 | }); 142 | vorpal.isCommandArgKeyPairNormalized = false; 143 | obj(vorpal.execSync('multiple "hello=world" \'hello="world"\' "hello=`world`"')).should.equal(fixture); 144 | vorpal.isCommandArgKeyPairNormalized = true; 145 | }); 146 | 147 | it('should execute multi-word command with arguments', function () { 148 | var fixture = obj({ options: {}, variadic: ['and', 'so', 'on'] }); 149 | obj(vorpal.execSync('multi word command and so on')).should.equal(fixture); 150 | }); 151 | 152 | it('should parse command with undefine in it as invalid', function () { 153 | var fixture = obj("Invalid command."); 154 | obj(vorpal.execSync('has undefine in it')).should.equal(fixture); 155 | }) 156 | }); 157 | 158 | describe('option parsing', function () { 159 | it('should execute a command with no options', function () { 160 | var fixture = obj({ options: {} }); 161 | obj(vorpal.execSync('foo')).should.equal(fixture); 162 | }); 163 | 164 | it('should execute a command with args and no options', function () { 165 | var fixture = obj({ options: {}, args: ['bar', 'smith'] }); 166 | obj(vorpal.execSync('foo bar smith')).should.equal(fixture); 167 | }); 168 | 169 | describe('options before an arg', function () { 170 | it('should accept a short boolean option', function () { 171 | var fixture = obj({ options: { bool: true }, args: ['bar', 'smith'] }); 172 | obj(vorpal.execSync('foo -b bar smith')).should.equal(fixture); 173 | }); 174 | 175 | it('should accept a long boolean option', function () { 176 | var fixture = obj({ options: { bool: true }, args: ['bar', 'smith'] }); 177 | obj(vorpal.execSync('foo --bool bar smith')).should.equal(fixture); 178 | }); 179 | 180 | it('should accept a short optional option', function () { 181 | var fixture = obj({ options: { optional: 'cheese' }, args: ['bar', 'smith'] }); 182 | obj(vorpal.execSync('foo --o cheese bar smith')).should.equal(fixture); 183 | }); 184 | 185 | it('should accept a long optional option', function () { 186 | var fixture = obj({ options: { optional: 'cheese' }, args: ['bar', 'smith'] }); 187 | obj(vorpal.execSync('foo --optional cheese bar smith')).should.equal(fixture); 188 | }); 189 | 190 | it('should accept a short required option', function () { 191 | var fixture = obj({ options: { required: 'cheese' }, args: ['bar', 'smith'] }); 192 | obj(vorpal.execSync('foo -r cheese bar smith')).should.equal(fixture); 193 | }); 194 | 195 | it('should accept a long required option', function () { 196 | var fixture = obj({ options: { required: 'cheese' }, args: ['bar', 'smith'] }); 197 | obj(vorpal.execSync('foo --required cheese bar smith')).should.equal(fixture); 198 | }); 199 | }); 200 | 201 | describe('options after args', function () { 202 | it('should accept a short boolean option', function () { 203 | var fixture = obj({ options: { bool: true }, args: ['bar', 'smith'] }); 204 | obj(vorpal.execSync('foo bar smith -b ')).should.equal(fixture); 205 | }); 206 | 207 | it('should accept a long boolean option', function () { 208 | var fixture = obj({ options: { bool: true }, args: ['bar', 'smith'] }); 209 | obj(vorpal.execSync('foo bar smith --bool ')).should.equal(fixture); 210 | }); 211 | 212 | it('should accept a short optional option', function () { 213 | var fixture = obj({ options: { optional: 'cheese' }, args: ['bar', 'smith'] }); 214 | obj(vorpal.execSync('foo bar smith --o cheese ')).should.equal(fixture); 215 | }); 216 | 217 | it('should accept a long optional option', function () { 218 | var fixture = obj({ options: { optional: 'cheese' }, args: ['bar', 'smith'] }); 219 | obj(vorpal.execSync('foo bar smith --optional cheese ')).should.equal(fixture); 220 | }); 221 | 222 | it('should accept a short required option', function () { 223 | var fixture = obj({ options: { required: 'cheese' }, args: ['bar', 'smith'] }); 224 | obj(vorpal.execSync('foo bar smith -r cheese ')).should.equal(fixture); 225 | }); 226 | 227 | it('should accept a long required option', function () { 228 | var fixture = obj({ options: { required: 'cheese' }, args: ['bar', 'smith'] }); 229 | obj(vorpal.execSync('foo bar smith --required cheese ')).should.equal(fixture); 230 | }); 231 | }); 232 | 233 | describe('options without an arg', function () { 234 | it('should accept a short boolean option', function () { 235 | var fixture = obj({ options: { bool: true }}); 236 | obj(vorpal.execSync('foo -b ')).should.equal(fixture); 237 | }); 238 | 239 | it('should accept a long boolean option', function () { 240 | var fixture = obj({ options: { bool: true }}); 241 | obj(vorpal.execSync('foo --bool ')).should.equal(fixture); 242 | }); 243 | 244 | it('should accept a short optional option', function () { 245 | var fixture = obj({ options: { optional: 'cheese' }}); 246 | obj(vorpal.execSync('foo --o cheese ')).should.equal(fixture); 247 | }); 248 | 249 | it('should accept a long optional option', function () { 250 | var fixture = obj({ options: { optional: 'cheese' }}); 251 | obj(vorpal.execSync('foo --optional cheese ')).should.equal(fixture); 252 | }); 253 | 254 | it('should accept a short required option', function () { 255 | var fixture = obj({ options: { required: 'cheese' }}); 256 | obj(vorpal.execSync('foo -r cheese ')).should.equal(fixture); 257 | }); 258 | 259 | it('should accept a long required option', function () { 260 | var fixture = obj({ options: { required: 'cheese' }}); 261 | obj(vorpal.execSync('foo --required cheese ')).should.equal(fixture); 262 | }); 263 | }); 264 | 265 | describe('option validation', function () { 266 | it('should execute a boolean option without an arg', function () { 267 | var fixture = obj({ options: { bool: true }}); 268 | obj(vorpal.execSync('foo -b')).should.equal(fixture); 269 | }); 270 | 271 | it('should execute an optional option without an arg', function () { 272 | var fixture = obj({ options: { optional: true }}); 273 | obj(vorpal.execSync('foo -o')).should.equal(fixture); 274 | }); 275 | 276 | it('should execute an optional option with an arg', function () { 277 | var fixture = obj({ options: { optional: 'cows' }}); 278 | obj(vorpal.execSync('foo -o cows')).should.equal(fixture); 279 | }); 280 | 281 | it('should execute a required option with an arg', function () { 282 | var fixture = obj({ options: { required: 'cows' }}); 283 | obj(vorpal.execSync('foo -r cows')).should.equal(fixture); 284 | }); 285 | 286 | it('should throw help on a required option without an arg', function () { 287 | var fixture = "\n Missing required value for option --required. Showing Help:"; 288 | mute(); 289 | vorpal.execSync('foo -r').should.equal(fixture); 290 | unmute(); 291 | }); 292 | }); 293 | 294 | describe('negated options', function () { 295 | it('should make a boolean option false', function () { 296 | var fixture = obj({ options: { bool: false }, args: ['cows'] }); 297 | obj(vorpal.execSync('foo --no-bool cows')).should.equal(fixture); 298 | }); 299 | 300 | it('should make an unfilled optional option false', function () { 301 | var fixture = obj({ options: { optional: false }, args: ['cows'] }); 302 | obj(vorpal.execSync('foo --no-optional cows')).should.equal(fixture); 303 | }); 304 | 305 | it('should ignore a filled optional option', function () { 306 | var fixture = obj({ options: { optional: false }, args: ['cows'] }); 307 | obj(vorpal.execSync('foo --no-optional cows')).should.equal(fixture); 308 | }); 309 | 310 | it('should return help on a required option', function () { 311 | var fixture = "\n Missing required value for option --required. Showing Help:"; 312 | mute(); 313 | vorpal.execSync('foo --no-required cows').should.equal(fixture); 314 | unmute(); 315 | }); 316 | 317 | it('should throw help on an unknown option', function() { 318 | var fixture = "\n Invalid option: 'unknown'. Showing Help:"; 319 | vorpal.execSync('foo --unknown').should.equal(fixture); 320 | }); 321 | 322 | it('should allow unknown options when allowUnknownOptions is set to true', function() { 323 | var fixture = obj({ options: { unknown: true }}); 324 | obj(vorpal.execSync('bar --unknown')).should.equal(fixture); 325 | }); 326 | 327 | it('should allow the allowUnknownOptions state to be set with a boolean', function() { 328 | var fixture = "\n Invalid option: 'unknown'. Showing Help:"; 329 | vorpal.execSync('baz --unknown').should.equal(fixture); 330 | }); 331 | }); 332 | }); 333 | 334 | 335 | describe('help menu', function () { 336 | var longFixture = 'Twas brillig and the slithy toves, did gyre and gimble in the wabe. All mimsy were the borogoves. And the mome wraths outgrabe. Beware the Jabberwock, my son. The claws that bite, the jaws that catch. Beware the jubjub bird and shun, the frumious bandersnatch. Twas brillig and the slithy toves, did gyre and gimble in the wabe. All mimsy were the borogoves. And the mome wraths outgrabe. Beware the Jabberwock, my son. The claws that bite, the jaws that catch. Beware the jubjub bird and shun, the frumious bandersnatch. Twas brillig and the slithy toves, did gyre and gimble in the wabe. All mimsy were the borogoves. And the mome wraths outgrabe. Beware the Jabberwock, my son. The claws that bite, the jaws that catch. Beware the jubjub bird and shun, the frumious bandersnatch.'; 337 | var shortFixture = 'Twas brillig and the slithy toves.'; 338 | var help; 339 | 340 | before(function () { 341 | help = Vorpal(); 342 | help.command('foo [args...]') 343 | .action(function (args, cb) { 344 | return args; 345 | }); 346 | }); 347 | 348 | it.skip('show help on an invalid command', function () { 349 | stdout = ''; 350 | mute(); 351 | var fixture = '\n Invalid Command. Showing Help:\n\n Commands:\n\n help [command...] Provides help for a given command.\n exit Exits application.\n foo [args...] \n\n\n'; 352 | help.execSync('cows') 353 | unmute(); 354 | stdout.should.equal(fixture); 355 | }); 356 | }); 357 | 358 | describe('descriptors', function () { 359 | var instance; 360 | 361 | beforeEach(function () { 362 | instance = Vorpal(); 363 | }); 364 | 365 | it('sets the version', function () { 366 | instance.version('1.2.3'); 367 | assert.equal(instance._version, '1.2.3'); 368 | }); 369 | 370 | it('sets the title', function () { 371 | instance.title('Vorpal'); 372 | assert.equal(instance._title, 'Vorpal'); 373 | }); 374 | 375 | it('sets the description', function () { 376 | instance.description('A CLI tool.'); 377 | assert.equal(instance._description, 'A CLI tool.'); 378 | }); 379 | 380 | it('sets the banner', function () { 381 | instance.banner('VORPAL'); 382 | assert.equal(instance._banner, 'VORPAL'); 383 | }); 384 | }); 385 | --------------------------------------------------------------------------------