UNPKG

frameless

Version:

a tiny framework to write command line programs

987 lines (885 loc) 23.9 kB
var optimist = require('optimist') , prompt = require('prompt') , path = require('path') , eyes = require('eyes') , crypto = require('crypto') , fs = require('fs') , async = require('async') , keypress = require('keypress') , tty = require('tty') , readline = require('readline') , rl ; var frameless , inspect , VERBOSE = process.env.CLIFV || false , SAVE = process.env.CLIFS || false , CONFIRM = process.env.CLIFC || false // // make control+c respond by exiting // , kp_listeners = [{name: 'c', f: function () { process.stdin.pause(); }}] ; // ———————————————————————————————————————————————————————————————— prompts ~~ // // function `relax` // // gets a bunch of things it needs and a callback continuation function // frameless = function relax() { // // last argument is the callback, rest are required things // var cb = [].pop.call(arguments) , override = frameless.argv ; // // sometimes we want to override the `argv` // when we need to do this we give the argv object as the last argument // so the callback becomes the one before the last and so fourth // if (typeof cb !== 'function') { override = cb; cb = [].pop.call(arguments); } var args = arguments; // // get our configuration from the dotfile // frameless.load_dotfile(function (cfg) { // // set our PS1 // // prompt.message = frameless.PS1; // // if people are not running in setup mode we can just set the values with // the config we got // // this however looses precedent to command line inputed options // so --name will win over property `name` // if(!frameless.argv.setup) { Object.keys(cfg).forEach(function (k) { if(!override[k]) { if (VERBOSE) { frameless.silly('set to ' + cfg[k], k); } override[k] = cfg[k]; } }); } // // if they are in set up mode we need to at the very least unlink their // config file so the end user does not get stranded in loops trying to // fix the dotfile // else { frameless.destroy_dotfile(); } // // override with whatever arguments where passed // or something passed to this function // prompt.override = override; prompt.delimiter = ''; // // start our prompt // prompt.start(); // // gather information on if we should or not save each options // var prompt_save = {} , prompt_load = {} ; // // gather prompts from remaining arguments (last is `cb`) // var prompt_info = [].reduce.call(args, function(ac, arg) { // // strings need to be put in format expected by prompt // if you only passed a string then it's required // var prop = typeof arg === 'string' ? { name : arg , required : true } : arg ; // // check for save/no-save // if(typeof prop.save === 'boolean') { prompt_save[prop.name] = prop.save; } // // default to save // else { prompt_save[prop.name] = true; } // // determine what we need to load // if(prop.load) { prompt_load[prop.name] = typeof prop.load === 'string' ? prop.load : 'utf-8' ; } // // make sure user is not trying to use a reserved flag // frameless.reserved_options.forEach(function (word) { if (prop.name === word) { frameless.err( '`--' + word + '` is an reserved option. please rename it'); process.exit(); } }); // // make sensitive info sensitive // frameless.sensitive_options.forEach(function(word) { if (~prop.name.toLowerCase().indexOf(word.toLowerCase())) { prop.hidden = true; } }); // // if we already have this option in our cfg make whats in cfg // the default to save the end user time retyping everything // // dont do this for encrypted things as it would show unencrypted // results in plain text // if(cfg[prop.name] && !prop.hidden) { prop['default'] = cfg[prop.name]; } // // accumulate the transformed option // ac.push(prop); // // return our `nascent` object // return ac; }, []); if(VERBOSE) { frameless.silly(prompt_info, 'prompt'); } // // halp: --help, --halp, -h, --usage // if (override.help || override.h || override.halp || override.usage) { frameless.usage(prompt_info); return; } // // version // if (override.version || override.v) { frameless.pp_version(); return; } // // read the variables that are not passed by `optimist` // prompt.get(prompt_info, function(err, p) { if (err) { return frameless.err(err, true); } function return_to_sender() { if(frameless.argv.setup) { frameless.info(frameless.dotfile, '������'); } else { cb(p); } } var keys = Object.keys(p) , to_load = keys.filter( function filter_files_to_load(prop) { return prompt_load[prop]; }) ; if(VERBOSE) { frameless.silly(p, 'input'); } async.map(to_load , function read_file(prop, cb) { var encoding = prompt_load[prop]; // // people can specify load as a boolean or encoding // boolean value true will default to utf8 // encoding = typeof encoding === 'string' ? encoding : 'utf8'; // // read the file // fs.readFile(p[prop], encoding, cb); } , function (err, responses) { if (err) { return frameless.err(err, true); } // // update our config file according to what was passed by prompt/argv // do not set things that are not supposed to be saved // keys.forEach(function (k) { if(prompt_save[k]) { cfg[k] = p[k]; } }); // // replace the values of our prompt with the files loaded // with `load: true` // for(var i = 0; i < to_load.length; i++) { p[to_load[i]] = responses[i]; } // // check if we can skip the confirmation prompt // if(!CONFIRM || frameless.argv.yes || frameless.argv.y) { frameless.save_dotfile(cfg, return_to_sender); return; } // // confirmation prompt // prompt.get( { name : 'continue' , description : 'are you sure?' , required : true } , function(err, confirm) { if (err) { return frameless.err(err, true); } if (~['y', 'yes'].indexOf(confirm["continue"].toLowerCase())) { do_ttymagic(); frameless.save_dotfile(cfg, return_to_sender); return; } else { return frameless.aborted(confirm["continue"]); } }); }); }); }); }; // ——————————————————————————————————————————————————————————————— defaults ~~ // // stuff like raw mode, and help can be auto generated for you // frameless.is_tty = false; function do_ttymagic() { if(tty.isatty(process.stdin)) { frameless.is_tty = true; rl = readline.createInterface(process.stdin, process.stdout); // // make `process.stdin` begin emitting "keypress" events // keypress(process.stdin); // // listen for the `keypress` event // process.stdin.on('keypress', function (ch, key) { // // iterate our kp_listeners // for (var i = 0; i < kp_listeners.length; i++) { var kp = kp_listeners[i]; // // if we have something to do // if(key && key.ctrl && key.name === kp.name) { // // do eet // kp.f(); break; } } }); process.stdin.setRawMode(true); process.stdin.resume(); } } // // function `keypress` // // register listeners for control+key // frameless.keypress = function (name, f) { kp_listeners.push({name: name, f: function () { if (VERBOSE) { frameless.silly('keypress on control+'+name, name); } f(); }}); }; frameless.kp_listeners = kp_listeners; // // remove color output // frameless.raw = function () { [ 'bold', 'underline', 'strikethrough', 'italic', 'inverse', 'grey' , 'black', 'yellow', 'red', 'green', 'blue', 'white', 'cyan' , 'magenta'].forEach(function(i) { Object.defineProperty(String.prototype, i, { get: function() { return this; } }); }); inspect = eyes.inspector({ stream: null,styles: {} }); }; // // `raw`, aka `no-colors` mode // if (optimist.argv.raw) { frameless.raw(); } // // default is colors mode // else { inspect = eyes.inspector({ stream: null }); } // // verbose mode outputs more stuff // if (optimist.argv.verbose) { VERBOSE = true; } if (optimist.argv["no-verbose"]) { VERBOSE = false; } // // save stuff into a dotfile for reuse // if (optimist.argv.save) { SAVE = true; } if (optimist.argv["no-save"]) { SAVE = false; } // ————————————————————————————————————————————————————————————— hackers <3 ~~ // // these are things you can customize from your scripts // if you think i did a wrong choice with the defaults send in a pr // // if you wish this was modular the right way, write the code and send a pr // you might want to ask if i think that is the right way, i really dislike // complicated code bases. don't we all? // // // give users direct access to argv and other fun stuff from optimist // frameless.optimist = optimist; frameless.argv = optimist.argv; // // allow people to know if we are in `verbose`, `confirm`, and `save` // frameless.save = function (b) { if(typeof b === 'boolean') { if(VERBOSE) { frameless.silly('set save to ' + b); } SAVE = b; } else { return SAVE; } }; frameless.verbose = function (b) { if(typeof b === 'boolean') { if(VERBOSE) { frameless.silly('set verbose to ' + b); } VERBOSE = b; } else { return VERBOSE; } }; frameless.confirm = function (b) { if(typeof b === 'boolean') { if(VERBOSE) { frameless.silly('set confirm to ' + b); } CONFIRM = b; } else { return CONFIRM; } }; // // allow to override the name of the dotfile file // default to null which will make frameless resolve to program name // frameless.dotfile_path = function (name) { if(VERBOSE) { frameless.silly('set dotfile to ' + name); } frameless.dotfile = path.join(frameless.tilde, '.' + name + '.f.json'); }; // // the program name // frameless.program_name = frameless.argv.$0.replace(/ |\/|\\/g, '_') || 'ƒ'; // // default encryption key // please override with your own // frameless.key = [ 'c11094d856cae4ac4f2e0a1dcf68ba2ad50d313ce6495c12a8c86792243e9094' , '6de079d0c32d690bd20948ca2fc7fafcc271f855125365b6776d4de4ac8d2296' , '144e1c7c3da392db710bcdb390de33c3169e420b3295f274b3e14fb89b84e2b7' , '3587ad4985305a0f242b507de062aa6186a84668a997fb08cb6cd9b37dba2008' , 'c520c5e586f2e21355bf3ff88a0630bfd8b1040291da66c9e6eb53adb50b7006' , 'fd807084c26576ed020517aa71cd7d094e9a66eafc520c9771e97c4759f42956' , '990bd7a3b96629bd0cbf44bf32085a20cbfb06dbb80f8b7903845b6f58484750' , '65d1041ebf52ecf8c8bd54465f54e80ba2a96d682a50eaae2205d9f0df8aafea' , 'f6fb5897b68e44602ed08b2119c468fcf726691bea23f6755922bd17f842389b' , 'e37f4e621eff6f1dd6ba08613913eff2848dc07f8d7b4b8ce29bb315a714ffc8' , 'c07b94cde54f3c19b5cd2a2a22b2ef3ebb6d197a33ec36fbb62b801f11494ffb' , 'fc6abb102def3729d211d07a8931a17553fe794f4abcca24148a418ea7396538' , 'cf550f9f427eeaa5cb44fbc9bc98d44a7b30c18633afd16a25cc39af4f8366c6' , '563409962525f5f9f285b13a3575f20fb0bb39103a00c1739dfc7333acf5dd18' , 'b47bf744140ded6bd1a60c4aec6db311245228aac2706cdc5a8f1befd8315b0a' , '4b9f993bee4f78f2f8bdca27199c6dbad50c56fa41ea795898ae607772d2056c' , 'badd2484bf7d24ece629b93042a4bfbb92c272f9836919c117483b7db4a71876' , '9c26996f0a47e54e4fc3db3e40339939abe1056955ec83b908d75e117c3466d0' ].join(''); // // so i can complain if you dont change this // frameless.original_key = frameless.key; // // which algorithm to use to encrypt the key // frameless.cipher_type = 'aes-256-cbc'; // // home directory // frameless.tilde = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'] ; // // the current `ƒ` version // frameless.version = require('../package.json').version; // // logger function, should behave like console.log // frameless.logger = console.log; // // info not to be logged *ever* // frameless.sensitive_options = [ 'password' , 'auth' , 'key' , 'apiKey' , 'secret' , 'apiSecret' ] ; // // reserved arguments // frameless.reserved_options = [ 'h' , 'help' , 'halp' , 'usage' , 'raw' , 'verbose' , 'v' , 'version' , 'json' , 'save' , 'y' , 'yes' , 'no-verbose' , 'no-save' , 'setup' ] ; // —————————————————————————————————————————————————————————————————— style ~~ // // default messages and colors for different log levels // frameless.msg = { ok : { label: 'λ', color: 'grey' } , err : { label: 'ℯ', color: 'red' } , warn : { label: '!', color: 'yellow' } , info : { label: 'ⅰ', color: 'blue' } , silly: { label: '☃', color: 'magenta'} , dot : { label: '.', color: 'green' } } ; // // default usage function // frameless.usage = function (opts) { // // print for all program specific options // var all_opts = opts.map(function(req) { return frameless.pp_option( { v : req.name , d : req['default'] ? req['default'] : null } ); }); // // print for all frameless internal options // if(SAVE) { all_opts.push(frameless.pp_option({v: 'setup'})); } all_opts.push(frameless.pp_option({v: 'verbose'})); all_opts.push(frameless.pp_option({v: 'help', s: 'h'})); all_opts.push(frameless.pp_option({v: 'version', s: 'v'})); if(CONFIRM) { all_opts.push(frameless.pp_option({v: 'yes', s: 'y'})); } var usage = [ frameless.program_name , all_opts.join('\n') ].join('\n') ; frameless.ok(usage, frameless.version); }; // // function `pp_version` // // prints the version // frameless.pp_version = function () { frameless.ok(frameless.version, 'version'); }; // // PS1 dawg // frameless.PS1 = 'ƒ' + '. '.green; // // function `pp` // // returns a pretty print string for logging // // this is an `internal` function not meant to be consumed by apps // but as in everything that is good, yes you can! // // args: // @level: one of the levels as defined in `frameless.msg` // @msg : the message to log, can either be a string or an object // [dude a string is an object, dur dur dur] // @opts : currently a key, which replaces `level.label` as the message to // be logged and omit_newline if you don't want a new line before // your pretty printed output // // frameless.pp = function (level, msg, opts) { opts = opts || {}; msg = typeof msg === 'string' ? msg : frameless.argv.raw ? JSON.stringify(msg, null, 2) : inspect(msg) ; // // default the level to info in an invalid one was provided // if(!frameless.msg[level]) { if(VERBOSE) { frameless.silly('invalid level <' + level + '> changed to info.'); } level = 'info'; } var _n = opts.newline ? '\n' : ''; var _k = typeof opts.key === 'string' ? opts.key : null; // // on non raw requests we can default the key to the default key label // if(!_k && !frameless.argv.raw) { _k = frameless.msg[level].label; } if(_k) { return _n + _k[frameless.msg[level].color] + frameless.msg.dot.label[frameless.msg.dot.color] + ' ' + msg; } else { return _n + msg[frameless.msg[level].color]; } }; // // function `pp_option` // // prints out option for usage // frameless.pp_option = function (opt) { var s_opt = []; // // shortcut options, `-v` // if(typeof opt.s === 'string') { s_opt.push((opt.s.length === 1 ? '—' : '--').green + opt.s); } // // verbose options, `--version` // if(typeof opt.v === 'string') { s_opt.push((opt.v.length === 1 ? '—' : '--').green + opt.v); } // // `d` is default value // return s_opt.join(', '.grey) + ' ' + (opt.d || '').grey; }; // // function `aborted` // // prints when you abort an operation on prompt // frameless.aborted = function (msg) { frameless.err('aborted by you. ' + '['.blue + msg + ']'.blue); }; // // function `err` // // pretty prints an error // frameless.err = function(err, newline) { var to_print = VERBOSE ? err : (err.message || err); frameless.logger( frameless.pp('err', to_print, { newline: newline , key: frameless.msg.err.label }) ); }; // // function `ok`, `warn`, `info`, `silly` // // exposes methods to pretty print messages on these levels // ['ok', 'warn', 'info', 'silly'].forEach(function (l) { frameless[l] = function(msg, key) { frameless.logger(frameless.pp(l, msg, {key: key})); }; }); // // function `print` // // just a default callback helper that does what you expect // if theres an error its prints it as such, // if not it prints the success // frameless.print = function(err, response) { if (err) { return frameless.err(err, true); } frameless.ok(response); }; // ————————————————————————————————————————————————————————————————— crypto ~~ // // function `encrypt` // // encrypts some shit, namely @wat // frameless.encrypt = function (wat) { if(VERBOSE) { if (frameless.key === frameless.original_key) { frameless.silly('you should use a different key', 'key'); } frameless.silly('encrypting ' + wat.length + ' chars'); } try { var cipher = crypto.createCipher(frameless.cipher_type, frameless.key) , ret = cipher.update(wat, 'utf8', 'hex') + cipher.final('hex') ; return ret; } catch (e) { if(VERBOSE) { frameless.silly(wat + ' failed to encrypt: ' + e.message); } return; } }; // // function `decrypt` // // dude, common. yu reading thiz? // frameless.decrypt = function (encoded) { if(VERBOSE) { if (frameless.key === frameless.original_key) { frameless.silly('you should use a different key', 'key'); } frameless.silly('decrypting ' + encoded); } try { var decipher = crypto.createDecipher(frameless.cipher_type, frameless.key) , ret = decipher.update(encoded, 'hex', 'utf8') + decipher.final('utf8') ; return ret; } catch (e) { if(VERBOSE) { frameless.silly(encoded + ' failed to decrypt: ' + e.message); } return; } }; // ——————————————————————————————————————————————————————————————— .dotfile ~~ frameless.dotfile_path(frameless.program_name); // // try to get the user rsa key as default // var key_path = path.join(frameless.tilde, '.ssh', 'id_rsa'); try { key = fs.readFileSync(key_path, 'utf-8'); if(VERBOSE) { frameless.silly('loaded rsa key from ' + key_path); } } catch (e) { if(VERBOSE) { frameless.silly('user private key was not in ' + key_path + '. using default key'); } } // // function `load_dotfile` // // loads a dotfile, if one does not exist it returns an empty object // frameless.load_dotfile = function (cb) { if(VERBOSE) { frameless.silly(frameless.dotfile, 'load'); } var cfg = {}; fs.readFile(frameless.dotfile, 'utf8', function (err, cfg) { // // if there's no file give an empty object // if(err) { return cb({}); } try { cfg = JSON.parse(cfg); // // decode each of the encrypted thingies // Object.keys(cfg).forEach(function (k) { for(var i=0; i < frameless.sensitive_options.length; i++) { var sensitive_word = frameless.sensitive_options[i].toLowerCase(); if(~k.toLowerCase().indexOf(sensitive_word)) { cfg[k] = frameless.decrypt(cfg[k]); break; } } }); return cb(cfg); } catch (e) { if(VERBOSE) { frameless.silly('load failed for ' + frameless.dotfile + ' due to reasons ' + e.message); } return cb({}); } }); }; // // function `save_dotfile` // // saves the dotfile // the return value in the callback informs if a save operation was performed // frameless.save_dotfile = function (cfg, cb) { if(VERBOSE) { frameless.silly(frameless.dotfile, 'save'); } if(!SAVE) { return cb(false); } // // no updates, just leave // if(!Object.keys(cfg).length) { if(VERBOSE) { frameless.silly('nothing to save'); } return cb(false); } // // encrypt all the sensitive thingies // Object.keys(cfg).forEach(function (k) { for(var i=0; i < frameless.sensitive_options.length; i++) { var sensitive_word = frameless.sensitive_options[i].toLowerCase(); if(~k.toLowerCase().indexOf(sensitive_word)) { cfg[k] = frameless.encrypt(cfg[k]); break; } } }); // // actually write the file // fs.writeFile(frameless.dotfile, JSON.stringify(cfg, null, 1), function (e) { if(e) { if(VERBOSE) { frameless.silly('save failed for ' + frameless.dotfile + ' due to reasons ' + e.message); } return cb(false); } // // set modes so only the end user can read and no other user // with access to the laptop/computer/television/whatever // if(process.platform !== 'win32') { fs.chmod(frameless.dotfile, '600', function (err) { return cb(true); }); } // // just continue then // else { return cb(true); } }); }; // // function `destroy_dotfile` // // unlinks a dotfile // frameless.destroy_dotfile = function (cb) { cb = cb || function (){}; if(VERBOSE) { frameless.silly(frameless.dotfile, 'unlink'); } fs.exists(frameless.dotfile, function (exists) { if(exists) { fs.unlink(frameless.dotfile, function (err) { if(err && VERBOSE) { frameless.silly('unlink failed for ' + frameless.dotfile + ' due to reasons ' + err.message); } cb(); }); } else { return cb(); } }); }; // ———————————————————————————————————————————————————————————————— .ohai!1 ~~ module.exports = frameless;