├── .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 | [](https://travis-ci.org/stojanovic/souffleur)
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## Installation
12 |
13 | [](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 |
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 |
--------------------------------------------------------------------------------