UNPKG

33.8 kBJavaScriptView Raw
1"use strict";
2
3/**
4 * Module dependencies.
5 */
6
7var _ = require("lodash")
8 , EventEmitter = require("events").EventEmitter
9 , Command = require("./command")
10 , VantageServer = require("./server")
11 , VantageClient = require("./client")
12 , VantageUtil = require("./util")
13 , ui = require("./ui")
14 , Session = require("./session")
15 , intercept = require("./intercept")
16 , commons = require("./vantage-commons")
17 , basicAuth = require("vantage-auth-basic")
18 , os = require("os")
19 , minimist = require("minimist")
20 , npm = require("npm")
21 , repl = require("vantage-repl")
22 , temp = require("temp")
23 , chalk = require("chalk")
24 ; require("native-promise-only")
25 ;
26
27/**
28 * Initialize a new `Vantage` instance.
29 *
30 * @return {Vantage}
31 * @api public
32 */
33
34function Vantage() {
35
36 if (!(this instanceof Vantage)) { return new Vantage(); }
37
38 // Program version
39 // Exposed through vantage.version(str);
40 this._version = "";
41
42 // Registered `vantage.command` commands and
43 // their options.
44 this.commands = [];
45
46 // Queue of IP requests, executed async, in sync.
47 this._queue = [];
48
49 // Current command being executed.
50 this._command = void 0;
51
52 // Expose UI.
53 this.ui = ui;
54
55 // Exposed through vantage.delimiter(str).
56 this._delimiter = "local@" + String(os.hostname()).split(".")[0] + "~$ ";
57 ui.setDelimiter(this._delimiter);
58
59 // Banner to display on login to a system. If null,
60 // doesn't display a banner.
61 this._banner = void 0;
62
63 // Vantage client connects to other instances
64 // of Vantage.
65 this.client = new VantageClient(this);
66
67 // Vantage server receives connections from
68 // other vantages. Activated by vantage.listen();
69 this.server = new VantageServer(this);
70
71 // Whether all stdout is being hooked through a function.
72 this._hooked = false;
73
74 // Expose common utilities, like padding.
75 this.util = VantageUtil;
76
77 // If authentication is used, it is called through this fn.
78 this._authFn = void 0;
79
80 // Active vantage server session.
81 this.session = new Session({
82 local: true,
83 user: "local",
84 parent: this,
85 delimiter: this._delimiter
86 });
87
88 this._init();
89 return this;
90}
91
92/**
93 * Vantage prototype.
94 */
95
96var vantage = Vantage.prototype;
97
98/**
99 * Extend Vantage prototype as an event emitter.
100 */
101
102Vantage.prototype.__proto__ = EventEmitter.prototype;
103
104/**
105 * Expose `Vantage`.
106 */
107
108exports = module.exports = Vantage;
109
110/**
111 * Extension to `constructor`.
112 * @api private
113 */
114
115Vantage.prototype._init = function() {
116 var self = this;
117
118 ui.on("vantage_ui_keypress", function(data){
119 self._onKeypress(data.key, data.value);
120 });
121
122 self
123 .use(commons)
124 .use(repl);
125};
126
127/**
128 * Sets version of your application's API.
129 *
130 * @param {String} version
131 * @return {Vantage}
132 * @api public
133 */
134
135vantage.version = function(version) {
136 this._version = version;
137 return this;
138};
139
140/**
141 * Sets the permanent delimiter for this
142 * Vantage server instance.
143 *
144 * @param {String} str
145 * @return {Vantage}
146 * @api public
147 */
148
149vantage.delimiter = function(str) {
150 this._delimiter = str;
151 if (this.session.isLocal() && !this.session.client) {
152 this.session.delimiter(str);
153 }
154 return this;
155};
156
157/**
158 * Programatically connect to another server
159 * instance running Vantage.
160 *
161 * @param {Server} server
162 * @param {Integer} port
163 * @param {Object} options
164 * @param {Function} cb
165 * @return {Promise}
166 * @api public
167 */
168
169vantage.connect = function(server, port, options, cb) {
170 return this.client.connect.call(this.client, server, port, options, cb);
171};
172
173/**
174 * Imports a library of Vantage API commands
175 * from another Node module as an extension
176 * of Vantage.
177 *
178 * @param {Array} commands
179 * @return {Vantage}
180 * @api public
181 */
182
183vantage.use = function(commands, options) {
184 if (!commands) { return this; }
185 if (_.isFunction(commands)) {
186 commands.call(this, this, options);
187 } else if (_.isString(commands)) {
188 return this.use(require(commands), options);
189 } else {
190 commands = _.isArray(commands) ? commands : [commands];
191 for (var i = 0; i < commands.length; ++i) {
192 var cmd = commands[i];
193 if (cmd.command) {
194 var command = this.command(cmd.command);
195 if (cmd.description) {
196 command.description(cmd.description);
197 }
198 if (cmd.options) {
199 cmd.options = _.isArray(cmd.options) ? cmd.options : [cmd.options];
200 for (var j = 0; j < cmd.options.length; ++j) {
201 command.option(cmd.options[j][0], cmd.options[j][1]);
202 }
203 }
204 if (cmd.action) {
205 command.action(cmd.action);
206 }
207 }
208 }
209 }
210 return this;
211};
212
213/**
214 * Requires a vantage module / middleware and
215 * and `.use`s it. If the module doesn't exist
216 * locally, it will NPM install it into a temp
217 * directory and then use it.
218 *
219 * @param {String} key
220 * @param {String} value
221 * @return {Function}
222 * @api private
223 */
224
225vantage._use = function(options, callback) {
226
227 var self = this
228 , config
229 , registeredCommands = 0
230 ;
231
232 options = (_.isString(options))
233 ? { module: options }
234 : (options || {});
235
236 options = _.defaults(options, {
237 loglevel: "silent"
238 });
239
240 config = {
241 loglevel: options.loglevel,
242 production: true
243 };
244
245 function registryCounter() {
246 registeredCommands++;
247 }
248
249 function load(cbk) {
250 npm.load(config, function(){
251 npm.registry.log.level = config.loglevel;
252 npm.commands.install(temp.dir, [options.module], function(err, data){
253 if (err) {
254 cbk(err, data);
255 } else {
256 var dir = temp.dir + "/node_modules/" + options.module;
257 var mod = require(dir);
258 cbk(void 0, mod);
259 }
260 });
261 });
262 }
263
264 load(function(err, mod){
265 if (err) {
266 callback(true, "Error downloading module: " + mod);
267 } else {
268 self.on("command_registered", registryCounter);
269 self.use(mod);
270 self.removeListener("command_registered", registryCounter);
271 var data = {
272 registeredCommands: registeredCommands
273 };
274 callback(void 0, data);
275 }
276 });
277
278};
279
280/**
281 * Write a banner on remote login.
282 *
283 * @param {String} banner
284 * @return {Vantage}
285 * @api public
286 */
287
288vantage.banner = function(banner) {
289 this._banner = banner || void 0;
290 return this;
291};
292
293/**
294 * Registers a new command in the vantage API.
295 *
296 * @param {String} name
297 * @param {String} desc
298 * @param {Object} opts
299 * @return {Command}
300 * @api public
301 */
302
303vantage.command = function(name, desc, opts) {
304 opts = opts || {};
305 name = String(name);
306
307 var argsRegExp = /(\[[^\]]*\]|\<[^\>]*\>)/g;
308 var args = [];
309 var arg;
310
311 while ((arg = argsRegExp.exec(name)) != null) {
312 args.push(arg[1]);
313 }
314
315 var cmdNameRegExp = /^([^\[\<]*)/;
316 var cmdName = cmdNameRegExp.exec(name)[0].trim();
317
318 var cmd = new Command(cmdName, exports);
319
320 if (desc) {
321 cmd.description(desc);
322 this.executables = true;
323 }
324 cmd._noHelp = !!opts.noHelp;
325 cmd._mode = opts.mode || false;
326 cmd._catch = opts.catch || false;
327 cmd._parseExpectedArgs(args);
328 cmd.parent = this;
329
330 var exists = false;
331 for (var i = 0; i < this.commands.length; ++i) {
332 exists = (this.commands[i]._name === cmd._name) ? true : exists;
333 if (exists) {
334 this.commands[i] = cmd;
335 break;
336 }
337 }
338 if (!exists) {
339 this.commands.push(cmd);
340 }
341
342 this.emit("command_registered", { command: cmd, name: name });
343
344 return cmd;
345};
346
347/**
348 * Registers a new "mode" command in the vantage API.
349 *
350 * @param {String} name
351 * @param {String} desc
352 * @param {Object} opts
353 * @return {Command}
354 * @api public
355 */
356
357vantage.mode = function(name, desc, opts) {
358 return this.command(name, desc, _.extend((opts || {}), { mode: true }));
359};
360
361/**
362 * Registers a "catch" command in the vantage API.
363 * This is executed when no command matches are found.
364 *
365 * @param {String} name
366 * @param {String} desc
367 * @param {Object} opts
368 * @return {Command}
369 * @api public
370 */
371
372vantage.catch = function(name, desc, opts) {
373 return this.command(name, desc, _.extend((opts || {}), { catch: true }));
374};
375
376
377/**
378 * Delegates to ui.log.
379 *
380 * @param {String} log
381 * @return {Vantage}
382 * @api public
383 */
384
385vantage.log = function() {
386 this.ui.log.apply(this.ui, arguments);
387 return this;
388};
389
390/**
391 * Intercepts all logging through `vantage.log`
392 * and runs it through the function declared by
393 * `vantage.pipe()`.
394 *
395 * @param {Function} fn
396 * @return {Vantage}
397 * @api public
398 */
399
400vantage.pipe = function(fn) {
401 if (this.ui) {
402 this.ui._pipeFn = fn;
403 }
404 return this;
405};
406
407/**
408 * If Vantage is the local terminal,
409 * hook all stdout, through a fn.
410 *
411 * @return {Vantage}
412 * @api private
413 */
414
415vantage.hook = function(fn) {
416 if (fn !== undefined) {
417 this._hook(fn);
418 } else {
419 this._unhook();
420 }
421 return this;
422};
423
424/**
425 * Unhooks stdout capture.
426 *
427 * @return {Vantage}
428 * @api public
429 */
430
431vantage._unhook = function() {
432 if (this._hooked && this._unhook !== undefined) {
433 this._unhook();
434 this._hooked = false;
435 }
436 return this;
437};
438
439/**
440 * Hooks all stdout through a given function.
441 *
442 * @param {Function} fn
443 * @return {Vantage}
444 * @api public
445 */
446
447vantage._hook = function(fn) {
448 if (this._hooked && this._unhook !== undefined) {
449 this._unhook();
450 }
451 this._unhook = intercept(fn);
452 this._hooked = true;
453 return this;
454};
455
456/**
457 * Hook the tty prompt to this given instance
458 * of vantage.
459 *
460 * @return {Vantage}
461 * @api public
462 */
463
464vantage.show = function() {
465 ui.attach(this);
466 return this;
467};
468
469/**
470 * Disables the vantage prompt on the
471 * local terminal.
472 *
473 * @return {Vantage}
474 * @api public
475 */
476
477vantage.hide = function() {
478 ui.detach(this);
479 return this;
480};
481
482/**
483 * Listener for a UI keypress. Either
484 * handles the keypress locally or sends
485 * it upstream.
486 *
487 * @param {String} key
488 * @param {String} value
489 * @api private
490 */
491
492vantage._onKeypress = function(key, value) {
493 var self = this;
494 if (this.session.isLocal() && !this.session.client) {
495 this.session.getKeypressResult(key, value, function(err, result) {
496 if (result !== undefined) {
497 if (_.isArray(result)) {
498 var formatted = VantageUtil.prettifyArray(result);
499 self.ui.imprint();
500 self.session.log(formatted);
501 } else {
502 self.ui.redraw(result);
503 }
504
505 //self.ui.redraw(result);
506 }
507 });
508 } else {
509 this._send("vantage-keypress-upstream", "upstream", {
510 key: key,
511 value: value,
512 sessionId: this.session.id
513 });
514 }
515};
516
517/**
518 * For use in vantage API commands, sends
519 * a prompt command downstream to the local
520 * terminal. Executes a prompt and returns
521 * the response upstream to the API command.
522 *
523 * @param {Object} options
524 * @param {Function} cb
525 * @return {Vantage}
526 * @api public
527 */
528
529vantage.prompt = function(options, cb) {
530 var self = this;
531 options = options || {};
532 var ssn = self.getSessionById(options.sessionId);
533
534 if (!ssn) {
535 throw new Error("Vantage.prompt was called without a passed Session ID.");
536 }
537
538 function handler(data) {
539 var response = data.value;
540 self.removeListener("vantage-prompt-upstream", handler);
541 cb(response);
542 }
543
544 if (ssn.isLocal()) {
545 ui.setDelimiter(options.message || ssn.delimiter);
546 ui.prompt(options, function(result) {
547 ui.setDelimiter(ssn.delimiter);
548 cb(result);
549 });
550 } else {
551 self.on("vantage-prompt-upstream", handler);
552 self._send("vantage-prompt-downstream", "downstream", { options: options, value: void 0, sessionId: ssn.id });
553 }
554 return self;
555};
556
557/**
558 * Renders the CLI prompt or sends the
559 * request to do so downstream.
560 *
561 * @param {Object} data
562 * @return {Vantage}
563 * @api private
564 */
565
566vantage._prompt = function(data) {
567 var self = this;
568 data = data || {};
569 if (!data.sessionId) {
570 data.sessionId = self.session.id;
571 }
572 var ssn = self.getSessionById(data.sessionId);
573
574 // If we somehow got to _prompt and aren't the
575 // local client, send the command downstream.
576 if (!ssn.isLocal()) {
577 this._send("vantage-resume-downstream", "downstream", { sessionId: data.sessionId });
578 return self;
579 }
580
581 if (ui.midPrompt()) { return self; }
582
583 ui.prompt({
584 type: "input",
585 name: "command",
586 message: ssn.fullDelimiter()
587 }, function(result){
588 if (self.ui._cancelled === true) { self.ui._cancelled = false; return; }
589 var str = String(result.command).trim();
590 self.emit("client_prompt_submit", str);
591 if (str === "" || str === "undefined") { self._prompt(data); return; }
592 self.exec(str, function(){
593 self._prompt(data);
594 });
595 });
596
597 return self;
598};
599
600/**
601 * Executes a vantage API command and
602 * returns the response either through a
603 * callback or Promise in the absence
604 * of a callback.
605 *
606 * A little black magic here - because
607 * we sometimes have to send commands 10
608 * miles upstream through 80 other instances
609 * of vantage and we aren't going to send
610 * the callback / promise with us on that
611 * trip, we store the command, callback,
612 * resolve and reject objects (as they apply)
613 * in a local vantage._command variable.
614 *
615 * When the command eventually comes back
616 * downstream, we dig up the callbacks and
617 * finally resolve or reject the promise, etc.
618 *
619 * Lastly, to add some more complexity, we throw
620 * command and callbacks into a queue that will
621 * be unearthed and sent in due time.
622 *
623 * @param {String} cmd
624 * @param {Function} cb
625 * @return {Promise or Vantage}
626 * @api public
627 */
628
629vantage.exec = function(cmd, args, cb) {
630 var self = this
631 , ssn = self.session
632 ;
633
634 cb = (_.isFunction(args)) ? args : cb;
635 args = args || {};
636
637 if (args.sessionId) {
638 ssn = self.getSessionById(args.sessionId);
639 }
640
641 var command = {
642 command: cmd,
643 args: args,
644 callback: cb,
645 session: ssn
646 };
647
648 if (cb !== undefined) {
649 self._queue.push(command);
650 self._queueHandler.call(self);
651 return self;
652 } else {
653 return new Promise(function(resolve, reject) {
654 command.resolve = resolve;
655 command.reject = reject;
656 self._queue.push(command);
657 self._queueHandler.call(self);
658 });
659 }
660};
661
662/**
663 * Commands issued to Vantage server
664 * are executed in sequence. Called once
665 * when a command is inserted or completes,
666 * shifts the next command in the queue
667 * and sends it to `vantage._execQueueItem`.
668 *
669 * @api private
670 */
671
672vantage._queueHandler = function() {
673 if (this._queue.length > 0 && this._command === undefined) {
674 var item = this._queue.shift();
675 this._execQueueItem(item);
676 }
677};
678
679/**
680 * Fires off execution of a command - either
681 * calling upstream or executing locally.
682 *
683 * @param {Object} cmd
684 * @api private
685 */
686
687vantage._execQueueItem = function(cmd) {
688 var self = this;
689 if (cmd.session.isLocal() && !cmd.session.client) {
690 this._exec(cmd);
691 } else {
692 self._command = cmd;
693 self._send("vantage-command-upstream", "upstream", {
694 command: cmd.command,
695 args: cmd.args,
696 completed: false,
697 sessionId: cmd.session.id
698 });
699 }
700};
701
702/**
703 * Executes a vantage API command.
704 * Warning: Dragons lie beyond this point.
705 *
706 * @param {String} item
707 * @api private
708 */
709
710vantage._exec = function(item) {
711
712 var self = this
713 , parts
714 , match = false
715 , modeCommand
716 , args = {}
717 ;
718
719 function parseArgsByType(arg, cmdArg) {
720 if (arg && cmdArg.variadic === true) {
721 args[cmdArg.name] = remainingArgs;
722 } else if (arg) {
723 args[cmdArg.name] = arg;
724 remainingArgs.shift();
725 }
726 }
727
728 function validateArg(arg, cmdArg) {
729 if (!arg && cmdArg.required === true) {
730 item.session.log(" ");
731 item.session.log(" Missing required argument. Showing Help:");
732 item.session.log(match.helpInformation());
733 item.callback();
734 return false;
735 } else {
736 return true;
737 }
738 }
739
740 item = item || {};
741
742 if (!item.session) {
743 throw new Error("Fatal Error: No session was passed into command for execution: " + item);
744 }
745
746 if (String(item.command).indexOf("undefine") > -1) {
747 console.trace("Undefined ._exec command passed.");
748 throw new Error("vantage._exec was called with an undefined command.");
749 }
750
751 item.command = item.command || "";
752 modeCommand = item.command;
753 item.command = (item.session._mode) ? item.session._mode : item.command;
754 parts = item.command.split(" ");
755
756 // History for our "up" and "down" arrows.
757 item.session.history((item.session._mode ? modeCommand : item.command));
758
759 // Reverse drill-down the string until you find the
760 // first command match.
761 for (var i = 0; i < parts.length; ++i) {
762 var subcommand = String(parts.slice(0, parts.length - i).join(" ")).trim().toLowerCase();
763 match = _.findWhere(this.commands, { _name: subcommand }) || match;
764 if (!match) {
765 for (var j = 0; j < this.commands.length; ++j) {
766 var idx = this.commands[j]._aliases.indexOf(subcommand);
767 match = (idx > -1) ? this.commands[j] : match;
768 }
769 }
770 if (match) {
771 args = parts.slice(parts.length - i, parts.length).join(" ");
772 break;
773 }
774 }
775
776 if (!match) {
777 match = _.findWhere(this.commands, { _catch: true });
778 args = item.command;
779 }
780
781 // Match means we found a suitable command.
782 if (match) {
783
784 var fn = match._fn
785 , afterFn = match._after
786 , init = match._init || function(arrgs, cb) { cb(); }
787 , delimiter = match._delimiter || String(item.command).toLowerCase() + ":"
788 , origArgs = match._args
789 , origOptions = match.options
790 , variadic = _.findWhere(origArgs, { variadic: true })
791 ;
792
793 var parsedBasic = VantageUtil.parseArgs(args);
794
795 // This basically makes the arguments human readable.
796 var parsedArgs = minimist(parsedBasic);
797
798 parsedArgs._ = parsedArgs._ || [];
799 args = {};
800 args.options = {};
801
802 // Looks for a help arg and throws help if any.
803 if (parsedArgs.help || parsedArgs._.indexOf("/?") > -1) {
804 item.session.log(match.helpInformation());
805 item.callback(); return;
806 }
807
808 var remainingArgs = _.clone(parsedArgs._);
809
810 var supportedArgs = 10;
811 var valid = true;
812 for (var i = 0; i < supportedArgs; ++i) {
813 if (origArgs[i]) {
814 valid = (!valid) ? false : validateArg(parsedArgs._[i], origArgs[i]);
815 if (!valid) { break; }
816 parseArgsByType(parsedArgs._[i], origArgs[i]);
817 }
818 }
819
820 if (!valid) {
821 return;
822 }
823
824 // Looks for ommitted required options
825 // and throws help.
826 for (var k = 0; k < origOptions.length; ++k) {
827 var o = origOptions[k];
828 var short = String(o.short || "").replace(/-/g, "");
829 var long = String(o.long || "").replace(/--no-/g, "").replace(/-/g, "");
830 var exist = parsedArgs[short] || parsedArgs[long];
831 if (exist === true && o.required !== 0) {
832 item.session.log(" ");
833 item.session.log(" Missing required option. Showing Help:");
834 item.session.log(match.helpInformation());
835 item.callback("Missing required option.");
836 return;
837 }
838 if (exist !== undefined) {
839 args.options[long || short] = exist;
840 }
841 }
842
843 // If this command throws us into a "mode",
844 // prepare for it.
845 if (match._mode === true && !item.session._mode) {
846 // Assign vantage to be in a "mode".
847 item.session._mode = item.command;
848 // Execute the mode's `init` function
849 // instead of the `action` function.
850 fn = init;
851 // Reassign the command history to a
852 // cache, replacing it with a blank
853 // history for the mode.
854 self._histCache = _.clone(self._hist);
855 self._histCtrCache = parseFloat(self._histCtr);
856 self._hist = [];
857 self._histCtr = 0;
858
859 item.session.modeDelimiter(delimiter);
860
861 } else if (item.session._mode) {
862 if (String(modeCommand).trim() === "exit") {
863 self._exitMode({ sessionId: item.session.id });
864 if (item.callback) {
865 item.callback.call(self);
866 } else if (item.resolve !== undefined) {
867 item.resolve();
868 }
869 return;
870 }
871
872 // This executes when actually in a "mode"
873 // session. We now pass in the raw text of what
874 // is typed into the first param of `action`
875 // instead of arguments.
876 args = modeCommand;
877 }
878
879 // If args were passed into the programmatic
880 // `vantage.exec(cmd, args, callback)`, merge
881 // them here.
882 if (item.args && _.isObject(item.args)) {
883 args = _.extend(args, item.args);
884 }
885
886 // Warning: Do not touch unless you have a
887 // really good understand of callbacks and
888 // Promises (I don't).
889
890 // So what I think I made this do, is call the
891 // function declared in the command's .action()
892 // method.
893
894 // If calling it seems to return a Promise, we
895 // are going to guess they didn't call the
896 // callback we passed in.
897
898 // If the 'action' function didn't throw an
899 // error, call the `exec`'s callback if it
900 // exists, and call it's `resolve` if its a
901 // Promise.
902
903 // If the `action` function threw an error,
904 // callback with the error or reject the Promise.
905
906 // Call the vantage API function.
907 var res = fn.call(item.session, args, function() {
908 self.emit("client_command_executed", { command: item.command });
909 var fixedArgs = VantageUtil.fixArgsForApply(arguments);
910
911 var error = fixedArgs[0];
912 if (item.callback) {
913 item.callback.apply(self, fixedArgs);
914 if (afterFn) { afterFn.call(item.session, args); }
915 } else {
916 if (error !== undefined && item.reject !== undefined) {
917 item.reject.apply(self, fixedArgs);
918 if (afterFn) { afterFn.call(item.session, args); }
919 return;
920 } else if (item.resolve !== undefined) {
921 item.resolve.apply(self, fixedArgs);
922 if (afterFn) { afterFn.call(item.session, args); }
923 return;
924 }
925 }
926 });
927
928 // If the Vantage API function as declared by the user
929 // returns a promise, then we do this.
930 if (res && _.isFunction(res.then)) {
931 res.then(function(data) {
932 if (item.session.isLocal()) {
933 self.emit("client_command_executed", { command: item.command });
934 }
935 if (item.callback !== undefined) {
936 item.callback(void 0, data); // hmmm changed...
937 if (afterFn) { afterFn.call(item.session, args); }
938 } else if (item.resolve !== undefined) {
939 item.resolve(data);
940 if (afterFn) { afterFn.call(item.session, args); }
941 return;
942 }
943 }).catch(function(err) {
944 item.session.log(chalk.red("Error: ") + err);
945 if (item.session.isLocal()) {
946 self.emit("client_command_error", { command: item.command, error: err });
947 }
948 if (item.callback !== undefined) {
949 item.callback(true, err);
950 if (afterFn) { afterFn.call(item.session, args); }
951 } else if (item.reject !== undefined) {
952 item.reject(err);
953 if (afterFn) { afterFn.call(item.session, args); }
954 }
955 });
956 }
957 } else {
958 // If no command match, just return.
959 item.session.log(this._commandHelp(item.command));
960
961 // To do - if `exec` uses Promises,
962 // I think we need to return a promise here...
963 item.callback();
964 }
965};
966
967/**
968 * Imports an authentication middleware
969 * module to replace the server's auth
970 * function, which is called when a remote
971 * instance of vantage connects.
972 *
973 * @param {Function} middleware
974 * @param {Object} options
975 * @return {Vantage}
976 * @api public
977 */
978
979vantage.auth = function(middleware, options) {
980 middleware = (middleware === "basic") ? basicAuth
981 : middleware
982 ;
983
984 if (!middleware) {
985 this._authFn = void 0;
986 } else if (!_.isFunction(middleware)) {
987 this._authFn = void 0;
988 throw new Error("Invalid middleware string passed into Vantage.auth: " + middleware);
989 } else {
990 var fn = middleware.call(this, this, options);
991 this._authFn = fn;
992 }
993 return this;
994};
995
996/**
997 * Calls authentication middleware
998 *
999 * @param {Object} args
1000 * @param {Function} cb
1001 * @api private
1002 */
1003
1004vantage._authenticate = function(args, cb) {
1005 var ssn = this.getSessionById(args.sessionId);
1006 if (!this._authFn) {
1007 var nodeEnv = process.env.NODE_ENV || "development";
1008 if (nodeEnv !== "development") {
1009 var msg = "The Node server you are connecting to appears "
1010 + "to be a production server, and yet its Vantage.js "
1011 + "instance does not support any means of authentication. \n"
1012 + "To connect to this server without authentication, "
1013 + "ensure its 'NODE_ENV' environmental variable is set "
1014 + "to 'development' (it is currently '" + nodeEnv + "').";
1015 ssn.log(chalk.yellow(msg));
1016 ssn.authenticating = false;
1017 ssn.authenticated = false;
1018 cb(msg, false);
1019 } else {
1020 ssn.authenticating = false;
1021 ssn.authenticated = true;
1022 cb(void 0, true);
1023 }
1024 } else {
1025 this._authFn.call(ssn, args, function(message, authenticated) {
1026 ssn.authenticating = false;
1027 ssn.authenticated = authenticated;
1028 if (authenticated === true) {
1029 cb(void 0, true);
1030 } else {
1031 cb(message);
1032 }
1033 });
1034 }
1035};
1036
1037/**
1038 * Exits out of a give "mode" one is in.
1039 * Reverts history and delimiter back to
1040 * regular vantage usage.
1041 *
1042 * @api private
1043 */
1044
1045vantage._exitMode = function(options) {
1046 var ssn = this.getSessionById(options.sessionId);
1047 ssn._mode = false;
1048 this._hist = this._histCache;
1049 this._histCtr = this._histCtrCache;
1050 this._histCache = [];
1051 this._histCtrCache = 0;
1052 ssn.modeDelimiter(false);
1053};
1054
1055/**
1056 * Removes a given command from the list of
1057 * commands.
1058 *
1059 * @param {String} cmd
1060 * @return {Vantage}
1061 * @api public
1062 */
1063
1064vantage.removeCommand = function(name) {
1065 this.commands = _.reject(this.commands, function(command){
1066 if (command._name === name) {
1067 return true;
1068 }
1069 })
1070 return this;
1071};
1072
1073/**
1074 * Hides a given command from the list of commands.
1075 *
1076 * @param {String} cmd
1077 * @return {Vantage}
1078 * @api public
1079 */
1080
1081vantage.hideCommand = function(name) {
1082 this.commands = _.map(this.commands, function(command){
1083 if (command._name === name) {
1084 command._hidden = true;
1085 }
1086 return command;
1087 })
1088 return this;
1089};
1090
1091/**
1092 * Returns help string for a given command.
1093 *
1094 * @param {String} command
1095 * @api private
1096 */
1097
1098vantage._commandHelp = function(command) {
1099 if (!this.commands.length) { return ""; }
1100
1101 var matches = [];
1102 var singleMatches = [];
1103
1104 command = (command) ? String(command).trim().toLowerCase() : void 0;
1105 for (var i = 0; i < this.commands.length; ++i) {
1106 var parts = String(this.commands[i]._name).split(" ");
1107 if (parts.length === 1 && parts[0] === command && !this.commands[i]._hidden && !this.commands[i]._catch) { singleMatches.push(command); }
1108 var str = "";
1109 for (var j = 0; j < parts.length; ++j) {
1110 str = String(str + " " + parts[j]).trim();
1111 if (str === command && !this.commands[i]._hidden && !this.commands[i]._catch) {
1112 matches.push(this.commands[i]);
1113 break;
1114 }
1115 }
1116 }
1117
1118 var invalidString =
1119 (command && matches.length === 0 && singleMatches.length === 0)
1120 ? ["", " Invalid Command. Showing Help:", ""].join("\n")
1121 : "";
1122
1123 var commandMatch = (matches.length > 0) ? true : false;
1124 var commandMatchLength = (commandMatch) ? String(command).trim().split(" ").length + 1 : 1;
1125 matches = (matches.length === 0) ? this.commands : matches;
1126
1127 var commands = matches.filter(function(cmd) {
1128 return !cmd._noHelp;
1129 }).filter(function(cmd){
1130 return !cmd._catch;
1131 }).filter(function(cmd){
1132 return !cmd._hidden;
1133 }).filter(function(cmd){
1134 return (String(cmd._name).trim().split(" ").length <= commandMatchLength);
1135 }).map(function(cmd) {
1136 var args = cmd._args.map(function(arg) {
1137 return VantageUtil.humanReadableArgName(arg);
1138 }).join(" ");
1139
1140 return [
1141 cmd._name
1142 + (cmd._alias
1143 ? "|" + cmd._alias
1144 : "")
1145 + (cmd.options.length
1146 ? " [options]"
1147 : "")
1148 + " " + args
1149 , cmd.description()
1150 ];
1151 });
1152
1153 var width = commands.reduce(function(max, commandX) {
1154 return Math.max(max, commandX[0].length);
1155 }, 0);
1156
1157 var counts = {};
1158
1159 var groups = _.uniq(matches.filter(function(cmd) {
1160 return (String(cmd._name).trim().split(" ").length > commandMatchLength);
1161 }).map(function(cmd){
1162 return String(cmd._name).split(" ").slice(0, commandMatchLength).join(" ");
1163 }).map(function(cmd){
1164 counts[cmd] = counts[cmd] || 0;
1165 counts[cmd]++;
1166 return cmd;
1167 })).map(function(cmd){
1168 return " " + VantageUtil.pad(cmd + " *", width) + " " + counts[cmd] + " sub-command" + ((counts[cmd] === 1) ? "" : "s") + ".";
1169 });
1170
1171 var results = [
1172 invalidString + "\n Commands:"
1173 , ""
1174 , commands.map(function(cmd) {
1175 return VantageUtil.pad(cmd[0], width) + " " + cmd[1];
1176 }).join("\n").replace(/^/gm, " ")
1177 , (groups.length < 1
1178 ? ""
1179 : "\n Command Groups:\n\n" + groups.join("\n") + "\n")
1180 ].join("\n");
1181
1182 return results;
1183};
1184
1185/**
1186 * Abstracts the logic for sending and
1187 * receiving sockets upstream and downstream.
1188 *
1189 * To do: Has the start of logic for vantage sessions,
1190 * which I haven't fully confronted yet.
1191 *
1192 * @param {String} str
1193 * @param {String} direction
1194 * @param {String} data
1195 * @param {Object} options
1196 * @api private
1197 */
1198
1199vantage._send = function(str, direction, data, options) {
1200 options = options || {};
1201 data = data || {};
1202 var ssn = this.getSessionById(data.sessionId);
1203 if (!ssn) {
1204 throw new Error("No Sessions logged for ID " + data.sessionId + " in vantage._send.");
1205 }
1206 if (direction === "upstream") {
1207 if (ssn.client) {
1208 ssn.client.emit(str, data);
1209 }
1210 } else if (direction === "downstream") {
1211 if (ssn.server) {
1212 ssn.server.emit(str, data);
1213 }
1214 }
1215};
1216
1217/**
1218 * Handles the 'middleman' in a 3+-way vagrant session.
1219 * If a vagrant instance is a 'client' and 'server', it is
1220 * now considered a 'proxy' and its sole purpose is to proxy
1221 * information through, upstream or downstream.
1222 *
1223 * If vantage is not a proxy, it resolves a promise for further
1224 * code that assumes one is now an end user. If it ends up
1225 * piping the traffic through, it never resolves the promise.
1226 *
1227 * @param {String} str
1228 * @param {String} direction
1229 * @param {String} data
1230 * @param {Object} options
1231 * @api private
1232 */
1233vantage._proxy = function(str, direction, data, options) {
1234 var self = this;
1235 return new Promise(function(resolve){
1236 var ssn = self.getSessionById(data.sessionId);
1237 if (ssn && (!ssn.isLocal() && ssn.client)) {
1238 self._send(str, direction, data, options);
1239 } else {
1240 resolve();
1241 }
1242 });
1243};
1244
1245/**
1246 * Starts vantage listening as a server.
1247 *
1248 * @param {Mixed} app
1249 * @param {Object} options
1250 * @return {Vantage}
1251 * @api public
1252 */
1253
1254vantage.listen = function(app, options, cb) {
1255 this.server.init(app, options, cb);
1256 return this;
1257};
1258
1259/**
1260 * Returns session by id.
1261 *
1262 * @param {Integer} id
1263 * @return {Session}
1264 * @api public
1265 */
1266
1267vantage.getSessionById = function(id) {
1268 if (_.isObject(id)) {
1269 throw new Error("vantage.getSessionById: id " + JSON.stringify(id) + " should not be an object.");
1270 }
1271 var ssn = _.findWhere(this.server.sessions, { id: id });
1272 ssn = (this.session.id === id) ? this.session : ssn;
1273 if (!id) {
1274 throw new Error("vantage.getSessionById was called with no ID passed.");
1275 }
1276 if (!ssn) {
1277 var sessions = {
1278 local: this.session.id,
1279 server: _.pluck(this.server.sessions, "id")
1280 };
1281 throw new Error("No session found for id " + id + " in vantage.getSessionById. Sessions: " + JSON.stringify(sessions));
1282 }
1283 return ssn;
1284};
1285
1286/**
1287 * Kills a remote vantage session. If user
1288 * is running on a direct terminal, will kill
1289 * node instance after confirmation.
1290 *
1291 * @param {Object} options
1292 * @param {Function} cb
1293 * @api private
1294 */
1295
1296vantage.exit = function(options) {
1297 var self = this;
1298 var ssn = this.getSessionById(options.sessionId);
1299 if (ssn.isLocal()) {
1300 if (options.force) {
1301 process.exit(1);
1302 } else {
1303 this.prompt({
1304 type: "confirm",
1305 name: "continue",
1306 default: false,
1307 message: "This will actually kill this node process. Continue?",
1308 sessionId: ssn.id
1309 }, function(result){
1310 if (result.continue) {
1311 process.exit(1);
1312 } else {
1313 self._prompt({ sessionId: ssn.id });
1314 }
1315 });
1316 }
1317 } else {
1318 ssn.server.emit("vantage-close-downstream", { sessionId: ssn.id });
1319 }
1320};