├── .gitignore ├── .npmrc ├── demo.js ├── index.js ├── license.md ├── package.json ├── readme.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # build output 5 | build.js 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | require('./build')() 2 | .then(email => { 3 | console.log('\n> Thanks ' + email) 4 | }) 5 | .catch(() => { 6 | console.log('\n> Aborted! Bye') 7 | }) 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ansi = require('ansi-escapes') 2 | const chalk = require('chalk') 3 | 4 | // eslint-disable-next-line no-multi-assign 5 | module.exports = exports.default = function emailPrompt({ 6 | start = '> Enter your email: ', 7 | domains = new Set([ 8 | 'aol.com', 9 | 'gmail.com', 10 | 'google.com', 11 | 'yahoo.com', 12 | 'ymail.com', 13 | 'hotmail.com', 14 | 'live.com', 15 | 'outlook.com', 16 | 'inbox.com', 17 | 'mail.com', 18 | 'gmx.com', 19 | 'icloud.com', 20 | 'zeit.co', 21 | 'vercel.com', 22 | 'hey.com' 23 | ]), 24 | forceLowerCase = true, 25 | suggestionColor = 'gray', 26 | autoCompleteChars = new Set([ 27 | '\t' /* tab */, 28 | '\r' /* return */, 29 | '\x1b[C' /* right arrow */, 30 | ' ' /* Spacebar */ 31 | ]), 32 | resolveChars = new Set(['\r']), 33 | abortChars = new Set(['\x03']), 34 | allowInvalidChars = false 35 | } = {}) { 36 | return new Promise((resolve, reject) => { 37 | // Some environments (e.g., cygwin) don't provide a tty 38 | if (!process.stdin.setRawMode) { 39 | return reject(new Error('stdin lacks setRawMode support')) 40 | } 41 | 42 | const isRaw = process.stdin.isRaw 43 | 44 | process.stdout.write(start) 45 | process.stdin.setRawMode(true) 46 | process.stdin.resume() 47 | 48 | let val = '' 49 | let suggestion = '' 50 | let caretOffset = 0 51 | 52 | // To make `for..of` work with buble 53 | const _domains = Array.from(domains) 54 | 55 | const ondata = v => { 56 | const s = v.toString() 57 | 58 | // Abort upon ctrl+C 59 | if (abortChars.has(s)) { 60 | restore() 61 | return reject(new Error('User abort')) 62 | } 63 | 64 | let completion = '' 65 | 66 | // If we have a suggestion *and* 67 | // the user is at the end of the line *and* 68 | // the user pressed one of the keys to trigger completion 69 | if (suggestion !== '' && !caretOffset && autoCompleteChars.has(s)) { 70 | val += suggestion 71 | suggestion = '' 72 | } else { 73 | if (s === '\x1b[D') { 74 | if (val.length > Math.abs(caretOffset)) { 75 | caretOffset-- 76 | } 77 | } else if (s === '\x1b[C') { 78 | if (caretOffset < 0) { 79 | caretOffset++ 80 | } 81 | } else if (s === '\x08' || s === '\x7f') { 82 | // Delete key needs splicing according to caret position 83 | val = val.substr(0, val.length + caretOffset - 1) + 84 | val.substr(val.length + caretOffset) 85 | } else { 86 | if (resolveChars.has(s)) { 87 | restore() 88 | return resolve(val) 89 | } 90 | 91 | if (!allowInvalidChars) { 92 | // Disallow more than one @ 93 | if (/@/.test(val) && s === '@') { 94 | return 95 | } 96 | 97 | if (/[^A-z0-9-+_.@]/.test(s)) { 98 | return 99 | } 100 | } 101 | 102 | const add = forceLowerCase ? s.toLowerCase() : s 103 | val = val.substr(0, val.length + caretOffset) + add + 104 | val.substr(val.length + caretOffset) 105 | } 106 | 107 | const parts = val.split('@') 108 | if (parts.length === 2 && parts[1].length > 0) { 109 | const [, _host] = parts 110 | const host = _host.toLowerCase() 111 | for (const domain of _domains) { 112 | if (host === domain) { 113 | break 114 | } 115 | 116 | if (host === domain.substr(0, host.length)) { 117 | suggestion = domain.substr(host.length) 118 | completion = chalk[suggestionColor](suggestion) 119 | completion += ansi.cursorBackward(domain.length - host.length) 120 | break 121 | } 122 | } 123 | } 124 | 125 | if (completion.length === 0) { 126 | suggestion = '' 127 | } 128 | } 129 | 130 | process.stdout.write(ansi.eraseLines(1) + start + val + completion) 131 | if (caretOffset) { 132 | process.stdout.write(ansi.cursorBackward(Math.abs(caretOffset))) 133 | } 134 | } 135 | 136 | const restore = () => { 137 | process.stdin.setRawMode(isRaw) 138 | process.stdin.pause() 139 | process.stdin.removeListener('data', ondata) 140 | } 141 | 142 | process.stdin.on('data', ondata) 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Vercel, Inc. 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "email-prompt", 3 | "version": "0.4.0", 4 | "repository": "vercel/email-prompt", 5 | "license": "MIT", 6 | "files": [ 7 | "build.js" 8 | ], 9 | "main": "build.js", 10 | "description": "CLI email prompt with autocompletion and built-in validation", 11 | "scripts": { 12 | "build": "buble -y dangerousForOf index.js > build.js", 13 | "prepublish": "npm run build" 14 | }, 15 | "dependencies": { 16 | "ansi-escapes": "2.0.0", 17 | "chalk": "1.1.3" 18 | }, 19 | "devDependencies": { 20 | "buble": "0.15.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # email-prompt 2 | 3 | CLI email prompt featuring autocompletion and validation. 4 | Powers [vercel](https://vercel.com/) `--login`. 5 | 6 | ![prompt](https://cloud.githubusercontent.com/assets/13041/15456597/36b76246-202a-11e6-99e8-3839514bed57.gif) 7 | 8 | ## Usage 9 | 10 | ```js 11 | import emailPrompt from 'email-prompt'; 12 | 13 | let email; 14 | 15 | try { 16 | email = await emailPrompt({ 17 | /* options */ 18 | }); 19 | } catch (err) { 20 | console.log('\n> Aborted!'); 21 | return; 22 | } 23 | 24 | console.log('\n> Hello ' + email); 25 | ``` 26 | 27 | To run the demo, [clone](https://help.github.com/articles/cloning-a-repository/) the project and run: 28 | 29 | ```bash 30 | npm install 31 | node demo 32 | ``` 33 | 34 | ### Options 35 | 36 | - `start` (`String`): the beginning of the prompt. Defaults to `> Enter your email: ` 37 | - `domains` (`Set`): domain names to autocomplete (as `String`). Defaults to: 38 | - `aol.com` 39 | - `gmail.com` 40 | - `google.com` 41 | - `yahoo.com` 42 | - `ymail.com` 43 | - `hotmail.com` 44 | - `live.com` 45 | - `outlook.com` 46 | - `inbox.com` 47 | - `mail.com` 48 | - `gmx.com` 49 | - `icloud.com` 50 | - `hey.com` 51 | - `zeit.co` 52 | - `vercel.com` 53 | - `forceLowerCase` (`Boolean`): converts all input to lowercase. Defaults to `true`. 54 | - `suggestionColor` (`String`): a [chalk](https://github.com/chalk/chalk) color. Defaults to `gray` 55 | - `autocompleteChars` (`Set`): a set of chars that trigger autocompletion. Defaults to: 56 | - ↹ Tab 57 | - ↵ Return (enter) 58 | - → Right arrow 59 | - `resolveChars` (`Set`): a set of chars that resolve the promise. Defaults to ↵return 60 | - `abortChars` (`Set`): a set of chars that abort the process. Defaults to Ctrl+C 61 | - `allowInvalidChars` (`Boolean`): controls whether non-email chars are accepted. Defaults to `false` 62 | 63 | ### Notes 64 | 65 | Some important implementation details: 66 | 67 | - `email-prompt` automatically adapts the mode of `process.stdin` for you. 68 | - The `stdin` stream is `resume`d and `pause`d upon the promise being 69 | settled. 70 | - When the promise resolves or rejects, the previous stdin mode is restored. 71 | - The `tty` mode is set to `raw`, which means all the caret interactions 72 | that you come to expect in a regular `stdin` prompt are simulated. 73 | This gives us fine-grained control over the output and powers the 74 | validation. 75 | 76 | ## Authors 77 | 78 | - Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) - [Vercel](https://vercel.com) 79 | - Leo Lamprecht ([@notquiteleo](https://twitter.com/notquiteleo)) - [Vercel](https://vercel.com) 80 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | acorn-jsx@^3.0.1: 6 | version "3.0.1" 7 | resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" 8 | dependencies: 9 | acorn "^3.0.4" 10 | 11 | acorn-object-spread@^1.0.0: 12 | version "1.0.0" 13 | resolved "https://registry.yarnpkg.com/acorn-object-spread/-/acorn-object-spread-1.0.0.tgz#48ead0f4a8eb16995a17a0db9ffc6acaada4ba68" 14 | dependencies: 15 | acorn "^3.1.0" 16 | 17 | acorn@^3.0.4, acorn@^3.1.0, acorn@^3.3.0: 18 | version "3.3.0" 19 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" 20 | 21 | ansi-escapes@2.0.0: 22 | version "2.0.0" 23 | resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b" 24 | 25 | ansi-regex@^2.0.0: 26 | version "2.1.1" 27 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 28 | 29 | ansi-styles@^2.2.1: 30 | version "2.2.1" 31 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 32 | 33 | buble@0.15.2: 34 | version "0.15.2" 35 | resolved "https://registry.yarnpkg.com/buble/-/buble-0.15.2.tgz#547fc47483f8e5e8176d82aa5ebccb183b02d613" 36 | dependencies: 37 | acorn "^3.3.0" 38 | acorn-jsx "^3.0.1" 39 | acorn-object-spread "^1.0.0" 40 | chalk "^1.1.3" 41 | magic-string "^0.14.0" 42 | minimist "^1.2.0" 43 | os-homedir "^1.0.1" 44 | 45 | chalk@1.1.3, chalk@^1.1.3: 46 | version "1.1.3" 47 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 48 | dependencies: 49 | ansi-styles "^2.2.1" 50 | escape-string-regexp "^1.0.2" 51 | has-ansi "^2.0.0" 52 | strip-ansi "^3.0.0" 53 | supports-color "^2.0.0" 54 | 55 | escape-string-regexp@^1.0.2: 56 | version "1.0.5" 57 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 58 | 59 | has-ansi@^2.0.0: 60 | version "2.0.0" 61 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 62 | dependencies: 63 | ansi-regex "^2.0.0" 64 | 65 | magic-string@^0.14.0: 66 | version "0.14.0" 67 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.14.0.tgz#57224aef1701caeed273b17a39a956e72b172462" 68 | dependencies: 69 | vlq "^0.2.1" 70 | 71 | minimist@^1.2.0: 72 | version "1.2.5" 73 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 74 | 75 | os-homedir@^1.0.1: 76 | version "1.0.2" 77 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 78 | 79 | strip-ansi@^3.0.0: 80 | version "3.0.1" 81 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 82 | dependencies: 83 | ansi-regex "^2.0.0" 84 | 85 | supports-color@^2.0.0: 86 | version "2.0.0" 87 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 88 | 89 | vlq@^0.2.1: 90 | version "0.2.2" 91 | resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.2.tgz#e316d5257b40b86bb43cb8d5fea5d7f54d6b0ca1" 92 | --------------------------------------------------------------------------------