├── .gitignore ├── .travis.yml ├── README.md ├── example.js ├── index.js ├── lib ├── assign.js ├── cbify.js └── normalizeOptions.js ├── package.json ├── test └── index.js └── types ├── autocomplete.js ├── confirm.js ├── index.js └── text.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | /coverage 4 | /.nyc_output 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "6" 3 | - "7" 4 | sudo: false 5 | language: node_js 6 | script: "npm run test:coverage && npm run test:coverage:report" 7 | after_script: "npm i -g codecov.io && cat ./coverage/lcov.info | codecov" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cli-prompter 2 | 3 | interactive user prompts for the command-line interface 4 | 5 | ```shell 6 | npm install --save cli-prompter 7 | ``` 8 | 9 | built on top of the [`prompt-skeleton`](https://github.com/derhuerst/prompt-skeleton) ecosystem. 10 | 11 | ## example 12 | 13 | ```js 14 | const prompter = require('cli-prompter') 15 | const range = require('array-range') 16 | const randomWord = require('random-word') 17 | const licenses = require('spdx-license-list/simple'); 18 | const getUserName = require('username').sync 19 | 20 | const questions = [{ 21 | type: 'text', 22 | name: 'name', 23 | message: "Give your app a name", 24 | default: range(2).map(randomWord).join('-') 25 | }, { 26 | type: 'text', 27 | name: 'description', 28 | message: "How would you describe the app?", 29 | default: "there are many like it, but this one is mine.", 30 | }, { 31 | type: 'text', 32 | name: 'author', 33 | message: "What is your name on GitHub?", 34 | default: getUserName(), 35 | }, { 36 | type: 'autocomplete', 37 | name: 'license', 38 | message: "Choose a license:", 39 | suggest: suggestLicenses, 40 | default: 'ISC', 41 | }, { 42 | type: 'confirm', 43 | message: 'Continue?', 44 | default: true 45 | }] 46 | 47 | prompter(questions, (err, values) => { 48 | if (err) throw err 49 | console.log(values) 50 | }) 51 | ``` 52 | 53 | see the full example at [./example](./example.js). 54 | 55 | ## usage 56 | 57 | ### `Prompter = require('cli-prompter')` 58 | 59 | ### `Prompter(questions, (err, values) => {})` 60 | 61 | `questions` is an array of objects that each describe a prompt for the user. 62 | 63 | a question object **must** have a string `type` to determine what [prompt type](#prompt-types) will handle it. 64 | 65 | most prompt types support the following: 66 | 67 | ```js 68 | { 69 | type: String, 70 | name: String, 71 | message: String, 72 | default // optional 73 | } 74 | ``` 75 | 76 | the callback will be called when all questions have been answered with either an error or all the answered values. 77 | 78 | ## prompt types 79 | 80 | there are a [wide variety of prompts] available thanks to [@derhuerst](https://github.com/derhuerst)'s [`prompt-skeleton`](https://github.com/derhuerst/prompt-skeleton). 81 | 82 | ### TODO 83 | 84 | - [ ] [date](https://github.com/derhuerst/date-prompt) 85 | - [ ] [mail](https://github.com/derhuerst/mail-prompt) 86 | - [ ] [multiselect](https://github.com/derhuerst/multiselect-prompt) 87 | - [ ] [number](https://github.com/derhuerst/number-prompt) 88 | - [ ] [range](https://github.com/derhuerst/range-prompt) 89 | - [ ] [select](https://github.com/derhuerst/select-prompt) 90 | - [x] [text](https://github.com/derhuerst/text-prompt) 91 | - [ ] [tree-select](https://github.com/derhuerst/tree-select-prompt) 92 | - [ ] [switch](https://github.com/derhuerst/switch-prompt) 93 | - [x] [autocomplete](https://github.com/derhuerst/cli-autocomplete) 94 | - [x] confirm (based on `inquirer` and using `text`) 95 | 96 | here's what we have so far: 97 | 98 | ### text 99 | 100 | ```shell 101 | ✔ Give your app a name (pythonomorphs-rewinders) 102 | ``` 103 | 104 | ```js 105 | { 106 | name: String, 107 | message: String, 108 | default: String // optional 109 | } 110 | ``` 111 | 112 | ### confirm 113 | 114 | ```shell 115 | ✔ Continue? (Y/n) … 116 | ``` 117 | 118 | ```js 119 | { 120 | name: String, // optional 121 | message: String, 122 | default: Boolean // optional 123 | } 124 | ``` 125 | 126 | ### autocomplete 127 | 128 | ```shell 129 | ? Choose a license: (ISC) › GPL 130 | NGPL 131 | LGPLLR 132 | GPL-1.0 133 | GPL-2.0 134 | GPL-3.0 135 | GPL-2.0+ 136 | LGPL-2.1 137 | LGPL-3.0 138 | LGPL-2.0 139 | AGPL-3.0 140 | ``` 141 | 142 | ```js 143 | { 144 | name: String, 145 | message: String, 146 | default: String, // optional 147 | suggest: ({ input, values }, cb) => cb(err, suggestions) 148 | } 149 | ``` 150 | 151 | where `suggestions` is an array of either: 152 | 153 | - strings 154 | - objects with `title` and `value` keys 155 | 156 | ## license 157 | 158 | The Apache License 159 | 160 | Copyright © 2017 Michael Williams 161 | 162 | Licensed under the Apache License, Version 2.0 (the "License"); 163 | you may not use this file except in compliance with the License. 164 | You may obtain a copy of the License at 165 | 166 | http://www.apache.org/licenses/LICENSE-2.0 167 | 168 | Unless required by applicable law or agreed to in writing, software 169 | distributed under the License is distributed on an "AS IS" BASIS, 170 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 171 | See the License for the specific language governing permissions and 172 | limitations under the License. 173 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const prompter = require('./') 2 | const range = require('array-range') 3 | const randomWord = require('random-word') 4 | const licenses = require('spdx-license-list/simple') 5 | const getUserName = require('username').sync 6 | 7 | const questions = [{ 8 | type: 'text', 9 | name: 'name', 10 | message: 'Give your app a name', 11 | default: range(2).map(randomWord).join('-') 12 | }, { 13 | type: 'text', 14 | name: 'description', 15 | message: 'How would you describe the app?', 16 | default: 'there are many like it, but this one is mine.' 17 | }, { 18 | type: 'text', 19 | name: 'author', 20 | message: 'What is your name on GitHub?', 21 | default: getUserName() 22 | }, { 23 | type: 'autocomplete', 24 | name: 'license', 25 | message: 'Choose a license:', 26 | suggest: suggestLicenses, 27 | default: 'ISC' 28 | }, { 29 | type: 'confirm', 30 | message: 'Continue?', 31 | default: true 32 | }] 33 | 34 | prompter(questions, (err, values) => { 35 | if (err) throw err 36 | console.log(values) 37 | }) 38 | 39 | function suggestLicenses ({ input }, cb) { 40 | const suggested = Array.from(licenses) 41 | .filter(filter(input)) 42 | .sort((a, b) => a.length - b.length) 43 | cb(null, suggested) 44 | } 45 | 46 | function filter (input) { 47 | return function (text) { 48 | return new RegExp(input, 'i').exec(text) !== null 49 | // return new RegExp('^' + input, 'i').exec(text) !== null 50 | } 51 | } 52 | 53 | process.on('unhandledRejection', console.error) 54 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const waterfall = require('run-waterfall') 2 | 3 | module.exports = cliPrompter 4 | 5 | const promptTypes = require('./types') 6 | 7 | function cliPrompter (questions, cb) { 8 | waterfall([ 9 | cb => cb(null, {}), // start the waterfall with values 10 | ...questions.map(promptQuestion) 11 | ], cb) 12 | } 13 | 14 | function promptQuestion (question) { 15 | const { type } = question 16 | const Prompter = promptTypes[type] 17 | 18 | return (values, cb) => { 19 | Prompter({ question, values }, cb) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /lib/assign.js: -------------------------------------------------------------------------------- 1 | const { assign } = Object 2 | module.exports = (values, name, value) => { 3 | return assign({}, values, { [name]: value }) 4 | } 5 | -------------------------------------------------------------------------------- /lib/cbify.js: -------------------------------------------------------------------------------- 1 | module.exports = function cbify (promise) { 2 | return cb => { 3 | promise 4 | .then(value => cb(null, value)) 5 | .catch(cb) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/normalizeOptions.js: -------------------------------------------------------------------------------- 1 | const is = require('typeof-is') 2 | 3 | module.exports = function (options) { 4 | return options.map(option => { 5 | if (is.object(option) && option.title && option.value) return option 6 | else if (is.string(option)) return { title: option, value: option } 7 | else throw new Error('cli-prompter: not sure how to normalize these options') 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli-prompter", 3 | "version": "1.0.1", 4 | "description": "interactive user prompts for the command-line interface", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node-dev example", 8 | "test:deps": "dependency-check . && dependency-check . --extra --no-dev -i es2040", 9 | "test:lint": "standard", 10 | "test:node": "NODE_ENV=test run-default tape test/*.js --", 11 | "test:coverage": "NODE_ENV=test nyc npm run test:node", 12 | "test:coverage:report": "nyc report --reporter=lcov npm run test:node", 13 | "test": "npm-run-all -s test:node test:lint test:deps" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/ahdinosaur/cli-prompter.git" 18 | }, 19 | "keywords": [], 20 | "author": "Mikey (http://dinosaur.is)", 21 | "license": "Apache-2.0", 22 | "bugs": { 23 | "url": "https://github.com/ahdinosaur/cli-prompter/issues" 24 | }, 25 | "homepage": "https://github.com/ahdinosaur/cli-prompter#readme", 26 | "devDependencies": { 27 | "array-range": "^1.0.1", 28 | "dependency-check": "^2.7.0", 29 | "node-dev": "^3.1.3", 30 | "npm-run-all": "^4.0.1", 31 | "nyc": "^10.1.2", 32 | "random-word": "^2.0.0", 33 | "run-default": "^1.0.0", 34 | "spdx-license-list": "^3.0.1", 35 | "standard": "^8.6.0", 36 | "tape": "^4.6.3", 37 | "username": "^2.3.0" 38 | }, 39 | "dependencies": { 40 | "cli-autocomplete": "0.4.1", 41 | "lagden-promisify": "^3.0.0", 42 | "run-waterfall": "^1.1.3", 43 | "text-prompt": "^0.1.1", 44 | "typeof-is": "^1.0.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | 3 | const cliPrompter = require('../') 4 | 5 | test('cli-prompter', function (t) { 6 | t.ok(cliPrompter, 'module is require-able') 7 | t.end() 8 | }) 9 | -------------------------------------------------------------------------------- /types/autocomplete.js: -------------------------------------------------------------------------------- 1 | const pify = require('lagden-promisify') 2 | const autocompletePrompt = require('cli-autocomplete') 3 | 4 | const assign = require('../lib/assign') 5 | const normalizeOptions = require('../lib/normalizeOptions') 6 | 7 | module.exports = function AutoComplete ({ question, values }, cb) { 8 | const { name, message, default: dft, suggest } = question 9 | const display = `${message}${dft ? ` (${dft})` : ''})` 10 | const suggester = Suggester({ suggest, values }) 11 | return autocompletePrompt(display, suggester) 12 | .once('submit', value => { 13 | if (value === '') { 14 | if (dft) value = dft 15 | else return AutoComplete({ question, values }, cb) // retry 16 | } 17 | const nextValues = assign(values, name, value) 18 | cb(null, nextValues) 19 | }) 20 | .once('error', cb) 21 | } 22 | 23 | function Suggester ({ suggest, values }) { 24 | return pify(function (input, cb) { 25 | return suggest({ input, values }, (err, suggestions) => { 26 | if (err) cb(err) 27 | else cb(null, normalizeOptions(suggestions)) 28 | }) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /types/confirm.js: -------------------------------------------------------------------------------- 1 | const assign = require('../lib/assign') 2 | const textPrompt = require('text-prompt') 3 | 4 | module.exports = function Confirm ({ question, values }, cb) { 5 | const { name, message, default: dft } = question 6 | const display = `${message} (${dft ? 'Y/n' : 'y/N'})` 7 | return textPrompt(display) 8 | .once('submit', value => { 9 | if (!confirmed(value)) return Confirm({ question, values }, cb) 10 | if (name) values = assign(values, name, true) 11 | cb(null, values) 12 | }) 13 | .once('error', cb) 14 | 15 | function confirmed (input) { 16 | var value = dft 17 | if (input != null && input !== '') { 18 | value = /^y(es)?/i.test(input) 19 | } 20 | return value 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /types/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | autocomplete: require('./autocomplete'), 3 | confirm: require('./confirm'), 4 | text: require('./text') 5 | } 6 | -------------------------------------------------------------------------------- /types/text.js: -------------------------------------------------------------------------------- 1 | const assign = require('../lib/assign') 2 | const textPrompt = require('text-prompt') 3 | 4 | module.exports = function Text ({ question, values }, cb) { 5 | const { name, message, default: dft } = question 6 | let display = `${message}${dft !== undefined ? ` (${dft})` : ''}` 7 | return textPrompt(display) 8 | .once('submit', value => { 9 | if (value === '') { 10 | if (dft) value = dft 11 | else return Text({ question, values }, cb) // retry 12 | } 13 | const nextValues = assign(values, name, value) 14 | cb(null, nextValues) 15 | }) 16 | .once('error', cb) 17 | } 18 | --------------------------------------------------------------------------------