├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENCE ├── README.md ├── demo.gif ├── index.js ├── package.json ├── souffleur.png └── spec ├── souffleur-spec.js └── support ├── jasmine-runner.js └── jasmine.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "defaults", 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 6, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "semi": ["error", "never"], 13 | "no-console": "off", 14 | "indent": ["error", 2], 15 | "quotes": ["error", "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 16 | "prefer-arrow-callback": "error" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "5" 5 | - "4" 6 | - "4.3.2" 7 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Slobodan Stojanović 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Souffleur 2 | Simple promise-based command line prompt with retry for empty answers and without external dependencies. 3 | 4 | [![Build Status](https://travis-ci.org/stojanovic/souffleur.svg)](https://travis-ci.org/stojanovic/souffleur) 5 | 6 |

7 | souffleur 8 |
9 |

10 | 11 | ## Installation 12 | 13 | [![](https://nodei.co/npm/souffleur.svg?downloads=true&downloadRank=true&stars=true)](https://www.npmjs.com/package/souffleur) 14 | 15 | ```bash 16 | npm install souffleur 17 | ``` 18 | 19 | ## Usage 20 | 21 | Souffleur is simple promise based prompt. 22 | 23 | If you have just one question pass the question as a string or object to `souffleur` or if you have more questions pass an array. 24 | 25 | It'll return an object with each question as a key and each answer as a value. 26 | 27 | ``` 28 | const prompt = require('souffleur') 29 | 30 | // For single question 31 | prompt('Any question') 32 | .then(results => console.log(results)) 33 | // Returns {"Any question": "some answer"} 34 | 35 | // For optional questions 36 | prompt({ 37 | question: 'Optional question', 38 | optional: true 39 | }) 40 | .then(results => console.log(results)) 41 | // Returns {"Optional question": null} if answer is empty 42 | 43 | // For default values 44 | prompt({ 45 | question: 'Question', 46 | default: 42 47 | }) 48 | .then(results => console.log(results)) 49 | // Returns {"Question": 42} if answer is empty 50 | 51 | // For multiple questions 52 | prompt([ 53 | 'Question1', 54 | { 55 | question: 'Question 2', 56 | color: 'green', 57 | optional: true 58 | }]) 59 | .then(results => console.log(results)) 60 | // Returns {"Question1": "Answer1", "Question 1": "Answer 2"} 61 | ``` 62 | If you pass an empty answer it'll prompt again with the same question unless you mark that question as an optional. 63 | 64 | Simple demo: 65 | 66 | demo 67 | 68 | ## API 69 | 70 | ### `suffleur(questions, [PromiseImplementation])` 71 | 72 | #### - `questions` 73 | 74 | Questions can be string, object or an array. 75 | 76 | In case of a string, that string will be used as a question and an answer will be required. 77 | 78 | If you pass an object following options are allowed: 79 | 80 | ```js 81 | { 82 | question: '', // String, required 83 | color: 'cyan', // String, optional, one of the provided colors, default is cyan 84 | default: '', // String or a number, optional, the default answer to the question 85 | optional: false // Boolean, optional, is an answer required, default is true 86 | } 87 | ``` 88 | 89 | Available colors: 90 | 91 | - cyan 92 | - blue 93 | - green 94 | - magenta 95 | - red 96 | - yellow 97 | 98 | String questions are the same as following object: 99 | 100 | ```js 101 | { 102 | question: 'Some question', 103 | color: 'cyan', 104 | optional: false 105 | } 106 | ``` 107 | 108 | To pass multiple questions just pass an array of strings and objects. 109 | 110 | 111 | #### - `PromiseImplementation` (optional) 112 | 113 | Pass your promise implementation if you don't want to use the default one, you can use Bluebird and any other A+ Promise library. 114 | 115 | #### - `return` 116 | 117 | Souffleur always returns an object with questions as keys and answers as values. 118 | 119 | If answer was empty, value will be null. 120 | 121 | ## Running tests 122 | 123 | Run all the tests: 124 | 125 | ```bash 126 | npm run test 127 | ``` 128 | 129 | Run only some tests: 130 | 131 | ```bash 132 | npm run test -- filter=prefix 133 | ``` 134 | 135 | Get detailed hierarchical test name reporting: 136 | 137 | ```bash 138 | npm run test -- full 139 | ``` 140 | 141 | ## Other 142 | 143 | Feather icon by [Mister Pixel from the Noun Project](https://thenounproject.com/MisterPixel/). 144 | 145 | ## Licence 146 | 147 | MIT - see [LICENCE](LICENCE) 148 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stojanovic/souffleur/e7def86f4b9cb9db4481311615295380907f19f9/demo.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const readline = require('readline') 4 | 5 | const colors = { 6 | reset: '\x1b[0m', 7 | default: '\x1b[39m', 8 | dim: '\x1b[2m', 9 | blue: '\x1b[34m', 10 | cyan: '\x1b[36m', 11 | green: '\x1b[32m', 12 | magenta: '\x1b[35m', 13 | red: '\x1b[31m', 14 | yellow: '\x1b[33m' 15 | } 16 | 17 | module.exports = function prompt(questions, PromiseImplementation, results) { 18 | Promise = PromiseImplementation || global.Promise 19 | const rl = readline.createInterface({ 20 | input: process.stdin, 21 | output: process.stdout 22 | }) 23 | 24 | function singlePrompt(question) { 25 | let questionObject = question 26 | if (typeof question === 'string') 27 | questionObject = { 28 | question: question, 29 | color: 'cyan', 30 | optional: false 31 | } 32 | 33 | let color = questionObject.color || 'cyan' 34 | let questionText = `${colors[color]}${questionObject.question}` 35 | 36 | if (questionObject.default) 37 | questionText += ` ${colors['dim']}(${questionObject.default})` 38 | 39 | questionText += `:${colors.reset} ` 40 | 41 | return new Promise((resolve, reject) => 42 | rl.question(questionText, answer => { 43 | rl.close() 44 | 45 | if (!answer && !questionObject.optional && !questionObject.default) { 46 | console.log(`\n${colors.red}Answer can't be empty!${colors.reset}\n`) 47 | return reject(question) 48 | } 49 | 50 | resolve({ 51 | question: questionObject.question, 52 | answer: answer || questionObject.default || null 53 | }) 54 | }) 55 | ) 56 | } 57 | 58 | if (typeof questions === 'string' || (typeof questions === 'object' && questions.question)) 59 | questions = [questions] 60 | 61 | if (!Array.isArray(questions)) 62 | throw new Error('First argument needs to be an array, string or object') 63 | 64 | if (!results) 65 | results = {} 66 | 67 | if (questions.length) 68 | return singlePrompt(questions.shift()) 69 | .then(response => { 70 | results[response.question] = response.answer 71 | return prompt(questions, Promise, results) 72 | }) 73 | .catch(question => { 74 | if (typeof question === 'string' || typeof question === 'object') { 75 | questions.unshift(question) 76 | return prompt(questions, Promise, results) 77 | } 78 | 79 | return question 80 | }) 81 | 82 | return Promise.resolve(results) 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "souffleur", 3 | "version": "2.0.1", 4 | "description": "Simple command line prompt with retry for empty answers", 5 | "main": "index.js", 6 | "scripts": { 7 | "pretest": "eslint lib spec *.js", 8 | "test": "node spec/support/jasmine-runner.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/stojanovic/souffleur.git" 13 | }, 14 | "keywords": [ 15 | "command", 16 | "line", 17 | "prompt", 18 | "cli" 19 | ], 20 | "author": "Slobodan Stojanovic (http://slobodan.me/)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/stojanovic/souffleur/issues" 24 | }, 25 | "homepage": "https://github.com/stojanovic/souffleur", 26 | "devDependencies": { 27 | "eslint": "^2.12.0", 28 | "eslint-config-defaults": "^9.0.0", 29 | "jasmine": "^2.4.1", 30 | "jasmine-spec-reporter": "^2.5.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /souffleur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stojanovic/souffleur/e7def86f4b9cb9db4481311615295380907f19f9/souffleur.png -------------------------------------------------------------------------------- /spec/souffleur-spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect */ 2 | 'use srict' 3 | 4 | const prompt = require('../index') 5 | 6 | describe('Souffleur (prompt)', () => { 7 | 8 | it('should be a function', () => { 9 | expect(typeof prompt) 10 | .toBe('function') 11 | }) 12 | 13 | it('should throw an error if at least one question is not provided', () => 14 | expect(prompt) 15 | .toThrowError('First argument needs to be an array, string or object') 16 | ) 17 | 18 | it('should throw an error if first argument is not a string, an array or an object', () => 19 | expect(() => prompt(123)) 20 | .toThrowError('First argument needs to be an array, string or object') 21 | ) 22 | 23 | it('should return an object with question as a key and answer as a value if just one question is passed', () => { 24 | prompt({ 25 | question: 'question' 26 | }) 27 | .then(results => 28 | expect(results) 29 | .toBe({ 30 | question: 'answer' 31 | }) 32 | ) 33 | 34 | process.stdin.emit('data', 'answer') 35 | }) 36 | 37 | it('should return an object with question as a key and answer as a value if just one question is passed as a string', () => { 38 | prompt('question') 39 | .then(results => 40 | expect(results) 41 | .toBe({ 42 | question: 'answer' 43 | }) 44 | ) 45 | 46 | process.stdin.emit('data', 'answer') 47 | }) 48 | 49 | it('should return an object with question as a key and answer as a value if just one question is passed as an object', () => { 50 | prompt({ 51 | question: 'question' 52 | }) 53 | .then(results => 54 | expect(results) 55 | .toBe({ 56 | question: 'answer' 57 | }) 58 | ) 59 | 60 | process.stdin.emit('data', 'answer') 61 | }) 62 | 63 | it('should return an object with both question as keys and both answera as values if two question is passed', () => { 64 | prompt(['question1', 'question2']) 65 | .then(results => 66 | expect(results) 67 | .toBe({ 68 | question1: 'answer1', 69 | question2: 'answer2' 70 | }) 71 | ) 72 | 73 | process.stdin.emit('data', 'answer1') 74 | process.stdin.emit('data', 'answer2') 75 | }) 76 | 77 | it('should retry if the answer is empty', () => { 78 | prompt('question') 79 | .then(results => 80 | expect(results) 81 | .toBe({ 82 | question: 'answer' 83 | }) 84 | ) 85 | 86 | process.stdin.emit('data', '') 87 | process.stdin.emit('data', 'answer') 88 | }) 89 | 90 | it('should not retry if the answer is empty and optional', () => { 91 | prompt({ 92 | question: 'question', 93 | optional: true 94 | }) 95 | .then(results => 96 | expect(results) 97 | .toBe({ 98 | question: null 99 | }) 100 | ) 101 | 102 | process.stdin.emit('data', '') 103 | }) 104 | 105 | it('should use the default answer if the answer is empty and default one exists', () => { 106 | prompt({ 107 | question: 'question', 108 | default: 42 109 | }) 110 | .then(results => 111 | expect(results) 112 | .toBe({ 113 | question: 42 114 | }) 115 | ) 116 | 117 | process.stdin.emit('data', '') 118 | }) 119 | 120 | it('should work if you have spaces in the question', () => { 121 | prompt('Question with spaces') 122 | .then(results => 123 | expect(results) 124 | .toBe({ 125 | 'Question with spaces': 'answer' 126 | }) 127 | ) 128 | 129 | process.stdin.emit('data', 'answer') 130 | }) 131 | 132 | it('should keep the last answer if two questions are the same', () => { 133 | prompt(['Question', 'Question']) 134 | .then(results => 135 | expect(results) 136 | .toBe({ 137 | 'Question': 'Answer 2' 138 | }) 139 | ) 140 | 141 | process.stdin.emit('data', 'Answer 1') 142 | process.stdin.emit('data', 'Answer 2') 143 | }) 144 | }) 145 | 146 | process.stdout.setMaxListeners(30) 147 | process.stdin.setMaxListeners(30) 148 | -------------------------------------------------------------------------------- /spec/support/jasmine-runner.js: -------------------------------------------------------------------------------- 1 | /*global jasmine, require, process*/ 2 | var Jasmine = require('jasmine'), 3 | SpecReporter = require('jasmine-spec-reporter'), 4 | noop = function () {}, 5 | jrunner = new Jasmine(), 6 | filter 7 | 8 | process.argv.slice(2).forEach(option => { 9 | 'use strict' 10 | if (option === 'full') { 11 | jrunner.configureDefaultReporter({print: noop}) // remove default reporter logs 12 | jasmine.getEnv().addReporter(new SpecReporter()) // add jasmine-spec-reporter 13 | } 14 | if (option.match('^filter=')) 15 | filter = option.match('^filter=(.*)')[1] 16 | }) 17 | 18 | jrunner.loadConfigFile() // load jasmine.json configuration 19 | jrunner.execute(undefined, filter) 20 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ] 9 | } 10 | --------------------------------------------------------------------------------