| /* |
| * prompt.js: Simple prompt for prompting information from the command line |
| * |
| * (C) 2010, Nodejitsu Inc. |
| * |
| */ |
| |
| var events = require('events'), |
| readline = require('readline'), |
| utile = require('utile'), |
| async = utile.async, |
| read = require('read'), |
| validate = require('revalidator').validate, |
| winston = require('winston'); |
| |
| // |
| // Monkey-punch readline.Interface to work-around |
| // https://github.com/joyent/node/issues/3860 |
| // |
| readline.Interface.prototype.setPrompt = function(prompt, length) { |
| this._prompt = prompt; |
| if (length) { |
| this._promptLength = length; |
| } else { |
| var lines = prompt.split(/[\r\n]/); |
| var lastLine = lines[lines.length - 1]; |
| this._promptLength = lastLine.replace(/\u001b\[(\d+(;\d+)*)?m/g, '').length; |
| } |
| }; |
| |
| // |
| // Expose version using `pkginfo` |
| // |
| require('pkginfo')(module, 'version'); |
| |
| var stdin, stdout, history = []; |
| var prompt = module.exports = Object.create(events.EventEmitter.prototype); |
| var logger = prompt.logger = new winston.Logger({ |
| transports: [new (winston.transports.Console)()] |
| }); |
| |
| prompt.started = false; |
| prompt.paused = false; |
| prompt.allowEmpty = false; |
| prompt.message = 'prompt'; |
| prompt.delimiter = ': '; |
| prompt.colors = true; |
| |
| // |
| // Create an empty object for the properties |
| // known to `prompt` |
| // |
| prompt.properties = {}; |
| |
| // |
| // Setup the default winston logger to use |
| // the `cli` levels and colors. |
| // |
| logger.cli(); |
| |
| // |
| // ### function start (options) |
| // #### @options {Object} **Optional** Options to consume by prompt |
| // Starts the prompt by listening to the appropriate events on `options.stdin` |
| // and `options.stdout`. If no streams are supplied, then `process.stdin` |
| // and `process.stdout` are used, respectively. |
| // |
| prompt.start = function (options) { |
| if (prompt.started) { |
| return; |
| } |
| |
| options = options || {}; |
| stdin = options.stdin || process.stdin; |
| stdout = options.stdout || process.stdout; |
| |
| // |
| // By default: Remember the last `10` prompt property / |
| // answer pairs and don't allow empty responses globally. |
| // |
| prompt.memory = options.memory || 10; |
| prompt.allowEmpty = options.allowEmpty || false; |
| prompt.message = options.message || prompt.message; |
| prompt.delimiter = options.delimiter || prompt.delimiter; |
| prompt.colors = options.colors || prompt.colors; |
| |
| if (process.platform !== 'win32') { |
| // windows falls apart trying to deal with SIGINT |
| process.on('SIGINT', function () { |
| stdout.write('\n'); |
| process.exit(1); |
| }); |
| } |
| |
| prompt.emit('start'); |
| prompt.started = true; |
| return prompt; |
| }; |
| |
| // |
| // ### function pause () |
| // Pauses input coming in from stdin |
| // |
| prompt.pause = function () { |
| if (!prompt.started || prompt.paused) { |
| return; |
| } |
| |
| stdin.pause(); |
| prompt.emit('pause'); |
| prompt.paused = true; |
| return prompt; |
| }; |
| |
| // |
| // ### function resume () |
| // Resumes input coming in from stdin |
| // |
| prompt.resume = function () { |
| if (!prompt.started || !prompt.paused) { |
| return; |
| } |
| |
| stdin.resume(); |
| prompt.emit('resume'); |
| prompt.paused = false; |
| return prompt; |
| }; |
| |
| // |
| // ### function history (search) |
| // #### @search {Number|string} Index or property name to find. |
| // Returns the `property:value` pair from within the prompts |
| // `history` array. |
| // |
| prompt.history = function (search) { |
| if (typeof search === 'number') { |
| return history[search] || {}; |
| } |
| |
| var names = history.map(function (pair) { |
| return typeof pair.property === 'string' |
| ? pair.property |
| : pair.property.name; |
| }); |
| |
| if (~names.indexOf(search)) { |
| return null; |
| } |
| |
| return history.filter(function (pair) { |
| return typeof pair.property === 'string' |
| ? pair.property === search |
| : pair.property.name === search; |
| })[0]; |
| }; |
| |
| // |
| // ### function get (schema, callback) |
| // #### @schema {Array|Object|string} Set of variables to get input for. |
| // #### @callback {function} Continuation to pass control to when complete. |
| // Gets input from the user via stdin for the specified message(s) `msg`. |
| // |
| prompt.get = function (schema, callback) { |
| // |
| // Transforms a full JSON-schema into an array describing path and sub-schemas. |
| // Used for iteration purposes. |
| // |
| function untangle(schema, path) { |
| var results = []; |
| path = path || []; |
| |
| if (schema.properties) { |
| // |
| // Iterate over the properties in the schema and use recursion |
| // to process sub-properties. |
| // |
| Object.keys(schema.properties).forEach(function (key) { |
| var obj = {}; |
| obj[key] = schema.properties[key]; |
| |
| // |
| // Concat a sub-untangling to the results. |
| // |
| results = results.concat(untangle(obj[key], path.concat(key))); |
| }); |
| |
| // Return the results. |
| return results; |
| } |
| |
| // |
| // This is a schema "leaf". |
| // |
| return { |
| path: path, |
| schema: schema |
| }; |
| } |
| |
| // |
| // Iterate over the values in the schema, represented as |
| // a legit single-property object subschemas. Accepts `schema` |
| // of the forms: |
| // |
| // 'prop-name' |
| // |
| // ['string-name', { path: ['or-well-formed-subschema'], properties: ... }] |
| // |
| // { path: ['or-well-formed-subschema'], properties: ... ] } |
| // |
| // { properties: { 'schema-with-no-path' } } |
| // |
| // And transforms them all into |
| // |
| // { path: ['path', 'to', 'property'], properties: { path: { to: ...} } } |
| // |
| function iterate(schema, get, done) { |
| var iterator = [], |
| result = {}; |
| |
| if (typeof schema === 'string') { |
| // |
| // We can iterate over a single string. |
| // |
| iterator.push({ |
| path: [schema], |
| schema: prompt.properties[schema.toLowerCase()] || {} |
| }); |
| } |
| else if (Array.isArray(schema)) { |
| // |
| // An array of strings and/or single-prop schema and/or no-prop schema. |
| // |
| iterator = schema.map(function (element) { |
| if (typeof element === 'string') { |
| return { |
| path: [element], |
| schema: prompt.properties[element.toLowerCase()] || {} |
| }; |
| } |
| else if (element.properties) { |
| return { |
| path: [Object.keys(element.properties)[0]], |
| schema: element.properties[Object.keys(element.properties)[0]] |
| }; |
| } |
| else if (element.path && element.schema) { |
| return element; |
| } |
| else { |
| return { |
| path: [element.name || 'question'], |
| schema: element |
| }; |
| } |
| }); |
| } |
| else if (schema.properties) { |
| // |
| // Or a complete schema `untangle` it for use. |
| // |
| iterator = untangle(schema); |
| } |
| else { |
| // |
| // Or a partial schema and path. |
| // TODO: Evaluate need for this option. |
| // |
| iterator = [{ |
| schema: schema.schema ? schema.schema : schema, |
| path: schema.path || [schema.name || 'question'] |
| }]; |
| } |
| |
| // |
| // Now, iterate and assemble the result. |
| // |
| async.forEachSeries(iterator, function (branch, next) { |
| get(branch, function assembler(err, line) { |
| if (err) { |
| return next(err); |
| } |
| |
| function build(path, line) { |
| var obj = {}; |
| if (path.length) { |
| obj[path[0]] = build(path.slice(1), line); |
| return obj; |
| } |
| |
| return line; |
| } |
| |
| function attach(obj, attr) { |
| var keys; |
| if (typeof attr !== 'object' || attr instanceof Array) { |
| return attr; |
| } |
| |
| keys = Object.keys(attr); |
| if (keys.length) { |
| if (!obj[keys[0]]) { |
| obj[keys[0]] = {}; |
| } |
| obj[keys[0]] = attach(obj[keys[0]], attr[keys[0]]); |
| } |
| |
| return obj; |
| } |
| |
| result = attach(result, build(branch.path, line)); |
| next(); |
| }); |
| }, function (err) { |
| return err ? done(err) : done(null, result); |
| }); |
| } |
| |
| iterate(schema, function get(target, next) { |
| prompt.getInput(target, function (err, line) { |
| return err ? next(err) : next(null, line); |
| }); |
| }, callback); |
| |
| return prompt; |
| }; |
| |
| // |
| // ### function confirm (msg, callback) |
| // #### @msg {Array|Object|string} set of message to confirm |
| // #### @callback {function} Continuation to pass control to when complete. |
| // Confirms a single or series of messages by prompting the user for a Y/N response. |
| // Returns `true` if ALL messages are answered in the affirmative, otherwise `false` |
| // |
| // `msg` can be a string, or object (or array of strings/objects). |
| // An object may have the following properties: |
| // |
| // { |
| // description: 'yes/no' // message to prompt user |
| // pattern: /^[yntf]{1}/i // optional - regex defining acceptable responses |
| // yes: /^[yt]{1}/i // optional - regex defining `affirmative` responses |
| // message: 'yes/no' // optional - message to display for invalid responses |
| // } |
| // |
| prompt.confirm = function (/* msg, options, callback */) { |
| var args = Array.prototype.slice.call(arguments), |
| msg = args.shift(), |
| callback = args.pop(), |
| opts = args.shift(), |
| vars = !Array.isArray(msg) ? [msg] : msg, |
| RX_Y = /^[yt]{1}/i, |
| RX_YN = /^[yntf]{1}/i; |
| |
| function confirm(target, next) { |
| var yes = target.yes || RX_Y, |
| options = utile.mixin({ |
| description: typeof target === 'string' ? target : target.description||'yes/no', |
| pattern: target.pattern || RX_YN, |
| name: 'confirm', |
| message: target.message || 'yes/no' |
| }, opts || {}); |
| |
| |
| prompt.get([options], function (err, result) { |
| next(err ? false : yes.test(result[options.name])); |
| }); |
| } |
| |
| async.rejectSeries(vars, confirm, function(result) { |
| callback(null, result.length===0); |
| }); |
| }; |
| |
| |
| // Variables needed outside of getInput for multiline arrays. |
| var tmp = []; |
| |
| |
| // ### function getInput (prop, callback) |
| // #### @prop {Object|string} Variable to get input for. |
| // #### @callback {function} Continuation to pass control to when complete. |
| // Gets input from the user via stdin for the specified message `msg`. |
| // |
| prompt.getInput = function (prop, callback) { |
| var schema = prop.schema || prop, |
| propName = prop.path && prop.path.join(':') || prop, |
| storedSchema = prompt.properties[propName.toLowerCase()], |
| delim = prompt.delimiter, |
| defaultLine, |
| against, |
| hidden, |
| length, |
| valid, |
| name, |
| raw, |
| msg; |
| |
| // |
| // If there is a stored schema for `propName` in `propmpt.properties` |
| // then use it. |
| // |
| if (schema instanceof Object && !Object.keys(schema).length && |
| typeof storedSchema !== 'undefined') { |
| schema = storedSchema; |
| } |
| |
| // |
| // Build a proper validation schema if we just have a string |
| // and no `storedSchema`. |
| // |
| if (typeof prop === 'string' && !storedSchema) { |
| schema = {}; |
| } |
| |
| schema = convert(schema); |
| defaultLine = schema.default; |
| name = prop.description || schema.description || propName; |
| raw = prompt.colors |
| ? [prompt.message, delim + name.grey, delim.grey] |
| : [prompt.message, delim + name, delim]; |
| |
| prop = { |
| schema: schema, |
| path: propName.split(':') |
| }; |
| |
| // |
| // If the schema has no `properties` value then set |
| // it to an object containing the current schema |
| // for `propName`. |
| // |
| if (!schema.properties) { |
| schema = (function () { |
| var obj = { properties: {} }; |
| obj.properties[propName] = schema; |
| return obj; |
| })(); |
| } |
| |
| // |
| // Handle overrides here. |
| // TODO: Make overrides nestable |
| // |
| if (prompt.override && prompt.override[propName]) { |
| if (prompt._performValidation(name, prop, prompt.override, schema, -1, callback)) { |
| return callback(null, prompt.override[propName]); |
| } |
| |
| delete prompt.override[propName]; |
| } |
| |
| var type = (schema.properties && schema.properties[name] && |
| schema.properties[name].type || '').toLowerCase().trim(), |
| wait = type === 'array'; |
| |
| if (type === 'array') { |
| length = prop.schema.maxItems; |
| if (length) { |
| msg = (tmp.length + 1).toString() + '/' + length.toString(); |
| } |
| else { |
| msg = (tmp.length + 1).toString(); |
| } |
| msg += delim; |
| raw.push(prompt.colors ? msg.grey : msg); |
| } |
| |
| // |
| // Calculate the raw length and colorize the prompt |
| // |
| length = raw.join('').length; |
| raw[0] = raw[0]; |
| msg = raw.join(''); |
| |
| if (schema.help) { |
| schema.help.forEach(function (line) { |
| logger.help(line); |
| }); |
| } |
| |
| // |
| // Emit a "prompting" event |
| // |
| prompt.emit('prompt', prop); |
| |
| // |
| // If there is no default line, set it to an empty string |
| // |
| if(typeof defaultLine === 'undefined') { |
| defaultLine = ''; |
| } |
| |
| // |
| // set to string for readline ( will not accept Numbers ) |
| // |
| defaultLine = defaultLine.toString(); |
| |
| // |
| // Make the actual read |
| // |
| read({ |
| prompt: msg, |
| silent: prop.schema && prop.schema.hidden, |
| default: defaultLine, |
| input: stdin, |
| output: stdout |
| }, function (err, line) { |
| if (err && wait === false) { |
| return callback(err); |
| } |
| |
| var against = {}, |
| numericInput, |
| isValid; |
| |
| if (line !== '') { |
| |
| if (schema.properties[name]) { |
| var type = (schema.properties[name].type || '').toLowerCase().trim() || undefined; |
| |
| // |
| // Attempt to parse input as a float if the schema expects a number. |
| // |
| if (type == 'number') { |
| numericInput = parseFloat(line, 10); |
| if (!isNaN(numericInput)) { |
| line = numericInput; |
| } |
| } |
| |
| // |
| // Attempt to parse input as a boolean if the schema expects a boolean |
| // |
| if (type == 'boolean') { |
| if(line === "true") { |
| line = true; |
| } |
| if(line === "false") { |
| line = false; |
| } |
| } |
| |
| // |
| // If the type is an array, wait for the end. Fixes #54 |
| // |
| if (type == 'array') { |
| var length = prop.schema.maxItems; |
| if (err) { |
| if (err.message == 'canceled') { |
| wait = false; |
| stdout.write('\n'); |
| } |
| } |
| else { |
| if (length) { |
| if (tmp.length + 1 < length) { |
| isValid = false; |
| wait = true; |
| } |
| else { |
| isValid = true; |
| wait = false; |
| } |
| } |
| else { |
| isValid = false; |
| wait = true; |
| } |
| tmp.push(line); |
| } |
| line = tmp; |
| } |
| } |
| |
| against[propName] = line; |
| } |
| |
| if (prop && prop.schema.before) { |
| line = prop.schema.before(line); |
| } |
| |
| // Validate |
| if (isValid === undefined) isValid = prompt._performValidation(name, prop, against, schema, line, callback); |
| |
| if (!isValid) { |
| return prompt.getInput(prop, callback); |
| } |
| |
| // |
| // Log the resulting line, append this `property:value` |
| // pair to the history for `prompt` and respond to |
| // the callback. |
| // |
| logger.input(line.yellow); |
| prompt._remember(propName, line); |
| callback(null, line); |
| |
| // Make sure `tmp` is emptied |
| tmp = []; |
| }); |
| }; |
| |
| // |
| // ### function performValidation (name, prop, against, schema, line, callback) |
| // #### @name {Object} Variable name |
| // #### @prop {Object|string} Variable to get input for. |
| // #### @against {Object} Input |
| // #### @schema {Object} Validation schema |
| // #### @line {String|Boolean} Input line |
| // #### @callback {function} Continuation to pass control to when complete. |
| // Perfoms user input validation, print errors if needed and returns value according to validation |
| // |
| prompt._performValidation = function (name, prop, against, schema, line, callback) { |
| var numericInput, valid, msg; |
| |
| try { |
| valid = validate(against, schema); |
| } |
| catch (err) { |
| return (line !== -1) ? callback(err) : false; |
| } |
| |
| if (!valid.valid) { |
| msg = line !== -1 ? 'Invalid input for ' : 'Invalid command-line input for '; |
| |
| if (prompt.colors) { |
| logger.error(msg + name.grey); |
| } |
| else { |
| logger.error(msg + name); |
| } |
| |
| if (prop.schema.message) { |
| logger.error(prop.schema.message); |
| } |
| |
| prompt.emit('invalid', prop, line); |
| } |
| |
| return valid.valid; |
| }; |
| |
| // |
| // ### function addProperties (obj, properties, callback) |
| // #### @obj {Object} Object to add properties to |
| // #### @properties {Array} List of properties to get values for |
| // #### @callback {function} Continuation to pass control to when complete. |
| // Prompts the user for values each of the `properties` if `obj` does not already |
| // have a value for the property. Responds with the modified object. |
| // |
| prompt.addProperties = function (obj, properties, callback) { |
| properties = properties.filter(function (prop) { |
| return typeof obj[prop] === 'undefined'; |
| }); |
| |
| if (properties.length === 0) { |
| return callback(obj); |
| } |
| |
| prompt.get(properties, function (err, results) { |
| if (err) { |
| return callback(err); |
| } |
| else if (!results) { |
| return callback(null, obj); |
| } |
| |
| function putNested (obj, path, value) { |
| var last = obj, key; |
| |
| while (path.length > 1) { |
| key = path.shift(); |
| if (!last[key]) { |
| last[key] = {}; |
| } |
| |
| last = last[key]; |
| } |
| |
| last[path.shift()] = value; |
| } |
| |
| Object.keys(results).forEach(function (key) { |
| putNested(obj, key.split('.'), results[key]); |
| }); |
| |
| callback(null, obj); |
| }); |
| |
| return prompt; |
| }; |
| |
| // |
| // ### @private function _remember (property, value) |
| // #### @property {Object|string} Property that the value is in response to. |
| // #### @value {string} User input captured by `prompt`. |
| // Prepends the `property:value` pair into the private `history` Array |
| // for `prompt` so that it can be accessed later. |
| // |
| prompt._remember = function (property, value) { |
| history.unshift({ |
| property: property, |
| value: value |
| }); |
| |
| // |
| // If the length of the `history` Array |
| // has exceeded the specified length to remember, |
| // `prompt.memory`, truncate it. |
| // |
| if (history.length > prompt.memory) { |
| history.splice(prompt.memory, history.length - prompt.memory); |
| } |
| }; |
| |
| // |
| // ### @private function convert (schema) |
| // #### @schema {Object} Schema for a property |
| // Converts the schema into new format if it is in old format |
| // |
| function convert(schema) { |
| var newProps = Object.keys(validate.messages), |
| newSchema = false, |
| key; |
| |
| newProps = newProps.concat(['description', 'dependencies']); |
| |
| for (key in schema) { |
| if (newProps.indexOf(key) > 0) { |
| newSchema = true; |
| break; |
| } |
| } |
| |
| if (!newSchema || schema.validator || schema.warning || typeof schema.empty !== 'undefined') { |
| schema.description = schema.message; |
| schema.message = schema.warning; |
| |
| if (typeof schema.validator === 'function') { |
| schema.conform = schema.validator; |
| } else { |
| schema.pattern = schema.validator; |
| } |
| |
| if (typeof schema.empty !== 'undefined') { |
| schema.required = !(schema.empty); |
| } |
| |
| delete schema.warning; |
| delete schema.validator; |
| delete schema.empty; |
| } |
| |
| return schema; |
| } |