1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | var chalk = require('chalk');
|
18 |
|
19 | var apiResolver;
|
20 | var logging = true;
|
21 |
|
22 | var logBuffer = '';
|
23 | var errBuffer = '';
|
24 |
|
25 | var logged = false;
|
26 | var cli = module.exports;
|
27 | var Promise = require('bluebird');
|
28 |
|
29 | var ui = exports;
|
30 |
|
31 | var readline = require('readline');
|
32 |
|
33 | var Writable = require('stream').Writable;
|
34 |
|
35 | var isWin = require('./common').isWin;
|
36 |
|
37 | function MutableStdout(opts) {
|
38 | Writable.call(this, opts);
|
39 | }
|
40 | MutableStdout.prototype = Object.create(Writable.prototype);
|
41 | MutableStdout.prototype._write = function(chunk, encoding, callback) {
|
42 | if (!this.muted)
|
43 | process.stdout.write(chunk, encoding);
|
44 | callback();
|
45 | };
|
46 |
|
47 | var mutableStdout = new MutableStdout();
|
48 |
|
49 | ui.logLevel = 3;
|
50 | var logTypes = ['err', 'warn', 'ok', 'info', 'debug'];
|
51 |
|
52 |
|
53 | var inputRedact;
|
54 | process.on('exit', function() {
|
55 | if (!apiResolver) {
|
56 | if (inputRedact) {
|
57 | console.log('');
|
58 | inputRedact();
|
59 | }
|
60 | if (logBuffer)
|
61 | process.stdout.write(logBuffer);
|
62 | if (errBuffer)
|
63 | process.stderr.write(errBuffer);
|
64 | }
|
65 | });
|
66 |
|
67 | exports.setResolver = function(resolver) {
|
68 | apiResolver = resolver;
|
69 | };
|
70 |
|
71 |
|
72 |
|
73 | var useDefaults;
|
74 | exports.useDefaults = function(_useDefaults) {
|
75 | if (_useDefaults === undefined)
|
76 | _useDefaults = true;
|
77 | useDefaults = _useDefaults;
|
78 | };
|
79 |
|
80 | exports.setLogLevel = function(level) {
|
81 | if (!level)
|
82 | return;
|
83 |
|
84 | var levelIndex = logTypes.indexOf(level);
|
85 |
|
86 | if (levelIndex == -1)
|
87 | ui.log('warn', 'Unknown log level: ' + level);
|
88 | else
|
89 | ui.logLevel = levelIndex;
|
90 | };
|
91 |
|
92 | var format = exports.format = {
|
93 | q: function(msg, opt) {
|
94 | return moduleMsg(msg + (opt ? ' [' + opt + ']' : '') + ': ');
|
95 | },
|
96 | err: function(msg) {
|
97 | return '\n' + chalk.red.bold('err ') + moduleMsg(msg, false, false);
|
98 | },
|
99 | info: function(msg) {
|
100 | return ' ' + moduleMsg(msg, true);
|
101 | },
|
102 | warn: function(msg) {
|
103 | return '\n' + chalk.yellow.bold('warn ') + moduleMsg(msg, true);
|
104 | },
|
105 | ok: function(msg) {
|
106 | return chalk.green.bold('ok ') + moduleMsg(msg, true);
|
107 | }
|
108 | };
|
109 |
|
110 | exports.log = function(type, msg) {
|
111 | if (apiResolver)
|
112 | return apiResolver.emit('log', type, msg);
|
113 |
|
114 | logged = true;
|
115 |
|
116 | if (arguments.length === 1) {
|
117 | msg = type;
|
118 | type = null;
|
119 | }
|
120 |
|
121 | msg = msg || '';
|
122 |
|
123 | msg = msg.toString();
|
124 |
|
125 | if (type)
|
126 | msg = (format[type] || format.info)(msg.toString());
|
127 |
|
128 | var logLevel = logTypes.indexOf(type);
|
129 | if (logLevel == -1)
|
130 | logLevel = 3;
|
131 | if (logLevel <= ui.logLevel) {
|
132 | if (logging) {
|
133 | if (type != 'err')
|
134 | console.log(msg);
|
135 | else
|
136 | console.error(msg);
|
137 | }
|
138 | else {
|
139 | if (type != 'err')
|
140 | logBuffer += msg + '\n';
|
141 | else
|
142 | errBuffer += msg + '\n';
|
143 | }
|
144 | }
|
145 | };
|
146 |
|
147 | function isControlTerminateChar(c, control) {
|
148 | var formatChars = ['%', '`', '_'];
|
149 | formatChars.splice(formatChars.indexOf(control), 1);
|
150 | return !c || ('\r\n ,.:?' + formatChars.join('')).indexOf(c) != -1;
|
151 | }
|
152 |
|
153 | function highlight(text) {
|
154 | text = text.replace(/([\s\`\(]|^)%([^%]+)%/g, function(match, prefix, capture, index) {
|
155 | if (isControlTerminateChar(text[index + match.length], '%'))
|
156 | return match.replace('%' + capture + '%', chalk.bold(capture));
|
157 | else
|
158 | return match;
|
159 | });
|
160 | text = text.replace(/([\s\`\(]|^)\`([^\`]+)\`/g, function(match, prefix, capture, index) {
|
161 | if (isControlTerminateChar(text[index + match.length], '`'))
|
162 | return match.replace('`' + capture + '`', chalk.cyan(capture));
|
163 | else
|
164 | return match;
|
165 | });
|
166 | text = text.replace(/([\s\`\%]|^)\_([^\_]+)\_/g, function(match, prefix, capture, index) {
|
167 | if (isControlTerminateChar(text[index + match.length], '_'))
|
168 | return match.replace('_' + capture + '_', chalk.underline(capture));
|
169 | else
|
170 | return match;
|
171 | });
|
172 | return text.replace(/\t/g, ' ');
|
173 | }
|
174 |
|
175 | exports.wordWrap = wordWrap;
|
176 | function wordWrap(text, columns, leftIndent, rightIndent, skipFirstLineIndent) {
|
177 | leftIndent = leftIndent || 0;
|
178 | rightIndent = rightIndent || 0;
|
179 |
|
180 | var leftSpaces = '';
|
181 | var i;
|
182 |
|
183 | for (i = 0; i < leftIndent; i++)
|
184 | leftSpaces += ' ';
|
185 |
|
186 |
|
187 | columns = columns - leftIndent - rightIndent;
|
188 | var output = [];
|
189 | var lastBreak = 0;
|
190 | var lastSpace = 0;
|
191 | var skipLength = 0;
|
192 | var controlStart = false;
|
193 | for (i = 0; i < text.length; i++) {
|
194 | if (text[i] == '`' || text[i] == '%' || text[i] == '_') {
|
195 | if (controlStart && isControlTerminateChar(text[i + 1], text[i])) {
|
196 | controlStart = false;
|
197 | skipLength++;
|
198 | continue;
|
199 | }
|
200 | else if (lastBreak == lastSpace || !text[i - 1] || (' `.,:?' + (text[i] == '_' ? '%' : '(')).indexOf(text[i - 1]) != -1) {
|
201 | controlStart = true;
|
202 | skipLength++;
|
203 | continue;
|
204 | }
|
205 | }
|
206 | if (text[i] == ' ')
|
207 | lastSpace = i;
|
208 |
|
209 |
|
210 | if (text[i] == '\n') {
|
211 | lastSpace = i;
|
212 | output.push((output.length == 0 && skipFirstLineIndent ? '' : leftSpaces) + text.substring(lastBreak, i) + '\n');
|
213 | lastSpace = i;
|
214 | lastBreak = i + 1;
|
215 | skipLength = 0;
|
216 | }
|
217 |
|
218 |
|
219 | else if ((i - skipLength) != lastBreak &&
|
220 | (i - lastBreak - skipLength) % columns == 0 &&
|
221 | lastSpace > lastBreak) {
|
222 | output.push((output.length == 0 && skipFirstLineIndent ? '' : leftSpaces) + text.substring(lastBreak, lastSpace) + '\n');
|
223 | lastBreak = lastSpace + 1;
|
224 | skipLength = 0;
|
225 | }
|
226 | }
|
227 | output.push((output.length == 0 && skipFirstLineIndent ? '' : leftSpaces) + text.substr(lastBreak));
|
228 | return output.join('');
|
229 | }
|
230 |
|
231 | function moduleMsg(msg, tab, wrap) {
|
232 | if (tab)
|
233 | msg = wordWrap(msg, process.stdout.columns + (isWin ? -1 : 0), tab ? 5 : 0).substr(5);
|
234 | else if (wrap)
|
235 | msg = wordWrap(msg, process.stdout.columns + (isWin ? -1 : 0));
|
236 |
|
237 |
|
238 | return highlight(msg);
|
239 | }
|
240 |
|
241 | var inputQueue = [];
|
242 | var confirm = exports.confirm = function(msg, def, options) {
|
243 | if (typeof def == 'object') {
|
244 | options = def;
|
245 | def = undefined;
|
246 | }
|
247 | options = options || {};
|
248 |
|
249 | if (apiResolver) {
|
250 | if (useDefaults) {
|
251 | apiResolver.emit('prompt', {
|
252 | type: 'confirm',
|
253 | message: msg,
|
254 | default: def
|
255 | });
|
256 | return Promise.resolve(!!def);
|
257 | }
|
258 | else {
|
259 | return new Promise(function(resolve) {
|
260 | apiResolver.emit('prompt', {
|
261 | type: 'confirm',
|
262 | message: msg,
|
263 | default: def
|
264 | }, function(answer) {
|
265 | resolve(answer);
|
266 | });
|
267 | });
|
268 | }
|
269 | }
|
270 |
|
271 | var defText;
|
272 | if (def === true)
|
273 | defText = 'Yes';
|
274 | else if (def === false)
|
275 | defText = 'No';
|
276 | else
|
277 | def = undefined;
|
278 |
|
279 | if (useDefaults) {
|
280 | if (def === undefined)
|
281 | defText = 'Yes';
|
282 | process.stdout.write(format.q(msg) + defText + '\n');
|
283 | return Promise.resolve(!!def);
|
284 | }
|
285 |
|
286 | return cli.input(msg + (defText ? '' : ' [Yes/No]'), defText, {
|
287 | edit: options.edit,
|
288 | info: options.info,
|
289 | silent: options.silent,
|
290 | options: ['Yes', 'No']
|
291 | })
|
292 | .then(function(reply) {
|
293 | if (reply == 'Yes')
|
294 | return true;
|
295 | if (reply == 'No')
|
296 | return false;
|
297 | return confirm(msg, def, options);
|
298 | });
|
299 | };
|
300 |
|
301 | var inputFunc = function() {};
|
302 | process.stdin.on('keypress', function(chunk, key) {
|
303 | inputFunc(key);
|
304 | });
|
305 |
|
306 | exports.input = function(msg, def, options, queue) {
|
307 | if (typeof def == 'object' && def !== null) {
|
308 | options = def;
|
309 | def = undefined;
|
310 | }
|
311 | options = options || {};
|
312 | if (typeof options == 'boolean')
|
313 | options = {
|
314 | silent: true,
|
315 | edit: false,
|
316 | clearOnType: false,
|
317 | info: undefined,
|
318 | hideInfo: true,
|
319 | completer: undefined,
|
320 | validate: undefined,
|
321 | validationError: undefined,
|
322 | optionalOptions: false
|
323 | };
|
324 |
|
325 | options.completer = function(partialLine) {
|
326 | if (!partialLine.length || !options.options)
|
327 | return [[], partialLine];
|
328 |
|
329 | partialLine = partialLine.toLowerCase().trim();
|
330 |
|
331 | var hits = options.options.filter(function(c) {
|
332 | return c.toLowerCase().indexOf(partialLine) == 0;
|
333 | });
|
334 |
|
335 |
|
336 | return [hits.length ? [hits[0]] : [], partialLine];
|
337 | };
|
338 |
|
339 | options = options || {};
|
340 |
|
341 | if (useDefaults) {
|
342 | process.stdout.write(format.q(msg) + def + '\n');
|
343 | return Promise.resolve(def);
|
344 | }
|
345 |
|
346 | if (apiResolver)
|
347 | return new Promise(function(resolve) {
|
348 | apiResolver.emit('prompt', {
|
349 | type: 'input',
|
350 | message: msg,
|
351 | default: def
|
352 | }, function(answer) {
|
353 | resolve(answer.input);
|
354 | });
|
355 | });
|
356 |
|
357 | return new Promise(function(resolve, reject) {
|
358 | if (!logging && !queue)
|
359 | return inputQueue.push({
|
360 | args: [msg, def, options, true],
|
361 | resolve: resolve,
|
362 | reject: reject
|
363 | });
|
364 |
|
365 |
|
366 |
|
367 | var infoLines = 1;
|
368 | if (options.info || options.validationError) {
|
369 | var infoText = '\n';
|
370 | if (options.info)
|
371 | infoText += highlight(wordWrap(options.info, process.stdout.columns, 5, 5)) + '\n';
|
372 | if (options.validationError)
|
373 | infoText += format.warn(wordWrap(options.validationError, process.stdout.columns, 0, 5)) + '\n';
|
374 | infoText += '\n';
|
375 | process.stdout.write(infoText);
|
376 | infoLines = infoText.split('\n').length;
|
377 | }
|
378 | if (!('hideInfo' in options))
|
379 | options.hideInfo = true;
|
380 |
|
381 | if (logging && logged)
|
382 | process.stdout.write('\n');
|
383 | logging = false;
|
384 |
|
385 | var rl = readline.createInterface({
|
386 | input: process.stdin,
|
387 | output: mutableStdout,
|
388 | terminal: true,
|
389 | completer: options.completer
|
390 | });
|
391 |
|
392 | var questionMessage = format.q(msg, !options.edit && def || '');
|
393 | infoLines += questionMessage.split('\n').length - 1;
|
394 |
|
395 | if (options.options)
|
396 | var ctrlOpt = new CtrlOption(options.options, rl);
|
397 | if (!options.edit)
|
398 | inputFunc = function(key) {
|
399 | if (!key)
|
400 | return;
|
401 | var i;
|
402 | if (options.clearOnType && key.name != 'return' && key.name != 'backspace' && key.name != 'left') {
|
403 |
|
404 | for (i = 0; i < (def || '').length; i++)
|
405 | process.stdin.emit('keypress', '\b', { name: 'backspace' });
|
406 | }
|
407 |
|
408 | if (ctrlOpt) {
|
409 | if (key.name === 'up')
|
410 | ctrlOpt.moveUp();
|
411 | if (key.name === 'down')
|
412 | ctrlOpt.moveDown();
|
413 | }
|
414 |
|
415 | else if (key.name == 'up' || key.name == 'down') {
|
416 | rl.write(null, { ctrl: true, name: 'u' });
|
417 | for (i = 0; i < def.length; i++)
|
418 | process.stdin.emit('keypress', def[i], { name: def[i] });
|
419 | }
|
420 | };
|
421 |
|
422 | rl.question(questionMessage, function(inputVal) {
|
423 | inputFunc = function() {};
|
424 | if (mutableStdout.muted)
|
425 | console.log('');
|
426 | rl.close();
|
427 |
|
428 |
|
429 | if (options.options && !options.optionalOptions)
|
430 | inputVal = options.completer(inputVal)[0][0] || '';
|
431 |
|
432 | inputVal = inputVal.trim();
|
433 |
|
434 | if (!options.edit)
|
435 | inputVal = inputVal || def;
|
436 |
|
437 | var retryErr;
|
438 | if (options.validate)
|
439 | retryErr = options.validate(inputVal);
|
440 |
|
441 | inputRedact(inputVal, !!retryErr, options.silent);
|
442 | inputRedact = null;
|
443 | mutableStdout.muted = false;
|
444 |
|
445 | if (retryErr)
|
446 | return cli.input(msg, def, {
|
447 | info: options.info,
|
448 | silent: options.silent,
|
449 | edit: options.edit,
|
450 | clearOnType: options.clearOnType,
|
451 | completer: options.completer,
|
452 | validate: options.validate,
|
453 | validationError: retryErr.toString(),
|
454 | optionalOptions: options.optionalOptions,
|
455 | hideInfo: options.hideInfo
|
456 | }, true).then(resolve, reject);
|
457 |
|
458 |
|
459 | var next = inputQueue.shift();
|
460 | if (next)
|
461 | cli.input.apply(null, next.args).then(next.resolve, next.reject);
|
462 | else {
|
463 | process.stdout.write(logBuffer);
|
464 | process.stderr.write(errBuffer);
|
465 | logBuffer = '';
|
466 | errBuffer = '';
|
467 | logging = true;
|
468 | logged = false;
|
469 | }
|
470 |
|
471 | resolve(inputVal);
|
472 | });
|
473 |
|
474 | inputRedact = function(inputVal, retry, silent) {
|
475 | inputVal = inputVal || '';
|
476 | var inputText = !retry ? format.q(msg, !options.edit && def || '') + (silent ? '' : inputVal) + '\n' : '';
|
477 |
|
478 | process.stdout.write('\033[' + (options.hideInfo || retry ? infoLines : 0) + 'A\033[J' + inputText);
|
479 | };
|
480 |
|
481 | mutableStdout.muted = false;
|
482 |
|
483 | if (options.silent)
|
484 | mutableStdout.muted = true;
|
485 |
|
486 | if (def && options.edit)
|
487 | rl.write(def);
|
488 | });
|
489 | };
|
490 |
|
491 | function CtrlOption(options, rl) {
|
492 | this.options = options || {};
|
493 | this.rl = rl;
|
494 | }
|
495 | CtrlOption.prototype = {
|
496 | cursol: 0,
|
497 | moveUp: function(){
|
498 | var next = this.cursol + 1;
|
499 | this.cursol = (next >= this.options.length ) ? 0 : next;
|
500 | this.move();
|
501 | },
|
502 | moveDown: function(){
|
503 | var next = this.cursol - 1;
|
504 | this.cursol = (next === -1 ) ? (this.options.length - 1) : next ;
|
505 | this.move();
|
506 | },
|
507 | move: function() {
|
508 | this.rl.write(null, {ctrl: true, name: 'u'});
|
509 |
|
510 | var input = this.options[this.cursol].split('');
|
511 | for (var idx in input)
|
512 | process.stdin.emit('keypress', input[idx], { name: input[idx] });
|
513 | },
|
514 | };
|