├── .gitignore ├── .npmignore ├── License.md ├── Makefile ├── Readme.md ├── bin ├── commands │ ├── generate │ │ ├── command.js │ │ └── middleware.js │ └── new.js ├── main.js └── ronin ├── build ├── command.js ├── program.js └── util.js ├── circle.yml ├── index.js ├── lib ├── command.js └── program.js ├── package.json └── test ├── fixtures └── hello-world │ ├── commands │ ├── apps.js │ ├── apps │ │ ├── add.js │ │ ├── destroy.js │ │ └── edit.js │ └── generate │ │ └── project.js │ └── middleware │ └── auth.js └── ronin.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | lib 3 | Makefile 4 | License.md 5 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Vadim Demedes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = $(wildcard lib/*.js) 2 | DEST = $(SRC:lib/%.js=build/%.js) 3 | 4 | build: $(DEST) 5 | build/%.js: lib/%.js 6 | mkdir -p $(@D) 7 | ./node_modules/.bin/babel -L all $< -o $@ 8 | 9 | clean: 10 | rm -rf build 11 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | **Ronin is temporarily unmaintained, check back later for updates!** 2 | 3 | # Ronin 4 | 5 | Toolkit for building shining CLI programs in Node.js. 6 | 7 | [![Circle CI](https://circleci.com/gh/vdemedes/ronin.svg?style=svg)](https://circleci.com/gh/vdemedes/ronin) 8 | 9 | ## Features 10 | 11 | - *Forced & clean* organization of program code 12 | - Command name generation based on folder structure 13 | - CLI tool to quickly create program skeleton and commands 14 | - Auto-generated usage and help 15 | - Small codebase (269 sloc) 16 | - **Program auto-updates itself when new version is available** 17 | 18 | ## Installation 19 | 20 | ``` 21 | npm install ronin --global 22 | ``` 23 | 24 | ## Getting Started 25 | 26 | ### Creating basic structure 27 | 28 | Execute the following command to generate basic skeleton for your program: 29 | 30 | ``` 31 | ronin new hello-world 32 | ``` 33 | 34 | Ronin will create a hello-world directory (if it does not exists or empty) and put everything that's needed to start developing your CLI tool immediately: 35 | 36 | ![Output](http://cl.ly/image/3p1R160V2Z2W/embed) 37 | 38 | ### Initialization 39 | 40 | Here's how to initialize CLI program using Ronin: 41 | 42 | ```javascript 43 | var ronin = require('ronin'); 44 | 45 | var program = ronin(__dirname); // root path, where commands folder is 46 | 47 | program.run(); 48 | ``` 49 | 50 | ### Creating commands 51 | 52 | Next, to setup some commends, simply create folders and files. 53 | The structure you create, will be reflected in your program. 54 | For example, if you create such folders and files: 55 | 56 | ``` 57 | commands/ 58 | -- apps.js 59 | -- apps/ 60 | -- add.js 61 | -- remove.js 62 | -- keys/ 63 | -- dump.js 64 | ``` 65 | 66 | In result, Ronin, will generate these commands for you automatically: 67 | 68 | ``` 69 | $ hello-world apps 70 | $ hello-world apps add 71 | $ hello-world apps remove 72 | $ hello-world keys dump 73 | ``` 74 | 75 | Each folder is treated like a namespace and each file like a command, where file name is command name. 76 | 77 | To actually create handlers for those commands, in each file, Command should be defined: 78 | 79 | ```javascript 80 | var Command = require('ronin').Command; 81 | 82 | var AppsAddCommand = module.exports = Command.extend({ 83 | desc: 'This command adds application', 84 | 85 | run: function (name) { 86 | // create an app with name given in arguments 87 | } 88 | }); 89 | ``` 90 | 91 | To run this command, execute: 92 | 93 | ``` 94 | $ hello-world apps add great-app 95 | ``` 96 | 97 | Whatever arguments passed to command after command name, will be passed to .run() method in the same order they were written. 98 | 99 | #### Specifying options 100 | 101 | You can specify options and their properties using *options* object. 102 | 103 | ```javascript 104 | var AppsDestroyCommand = module.exports = Command.extend({ 105 | desc: 'This command removes application', 106 | 107 | options: { 108 | name: 'string', 109 | force: { 110 | type: 'boolean', 111 | alias: 'f' 112 | } 113 | }, 114 | 115 | run: function (name, force) { 116 | if (!force) { 117 | throw new Error('--force option is required to remove application'); 118 | } 119 | 120 | // remove app 121 | } 122 | }); 123 | ``` 124 | 125 | **Note**: Options will be passed to .run() method in the same order they were defined. 126 | 127 | #### Customizing help 128 | 129 | By default, Ronin generates help for each command and for whole program automatically. 130 | If you wish to customize the output, override .help() method in your command (program help can not be customized at the moment): 131 | 132 | ```javascript 133 | var HelloCommand = Command.extend({ 134 | help: function () { 135 | return 'Usage: ' + this.programName + ' ' + this.name + ' [OPTIONS]'; 136 | }, 137 | 138 | desc: 'Hello world' 139 | }); 140 | ``` 141 | 142 | #### Customizing command delimiter 143 | 144 | By default, Ronin separates sub-commands with a space. 145 | If you want to change that delimiter, just specify this option when initializing Ronin: 146 | 147 | ```javascript 148 | var program = ronin(); 149 | 150 | program.set({ 151 | path: __dirname, 152 | delimiter: ':' 153 | }); 154 | 155 | program.run(); 156 | ``` 157 | 158 | After that, `apps create` command will become `apps:create`. 159 | 160 | 161 | ### Middleware 162 | 163 | There are often requirements to perform the same operations/checks for many commands. 164 | For example, user authentication. 165 | In order to avoid code repetition, Ronin implements middleware concept. 166 | Middleware is just a function, that accepts the same arguments as .run() function + callback function. 167 | Middleware functions can be asynchronous, it makes no difference for Ronin. 168 | 169 | Let's take a look at this example: 170 | 171 | ```javascript 172 | var UsersAddCommand = Command.extend({ 173 | use: ['auth', 'beforeRun'], 174 | 175 | run: function (name) { 176 | // actual users add command 177 | }, 178 | 179 | beforeRun: function (name, next) { 180 | // will execute before .run() 181 | 182 | // MUST call next() when done 183 | next(); 184 | } 185 | }); 186 | ``` 187 | 188 | In this example, we've got 2 middleware functions: auth and beforeRun. 189 | Ronin allows you to write middleware functions inside commands or inside `root/middleware` directory. 190 | So in this example, Ronin will detect that `beforeRun` function is defined inside a command and `auth` function will be `require`d from `root/middleware/auth.js` file. 191 | 192 | **Note**: To interrupt the whole program and stop execution, just throw an error. 193 | 194 | 195 | ### Auto-updating 196 | 197 | To make your program auto-update itself, only 1 line of code needed. 198 | Go to your program's index file and replace `program.run()` with this: 199 | 200 | ```javascript 201 | program.autoupdate(function () { 202 | program.run(); 203 | }); 204 | ``` 205 | 206 | From now on, your program will check for updates once a day and if new update is available, it will automatically install it. 207 | **How cool is this?** 208 | 209 | ## Tests 210 | 211 | ``` 212 | npm test 213 | ``` 214 | 215 | ## License 216 | 217 | Ronin is released under the MIT License. 218 | -------------------------------------------------------------------------------- /bin/commands/generate/command.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var Command = require('../../../').Command; 6 | 7 | var spawn = require('child_process').spawn; 8 | 9 | 10 | /** 11 | * New command 12 | */ 13 | 14 | var NewCommand = Command.extend({ 15 | desc: 'Create new command', 16 | 17 | run: function (name) { 18 | spawn('yo', ['ronin:command', name], { 19 | cwd: process.cwd(), 20 | stdio: 'inherit' 21 | }); 22 | } 23 | }); 24 | 25 | 26 | /** 27 | * Expose command 28 | */ 29 | 30 | module.exports = NewCommand; -------------------------------------------------------------------------------- /bin/commands/generate/middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var Command = require('../../../').Command; 6 | 7 | var spawn = require('child_process').spawn; 8 | 9 | 10 | /** 11 | * New middleware 12 | */ 13 | 14 | var NewMiddleware = Command.extend({ 15 | desc: 'Create new middleware', 16 | 17 | run: function (name) { 18 | spawn('yo', ['ronin:middleware', name], { 19 | cwd: process.cwd(), 20 | stdio: 'inherit' 21 | }); 22 | } 23 | }); 24 | 25 | 26 | /** 27 | * Expose command 28 | */ 29 | 30 | module.exports = NewMiddleware; -------------------------------------------------------------------------------- /bin/commands/new.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var Command = require('../../').Command; 6 | 7 | var spawn = require('child_process').spawn; 8 | 9 | 10 | /** 11 | * New application 12 | */ 13 | 14 | var NewApplication = Command.extend({ 15 | desc: 'Create new application', 16 | 17 | run: function (name) { 18 | var execFile = process.platform === "win32" ? "yo.cmd" : "yo"; 19 | 20 | spawn(execFile, ['ronin', name], { 21 | cwd: process.cwd(), 22 | stdio: 'inherit' 23 | }); 24 | } 25 | }); 26 | 27 | 28 | /** 29 | * Expose command 30 | */ 31 | 32 | module.exports = NewApplication; -------------------------------------------------------------------------------- /bin/main.js: -------------------------------------------------------------------------------- 1 | var ronin = require('../'); 2 | 3 | var program = ronin(); 4 | 5 | program.set({ 6 | path: __dirname, 7 | desc: 'Ronin CLI utility to create base for programs' 8 | }); 9 | 10 | program.run(); 11 | -------------------------------------------------------------------------------- /bin/ronin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./main'); -------------------------------------------------------------------------------- /build/command.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; 4 | 5 | /** 6 | * Dependencies 7 | */ 8 | 9 | var Class = require("class-extend"); 10 | 11 | var minimist = require("minimist"); 12 | var table = require("text-table"); 13 | var async = require("async"); 14 | var os = require("os"); 15 | 16 | // path shortcuts 17 | var join = require("path").join; 18 | 19 | /** 20 | * Command 21 | */ 22 | 23 | var Command = (function () { 24 | function Command() { 25 | _classCallCheck(this, Command); 26 | 27 | if (!this.options) this.options = {}; 28 | if (!this.use) this.use = []; 29 | if (!this.desc) this.desc = ""; 30 | 31 | this.configure(); 32 | } 33 | 34 | Command.prototype.configure = function configure() {}; 35 | 36 | Command.prototype.run = function run() {}; 37 | 38 | 39 | 40 | 41 | /** 42 | * Display help for a command 43 | * 44 | * @param {String} type 45 | * @api public 46 | */ 47 | Command.prototype.help = function help(type) { 48 | var _this = this; 49 | if (type === "compact") { 50 | return this.desc; 51 | }var help = ""; 52 | 53 | help += "Usage: " + this.usage() + "" + (os.EOL + os.EOL); 54 | 55 | if (this.desc) { 56 | help += " " + this.desc + "" + (os.EOL + os.EOL); 57 | } 58 | 59 | // generate a list of sub-commands 60 | var names = Object.keys(this.program.commands).filter(function (name) { 61 | // add command to the list if 62 | // - it does not equal to itself 63 | // - it contains the name of a current command 64 | return name !== _this.name && name.split(_this.program.delimiter).indexOf(_this.name) > -1; 65 | }); 66 | 67 | // no sub-commands 68 | if (!names.length) { 69 | return help; 70 | } // sort commands alphabetically 71 | names.sort(function (a, b) { 72 | return a.localeCompare(b); 73 | }); 74 | 75 | // get description for each command 76 | var commands = names.map(function (name) { 77 | var command = _this.program._get(name); 78 | var desc = new command().help("compact"); 79 | 80 | return [name, desc]; 81 | }); 82 | 83 | // output the list 84 | help += "Additional commands:" + (os.EOL + os.EOL); 85 | help += table(commands); 86 | 87 | return help; 88 | }; 89 | 90 | 91 | 92 | 93 | /** 94 | * Return usage information for this command 95 | * 96 | * @api public 97 | */ 98 | Command.prototype.usage = function usage() { 99 | return "" + this.program.name + " " + this.name + " [OPTIONS]"; 100 | }; 101 | 102 | 103 | 104 | 105 | /** 106 | * Build options for minimist 107 | * 108 | * @api private 109 | */ 110 | Command.prototype._buildOptions = function _buildOptions(options) { 111 | // options for minimist 112 | var parseOptions = { 113 | string: [], 114 | boolean: [], 115 | alias: {}, 116 | "default": {} 117 | }; 118 | 119 | // convert command options 120 | // into options for minimist 121 | Object.keys(options).forEach(function (name) { 122 | var option = options[name]; 123 | 124 | // option can be a string 125 | // assume it's a type 126 | if (option === "string") { 127 | option = { 128 | type: option 129 | }; 130 | } 131 | 132 | if (option.type === "string") { 133 | parseOptions.string.push(name); 134 | } 135 | 136 | if (option.type === "boolean") { 137 | parseOptions.boolean.push(name); 138 | } 139 | 140 | // "aliases" or "alias" property 141 | // can be string or array 142 | // need to always convert to array 143 | var aliases = option.aliases || option.alias || []; 144 | if (typeof aliases === "string") aliases = [aliases]; 145 | 146 | aliases.forEach(function (alias) { 147 | return parseOptions.alias[alias] = name; 148 | }); 149 | 150 | if (option["default"]) { 151 | parseOptions["default"][name] = option["default"]; 152 | } 153 | }); 154 | 155 | return parseOptions; 156 | }; 157 | 158 | 159 | 160 | 161 | /** 162 | * Parse arguments 163 | * 164 | * @api private 165 | */ 166 | Command.prototype._buildArgs = function _buildArgs() { 167 | var program = this.program; 168 | var command = this; 169 | 170 | // options for parser 171 | var parseOptions = undefined; 172 | 173 | // global options 174 | var options = program.options || {}; 175 | 176 | parseOptions = this._buildOptions(options); 177 | this.global = minimist(process.argv, parseOptions); 178 | 179 | // command options 180 | options = command.options; 181 | 182 | parseOptions = this._buildOptions(options); 183 | var args = this.options = minimist(process.argv, parseOptions); 184 | 185 | // arguments for .run() method 186 | // need to build them in the same order 187 | // they were defined 188 | var handlerArgs = []; 189 | 190 | Object.keys(options).forEach(function (name) { 191 | var option = options[name]; 192 | var value = args[name]; 193 | 194 | if (option.required && !value) { 195 | throw new Error("No value provided for required argument '" + name + "'"); 196 | } 197 | 198 | handlerArgs.push(args[name]); 199 | }); 200 | 201 | // append arguments (don't confuse with options) 202 | handlerArgs.push.apply(handlerArgs, args._); 203 | 204 | return handlerArgs; 205 | }; 206 | 207 | 208 | 209 | 210 | /** 211 | * Run command 212 | * 213 | * @api public 214 | */ 215 | Command.prototype.execute = function execute() { 216 | var program = this.program; 217 | var command = this; 218 | 219 | var args = this._buildArgs(); 220 | 221 | var middleware = this.use || []; 222 | 223 | // middleware must always be an array 224 | if (!(middleware instanceof Array)) middleware = [middleware]; 225 | 226 | // last function in a middleware 227 | // is the actual command 228 | middleware.push(this.run); 229 | 230 | var stack = middleware.map(function (fn, index) { 231 | // if middleware item is a string 232 | // it is a function name 233 | if (typeof fn === "string") { 234 | // check if function with this name 235 | // exists in Command.prototype 236 | // else, search in middleware/ dir 237 | if (typeof command[fn] === "function") { 238 | fn = command[fn]; 239 | } else { 240 | var path = join(program.path, "middleware", fn); 241 | fn = require(path); 242 | } 243 | } 244 | 245 | if (index === middleware.length - 1) { 246 | var bindArgs = Array.prototype.slice.call(args); 247 | bindArgs.unshift(command); 248 | 249 | fn = fn.bind.apply(fn, bindArgs); 250 | fn.displayName = "run"; 251 | } else { 252 | fn = fn.bind(command); 253 | } 254 | 255 | return fn; 256 | }); 257 | 258 | async.forEachSeries(stack, function (fn, next) { 259 | if ("run" === fn.displayName) { 260 | fn(); 261 | next(); 262 | } else { 263 | fn(next); 264 | } 265 | }); 266 | }; 267 | 268 | Command.extend = function extend() { 269 | return Class.extend.apply(this, arguments); 270 | }; 271 | 272 | return Command; 273 | })(); 274 | 275 | 276 | 277 | 278 | /** 279 | * Expose `Command` 280 | */ 281 | 282 | module.exports = Command; 283 | -------------------------------------------------------------------------------- /build/program.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _toConsumableArray = function (arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }; 4 | 5 | var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; 6 | 7 | /** 8 | * Dependencies 9 | */ 10 | 11 | var minimist = require("minimist"); 12 | var flatten = require("lodash.flatten"); 13 | var semver = require("semver"); 14 | var spawn = require("child_process").spawn; 15 | var chalk = require("chalk"); 16 | var table = require("text-table"); 17 | var glob = require("glob").sync; 18 | var exec = require("child_process").exec; 19 | var fs = require("fs"); 20 | var os = require("os"); 21 | 22 | // path shortcuts 23 | var separator = require("path").sep; 24 | var normalize = require("path").normalize; 25 | var basename = require("path").basename; 26 | var join = require("path").join; 27 | 28 | // utilities 29 | require("./util"); 30 | 31 | /** 32 | * Program 33 | */ 34 | 35 | var Program = (function () { 36 | function Program(options) { 37 | _classCallCheck(this, Program); 38 | 39 | this.name = basename(process.argv[1]); 40 | this.path = normalize(process.cwd()); 41 | this.delimiter = " "; 42 | this.stdout = process.stdout; 43 | this.commands = {}; 44 | 45 | // if options is a string 46 | // assume it's a path 47 | if ("string" === typeof options) this.path = options; 48 | 49 | // if it's an object 50 | // extend this with it 51 | if ("object" === typeof options) Object.assign(this, options); 52 | } 53 | 54 | /** 55 | * Set option 56 | * 57 | * @param {String,Object} key 58 | * @param {Mixed} value - value 59 | * @returns {Mixed} 60 | * @api public 61 | */ 62 | Program.prototype.set = function set(key, value) { 63 | if ("object" === typeof key) { 64 | return Object.assign(this, key); 65 | } 66 | 67 | return this[key] = value; 68 | }; 69 | 70 | 71 | 72 | 73 | /** 74 | * Get option 75 | * 76 | * @param {String} key 77 | * @returns {Mixed} 78 | * @api public 79 | */ 80 | Program.prototype.get = function get(key) { 81 | return this[key]; 82 | }; 83 | 84 | 85 | 86 | 87 | /** 88 | * Setup commands 89 | * 90 | * @api private 91 | */ 92 | Program.prototype.setupCommands = function setupCommands() { 93 | var _this = this; 94 | if (!fs.existsSync(this.path)) { 95 | return; 96 | }var files = glob(join(this.path, "commands", "**", "*")); 97 | 98 | files.forEach(function (path) { 99 | // next 2 lines are for windows 100 | // compatibility, because glob 101 | // returns "/" instead of "\" 102 | // and lower-cased drive letter 103 | path = path.replace(/\//g, separator).replace(/^./, function ($1) { 104 | return $1.toUpperCase(); 105 | }).replace(join(_this.path, "commands"), "").replace(separator, ""); 106 | 107 | // include only .js files 108 | if (/\.js$/.test(path)) { 109 | var _name = path.split(separator).join(_this.delimiter).replace(".js", ""); 110 | 111 | _this.commands[_name] = join(_this.path, "commands", path); 112 | } 113 | }); 114 | }; 115 | 116 | 117 | 118 | 119 | /** 120 | * Run a program 121 | * 122 | * @api public 123 | */ 124 | Program.prototype.run = function run() { 125 | var _this = this; 126 | // catch exceptions 127 | process.on("uncaughtException", function (err) { 128 | var nativeErrors = ["EvalError", "InternalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError"]; 129 | 130 | // if the error is related to code 131 | // throw it 132 | // if not, assume that it was thrown on purpose 133 | if (nativeErrors.indexOf(err.name) > -1) { 134 | _this.stdout.write(err.stack + os.EOL); 135 | } else { 136 | _this.stdout.write(chalk.red("error") + "\t" + err.message + os.EOL); 137 | } 138 | 139 | process.exit(1); 140 | }); 141 | 142 | // search through ./commands folder 143 | // and register all commands 144 | this.setupCommands(); 145 | 146 | // strip executable and path to a script 147 | process.argv.splice(0, 2); 148 | 149 | // parse arguments 150 | var options = minimist(process.argv); 151 | var argv = options._; 152 | 153 | if (this.delimiter !== " ") { 154 | argv = argv.map(function (arg) { 155 | return arg.split(_this.delimiter); 156 | }); 157 | argv = flatten(argv); 158 | } 159 | 160 | // determine if it is a help command 161 | var isHelp = !argv.length || options.h || options.help; 162 | 163 | // find matching command 164 | var names = Object.keys(this.commands); 165 | 166 | var matches = names.filter(function (name) { 167 | var isValid = true; 168 | 169 | name.split(_this.delimiter).forEach(function (part, i) { 170 | if (part !== argv[i]) isValid = false; 171 | }); 172 | 173 | return isValid; 174 | }); 175 | 176 | var name = matches.reverse()[0] || ""; 177 | var command = this._get(name); 178 | 179 | // strip command name from arguments 180 | var delimiters = this.delimiter === " " ? name.split(this.delimiter).length : 1; 181 | 182 | process.argv.splice(0, delimiters); 183 | 184 | // execute 185 | if (isHelp) { 186 | var help = undefined; 187 | 188 | if (command) { 189 | help = new command().help(); 190 | } else { 191 | help = this.help(); 192 | } 193 | 194 | this.stdout.write(help + os.EOL); 195 | } else { 196 | this.invoke(command); 197 | } 198 | }; 199 | 200 | 201 | 202 | 203 | /** 204 | * Lazy load command 205 | * 206 | * @api private 207 | */ 208 | Program.prototype._get = function _get(name) { 209 | var path = this.commands[name]; 210 | 211 | // if value is not string (not path) 212 | // then this command was already require()'ed 213 | if (typeof path !== "string") { 214 | return path; 215 | }var command = require(path); 216 | 217 | // create a bridge between command 218 | // and a program 219 | command.prototype.program = this; 220 | 221 | // and tell it what is its name 222 | command.prototype.name = name; 223 | 224 | // save to prevent the same work ^ 225 | this.commands[name] = command; 226 | 227 | return command; 228 | }; 229 | 230 | 231 | 232 | 233 | /** 234 | * Auto-update a program 235 | * 236 | * @api public 237 | */ 238 | Program.prototype.autoupdate = function autoupdate(done) { 239 | var pkg = require(join(this.path, "package.json")); 240 | 241 | var name = pkg.name; 242 | var version = pkg.version; 243 | 244 | // file in OS tmp directory 245 | // to keep track when autoupdate 246 | // was last executed 247 | var tmpPath = join(os.tmpdir(), name); 248 | 249 | var shouldCheck = true; 250 | 251 | try { 252 | // get mtime of tracking file 253 | var stat = fs.statSync(tmpPath); 254 | 255 | // if file was touched earlier 256 | // than a day ago 257 | // update its mtime and autoupdate 258 | // else just run the program 259 | if (new Date() - stat.mtime > 60 * 60 * 24 * 1000) { 260 | fs.writeFileSync(tmpPath, "", "utf-8"); 261 | } else { 262 | shouldCheck = false; 263 | } 264 | } catch (e) { 265 | // no file was created, need to create 266 | fs.writeFileSync(tmpPath, "", "utf-8"); 267 | } 268 | 269 | if (!shouldCheck) { 270 | return done(); 271 | } // get the latest version of itself 272 | exec("npm info " + name + " version", function (err, latestVersion) { 273 | // compare using semver 274 | var updateAvailable = err || !latestVersion ? false : semver.gt(latestVersion, version); 275 | 276 | if (!updateAvailable) return done(); 277 | 278 | exec("npm install -g " + name, function () { 279 | // execute the same command 280 | // but on the updated program 281 | spawn("node", process.argv.slice(1), { stdio: "inherit" }); 282 | }); 283 | }); 284 | }; 285 | 286 | 287 | 288 | 289 | /** 290 | * Show help 291 | * 292 | * @param {String} command 293 | * @api public 294 | */ 295 | Program.prototype.help = function help() { 296 | var _this = this; 297 | // calculate a maximum number 298 | // of delimiters in all command names 299 | var max = function (arr) { 300 | return Math.max.apply(Math, _toConsumableArray(arr)); 301 | }; 302 | 303 | var delimiters = max(Object.keys(this.commands).map(function (key) { 304 | return key.split(_this.delimiter).length; 305 | })); 306 | 307 | // build help output 308 | var help = ""; 309 | 310 | help += "Usage: " + this.name + " COMMAND [OPTIONS]" + (os.EOL + os.EOL); 311 | 312 | if (this.desc) { 313 | help += " " + this.desc + "" + (os.EOL + os.EOL); 314 | } 315 | 316 | // build a list of program's top commands 317 | var names = Object.keys(this.commands).filter(function (name) { 318 | var parts = name.split(_this.delimiter); 319 | 320 | // determine if parent command exists 321 | // e.g. `apps` in case of `apps add` 322 | var parent = parts.length > 1 && parts[parts.length - 2]; 323 | var parentExists = parent && _this.commands[parent]; 324 | 325 | // include command if it does not have a parent 326 | return !parentExists || parts.length < delimiters; 327 | }); 328 | 329 | // program does not have commands 330 | if (!names.length) { 331 | return; 332 | } // sort commands alphabetically 333 | names.sort(function (a, b) { 334 | return a.localeCompare(b); 335 | }); 336 | 337 | // get description for each command 338 | var commands = names.map(function (name) { 339 | var command = _this._get(name); 340 | var desc = new command().help("compact"); 341 | 342 | return [name, desc]; 343 | }); 344 | 345 | // output a list of commands 346 | help += "Available commands:" + (os.EOL + os.EOL); 347 | help += table(commands); 348 | 349 | return help; 350 | }; 351 | 352 | 353 | 354 | 355 | /** 356 | * Execute command 357 | * 358 | * @param {String} command 359 | * @api public 360 | */ 361 | Program.prototype.invoke = function invoke(command) { 362 | if (typeof command === "string") command = this._get(command); 363 | 364 | new command().execute(); 365 | }; 366 | 367 | return Program; 368 | })(); 369 | 370 | 371 | 372 | 373 | /** 374 | * Expose `Program` 375 | */ 376 | 377 | module.exports = Program; 378 | -------------------------------------------------------------------------------- /build/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Dependencies 5 | */ 6 | 7 | var fs = require("fs"); 8 | 9 | 10 | /** 11 | * Utilities 12 | */ 13 | 14 | 15 | /** 16 | * Object.assign polyfill 17 | * 18 | * @param {Object} dest 19 | * @param {Object} src 20 | * @returns {Object} 21 | * @api public 22 | */ 23 | 24 | if (!Object.assign) { 25 | Object.assign = function (dest, src) { 26 | Object.keys(src).forEach(function (key) { 27 | return dest[key] = src[key]; 28 | }); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.10.35 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Program = require('./build/program'); 2 | var Command = require('./build/command'); 3 | 4 | var exports = module.exports = function createProgram (options) { 5 | return new Program(options); 6 | }; 7 | 8 | exports.Command = Command; -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var Class = require('class-extend'); 6 | 7 | var minimist = require('minimist'); 8 | var table = require('text-table'); 9 | var async = require('async'); 10 | var os = require('os'); 11 | 12 | // path shortcuts 13 | var join = require('path').join; 14 | 15 | /** 16 | * Command 17 | */ 18 | 19 | class Command { 20 | constructor () { 21 | if (!this.options) this.options = {}; 22 | if (!this.use) this.use = []; 23 | if (!this.desc) this.desc = ''; 24 | 25 | this.configure(); 26 | } 27 | 28 | configure () { 29 | 30 | } 31 | 32 | run () { 33 | 34 | } 35 | 36 | 37 | /** 38 | * Display help for a command 39 | * 40 | * @param {String} type 41 | * @api public 42 | */ 43 | help (type) { 44 | if (type === 'compact') return this.desc; 45 | 46 | let help = ''; 47 | 48 | help += `Usage: ${ this.usage() }${ os.EOL + os.EOL }`; 49 | 50 | if (this.desc) { 51 | help += ` ${ this.desc }${ os.EOL + os.EOL }`; 52 | } 53 | 54 | // generate a list of sub-commands 55 | let names = Object.keys(this.program.commands).filter(name => { 56 | // add command to the list if 57 | // - it does not equal to itself 58 | // - it contains the name of a current command 59 | return name !== this.name && name.split(this.program.delimiter).indexOf(this.name) > -1; 60 | }); 61 | 62 | // no sub-commands 63 | if (!names.length) return help; 64 | 65 | // sort commands alphabetically 66 | names.sort((a, b) => a.localeCompare(b)); 67 | 68 | // get description for each command 69 | let commands = names.map(name => { 70 | let command = this.program._get(name); 71 | let desc = new command().help('compact'); 72 | 73 | return [name, desc]; 74 | }); 75 | 76 | // output the list 77 | help += `Additional commands:${ os.EOL + os.EOL }`; 78 | help += table(commands); 79 | 80 | return help; 81 | } 82 | 83 | 84 | /** 85 | * Return usage information for this command 86 | * 87 | * @api public 88 | */ 89 | usage () { 90 | return `${ this.program.name } ${ this.name } [OPTIONS]`; 91 | } 92 | 93 | 94 | /** 95 | * Build options for minimist 96 | * 97 | * @api private 98 | */ 99 | _buildOptions (options) { 100 | // options for minimist 101 | let parseOptions = { 102 | string: [], 103 | boolean: [], 104 | alias: {}, 105 | default: {} 106 | }; 107 | 108 | // convert command options 109 | // into options for minimist 110 | Object.keys(options).forEach(name => { 111 | let option = options[name]; 112 | 113 | // option can be a string 114 | // assume it's a type 115 | if (option === 'string') { 116 | option = { 117 | type: option 118 | }; 119 | } 120 | 121 | if (option.type === 'string') { 122 | parseOptions.string.push(name); 123 | } 124 | 125 | if (option.type === 'boolean') { 126 | parseOptions.boolean.push(name); 127 | } 128 | 129 | // "aliases" or "alias" property 130 | // can be string or array 131 | // need to always convert to array 132 | let aliases = option.aliases || option.alias || []; 133 | if (typeof aliases === 'string') aliases = [aliases]; 134 | 135 | aliases.forEach(alias => parseOptions.alias[alias] = name); 136 | 137 | if (option.default) { 138 | parseOptions.default[name] = option.default; 139 | } 140 | }); 141 | 142 | return parseOptions; 143 | } 144 | 145 | 146 | /** 147 | * Parse arguments 148 | * 149 | * @api private 150 | */ 151 | _buildArgs () { 152 | let program = this.program; 153 | let command = this; 154 | 155 | // options for parser 156 | let parseOptions; 157 | 158 | // global options 159 | let options = program.options || {}; 160 | 161 | parseOptions = this._buildOptions(options); 162 | this.global = minimist(process.argv, parseOptions); 163 | 164 | // command options 165 | options = command.options; 166 | 167 | parseOptions = this._buildOptions(options); 168 | let args = this.options = minimist(process.argv, parseOptions); 169 | 170 | // arguments for .run() method 171 | // need to build them in the same order 172 | // they were defined 173 | let handlerArgs = []; 174 | 175 | Object.keys(options).forEach(name => { 176 | let option = options[name]; 177 | let value = args[name]; 178 | 179 | if (option.required && !value) { 180 | throw new Error(`No value provided for required argument '${ name }'`); 181 | } 182 | 183 | handlerArgs.push(args[name]); 184 | }); 185 | 186 | // append arguments (don't confuse with options) 187 | handlerArgs.push.apply(handlerArgs, args._); 188 | 189 | return handlerArgs; 190 | } 191 | 192 | 193 | /** 194 | * Run command 195 | * 196 | * @api public 197 | */ 198 | execute () { 199 | let program = this.program; 200 | let command = this; 201 | 202 | let args = this._buildArgs(); 203 | 204 | let middleware = this.use || []; 205 | 206 | // middleware must always be an array 207 | if (! (middleware instanceof Array)) middleware = [middleware]; 208 | 209 | // last function in a middleware 210 | // is the actual command 211 | middleware.push(this.run); 212 | 213 | let stack = middleware.map((fn, index) => { 214 | // if middleware item is a string 215 | // it is a function name 216 | if (typeof fn === 'string') { 217 | // check if function with this name 218 | // exists in Command.prototype 219 | // else, search in middleware/ dir 220 | if (typeof command[fn] === 'function') { 221 | fn = command[fn]; 222 | } else { 223 | let path = join(program.path, 'middleware', fn); 224 | fn = require(path); 225 | } 226 | } 227 | 228 | if (index === middleware.length - 1) { 229 | let bindArgs = Array.prototype.slice.call(args); 230 | bindArgs.unshift(command); 231 | 232 | fn = fn.bind.apply(fn, bindArgs); 233 | fn.displayName = 'run'; 234 | } else { 235 | fn = fn.bind(command); 236 | } 237 | 238 | return fn; 239 | }); 240 | 241 | async.forEachSeries(stack, (fn, next) => { 242 | if ('run' === fn.displayName) { 243 | fn(); 244 | next(); 245 | } else { 246 | fn(next); 247 | } 248 | }); 249 | } 250 | 251 | static extend () { 252 | return Class.extend.apply(this, arguments); 253 | } 254 | } 255 | 256 | 257 | /** 258 | * Expose `Command` 259 | */ 260 | 261 | module.exports = Command; 262 | -------------------------------------------------------------------------------- /lib/program.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var minimist = require('minimist'); 6 | var flatten = require('lodash.flatten'); 7 | var assign = require('object-assign'); 8 | var semver = require('semver'); 9 | var spawn = require('child_process').spawn; 10 | var chalk = require('chalk'); 11 | var table = require('text-table'); 12 | var glob = require('glob').sync; 13 | var exec = require('child_process').exec; 14 | var fs = require('fs'); 15 | var os = require('os'); 16 | 17 | // path shortcuts 18 | var separator = require('path').sep; 19 | var normalize = require('path').normalize; 20 | var basename = require('path').basename; 21 | var join = require('path').join; 22 | 23 | /** 24 | * Program 25 | */ 26 | 27 | class Program { 28 | constructor (options) { 29 | this.name = basename(process.argv[1]); 30 | this.path = normalize(process.cwd()); 31 | this.delimiter = ' '; 32 | this.stdout = process.stdout; 33 | this.commands = {}; 34 | 35 | // if options is a string 36 | // assume it's a path 37 | if ('string' === typeof options) this.path = options; 38 | 39 | // if it's an object 40 | // extend this with it 41 | if ('object' === typeof options) assign(this, options); 42 | } 43 | 44 | /** 45 | * Set option 46 | * 47 | * @param {String,Object} key 48 | * @param {Mixed} value - value 49 | * @returns {Mixed} 50 | * @api public 51 | */ 52 | set (key, value) { 53 | if ('object' === typeof key) { 54 | return assign(this, key); 55 | } 56 | 57 | return this[key] = value; 58 | } 59 | 60 | 61 | /** 62 | * Get option 63 | * 64 | * @param {String} key 65 | * @returns {Mixed} 66 | * @api public 67 | */ 68 | get (key) { 69 | return this[key]; 70 | } 71 | 72 | 73 | /** 74 | * Setup commands 75 | * 76 | * @api private 77 | */ 78 | setupCommands () { 79 | if (!fs.existsSync(this.path)) return; 80 | 81 | let files = glob(join(this.path, 'commands', '**', '*')); 82 | 83 | files.forEach(path => { 84 | // next 2 lines are for windows 85 | // compatibility, because glob 86 | // returns "/" instead of "\" 87 | // and lower-cased drive letter 88 | path = path.replace(/\//g, separator) 89 | .replace(/^./, $1 => $1.toUpperCase()) 90 | .replace(join(this.path, 'commands'), '') 91 | .replace(separator, ''); 92 | 93 | // include only .js files 94 | if (/\.js$/.test(path)) { 95 | let name = path.split(separator) 96 | .join(this.delimiter) 97 | .replace('.js', ''); 98 | 99 | this.commands[name] = join(this.path, 'commands', path); 100 | } 101 | }); 102 | } 103 | 104 | 105 | /** 106 | * Run a program 107 | * 108 | * @api public 109 | */ 110 | run () { 111 | // catch exceptions 112 | process.on('uncaughtException', err => { 113 | let nativeErrors = [ 114 | 'EvalError', 115 | 'InternalError', 116 | 'RangeError', 117 | 'ReferenceError', 118 | 'SyntaxError', 119 | 'TypeError', 120 | 'URIError' 121 | ]; 122 | 123 | // if the error is related to code 124 | // throw it 125 | // if not, assume that it was thrown on purpose 126 | if (nativeErrors.indexOf(err.name) > -1) { 127 | this.stdout.write(err.stack + os.EOL); 128 | } else { 129 | this.stdout.write(chalk.red('error') + '\t' + err.message + os.EOL); 130 | } 131 | 132 | process.exit(1); 133 | }); 134 | 135 | // search through ./commands folder 136 | // and register all commands 137 | this.setupCommands(); 138 | 139 | // strip executable and path to a script 140 | process.argv.splice(0, 2); 141 | 142 | // parse arguments 143 | let options = minimist(process.argv); 144 | let argv = options._; 145 | 146 | if (this.delimiter !== ' ') { 147 | argv = argv.map(arg => arg.split(this.delimiter)); 148 | argv = flatten(argv); 149 | } 150 | 151 | // determine if it is a help command 152 | let isHelp = !argv.length || options.h || options.help; 153 | 154 | // find matching command 155 | let names = Object.keys(this.commands); 156 | 157 | let matches = names.filter(name => { 158 | let isValid = true; 159 | 160 | name.split(this.delimiter).forEach((part, i) => { 161 | if (part !== argv[i]) isValid = false; 162 | }); 163 | 164 | return isValid; 165 | }); 166 | 167 | let name = matches.reverse()[0] || ''; 168 | let command = this._get(name); 169 | 170 | // strip command name from arguments 171 | let delimiters = this.delimiter === ' ' ? name.split(this.delimiter).length : 1; 172 | 173 | process.argv.splice(0, delimiters); 174 | 175 | // execute 176 | if (isHelp) { 177 | let help; 178 | 179 | if (command) { 180 | help = new command().help(); 181 | } else { 182 | help = this.help(); 183 | } 184 | 185 | this.stdout.write(help + os.EOL); 186 | } else { 187 | this.invoke(command); 188 | } 189 | } 190 | 191 | 192 | /** 193 | * Lazy load command 194 | * 195 | * @api private 196 | */ 197 | _get (name) { 198 | let path = this.commands[name]; 199 | 200 | // if value is not string (not path) 201 | // then this command was already require()'ed 202 | if (typeof path !== 'string') return path; 203 | 204 | let command = require(path); 205 | 206 | // create a bridge between command 207 | // and a program 208 | command.prototype.program = this; 209 | 210 | // and tell it what is its name 211 | command.prototype.name = name; 212 | 213 | // save to prevent the same work ^ 214 | this.commands[name] = command; 215 | 216 | return command; 217 | } 218 | 219 | 220 | /** 221 | * Auto-update a program 222 | * 223 | * @api public 224 | */ 225 | autoupdate (done) { 226 | let pkg = require(join(this.path, 'package.json')); 227 | 228 | let name = pkg.name; 229 | let version = pkg.version; 230 | 231 | // file in OS tmp directory 232 | // to keep track when autoupdate 233 | // was last executed 234 | let tmpPath = join(os.tmpdir(), name); 235 | 236 | let shouldCheck = true; 237 | 238 | try { 239 | // get mtime of tracking file 240 | let stat = fs.statSync(tmpPath); 241 | 242 | // if file was touched earlier 243 | // than a day ago 244 | // update its mtime and autoupdate 245 | // else just run the program 246 | if (new Date - stat.mtime > 60 * 60 * 24 * 1000) { 247 | fs.writeFileSync(tmpPath, '', 'utf-8'); 248 | } else { 249 | shouldCheck = false; 250 | } 251 | } catch (e) { 252 | // no file was created, need to create 253 | fs.writeFileSync(tmpPath, '', 'utf-8'); 254 | } 255 | 256 | if (!shouldCheck) return done(); 257 | 258 | // get the latest version of itself 259 | exec('npm info ' + name + ' version', (err, latestVersion) => { 260 | // compare using semver 261 | let updateAvailable = err || !latestVersion ? false : semver.gt(latestVersion, version); 262 | 263 | if (!updateAvailable) return done(); 264 | 265 | exec('npm install -g ' + name, () => { 266 | // execute the same command 267 | // but on the updated program 268 | spawn('node', process.argv.slice(1), { stdio: 'inherit' }); 269 | }); 270 | }); 271 | } 272 | 273 | 274 | /** 275 | * Show help 276 | * 277 | * @param {String} command 278 | * @api public 279 | */ 280 | help () { 281 | // calculate a maximum number 282 | // of delimiters in all command names 283 | const max = arr => Math.max(...arr); 284 | 285 | let delimiters = max(Object.keys(this.commands).map(key => key.split(this.delimiter).length)); 286 | 287 | // build help output 288 | let help = ''; 289 | 290 | help += `Usage: ${ this.name } COMMAND [OPTIONS]${ os.EOL + os.EOL }`; 291 | 292 | if (this.desc) { 293 | help += ` ${ this.desc }${ os.EOL + os.EOL }`; 294 | } 295 | 296 | // build a list of program's top commands 297 | let names = Object.keys(this.commands).filter(name => { 298 | let parts = name.split(this.delimiter); 299 | 300 | // determine if parent command exists 301 | // e.g. `apps` in case of `apps add` 302 | let parent = parts.length > 1 && parts[parts.length - 2]; 303 | let parentExists = parent && this.commands[parent]; 304 | 305 | // include command if it does not have a parent 306 | return !parentExists || parts.length < delimiters; 307 | }); 308 | 309 | // program does not have commands 310 | if (!names.length) return; 311 | 312 | // sort commands alphabetically 313 | names.sort((a, b) => a.localeCompare(b)); 314 | 315 | // get description for each command 316 | let commands = names.map(name => { 317 | let command = this._get(name); 318 | let desc = new command().help('compact'); 319 | 320 | return [name, desc]; 321 | }); 322 | 323 | // output a list of commands 324 | help += `Available commands:${ os.EOL + os.EOL }`; 325 | help += table(commands); 326 | 327 | return help; 328 | } 329 | 330 | 331 | /** 332 | * Execute command 333 | * 334 | * @param {String} command 335 | * @api public 336 | */ 337 | invoke (command) { 338 | if (typeof command === 'string') command = this._get(command); 339 | 340 | new command().execute(); 341 | } 342 | } 343 | 344 | 345 | /** 346 | * Expose `Program` 347 | */ 348 | 349 | module.exports = Program; 350 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ronin", 3 | "version": "0.3.11", 4 | "description": "Libary to build shining CLI tools", 5 | "author": "Vadim Demedes ", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha" 8 | }, 9 | "keywords": [ 10 | "cli", 11 | "command", 12 | "command-line" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/vdemedes/ronin" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/vdemedes/ronin/issues" 20 | }, 21 | "devDependencies": { 22 | "babel": "^4.1.1", 23 | "chai": "^1.10.0", 24 | "mocha": "^2.0.1" 25 | }, 26 | "dependencies": { 27 | "async": "^0.9.0", 28 | "chalk": "^0.5.1", 29 | "class-extend": "^0.1.1", 30 | "glob": "^4.3.1", 31 | "inflect": "^0.3.0", 32 | "lodash.flatten": "^3.0.0", 33 | "minimist": "^1.1.0", 34 | "object-assign": "^4.0.1", 35 | "semver": "^4.2.0", 36 | "text-table": "^0.2.0" 37 | }, 38 | "bin": { 39 | "ronin": "./bin/ronin" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/hello-world/commands/apps.js: -------------------------------------------------------------------------------- 1 | var Command = require('../../../../').Command; 2 | 3 | var AppsCommand = module.exports = Command.extend({ 4 | desc: 'List applications', 5 | 6 | run: function () { 7 | this.program.stdout.write('apps\n'); 8 | } 9 | }); -------------------------------------------------------------------------------- /test/fixtures/hello-world/commands/apps/add.js: -------------------------------------------------------------------------------- 1 | var Command = require('../../../../../').Command; 2 | 3 | var AppsAddCommand = module.exports = Command.extend({ 4 | desc: 'Add application', 5 | 6 | options: { 7 | stack: 'string' 8 | }, 9 | 10 | run: function (stack, name) { 11 | this.program.stdout.write('apps add ' + this.options.stack + ' ' + name + '\n'); 12 | } 13 | }); -------------------------------------------------------------------------------- /test/fixtures/hello-world/commands/apps/destroy.js: -------------------------------------------------------------------------------- 1 | var Command = require('../../../../../').Command; 2 | 3 | var AppsDestroyCommand = module.exports = Command.extend({ 4 | desc: 'Destroy application', 5 | 6 | options: { 7 | force: { 8 | type: 'boolean', 9 | alias: 'f' 10 | } 11 | }, 12 | 13 | run: function (force, name) { 14 | this.program.stdout.write('apps destroy ' + name + ' ' + force + '\n'); 15 | } 16 | }); -------------------------------------------------------------------------------- /test/fixtures/hello-world/commands/apps/edit.js: -------------------------------------------------------------------------------- 1 | var Command = require('../../../../../').Command; 2 | 3 | var AppsEditCommand = module.exports = Command.extend({ 4 | desc: 'Edit application', 5 | use: ['auth', 'beforeRun'], 6 | 7 | run: function (name) { 8 | this.program.stdout.write('apps edit ' + name + '\n'); 9 | }, 10 | 11 | beforeRun: function (next) { 12 | this.program.stdout.write('beforeRun\n'); 13 | 14 | next(); 15 | } 16 | }); -------------------------------------------------------------------------------- /test/fixtures/hello-world/commands/generate/project.js: -------------------------------------------------------------------------------- 1 | var Command = require('../../../../../').Command; 2 | 3 | var GenerateProjectCommand = module.exports = Command.extend({ 4 | desc: 'Generate new project', 5 | options: { 6 | verbose: 'boolean' 7 | }, 8 | 9 | run: function (verbose, name) { 10 | this.program.stdout.write('generate project ' + name + ' ' + verbose + ' ' + this.global.app + ' ' + this.global.a + '\n'); 11 | } 12 | }); -------------------------------------------------------------------------------- /test/fixtures/hello-world/middleware/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = function (next) { 2 | this.program.stdout.write('auth\n'); 3 | 4 | setTimeout(next, 500); 5 | }; -------------------------------------------------------------------------------- /test/ronin.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | require('chai').should(); 6 | 7 | var resolve = require('path').resolve; 8 | var ronin = require('../'); 9 | var chalk = require('chalk'); 10 | 11 | 12 | /** 13 | * Tests 14 | */ 15 | 16 | describe ('Ronin', function () { 17 | afterEach(function () { 18 | process.removeAllListeners('uncaughtException'); 19 | }); 20 | 21 | describe ('Setup', function () { 22 | it ('should setup application with no arguments', function () { 23 | var program = ronin(); 24 | 25 | program.path.should.equal(resolve(__dirname + '/../')); 26 | program.delimiter.should.equal(' '); 27 | program.name.should.equal('_mocha'); 28 | }); 29 | 30 | it ('should setup application with root path', function () { 31 | var program = ronin(__dirname); 32 | 33 | program.path.should.equal(__dirname); 34 | program.delimiter.should.equal(' '); 35 | program.name.should.equal('_mocha'); 36 | }); 37 | 38 | it ('should setup application with arguments', function () { 39 | var program = ronin({ 40 | path: __dirname, 41 | delimiter: ':', 42 | name: 'hello-world' 43 | }); 44 | 45 | program.path.should.equal(__dirname); 46 | program.delimiter.should.equal(':'); 47 | program.name.should.equal('hello-world'); 48 | }); 49 | 50 | it ('should setup application with arguments using .set()', function () { 51 | var program = ronin(); 52 | 53 | program.set({ 54 | path: __dirname, 55 | delimiter: ':', 56 | name: 'hello-world' 57 | }); 58 | 59 | program.path.should.equal(__dirname); 60 | program.delimiter.should.equal(':'); 61 | program.name.should.equal('hello-world'); 62 | }); 63 | }); 64 | 65 | describe ('Commands', function () { 66 | it ('should show root help', function () { 67 | var program = createProgram(); 68 | 69 | var cases = [ 70 | 'node hello-world.js', 71 | 'node hello-world.js -h', 72 | 'node hello-world.js --help' 73 | ]; 74 | 75 | cases.forEach(function (args) { 76 | var stdout = outputStream(); 77 | program.stdout = stdout; 78 | 79 | process.argv = args.split(' '); 80 | program.run(); 81 | 82 | stdout.output.should.equal( 83 | 'Usage: hello-world COMMAND [OPTIONS]\n\n' + 84 | ' Hello World application\n\n' + 85 | 'Available commands:\n\n' + 86 | 'apps List applications\n' + 87 | 'generate project Generate new project\n' 88 | ); 89 | }); 90 | }); 91 | 92 | it ('should show help for individual command', function () { 93 | var program = createProgram(); 94 | 95 | var cases = [ 96 | 'node hello-world.js apps -h', 97 | 'node hello-world.js apps --help' 98 | ]; 99 | 100 | cases.forEach(function (args) { 101 | var stdout = outputStream(); 102 | program.stdout = stdout; 103 | 104 | process.argv = args.split(' '); 105 | program.run(); 106 | 107 | stdout.output.should.equal( 108 | 'Usage: hello-world apps [OPTIONS]\n\n' + 109 | ' List applications\n\n' + 110 | 'Additional commands:\n\n' + 111 | 'apps add Add application\n' + 112 | 'apps destroy Destroy application\n' + 113 | 'apps edit Edit application\n' 114 | ); 115 | }); 116 | }); 117 | 118 | it ('should execute command with no arguments', function () { 119 | var stdout = outputStream(); 120 | var program = createProgram(); 121 | program.stdout = stdout; 122 | 123 | process.argv = 'node hello-world.js apps'.split(' '); 124 | program.run(); 125 | 126 | stdout.output.should.equal('apps\n'); 127 | }); 128 | 129 | it ('should execute command with an option and argument', function () { 130 | var program = createProgram(); 131 | 132 | var cases = [ 133 | 'node hello-world.js apps add --stack cedar some-app', 134 | 'node hello-world.js apps add some-app --stack cedar' 135 | ]; 136 | 137 | cases.forEach(function (args) { 138 | var stdout = outputStream(); 139 | program.stdout = stdout; 140 | 141 | process.argv = args.split(' '); 142 | program.run(); 143 | 144 | stdout.output.should.equal('apps add cedar some-app\n'); 145 | }); 146 | }); 147 | 148 | it ('should execute command with an alias option and argument', function () { 149 | var program = createProgram(); 150 | 151 | var cases = [ 152 | 'node hello-world.js apps destroy some-app --force', 153 | 'node hello-world.js apps destroy some-app --force true', 154 | 'node hello-world.js apps destroy --force some-app', 155 | 'node hello-world.js apps destroy some-app -f', 156 | 'node hello-world.js apps destroy some-app -f true', 157 | 'node hello-world.js apps destroy -f some-app' 158 | ]; 159 | 160 | cases.forEach(function (args) { 161 | var stdout = outputStream(); 162 | program.stdout = stdout; 163 | 164 | process.argv = args.split(' '); 165 | program.run(); 166 | 167 | stdout.output.should.equal('apps destroy some-app true\n'); 168 | }); 169 | }); 170 | 171 | it ('should execute command with a global option', function () { 172 | var program = createProgram(); 173 | 174 | var cases = [ 175 | 'node hello-world.js generate project hello --verbose --app world', 176 | 'node hello-world.js generate project hello -a world --verbose', 177 | 'node hello-world.js generate project hello --app world --verbose' 178 | ]; 179 | 180 | cases.forEach(function (args) { 181 | var stdout = outputStream(); 182 | program.stdout = stdout; 183 | 184 | process.argv = args.split(' '); 185 | program.run(); 186 | 187 | stdout.output.should.equal('generate project hello true world world\n'); 188 | }); 189 | }); 190 | }); 191 | 192 | describe ('Middleware', function () { 193 | it ('should execute middleware', function (done) { 194 | var stdout = outputStream(); 195 | var program = createProgram(); 196 | program.stdout = stdout; 197 | 198 | process.argv = 'node hello-world.js apps edit some-app'.split(' '); 199 | program.run(); 200 | 201 | setTimeout(function () { 202 | stdout.output.should.equal( 203 | 'auth\n' + 204 | 'beforeRun\n' + 205 | 'apps edit some-app\n' 206 | ); 207 | 208 | done(); 209 | }, 1000); 210 | }); 211 | }); 212 | }); 213 | 214 | function createProgram () { 215 | return ronin({ 216 | path: __dirname + '/fixtures/hello-world', 217 | name: 'hello-world', 218 | desc: 'Hello World application', 219 | options: { 220 | app: { 221 | type: 'string', 222 | alias: 'a' 223 | } 224 | } 225 | }); 226 | } 227 | 228 | function outputStream () { 229 | var stdout = require('stream').Writable(); 230 | stdout.output = ''; 231 | stdout._write = function (chunk, encoding, next) { 232 | stdout.output += chunk; 233 | next(); 234 | }; 235 | 236 | return stdout; 237 | } --------------------------------------------------------------------------------