├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── dev ├── doc └── coverage.html ├── index.js ├── lib ├── NullStream.js ├── Request.js ├── Response.js ├── Shell.js ├── Styles.js ├── plugins │ ├── cloud9.js │ ├── coffee.js │ ├── completer.js │ ├── error.js │ ├── help.js │ ├── history.js │ ├── http.js │ ├── redis.js │ ├── router.js │ ├── stylus.js │ └── test.js ├── routes │ ├── confirm.js │ ├── prompt.js │ ├── shellOnly.js │ └── timeout.js ├── start_stop.js └── utils.js ├── package.json ├── samples ├── cloud9 │ └── sample.js ├── coffee │ ├── lib │ │ ├── .gitignore │ │ └── hello.coffee │ └── sample.coffee ├── error │ └── sample.js ├── http │ ├── app.js │ ├── db │ │ └── .gitignore │ ├── logs │ │ └── .gitignore │ ├── sample.js │ └── tmp │ │ └── .gitignore ├── params │ ├── ami.js │ └── sample.js ├── question.coffee ├── redis │ ├── redis.conf │ └── sample.js └── routes │ └── sample.js ├── src ├── NullStream.coffee ├── Request.coffee ├── Response.coffee ├── Shell.coffee ├── Styles.coffee ├── plugins │ ├── cloud9.coffee │ ├── coffee.coffee │ ├── completer.coffee │ ├── error.coffee │ ├── help.coffee │ ├── history.coffee │ ├── http.coffee │ ├── redis.coffee │ ├── router.coffee │ ├── stylus.coffee │ └── test.coffee ├── routes │ ├── confirm.coffee │ ├── prompt.coffee │ ├── shellOnly.coffee │ └── timeout.coffee ├── start_stop.coffee └── utils.coffee └── test ├── confirm.coffee ├── error.coffee ├── mocha.opts ├── plugin_http.coffee ├── plugin_http └── app.coffee ├── question.coffee ├── router.coffee ├── shell.coffee ├── start_stop.coffee ├── start_stop ├── server.js └── test_attach.coffee └── styles.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | yarn.lock 3 | package-lock.json 4 | node_modules 5 | samples/http/db/* 6 | samples/http/logs/* 7 | samples/http/tmp/* 8 | samples/redis/dump.rdb 9 | !.gitignore 10 | samples/cloud9/cloud9.err.log 11 | samples/cloud9/cloud9.out.log 12 | samples/cloud9/cloud9.pid 13 | samples/redis/redis.err.log 14 | samples/redis/redis.out.log 15 | samples/coffee/coffee.err.log 16 | samples/coffee/coffee.out.log 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # List of API changes and enhancements 3 | 4 | ## Version 0.5.1 5 | 6 | * fix: use Buffer.from 7 | * fix: validate parent dir before start 8 | 9 | ## Version 0.5.0 10 | 11 | Breaking changes: the version requires Node.js version 8 and above. 12 | 13 | * package: remove cov 14 | * package: latest coffee and mocha 15 | * package: change project url 16 | 17 | ## Version 0.2.10 18 | 19 | Move coffee as a dev dependency 20 | 21 | ## Version 0.2.9 22 | 23 | Make it a better citizen 24 | Generate JS code 25 | Rename `attach` option to `detached` in start_stop 26 | Restart on file change in start_stop 27 | 28 | ## Version 0.2.8 29 | 30 | Broader Node.js requirement to 0.6 31 | 32 | ## Version 0.2.7 33 | 34 | Fix Win7 completion (by [t101jv](https://github.com/t101jv)) 35 | Fix compatibility with Node.js version 0.8 36 | 37 | ## Version 0.2.6 38 | 39 | Reflect latest CoffeeScript strict mode 40 | 41 | ## Version 0.2.5 42 | 43 | Add cmd and path options to http plugin 44 | Stylus plugin 45 | StartStop script improvement 46 | 47 | ## Version 0.2.4 48 | 49 | `Shell.question` has been removed, use `req.question` instead 50 | `Shell.confirm` has been removed, use `req.confirm` instead 51 | Add `styles.unstyle` 52 | `detach` option changed in favor of `attach` in `start_stop` 53 | `start_stop` processes are now run as daemon by default 54 | 55 | ## Version 0.2.3 56 | 57 | Parameters routes contrains 58 | Add chdir setting 59 | Workspace discovery start from script root instead of cwd 60 | 61 | ## Version 0.2.2 62 | 63 | Plugin Cloud9, HTTP and Redis must be defined before the route plugin 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2010, SARL Adaltas. 2 | All rights reserved. 3 | 4 | Redistribution and use of this software in source and binary forms, with or 5 | without modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | - Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | - Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | - Neither the name of SARL Adaltas nor the names of its contributors may be 16 | used to endorse or promote products derived from this software without 17 | specific prior written permission of SARL Adaltas. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shell: applications with pluggable middleware 2 | 3 | Shell brings a Connect inspired API, Express inspired routing, and other 4 | similar functionality to console based applications. 5 | 6 | * Run both in shell mode and command mode 7 | * First class citizen for console application (arrows, ctrl-a, ctrl-u,...) 8 | * User friendly with history, help messages and many other plugins 9 | * Foundation to structure and build complex based applications 10 | * Command matching, parameters and advanced functionnalities found in Express routing 11 | * Flexible architecture based on middlewares for plugin creation and routing enhancement 12 | * Familiar API for those of us using Connect or Express 13 | * Predifined commands through plugins for Redis, HTTP servers, Cloud9, CoffeeScript, ... 14 | 15 | Installation 16 | ------------ 17 | 18 | Shell is open source and licensed under the new BSD license. 19 | 20 | ```bash 21 | npm install shell 22 | ``` 23 | 24 | Quick start 25 | ----------- 26 | 27 | The example below illustrate how to code a simple Redis client. 28 | 29 | ```javascript 30 | var shell = require('shell'); 31 | // Initialization 32 | var app = new shell( { chdir: __dirname } ) 33 | // Middleware registration 34 | app.configure(function() { 35 | app.use(function(req, res, next){ 36 | app.client = require('redis').createClient() 37 | next() 38 | }); 39 | app.use(shell.history({ 40 | shell: app 41 | })); 42 | app.use(shell.completer({ 43 | shell: app 44 | })); 45 | app.use(shell.redis({ 46 | config: 'redis.conf', 47 | pidfile: 'redis.pid' 48 | })); 49 | app.use(shell.router({ 50 | shell: app 51 | })); 52 | app.use(shell.help({ 53 | shell: app, 54 | introduction: true 55 | })); 56 | }); 57 | // Command registration 58 | app.cmd('redis keys :pattern', 'Find keys', function(req, res, next){ 59 | app.client.keys(req.params.pattern, function(err, keys){ 60 | if(err){ return res.styles.red(err.message), next(); } 61 | res.cyan(keys.join('\n')||'no keys'); 62 | res.prompt(); 63 | }); 64 | }); 65 | // Event notification 66 | app.on('quit', function(){ 67 | app.client.quit(); 68 | }); 69 | ``` 70 | 71 | Creating and Configuring a Shell 72 | -------------------------------- 73 | 74 | ```javascript 75 | var app = shell(); 76 | app.configure(function() { 77 | app.use(shell.history({shell: app})); 78 | app.use(shell.completer({shell: app})); 79 | app.use(shell.help({shell: app, introduction: true})); 80 | }); 81 | app.configure('prod', function() { 82 | app.set('title', 'Production Mode'); 83 | }); 84 | ``` 85 | 86 | Shell settings 87 | -------------- 88 | 89 | The constructor `shell` takes an optional object. Options are: 90 | 91 | - `chdir` , Changes the current working directory of the process, a string of the directory, boolean true will default to the `workspace` (in which case `workspace` must be provided or discoverable) 92 | - `prompt` , Character for command prompt, Defaults to ">>" 93 | - `stdin` , Source to read from 94 | - `stdout` , Destination to write to 95 | - `env` , Running environment, Defaults to the `env` setting (or `NODE_ENV` if defined, eg: `production`, `development`). 96 | - `isShell` , Detect whether the command is run inside a shell or as a single command. 97 | - `noPrompt` , Do not prompt the user for a command, useful to plug your own starting mechanism (eg: starting with a question). 98 | - `workspace` , Project root directory or null if none was found. The discovery strategy starts from the current working directory and traverses each parent dir looking for a `node_module` directory or a `package.json` file. 99 | 100 | Shell settings may be set by calling `app.set('key', value)`. They can be retrieved by calling the same function without a second argument. 101 | 102 | ```javascript 103 | var app = new shell({ 104 | chdir: true 105 | }); 106 | app.set('env', 'prod'); 107 | app.configure('prod', function() { 108 | console.log(app.set('env')); 109 | }); 110 | ``` 111 | 112 | As with Express, `app.configure` allows the customization of plugins for all or specific environments, while `app.use` registers plugins. 113 | 114 | If `app.configure` is called without specifying the environment as the first argument, the provided callback is always called. Otherwise, the environment must match the `env` setting or the global variable `NODE_ENV`. 115 | 116 | Middlewares and plugins 117 | ----------------------- 118 | 119 | Shell is build on a middleware architecture. When a command is issued, multiple callbacks are executed sequentially until one decide to stop the process (calling `res.prompt()` or `shell.quit`. Those callbacks are called middlewares. A callback recieves 3 arguments: a `request` object, a `response` object and the next callback. Traditionnaly, `request` deals with `stdin` while `response` deals with `stdout`. 120 | 121 | A plugin is simply a function which configure and return a middleware. Same plugin also enrich the Shell application with new routes and functions. 122 | 123 | Shell events 124 | ------------ 125 | 126 | The following events may be emitted: 127 | 128 | - `"command"` , listen to all executed commands, provide the command name as first argument. 129 | - `#{command}` , listen to a particular event. 130 | - `"quit"` , called when the application is about to quit. 131 | - `"error"` , called on error providing the error object as the first callback argument. 132 | - `"exit"` , called when the process exit. 133 | 134 | Request parameter 135 | ----------------- 136 | 137 | The request object contains the following properties: 138 | 139 | - `shell` , (required) A reference to your shell application. 140 | - `command` , Command entered by the user 141 | - `params` , Parameters object extracted from the command, defined by the `shell.router` middleware 142 | - `question` , Ask questions with optionally suggested and default answers 143 | - `confirm` , Ask a question expecting a boolean answer 144 | 145 | Response parameter 146 | ------------------ 147 | 148 | The response object inherits from styles containing methods for printing, coloring and bolding: 149 | 150 | Colors: 151 | 152 | - `black` 153 | - `white` 154 | - `yellow` 155 | - `blue` 156 | - `cyan` 157 | - `green` 158 | - `magenta` 159 | - `red` 160 | - `bgcolor` 161 | - `color` 162 | - `nocolor` 163 | 164 | Style: 165 | 166 | - `regular` 167 | - `weight` 168 | - `bold` 169 | 170 | Display: 171 | 172 | 173 | - `prompt` , Exits the current command and return user to the prompt. 174 | - `ln` , Print a new line 175 | - `print` , Print a text 176 | - `println` , Print a text followed by a new line 177 | - `reset` , Stop any formating like color or bold 178 | - `pad` , Print a text with a fixed padding 179 | - `raw` , Return a text 180 | 181 | Router plugin 182 | ------------- 183 | 184 | The functionality provided by the 'routes' module is very similar to that of 185 | express. Options passed during creation are: 186 | 187 | - `shell` , (required) A reference to your shell application. 188 | - `sensitive` , (optional) Defaults to `false`, set to `true` if the match should be case sensitive. 189 | 190 | New routes are defined with the `cmd` method. A route is made of pattern against which the user command is matched, an optional description and one or more route specific middlewares to handle the command. The pattern is either a string or a regular expression. Middlewares receive three parameters: a request object, a response object, and a function. Command parameters are substituted and made available in the `params` object of the request parameter. 191 | 192 | Parameters can have restrictions in parenthesis immediately following the 193 | keyword, as in express: `:id([0-9]+)`. See the `list` route in the example: 194 | 195 | ```javascript 196 | var app = new shell(); 197 | app.configure(function(){ 198 | app.use(shell.router({ 199 | shell: app 200 | })); 201 | }); 202 | 203 | // Route middleware 204 | var auth = function(req, res, next){ 205 | if(req.params.uid == process.getuid()){ 206 | next() 207 | }else{ 208 | throw new Error('Not me'); 209 | } 210 | } 211 | 212 | // Global parameter substitution 213 | app.param('uid', function(req, res, next){ 214 | exec('whoami', function(err, stdout, sdterr){ 215 | req.params.username = stdout; 216 | next(); 217 | }); 218 | }); 219 | 220 | // Simple command 221 | app.cmd('help', function(req, res){ 222 | res.cyan('Run this command `./ami user ' + process.getuid() + '`'); 223 | res.prompt() 224 | }); 225 | 226 | // Command with parameter 227 | app.cmd('user :uid', auth, function(req, res){ 228 | res.cyan('Yes, you are ' + req.params.username); 229 | }); 230 | 231 | // Command with contrained parameter 232 | app.cmd('user :id([0-9]+)', function(req, res) { 233 | res.cyan('User id is ' + req.params.id); 234 | res.prompt(); 235 | }); 236 | ``` 237 | 238 | Contributors 239 | ------------ 240 | 241 | * David Worms : 242 | * Tony: 243 | * Russ Frank : 244 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shell = require('shell'); 4 | 5 | var app = module.exports = shell({chdir: true}); 6 | 7 | app.configure(function(){ 8 | app.use( shell.history({shell: app}) ); 9 | app.use( shell.completer({shell: app}) ); 10 | app.use( shell.cloud9({port: '4102'}) ); 11 | app.use( shell.router({shell: app}) ); 12 | app.use( shell.help({shell: app, introduction: true}) ); 13 | app.use( shell.error({shell: app}) ); 14 | }); 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | // Core 3 | var Shell = module.exports = require('./lib/Shell'); 4 | Shell.styles = require('./lib/Styles'); 5 | Shell.NullStream = require('./lib/NullStream'); 6 | 7 | // Plugins 8 | Shell.cloud9 = require('./lib/plugins/cloud9'); 9 | Shell.coffee = require('./lib/plugins/coffee'); 10 | Shell.completer = require('./lib/plugins/completer'); 11 | Shell.error = require('./lib/plugins/error'); 12 | Shell.help = require('./lib/plugins/help'); 13 | Shell.history = require('./lib/plugins/history'); 14 | Shell.http = require('./lib/plugins/http'); 15 | Shell.router = require('./lib/plugins/router'); 16 | Shell.redis = require('./lib/plugins/redis'); 17 | Shell.stylus = require('./lib/plugins/stylus'); 18 | Shell.test = require('./lib/plugins/test'); 19 | 20 | // Routes 21 | Shell.routes = { 22 | confirm: require('./lib/routes/confirm'), 23 | prompt: require('./lib/routes/prompt'), 24 | shellOnly: require('./lib/routes/shellOnly') 25 | }; 26 | 27 | Shell.Shell = function(settings){ 28 | console.warn('Deprecated, use `shell()` instead of `shell.Shell()`'); 29 | return new Shell( settings ); 30 | }; -------------------------------------------------------------------------------- /lib/NullStream.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var NullStream, events; 3 | 4 | events = require('events'); 5 | 6 | module.exports = NullStream = (function() { 7 | class NullStream extends events.EventEmitter { 8 | pause() {} 9 | 10 | resume() {} 11 | 12 | pipe() {} 13 | 14 | write(data) { 15 | return this.emit('data', data); 16 | } 17 | 18 | end() { 19 | return this.emit('close'); 20 | } 21 | 22 | // Shared API 23 | destroy() {} 24 | 25 | destroySoon() {} 26 | 27 | }; 28 | 29 | // Readable Stream 30 | NullStream.prototype.readable = true; 31 | 32 | // Writable Stream 33 | NullStream.prototype.writable = true; 34 | 35 | return NullStream; 36 | 37 | }).call(this); 38 | -------------------------------------------------------------------------------- /lib/Request.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var Request, each; 3 | 4 | each = require('each'); 5 | 6 | module.exports = Request = class Request { 7 | constructor(shell, command) { 8 | this.shell = shell; 9 | this.command = command; 10 | } 11 | 12 | /* 13 | Ask one or more questions 14 | */ 15 | question(questions, callback) { 16 | var answers, isObject, multiple, q, v; 17 | isObject = function(v) { 18 | return typeof v === 'object' && (v != null) && !Array.isArray(v); 19 | }; 20 | multiple = true; 21 | answers = {}; 22 | if (isObject(questions)) { 23 | questions = (function() { 24 | var results; 25 | results = []; 26 | for (q in questions) { 27 | v = questions[q]; 28 | if (v == null) { 29 | v = {}; 30 | } 31 | if (!isObject(v)) { 32 | v = { 33 | value: v 34 | }; 35 | } 36 | v.name = q; 37 | results.push(v); 38 | } 39 | return results; 40 | })(); 41 | } else if (typeof questions === 'string') { 42 | multiple = false; 43 | questions = [ 44 | { 45 | name: questions, 46 | value: '' 47 | } 48 | ]; 49 | } 50 | return each(questions).call((question, next) => { 51 | q = `${question.name} `; 52 | if (question.value) { 53 | q += `[${question.value}] `; 54 | } 55 | return this.shell.interface().question(q, function(answer) { 56 | if (answer.substr(-1, 1) === '\n') { 57 | answer = answer.substr(0, answer.length - 1); 58 | } 59 | answers[question.name] = answer === '' ? question.value : answer; 60 | return next(); 61 | }); 62 | }).next(function() { 63 | if (!multiple) { 64 | answers = answers[questions[0].name]; 65 | } 66 | return callback(answers); 67 | }); 68 | } 69 | 70 | /* 71 | Ask a question expecting a boolean answer 72 | */ 73 | confirm(msg, defaultTrue, callback) { 74 | var args, base, base1, keyFalse, keyTrue, key_false, key_true, question; 75 | args = arguments; 76 | if (!callback) { 77 | callback = defaultTrue; 78 | defaultTrue = true; 79 | } 80 | if ((base = this.shell.settings).key_true == null) { 81 | base.key_true = 'y'; 82 | } 83 | if ((base1 = this.shell.settings).key_false == null) { 84 | base1.key_false = 'n'; 85 | } 86 | key_true = this.shell.settings.key_true.toLowerCase(); 87 | key_false = this.shell.settings.key_false.toLowerCase(); 88 | keyTrue = defaultTrue ? key_true.toUpperCase() : key_true; 89 | keyFalse = defaultTrue ? key_false : key_false.toUpperCase(); 90 | msg += ' '; 91 | msg += `[${keyTrue}${keyFalse}] `; 92 | question = this.shell.styles.raw(msg, { 93 | color: 'green' 94 | }); 95 | return this.shell.interface().question(question, (answer) => { 96 | var accepted, valid; 97 | accepted = ['', key_true, key_false]; 98 | if (answer.substr(-1, 1) === '\n') { 99 | answer = answer.substr(0, answer.length - 1); 100 | } 101 | answer = answer.toLowerCase(); 102 | valid = accepted.indexOf(answer) !== -1; 103 | if (!valid) { 104 | return this.confirm.apply(this, args); 105 | } 106 | return callback(answer === key_true || (defaultTrue && answer === '')); 107 | }); 108 | } 109 | 110 | }; 111 | -------------------------------------------------------------------------------- /lib/Response.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var Response, pad, styles; 3 | 4 | styles = require('./Styles'); 5 | 6 | pad = require('pad'); 7 | 8 | module.exports = Response = (function() { 9 | class Response extends styles { 10 | constructor(settings) { 11 | super(settings); 12 | this.shell = settings.shell; 13 | } 14 | 15 | prompt() { 16 | return this.shell.prompt(); 17 | } 18 | 19 | }; 20 | 21 | Response.prototype.pad = pad; 22 | 23 | return Response; 24 | 25 | }).call(this); 26 | -------------------------------------------------------------------------------- /lib/Shell.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var EventEmitter, Interface, Request, Response, Shell, events, readline, styles, util, utils; 3 | 4 | util = require('util'); 5 | 6 | readline = require('readline'); 7 | 8 | events = require('events'); 9 | 10 | EventEmitter = events.EventEmitter; 11 | 12 | utils = require('./utils'); 13 | 14 | styles = require('./Styles'); 15 | 16 | Request = require('./Request'); 17 | 18 | Response = require('./Response'); 19 | 20 | // Fix readline interface 21 | Interface = require('readline').Interface; 22 | 23 | Interface.prototype.setPrompt = (function(parent) { 24 | return function(prompt, length) { 25 | var args; 26 | args = Array.prototype.slice.call(arguments); 27 | if (!args[1]) { 28 | args[1] = styles.unstyle(args[0]).length; 29 | } 30 | return parent.apply(this, args); 31 | }; 32 | })(Interface.prototype.setPrompt); 33 | 34 | module.exports = function(settings) { 35 | return new Shell(settings); 36 | }; 37 | 38 | Shell = (function() { 39 | class Shell extends EventEmitter { 40 | constructor(settings = {}) { 41 | var base, base1, base2, ref, ref1, ref2; 42 | super(settings); 43 | // EventEmitter.call @ 44 | this.tmp = {}; 45 | this.settings = settings; 46 | if ((base = this.settings).prompt == null) { 47 | base.prompt = '>> '; 48 | } 49 | if ((base1 = this.settings).stdin == null) { 50 | base1.stdin = process.stdin; 51 | } 52 | if ((base2 = this.settings).stdout == null) { 53 | base2.stdout = process.stdout; 54 | } 55 | this.set('env', (ref = (ref1 = this.settings.env) != null ? ref1 : process.env.NODE_ENV) != null ? ref : 'development'); 56 | this.set('command', typeof settings.command !== 'undefined' ? settings.command : process.argv.slice(2).join(' ')); 57 | this.stack = []; 58 | this.styles = styles({ 59 | stdout: this.settings.stdout 60 | }); 61 | process.on('beforeExit', () => { 62 | return this.emit('exit'); 63 | }); 64 | process.on('uncaughtException', (e) => { 65 | this.emit('exit', [e]); 66 | this.styles.red('Internal error, closing...').ln(); 67 | console.error(e.message); 68 | console.error(e.stack); 69 | return process.exit(); 70 | }); 71 | this.isShell = (ref2 = this.settings.isShell) != null ? ref2 : process.argv.length === 2; 72 | if (this.isShell) { 73 | this.interface(); 74 | } 75 | // Project root directory 76 | if (settings.workspace == null) { 77 | settings.workspace = utils.workspace(); 78 | } 79 | if (settings.chdir === true) { 80 | // Current working directory 81 | process.chdir(settings.workspace); 82 | } 83 | if (typeof settings.chdir === 'string') { 84 | process.chdir(settings.chdir); 85 | } 86 | // Start 87 | process.nextTick(() => { 88 | var command, noPrompt; 89 | if (this.isShell) { 90 | command = this.set('command'); 91 | noPrompt = this.set('noPrompt'); 92 | if (command) { 93 | return this.run(command); 94 | } else if (!noPrompt) { 95 | return this.prompt(); 96 | } 97 | } else { 98 | command = this.set('command'); 99 | if (command) { 100 | return this.run(command); 101 | } 102 | } 103 | }); 104 | return this; 105 | } 106 | 107 | 108 | // Return the readline interface and create it if not yet initialized 109 | interface() { 110 | if (this._interface != null) { 111 | return this._interface; 112 | } 113 | return this._interface = readline.createInterface(this.settings.stdin, this.settings.stdout); 114 | } 115 | 116 | 117 | // Configure callback for the given `env` 118 | configure(env, fn) { 119 | if (typeof env === 'function') { 120 | fn = env; 121 | env = 'all'; 122 | } 123 | if (env === 'all' || env === this.settings.env) { 124 | fn.call(this); 125 | } 126 | return this; 127 | } 128 | 129 | 130 | // Configure callback for the given `env` 131 | use(handle) { 132 | // Add the route, handle pair to the stack 133 | if (handle) { 134 | this.stack.push({ 135 | route: null, 136 | handle: handle 137 | }); 138 | } 139 | return this; 140 | } 141 | 142 | 143 | // Run a command 144 | run(command) { 145 | var index, next, req, res, self; 146 | command = command.trim(); 147 | this.emit('command', [command]); 148 | this.emit(command, []); 149 | self = this; 150 | req = new Request(this, command); 151 | res = new Response({ 152 | shell: this, 153 | stdout: this.settings.stdout 154 | }); 155 | index = 0; 156 | next = function(err) { 157 | var arity, layer, text; 158 | layer = self.stack[index++]; 159 | if (!layer) { 160 | if (err) { 161 | return self.emit('error', err); 162 | } 163 | if (command !== '') { 164 | text = `Command failed to execute ${command}`; 165 | if (err) { 166 | text += `: ${err.message || err.name}`; 167 | } 168 | res.red(text); 169 | } 170 | return res.prompt(); 171 | } 172 | arity = layer.handle.length; 173 | if (err) { 174 | if (arity === 4) { 175 | self.emit('error', err); 176 | return layer.handle(err, req, res, next); 177 | } else { 178 | return next(err); 179 | } 180 | } else if (arity < 4) { 181 | return layer.handle(req, res, next); 182 | } else { 183 | return next(); 184 | } 185 | }; 186 | return next(); 187 | } 188 | 189 | set(setting, val) { 190 | if (val == null) { 191 | if (this.settings.hasOwnProperty(setting)) { 192 | return this.settings[setting]; 193 | } else if (this.parent) { 194 | // for the future, parent being undefined for now 195 | return this.parent.set(setting); 196 | } 197 | } else { 198 | this.settings[setting] = val; 199 | return this; 200 | } 201 | } 202 | 203 | 204 | // Display prompt 205 | prompt() { 206 | var text; 207 | if (this.isShell) { 208 | text = this.styles.raw(this.settings.prompt, { 209 | color: 'green' 210 | }); 211 | return this.interface().question(text, this.run.bind(this)); 212 | } else { 213 | this.styles.ln(); 214 | if (process.versions) { 215 | return this.quit(); 216 | } else { 217 | 218 | // Node v0.6.1 throw error 'process.stdout cannot be closed' 219 | this.settings.stdout.destroySoon(); 220 | return this.settings.stdout.on('close', function() { 221 | return process.exit(); 222 | }); 223 | } 224 | } 225 | } 226 | 227 | 228 | // Command quit 229 | quit(params) { 230 | this.emit('quit'); 231 | this.interface().close(); 232 | return this.settings.stdin.destroy(); 233 | } 234 | 235 | }; 236 | 237 | 238 | // Store commands 239 | Shell.prototype.cmds = {}; 240 | 241 | return Shell; 242 | 243 | }).call(this); 244 | 245 | //@set 'stdin', null 246 | module.exports.Shell = Shell; 247 | -------------------------------------------------------------------------------- /lib/Styles.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var Styles, bgcolors, code, color, colors; 3 | 4 | colors = { 5 | black: 30, 6 | red: 31, 7 | green: 32, 8 | yellow: 33, 9 | blue: 34, 10 | magenta: 35, 11 | cyan: 36, 12 | white: 37 13 | }; 14 | 15 | bgcolors = { 16 | black: 40, 17 | red: 41, 18 | green: 42, 19 | yellow: 43, 20 | blue: 44, 21 | magenta: 45, 22 | cyan: 46, 23 | white: 47 24 | }; 25 | 26 | module.exports = Styles = function(settings = {}) { 27 | var ref; 28 | if (!(this instanceof Styles)) { 29 | return new Styles(settings); 30 | } 31 | this.settings = settings; 32 | this.settings.stdout = (ref = settings.stdout) != null ? ref : process.stdout; 33 | // Current state 34 | this.current = { 35 | weight: 'regular' 36 | }; 37 | // Export colors 38 | this.colors = colors; 39 | this.bgcolors = bgcolors; 40 | return this; 41 | }; 42 | 43 | // Color 44 | Styles.prototype.color = function(color, text) { 45 | this.print(text, { 46 | color: color 47 | }); 48 | if (!text) { 49 | // Save state if no text 50 | this.current.color = color; 51 | } 52 | return this; 53 | }; 54 | 55 | for (color in colors) { 56 | code = colors[color]; 57 | (function(color) { 58 | return Styles.prototype[color] = function(text) { 59 | return this.color(color, text); 60 | }; 61 | })(color); 62 | } 63 | 64 | Styles.prototype.nocolor = function(text) { 65 | return this.color(null, text); 66 | }; 67 | 68 | // bgcolor 69 | Styles.prototype.bgcolor = function(bgcolor) { 70 | if (bgcolor == null) { 71 | bgcolor = 0; 72 | } 73 | this.print('\x1B[' + bgcolor + ';m39'); 74 | return this; 75 | }; 76 | 77 | // Font weight 78 | Styles.prototype.weight = function(weight, text) { 79 | this.print(text, { 80 | weight: weight 81 | }); 82 | if (!text) { 83 | // Save state if no text 84 | this.current.weight = weight; 85 | } 86 | return this; 87 | }; 88 | 89 | Styles.prototype.bold = function(text) { 90 | return this.weight('bold', text); 91 | }; 92 | 93 | Styles.prototype.regular = function(text) { 94 | return this.weight('regular', text); 95 | }; 96 | 97 | // Print 98 | Styles.prototype.print = function(text, settings) { 99 | this.settings.stdout.write(this.raw(text, settings)); 100 | return this; 101 | }; 102 | 103 | Styles.prototype.println = function(text) { 104 | this.settings.stdout.write(text + '\n'); 105 | return this; 106 | }; 107 | 108 | Styles.prototype.ln = function() { 109 | this.settings.stdout.write('\n'); 110 | return this; 111 | }; 112 | 113 | // Others 114 | Styles.prototype.raw = function(text, settings) { 115 | var raw; 116 | raw = ''; 117 | if (settings == null) { 118 | settings = {}; 119 | } 120 | if (settings.color !== null && (settings.color || this.current.color)) { 121 | raw += '\x1b[' + this.colors[settings.color || this.current.color] + 'm'; 122 | } else { 123 | raw += '\x1b[39m'; 124 | } 125 | switch (settings.weight || this.current.weight) { 126 | case 'bold': 127 | raw += '\x1b[1m'; 128 | break; 129 | case 'regular': 130 | raw += '\x1b[22m'; 131 | break; 132 | default: 133 | throw new Error('Invalid weight "' + weight + '" (expect "bold" or "regular")'); 134 | } 135 | if (text) { 136 | // Print text if any 137 | raw += text; 138 | // Restore state if any 139 | if (this.current.color && this.current.color !== settings.color) { 140 | raw += this.raw(null, this.current.color); 141 | } 142 | if (this.current.weight && this.current.weight !== settings.weight) { 143 | raw += this.raw(null, this.current.weight); 144 | } 145 | } 146 | return raw; 147 | }; 148 | 149 | Styles.prototype.reset = function(text) { 150 | return this.print(null, { 151 | color: null, 152 | weight: 'regular' 153 | }); 154 | }; 155 | 156 | // Remove style 157 | Styles.unstyle = function(text) { 158 | return text.replace(/\x1b.*?m/g, ''); 159 | }; 160 | -------------------------------------------------------------------------------- /lib/plugins/cloud9.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var start_stop; 3 | 4 | start_stop = require('../start_stop'); 5 | 6 | /* 7 | 8 | Cloud9 plugin 9 | ============= 10 | 11 | Register two commands, `cloud9 start` and `cloud9 stop`. Unless provided, 12 | the Cloud9 workspace will be automatically discovered if your project root 13 | directory contains a "package.json" file or a "node_module" directory. 14 | 15 | Options: 16 | 17 | - `config` , Load the configuration from a config file. Overrides command-line options. Defaults to `null`. 18 | - `group` , Run child processes with a specific group. 19 | - `user` , Run child processes as a specific user. 20 | - `action` , Define an action to execute after the Cloud9 server is started. Defaults to `null`. 21 | - `ip` , IP address where Cloud9 will serve from. Defaults to `"127.0.0.1"`. 22 | - `port` , Port number where Cloud9 will serve from. Defaults to `3000`. 23 | - `workspace`, Path to the workspace that will be loaded in Cloud9, Defaults to `Shell.set('workspace')`. 24 | - `detached` , Wether the Cloud9 process should be attached to the current process. If not defined, default to `false` (the server doesn't run as a daemon). 25 | - `pidfile` , Path to the file storing the daemon process id. Defaults to `"/.node_shell/#{md5}.pid"` 26 | - `stdout` , Writable stream or file path to redirect cloud9 stdout. 27 | - `stderr` , Writable stream or file path to redirect cloud9 stderr. 28 | 29 | Example: 30 | 31 | ```javascript 32 | var app = new shell(); 33 | app.configure(function() { 34 | app.use(shell.router({ 35 | shell: app 36 | })); 37 | app.use(shell.cloud9({ 38 | shell: app, 39 | ip: '0.0.0.0' 40 | })); 41 | app.use(shell.help({ 42 | shell: app, 43 | introduction: true 44 | })); 45 | }); 46 | ``` 47 | 48 | **Important:** If you encounter issue while installing cloud9, it might be because the npm module expect an older version of Node. 49 | 50 | Here's the procedure to use the latest version: 51 | 52 | ``` 53 | git clone https://github.com/ajaxorg/cloud9.git 54 | cd cloud9 55 | git submodule update --init --recursive 56 | npm link 57 | ``` 58 | 59 | */ 60 | module.exports = function(settings = {}) { 61 | var cmd; 62 | cmd = function() { 63 | var args; 64 | args = []; 65 | args.push('-w'); 66 | args.push(settings.workspace); 67 | // Arguments 68 | if (settings.config) { 69 | args.push('-c'); 70 | args.push(settings.config); 71 | } 72 | if (settings.group) { 73 | args.push('-g'); 74 | args.push(settings.group); 75 | } 76 | if (settings.user) { 77 | args.push('-u'); 78 | args.push(settings.user); 79 | } 80 | if (settings.action) { 81 | args.push('-a'); 82 | args.push(settings.action); 83 | } 84 | if (settings.ip) { 85 | args.push('-l'); 86 | args.push(settings.ip); 87 | } 88 | if (settings.port) { 89 | args.push('-p'); 90 | args.push(settings.port); 91 | } 92 | return `cloud9 ${args.join(' ')}`; 93 | }; 94 | return function(req, res, next) { 95 | var app; 96 | app = req.shell; 97 | if (app.tmp.cloud9) { 98 | // Caching 99 | return next(); 100 | } 101 | app.tmp.cloud9 = true; 102 | // Workspace 103 | if (settings.workspace == null) { 104 | settings.workspace = app.set('workspace'); 105 | } 106 | if (!settings.workspace) { 107 | return next(new Error('No workspace provided')); 108 | } 109 | settings.cmd = cmd(); 110 | // Register commands 111 | app.cmd('cloud9 start', 'Start Cloud9', function(req, res, next) { 112 | // Launch process 113 | return start_stop.start(settings, function(err, pid) { 114 | var ip, message, port; 115 | if (err) { 116 | return next(err); 117 | } 118 | if (!pid) { 119 | res.cyan('Cloud9 already started').ln(); 120 | return res.prompt(); 121 | } 122 | ip = settings.ip || '127.0.0.1'; 123 | port = settings.port || 3000; 124 | message = `Cloud9 started http://${ip}:${port}`; 125 | res.cyan(message).ln(); 126 | return res.prompt(); 127 | }); 128 | }); 129 | app.cmd('cloud9 stop', 'Stop Cloud9', function(req, res, next) { 130 | return start_stop.stop(settings, function(err, success) { 131 | if (success) { 132 | res.cyan('Cloud9 successfully stoped').ln(); 133 | } else { 134 | res.magenta('Cloud9 was not started').ln(); 135 | } 136 | return res.prompt(); 137 | }); 138 | }); 139 | return next(); 140 | }; 141 | }; 142 | -------------------------------------------------------------------------------- /lib/plugins/coffee.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var enrichFiles, fs, start_stop; 3 | 4 | fs = require('fs'); 5 | 6 | start_stop = require('../start_stop'); 7 | 8 | // Sanitize a list of files separated by spaces 9 | enrichFiles = function(files) { 10 | return files.split(' ').map(function(file) { 11 | if (file.substr(0, 1) !== '/') { 12 | file = '/' + file; 13 | } 14 | if (file.substr(-1, 1) !== '/' && fs.statSync(file).isDirectory()) { 15 | file += '/'; 16 | } 17 | return file; 18 | }).join(' '); 19 | }; 20 | 21 | /* 22 | 23 | CoffeeScript plugin 24 | =================== 25 | 26 | Start Coffee in `--watch` mode, so scripts are instantly compiled into Javascript. 27 | 28 | Options: 29 | 30 | - `src` , Directory where ".coffee" are stored. Each ".coffee" script will be compiled into a .js JavaScript file of the same name. 31 | - `join` , Before compiling, concatenate all scripts together in the order they were passed, and write them into the specified file. Useful for building large projects. 32 | - `output` , Directory where compiled JavaScript files are written. Used in conjunction with "compile". 33 | - `lint` , If the `jsl` (JavaScript Lint) command is installed, use it to check the compilation of a CoffeeScript file. 34 | - `require` , Load a library before compiling or executing your script. Can be used to hook in to the compiler (to add Growl notifications, for example). 35 | - `detached` , Wether the Coffee process should be attached to the current process. If not defined, default to `false` (the server doesn't run as a daemon). 36 | - `pidfile` , Path to the file storing the daemon process id. Defaults to `"/.node_shell/#{md5}.pid"` 37 | - `stdout` , Writable stream or file path to redirect cloud9 stdout. 38 | - `stderr` , Writable stream or file path to redirect cloud9 stderr. 39 | - `workspace`, Project directory used to resolve relative paths. 40 | 41 | Example: 42 | 43 | ```javascript 44 | var app = new shell(); 45 | app.configure(function() { 46 | app.use(shell.router({ 47 | shell: app 48 | })); 49 | app.use(shell.coffee({ 50 | shell: app 51 | })); 52 | app.use(shell.help({ 53 | shell: app, 54 | introduction: true 55 | })); 56 | }); 57 | ``` 58 | 59 | */ 60 | module.exports = function(settings = {}) { 61 | var cmd, shell; 62 | if (!settings.shell) { 63 | // Validation 64 | throw new Error('No shell provided'); 65 | } 66 | shell = settings.shell; 67 | // Default settings 68 | if (settings.workspace == null) { 69 | settings.workspace = shell.set('workspace'); 70 | } 71 | if (!settings.workspace) { 72 | throw new Error('No workspace provided'); 73 | } 74 | cmd = function() { 75 | var args; 76 | args = []; 77 | 78 | if (settings.join) { 79 | args.push('-j'); 80 | args.push(enrichFiles(settings.join)); 81 | } 82 | // Watch the modification times of the coffee-scripts, 83 | // recompiling as soon as a change occurs. 84 | args.push('-w'); 85 | if (settings.lint) { 86 | args.push('-l'); 87 | } 88 | if (settings.require) { 89 | args.push('-r'); 90 | args.push(settings.require); 91 | } 92 | // Compile the JavaScript without the top-level function 93 | // safety wrapper. (Used for CoffeeScript as a Node.js module.) 94 | args.push('-b'); 95 | if (settings.output) { 96 | args.push('-o'); 97 | args.push(enrichFiles(settings.output)); 98 | } 99 | if (!settings.compile) { 100 | settings.compile = settings.workspace; 101 | } 102 | if (settings.compile) { 103 | args.push('-c'); 104 | args.push(enrichFiles(settings.compile)); 105 | } 106 | return cmd = 'coffee ' + args.join(' '); 107 | }; 108 | settings.cmd = cmd(); 109 | // Register commands 110 | shell.cmd('coffee start', 'Start CoffeeScript', function(req, res, next) { 111 | return start_stop.start(settings, function(err, pid) { 112 | var message; 113 | if (err) { 114 | return next(err); 115 | } 116 | if (!pid) { 117 | return res.cyan('Already Started').ln(); 118 | } 119 | message = "CoffeeScript started"; 120 | res.cyan(message).ln(); 121 | return res.prompt(); 122 | }); 123 | }); 124 | return shell.cmd('coffee stop', 'Stop CoffeeScript', function(req, res, next) { 125 | return start_stop.stop(settings, function(err, success) { 126 | if (success) { 127 | res.cyan('CoffeeScript successfully stoped').ln(); 128 | } else { 129 | res.magenta('CoffeeScript was not started').ln(); 130 | } 131 | return res.prompt(); 132 | }); 133 | }); 134 | }; 135 | -------------------------------------------------------------------------------- /lib/plugins/completer.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | /* 3 | 4 | Completer plugin 5 | ================ 6 | 7 | Provides tab completion. Options passed during creation are: 8 | 9 | - `shell` , (required) A reference to your shell application. 10 | 11 | */ 12 | module.exports = function(settings) { 13 | var shell; 14 | if (!settings.shell) { 15 | // Validation 16 | throw new Error('No shell provided'); 17 | } 18 | shell = settings.shell; 19 | // Plug completer to interface 20 | if (!shell.isShell) { 21 | return; 22 | } 23 | shell.interface().completer = function(text, cb) { 24 | var command, i, len, route, routes, suggestions; 25 | suggestions = []; 26 | routes = shell.routes; 27 | for (i = 0, len = routes.length; i < len; i++) { 28 | route = routes[i]; 29 | command = route.command; 30 | if (command.substr(0, text.length) === text) { 31 | suggestions.push(command); 32 | } 33 | } 34 | return cb(false, [suggestions, text]); 35 | }; 36 | return null; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/plugins/error.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | module.exports = function(settings) { 3 | var shell; 4 | if (!settings.shell) { 5 | // Validation 6 | throw new Error('No shell provided'); 7 | } 8 | shell = settings.shell; 9 | // Define empty error handler to avoir shell to trow error of no event 10 | // handler are defined 11 | shell.on('error', function() {}); 12 | // Route 13 | return function(err, req, res, next) { 14 | var k, v; 15 | if (err.message) { 16 | res.red(err.message).ln(); 17 | } 18 | if (err.stack) { 19 | res.red(err.stack).ln(); 20 | } 21 | for (k in err) { 22 | v = err[k]; 23 | if (k === 'message') { 24 | continue; 25 | } 26 | if (k === 'stack') { 27 | continue; 28 | } 29 | if (typeof v === 'function') { 30 | continue; 31 | } 32 | res.magenta(k).white(': ').red(v).ln(); 33 | } 34 | return res.prompt(); 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /lib/plugins/help.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var pad; 3 | 4 | pad = require('pad'); 5 | 6 | /* 7 | 8 | Help Plugin 9 | ----------- 10 | 11 | Display help when the user types "help" or runs commands without arguments. 12 | Command help is only displayed if a description was provided during the 13 | command registration. Additionnaly, a new `shell.help()` function is made available. 14 | 15 | Options passed during creation are: 16 | 17 | - `shell` , (required) A reference to your shell application. 18 | - `introduction` , Print message 'Type "help" or press enter for a list of commands' if boolean `true`, or a custom message if a `string` 19 | 20 | Usage 21 | 22 | app = shell() 23 | app.configure -> 24 | app.use shell.router shell: app 25 | app.use shell.help 26 | shell: app 27 | introduction: true 28 | 29 | */ 30 | module.exports = function(settings) { 31 | var shell, text; 32 | if (!settings.shell) { 33 | // Validation 34 | throw new Error('No shell provided'); 35 | } 36 | shell = settings.shell; 37 | // Register function 38 | shell.help = function(req, res, next) { 39 | var i, len, route, routes, text; 40 | res.cyan('Available commands:'); 41 | res.ln(); 42 | routes = shell.routes; 43 | for (i = 0, len = routes.length; i < len; i++) { 44 | route = routes[i]; 45 | text = pad(route.command, 20); 46 | if (route.description) { 47 | res.cyan(text).white(route.description).ln(); 48 | } 49 | } 50 | return res.prompt(); 51 | }; 52 | // Register commands 53 | shell.cmd('help', 'Show this message', shell.help.bind(shell)); 54 | shell.cmd('', shell.help.bind(shell)); 55 | // Print introduction message 56 | if (shell.isShell && settings.introduction) { 57 | text = typeof settings.introduction === 'string' ? settings.introduction : 'Type "help" or press enter for a list of commands'; 58 | return shell.styles.println(text); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /lib/plugins/history.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var Interface, crypto, fs, hash; 3 | 4 | fs = require('fs'); 5 | 6 | crypto = require('crypto'); 7 | 8 | Interface = require('readline').Interface; 9 | 10 | hash = function(value) { 11 | return crypto.createHash('md5').update(value).digest('hex'); 12 | }; 13 | 14 | /* 15 | 16 | History plugin 17 | ============== 18 | 19 | Persistent command history over multiple sessions. Options passed during creation are: 20 | 21 | - `shell` , (required) A reference to your shell application. 22 | - `name` , Identify your project history file, default to the hash of the exectuted file 23 | - `dir` , Location of the history files, defaults to `"#{process.env['HOME']}/.node_shell"` 24 | 25 | */ 26 | module.exports = function(settings) { 27 | var e, file, json, shell, stream; 28 | if (!settings.shell) { 29 | // Validation 30 | throw new Error('No shell provided'); 31 | } 32 | shell = settings.shell; 33 | // Only in shell mode 34 | if (!settings.shell.isShell) { 35 | return; 36 | } 37 | // Persist readline history 38 | if (settings.dir == null) { 39 | settings.dir = `${process.env['HOME']}/.node_shell`; 40 | } 41 | if (settings.name == null) { 42 | settings.name = hash(process.argv[1]); 43 | } 44 | file = `${settings.dir}/${settings.file}`; 45 | if (!fs.existsSync(settings.dir)) { 46 | // Create store directory 47 | fs.mkdirSync(settings.dir, 0o0700); 48 | } 49 | // Look for previous history 50 | if (fs.existsSync(file)) { 51 | try { 52 | json = fs.readFileSync(file, 'utf8') || '[]'; 53 | settings.shell.interface().history = JSON.parse(json); 54 | } catch (error) { 55 | e = error; 56 | settings.shell.styles.red('Corrupted history file').ln(); 57 | } 58 | } 59 | // Write new history 60 | stream = fs.createWriteStream(file, { 61 | flag: 'w' 62 | }); 63 | Interface.prototype._addHistory = (function(parent) { 64 | return function() { 65 | var buffer; 66 | if (this.history.length) { 67 | buffer = Buffer.from(JSON.stringify(this.history), 'utf8'); 68 | fs.writeSync(stream.fd, buffer, 0, buffer.length, 0); 69 | } 70 | return parent.apply(this, arguments); 71 | }; 72 | })(Interface.prototype._addHistory); 73 | return null; 74 | }; 75 | -------------------------------------------------------------------------------- /lib/plugins/http.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var existsSync, fs, path, start_stop; 3 | 4 | fs = require('fs'); 5 | 6 | path = require('path'); 7 | 8 | existsSync = fs.existsSync || path.existsSync; 9 | 10 | start_stop = require('../start_stop'); 11 | 12 | /* 13 | 14 | HTTP server 15 | =========== 16 | 17 | Register two commands, `http start` and `http stop`. The start command will 18 | search for "./server.js" and "./app.js" (and additionnaly their CoffeeScript 19 | alternatives) to run by `node`. 20 | 21 | The following properties may be provided as settings: 22 | 23 | - `message_start` Message to display once the server is started 24 | - `message_stop` Message to display once the server is stoped 25 | - `workspace` Project directory used to resolve relative paths and search for "server" and "app" scripts. 26 | - `cmd` Command to start the server, not required if path is provided or if the script is discoverable 27 | - `path` Path to the js/coffee script starting the process, may be relative to the workspace, extension isn't required. 28 | 29 | Properties derived from the start_stop utility: 30 | 31 | - `detached` Wether the HTTP process should be attached to the current process. If not defined, default to `false` (the server doesn't run as a daemon). 32 | - `pidfile` Path to the file storing the daemon process id. Defaults to `"/.node_shell/#{md5}.pid"` 33 | - `stdout` Writable stream or file path to redirect the server stdout. 34 | - `stderr` Writable stream or file path to redirect the server stderr. 35 | 36 | Example: 37 | 38 | ```javascript 39 | var app = new shell(); 40 | app.configure(function() { 41 | app.use(shell.router({ 42 | shell: app 43 | })); 44 | app.use(shell.http({ 45 | shell: app 46 | })); 47 | app.use(shell.help({ 48 | shell: app, 49 | introduction: true 50 | })); 51 | }); 52 | ``` 53 | 54 | */ 55 | module.exports = function() { 56 | var cmd, http, route, settings; 57 | settings = {}; 58 | cmd = function() { 59 | var i, len, search, searchs; 60 | searchs = settings.path ? [settings.path] : ['app', 'server', 'lib/app', 'lib/server']; 61 | for (i = 0, len = searchs.length; i < len; i++) { 62 | search = searchs[i]; 63 | search = path.resolve(settings.workspace, search); 64 | if (existsSync(`${search}`)) { 65 | if (search.substr(-4) === '.coffee') { 66 | return `coffee ${search}`; 67 | } else { 68 | return `node ${search}`; 69 | } 70 | } 71 | if (existsSync(`${search}.js`)) { 72 | return `node ${search}.js`; 73 | } else if (existsSync(`${search}.coffee`)) { 74 | return `coffee ${search}.coffee`; 75 | } 76 | } 77 | throw new Error('Failed to discover a "server.js" or "app.js" file'); 78 | }; 79 | http = null; 80 | // Register commands 81 | route = function(req, res, next) { 82 | var app; 83 | app = req.shell; 84 | if (app.tmp.http) { 85 | // Caching 86 | return next(); 87 | } 88 | app.tmp.http = true; 89 | // Workspace settings 90 | if (settings.workspace == null) { 91 | settings.workspace = app.set('workspace'); 92 | } 93 | if (!settings.workspace) { 94 | throw new Error('No workspace provided'); 95 | } 96 | // Messages 97 | if (settings.message_start == null) { 98 | settings.message_start = 'HTTP server successfully started'; 99 | } 100 | if (settings.message_stop == null) { 101 | settings.message_stop = 'HTTP server successfully stopped'; 102 | } 103 | if (!settings.cmd) { 104 | settings.cmd = cmd(); 105 | } 106 | app.cmd('http start', 'Start HTTP server', function(req, res, next) { 107 | return http = start_stop.start(settings, function(err, pid) { 108 | if (err) { 109 | return next(err); 110 | } 111 | if (!pid) { 112 | return res.cyan('HTTP server already started').ln() && res.prompt(); 113 | } 114 | res.cyan(settings.message_start).ln(); 115 | return res.prompt(); 116 | }); 117 | }); 118 | app.cmd('http stop', 'Stop HTTP server', function(req, res, next) { 119 | return start_stop.stop(settings, function(err, success) { 120 | if (success) { 121 | res.cyan(settings.message_stop).ln(); 122 | } else { 123 | res.magenta('HTTP server was not started').ln(); 124 | } 125 | return res.prompt(); 126 | }); 127 | }); 128 | return next(); 129 | }; 130 | if (arguments.length === 1) { 131 | settings = arguments[0]; 132 | return route; 133 | } else { 134 | return route.apply(null, arguments); 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /lib/plugins/redis.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var start_stop; 3 | 4 | start_stop = require('../start_stop'); 5 | 6 | /* 7 | Redis Plugin 8 | ============ 9 | 10 | Register two commands, `redis start` and `redis stop`. The following properties may be provided as settings: 11 | 12 | - `config` , Path to the configuration file. Required to launch redis. 13 | - `detached` , Wether the Redis process should be attached to the current process. If not defined, default to `false` (the server doesn't run as a daemon). 14 | - `pidfile` , Path to the file storing the daemon process id. Defaults to `"/.node_shell/#{md5}.pid"` 15 | - `stdout` , Writable stream or file path to redirect cloud9 stdout. 16 | - `stderr` , Writable stream or file path to redirect cloud9 stderr. 17 | 18 | Example: 19 | 20 | ```javascript 21 | var app = shell(); 22 | app.configure(function() { 23 | app.use(shell.router({ 24 | shell: app 25 | })); 26 | app.use(shell.redis({ 27 | shell: app, 28 | config: __dirname+'/redis.conf') 29 | })); 30 | app.use(shell.help({ 31 | shell: app, 32 | introduction: true 33 | })); 34 | }); 35 | ``` 36 | */ 37 | module.exports = function() { 38 | var redis, route, settings; 39 | settings = {}; 40 | // Register commands 41 | redis = null; 42 | route = function(req, res, next) { 43 | var app; 44 | app = req.shell; 45 | if (app.tmp.redis) { 46 | // Caching 47 | return next(); 48 | } 49 | app.tmp.redis = true; 50 | // Default settings 51 | if (settings.workspace == null) { 52 | settings.workspace = app.set('workspace'); 53 | } 54 | if (settings.config == null) { 55 | settings.config = ''; 56 | } 57 | settings.cmd = `redis-server ${settings.config}`; 58 | app.cmd('redis start', 'Start Redis', function(req, res, next) { 59 | // Launch process 60 | return redis = start_stop.start(settings, function(err, pid) { 61 | if (err) { 62 | return next(err); 63 | } 64 | if (!pid) { 65 | res.cyan('Redis already started').ln(); 66 | return res.prompt(); 67 | } 68 | res.cyan('Redis started').ln(); 69 | return res.prompt(); 70 | }); 71 | }); 72 | app.cmd('redis stop', 'Stop Redis', function(req, res, next) { 73 | return start_stop.stop(settings, function(err, success) { 74 | if (success) { 75 | res.cyan('Redis successfully stoped').ln(); 76 | } else { 77 | res.magenta('Redis was not started').ln(); 78 | } 79 | return res.prompt(); 80 | }); 81 | }); 82 | return next(); 83 | }; 84 | if (arguments.length === 1) { 85 | settings = arguments[0]; 86 | return route; 87 | } else { 88 | return route.apply(null, arguments); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /lib/plugins/router.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var match, normalize, querystring, utils, 3 | indexOf = [].indexOf; 4 | 5 | utils = require('../utils'); 6 | 7 | querystring = { 8 | unescape: function(str) { 9 | return decodeURIComponent(str); 10 | }, 11 | parse: function(qs, sep, eq) { 12 | var k, kvp, l, len, obj, ref, v, vkps, x; 13 | sep = sep || '&'; 14 | eq = eq || '='; 15 | obj = {}; 16 | if (typeof qs !== 'string') { 17 | return obj; 18 | } 19 | vkps = qs.split(sep); 20 | for (l = 0, len = vkps.length; l < len; l++) { 21 | kvp = vkps[l]; 22 | x = kvp.split(eq); 23 | k = querystring.unescape(x[0], true); 24 | v = querystring.unescape(x.slice(1).join(eq), true); 25 | if (ref = !k, indexOf.call(obj, ref) >= 0) { 26 | obj[k] = v; 27 | } else if (!Array.isArray(obj[k])) { 28 | obj[k] = [obj[k], v]; 29 | } else { 30 | obj[k].push(v); 31 | } 32 | } 33 | return obj; 34 | } 35 | }; 36 | 37 | // produce regular expression from string 38 | normalize = function(command, keys, sensitive) { 39 | // regexp factors: 40 | // 0. match a literal ':' 41 | // 1. 'key': match 1 or more word characters followed by : 42 | // 2. 'format': match anything inside (), should be a regexp factor ie ([0-9]+) 43 | // 3. 'optional': match an optional literal '?' 44 | command = command.concat('/?').replace(/\/\(/g, '(?:/').replace(/:(\w+)(\(.*\))?(\?)?/g, function(_, key, format, optional) { 45 | keys.push(key); 46 | format = format || '([^ ]+)'; // provide default format 47 | optional = optional || ''; 48 | return format + optional; 49 | }).replace(/([\/.])/g, '\\$1').replace(/\*/g, '(.+)'); 50 | return new RegExp('^' + command + '$', (sensitive != null ? 'i' : void 0)); 51 | }; 52 | 53 | match = function(req, routes, i) { 54 | var captures, index, j, key, keys, regexp, route, val; 55 | //from ?= 0 56 | //to = routes.length - 1 57 | //for (len = routes.length; i < len; ++i) { 58 | //for i in [from .. to] 59 | if (i == null) { 60 | i = 0; 61 | } 62 | while (i < routes.length) { 63 | route = routes[i]; 64 | //fn = route.callback 65 | regexp = route.regexp; 66 | keys = route.keys; 67 | captures = regexp.exec(req.command); 68 | if (captures) { 69 | route.params = {}; 70 | index = 0; 71 | //for (j = 1, len = captures.length; j < len; ++j) { 72 | //for j in [1 .. captures.length] 73 | j = 1; 74 | while (j < captures.length) { 75 | key = keys[j - 1]; 76 | val = typeof captures[j] === 'string' ? querystring.unescape(captures[j]) : captures[j]; 77 | if (key) { 78 | route.params[key] = val; 79 | } else { 80 | route.params['' + index] = val; 81 | index++; 82 | } 83 | j++; 84 | } 85 | req._route_index = i; 86 | return route; 87 | } 88 | i++; 89 | } 90 | return null; 91 | }; 92 | 93 | module.exports = function(settings) { 94 | var params, routes, shell; 95 | if (!settings.shell) { 96 | // Validation 97 | throw new Error('No shell provided'); 98 | } 99 | shell = settings.shell; 100 | if (settings.sensitive == null) { 101 | settings.sensitive = true; 102 | } 103 | // Expose routes 104 | routes = shell.routes = []; 105 | params = {}; 106 | shell.param = function(name, fn) { 107 | if (Array.isArray(name)) { 108 | name.forEach(function(name) { 109 | return this.param(name, fn); 110 | }, this); 111 | } else { 112 | if (':' === name[0]) { 113 | name = name.substr(1); 114 | } 115 | params[name] = fn; 116 | } 117 | return this; 118 | }; 119 | shell.cmd = function(command, description, middleware1, middleware2, fn) { 120 | var args, keys, route; 121 | args = Array.prototype.slice.call(arguments); 122 | route = {}; 123 | route.command = args.shift(); 124 | if (typeof args[0] === 'string') { 125 | route.description = args.shift(); 126 | } 127 | route.middlewares = utils.flatten(args); 128 | keys = []; 129 | route.regexp = route.command instanceof RegExp ? route.command : normalize(route.command, keys, settings.sensitive); 130 | route.keys = keys; 131 | routes.push(route); 132 | return this; 133 | }; 134 | // Register 'quit' command 135 | shell.cmd('quit', 'Exit this shell', shell.quit.bind(shell)); 136 | // middleware 137 | return function(req, res, next) { 138 | var i, pass, route, self; 139 | route = null; 140 | self = this; 141 | i = 0; 142 | pass = function(i) { 143 | var keys, param; 144 | route = match(req, routes, i); 145 | if (!route) { 146 | return next(); 147 | } 148 | i = 0; 149 | keys = route.keys; 150 | req.params = route.params; 151 | // Param preconditions 152 | // From expresso guide: There are times when we may want to "skip" passed 153 | // remaining route middleware, but continue matching subsequent routes. To 154 | // do this we invoke `next()` with the string "route" `next('route')`. If no 155 | // remaining routes match the request url then Express will respond with 404 Not Found. 156 | param = function(err) { 157 | var fn, key, nextMiddleware, val; 158 | try { 159 | key = keys[i++]; 160 | val = req.params[key]; 161 | fn = params[key]; 162 | if ('route' === err) { 163 | return pass(req._route_index + 1); 164 | // Error 165 | } else if (err) { 166 | return next(err); 167 | // Param has callback 168 | } else if (fn) { 169 | // Return style 170 | if (1 === fn.length) { 171 | req.params[key] = fn(val); 172 | return param(); 173 | } else { 174 | // Middleware style 175 | return fn(req, res, param, val); 176 | } 177 | // Finished processing params 178 | } else if (!key) { 179 | // route middleware 180 | i = 0; 181 | nextMiddleware = function(err) { 182 | fn = route.middlewares[i++]; 183 | if ('route' === err) { 184 | return pass(req._route_index + 1); 185 | } else if (err) { 186 | return next(err); 187 | } else if (fn) { 188 | return fn(req, res, nextMiddleware); 189 | } else { 190 | return pass(req._route_index + 1); 191 | } 192 | }; 193 | //route.callback.call self, req, res, (err) -> 194 | //if err 195 | //next err 196 | //else 197 | //pass req._route_index + 1 198 | return nextMiddleware(); 199 | } else { 200 | // More params 201 | return param(); 202 | } 203 | } catch (error) { 204 | err = error; 205 | return next(err); 206 | } 207 | }; 208 | return param(); 209 | }; 210 | return pass(); 211 | }; 212 | }; 213 | -------------------------------------------------------------------------------- /lib/plugins/stylus.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var enrichFiles, path, start_stop; 3 | 4 | path = require('path'); 5 | 6 | start_stop = require('../start_stop'); 7 | 8 | // Sanitize a list of files separated by spaces 9 | enrichFiles = function(files) { 10 | return files.split(' ').map(function(file) { 11 | path.normalize(file); 12 | // Stylus doesn't like trailing `/` in the use option 13 | if (file.substr(-1, 1) === '/') { 14 | file = file.substr(0, file.length - 1); 15 | } 16 | return file; 17 | }).join(' '); 18 | }; 19 | 20 | /* 21 | 22 | Stylus plugin 23 | ------------- 24 | Start/stop a daemon to watch and convert stylus files to css. 25 | 26 | Options include: 27 | * `output` Output to when passing files. 28 | * `input` Add to lookup paths 29 | 30 | */ 31 | module.exports = function(settings = {}) { 32 | var cmd, shell; 33 | if (!settings.shell) { 34 | // Validation 35 | throw new Error('No shell provided'); 36 | } 37 | shell = settings.shell; 38 | // Default settings 39 | if (settings.workspace == null) { 40 | settings.workspace = shell.set('workspace'); 41 | } 42 | if (!settings.workspace) { 43 | throw new Error('No workspace provided'); 44 | } 45 | cmd = function() { 46 | var args; 47 | args = []; 48 | // Watch the modification times of the coffee-scripts, 49 | // recompiling as soon as a change occurs. 50 | args.push('-w'); 51 | if (settings.use) { 52 | args.push('-u'); 53 | args.push(enrichFiles(settings.use)); 54 | } 55 | if (settings.output) { 56 | args.push('-o'); 57 | args.push(enrichFiles(settings.output)); 58 | } 59 | if (!settings.input) { 60 | settings.input = settings.workspace; 61 | } 62 | if (settings.input) { 63 | args.push(enrichFiles(settings.input)); 64 | } 65 | return cmd = 'stylus ' + args.join(' '); 66 | }; 67 | settings.cmd = cmd(); 68 | //console.log settings.cmd 69 | // Register commands 70 | shell.cmd('stylus start', 'Start CoffeeScript', function(req, res, next) { 71 | return start_stop.start(settings, function(err, pid) { 72 | var message; 73 | if (err) { 74 | return next(err); 75 | } 76 | if (!pid) { 77 | return res.cyan('Already Started').ln(); 78 | } 79 | message = "Stylus started"; 80 | res.cyan(message).ln(); 81 | return res.prompt(); 82 | }); 83 | }); 84 | return shell.cmd('stylus stop', 'Stop Stylus', function(req, res, next) { 85 | return start_stop.stop(settings, function(err, success) { 86 | if (success) { 87 | res.cyan('Stylus successfully stoped').ln(); 88 | } else { 89 | res.magenta('Stylus was not started').ln(); 90 | } 91 | return res.prompt(); 92 | }); 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /lib/plugins/test.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var exec, existsSync, fs, path; 3 | 4 | fs = require('fs'); 5 | 6 | path = require('path'); 7 | 8 | existsSync = fs.existsSync || path.existsSync; 9 | 10 | exec = require('child_process').exec; 11 | 12 | module.exports = function(settings) { 13 | var shell; 14 | if (!settings.shell) { 15 | // Validation 16 | throw new Error('No shell provided'); 17 | } 18 | shell = settings.shell; 19 | // Default settings 20 | if (settings.workspace == null) { 21 | settings.workspace = shell.set('workspace'); 22 | } 23 | if (!settings.workspace) { 24 | throw new Error('No workspace provided'); 25 | } 26 | if (settings.glob == null) { 27 | settings.glob = 'test/*.js'; 28 | } 29 | // Register commands 30 | shell.cmd('test', 'Run all test', function(req, res, next) { 31 | var i, len, p, paths, run; 32 | run = function(cmd) { 33 | var args, expresso; 34 | args = []; 35 | args.push(cmd); 36 | if (settings.coverage) { 37 | args.push('--cov'); 38 | } 39 | if (settings.serial) { 40 | args.push('--serial'); 41 | } 42 | if (settings.glob) { 43 | args.push(settings.glob); 44 | } 45 | expresso = exec('cd ' + settings.workspace + ' && ' + args.join(' ')); 46 | expresso.stdout.on('data', function(data) { 47 | return res.cyan(data); 48 | }); 49 | expresso.stderr.on('data', function(data) { 50 | return res.magenta(data); 51 | }); 52 | return expresso.on('exit', function(code) { 53 | return res.prompt(); 54 | }); 55 | }; 56 | paths = [].concat(module.paths, require.paths); 57 | for (i = 0, len = paths.length; i < len; i++) { 58 | p = paths[i]; 59 | if (existsSync(p + '/expresso/bin/expresso')) { 60 | return run(p); 61 | } 62 | } 63 | res.magenta('Expresso not found').ln(); 64 | return res.prompt(); 65 | }); 66 | return shell.cmd('test :pattern', 'Run specific tests', function(req, res, next) {}); 67 | }; 68 | 69 | //todo 70 | -------------------------------------------------------------------------------- /lib/routes/confirm.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | /* 3 | 4 | Confirm route 5 | ============= 6 | 7 | The `confirm` route ask the user if he want to continue the process. If the answer is `true`, the following routes are executed. Otherwise, the process is stoped. 8 | 9 | ```javascript 10 | var app = new shell(); 11 | app.configure(function() { 12 | app.use(shell.router({ 13 | shell: app 14 | })); 15 | }); 16 | app.cmd('install', [ 17 | shell.routes.confirm('Do you confirm?'), 18 | my_app.routes.download, 19 | my_app.routes.configure 20 | ]); 21 | ``` 22 | 23 | */ 24 | module.exports = function(message) { 25 | return function(req, res, next) { 26 | return req.confirm(message, true, function(confirmed) { 27 | if (!confirmed) { 28 | return res.prompt(); 29 | } 30 | return next(); 31 | }); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/routes/prompt.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | /* 3 | 4 | Prompt route 5 | ============ 6 | 7 | The `prompt` route is a convenient function to stop command once a few routes are executed. You can simply pass the the `shell.routes.prompt` function or call it with a message argument. 8 | 9 | ```javascript 10 | var app = new shell(); 11 | app.configure(function() { 12 | app.use(shell.router({ 13 | shell: app 14 | })); 15 | }); 16 | app.cmd('install', [ 17 | my_app.routes.download, 18 | my_app.routes.configure, 19 | shell.routes.prompt('Installation is finished') 20 | ]); 21 | ``` 22 | 23 | */ 24 | module.exports = function(req, res, next) { 25 | var message; 26 | if (arguments.length === 1) { 27 | message = arguments[0]; 28 | return function(req, res, next) { 29 | res.white(message); 30 | res.ln(); 31 | return res.prompt(); 32 | }; 33 | } else { 34 | return res.prompt(); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /lib/routes/shellOnly.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | /* 3 | 4 | `routes.shellOnly` 5 | ================== 6 | 7 | Ensure the current process is running in shell mode. 8 | 9 | */ 10 | module.exports = function(req, res, next) { 11 | if (!req.shell.isShell) { 12 | res.red('Command may only be executed inside a running shell'); 13 | res.prompt(); 14 | return; 15 | } 16 | return next(); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/routes/timeout.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | /* 3 | 4 | Timeout route 5 | ============= 6 | 7 | The `timeout` route will wait for the provided period (in millisenconds) before executing the following route. 8 | 9 | ```javascript 10 | var app = new shell(); 11 | app.configure(function() { 12 | app.use(shell.router({ 13 | shell: app 14 | })); 15 | }); 16 | app.cmd('restart', [ 17 | my_app.routes.stop, 18 | shell.routes.timeout(1000), 19 | my_app.routes.start 20 | ]); 21 | ``` 22 | 23 | */ 24 | module.exports = function(timeout) { 25 | return function(req, res, next) { 26 | return setTimeout(timeout, next); 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /lib/start_stop.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var crypto, exec, exists, fs, md5, path, spawn, start_stop; 3 | 4 | crypto = require('crypto'); 5 | 6 | ({exec, spawn} = require('child_process')); 7 | 8 | fs = require('fs'); 9 | 10 | path = require('path'); 11 | 12 | exists = fs.exists || path.exists; 13 | 14 | md5 = function(cmd) { 15 | return crypto.createHash('md5').update(cmd).digest('hex'); 16 | }; 17 | 18 | /* 19 | `start_stop`: Unix process management 20 | ------------------------------------- 21 | 22 | The library start and stop unix child process. Process are by default 23 | daemonized and will keep running even if your current process exit. For 24 | conveniency, they may also be attached to the current process by 25 | providing the `attach` option. 26 | 27 | */ 28 | module.exports = start_stop = { 29 | /* 30 | 31 | `start(options, callback)` 32 | -------------------------- 33 | Start a prcess as a daemon (default) or as a child of the current process. Options includes 34 | all the options of the "child_process.exec" function plus a few specific ones. 35 | 36 | `options` , Object with the following properties: 37 | * `cmd` , Command to run 38 | * `cwd` , Current working directory of the child process 39 | * `detached` , Detached the child process from the current process 40 | * `pidfile` , Path to the file storing the child pid 41 | * `stdout` , Path to the file where standard output is redirected 42 | * `stderr` , Path to the file where standard error is redirected 43 | * `strict` , Send an error when a pid file exists and reference 44 | an unrunning pid. 45 | * `watch` , Watch for file changes 46 | * `watchIgnore` , List of ignore files 47 | 48 | `callback` , Received arguments are: 49 | * `err` , Error if any 50 | * `pid` , Process id of the new child 51 | 52 | */ 53 | start: function(options, callback) { 54 | var c, check_pid, child, cmdStderr, cmdStdout, start, stderr, stdout, watch; 55 | if (options.attach != null) { 56 | console.log('Option attach was renamed to attached to be consistent with the new spawn option'); 57 | options.detached = !options.attach; 58 | } 59 | if (options.detached) { 60 | child = null; 61 | cmdStdout = typeof options.stdout === 'string' ? options.stdout : '/dev/null'; 62 | cmdStderr = typeof options.stderr === 'string' ? options.stderr : '/dev/null'; 63 | check_pid = function() { 64 | return start_stop.pid(options, function(err, pid) { 65 | if (!pid) { 66 | return watch(); 67 | } 68 | return start_stop.running(pid, function(err, pid) { 69 | if (pid) { 70 | return callback(new Error(`Pid ${pid} already running`)); 71 | } 72 | // Pid file reference an unrunning process 73 | if (options.strict) { 74 | return callback(new Error("Pid file reference a dead process")); 75 | } else { 76 | return watch(); 77 | } 78 | }); 79 | }); 80 | }; 81 | watch = function() { 82 | var ignore, ioptions; 83 | if (!options.watch) { 84 | return start(); 85 | } 86 | if (typeof options.watch !== 'string') { 87 | options.watch = options.cwd || process.cwd; 88 | } 89 | ioptions = { 90 | path: options.watch, 91 | ignoreFiles: [".startstopignore"] || options.watchIgnoreFiles 92 | }; 93 | ignore = require('fstream-ignore'); 94 | ignore(ioptions).on('child', function(c) { 95 | // c.on 'ignoreFile', (path, content) -> 96 | // console.log 'ignore', path, content.toString() 97 | return fs.watchFile(c.path, function(curr, prev) { 98 | console.log(c.path); 99 | return start_stop.stop(options, function(e) { 100 | return start_stop.start(options, function(e) { 101 | return console.log('restarted', e); 102 | }); 103 | }); 104 | }); 105 | }); 106 | // a file has changed, restart the child process 107 | // child.kill('SIGHUP') 108 | // child.on 'exit', (code, signal) -> 109 | // console.log('child process terminated due to receipt of signal '+signal) 110 | // start() 111 | // .on 'ignoreFile', (path, content) -> 112 | // console.log 'ignore', path, content.toString() 113 | return start(); 114 | }; 115 | // Start the process 116 | start = function() { 117 | var piddir; 118 | piddir = path.dirname(options.pidfile); 119 | return exists(piddir, function(exists) { 120 | var cmd, info, pipe; 121 | if (!exists) { 122 | return callback(new Error(`Pid directory does not exist: ${piddir}.`)); 123 | } 124 | pipe = `${cmdStdout} 2>${cmdStdout}`; 125 | info = 'echo $? $!'; 126 | cmd = `${options.cmd} ${pipe} & ${info}`; 127 | return child = exec(cmd, options, function(err, stdout, stderr) { 128 | var code, msg, pid; 129 | [code, pid] = stdout.split(' '); 130 | code = parseInt(code, 10); 131 | pid = parseInt(pid, 10); 132 | if (code !== 0) { 133 | msg = `Process exit with code ${code}`; 134 | return callback(new Error(msg)); 135 | } 136 | return fs.writeFile(options.pidfile, '' + pid, function(err) { 137 | return callback(null, pid); 138 | }); 139 | }); 140 | }); 141 | }; 142 | // Do the job 143 | return check_pid(); // Kill child on exit if started in attached mode 144 | } else { 145 | c = exec(options.cmd); 146 | if (typeof options.stdout === 'string') { 147 | stdout = fs.createWriteStream(options.stdout); 148 | } else if (options.stdout !== null && typeof options.stdout === 'object') { 149 | stdout = options.stdout; 150 | } else { 151 | stdout = null; 152 | } 153 | if (typeof options.stderr === 'string') { 154 | stdout = fs.createWriteStream(options.stderr); 155 | } else if (options.stderr !== null && typeof options.stderr === 'object') { 156 | stderr = options.stderr; 157 | } else { 158 | stderr = null; 159 | } 160 | return process.nextTick(function() { 161 | // Block the command if not in shell and process is attached 162 | options.pid = c.pid; 163 | return callback(null, c.pid); 164 | }); 165 | } 166 | }, 167 | /* 168 | 169 | `stop(options, callback)` 170 | ------------------------- 171 | Stop a process. In daemon mode, the pid is obtained from the `pidfile` option which, if 172 | not provided, can be guessed from the `cmd` option used to start the process. 173 | 174 | `options` , Object with the following properties: 175 | * `detached` , Detach the child process to the current process 176 | * `cmd` , Command used to run the process, in case no pidfile is provided 177 | * `pid` , Pid to kill in attach mode 178 | * `pidfile` , Path to the file storing the child pid 179 | * `strict` , Send an error when a pid file exists and reference 180 | an unrunning pid. 181 | 182 | `callback` , Received arguments are: 183 | * `err` , Error if any 184 | * `stoped` , True if the process was stoped 185 | 186 | */ 187 | stop: function(options, callback) { 188 | var kill; 189 | if (options.attach != null) { 190 | console.log('Option attach was renamed to attached to be consistent with the new spawn option'); 191 | options.detached = !options.attach; 192 | } 193 | // Stoping a provided PID 194 | if (typeof options === 'string' || typeof options === 'number') { 195 | options = { 196 | pid: parseInt(options, 10), 197 | detached: false 198 | }; 199 | } 200 | kill = function(pid) { 201 | var cmds; 202 | // Not trully recursive, potential scripts: 203 | // http://machine-cycle.blogspot.com/2009/05/recursive-kill-kill-process-tree.html 204 | // http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2004-05/1108.html 205 | cmds = `for i in \`ps -ef | awk '$3 == '${pid}' { print $2 }'\` 206 | do 207 | kill $i 208 | done 209 | kill ${pid}`; 210 | return exec(cmds, function(err, stdout, stderr) { 211 | if (err) { 212 | return callback(new Error(`Unexpected exit code ${err.code}`)); 213 | } 214 | options.pid = null; 215 | return callback(null, true); 216 | }); 217 | }; 218 | if (options.detached) { 219 | return start_stop.pid(options, function(err, pid) { 220 | if (err) { 221 | return callback(err); 222 | } 223 | if (!pid) { 224 | return callback(null, false); 225 | } 226 | return fs.unlink(options.pidfile, function(err) { 227 | if (err) { 228 | return callback(err); 229 | } 230 | return start_stop.running(pid, function(err, running) { 231 | if (!running) { 232 | if (options.strict) { 233 | return callback(new Error("Pid file reference a dead process")); 234 | } else { 235 | return callback(null, false); 236 | } 237 | } 238 | return kill(pid); 239 | }); 240 | }); 241 | }); 242 | } else { 243 | return kill(options.pid); 244 | } 245 | }, 246 | /* 247 | 248 | `pid(options, callback)` 249 | ------------------------ 250 | Retrieve a process pid. The pid value is return only if the command is running 251 | otherwise it is set to false. 252 | 253 | `options` , Object with the following properties: 254 | * `detached` , True if the child process is not attached to the current process 255 | * `cmd` , Command used to run the process, in case no pidfile is provided 256 | * `pid` , Pid to kill if not running in detached mode 257 | * `pidfile` , Path to the file storing the child pid 258 | 259 | `callback` , Received arguments are: 260 | * `err` , Error if any 261 | * `pid` , Process pid. Pid is null if there are no pid file or 262 | if the process isn't running. 263 | 264 | */ 265 | pid: function(options, callback) { 266 | if (options.attach != null) { 267 | console.log('Option attach was renamed to attached to be consistent with the new spawn option'); 268 | options.detached = !options.attach; 269 | } 270 | // Attach mode 271 | if (!options.detached) { 272 | if (options.pid == null) { 273 | return new Error('Expect a pid property in attached mode'); 274 | } 275 | return callback(null, options.pid); 276 | } 277 | // Deamon mode 278 | return start_stop.file(options, function(err, file, exists) { 279 | if (!exists) { 280 | return callback(null, false); 281 | } 282 | return fs.readFile(options.pidfile, 'ascii', function(err, pid) { 283 | if (err) { 284 | return callback(err); 285 | } 286 | pid = pid.trim(); 287 | return callback(null, pid); 288 | }); 289 | }); 290 | }, 291 | /* 292 | 293 | `file(options, callback)` 294 | ------------------------- 295 | Retrieve information relative to the file storing the pid. Retrieve 296 | the path to the file storing the pid number and whether 297 | it exists or not. Note, it will additionnaly enrich the `options` 298 | argument with a pidfile property unless already present. 299 | 300 | `options` , Object with the following properties: 301 | * `detached` , True if the child process is not attached to the current process 302 | * `cmd` , Command used to run the process, in case no pidfile is provided 303 | * `pid` , Pid to kill in attach mode 304 | * `pidfile` , Path to the file storing the child pid 305 | 306 | `callback` , Received arguments are: 307 | * `err` , Error if any 308 | * `path` , Path to the file storing the pid, null in attach mode 309 | * `exists` , True if the file is created 310 | 311 | */ 312 | file: function(options, callback) { 313 | var createDir, dir, pidFileExists, start; 314 | if (options.attach != null) { 315 | console.log('Option attach was renamed to detached to be consistent with the spawn API'); 316 | options.detached = !options.attach; 317 | } 318 | if (!options.detached) { 319 | return callback(null, null, false); 320 | } 321 | dir = path.resolve(process.env['HOME'], '.node_shell'); 322 | start = function() { 323 | var file; 324 | if (options.pidfile) { 325 | return pidFileExists(); 326 | } 327 | file = md5(options.cmd); 328 | options.pidfile = `${dir}/${file}.pid`; 329 | return exists(dir, function(dirExists) { 330 | if (!dirExists) { 331 | return createDir(); 332 | } 333 | return pidFileExists(); 334 | }); 335 | }; 336 | createDir = function() { 337 | return fs.mkdir(dir, 0o0700, function(err) { 338 | if (err) { 339 | return callback(err); 340 | } 341 | return pidFileExists(); 342 | }); 343 | }; 344 | pidFileExists = function() { 345 | return exists(options.pidfile, function(pidFileExists) { 346 | return callback(null, options.pidfile, pidFileExists); 347 | }); 348 | }; 349 | return start(); 350 | }, 351 | /* 352 | 353 | `running(pid, callback)` 354 | ------------------------ 355 | 356 | Test if a pid match a running process. 357 | 358 | `pid` , Process id to test 359 | 360 | `callback` , Received arguments are: 361 | * `err` , Error if any 362 | * `running` , True if pid match a running process 363 | 364 | */ 365 | running: function(pid, callback) { 366 | return exec(`kill -0 ${pid}`, function(err, stdout, stderr) { 367 | if (err && err.code !== 1) { 368 | return callback(err); 369 | } 370 | return callback(null, !err); 371 | }); 372 | } 373 | }; 374 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | var existsSync, fs, path; 3 | 4 | fs = require('fs'); 5 | 6 | path = require('path'); 7 | 8 | existsSync = fs.existsSync || path.existsSync; 9 | 10 | module.exports = { 11 | flatten: function(arr, ret) { 12 | var i, j, ref; 13 | if (ret == null) { 14 | ret = []; 15 | } 16 | for (i = j = 0, ref = arr.length; (0 <= ref ? j < ref : j > ref); i = 0 <= ref ? ++j : --j) { 17 | if (Array.isArray(arr[i])) { 18 | this.flatten(arr[i], ret); 19 | } else { 20 | ret.push(arr[i]); 21 | } 22 | } 23 | return ret; 24 | }, 25 | // Discovery the project root directory or return null if undiscoverable 26 | workspace: function() { 27 | var dir, dirs, j, len; 28 | //dirs = require('module')._nodeModulePaths process.cwd() 29 | dirs = require('module')._nodeModulePaths(process.argv[1]); 30 | for (j = 0, len = dirs.length; j < len; j++) { 31 | dir = dirs[j]; 32 | if (existsSync(dir) || existsSync(path.normalize(dir + '/../package.json'))) { 33 | return path.normalize(dir + '/..'); 34 | } 35 | } 36 | }, 37 | checkPort: function(port, host, callback) { 38 | var cmd; 39 | cmd = exec(`nc ${host} ${port} < /dev/null`); 40 | return cmd.on('exit', function(code) { 41 | if (code === 0) { 42 | return callback(true); 43 | } 44 | if (code === 1) { 45 | return callback(false); 46 | } 47 | return callback(new Error('The nc (or netcat) utility is required')); 48 | }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shell", 3 | "version": "0.5.1", 4 | "description": "Full features and pretty console applications", 5 | "homepage": "https://github.com/adaltas/node-shell", 6 | "license": "BSD-3-Clause", 7 | "author": "David Worms ", 8 | "contributors": [ 9 | { 10 | "name": "David Worms", 11 | "email": "david@adaltas.com" 12 | }, 13 | { 14 | "name": "Tony", 15 | "email": "https://github.com/Zearin" 16 | }, 17 | { 18 | "name": "Russ Frank", 19 | "email": "https://github.com/russfrank" 20 | } 21 | ], 22 | "engines": { 23 | "node": ">= 0.6.0" 24 | }, 25 | "dependencies": { 26 | "each": "latest", 27 | "express": "^4.18.2", 28 | "optimist": "latest", 29 | "pad": "latest" 30 | }, 31 | "optionDependency": { 32 | "express": "4.16.2" 33 | }, 34 | "devDependencies": { 35 | "coffeescript": "^2.7.0", 36 | "mocha": "latest", 37 | "should": "latest" 38 | }, 39 | "keywords": [ 40 | "cli", 41 | "console", 42 | "colors", 43 | "xterm", 44 | "args", 45 | "argument" 46 | ], 47 | "mocha": { 48 | "throw-deprecation": true, 49 | "require": [ 50 | "should", 51 | "coffeescript/register" 52 | ], 53 | "inline-diffs": true, 54 | "timeout": 40000, 55 | "reporter": "spec", 56 | "recursive": true 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "https://github.com/adaltas/node-shell.git" 61 | }, 62 | "scripts": { 63 | "preversion": "grep '## Trunk' CHANGELOG.md && npm test", 64 | "version": "version=`grep '^ \"version\": ' package.json | sed 's/.*\"\\([0-9\\.]*\\)\".*/\\1/'` && sed -i \"s/## Trunk/## Version $version/\" CHANGELOG.md && git add CHANGELOG.md", 65 | "postversion": "git push && git push --tags && npm publish", 66 | "patch": "npm version patch -m 'Bump to version %s'", 67 | "minor": "npm version minor -m 'Bump to version %s'", 68 | "major": "npm version major -m 'Bump to version %s'", 69 | "coffee": "./node_modules/.bin/coffee -b -o lib src", 70 | "pretest": "./node_modules/.bin/coffee -b -o lib src", 71 | "test": "./node_modules/.bin/mocha test/**.coffee" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /samples/cloud9/sample.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shell = require('shell'); 4 | 5 | var app = shell(); 6 | 7 | app.configure(function() { 8 | app.use(shell.history({ 9 | shell: app 10 | })); 11 | app.use(shell.cloud9({ 12 | ip: '0.0.0.0', 13 | port: '8999', 14 | stdout: __dirname+'/cloud9.out.log', 15 | stderr: __dirname+'/cloud9.err.log', 16 | pidfile: __dirname+'/cloud9.pid', 17 | detached: true 18 | })); 19 | app.use(shell.router({ 20 | shell: app 21 | })); 22 | app.use(shell.help({ 23 | shell: app, 24 | introduction: true 25 | })); 26 | }); 27 | -------------------------------------------------------------------------------- /samples/coffee/lib/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /samples/coffee/lib/hello.coffee: -------------------------------------------------------------------------------- 1 | 2 | console.log "Hi there!" 3 | -------------------------------------------------------------------------------- /samples/coffee/sample.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | shell = require 'shell' 4 | 5 | app = new shell 6 | project_dir: __dirname 7 | 8 | app.configure -> 9 | app.use shell.history 10 | shell: app 11 | app.use shell.router 12 | shell: app 13 | app.use shell.coffee 14 | shell: app 15 | stdout: __dirname + '/coffee.out.log' 16 | stderr: __dirname + '/coffee.err.log' 17 | pidfile: __dirname + '/coffee.pid' 18 | detached: true 19 | app.use shell.help 20 | shell: app 21 | introduction: true 22 | -------------------------------------------------------------------------------- /samples/error/sample.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var spawn = require('child_process').spawn, 4 | shell = require('shell'), 5 | app = new shell(); 6 | 7 | app.configure(function(){ 8 | app.use(shell.router({shell: app})); 9 | app.use(shell.help({shell: app, introduction: true})); 10 | app.use(shell.error({shell: app})); 11 | }); 12 | 13 | app.on('exit', function(){ 14 | if(app.server){ app.server.kill(); } 15 | if(app.client){ app.client.quit(); } 16 | }); 17 | 18 | app.cmd('error throw', 'Throw an error', function(req, res, next){ 19 | throw new Error('Test throw error'); 20 | }); 21 | 22 | app.cmd('error next', 'Pass an error in next', function(req, res, next){ 23 | next(new Error('Test next error')); 24 | }); -------------------------------------------------------------------------------- /samples/http/app.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express'); 3 | 4 | var app = module.exports = express.createServer(); 5 | 6 | app.configure(function(){ 7 | app.use(express.favicon()); 8 | app.use(express.methodOverride()); 9 | app.use(express.bodyParser()); 10 | app.use(express.cookieParser()); 11 | app.use(express.session({secret:'my key'})); 12 | app.use(app.router); 13 | app.use(express.errorHandler({ showStack: true, dumpExceptions: true })); 14 | }); 15 | 16 | app.get('/', function(req, res, next){ 17 | res.send('Welcome'); 18 | }); 19 | 20 | app.listen(3000); -------------------------------------------------------------------------------- /samples/http/db/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaltas/node-parameters/3ed8806f5366a94de09808d70b442e741c2a76b2/samples/http/db/.gitignore -------------------------------------------------------------------------------- /samples/http/logs/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaltas/node-parameters/3ed8806f5366a94de09808d70b442e741c2a76b2/samples/http/logs/.gitignore -------------------------------------------------------------------------------- /samples/http/sample.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var exec = require('child_process').exec; 5 | var spawn = require('child_process').spawn; 6 | var shell = require('shell'); 7 | 8 | process.chdir(__dirname); 9 | 10 | // App 11 | var app = new shell({ 12 | workspace: __dirname 13 | }); 14 | app.configure(function(){ 15 | app.use(shell.history({shell: app})); 16 | app.use(shell.completer({shell: app})); 17 | app.use(shell.http({ 18 | stdout: __dirname + '/logs/http.out.log', 19 | stderr: __dirname + '/logs/http.err.log', 20 | pidfile: __dirname + '/tmp/http.pid', 21 | detached: true 22 | })); 23 | app.use(shell.router({shell: app})); 24 | app.use(shell.help({shell: app, introduction: true})); 25 | app.use(shell.error({shell: app})); 26 | }); -------------------------------------------------------------------------------- /samples/http/tmp/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaltas/node-parameters/3ed8806f5366a94de09808d70b442e741c2a76b2/samples/http/tmp/.gitignore -------------------------------------------------------------------------------- /samples/params/ami.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shell = require('../..'); 4 | var exec = require('child_process').exec; 5 | 6 | var app = shell(); 7 | app.configure(function(){ 8 | app.use(shell.router({ 9 | shell: app 10 | })); 11 | }); 12 | // Route middleware 13 | var auth = function(req, res, next){ 14 | if(req.params.uid == process.getuid()){ 15 | next() 16 | }else{ 17 | throw new Error('Not me'); 18 | } 19 | } 20 | // Global parameter substitution 21 | app.param('uid', function(req, res, next){ 22 | exec('whoami', function(err, stdout, sdterr){ 23 | req.params.username = stdout; 24 | next(); 25 | }); 26 | }); 27 | // Simple command 28 | app.cmd('help', function(req, res){ 29 | res.cyan('Run this command `./ami user ' + process.getuid() + '`'); 30 | res.prompt() 31 | }); 32 | // Command with parameter and two route middlewares 33 | app.cmd('user :uid', auth, function(req, res){ 34 | res.cyan('Yes, you are ' + req.params.username); 35 | }); 36 | -------------------------------------------------------------------------------- /samples/params/sample.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shell = require('../..'), 4 | app = shell(), 5 | users = { 6 | '1': 'lulu', 7 | '2': 'toto' 8 | }; 9 | 10 | app.configure(function(){ 11 | app.use(shell.completer({shell: app})); 12 | app.use(shell.router({shell: app})); 13 | app.use(shell.help({shell: app, introduction: true})); 14 | app.use(shell.error({shell: app})); 15 | }); 16 | 17 | app.param('userIdShow', function(req, res, next){ 18 | var userId = req.params.userIdShow; 19 | var user = users[userId]; 20 | if( user ){ 21 | req.user = user; 22 | next(); 23 | }else{ 24 | next( new Error('User '+userId+' does not exist') ); 25 | } 26 | }); 27 | 28 | app.param('userIdGet', function(userId){ 29 | var user = users[userId]; 30 | if( user ){ 31 | return users[userId]; 32 | }else{ 33 | throw new Error('User does not exist') 34 | } 35 | }); 36 | 37 | app.cmd('show user :userIdShow', 'Test arity 3, example: "show user 2"', function(req, res, next){ 38 | res.cyan('User is '+req.user).ln(); 39 | res.prompt(); 40 | }); 41 | 42 | app.cmd('get user :userIdGet', 'Test arity 1, example: "show user 2"', function(req, res, next){ 43 | res.cyan('User is '+req.params.userIdGet).ln(); 44 | res.prompt(); 45 | }); 46 | -------------------------------------------------------------------------------- /samples/question.coffee: -------------------------------------------------------------------------------- 1 | 2 | shell = require '..' 3 | app = shell() 4 | app.configure -> 5 | app.use shell.router shell: app 6 | 7 | app.cmd 'test', (req, res, next) -> 8 | req.question 'Are you sure [yes|no]', (answer) -> 9 | console.log 'answer is: ', answer 10 | -------------------------------------------------------------------------------- /samples/redis/redis.conf: -------------------------------------------------------------------------------- 1 | 2 | daemonize no 3 | pidfile /tmp/redis.pid 4 | port 6379 5 | timeout 300 6 | loglevel debug 7 | logfile stdout 8 | databases 16 9 | save 900 1 10 | save 300 10 11 | save 60 10000 12 | rdbcompression yes 13 | dbfilename dump.rdb 14 | dir ./ 15 | slave-serve-stale-data yes 16 | appendonly no 17 | appendfsync everysec 18 | no-appendfsync-on-rewrite no 19 | diskstore-enabled no 20 | diskstore-path redis.ds 21 | cache-max-memory 0 22 | cache-flush-delay 0 23 | hash-max-zipmap-entries 512 24 | hash-max-zipmap-value 64 25 | list-max-ziplist-entries 512 26 | list-max-ziplist-value 64 27 | set-max-intset-entries 512 28 | activerehashing yes 29 | 30 | -------------------------------------------------------------------------------- /samples/redis/sample.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shell = require('shell'); 4 | // Initialization 5 | var app = new shell( { chdir: __dirname } ) 6 | // Middleware registration 7 | app.configure(function() { 8 | app.use(function(req, res, next){ 9 | app.client = require('redis').createClient() 10 | next() 11 | }); 12 | app.use(shell.history({ 13 | shell: app 14 | })); 15 | app.use(shell.completer({ 16 | shell: app 17 | })); 18 | app.use(shell.redis({ 19 | config: 'redis.conf', 20 | pidfile: 'redis.pid', 21 | detached: true 22 | })); 23 | app.use(shell.router({ 24 | shell: app 25 | })); 26 | app.use(shell.help({ 27 | shell: app, 28 | introduction: true 29 | })); 30 | }); 31 | // Command registration 32 | app.cmd('redis keys :pattern', 'Find keys', function(req, res, next){ 33 | app.client.keys(req.params.pattern, function(err, keys){ 34 | if(err){ return res.styles.red(err.message), next(); } 35 | res.cyan(keys.join('\n')||'no keys'); 36 | res.prompt(); 37 | }); 38 | }); 39 | // Event notification 40 | app.on('quit', function(){ 41 | app.client.quit(); 42 | }); 43 | -------------------------------------------------------------------------------- /samples/routes/sample.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shell = require('shell'), 4 | app = new shell(); 5 | 6 | app.configure(function(){ 7 | app.use(shell.router({shell: app})); 8 | app.use(shell.help({shell: app, introduction: true})); 9 | app.use(shell.error({shell: app})); 10 | }); 11 | 12 | app.on('exit', function(){ 13 | if(app.server){ app.server.kill(); } 14 | if(app.client){ app.client.quit(); } 15 | }); 16 | 17 | // Middlewares as multiple arguments 18 | 19 | app.cmd('multiple', 'Test multiple routes', function(req, res, next){ 20 | res.cyan('middleware 1').ln(); 21 | next(); 22 | }, function(req, res, next){ 23 | res.cyan('middleware 2').ln(); 24 | next(); 25 | }, function(req, res, next){ 26 | res.cyan('final callback').ln(); 27 | res.prompt(); 28 | }); 29 | 30 | // Middlewares as an array of arguments 31 | 32 | var routes = [ 33 | ['cmd1', 'Run command 1', function(req, res, next){ 34 | res.cyan('Running command 1').ln(); 35 | next(); 36 | }], 37 | ['cmd2', 'Run command 2', function(req, res, next){ 38 | res.cyan('Running command 2').ln(); 39 | next(); 40 | }], 41 | ['cmd3', 'Run command 3', function(req, res, next){ 42 | res.cyan('Running command 3').ln(); 43 | next(); 44 | }] 45 | ]; 46 | var middlewares = []; 47 | routes.forEach(function(route){ 48 | middlewares.push(route[2]); 49 | app.cmd.call(null, route[0], route[1], route[2], function(req, res, next){ 50 | res.cyan('Command "'+req.command+'" succeed'); 51 | res.prompt(); 52 | }); 53 | }); 54 | app.cmd('all', 'Run all command', middlewares, function(req, res, next){ 55 | res.cyan('All commands succeed'); 56 | res.prompt(); 57 | }) -------------------------------------------------------------------------------- /src/NullStream.coffee: -------------------------------------------------------------------------------- 1 | 2 | events = require 'events' 3 | 4 | module.exports = class NullStream extends events.EventEmitter 5 | # Readable Stream 6 | readable: true 7 | pause: -> 8 | resume: -> 9 | pipe: -> 10 | # Writable Stream 11 | writable: true 12 | write: (data) -> 13 | @emit 'data', data 14 | end: -> 15 | @emit 'close' 16 | # Shared API 17 | destroy: -> 18 | destroySoon: -> 19 | -------------------------------------------------------------------------------- /src/Request.coffee: -------------------------------------------------------------------------------- 1 | 2 | each = require 'each' 3 | 4 | module.exports = class Request 5 | constructor: (shell, command) -> 6 | @shell = shell 7 | @command = command 8 | ### 9 | Ask one or more questions 10 | ### 11 | question: (questions, callback) -> 12 | isObject = (v) -> typeof v is 'object' and v? and not Array.isArray v 13 | multiple = true 14 | answers = {} 15 | if isObject questions 16 | questions = for q, v of questions 17 | v ?= {} 18 | v = { value: v } unless isObject v 19 | v.name = q 20 | v 21 | else if typeof questions is 'string' 22 | multiple = false 23 | questions = [{name: questions, value: ''}] 24 | each(questions) 25 | .call (question, next) => 26 | q = "#{question.name} " 27 | q += "[#{question.value}] " if question.value 28 | @shell.interface().question q, (answer) -> 29 | answer = answer.substr(0, answer.length - 1) if answer.substr(-1, 1) is '\n' 30 | answers[question.name] = 31 | if answer is '' then question.value else answer 32 | next() 33 | .next -> 34 | answers = answers[questions[0].name] unless multiple 35 | return callback answers 36 | ### 37 | Ask a question expecting a boolean answer 38 | ### 39 | confirm: (msg, defaultTrue, callback) -> 40 | args = arguments 41 | unless callback 42 | callback = defaultTrue 43 | defaultTrue = true 44 | @shell.settings.key_true ?= 'y' 45 | @shell.settings.key_false ?= 'n' 46 | key_true = @shell.settings.key_true.toLowerCase() 47 | key_false = @shell.settings.key_false.toLowerCase() 48 | keyTrue = if defaultTrue then key_true.toUpperCase() else key_true 49 | keyFalse = if defaultTrue then key_false else key_false.toUpperCase() 50 | msg += ' ' 51 | msg += "[#{keyTrue}#{keyFalse}] " 52 | question = @shell.styles.raw( msg, {color: 'green'}) 53 | @shell.interface().question question, (answer) => 54 | accepted = ['', key_true, key_false] 55 | answer = answer.substr(0, answer.length - 1) if answer.substr(-1, 1) is '\n' 56 | answer = answer.toLowerCase() 57 | valid = accepted.indexOf(answer) isnt -1 58 | return @confirm.apply(@, args) unless valid 59 | callback answer is key_true or (defaultTrue and answer is '') 60 | -------------------------------------------------------------------------------- /src/Response.coffee: -------------------------------------------------------------------------------- 1 | 2 | styles = require './Styles' 3 | pad = require 'pad' 4 | 5 | module.exports = class Response extends styles 6 | constructor: (settings) -> 7 | super settings 8 | @shell = settings.shell 9 | pad: pad 10 | prompt: -> 11 | @shell.prompt() 12 | 13 | -------------------------------------------------------------------------------- /src/Shell.coffee: -------------------------------------------------------------------------------- 1 | 2 | util = require 'util' 3 | readline = require 'readline' 4 | events = require 'events' 5 | EventEmitter = events.EventEmitter 6 | utils = require './utils' 7 | styles = require './Styles' 8 | Request = require './Request' 9 | Response = require './Response' 10 | 11 | # Fix readline interface 12 | Interface = require('readline').Interface 13 | Interface.prototype.setPrompt = ( (parent) -> 14 | (prompt, length) -> 15 | args = Array.prototype.slice.call arguments 16 | args[1] = styles.unstyle(args[0]).length if not args[1] 17 | parent.apply @, args 18 | )( Interface.prototype.setPrompt ) 19 | 20 | module.exports = (settings) -> 21 | new Shell settings 22 | 23 | class Shell extends EventEmitter 24 | constructor: (settings = {}) -> 25 | super settings 26 | # EventEmitter.call @ 27 | @tmp = {} 28 | @settings = settings 29 | @settings.prompt ?= '>> ' 30 | @settings.stdin ?= process.stdin 31 | @settings.stdout ?= process.stdout 32 | @set 'env', @settings.env ? process.env.NODE_ENV ? 'development' 33 | @set 'command', 34 | if typeof settings.command isnt 'undefined' 35 | then settings.command 36 | else process.argv.slice(2).join(' ') 37 | @stack = [] 38 | @styles = styles {stdout: @settings.stdout} 39 | process.on 'beforeExit', => 40 | @emit 'exit' 41 | process.on 'uncaughtException', (e) => 42 | @emit 'exit', [e] 43 | @styles.red('Internal error, closing...').ln() 44 | console.error e.message 45 | console.error e.stack 46 | process.exit() 47 | @isShell = this.settings.isShell ? process.argv.length is 2 48 | @interface() if @isShell 49 | # Project root directory 50 | settings.workspace ?= utils.workspace() 51 | # Current working directory 52 | process.chdir settings.workspace if settings.chdir is true 53 | process.chdir settings.chdir if typeof settings.chdir is 'string' 54 | # Start 55 | process.nextTick => 56 | if @isShell 57 | command = @set 'command' 58 | noPrompt = @set 'noPrompt' 59 | if command 60 | @run command 61 | else if not noPrompt 62 | @prompt() 63 | else 64 | command = @set 'command' 65 | @run command if command 66 | return @ 67 | 68 | # Return the readline interface and create it if not yet initialized 69 | interface: () -> 70 | return @_interface if @_interface? 71 | @_interface = readline.createInterface @settings.stdin, @settings.stdout 72 | 73 | # Configure callback for the given `env` 74 | configure: (env, fn) -> 75 | if typeof env is 'function' 76 | fn = env 77 | env = 'all' 78 | if env is 'all' or env is @settings.env 79 | fn.call @ 80 | @ 81 | 82 | # Configure callback for the given `env` 83 | use: (handle) -> 84 | # Add the route, handle pair to the stack 85 | if handle 86 | @stack.push { route: null, handle: handle } 87 | # Allow chaining 88 | @ 89 | 90 | # Store commands 91 | cmds: {} 92 | 93 | # Run a command 94 | run: (command) -> 95 | command = command.trim() 96 | @emit 'command', [command] 97 | @emit command, [] 98 | self = @ 99 | req = new Request @, command 100 | res = new Response {shell: @, stdout: @settings.stdout} 101 | index = 0 102 | next = (err) -> 103 | layer = self.stack[ index++ ] 104 | if not layer 105 | return self.emit('error', err) if err 106 | if command isnt '' 107 | text = "Command failed to execute #{command}" 108 | text += ": #{err.message or err.name}" if err 109 | res.red text 110 | return res.prompt() 111 | arity = layer.handle.length 112 | if err 113 | if arity is 4 114 | self.emit('error', err) 115 | layer.handle err, req, res, next 116 | else 117 | next err 118 | else if arity < 4 119 | layer.handle req, res, next 120 | else 121 | next() 122 | next() 123 | 124 | set: (setting, val) -> 125 | if not val? 126 | if @settings.hasOwnProperty setting 127 | return @settings[setting] 128 | else if @parent 129 | # for the future, parent being undefined for now 130 | return @parent.set setting 131 | else 132 | @settings[setting] = val 133 | @ 134 | 135 | # Display prompt 136 | prompt: -> 137 | if @isShell 138 | text = @styles.raw( @settings.prompt, {color: 'green'}) 139 | @interface().question text, @run.bind(@) 140 | else 141 | @styles.ln() 142 | if process.versions 143 | @quit() 144 | else 145 | # Node v0.6.1 throw error 'process.stdout cannot be closed' 146 | @settings.stdout.destroySoon(); 147 | @settings.stdout.on 'close', -> 148 | process.exit() 149 | 150 | # Command quit 151 | quit: (params) -> 152 | @emit 'quit' 153 | @interface().close() 154 | @settings.stdin.destroy() 155 | #@set 'stdin', null 156 | 157 | 158 | module.exports.Shell = Shell 159 | -------------------------------------------------------------------------------- /src/Styles.coffee: -------------------------------------------------------------------------------- 1 | 2 | colors = 3 | black: 30 4 | red: 31 5 | green: 32 6 | yellow: 33 7 | blue: 34 8 | magenta: 35 9 | cyan: 36 10 | white: 37 11 | 12 | bgcolors = 13 | black: 40 14 | red: 41 15 | green: 42 16 | yellow: 43 17 | blue: 44 18 | magenta: 45 19 | cyan: 46 20 | white: 47 21 | 22 | module.exports = Styles = (settings = {}) -> 23 | if @ not instanceof Styles 24 | return new Styles settings 25 | @settings = settings 26 | @settings.stdout = settings.stdout ? process.stdout 27 | # Current state 28 | @current = 29 | weight: 'regular' 30 | # Export colors 31 | @colors = colors 32 | @bgcolors = bgcolors 33 | @ 34 | 35 | # Color 36 | Styles.prototype.color = (color, text) -> 37 | @print text, {color: color} 38 | # Save state if no text 39 | @current.color = color unless text 40 | @ 41 | 42 | for color, code of colors 43 | do (color) -> 44 | Styles.prototype[color] = (text) -> 45 | @color color, text 46 | 47 | Styles.prototype.nocolor = (text) -> 48 | @color null, text 49 | 50 | # bgcolor 51 | Styles.prototype.bgcolor = (bgcolor) -> 52 | bgcolor ?= 0 53 | @print '\x1B[' + bgcolor + ';m39' 54 | @ 55 | 56 | # Font weight 57 | Styles.prototype.weight = (weight, text) -> 58 | @print text, {weight: weight} 59 | if not text 60 | # Save state if no text 61 | @current.weight = weight 62 | @ 63 | 64 | Styles.prototype.bold = (text) -> 65 | @weight 'bold', text 66 | 67 | Styles.prototype.regular = (text) -> 68 | @weight 'regular', text 69 | 70 | # Print 71 | 72 | Styles.prototype.print = (text, settings) -> 73 | @settings.stdout.write @raw(text, settings) 74 | @ 75 | 76 | Styles.prototype.println = (text) -> 77 | @settings.stdout.write text + '\n' 78 | @ 79 | 80 | Styles.prototype.ln = -> 81 | @settings.stdout.write '\n' 82 | @ 83 | 84 | # Others 85 | 86 | Styles.prototype.raw = (text, settings) -> 87 | raw = ''; 88 | settings ?= {} 89 | if settings.color isnt null and ( settings.color or @current.color ) 90 | raw += '\x1b[' + @colors[settings.color or @current.color] + 'm' 91 | else 92 | raw += '\x1b[39m' 93 | switch settings.weight or @current.weight 94 | when 'bold' 95 | raw += '\x1b[1m' 96 | when 'regular' 97 | raw += '\x1b[22m' 98 | else 99 | throw new Error 'Invalid weight "' + weight + '" (expect "bold" or "regular")' 100 | if text 101 | # Print text if any 102 | raw += text 103 | # Restore state if any 104 | if @current.color and @current.color isnt settings.color 105 | raw += @raw null, @current.color 106 | if @current.weight and @current.weight isnt settings.weight 107 | raw += @raw null, @current.weight 108 | raw 109 | 110 | Styles.prototype.reset = (text) -> 111 | @print null, 112 | color: null 113 | weight: 'regular' 114 | 115 | # Remove style 116 | Styles.unstyle = (text) -> text.replace(/\x1b.*?m/g, '') 117 | 118 | -------------------------------------------------------------------------------- /src/plugins/cloud9.coffee: -------------------------------------------------------------------------------- 1 | 2 | start_stop = require '../start_stop' 3 | 4 | ### 5 | 6 | Cloud9 plugin 7 | ============= 8 | 9 | Register two commands, `cloud9 start` and `cloud9 stop`. Unless provided, 10 | the Cloud9 workspace will be automatically discovered if your project root 11 | directory contains a "package.json" file or a "node_module" directory. 12 | 13 | Options: 14 | 15 | - `config` , Load the configuration from a config file. Overrides command-line options. Defaults to `null`. 16 | - `group` , Run child processes with a specific group. 17 | - `user` , Run child processes as a specific user. 18 | - `action` , Define an action to execute after the Cloud9 server is started. Defaults to `null`. 19 | - `ip` , IP address where Cloud9 will serve from. Defaults to `"127.0.0.1"`. 20 | - `port` , Port number where Cloud9 will serve from. Defaults to `3000`. 21 | - `workspace`, Path to the workspace that will be loaded in Cloud9, Defaults to `Shell.set('workspace')`. 22 | - `detached` , Wether the Cloud9 process should be attached to the current process. If not defined, default to `false` (the server doesn't run as a daemon). 23 | - `pidfile` , Path to the file storing the daemon process id. Defaults to `"/.node_shell/#{md5}.pid"` 24 | - `stdout` , Writable stream or file path to redirect cloud9 stdout. 25 | - `stderr` , Writable stream or file path to redirect cloud9 stderr. 26 | 27 | Example: 28 | 29 | ```javascript 30 | var app = new shell(); 31 | app.configure(function() { 32 | app.use(shell.router({ 33 | shell: app 34 | })); 35 | app.use(shell.cloud9({ 36 | shell: app, 37 | ip: '0.0.0.0' 38 | })); 39 | app.use(shell.help({ 40 | shell: app, 41 | introduction: true 42 | })); 43 | }); 44 | ``` 45 | 46 | **Important:** If you encounter issue while installing cloud9, it might be because the npm module expect an older version of Node. 47 | 48 | Here's the procedure to use the latest version: 49 | 50 | ``` 51 | git clone https://github.com/ajaxorg/cloud9.git 52 | cd cloud9 53 | git submodule update --init --recursive 54 | npm link 55 | ``` 56 | 57 | ### 58 | module.exports = (settings = {}) -> 59 | cmd = () -> 60 | args = [] 61 | args.push '-w' 62 | args.push settings.workspace 63 | # Arguments 64 | if settings.config 65 | args.push '-c' 66 | args.push settings.config 67 | if settings.group 68 | args.push '-g' 69 | args.push settings.group 70 | if settings.user 71 | args.push '-u' 72 | args.push settings.user 73 | if settings.action 74 | args.push '-a' 75 | args.push settings.action 76 | if settings.ip 77 | args.push '-l' 78 | args.push settings.ip 79 | if settings.port 80 | args.push '-p' 81 | args.push settings.port 82 | "cloud9 #{args.join(' ')}" 83 | (req, res, next) -> 84 | app = req.shell 85 | # Caching 86 | return next() if app.tmp.cloud9 87 | app.tmp.cloud9 = true 88 | # Workspace 89 | settings.workspace ?= app.set 'workspace' 90 | return next(new Error 'No workspace provided') unless settings.workspace 91 | settings.cmd = cmd() 92 | # Register commands 93 | app.cmd 'cloud9 start', 'Start Cloud9', (req, res, next) -> 94 | # Launch process 95 | start_stop.start settings, (err, pid) -> 96 | return next err if err 97 | unless pid 98 | res.cyan('Cloud9 already started').ln() 99 | return res.prompt() 100 | ip = settings.ip or '127.0.0.1' 101 | port = settings.port or 3000 102 | message = "Cloud9 started http://#{ip}:#{port}" 103 | res.cyan( message ).ln() 104 | res.prompt() 105 | app.cmd 'cloud9 stop', 'Stop Cloud9', (req, res, next) -> 106 | start_stop.stop settings, (err, success) -> 107 | if success 108 | then res.cyan('Cloud9 successfully stoped').ln() 109 | else res.magenta('Cloud9 was not started').ln() 110 | res.prompt() 111 | next() 112 | 113 | -------------------------------------------------------------------------------- /src/plugins/coffee.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | start_stop = require '../start_stop' 4 | 5 | # Sanitize a list of files separated by spaces 6 | enrichFiles = (files) -> 7 | return files.split(' ').map( (file) -> 8 | if file.substr(0, 1) isnt '/' 9 | file = '/' + file 10 | if file.substr(-1, 1) isnt '/' and fs.statSync(file).isDirectory() 11 | file += '/' 12 | file 13 | ).join ' ' 14 | 15 | ### 16 | 17 | CoffeeScript plugin 18 | =================== 19 | 20 | Start Coffee in `--watch` mode, so scripts are instantly compiled into Javascript. 21 | 22 | Options: 23 | 24 | - `src` , Directory where ".coffee" are stored. Each ".coffee" script will be compiled into a .js JavaScript file of the same name. 25 | - `join` , Before compiling, concatenate all scripts together in the order they were passed, and write them into the specified file. Useful for building large projects. 26 | - `output` , Directory where compiled JavaScript files are written. Used in conjunction with "compile". 27 | - `lint` , If the `jsl` (JavaScript Lint) command is installed, use it to check the compilation of a CoffeeScript file. 28 | - `require` , Load a library before compiling or executing your script. Can be used to hook in to the compiler (to add Growl notifications, for example). 29 | - `detached` , Wether the Coffee process should be attached to the current process. If not defined, default to `false` (the server doesn't run as a daemon). 30 | - `pidfile` , Path to the file storing the daemon process id. Defaults to `"/.node_shell/#{md5}.pid"` 31 | - `stdout` , Writable stream or file path to redirect cloud9 stdout. 32 | - `stderr` , Writable stream or file path to redirect cloud9 stderr. 33 | - `workspace`, Project directory used to resolve relative paths. 34 | 35 | Example: 36 | 37 | ```javascript 38 | var app = new shell(); 39 | app.configure(function() { 40 | app.use(shell.router({ 41 | shell: app 42 | })); 43 | app.use(shell.coffee({ 44 | shell: app 45 | })); 46 | app.use(shell.help({ 47 | shell: app, 48 | introduction: true 49 | })); 50 | }); 51 | ``` 52 | 53 | ### 54 | module.exports = (settings = {}) -> 55 | # Validation 56 | throw new Error 'No shell provided' if not settings.shell 57 | shell = settings.shell 58 | # Default settings 59 | settings.workspace ?= shell.set 'workspace' 60 | throw new Error 'No workspace provided' if not settings.workspace 61 | cmd = () -> 62 | args = [] 63 | # 64 | if settings.join 65 | args.push '-j' 66 | args.push enrichFiles(settings.join) 67 | # Watch the modification times of the coffee-scripts, 68 | # recompiling as soon as a change occurs. 69 | args.push '-w' 70 | if settings.lint 71 | args.push '-l' 72 | if settings.require 73 | args.push '-r' 74 | args.push settings.require 75 | # Compile the JavaScript without the top-level function 76 | # safety wrapper. (Used for CoffeeScript as a Node.js module.) 77 | args.push '-b' 78 | if settings.output 79 | args.push '-o' 80 | args.push enrichFiles(settings.output) 81 | if not settings.compile 82 | settings.compile = settings.workspace 83 | if settings.compile 84 | args.push '-c' 85 | args.push enrichFiles(settings.compile) 86 | cmd = 'coffee ' + args.join(' ') 87 | settings.cmd = cmd() 88 | # Register commands 89 | shell.cmd 'coffee start', 'Start CoffeeScript', (req, res, next) -> 90 | start_stop.start settings, (err, pid) -> 91 | return next err if err 92 | return res.cyan('Already Started').ln() unless pid 93 | message = "CoffeeScript started" 94 | res.cyan( message ).ln() 95 | res.prompt() 96 | shell.cmd 'coffee stop', 'Stop CoffeeScript', (req, res, next) -> 97 | start_stop.stop settings, (err, success) -> 98 | if success 99 | then res.cyan('CoffeeScript successfully stoped').ln() 100 | else res.magenta('CoffeeScript was not started').ln() 101 | res.prompt() 102 | 103 | -------------------------------------------------------------------------------- /src/plugins/completer.coffee: -------------------------------------------------------------------------------- 1 | 2 | ### 3 | 4 | Completer plugin 5 | ================ 6 | 7 | Provides tab completion. Options passed during creation are: 8 | 9 | - `shell` , (required) A reference to your shell application. 10 | 11 | ### 12 | module.exports = (settings) -> 13 | # Validation 14 | throw new Error 'No shell provided' if not settings.shell 15 | shell = settings.shell 16 | # Plug completer to interface 17 | return unless shell.isShell 18 | shell.interface().completer = (text, cb) -> 19 | suggestions = [] 20 | routes = shell.routes 21 | for route in routes 22 | command = route.command 23 | if command.substr(0, text.length) is text 24 | suggestions.push command 25 | cb(false, [suggestions, text]) 26 | null 27 | -------------------------------------------------------------------------------- /src/plugins/error.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (settings) -> 3 | # Validation 4 | throw new Error 'No shell provided' if not settings.shell 5 | shell = settings.shell 6 | # Define empty error handler to avoir shell to trow error of no event 7 | # handler are defined 8 | shell.on 'error', -> 9 | # Route 10 | (err, req, res, next) -> 11 | res.red(err.message).ln() if err.message 12 | res.red(err.stack).ln() if err.stack 13 | for k, v of err 14 | continue if k is 'message' 15 | continue if k is 'stack' 16 | continue if typeof v is 'function' 17 | res.magenta(k).white(': ').red(v).ln() 18 | res.prompt() -------------------------------------------------------------------------------- /src/plugins/help.coffee: -------------------------------------------------------------------------------- 1 | 2 | pad = require 'pad' 3 | 4 | ### 5 | 6 | Help Plugin 7 | ----------- 8 | 9 | Display help when the user types "help" or runs commands without arguments. 10 | Command help is only displayed if a description was provided during the 11 | command registration. Additionnaly, a new `shell.help()` function is made available. 12 | 13 | Options passed during creation are: 14 | 15 | - `shell` , (required) A reference to your shell application. 16 | - `introduction` , Print message 'Type "help" or press enter for a list of commands' if boolean `true`, or a custom message if a `string` 17 | 18 | Usage 19 | 20 | app = shell() 21 | app.configure -> 22 | app.use shell.router shell: app 23 | app.use shell.help 24 | shell: app 25 | introduction: true 26 | 27 | ### 28 | module.exports = (settings) -> 29 | # Validation 30 | throw new Error 'No shell provided' if not settings.shell 31 | shell = settings.shell 32 | # Register function 33 | shell.help = (req, res, next) -> 34 | res.cyan 'Available commands:' 35 | res.ln() 36 | routes = shell.routes 37 | for route in routes 38 | text = pad route.command, 20 39 | res 40 | .cyan(text) 41 | .white(route.description) 42 | .ln() if route.description 43 | res.prompt() 44 | # Register commands 45 | shell.cmd 'help', 'Show this message', shell.help.bind shell 46 | shell.cmd '', shell.help.bind shell 47 | # Print introduction message 48 | if shell.isShell and settings.introduction 49 | text = 50 | if typeof settings.introduction is 'string' 51 | then settings.introduction 52 | else 'Type "help" or press enter for a list of commands' 53 | shell.styles.println text 54 | -------------------------------------------------------------------------------- /src/plugins/history.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | crypto = require 'crypto' 4 | Interface = require('readline').Interface 5 | 6 | hash = (value) -> crypto.createHash('md5').update(value).digest('hex') 7 | 8 | ### 9 | 10 | History plugin 11 | ============== 12 | 13 | Persistent command history over multiple sessions. Options passed during creation are: 14 | 15 | - `shell` , (required) A reference to your shell application. 16 | - `name` , Identify your project history file, default to the hash of the exectuted file 17 | - `dir` , Location of the history files, defaults to `"#{process.env['HOME']}/.node_shell"` 18 | 19 | ### 20 | module.exports = (settings) -> 21 | # Validation 22 | throw new Error 'No shell provided' if not settings.shell 23 | shell = settings.shell 24 | # Only in shell mode 25 | return if not settings.shell.isShell 26 | # Persist readline history 27 | settings.dir ?= "#{process.env['HOME']}/.node_shell" 28 | settings.name ?= hash process.argv[1] 29 | file = "#{settings.dir}/#{settings.file}" 30 | # Create store directory 31 | fs.mkdirSync settings.dir, 0o0700 unless fs.existsSync settings.dir 32 | # Look for previous history 33 | if fs.existsSync file 34 | try 35 | json = fs.readFileSync(file, 'utf8') or '[]' 36 | settings.shell.interface().history = JSON.parse json 37 | catch e 38 | settings.shell.styles.red('Corrupted history file').ln() 39 | # Write new history 40 | stream = fs.createWriteStream file, {flag: 'w'} 41 | Interface.prototype._addHistory = ((parent) -> -> 42 | if @history.length 43 | buffer = Buffer.from JSON.stringify( @history ), 'utf8' 44 | fs.writeSync stream.fd, buffer, 0, buffer.length, 0 45 | parent.apply @, arguments 46 | ) Interface.prototype._addHistory 47 | null 48 | 49 | -------------------------------------------------------------------------------- /src/plugins/http.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | path = require 'path' 4 | existsSync = fs.existsSync or path.existsSync 5 | start_stop = require '../start_stop' 6 | ### 7 | 8 | HTTP server 9 | =========== 10 | 11 | Register two commands, `http start` and `http stop`. The start command will 12 | search for "./server.js" and "./app.js" (and additionnaly their CoffeeScript 13 | alternatives) to run by `node`. 14 | 15 | The following properties may be provided as settings: 16 | 17 | - `message_start` Message to display once the server is started 18 | - `message_stop` Message to display once the server is stoped 19 | - `workspace` Project directory used to resolve relative paths and search for "server" and "app" scripts. 20 | - `cmd` Command to start the server, not required if path is provided or if the script is discoverable 21 | - `path` Path to the js/coffee script starting the process, may be relative to the workspace, extension isn't required. 22 | 23 | Properties derived from the start_stop utility: 24 | 25 | - `detached` Wether the HTTP process should be attached to the current process. If not defined, default to `false` (the server doesn't run as a daemon). 26 | - `pidfile` Path to the file storing the daemon process id. Defaults to `"/.node_shell/#{md5}.pid"` 27 | - `stdout` Writable stream or file path to redirect the server stdout. 28 | - `stderr` Writable stream or file path to redirect the server stderr. 29 | 30 | Example: 31 | 32 | ```javascript 33 | var app = new shell(); 34 | app.configure(function() { 35 | app.use(shell.router({ 36 | shell: app 37 | })); 38 | app.use(shell.http({ 39 | shell: app 40 | })); 41 | app.use(shell.help({ 42 | shell: app, 43 | introduction: true 44 | })); 45 | }); 46 | ``` 47 | 48 | ### 49 | module.exports = () -> 50 | settings = {} 51 | cmd = () -> 52 | searchs = if settings.path then [settings.path] else ['app', 'server', 'lib/app', 'lib/server'] 53 | for search in searchs 54 | search = path.resolve settings.workspace, search 55 | if existsSync "#{search}" 56 | if search.substr(-4) is '.coffee' 57 | then return "coffee #{search}" 58 | else return "node #{search}" 59 | if existsSync "#{search}.js" 60 | return "node #{search}.js" 61 | else if existsSync "#{search}.coffee" 62 | return "coffee #{search}.coffee" 63 | throw new Error 'Failed to discover a "server.js" or "app.js" file' 64 | http = null 65 | # Register commands 66 | route = (req, res, next) -> 67 | app = req.shell 68 | # Caching 69 | return next() if app.tmp.http 70 | app.tmp.http = true 71 | # Workspace settings 72 | settings.workspace ?= app.set 'workspace' 73 | throw new Error 'No workspace provided' if not settings.workspace 74 | # Messages 75 | settings.message_start ?= 'HTTP server successfully started' 76 | settings.message_stop ?= 'HTTP server successfully stopped' 77 | settings.cmd = cmd() unless settings.cmd 78 | app.cmd 'http start', 'Start HTTP server', (req, res, next) -> 79 | http = start_stop.start settings, (err, pid) -> 80 | return next err if err 81 | return res.cyan('HTTP server already started').ln() and res.prompt() unless pid 82 | res.cyan(settings.message_start).ln() 83 | res.prompt() 84 | app.cmd 'http stop', 'Stop HTTP server', (req, res, next) -> 85 | start_stop.stop settings, (err, success) -> 86 | if success 87 | then res.cyan(settings.message_stop).ln() 88 | else res.magenta('HTTP server was not started').ln() 89 | res.prompt() 90 | next() 91 | if arguments.length is 1 92 | settings = arguments[0] 93 | return route 94 | else 95 | route.apply null, arguments 96 | -------------------------------------------------------------------------------- /src/plugins/redis.coffee: -------------------------------------------------------------------------------- 1 | 2 | start_stop = require '../start_stop' 3 | 4 | ### 5 | Redis Plugin 6 | ============ 7 | 8 | Register two commands, `redis start` and `redis stop`. The following properties may be provided as settings: 9 | 10 | - `config` , Path to the configuration file. Required to launch redis. 11 | - `detached` , Wether the Redis process should be attached to the current process. If not defined, default to `false` (the server doesn't run as a daemon). 12 | - `pidfile` , Path to the file storing the daemon process id. Defaults to `"/.node_shell/#{md5}.pid"` 13 | - `stdout` , Writable stream or file path to redirect cloud9 stdout. 14 | - `stderr` , Writable stream or file path to redirect cloud9 stderr. 15 | 16 | Example: 17 | 18 | ```javascript 19 | var app = shell(); 20 | app.configure(function() { 21 | app.use(shell.router({ 22 | shell: app 23 | })); 24 | app.use(shell.redis({ 25 | shell: app, 26 | config: __dirname+'/redis.conf') 27 | })); 28 | app.use(shell.help({ 29 | shell: app, 30 | introduction: true 31 | })); 32 | }); 33 | ``` 34 | ### 35 | module.exports = () -> 36 | settings = {} 37 | # Register commands 38 | redis = null 39 | route = (req, res, next) -> 40 | app = req.shell 41 | # Caching 42 | return next() if app.tmp.redis 43 | app.tmp.redis = true 44 | # Default settings 45 | settings.workspace ?= app.set 'workspace' 46 | settings.config ?= '' 47 | settings.cmd = "redis-server #{settings.config}" 48 | app.cmd 'redis start', 'Start Redis', (req, res, next) -> 49 | # Launch process 50 | redis = start_stop.start settings, (err, pid) -> 51 | return next err if err 52 | unless pid 53 | res.cyan('Redis already started').ln() 54 | return res.prompt() 55 | res.cyan('Redis started').ln() 56 | res.prompt() 57 | app.cmd 'redis stop', 'Stop Redis', (req, res, next) -> 58 | start_stop.stop settings, (err, success) -> 59 | if success 60 | then res.cyan('Redis successfully stoped').ln() 61 | else res.magenta('Redis was not started').ln() 62 | res.prompt() 63 | next() 64 | if arguments.length is 1 65 | settings = arguments[0] 66 | return route 67 | else 68 | route.apply null, arguments 69 | -------------------------------------------------------------------------------- /src/plugins/router.coffee: -------------------------------------------------------------------------------- 1 | 2 | utils = require '../utils' 3 | 4 | querystring = 5 | unescape: (str) -> 6 | decodeURIComponent str 7 | parse: (qs, sep, eq) -> 8 | sep = sep or '&' 9 | eq = eq or '=' 10 | obj = {} 11 | return obj if typeof qs isnt 'string' 12 | vkps = qs.split sep 13 | for kvp in vkps 14 | x = kvp.split eq 15 | k = querystring.unescape x[0], true 16 | v = querystring.unescape x.slice(1).join(eq), true 17 | if not k in obj 18 | obj[k] = v 19 | else if not Array.isArray obj[k] 20 | obj[k] = [obj[k], v] 21 | else 22 | obj[k].push v 23 | obj 24 | 25 | # produce regular expression from string 26 | normalize = (command, keys, sensitive) -> 27 | command = command 28 | .concat('/?') 29 | .replace(/\/\(/g, '(?:/') 30 | # regexp factors: 31 | # 0. match a literal ':' 32 | # 1. 'key': match 1 or more word characters followed by : 33 | # 2. 'format': match anything inside (), should be a regexp factor ie ([0-9]+) 34 | # 3. 'optional': match an optional literal '?' 35 | .replace(/:(\w+)(\(.*\))?(\?)?/g, (_, key, format, optional) -> 36 | keys.push key 37 | format = format or '([^ ]+)' # provide default format 38 | optional = optional or '' 39 | return format + optional 40 | ) 41 | .replace(/([\/.])/g, '\\$1') 42 | .replace(/\*/g, '(.+)') 43 | new RegExp '^' + command + '$', ( 'i' if sensitive? ) 44 | 45 | match = (req, routes, i) -> 46 | #from ?= 0 47 | #to = routes.length - 1 48 | #for (len = routes.length; i < len; ++i) { 49 | #for i in [from .. to] 50 | i ?= 0 51 | while i < routes.length 52 | route = routes[i] 53 | #fn = route.callback 54 | regexp = route.regexp 55 | keys = route.keys 56 | captures = regexp.exec req.command 57 | if captures 58 | route.params = {} 59 | index = 0 60 | #for (j = 1, len = captures.length; j < len; ++j) { 61 | #for j in [1 .. captures.length] 62 | j = 1 63 | while j < captures.length 64 | key = keys[j-1] 65 | val = 66 | if typeof captures[j] is 'string' 67 | then querystring.unescape captures[j] 68 | else captures[j] 69 | if key 70 | route.params[key] = val 71 | else 72 | route.params[''+index] = val 73 | index++ 74 | j++ 75 | req._route_index = i 76 | return route 77 | i++ 78 | null 79 | 80 | module.exports = (settings) -> 81 | # Validation 82 | throw new Error 'No shell provided' if not settings.shell 83 | shell = settings.shell 84 | settings.sensitive ?= true 85 | # Expose routes 86 | routes = shell.routes = [] 87 | params = {} 88 | shell.param = (name, fn) -> 89 | if Array.isArray name 90 | name.forEach (name) -> 91 | this.param name, fn 92 | , this 93 | else 94 | name = name.substr(1) if ':' is name[0] 95 | params[name] = fn 96 | this 97 | shell.cmd = (command, description, middleware1, middleware2, fn) -> 98 | args = Array.prototype.slice.call arguments 99 | route = {} 100 | route.command = args.shift() 101 | route.description = args.shift() if typeof args[0] is 'string' 102 | route.middlewares = utils.flatten args 103 | keys = [] 104 | route.regexp = 105 | if route.command instanceof RegExp 106 | then route.command 107 | else normalize route.command, keys, settings.sensitive 108 | route.keys = keys 109 | routes.push route 110 | this 111 | # Register 'quit' command 112 | shell.cmd 'quit', 'Exit this shell', shell.quit.bind shell 113 | # middleware 114 | (req, res, next) -> 115 | route = null 116 | self = this 117 | i = 0 118 | pass = (i) -> 119 | route = match req, routes, i 120 | return next() if not route 121 | i = 0 122 | keys = route.keys 123 | req.params = route.params 124 | # Param preconditions 125 | # From expresso guide: There are times when we may want to "skip" passed 126 | # remaining route middleware, but continue matching subsequent routes. To 127 | # do this we invoke `next()` with the string "route" `next('route')`. If no 128 | # remaining routes match the request url then Express will respond with 404 Not Found. 129 | param = (err) -> 130 | try 131 | key = keys[ i++ ] 132 | val = req.params[ key ] 133 | fn = params[ key ] 134 | if 'route' is err 135 | pass req._route_index + 1 136 | # Error 137 | else if err 138 | next err 139 | # Param has callback 140 | else if fn 141 | # Return style 142 | if 1 is fn.length 143 | req.params[key] = fn val 144 | param() 145 | # Middleware style 146 | else 147 | fn req, res, param, val 148 | # Finished processing params 149 | else if not key 150 | # route middleware 151 | i = 0 152 | nextMiddleware = (err) -> 153 | fn = route.middlewares[ i++ ] 154 | if 'route' is err 155 | pass req._route_index + 1 156 | else if err 157 | next err 158 | else if fn 159 | fn req, res, nextMiddleware 160 | else 161 | pass req._route_index + 1 162 | #route.callback.call self, req, res, (err) -> 163 | #if err 164 | #next err 165 | #else 166 | #pass req._route_index + 1 167 | nextMiddleware() 168 | # More params 169 | else 170 | param() 171 | catch err 172 | next err 173 | param() 174 | pass() 175 | -------------------------------------------------------------------------------- /src/plugins/stylus.coffee: -------------------------------------------------------------------------------- 1 | 2 | path = require 'path' 3 | start_stop = require '../start_stop' 4 | 5 | # Sanitize a list of files separated by spaces 6 | enrichFiles = (files) -> 7 | return files.split(' ').map( (file) -> 8 | path.normalize file 9 | # Stylus doesn't like trailing `/` in the use option 10 | if file.substr(-1, 1) is '/' 11 | file = file.substr 0, file.length - 1 12 | file 13 | ).join ' ' 14 | 15 | ### 16 | 17 | Stylus plugin 18 | ------------- 19 | Start/stop a daemon to watch and convert stylus files to css. 20 | 21 | Options include: 22 | * `output` Output to when passing files. 23 | * `input` Add to lookup paths 24 | 25 | ### 26 | module.exports = (settings = {}) -> 27 | # Validation 28 | throw new Error 'No shell provided' if not settings.shell 29 | shell = settings.shell 30 | # Default settings 31 | settings.workspace ?= shell.set 'workspace' 32 | throw new Error 'No workspace provided' if not settings.workspace 33 | cmd = () -> 34 | args = [] 35 | # Watch the modification times of the coffee-scripts, 36 | # recompiling as soon as a change occurs. 37 | args.push '-w' 38 | if settings.use 39 | args.push '-u' 40 | args.push enrichFiles(settings.use) 41 | if settings.output 42 | args.push '-o' 43 | args.push enrichFiles(settings.output) 44 | if not settings.input 45 | settings.input = settings.workspace 46 | if settings.input 47 | args.push enrichFiles(settings.input) 48 | cmd = 'stylus ' + args.join(' ') 49 | settings.cmd = cmd() 50 | #console.log settings.cmd 51 | # Register commands 52 | shell.cmd 'stylus start', 'Start CoffeeScript', (req, res, next) -> 53 | start_stop.start settings, (err, pid) -> 54 | return next err if err 55 | return res.cyan('Already Started').ln() unless pid 56 | message = "Stylus started" 57 | res.cyan( message ).ln() 58 | res.prompt() 59 | shell.cmd 'stylus stop', 'Stop Stylus', (req, res, next) -> 60 | start_stop.stop settings, (err, success) -> 61 | if success 62 | then res.cyan('Stylus successfully stoped').ln() 63 | else res.magenta('Stylus was not started').ln() 64 | res.prompt() 65 | 66 | -------------------------------------------------------------------------------- /src/plugins/test.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | path = require 'path' 4 | existsSync = fs.existsSync or path.existsSync 5 | exec = require('child_process').exec 6 | 7 | module.exports = (settings) -> 8 | # Validation 9 | throw new Error 'No shell provided' if not settings.shell 10 | shell = settings.shell 11 | # Default settings 12 | settings.workspace ?= shell.set 'workspace' 13 | throw new Error 'No workspace provided' if not settings.workspace 14 | settings.glob ?= 'test/*.js' 15 | # Register commands 16 | shell.cmd 'test', 'Run all test', (req, res, next) -> 17 | run = (cmd) -> 18 | args = [] 19 | args.push cmd 20 | if settings.coverage 21 | args.push '--cov' 22 | if settings.serial 23 | args.push '--serial' 24 | if settings.glob 25 | args.push settings.glob 26 | expresso = exec 'cd ' + settings.workspace + ' && ' + args.join(' ') 27 | expresso.stdout.on 'data', (data) -> 28 | res.cyan data 29 | expresso.stderr.on 'data', (data) -> 30 | res.magenta data 31 | expresso.on 'exit', (code) -> 32 | res.prompt() 33 | paths = [].concat module.paths, require.paths 34 | for p in paths 35 | if existsSync p + '/expresso/bin/expresso' 36 | return run p 37 | res.magenta('Expresso not found').ln() 38 | res.prompt() 39 | shell.cmd 'test :pattern', 'Run specific tests', (req, res, next) -> 40 | #todo 41 | -------------------------------------------------------------------------------- /src/routes/confirm.coffee: -------------------------------------------------------------------------------- 1 | 2 | ### 3 | 4 | Confirm route 5 | ============= 6 | 7 | The `confirm` route ask the user if he want to continue the process. If the answer is `true`, the following routes are executed. Otherwise, the process is stoped. 8 | 9 | ```javascript 10 | var app = new shell(); 11 | app.configure(function() { 12 | app.use(shell.router({ 13 | shell: app 14 | })); 15 | }); 16 | app.cmd('install', [ 17 | shell.routes.confirm('Do you confirm?'), 18 | my_app.routes.download, 19 | my_app.routes.configure 20 | ]); 21 | ``` 22 | 23 | ### 24 | module.exports = (message) -> 25 | (req, res, next) -> 26 | req.confirm message, true, (confirmed) -> 27 | return res.prompt() unless confirmed 28 | next() 29 | -------------------------------------------------------------------------------- /src/routes/prompt.coffee: -------------------------------------------------------------------------------- 1 | 2 | ### 3 | 4 | Prompt route 5 | ============ 6 | 7 | The `prompt` route is a convenient function to stop command once a few routes are executed. You can simply pass the the `shell.routes.prompt` function or call it with a message argument. 8 | 9 | ```javascript 10 | var app = new shell(); 11 | app.configure(function() { 12 | app.use(shell.router({ 13 | shell: app 14 | })); 15 | }); 16 | app.cmd('install', [ 17 | my_app.routes.download, 18 | my_app.routes.configure, 19 | shell.routes.prompt('Installation is finished') 20 | ]); 21 | ``` 22 | 23 | ### 24 | module.exports = (req, res, next) -> 25 | if arguments.length is 1 26 | message = arguments[0] 27 | return (req, res, next) -> 28 | res.white message 29 | res.ln() 30 | res.prompt() 31 | else 32 | res.prompt() -------------------------------------------------------------------------------- /src/routes/shellOnly.coffee: -------------------------------------------------------------------------------- 1 | 2 | ### 3 | 4 | `routes.shellOnly` 5 | ================== 6 | 7 | Ensure the current process is running in shell mode. 8 | 9 | ### 10 | module.exports = (req, res, next) -> 11 | if not req.shell.isShell 12 | res.red 'Command may only be executed inside a running shell' 13 | res.prompt() 14 | return 15 | next() -------------------------------------------------------------------------------- /src/routes/timeout.coffee: -------------------------------------------------------------------------------- 1 | 2 | ### 3 | 4 | Timeout route 5 | ============= 6 | 7 | The `timeout` route will wait for the provided period (in millisenconds) before executing the following route. 8 | 9 | ```javascript 10 | var app = new shell(); 11 | app.configure(function() { 12 | app.use(shell.router({ 13 | shell: app 14 | })); 15 | }); 16 | app.cmd('restart', [ 17 | my_app.routes.stop, 18 | shell.routes.timeout(1000), 19 | my_app.routes.start 20 | ]); 21 | ``` 22 | 23 | ### 24 | module.exports = (timeout) -> 25 | (req, res, next) -> 26 | setTimeout timeout, next 27 | -------------------------------------------------------------------------------- /src/start_stop.coffee: -------------------------------------------------------------------------------- 1 | 2 | crypto = require 'crypto' 3 | {exec, spawn} = require 'child_process' 4 | fs = require 'fs' 5 | path = require 'path' 6 | exists = fs.exists or path.exists 7 | 8 | md5 = (cmd) -> crypto.createHash('md5').update(cmd).digest('hex') 9 | 10 | ### 11 | `start_stop`: Unix process management 12 | ------------------------------------- 13 | 14 | The library start and stop unix child process. Process are by default 15 | daemonized and will keep running even if your current process exit. For 16 | conveniency, they may also be attached to the current process by 17 | providing the `attach` option. 18 | 19 | ### 20 | module.exports = start_stop = 21 | 22 | ### 23 | 24 | `start(options, callback)` 25 | -------------------------- 26 | Start a prcess as a daemon (default) or as a child of the current process. Options includes 27 | all the options of the "child_process.exec" function plus a few specific ones. 28 | 29 | `options` , Object with the following properties: 30 | * `cmd` , Command to run 31 | * `cwd` , Current working directory of the child process 32 | * `detached` , Detached the child process from the current process 33 | * `pidfile` , Path to the file storing the child pid 34 | * `stdout` , Path to the file where standard output is redirected 35 | * `stderr` , Path to the file where standard error is redirected 36 | * `strict` , Send an error when a pid file exists and reference 37 | an unrunning pid. 38 | * `watch` , Watch for file changes 39 | * `watchIgnore` , List of ignore files 40 | 41 | `callback` , Received arguments are: 42 | * `err` , Error if any 43 | * `pid` , Process id of the new child 44 | 45 | ### 46 | start: (options, callback) -> 47 | if options.attach? 48 | console.log 'Option attach was renamed to attached to be consistent with the new spawn option' 49 | options.detached = not options.attach 50 | if options.detached 51 | child = null 52 | cmdStdout = 53 | if typeof options.stdout is 'string' 54 | then options.stdout else '/dev/null' 55 | cmdStderr = 56 | if typeof options.stderr is 'string' 57 | then options.stderr else '/dev/null' 58 | check_pid = -> 59 | start_stop.pid options, (err, pid) -> 60 | return watch() unless pid 61 | start_stop.running pid, (err, pid) -> 62 | return callback new Error "Pid #{pid} already running" if pid 63 | # Pid file reference an unrunning process 64 | if options.strict 65 | then callback new Error "Pid file reference a dead process" 66 | else watch() 67 | watch = -> 68 | return start() unless options.watch 69 | options.watch = options.cwd or process.cwd unless typeof options.watch is 'string' 70 | ioptions = 71 | path: options.watch 72 | ignoreFiles: [".startstopignore"] or options.watchIgnoreFiles 73 | ignore = require 'fstream-ignore' 74 | ignore(ioptions) 75 | .on 'child', (c) -> 76 | # c.on 'ignoreFile', (path, content) -> 77 | # console.log 'ignore', path, content.toString() 78 | fs.watchFile c.path, (curr, prev) -> 79 | console.log c.path 80 | start_stop.stop options, (e) -> 81 | start_stop.start options, (e) -> 82 | console.log 'restarted', e 83 | # a file has changed, restart the child process 84 | # child.kill('SIGHUP') 85 | # child.on 'exit', (code, signal) -> 86 | # console.log('child process terminated due to receipt of signal '+signal) 87 | # start() 88 | # .on 'ignoreFile', (path, content) -> 89 | # console.log 'ignore', path, content.toString() 90 | 91 | start() 92 | # Start the process 93 | start = -> 94 | piddir = path.dirname(options.pidfile) 95 | exists piddir, (exists) -> 96 | return callback new Error "Pid directory does not exist: #{piddir}." unless exists 97 | pipe = "#{cmdStdout} 2>#{cmdStdout}" 98 | info = 'echo $? $!' 99 | cmd = "#{options.cmd} #{pipe} & #{info}" 100 | child = exec cmd, options, (err, stdout, stderr) -> 101 | [code, pid] = stdout.split(' ') 102 | code = parseInt code, 10 103 | pid = parseInt pid, 10 104 | if code isnt 0 105 | msg = "Process exit with code #{code}" 106 | return callback new Error msg 107 | fs.writeFile options.pidfile, '' + pid, (err) -> 108 | callback null, pid 109 | # Do the job 110 | check_pid() 111 | else # Kill child on exit if started in attached mode 112 | c = exec options.cmd 113 | if typeof options.stdout is 'string' 114 | stdout = fs.createWriteStream options.stdout 115 | else if options.stdout isnt null and typeof options.stdout is 'object' 116 | stdout = options.stdout 117 | else 118 | stdout = null 119 | if typeof options.stderr is 'string' 120 | stdout = fs.createWriteStream options.stderr 121 | else if options.stderr isnt null and typeof options.stderr is 'object' 122 | stderr = options.stderr 123 | else 124 | stderr = null 125 | process.nextTick -> 126 | # Block the command if not in shell and process is attached 127 | options.pid = c.pid 128 | callback null, c.pid 129 | 130 | ### 131 | 132 | `stop(options, callback)` 133 | ------------------------- 134 | Stop a process. In daemon mode, the pid is obtained from the `pidfile` option which, if 135 | not provided, can be guessed from the `cmd` option used to start the process. 136 | 137 | `options` , Object with the following properties: 138 | * `detached` , Detach the child process to the current process 139 | * `cmd` , Command used to run the process, in case no pidfile is provided 140 | * `pid` , Pid to kill in attach mode 141 | * `pidfile` , Path to the file storing the child pid 142 | * `strict` , Send an error when a pid file exists and reference 143 | an unrunning pid. 144 | 145 | `callback` , Received arguments are: 146 | * `err` , Error if any 147 | * `stoped` , True if the process was stoped 148 | 149 | ### 150 | stop: (options, callback) -> 151 | if options.attach? 152 | console.log 'Option attach was renamed to attached to be consistent with the new spawn option' 153 | options.detached = not options.attach 154 | # Stoping a provided PID 155 | if typeof options is 'string' or typeof options is 'number' 156 | options = {pid: parseInt(options, 10), detached: false} 157 | kill = (pid) -> 158 | # Not trully recursive, potential scripts: 159 | # http://machine-cycle.blogspot.com/2009/05/recursive-kill-kill-process-tree.html 160 | # http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2004-05/1108.html 161 | cmds = """ 162 | for i in `ps -ef | awk '$3 == '#{pid}' { print $2 }'` 163 | do 164 | kill $i 165 | done 166 | kill #{pid} 167 | """ 168 | exec cmds, (err, stdout, stderr) -> 169 | return callback new Error "Unexpected exit code #{err.code}" if err 170 | options.pid = null 171 | callback null, true 172 | if options.detached 173 | start_stop.pid options, (err, pid) -> 174 | return callback err if err 175 | return callback null, false unless pid 176 | fs.unlink options.pidfile, (err) -> 177 | return callback err if err 178 | start_stop.running pid, (err, running) -> 179 | unless running 180 | return if options.strict 181 | then callback new Error "Pid file reference a dead process" 182 | else callback null, false 183 | kill pid 184 | else 185 | kill options.pid 186 | 187 | ### 188 | 189 | `pid(options, callback)` 190 | ------------------------ 191 | Retrieve a process pid. The pid value is return only if the command is running 192 | otherwise it is set to false. 193 | 194 | `options` , Object with the following properties: 195 | * `detached` , True if the child process is not attached to the current process 196 | * `cmd` , Command used to run the process, in case no pidfile is provided 197 | * `pid` , Pid to kill if not running in detached mode 198 | * `pidfile` , Path to the file storing the child pid 199 | 200 | 201 | `callback` , Received arguments are: 202 | * `err` , Error if any 203 | * `pid` , Process pid. Pid is null if there are no pid file or 204 | if the process isn't running. 205 | 206 | ### 207 | pid: (options, callback) -> 208 | if options.attach? 209 | console.log 'Option attach was renamed to attached to be consistent with the new spawn option' 210 | options.detached = not options.attach 211 | # Attach mode 212 | unless options.detached 213 | return new Error 'Expect a pid property in attached mode' unless options.pid? 214 | return callback null, options.pid 215 | # Deamon mode 216 | start_stop.file options, (err, file, exists) -> 217 | return callback null, false unless exists 218 | fs.readFile options.pidfile, 'ascii', (err, pid) -> 219 | return callback err if err 220 | pid = pid.trim() 221 | callback null, pid 222 | 223 | ### 224 | 225 | `file(options, callback)` 226 | ------------------------- 227 | Retrieve information relative to the file storing the pid. Retrieve 228 | the path to the file storing the pid number and whether 229 | it exists or not. Note, it will additionnaly enrich the `options` 230 | argument with a pidfile property unless already present. 231 | 232 | `options` , Object with the following properties: 233 | * `detached` , True if the child process is not attached to the current process 234 | * `cmd` , Command used to run the process, in case no pidfile is provided 235 | * `pid` , Pid to kill in attach mode 236 | * `pidfile` , Path to the file storing the child pid 237 | 238 | `callback` , Received arguments are: 239 | * `err` , Error if any 240 | * `path` , Path to the file storing the pid, null in attach mode 241 | * `exists` , True if the file is created 242 | 243 | ### 244 | file: (options, callback) -> 245 | if options.attach? 246 | console.log 'Option attach was renamed to detached to be consistent with the spawn API' 247 | options.detached = not options.attach 248 | return callback null, null, false unless options.detached 249 | dir = path.resolve process.env['HOME'], '.node_shell' 250 | start = -> 251 | return pidFileExists() if options.pidfile 252 | file = md5 options.cmd 253 | options.pidfile = "#{dir}/#{file}.pid" 254 | exists dir, (dirExists) -> 255 | return createDir() unless dirExists 256 | pidFileExists() 257 | createDir = -> 258 | fs.mkdir dir, 0o0700, (err) -> 259 | return callback err if err 260 | pidFileExists() 261 | pidFileExists = -> 262 | exists options.pidfile, (pidFileExists) -> 263 | callback null, options.pidfile, pidFileExists 264 | start() 265 | 266 | ### 267 | 268 | `running(pid, callback)` 269 | ------------------------ 270 | 271 | Test if a pid match a running process. 272 | 273 | `pid` , Process id to test 274 | 275 | `callback` , Received arguments are: 276 | * `err` , Error if any 277 | * `running` , True if pid match a running process 278 | 279 | ### 280 | running: (pid, callback) -> 281 | exec "kill -0 #{pid}", (err, stdout, stderr) -> 282 | return callback err if err and err.code isnt 1 283 | callback null, not err 284 | -------------------------------------------------------------------------------- /src/utils.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | path = require 'path' 4 | existsSync = fs.existsSync or path.existsSync 5 | 6 | module.exports = 7 | flatten: (arr, ret) -> 8 | ret ?= [] 9 | for i in [0 ... arr.length] 10 | if Array.isArray arr[i] 11 | @flatten arr[i], ret 12 | else 13 | ret.push arr[i] 14 | ret 15 | # Discovery the project root directory or return null if undiscoverable 16 | workspace: () -> 17 | #dirs = require('module')._nodeModulePaths process.cwd() 18 | dirs = require('module')._nodeModulePaths process.argv[1] 19 | for dir in dirs 20 | if existsSync(dir) || existsSync(path.normalize(dir + '/../package.json')) 21 | return path.normalize dir + '/..' 22 | checkPort: (port, host, callback) -> 23 | cmd = exec "nc #{host} #{port} < /dev/null" 24 | cmd.on 'exit', (code) -> 25 | return callback true if code is 0 26 | return callback false if code is 1 27 | return callback new Error 'The nc (or netcat) utility is required' -------------------------------------------------------------------------------- /test/confirm.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require 'should' 3 | shell = require '../src/Shell' 4 | NullStream = require '../src/NullStream' 5 | router = require '../src/plugins/router' 6 | styles = require '../src/Styles' 7 | 8 | describe 'req confirm', -> 9 | it 'should provide a boolean', (next) -> 10 | answers = ['y\n', 'N\n'] 11 | stdin = new NullStream 12 | stdout = new NullStream 13 | stdout.on 'data', (data) -> 14 | return unless data.trim() 15 | styles.unstyle(data).should.eql 'Do u confirm? [Yn] ' 16 | @answer = not @answer 17 | stdin.emit 'data', Buffer.from(answers.shift()) 18 | app = shell 19 | workspace: "#{__dirname}/plugins_http" 20 | command: 'test string' 21 | stdin: stdin 22 | stdout: stdout 23 | app.configure -> 24 | app.use router shell: app 25 | app.cmd 'test string', (req, res) -> 26 | req.confirm 'Do u confirm?', (value) -> 27 | value.should.eql true 28 | req.confirm 'Do u confirm?', (value) -> 29 | value.should.eql false 30 | next() 31 | -------------------------------------------------------------------------------- /test/error.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require 'should' 3 | shell = require '../lib/Shell' 4 | NullStream = require '../lib/NullStream' 5 | router = require '../lib/plugins/router' 6 | error = require '../lib/plugins/error' 7 | 8 | describe 'plugin error', -> 9 | it 'should print a thrown error', (next) -> 10 | stdout = new NullStream 11 | out = '' 12 | stdout.on 'data', (data) -> 13 | out += data 14 | app = shell 15 | command: 'test error' 16 | stdin: new NullStream 17 | stdout: stdout 18 | app.configure -> 19 | app.use router shell: app 20 | app.use error shell: app 21 | app.cmd 'test error', (req, res) -> 22 | should.not.exist true 23 | app.on 'quit', -> 24 | out.should.match /AssertionError/ 25 | next() 26 | it 'should emit thrown error', (next) -> 27 | app = shell 28 | command: 'test error' 29 | stdin: new NullStream 30 | stdout: new NullStream 31 | app.configure -> 32 | app.use router shell: app 33 | app.use error shell: app 34 | app.cmd 'test error', (req, res) -> 35 | should.not.exist true 36 | app.on 'error', (err) -> 37 | err.name.should.eql 'AssertionError' 38 | next() 39 | it 'router should graph error from previous route and emit it', (next) -> 40 | app = shell 41 | command: 'test error' 42 | stdin: new NullStream 43 | stdout: new NullStream 44 | app.configure -> 45 | app.use router shell: app 46 | app.cmd 'test error', (req, res, n) -> 47 | n new Error 'My error' 48 | app.on 'error', (err) -> 49 | err.message.should.eql 'My error' 50 | next() 51 | 52 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --throw-deprecation 2 | --require should 3 | --require coffeescript/register 4 | --timeout 40000 5 | --reporter spec 6 | --recursive 7 | -------------------------------------------------------------------------------- /test/plugin_http.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require 'should' 3 | client = require 'http' 4 | shell = require '../lib/Shell' 5 | NullStream = require '../lib/NullStream' 6 | router = require '../lib/plugins/router' 7 | http = require '../lib/plugins/http' 8 | 9 | describe 'Plugin HTTP', -> 10 | it 'should start and stop an HTTP server in attach mode', (next) -> 11 | app = shell 12 | workspace: "#{__dirname}/plugin_http" 13 | command: null 14 | stdin: new NullStream 15 | stdout: new NullStream 16 | app.configure -> 17 | app.use http detached: false 18 | app.use router shell: app 19 | app.run 'http start' 20 | setTimeout -> 21 | client.get( 22 | host: 'localhost' 23 | port: 8834 24 | path: '/ping' 25 | , (res) -> 26 | res.on 'data', (chunk) -> 27 | chunk.toString().should.eql 'pong' 28 | app.run 'http stop' 29 | setTimeout -> 30 | client.get( 31 | host: 'localhost' 32 | port: 8834 33 | path: '/ping' 34 | , (res) -> 35 | should.not.exist false 36 | ).on 'error', (e) -> 37 | e.should.be.an.instanceof Error 38 | next() 39 | , 300 40 | ).on 'error', (e) -> 41 | should.not.exist e 42 | next e 43 | , 300 44 | -------------------------------------------------------------------------------- /test/plugin_http/app.coffee: -------------------------------------------------------------------------------- 1 | 2 | express = require('express') 3 | app = module.exports = express() 4 | 5 | app.get '/ping', (req, res) -> 6 | res.send 'pong' 7 | 8 | app.listen 8834 if process.argv[1] is __filename 9 | 10 | module.exports = app 11 | -------------------------------------------------------------------------------- /test/question.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require 'should' 3 | shell = require '../lib/Shell' 4 | NullStream = require '../lib/NullStream' 5 | router = require '../lib/plugins/router' 6 | 7 | describe 'Request question', -> 8 | it 'Question # req # string', (next) -> 9 | stdin = new NullStream 10 | stdout = new NullStream 11 | stdout.on 'data', (data) -> 12 | return unless data.trim() 13 | data.should.eql 'My question: ' 14 | stdin.emit 'data', 'My answer\n' 15 | app = shell 16 | workspace: "#{__dirname}/plugins_http" 17 | command: 'test string' 18 | stdin: stdin 19 | stdout: stdout 20 | app.configure -> 21 | app.use router shell: app 22 | app.cmd 'test string', (req, res) -> 23 | req.question 'My question:', (value) -> 24 | value.should.eql 'My answer' 25 | next() 26 | it 'Question # req # array of objects', (next) -> 27 | expects = ['Question 1 ', 'Question 2 [v 2] '] 28 | stdin = new NullStream 29 | stdout = new NullStream 30 | stdout.on 'data', (data) -> 31 | return unless data.trim() 32 | data.should.eql expects.shift() 33 | stdin.emit 'data', "Value #{2 - expects.length}\n" 34 | app = shell 35 | workspace: "#{__dirname}/plugins_http" 36 | command: 'test array' 37 | stdin: stdin 38 | stdout: stdout 39 | app.configure -> 40 | app.use router shell: app 41 | app.cmd 'test array', (req, res) -> 42 | req.question [ 43 | name: 'Question 1' 44 | , 45 | name: 'Question 2' 46 | value: 'v 2' 47 | ], (values) -> 48 | values.should.eql 49 | 'Question 1': 'Value 1' 50 | 'Question 2': 'Value 2' 51 | next() 52 | it 'Question # req # object', (next) -> 53 | expects = ['Question 1 ', 'Question 2 [v 2] ', 'Question 3 [v 3] '] 54 | stdin = new NullStream 55 | stdout = new NullStream 56 | stdout.on 'data', (data) -> 57 | return unless data.trim() 58 | data.should.eql expects.shift() 59 | stdin.emit 'data', "Value #{3 - expects.length}\n" 60 | app = shell 61 | workspace: "#{__dirname}/plugins_http" 62 | command: 'test object' 63 | stdin: stdin 64 | stdout: stdout 65 | app.configure -> 66 | app.use router shell: app 67 | app.cmd 'test object', (req, res) -> 68 | req.question 69 | 'Question 1': null 70 | 'Question 2': 'v 2' 71 | 'Question 3': { value: 'v 3'} 72 | , (values) -> 73 | values.should.eql 74 | 'Question 1': 'Value 1' 75 | 'Question 2': 'Value 2' 76 | 'Question 3': 'Value 3' 77 | next() 78 | -------------------------------------------------------------------------------- /test/router.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require 'should' 3 | shell = require '../lib/Shell' 4 | NullStream = require '../lib/NullStream' 5 | router = require '../lib/plugins/router' 6 | 7 | describe 'Plugin router', -> 8 | it 'Test simple', (next) -> 9 | app = shell 10 | command: 'test simple' 11 | stdin: new NullStream 12 | stdout: new NullStream 13 | app.configure -> 14 | app.use router shell: app 15 | app.cmd 'test simple', (req, res) -> 16 | next() 17 | it 'Test param # string', (next) -> 18 | app = shell 19 | command: 'test my_value' 20 | stdin: new NullStream 21 | stdout: new NullStream 22 | app.configure -> 23 | app.use router shell: app 24 | app.cmd 'test :my_param', (req, res) -> 25 | req.params.my_param.should.eql 'my_value' 26 | next() 27 | it 'Test param # special char', (next) -> 28 | app = shell 29 | command: 'test 12.32/abc' 30 | stdin: new NullStream 31 | stdout: new NullStream 32 | app.configure -> 33 | app.use router shell: app 34 | app.cmd 'test :my_param', (req, res) -> 35 | req.params.my_param.should.eql '12.32/abc' 36 | next() 37 | it 'Test # param with restriction # ok', (next) -> 38 | app = shell 39 | command: 'test 9034' 40 | stdin: new NullStream 41 | stdout: new NullStream 42 | app.configure -> 43 | app.use router shell: app 44 | app.cmd 'test :my_param([0-9]+)', (req, res) -> 45 | req.params.my_param.should.eql '9034' 46 | next() 47 | app.cmd 'test :my_param', (req, res) -> 48 | should.be.ok false 49 | it 'Test # param with restriction # error', (next) -> 50 | app = shell 51 | command: 'test abc' 52 | stdin: new NullStream 53 | stdout: new NullStream 54 | app.configure -> 55 | app.use router shell: app 56 | app.cmd 'test :my_param([0-9]+)', (req, res) -> 57 | should.be.ok false 58 | app.cmd 'test :my_param', (req, res) -> 59 | next() 60 | 61 | -------------------------------------------------------------------------------- /test/shell.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require 'should' 3 | shell = require '../lib/Shell' 4 | NullStream = require '../lib/NullStream' 5 | 6 | describe 'Shell', -> 7 | ### 8 | Note 9 | version 0.4.x didn't hold currentprocess if `process.stdin` 10 | was referenced, so `app.quit()` was not required 11 | ### 12 | it 'should construct with new call', -> 13 | app = new shell 14 | command: '' 15 | stdin: new NullStream 16 | stdout: new NullStream 17 | app.should.be.an.instanceof shell.Shell 18 | app.quit() 19 | it 'should construct with function call', -> 20 | app = shell 21 | command: '' 22 | stdin: new NullStream 23 | stdout: new NullStream 24 | app.should.be.an.instanceof shell.Shell 25 | app.quit() 26 | -------------------------------------------------------------------------------- /test/start_stop.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | path = require 'path' 4 | exists = fs.exists or path.exists 5 | should = require 'should' 6 | start_stop = require '../src/start_stop' 7 | 8 | describe 'StartStop', -> 9 | it 'should detach a child, start and stop', (next) -> 10 | cmd = "node #{__dirname}/start_stop/server.js" 11 | # Start the process 12 | start_stop.start cmd: cmd, detached: true, (err, pid) -> 13 | should.not.exist err 14 | pid.should.be.a.Number() 15 | # Check if process started 16 | start_stop.running pid, (err, running) -> 17 | should.not.exist err 18 | running.should.be.true 19 | # Stop process 20 | start_stop.stop cmd: cmd, detached: true, (err) -> 21 | should.not.exist err 22 | # Check if process stoped 23 | start_stop.running pid, (err, running) -> 24 | should.not.exist err 25 | running.should.be.False() 26 | next() 27 | it 'should detach a child and stop inactive process', (next) -> 28 | cmd = "node #{__dirname}/start_stop/server.js" 29 | # Stop process 30 | start_stop.stop cmd:cmd, detached: true, (err, stoped) -> 31 | should.not.exist err 32 | stoped.should.be.False() 33 | next() 34 | it 'should detach a child and stop inactive process with pidfile', (next) -> 35 | cmd = "node #{__dirname}/start_stop/server.js" 36 | pidfile = "#{__dirname}/start_stop/pidfile" 37 | fs.writeFile pidfile, "1234567", (err) -> 38 | # Check process doesnt exists 39 | start_stop.running 1234567, (err, running) -> 40 | should.not.exist err 41 | running.should.be.False() 42 | # Stop process 43 | start_stop.stop cmd:cmd, pidfile: pidfile, detached: true, (err, stoped) -> 44 | should.not.exist err 45 | stoped.should.be.False() 46 | # Pidfile shall be removed even if pid is invalid 47 | exists pidfile, (running) -> 48 | running.should.be.False() 49 | next() 50 | it 'should detach a child and honor the strict option', (next) -> 51 | # From the API 52 | # Send an error when a pid file exists and reference an unrunning pid. 53 | # Exist both in the start and stop functions 54 | # todo: test in start function 55 | cmd = "node #{__dirname}/start_stop/server.js" 56 | pidfile = "#{__dirname}/start_stop/pidfile" 57 | fs.writeFile pidfile, "1234567", (err) -> 58 | # Check process doesnt exists 59 | start_stop.running 1234567, (err, running) -> 60 | should.not.exist err 61 | running.should.be.False() 62 | # Stop process 63 | start_stop.stop cmd:cmd, pidfile: pidfile, strict: true, detached: true, (err, stoped) -> 64 | err.should.be.an.instanceof Error 65 | # Pidfile shall be removed even if pid is invalid 66 | exists pidfile, (running) -> 67 | running.should.be.False() 68 | next() 69 | it 'should detach a child and throw an error if pidfile not in directory', (next) -> 70 | cmd = "node #{__dirname}/start_stop/server.js" 71 | pidfile = "#{__dirname}/doesnotexist/pidfile" 72 | start_stop.start cmd:cmd, pidfile: pidfile, detached: true, (err, stoped) -> 73 | err.should.be.an.instanceof Error 74 | err.message.should.eql "Pid directory does not exist: #{__dirname}/doesnotexist." 75 | next() 76 | it 'should attach a child', (next) -> 77 | cmd = "node #{__dirname}/start_stop/server.js" 78 | # NOTE: the test fail on stop with error "Unexpected exit code 1" 79 | # when a test process is already running and binding the server port 80 | # An error should have been thrown in `start` instead of `stop` 81 | # since the process was never started. 82 | # Start the process 83 | start_stop.start cmd: cmd, detached: false, (err, pid) -> 84 | should.not.exist err 85 | pid.should.be.a.Number() 86 | # Check if process started 87 | start_stop.running pid, (err, running) -> 88 | should.not.exist err 89 | running.should.be.true 90 | # Stop process 91 | start_stop.stop pid, (err) -> 92 | should.not.exist err 93 | # Check if process stoped 94 | start_stop.running pid, (err, running) -> 95 | should.not.exist err 96 | running.should.be.False() 97 | next() 98 | # it 'should detach a child and restart on change', (next) -> 99 | # cmd = "node #{__dirname}/start_stop/server.js" 100 | # start_stop.start cmd: cmd, detached: true, watch: true, (err, pid) -> 101 | # should.not.exist err 102 | # pid.should.be.a 'number' 103 | # # Check if process started 104 | # start_stop.running pid, (err, running) -> 105 | # should.not.exist err 106 | # running.should.be.true 107 | # # Stop process 108 | # start_stop.stop pid, (err) -> 109 | # should.not.exist err 110 | # # Check if process stoped 111 | # start_stop.running pid, (err, running) -> 112 | # should.not.exist err 113 | # running.should.be.False() 114 | # next() 115 | -------------------------------------------------------------------------------- /test/start_stop/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Using reserved port `1783` to limit the risks of collision 4 | // See https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers 5 | 6 | var http = require('http'); 7 | http.createServer(function (req, res) { 8 | res.writeHead(200, {'Content-Type': 'text/plain'}); 9 | res.end('Hello World\n'); 10 | }).listen(1783, "127.0.0.1"); 11 | console.log('Server running at http://127.0.0.1:1783/'); -------------------------------------------------------------------------------- /test/start_stop/test_attach.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | start_stop = require '../../lib/start_stop' 4 | 5 | start_stop.start 6 | cmd: "#{__dirname}/server.js" 7 | detached: false 8 | , (err, pid) -> 9 | # Keep the process active 10 | -------------------------------------------------------------------------------- /test/styles.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require 'should' 3 | shell = require '../lib/Shell' 4 | styles = require '../lib/Styles' 5 | 6 | class Writer 7 | data: '' 8 | write: (data) -> @data += data 9 | 10 | describe 'Styles', -> 11 | it 'Test colors # no style', -> 12 | writer = new Writer 13 | styles( {stdout: writer} ) 14 | .println('Test default') 15 | writer.data.should.eql 'Test default\n' 16 | 17 | it 'Test colors # temporarily print bold then regular', -> 18 | writer = new Writer 19 | styles( {stdout: writer} ) 20 | .print('Test ').bold('bo').bold('ld').print(' or ').regular('reg').regular('ular').print(' and ').bold('bold').ln() 21 | writer.data.should.eql '\u001b[39m\u001b[22mTest \u001b[39m\u001b[22m\u001b[39m\u001b[1mbo\u001b[39m\u001b[22m\u001b[39m\u001b[1mld\u001b[39m\u001b[22m\u001b[39m\u001b[22m or \u001b[39m\u001b[22m\u001b[39m\u001b[22mreg\u001b[39m\u001b[22mular\u001b[39m\u001b[22m and \u001b[39m\u001b[22m\u001b[39m\u001b[1mbold\u001b[39m\u001b[22m\n' 22 | 23 | it 'Test colors # definitely pass to bold', -> 24 | writer = new Writer 25 | styles( {stdout: writer} ) 26 | .print('Test ').bold().print('bo').print('ld').regular().print(' or ').print('reg').print('ular').print(' and ').bold().print('bo').print('ld').regular().ln() 27 | writer.data.should.eql '\u001b[39m\u001b[22mTest \u001b[39m\u001b[22m\u001b[39m\u001b[1m\u001b[39m\u001b[1mbo\u001b[39m\u001b[1m\u001b[39m\u001b[1mld\u001b[39m\u001b[1m\u001b[39m\u001b[22m\u001b[39m\u001b[22m or \u001b[39m\u001b[22m\u001b[39m\u001b[22mreg\u001b[39m\u001b[22m\u001b[39m\u001b[22mular\u001b[39m\u001b[22m\u001b[39m\u001b[22m and \u001b[39m\u001b[22m\u001b[39m\u001b[1m\u001b[39m\u001b[1mbo\u001b[39m\u001b[1m\u001b[39m\u001b[1mld\u001b[39m\u001b[1m\u001b[39m\u001b[22m\n' 28 | 29 | it 'Test colors # temporary print green then blue', -> 30 | writer = new Writer 31 | styles( {stdout: writer} ) 32 | .print('Test ').green('gre').green('en').print(' or ').blue('bl').blue('ue').print(' and ').green('green').ln() 33 | writer.data.should.eql '\u001b[39m\u001b[22mTest \u001b[39m\u001b[22m\u001b[32m\u001b[22mgre\u001b[39m\u001b[22m\u001b[32m\u001b[22men\u001b[39m\u001b[22m\u001b[39m\u001b[22m or \u001b[39m\u001b[22m\u001b[34m\u001b[22mbl\u001b[39m\u001b[22m\u001b[34m\u001b[22mue\u001b[39m\u001b[22m\u001b[39m\u001b[22m and \u001b[39m\u001b[22m\u001b[32m\u001b[22mgreen\u001b[39m\u001b[22m\n' 34 | 35 | it 'Test colors # definitely pass to green', -> 36 | writer = new Writer 37 | styles( {stdout: writer} ) 38 | .print('Test ').green().print('gre').print('en').nocolor(' or ').blue().print('bl').print('ue').nocolor(' and ').green().print('gre').print('en').ln() 39 | .reset() 40 | writer.data.should.eql '\u001b[39m\u001b[22mTest \u001b[39m\u001b[22m\u001b[32m\u001b[22m\u001b[32m\u001b[22mgre\u001b[32m\u001b[22m\u001b[32m\u001b[22m\u001b[32m\u001b[22men\u001b[32m\u001b[22m\u001b[32m\u001b[22m\u001b[39m\u001b[22m or \u001b[32m\u001b[22m\u001b[32m\u001b[22m\u001b[34m\u001b[22m\u001b[34m\u001b[22mbl\u001b[34m\u001b[22m\u001b[34m\u001b[22m\u001b[34m\u001b[22mue\u001b[34m\u001b[22m\u001b[34m\u001b[22m\u001b[39m\u001b[22m and \u001b[34m\u001b[22m\u001b[34m\u001b[22m\u001b[32m\u001b[22m\u001b[32m\u001b[22mgre\u001b[32m\u001b[22m\u001b[32m\u001b[22m\u001b[32m\u001b[22men\u001b[32m\u001b[22m\u001b[32m\u001b[22m\n\u001b[39m\u001b[22m' 41 | --------------------------------------------------------------------------------