├── test ├── package.json └── test.js ├── .travis.yml ├── .gitignore ├── example ├── cli.js ├── index.js ├── package.json └── lib │ └── commands │ ├── handlers.js │ └── index.js ├── .jshintrc ├── package.json ├── Gruntfile.js ├── README.md └── index.js /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6.4' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | npm-debug.log 3 | .DS_Store 4 | .idea 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /example/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var example = require('./'); 6 | example(); 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true, 14 | "esversion": 6 15 | } 16 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(module) { 4 | var shell = require('simple-shell'); 5 | var commands = require('./lib/commands'); 6 | 7 | module.exports = function() { 8 | shell.initialize(); 9 | commands.forEach(function(cmd) { 10 | shell.registerCommand(cmd); 11 | }); 12 | console.log('Type ' + 'help'.green + ' to get started. To get help for any command just suffix the comand with ' + 'help'.green); 13 | shell.startConsole(); 14 | }; 15 | 16 | })(module); 17 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.1", 4 | "description": "CLI to show simple-shell usage", 5 | "author": { 6 | "name": "JigiJigi" 7 | }, 8 | "license": "MIT", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "cli" 14 | ], 15 | "files": [ 16 | "index.js", 17 | "cli.js", 18 | "lib" 19 | ], 20 | "bin": { 21 | "example": "cli.js" 22 | }, 23 | "dependencies": { 24 | "simple-shell": "0.1.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/lib/commands/handlers.js: -------------------------------------------------------------------------------- 1 | ((module) => { 2 | class Handlers { 3 | createProject(cmd, options, ctx) { 4 | // business logic 5 | console.log('created project \''.gray + options.name.green + '\' successfully'.gray); 6 | } 7 | 8 | createModule(cmd, options, ctx) { 9 | // business logic 10 | console.log('created module \''.gray + options.name.green + '\' successfully'.gray); 11 | } 12 | 13 | isCreateModuleCmdAvailable(ctx) { 14 | // any logic to determine if the command is available 15 | return ctx === 'project'; 16 | } 17 | } 18 | 19 | module.exports = new Handlers(); 20 | })(module); 21 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it */ 2 | 'use strict'; 3 | var assert = require('assert'); 4 | var SimpleShell = require('../'); 5 | 6 | describe('SimpleShell node module', function () { 7 | it('should have all the methods!', function () { 8 | SimpleShell.initialize({}); 9 | assert.notEqual(SimpleShell, undefined); 10 | assert.notEqual(SimpleShell.initialize, undefined); 11 | assert.notEqual(SimpleShell.startConsole, undefined); 12 | assert.notEqual(SimpleShell.registerCommand, undefined); 13 | assert.notEqual(SimpleShell.log, undefined); 14 | assert.notEqual(SimpleShell.info, undefined); 15 | assert.notEqual(SimpleShell.warn, undefined); 16 | assert.notEqual(SimpleShell.error, undefined); 17 | assert.notEqual(SimpleShell.success, undefined); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /example/lib/commands/index.js: -------------------------------------------------------------------------------- 1 | var handlers = require('./handlers'); 2 | 3 | module.exports = [ 4 | { 5 | name: 'create project', 6 | help: 'create a new project', 7 | context: 'project', 8 | handler: handlers.createProject, 9 | options: { 10 | name: { 11 | help: 'name of the project', 12 | required: true 13 | }, 14 | version: { 15 | help: 'project version', 16 | allowedValues: ['2', '3'], 17 | defaultValue: '3' 18 | } 19 | } 20 | }, 21 | { 22 | name: 'create module', 23 | help: 'create a module under the selected project', 24 | context: 'module', 25 | handler: handlers.createModule, 26 | options: { 27 | name: { 28 | help: 'name of the module', 29 | required: true 30 | } 31 | }, 32 | isAvailable: handlers.isCreateModuleCmdAvailable 33 | } 34 | ]; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-shell", 3 | "version": "0.1.3", 4 | "description": "A nodejs based custom command line interface (CLI) enabler", 5 | "author": { 6 | "name": "Bhagavan" 7 | }, 8 | "repository": "https://github.com/bhagn/simple-shell", 9 | "bugs": "https://github.com/bhagn/simple-shell/issues", 10 | "license": "MIT", 11 | "files": [ 12 | "index.js" 13 | ], 14 | "keywords": [ 15 | "custom", 16 | "cli", 17 | "command", 18 | "line", 19 | "interface", 20 | "shell" 21 | ], 22 | "dependencies": { 23 | "colors": "^1.0.3", 24 | "figlet": "^1.1.0", 25 | "inquirer": "^0.8.0", 26 | "lodash": "^3.6.0" 27 | }, 28 | "devDependencies": { 29 | "grunt": "1.0.1", 30 | "grunt-cli": "1.2.0", 31 | "grunt-contrib-jshint": "1.1.0", 32 | "grunt-contrib-nodeunit": "1.0.0", 33 | "grunt-contrib-watch": "1.0.0", 34 | "load-grunt-tasks": "3.5.2", 35 | "time-grunt": "1.4.0", 36 | "grunt-mocha-cli": "^1.11.0", 37 | "jshint-stylish": "^1.0.0" 38 | }, 39 | "scripts": { 40 | "test": "grunt" 41 | }, 42 | "engines": { 43 | "node": ">=0.10.28" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function (grunt) { 3 | // Show elapsed time at the end 4 | require('time-grunt')(grunt); 5 | // Load all grunt tasks 6 | require('load-grunt-tasks')(grunt); 7 | 8 | grunt.initConfig({ 9 | jshint: { 10 | options: { 11 | jshintrc: '.jshintrc', 12 | reporter: require('jshint-stylish') 13 | }, 14 | gruntfile: { 15 | src: ['Gruntfile.js'] 16 | }, 17 | js: { 18 | src: ['*.js'] 19 | }, 20 | test: { 21 | src: ['test/**/*.js'] 22 | } 23 | }, 24 | mochacli: { 25 | options: { 26 | reporter: 'nyan', 27 | bail: true 28 | }, 29 | all: ['test/*.js'] 30 | }, 31 | watch: { 32 | gruntfile: { 33 | files: '<%= jshint.gruntfile.src %>', 34 | tasks: ['jshint:gruntfile'] 35 | }, 36 | js: { 37 | files: '<%= jshint.js.src %>', 38 | tasks: ['jshint:js', 'mochacli'] 39 | }, 40 | test: { 41 | files: '<%= jshint.test.src %>', 42 | tasks: ['jshint:test', 'mochacli'] 43 | } 44 | } 45 | }); 46 | 47 | grunt.registerTask('default', ['jshint', 'mochacli']); 48 | }; 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-url]][daviddm-image] 2 | 3 | Inspired by the Spring Roo project, this library provides a simple way to write custom application specific shells with the following features: 4 | * Auto-complete commands 5 | * Command options 6 | * Sub commands 7 | * Extends from [`inquirer`](https://www.npmjs.com/package/inquirer) module - thus making all the features of `inquirer` available to the shell instance for taking user input. 8 | * Default `help` command which automatically prints help for all commands. Any command suffixed with help will display the help for that command. 9 | 10 | ## Install 11 | 12 | ```sh 13 | $ npm install --save simple-shell 14 | ``` 15 | 16 | 17 | ## Usage 18 | 19 | ```js 20 | var shell = require('simple-shell'); 21 | shell.initialize(shellOptions); 22 | 23 | // Register commands with the custom shell 24 | shell.registerCommand(cmdOptions); 25 | 26 | // Start the console and show prompt 27 | shell.startConsole(); 28 | 29 | #> 30 | ``` 31 | 32 | ### `shellOptions` 33 | ``` 34 | { 35 | name: , 36 | authorName: , 37 | version: , 38 | exitMessage: , 39 | prompt: 40 | } 41 | ``` 42 | 43 | * `name`: Name of the console application. default: `package.json:name`. 44 | * `authorName`: Name of the author/Owner of the application. default: `package.json:author.name`. 45 | * `version`: Versionof the application. default: `package.json:version`. 46 | * `exitMessage`: Message to be displayed when user quits the console app. default: `Good Bye!`. 47 | * `prompt`: Prompt to be displayed. Ex: `#>`. default: `package.json:name`. 48 | 49 | All the options are optional and will be fetched from `package.json` if not provided. 50 | 51 | ### `cmdOptions` 52 | ``` 53 | { 54 | name: , 55 | help: , 56 | context: , 57 | isAvailable: , 58 | options: { 59 | optionName: { 60 | help: , 61 | required: , 62 | defaultValue: , 63 | allowedValues: 64 | } 65 | }, 66 | handler: 67 | } 68 | ``` 69 | 70 | * `name`: Name of the command. 71 | * `help`: Help string for the command that will be displayed when the user runs `help`. 72 | * `context`: A string representing a context under which the command is running. If set, the application's context will be set to this string on successful execution of the command. 73 | * `isAvailable(context)`: The function that will be called to determine if a command is available for execution. default: always returns `true`. 74 | * `handler(cmd, options, context)`: The handler function that will be called to execute a command. If the command is not successful, it is expected to throw an `Error`. On successful execution of the command, the context will be set to `command.context`. 75 | * `optionName`: Name of the option. This will be presented as `--optionName` to the user. 76 | * `optionName.help`: Help string for the option. 77 | * `optionName.required`: Indicates if the option is a mandatory option or not. default: `false`. 78 | * `optionName.defaultValue`: The default value for an option if user doesn't provide one. 79 | * `optionName.allowedValues`: A list of valid values for the option. 80 | 81 | #### Pending documentation.. 82 | 83 | 84 | [npm-url]: https://npmjs.org/package/simple-shell 85 | [npm-image]: https://badge.fury.io/js/simple-shell.svg 86 | [travis-url]: https://travis-ci.org/bhagn/simple-shell 87 | [travis-image]: https://travis-ci.org/bhagn/simple-shell.svg?branch=master 88 | [daviddm-url]: https://david-dm.org/bhagn/simple-shell.svg?theme=shields.io 89 | [daviddm-image]: https://david-dm.org/bhagn/simple-shell 90 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(module) { 4 | var figlet = require('figlet'), 5 | readline = require('readline'), 6 | colors = require('colors'), 7 | SimpleShell = require('inquirer'), 8 | _ = require('lodash'); 9 | 10 | var pkg = null; 11 | try { 12 | pkg = require.main.require('./package.json'); 13 | } catch(e) { 14 | pkg = require('./package.json'); 15 | } 16 | 17 | var commands = {}, 18 | options = {}, 19 | applicationContext = null, 20 | getCmd = /^[A-Z|a-z][A-Z|a-z|0-9|\s]*/, 21 | extractOptions = /(-{2}[a-zA-Z0-9\-]+\s*(([a-zA-Z0-9.\/\\?=\*&+_%$\[\]{}#!:@]|-{1}[a-zA-Z0-9.\/\\?=\*&+_%$\[\]{}#!:@])*\s*)*)/g; 22 | 23 | 24 | function log() { 25 | console.log(arguments); 26 | } 27 | 28 | function info() { 29 | console.info(colors.blue(_.toArray(arguments).join(' '))); 30 | } 31 | 32 | function warn() { 33 | console.warning(_.toArray(arguments).join(' ').orange); 34 | } 35 | 36 | function error() { 37 | console.error(_.toArray(arguments).join(' ').red); 38 | } 39 | 40 | function success() { 41 | console.log(_.toArray(arguments).join(' ').green); 42 | } 43 | 44 | function completer(line) { 45 | 46 | var cmd = line.trim().match(getCmd); 47 | cmd = (cmd ? cmd[0] : '').trim(); 48 | 49 | var hits = []; 50 | 51 | _.forEach(commands, function(cmd, cmdName) { 52 | if (cmd.isAvailable(applicationContext) && 53 | cmdName.indexOf(line.trim()) === 0) { 54 | hits.push(cmdName); 55 | } 56 | }); 57 | 58 | var parts = cmd.split(' '); 59 | var ends = _.last(parts); 60 | 61 | if (parts.length > 1 && !commands[cmd]) { 62 | ends = _.last(parts); 63 | hits = _.map(hits, function(c) { 64 | return _.last(c.split(' ')); 65 | }); 66 | } 67 | 68 | if (commands[cmd]) { 69 | var lastOption = _.last(line.split(' ')).trim(); 70 | var optionName = ''; 71 | 72 | if (lastOption) { 73 | optionName = lastOption.split(/\s+/)[0].split('--')[1]; 74 | } 75 | 76 | if (lastOption && !commands[cmd].options[optionName]) { 77 | _.forEach(commands[cmd].options, function(config, op) { 78 | var option = '--' + op; 79 | if (_.startsWith(option, lastOption)) { 80 | hits.push(option); 81 | } 82 | }); 83 | } else { 84 | _.forEach(commands[cmd].options, function(config, op) { 85 | var option = '--' + op; 86 | 87 | if (line.indexOf(option) === -1) { 88 | hits.push(option + ' '); 89 | } 90 | }); 91 | } 92 | ends = _.last(line.trim().split(' ')); 93 | } 94 | 95 | return [hits, ends || line]; 96 | } 97 | 98 | function printHelp(cmdName) { 99 | var cmd = commands[cmdName]; 100 | console.log(cmd.name.green); 101 | console.log(' ', cmd.help.gray); 102 | for (var op in cmd.options) { 103 | //var required = cmd.options[op].required ? '['; 104 | console.log(_.padLeft(('\t--' + op).green, 10), 105 | ('[' + (cmd.options[op].required ? '*'.red: '?'.gray) + ']'), 106 | ':', cmd.options[op].help.gray); 107 | 108 | if(!_.isUndefined(cmd.options[op].defaultValue)) { 109 | console.log(_.padLeft('\t Default value: ' + `${cmd.options[op].defaultValue}`.gray)); 110 | } 111 | 112 | if (cmd.options[op].allowedValues && cmd.options[op].allowedValues.length > 0) { 113 | console.log(_.padLeft('\t Allowed values: ' + cmd.options[op].allowedValues.join(', ').gray)); 114 | } 115 | } 116 | } 117 | 118 | function helpHandler(line) { 119 | var cmd = line.trim() ? line.match(getCmd)[0].trim() : ''; 120 | 121 | if (commands[cmd]) { 122 | printHelp(cmd); 123 | return; 124 | } 125 | 126 | var completions = completer(line); 127 | 128 | for (var i=0, len=completions[0].length; i < len; i++) { 129 | if (completions[0][i].indexOf('--') !== -1) { 130 | console.log('continue'); 131 | continue; 132 | } 133 | 134 | var cmdName = line.replace(completions[1], completions[0][i]).trim(); 135 | printHelp(cmdName); 136 | } 137 | } 138 | 139 | var rl = readline.createInterface({ 140 | input: process.stdin, 141 | output: process.stdout, 142 | terminal: true, 143 | completer: completer 144 | }); 145 | 146 | function getDefaultOptions(cmd) { 147 | var cmdOptions = {}; 148 | 149 | if (_.isUndefined(commands[cmd])) { 150 | return cmdOptions; 151 | } 152 | 153 | _.forIn(commands[cmd].options, function(config, name) { 154 | if (!_.isUndefined(config.defaultValue)) { 155 | cmdOptions[name] = config.defaultValue; 156 | } 157 | }); 158 | 159 | return cmdOptions; 160 | } 161 | 162 | /** 163 | * Start the console and display the prompt. 164 | */ 165 | function startConsole() { 166 | var prompt = (options.prompt || options.name || pkg.name) + '> '; 167 | 168 | rl.setPrompt(prompt.yellow, prompt.length); 169 | rl.prompt(); 170 | 171 | rl.on('line', function(line) { 172 | if (!line || !line.trim() || line && _.endsWith(line.trim(), '^C')) { 173 | rl.prompt(); 174 | return; 175 | } 176 | 177 | var cmd = line.trim().match(getCmd)[0].trim(); 178 | var askingHelp = _.endsWith(line.trim(), ' help') || 179 | line.search(/(\s)*help$/) !== -1; 180 | 181 | if (askingHelp) { 182 | helpHandler(line.replace(/(\s)*help$/, '')); 183 | rl.prompt(); 184 | return; 185 | } 186 | 187 | if (!commands[cmd] || !commands[cmd].isAvailable(applicationContext)) { 188 | console.error('Unrecognized command: '.red, line); 189 | rl.prompt(); 190 | return; 191 | } 192 | 193 | var cmdOptions = getDefaultOptions(cmd); 194 | 195 | for (var i=0, len=commands[cmd]._required.length; i '; 244 | rl.setPrompt(_prompt.yellow, _prompt.length); 245 | } 246 | } else { 247 | console.error(result.stack); 248 | } 249 | } 250 | 251 | rl.prompt(); 252 | }); 253 | 254 | rl.on('SIGINT', function() { 255 | // readline.cursorTo(process.stdout, 0); 256 | // readline.clearLine(process.stdout, 0); 257 | 258 | // // process.stdout.clearLine(); 259 | rl.write('^C\n'); 260 | 261 | }); 262 | 263 | rl.on('close', function() { 264 | if (options.onBeforeExit) { 265 | options.onBeforeExit(); 266 | } 267 | console.log((options.exitMessage || '\nGood bye!').green); 268 | process.exit(); 269 | }); 270 | 271 | process.on('uncaughtException', function(e) { 272 | console.log(e.stack.red); 273 | rl.prompt(); 274 | }); 275 | } 276 | 277 | /** 278 | * Registers a command with the shell. 279 | * 280 | * @param {Object} command The command configuration. 281 | * @param {String} command.name Name of the command. 282 | * @param {String} command.help Help string for the command. 283 | * @param {String} command.context Set the context to this string on 284 | * successful execution of the command. 285 | * @param {Function} command.isAvailable Function that will be called to 286 | * determine if the command is currently 287 | * available for execution. This method 288 | * will be passed the `context`. 289 | * @param {Object} command.options options that the command can take. 290 | * These options will be presented as 291 | * `--optionName` 292 | * 293 | * @param {String} command.options.optionName Name of the option. 294 | * @param {String} command.options.optionName.help Help string for option. 295 | * @param {Boolean} command.options.optionName.required 296 | * @param {String} command.options.optionName.defaultValue Default value 297 | * @param {Array} command.options.optionsName.allowedValues List of allowed 298 | * values for the 299 | * option. 300 | */ 301 | function registerCommand(command) { 302 | /** 303 | * Command = { 304 | * name: , 305 | * help: , 306 | * context: , 307 | * isAvailable: , 308 | * options: { 309 | * optionName: { 310 | * help: , 311 | * required: , 312 | * defaultValue: , 313 | * allowedValues: 314 | * } 315 | * }, 316 | * handler: 317 | * } 318 | */ 319 | 320 | var funcName = 'Command Registrar'; 321 | 322 | var _default = { 323 | help: '', 324 | context: false, 325 | isAvailable: _.negate(_.noop), 326 | options: {} 327 | }; 328 | 329 | command = _.defaults(command, _default); 330 | 331 | command._required = []; 332 | _.forEach(command.options, function(op, name) { 333 | if (op.required) { 334 | command._required.push(name); 335 | } 336 | }); 337 | 338 | if(!command.hasOwnProperty('name')) { 339 | console.error(funcName.yellow, 340 | 'Failed to register command: Invalid Name'.red); 341 | return; 342 | } 343 | 344 | if(!command.hasOwnProperty('handler') || !_.isFunction(command.handler)) { 345 | console.error(funcName.yellow, 346 | 'Failed to register command: Invalid Handler'.red); 347 | return; 348 | } 349 | 350 | commands[command.name] = command; 351 | } 352 | 353 | /** 354 | * Initialize the shell with app-specific options 355 | * 356 | * @param {Object} config Application configuration options. 357 | * @param {String} config.name Name of the application. 358 | * @param {String} config.author Name of the author of the application. 359 | * @param {String} config.version Version of the application. 360 | */ 361 | function initialize(config) { 362 | 363 | options = config || {}; 364 | 365 | var banner = figlet.textSync((options.name || pkg.name).toUpperCase(), { 366 | font: 'Small', 367 | horizontalLayout: 'default', 368 | verticalLayout: 'default' 369 | }); 370 | 371 | var meta = ['Copyright(c) ', 372 | (new Date().getFullYear()), ', ', 373 | (options.author || pkg.author.name), 374 | ' | Version: ', 375 | (options.version || pkg.version), '\n'].join(''); 376 | 377 | console.log(banner.red); 378 | console.log(meta.gray); 379 | 380 | SimpleShell.registerCommand({ 381 | name: 'help', 382 | help: 'Show this help menu', 383 | handler: helpHandler 384 | }); 385 | 386 | SimpleShell.registerCommand({ 387 | name: 'exit', 388 | help: 'Exit the console', 389 | handler: function() { 390 | rl.close(); 391 | } 392 | }); 393 | } 394 | 395 | SimpleShell.initialize = initialize; 396 | SimpleShell.registerCommand = registerCommand; 397 | SimpleShell.startConsole = startConsole; 398 | SimpleShell.log = log; 399 | SimpleShell.info = info; 400 | SimpleShell.warn = warn; 401 | SimpleShell.error = error; 402 | SimpleShell.success = success; 403 | 404 | module.exports = SimpleShell; 405 | 406 | })(module); 407 | --------------------------------------------------------------------------------