Browse Source
* Add the skeleton of `now cc` * Add the `ls` command * Add `inquirer` dependency * Add the `set-default` command * Fix typo * Show the real number of cards when selecting the default one * Add the `ls` command * Fix: Do not throw if there's no cards in the account * Add `blessed` dependency * Add the first sketch of `now cc add` * Add instructions * Add labels * Save every element in the `elements` array instead of variables * Tweaks * Fix: update the element attribute if it's not a special case * Add the `name` input; Add moving between inputs; Make the state more reliable * Auto "detect" if the input is losing focus * Remove useless stuff * Add the ability to move between the fields with tab/shift+tab * Add CCV field * Make the cycling between the fields "infinite" * Add expiration date field and allow only numbers in the CCV field * The form shouldn't have a fixed height * Add the address box and label * Add the address fields * Remove blessed stuff * Add preliminary input field * output utils * add prompt for booleans * fix @matheuss linting problems * remove example * lint * error and info helpers * helper for embedded commands * Remove useless stuff * Add `trailing` option * Add `resolveChars` option * Add `validate` option * Add `strip-ansi` dependency * Add `credit-card` dependency * Add credit card masking * Add support for expiration date mask * Make things simpler * Add auto completion support * Always show the `card_` id prefix * Add `@google/maps` dependency * Always print the initial value if it's available * Add `stripe` dependency * Add `add()` method * Add billing related utils * Add `now cc add` * Rename `cc` to `billing` * Fix: log only one blank line * Refactor * Add list input component * This shouldn't be here * Add `code` output util * Add `now upgrade | downgrade` * add build step * make it more future-proof * more reliable build * remove lock for now * Hide the CCV * Print the new line before `Saving card` * Use the new `success` component * Add confirmation steps for `cc rm` and `cc set-default` * Temporarily monket patch Inquirer * Build before testing * Run the tests using the built files * Fix the `prepublish` script and run the `build` one before packaging * Improve `now help`master
Matheus Fernandes
8 years ago
committed by
GitHub
28 changed files with 1609 additions and 31 deletions
@ -0,0 +1,203 @@ |
|||||
|
#!/usr/bin/env node
|
||||
|
|
||||
|
const ansiEscapes = require('ansi-escapes') |
||||
|
const chalk = require('chalk') |
||||
|
const ccValidator = require('credit-card') |
||||
|
|
||||
|
const textInput = require('../lib/utils/input/text') |
||||
|
const countries = require('../lib/utils/billing/country-list') |
||||
|
const cardBrands = require('../lib/utils/billing/card-brands') |
||||
|
const geocode = require('../lib/utils/billing/geocode') |
||||
|
const success = require('../lib/utils/output/success') |
||||
|
const wait = require('../lib/utils/output/wait') |
||||
|
|
||||
|
function rightPad(string, n = 12) { |
||||
|
n -= string.length |
||||
|
return string + ' '.repeat(n > -1 ? n : 0) |
||||
|
} |
||||
|
|
||||
|
function expDateMiddleware(data) { |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
module.exports = function (creditCards) { |
||||
|
const state = { |
||||
|
error: undefined, |
||||
|
cardGroupLabel: `> ${chalk.bold('Enter your card details')}`, |
||||
|
|
||||
|
name: { |
||||
|
label: rightPad('Name'), |
||||
|
placeholder: 'John Appleseed', |
||||
|
validateValue: data => data.trim().length > 0 |
||||
|
}, |
||||
|
|
||||
|
cardNumber: { |
||||
|
label: rightPad('Number'), |
||||
|
mask: 'cc', |
||||
|
placeholder: '#### #### #### ####', |
||||
|
validateKeypress: (data, value) => ( |
||||
|
/\d/.test(data) && value.length < 19 |
||||
|
), |
||||
|
validateValue: data => { |
||||
|
data = data.replace(/ /g, '') |
||||
|
const type = ccValidator.determineCardType(data) |
||||
|
if (!type) { |
||||
|
return false |
||||
|
} |
||||
|
return ccValidator.isValidCardNumber(data, type) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
ccv: { |
||||
|
label: rightPad('CCV'), |
||||
|
mask: 'ccv', |
||||
|
placeholder: '###', |
||||
|
validateValue: data => { |
||||
|
const brand = state.cardNumber.brand.toLowerCase() |
||||
|
return ccValidator.doesCvvMatchType(data, brand) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
expDate: { |
||||
|
label: rightPad('Exp. Date'), |
||||
|
mask: 'expDate', |
||||
|
placeholder: 'mm / yyyy', |
||||
|
middleware: expDateMiddleware, |
||||
|
validateValue: data => !ccValidator.isExpired(...data.split(' / ')) |
||||
|
}, |
||||
|
|
||||
|
addressGroupLabel: `\n> ${chalk.bold('Enter your billing address')}`, |
||||
|
|
||||
|
country: { |
||||
|
label: rightPad('Country'), |
||||
|
async autoComplete(value) { |
||||
|
for (const country in countries) { |
||||
|
if (!Object.hasOwnProperty.call(countries, country)) { |
||||
|
continue |
||||
|
} |
||||
|
if (country.startsWith(value)) { |
||||
|
return country.substr(value.length) |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
}, |
||||
|
validateValue: value => countries[value] !== undefined |
||||
|
}, |
||||
|
|
||||
|
zipCode: { |
||||
|
label: rightPad('ZIP'), |
||||
|
validadeKeypress: data => data.trim().length > 0, |
||||
|
validateValue: data => data.trim().length > 0 |
||||
|
}, |
||||
|
|
||||
|
state: { |
||||
|
label: rightPad('State'), |
||||
|
validateValue: data => data.trim().length > 0 |
||||
|
}, |
||||
|
|
||||
|
city: { |
||||
|
label: rightPad('City'), |
||||
|
validateValue: data => data.trim().length > 0 |
||||
|
}, |
||||
|
|
||||
|
address1: { |
||||
|
label: rightPad('Address'), |
||||
|
validateValue: data => data.trim().length > 0 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function render() { |
||||
|
for (const key in state) { |
||||
|
if (!Object.hasOwnProperty.call(state, key)) { |
||||
|
continue |
||||
|
} |
||||
|
const piece = state[key] |
||||
|
if (typeof piece === 'string') { |
||||
|
console.log(piece) |
||||
|
} else if (typeof piece === 'object') { |
||||
|
let result |
||||
|
try { |
||||
|
result = await textInput({ |
||||
|
label: '- ' + piece.label, |
||||
|
initialValue: piece.initialValue || piece.value, |
||||
|
placeholder: piece.placeholder, |
||||
|
mask: piece.mask, |
||||
|
validateKeypress: piece.validateKeypress, |
||||
|
validateValue: piece.validateValue, |
||||
|
autoComplete: piece.autoComplete |
||||
|
}) |
||||
|
piece.value = result |
||||
|
if (key === 'cardNumber') { |
||||
|
let brand = cardBrands[ccValidator.determineCardType(result)] |
||||
|
piece.brand = brand |
||||
|
if (brand === 'American Express') { |
||||
|
state.ccv.placeholder = '#'.repeat(4) |
||||
|
} else { |
||||
|
state.ccv.placeholder = '#'.repeat(3) |
||||
|
} |
||||
|
brand = chalk.cyan(`[${brand}]`) |
||||
|
process.stdout.write( |
||||
|
`${chalk.cyan('✓')} ${piece.label}${result} ${brand}\n` |
||||
|
) |
||||
|
} else if (key === 'ccv') { |
||||
|
process.stdout.write( |
||||
|
`${chalk.cyan('✓')} ${piece.label}${'*'.repeat(result.length)}\n` |
||||
|
) |
||||
|
} else if (key === 'expDate') { |
||||
|
let text = result.split(' / ') |
||||
|
text = text[0] + chalk.gray(' / ') + text[1] |
||||
|
process.stdout.write(`${chalk.cyan('✓')} ${piece.label}${text}\n`) |
||||
|
} else if (key === 'zipCode') { |
||||
|
const stopSpinner = wait(piece.label + result) |
||||
|
const addressInfo = await geocode({ |
||||
|
country: state.country.value, |
||||
|
zipCode: result |
||||
|
}) |
||||
|
if (addressInfo.state) { |
||||
|
state.state.initialValue = addressInfo.state |
||||
|
} |
||||
|
if (addressInfo.city) { |
||||
|
state.city.initialValue = addressInfo.city |
||||
|
} |
||||
|
stopSpinner() |
||||
|
process.stdout.write(`${chalk.cyan('✓')} ${piece.label}${result}\n`) |
||||
|
} else { |
||||
|
process.stdout.write(`${chalk.cyan('✓')} ${piece.label}${result}\n`) |
||||
|
} |
||||
|
} catch (err) { |
||||
|
if (err.message === 'USER_ABORT') { |
||||
|
process.exit(1) |
||||
|
} else { |
||||
|
console.error(err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
console.log('') // new line
|
||||
|
const stopSpinner = wait('Saving card') |
||||
|
|
||||
|
try { |
||||
|
const res = await creditCards.add({ |
||||
|
name: state.name.value, |
||||
|
cardNumber: state.cardNumber.value, |
||||
|
ccv: state.ccv.value, |
||||
|
expDate: state.expDate.value, |
||||
|
country: state.country.value, |
||||
|
zipCode: state.zipCode.value, |
||||
|
state: state.state.value, |
||||
|
city: state.city.value, |
||||
|
address1: state.address1.value |
||||
|
}) |
||||
|
stopSpinner() |
||||
|
success(`${state.cardNumber.brand} ending in ${res.last4} was added to your account`) |
||||
|
} catch (err) { |
||||
|
stopSpinner() |
||||
|
const linesToClear = state.error ? 13 : 12 |
||||
|
process.stdout.write(ansiEscapes.eraseLines(linesToClear)) |
||||
|
state.error = `${chalk.red('> Error!')} ${err.message} Please make sure the info is correct` |
||||
|
await render() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render().catch(console.error) |
||||
|
} |
@ -0,0 +1,315 @@ |
|||||
|
#!/usr/bin/env node
|
||||
|
|
||||
|
// Native
|
||||
|
const {resolve} = require('path') |
||||
|
|
||||
|
// Packages
|
||||
|
const chalk = require('chalk') |
||||
|
const minimist = require('minimist') |
||||
|
const ms = require('ms') |
||||
|
|
||||
|
// Ours
|
||||
|
const login = require('../lib/login') |
||||
|
const cfg = require('../lib/cfg') |
||||
|
const {error} = require('../lib/error') |
||||
|
const NowCreditCards = require('../lib/credit-cards') |
||||
|
const indent = require('../lib/indent') |
||||
|
const listInput = require('../lib/utils/input/list') |
||||
|
const success = require('../lib/utils/output/success') |
||||
|
const promptBool = require('../lib/utils/output/prompt-bool') |
||||
|
|
||||
|
const argv = minimist(process.argv.slice(2), { |
||||
|
string: ['config', 'token'], |
||||
|
boolean: ['help', 'debug'], |
||||
|
alias: { |
||||
|
help: 'h', |
||||
|
config: 'c', |
||||
|
debug: 'd', |
||||
|
token: 't' |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
const subcommand = argv._[0] |
||||
|
|
||||
|
const help = () => { |
||||
|
console.log(` |
||||
|
${chalk.bold('𝚫 now billing')} <ls | add | rm | set-default> |
||||
|
|
||||
|
${chalk.dim('Options:')} |
||||
|
|
||||
|
-h, --help Output usage information |
||||
|
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file |
||||
|
-d, --debug Debug mode [off] |
||||
|
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token |
||||
|
|
||||
|
${chalk.dim('Examples:')} |
||||
|
|
||||
|
${chalk.gray('–')} Lists all your credit cards: |
||||
|
|
||||
|
${chalk.cyan('$ now billing ls')} |
||||
|
|
||||
|
${chalk.gray('–')} Adds a credit card (interactively): |
||||
|
|
||||
|
${chalk.cyan(`$ now billing add`)} |
||||
|
|
||||
|
${chalk.gray('–')} Removes a credit card: |
||||
|
|
||||
|
${chalk.cyan(`$ now billing rm <id>`)} |
||||
|
|
||||
|
${chalk.gray('–')} If the id is ommitted, you can choose interactively |
||||
|
|
||||
|
${chalk.gray('–')} Selects your default credit card: |
||||
|
|
||||
|
${chalk.cyan(`$ now billing set-default <id>`)} |
||||
|
|
||||
|
${chalk.gray('–')} If the id is ommitted, you can choose interactively |
||||
|
`)
|
||||
|
} |
||||
|
|
||||
|
// options
|
||||
|
const debug = argv.debug |
||||
|
const apiUrl = argv.url || 'https://api.zeit.co' |
||||
|
|
||||
|
if (argv.config) { |
||||
|
cfg.setConfigFile(argv.config) |
||||
|
} |
||||
|
|
||||
|
const exit = code => { |
||||
|
// we give stdout some time to flush out
|
||||
|
// because there's a node bug where
|
||||
|
// stdout writes are asynchronous
|
||||
|
// https://github.com/nodejs/node/issues/6456
|
||||
|
setTimeout(() => process.exit(code || 0), 100) |
||||
|
} |
||||
|
|
||||
|
if (argv.help || !subcommand) { |
||||
|
help() |
||||
|
exit(0) |
||||
|
} else { |
||||
|
const config = cfg.read() |
||||
|
|
||||
|
Promise.resolve(argv.token || config.token || login(apiUrl)) |
||||
|
.then(async token => { |
||||
|
try { |
||||
|
await run(token) |
||||
|
} catch (err) { |
||||
|
if (err.userError) { |
||||
|
error(err.message) |
||||
|
} else { |
||||
|
error(`Unknown error: ${err.stack}`) |
||||
|
} |
||||
|
exit(1) |
||||
|
} |
||||
|
}) |
||||
|
.catch(e => { |
||||
|
error(`Authentication error – ${e.message}`) |
||||
|
exit(1) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Builds a `choices` object that can be passesd to inquirer.prompt()
|
||||
|
function buildInquirerChoices(cards) { |
||||
|
return cards.cards.map(card => { |
||||
|
const _default = card.id === cards.defaultCardId ? ' ' + chalk.bold('(default)') : '' |
||||
|
const id = `${chalk.cyan(`ID: ${card.id}`)}${_default}` |
||||
|
const number = `${chalk.gray('#### ').repeat(3)}${card.last4}` |
||||
|
const str = [ |
||||
|
id, |
||||
|
indent(card.name, 2), |
||||
|
indent(`${card.brand} ${number}`, 2) |
||||
|
].join('\n') |
||||
|
|
||||
|
return { |
||||
|
name: str, // Will be displayed by Inquirer
|
||||
|
value: card.id, // Will be used to identify the answer
|
||||
|
short: card.id // Will be displayed after the users answers
|
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async function run(token) { |
||||
|
const start = new Date() |
||||
|
const creditCards = new NowCreditCards(apiUrl, token, {debug}) |
||||
|
const args = argv._.slice(1) |
||||
|
|
||||
|
switch (subcommand) { |
||||
|
case 'ls': |
||||
|
case 'list': { |
||||
|
const cards = await creditCards.ls() |
||||
|
const text = cards.cards.map(card => { |
||||
|
const _default = card.id === cards.defaultCardId ? ' ' + chalk.bold('(default)') : '' |
||||
|
const id = `${chalk.gray('-')} ${chalk.cyan(`ID: ${card.id}`)}${_default}` |
||||
|
const number = `${chalk.gray('#### ').repeat(3)}${card.last4}` |
||||
|
let address = card.address_line1 |
||||
|
|
||||
|
if (card.address_line2) { |
||||
|
address += `, ${card.address_line2}.` |
||||
|
} else { |
||||
|
address += '.' |
||||
|
} |
||||
|
|
||||
|
address += `\n${card.address_city}, ` |
||||
|
|
||||
|
if (card.address_state) { |
||||
|
address += `${card.address_state}, ` |
||||
|
} |
||||
|
|
||||
|
// TODO: Stripe is returning a two digit code for the country,
|
||||
|
// but we want the full country name
|
||||
|
address += `${card.address_zip}. ${card.address_country}` |
||||
|
|
||||
|
return [ |
||||
|
id, |
||||
|
indent(card.name, 2), |
||||
|
indent(`${card.brand} ${number}`, 2), |
||||
|
indent(address, 2) |
||||
|
].join('\n') |
||||
|
}).join('\n\n') |
||||
|
|
||||
|
const elapsed = ms(new Date() - start) |
||||
|
console.log(`> ${cards.cards.length} card${cards.cards.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`) |
||||
|
if (text) { |
||||
|
console.log(`\n${text}\n`) |
||||
|
} |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
case 'set-default': { |
||||
|
if (args.length > 1) { |
||||
|
error('Invalid number of arguments') |
||||
|
return exit(1) |
||||
|
} |
||||
|
|
||||
|
const start = new Date() |
||||
|
const cards = await creditCards.ls() |
||||
|
|
||||
|
if (cards.cards.length === 0) { |
||||
|
error('You have no credit cards to choose from') |
||||
|
return exit(0) |
||||
|
} |
||||
|
|
||||
|
let cardId = args[0] |
||||
|
|
||||
|
if (cardId === undefined) { |
||||
|
const elapsed = ms(new Date() - start) |
||||
|
const message = `Selecting a new default payment card from ${cards.cards.length} total ${chalk.gray(`[${elapsed}]`)}` |
||||
|
const choices = buildInquirerChoices(cards) |
||||
|
|
||||
|
cardId = await listInput({ |
||||
|
message, |
||||
|
choices, |
||||
|
separator: true, |
||||
|
abort: 'end' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TODO: check if the provided cardId (in case the user
|
||||
|
// typed `now billing set-default <some-id>`) is valid
|
||||
|
if (cardId) { |
||||
|
const label = `Are you sure that you to set this card as the default?` |
||||
|
const confirmation = await promptBool(label) |
||||
|
console.log('') // new line
|
||||
|
if (!confirmation) { |
||||
|
console.log('Aborted') |
||||
|
break |
||||
|
} |
||||
|
const start = new Date() |
||||
|
await creditCards.setDefault(cardId) |
||||
|
|
||||
|
const card = cards.cards.find(card => card.id === cardId) |
||||
|
const elapsed = ms(new Date() - start) |
||||
|
success(`${card.brand} ending in ${card.last4} is now the default ${chalk.gray(`[${elapsed}]`)}`) |
||||
|
} else { |
||||
|
console.log('No changes made') |
||||
|
} |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
case 'rm': |
||||
|
case 'remove': { |
||||
|
if (args.length > 1) { |
||||
|
error('Invalid number of arguments') |
||||
|
return exit(1) |
||||
|
} |
||||
|
|
||||
|
const start = new Date() |
||||
|
const cards = await creditCards.ls() |
||||
|
|
||||
|
if (cards.cards.length === 0) { |
||||
|
error('You have no credit cards to choose from to delete') |
||||
|
return exit(0) |
||||
|
} |
||||
|
|
||||
|
let cardId = args[0] |
||||
|
|
||||
|
if (cardId === undefined) { |
||||
|
const elapsed = ms(new Date() - start) |
||||
|
const message = `Selecting a card to ${chalk.underline('remove')} from ${cards.cards.length} total ${chalk.gray(`[${elapsed}]`)}` |
||||
|
const choices = buildInquirerChoices(cards) |
||||
|
|
||||
|
cardId = await listInput({ |
||||
|
message, |
||||
|
choices, |
||||
|
separator: true, |
||||
|
abort: 'start' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TODO: check if the provided cardId (in case the user
|
||||
|
// typed `now billing rm <some-id>`) is valid
|
||||
|
if (cardId) { |
||||
|
const label = `Are you sure that you want to remove this card?` |
||||
|
const confirmation = await promptBool(label) |
||||
|
console.log('') // new line
|
||||
|
if (!confirmation) { |
||||
|
console.log('Aborted') |
||||
|
break |
||||
|
} |
||||
|
const start = new Date() |
||||
|
await creditCards.rm(cardId) |
||||
|
|
||||
|
const deletedCard = cards.cards.find(card => card.id === cardId) |
||||
|
const remainingCards = cards.cards.filter(card => card.id !== cardId) |
||||
|
|
||||
|
let text = `${deletedCard.brand} ending in ${deletedCard.last4} was deleted` |
||||
|
// ${chalk.gray(`[${elapsed}]`)}
|
||||
|
|
||||
|
if (cardId === cards.defaultCardId) { |
||||
|
if (remainingCards.length === 0) { |
||||
|
// The user deleted the last card in their account
|
||||
|
text += `\n${chalk.yellow('Warning!')} You have no default card` |
||||
|
} else { |
||||
|
// We can't guess the current default card – let's ask the API
|
||||
|
const cards = await creditCards.ls() |
||||
|
const newDefaultCard = cards.cards.find(card => card.id === cards.defaultCardId) |
||||
|
|
||||
|
text += `\n${newDefaultCard.brand} ending in ${newDefaultCard.last4} in now default` |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const elapsed = ms(new Date() - start) |
||||
|
text += ` ${chalk.gray(`[${elapsed}]`)}` |
||||
|
success(text) |
||||
|
} else { |
||||
|
console.log('No changes made') |
||||
|
} |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
case 'add': { |
||||
|
require(resolve(__dirname, 'now-billing-add.js'))(creditCards) |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
default: |
||||
|
error('Please specify a valid subcommand: ls | add | rm | set-default') |
||||
|
help() |
||||
|
exit(1) |
||||
|
} |
||||
|
|
||||
|
creditCards.close() |
||||
|
} |
@ -0,0 +1,207 @@ |
|||||
|
#!/usr/bin/env node
|
||||
|
|
||||
|
// Packages
|
||||
|
const chalk = require('chalk') |
||||
|
const minimist = require('minimist') |
||||
|
const ms = require('ms') |
||||
|
const stripAnsi = require('strip-ansi') |
||||
|
|
||||
|
// Ours
|
||||
|
const login = require('../lib/login') |
||||
|
const cfg = require('../lib/cfg') |
||||
|
const NowPlans = require('../lib/plans') |
||||
|
const indent = require('../lib/indent') |
||||
|
const listInput = require('../lib/utils/input/list') |
||||
|
const code = require('../lib/utils/output/code') |
||||
|
const error = require('../lib/utils/output/error') |
||||
|
const success = require('../lib/utils/output/success') |
||||
|
const cmd = require('../lib/utils/output/cmd') |
||||
|
|
||||
|
const argv = minimist(process.argv.slice(2), { |
||||
|
string: ['config', 'token'], |
||||
|
boolean: ['help', 'debug'], |
||||
|
alias: { |
||||
|
help: 'h', |
||||
|
config: 'c', |
||||
|
debug: 'd', |
||||
|
token: 't' |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
const help = () => { |
||||
|
console.log(` |
||||
|
${chalk.bold('𝚫 now upgrade')} [plan] |
||||
|
|
||||
|
${chalk.dim('Options:')} |
||||
|
|
||||
|
-h, --help Output usage information |
||||
|
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file |
||||
|
-d, --debug Debug mode [off] |
||||
|
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token |
||||
|
|
||||
|
${chalk.dim('Examples:')} |
||||
|
|
||||
|
${chalk.gray('–')} List available plans and pick one interactively |
||||
|
|
||||
|
${chalk.cyan('$ now upgrade')} |
||||
|
|
||||
|
${chalk.yellow('NOTE:')} ${chalk.gray('Make sure you have a payment method, or add one:')} |
||||
|
|
||||
|
${chalk.cyan(`$ now billing add`)} |
||||
|
|
||||
|
${chalk.gray('–')} Pick a specific plan (premium): |
||||
|
|
||||
|
${chalk.cyan(`$ now upgrade premium`)} |
||||
|
`)
|
||||
|
} |
||||
|
|
||||
|
// options
|
||||
|
const debug = argv.debug |
||||
|
const apiUrl = argv.url || 'https://api.zeit.co' |
||||
|
|
||||
|
if (argv.config) { |
||||
|
cfg.setConfigFile(argv.config) |
||||
|
} |
||||
|
|
||||
|
const exit = code => { |
||||
|
// we give stdout some time to flush out
|
||||
|
// because there's a node bug where
|
||||
|
// stdout writes are asynchronous
|
||||
|
// https://github.com/nodejs/node/issues/6456
|
||||
|
setTimeout(() => process.exit(code || 0), 100) |
||||
|
} |
||||
|
|
||||
|
if (argv.help) { |
||||
|
help() |
||||
|
exit(0) |
||||
|
} else { |
||||
|
const config = cfg.read() |
||||
|
|
||||
|
Promise.resolve(argv.token || config.token || login(apiUrl)) |
||||
|
.then(async token => { |
||||
|
try { |
||||
|
await run(token) |
||||
|
} catch (err) { |
||||
|
if (err.userError) { |
||||
|
error(err.message) |
||||
|
} else { |
||||
|
error(`Unknown error: ${err.stack}`) |
||||
|
} |
||||
|
exit(1) |
||||
|
} |
||||
|
}) |
||||
|
.catch(e => { |
||||
|
error(`Authentication error – ${e.message}`) |
||||
|
exit(1) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function buildInquirerChoices(current, until) { |
||||
|
if (until) { |
||||
|
until = until.split(' ') |
||||
|
until = ' for ' + chalk.bold(until[0]) + ' more ' + until[1] |
||||
|
} else { |
||||
|
until = '' |
||||
|
} |
||||
|
const ossTitle = current === 'oss' ? |
||||
|
`oss FREE ${' '.repeat(28)} (current)` : |
||||
|
'oss FREE' |
||||
|
const premiumTitle = current === 'premium' ? |
||||
|
`premium $15/mo ${' '.repeat(24 - stripAnsi(until).length)} (current${until})` : |
||||
|
'premium $15/mo' |
||||
|
return [ |
||||
|
{ |
||||
|
name: [ |
||||
|
ossTitle, |
||||
|
indent('✓ All code is public and open-source', 2), |
||||
|
indent('✓ 20 deploys per month | 1GB monthly bandwidth', 2), |
||||
|
indent('✓ 1GB FREE storage | 1MB size limit per file', 2) |
||||
|
].join('\n'), |
||||
|
value: 'oss', |
||||
|
short: 'oss FREE' |
||||
|
}, |
||||
|
{ |
||||
|
name: [ |
||||
|
premiumTitle, |
||||
|
indent('✓ All code is private and secure', 2), |
||||
|
indent('✓ 1000 deploys per month | 50GB monthly bandwidth', 2), |
||||
|
indent('✓ 100GB storage | No filesize limit', 2) |
||||
|
].join('\n'), |
||||
|
value: 'premium', |
||||
|
short: 'premium $15/mo' |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
async function run(token) { |
||||
|
const args = argv._ |
||||
|
if (args.length > 1) { |
||||
|
error('Invalid number of arguments') |
||||
|
return exit(1) |
||||
|
} |
||||
|
|
||||
|
const start = new Date() |
||||
|
const plans = new NowPlans(apiUrl, token, {debug}) |
||||
|
|
||||
|
let planId = args[0] |
||||
|
|
||||
|
if (![undefined, 'oss', 'premium'].includes(planId)) { |
||||
|
error(`Invalid plan name – should be ${code('oss')} or ${code('premium')}`) |
||||
|
return exit(1) |
||||
|
} |
||||
|
|
||||
|
const currentPlan = await plans.getCurrent() |
||||
|
|
||||
|
if (planId === undefined) { |
||||
|
const elapsed = ms(new Date() - start) |
||||
|
|
||||
|
const message = `Selecting a plan for your account ${chalk.gray(`[${elapsed}]`)}` |
||||
|
const choices = buildInquirerChoices(currentPlan.id, currentPlan.until) |
||||
|
|
||||
|
planId = await listInput({ |
||||
|
message, |
||||
|
choices, |
||||
|
separator: true, |
||||
|
abort: 'end' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
if (planId === undefined || (planId === currentPlan.id && currentPlan.until === undefined)) { |
||||
|
return console.log('No changes made') |
||||
|
} |
||||
|
|
||||
|
let newPlan |
||||
|
|
||||
|
try { |
||||
|
newPlan = await plans.set(planId) |
||||
|
} catch (err) { |
||||
|
let errorBody |
||||
|
if (err.res && err.res.status === 400) { |
||||
|
errorBody = err.res.json() |
||||
|
} else { |
||||
|
const message = 'A network error has occurred. Please retry.' |
||||
|
errorBody = {message} |
||||
|
} |
||||
|
|
||||
|
const _err = (await errorBody).error |
||||
|
const {code, message} = _err |
||||
|
|
||||
|
if (code === 'customer_not_found' || code === 'source_not_found') { |
||||
|
error(`You have no payment methods available. Run ${cmd('now billing add')} to add one`) |
||||
|
} else { |
||||
|
error(`An unknow error occured. Please try again later ${message}`) |
||||
|
} |
||||
|
plans.close() |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (currentPlan.until && newPlan.id === 'premium') { |
||||
|
success(`The cancelation has been undone. You're back on the ${chalk.bold('Premium plan')}`) |
||||
|
} else if (newPlan.until) { |
||||
|
success(`Your plan will be switched to OSS in ${chalk.bold(newPlan.until)}. Your card will not be charged again`) |
||||
|
} else { |
||||
|
success(`You're now on the ${chalk.bold('Premium plan')}`) |
||||
|
} |
||||
|
|
||||
|
plans.close() |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
#!/usr/bin/env bash |
||||
|
rm -rf build |
||||
|
mkdir -p build/{lib,bin} |
||||
|
find lib/** -type d -exec mkdir -p build/{} \; |
||||
|
find bin/** -type d -exec mkdir -p build/{} \; |
||||
|
find lib/** -type f -exec node_modules/.bin/async-to-gen --out-file build/{} {} \; |
||||
|
find bin/** -type f -exec node_modules/.bin/async-to-gen --out-file build/{} {} \; |
||||
|
chmod +x build/bin/now.js |
||||
|
cp lib/utils/billing/*.json build/lib/utils/billing/ |
||||
|
cp package.json build/ |
@ -0,0 +1,70 @@ |
|||||
|
const stripe = require('stripe')('pk_live_alyEi3lN0kSwbdevK0nrGwTw') |
||||
|
|
||||
|
const Now = require('../lib') |
||||
|
|
||||
|
module.exports = class CreditCards extends Now { |
||||
|
|
||||
|
async ls() { |
||||
|
const res = await this._fetch('/www/user/cards') |
||||
|
const body = await res.json() |
||||
|
|
||||
|
return body |
||||
|
} |
||||
|
|
||||
|
async setDefault(cardId) { |
||||
|
await this._fetch('/www/user/cards/default', { |
||||
|
method: 'PUT', |
||||
|
body: {cardId} |
||||
|
}) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
async rm(cardId) { |
||||
|
await this._fetch(`/www/user/cards/${encodeURIComponent(cardId)}`, {method: 'DELEtE'}) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/* eslint-disable camelcase */ |
||||
|
add(card) { |
||||
|
return new Promise(async (resolve, reject) => { |
||||
|
const expDateParts = card.expDate.split(' / ') |
||||
|
card = { |
||||
|
name: card.name, |
||||
|
number: card.cardNumber, |
||||
|
cvc: card.ccv, |
||||
|
address_country: card.country, |
||||
|
address_zip: card.zipCode, |
||||
|
address_state: card.state, |
||||
|
address_city: card.city, |
||||
|
address_line1: card.address1 |
||||
|
} |
||||
|
|
||||
|
card.exp_month = expDateParts[0] |
||||
|
card.exp_year = expDateParts[1] |
||||
|
|
||||
|
try { |
||||
|
const stripeToken = (await stripe.tokens.create({card})).id |
||||
|
const res = await this._fetch('/www/user/cards', { |
||||
|
method: 'POST', |
||||
|
body: {stripeToken} |
||||
|
}) |
||||
|
|
||||
|
const body = await res.json() |
||||
|
|
||||
|
if (body.card && body.card.id) { |
||||
|
resolve({ |
||||
|
last4: body.card.last4 |
||||
|
}) |
||||
|
} else if (body.error && body.error.message) { |
||||
|
reject({message: body.error.message}) |
||||
|
} else { |
||||
|
reject('Unknown error') |
||||
|
} |
||||
|
} catch (err) { |
||||
|
reject({ |
||||
|
message: err.message || 'Unknown error' |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
@ -0,0 +1,48 @@ |
|||||
|
const ms = require('ms') |
||||
|
|
||||
|
const Now = require('../lib') |
||||
|
|
||||
|
async function parsePlan(res) { |
||||
|
let id |
||||
|
let until |
||||
|
|
||||
|
const {subscription} = await res.json() |
||||
|
|
||||
|
if (subscription) { |
||||
|
id = subscription.plan.id |
||||
|
|
||||
|
if (subscription.cancel_at_period_end) { |
||||
|
until = ms( |
||||
|
new Date(subscription.current_period_end * 1000) - new Date(), |
||||
|
{long: true} |
||||
|
) |
||||
|
} |
||||
|
} else { |
||||
|
id = 'oss' |
||||
|
} |
||||
|
|
||||
|
return {id, until} |
||||
|
} |
||||
|
|
||||
|
module.exports = class Plans extends Now { |
||||
|
|
||||
|
async getCurrent() { |
||||
|
const res = await this._fetch('/www/user/plan') |
||||
|
|
||||
|
return await parsePlan(res) |
||||
|
} |
||||
|
|
||||
|
async set(plan) { |
||||
|
const res = await this._fetch('/www/user/plan', { |
||||
|
method: 'PUT', |
||||
|
body: {plan} |
||||
|
}) |
||||
|
|
||||
|
if (res.ok) { |
||||
|
return await parsePlan(res) |
||||
|
} |
||||
|
const err = new Error(res.statusText) |
||||
|
err.res = res |
||||
|
throw err |
||||
|
} |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
{ |
||||
|
"VISA": "Visa", |
||||
|
"MASTERCARD": "MasterCard", |
||||
|
"AMERICANEXPRESS": "American Express", |
||||
|
"DINERSCLUB": "Diners Club", |
||||
|
"DISCOVER": "Discover", |
||||
|
"JCB": "JCB" |
||||
|
} |
@ -0,0 +1,251 @@ |
|||||
|
{ |
||||
|
"United States": "US", |
||||
|
"Afghanistan": "AF", |
||||
|
"Åland Islands": "AX", |
||||
|
"Albania": "AL", |
||||
|
"Algeria": "DZ", |
||||
|
"American Samoa": "AS", |
||||
|
"Andorra": "AD", |
||||
|
"Angola": "AO", |
||||
|
"Anguilla": "AI", |
||||
|
"Antarctica": "AQ", |
||||
|
"Antigua and Barbuda": "AG", |
||||
|
"Argentina": "AR", |
||||
|
"Armenia": "AM", |
||||
|
"Aruba": "AW", |
||||
|
"Australia": "AU", |
||||
|
"Austria": "AT", |
||||
|
"Azerbaijan": "AZ", |
||||
|
"Bahamas": "BS", |
||||
|
"Bahrain": "BH", |
||||
|
"Bangladesh": "BD", |
||||
|
"Barbados": "BB", |
||||
|
"Belarus": "BY", |
||||
|
"Belgium": "BE", |
||||
|
"Belize": "BZ", |
||||
|
"Benin": "BJ", |
||||
|
"Bermuda": "BM", |
||||
|
"Bhutan": "BT", |
||||
|
"Bolivia, Plurinational State of": "BO", |
||||
|
"Bonaire, Sint Eustatius and Saba": "BQ", |
||||
|
"Bosnia and Herzegovina": "BA", |
||||
|
"Botswana": "BW", |
||||
|
"Bouvet Island": "BV", |
||||
|
"Brazil": "BR", |
||||
|
"British Indian Ocean Territory": "IO", |
||||
|
"Brunei Darussalam": "BN", |
||||
|
"Bulgaria": "BG", |
||||
|
"Burkina Faso": "BF", |
||||
|
"Burundi": "BI", |
||||
|
"Cambodia": "KH", |
||||
|
"Cameroon": "CM", |
||||
|
"Canada": "CA", |
||||
|
"Cape Verde": "CV", |
||||
|
"Cayman Islands": "KY", |
||||
|
"Central African Republic": "CF", |
||||
|
"Chad": "TD", |
||||
|
"Chile": "CL", |
||||
|
"China": "CN", |
||||
|
"Christmas Island": "CX", |
||||
|
"Cocos (Keeling) Islands": "CC", |
||||
|
"Colombia": "CO", |
||||
|
"Comoros": "KM", |
||||
|
"Congo": "CG", |
||||
|
"CD": "Congo, the Democratic Republic of the", |
||||
|
"Cook Islands": "CK", |
||||
|
"Costa Rica": "CR", |
||||
|
"Côte d'Ivoire": "CI", |
||||
|
"Croatia": "HR", |
||||
|
"Cuba": "CU", |
||||
|
"Curaçao": "CW", |
||||
|
"Cyprus": "CY", |
||||
|
"Czech Republic": "CZ", |
||||
|
"Denmark": "DK", |
||||
|
"Djibouti": "DJ", |
||||
|
"Dominica": "DM", |
||||
|
"Dominican Republic": "DO", |
||||
|
"Ecuador": "EC", |
||||
|
"Egypt": "EG", |
||||
|
"El Salvador": "SV", |
||||
|
"Equatorial Guinea": "GQ", |
||||
|
"Eritrea": "ER", |
||||
|
"Estonia": "EE", |
||||
|
"Ethiopia": "ET", |
||||
|
"Falkland Islands (Malvinas)": "FK", |
||||
|
"Faroe Islands": "FO", |
||||
|
"Fiji": "FJ", |
||||
|
"Finland": "FI", |
||||
|
"France": "FR", |
||||
|
"French Guiana": "GF", |
||||
|
"French Polynesia": "PF", |
||||
|
"French Southern Territories": "TF", |
||||
|
"Gabon": "GA", |
||||
|
"Gambia": "GM", |
||||
|
"Georgia": "GE", |
||||
|
"Germany": "DE", |
||||
|
"Ghana": "GH", |
||||
|
"Gibraltar": "GI", |
||||
|
"Greece": "GR", |
||||
|
"Greenland": "GL", |
||||
|
"Grenada": "GD", |
||||
|
"Guadeloupe": "GP", |
||||
|
"Guam": "GU", |
||||
|
"Guatemala": "GT", |
||||
|
"Guernsey": "GG", |
||||
|
"Guinea": "GN", |
||||
|
"Guinea-Bissau": "GW", |
||||
|
"Guyana": "GY", |
||||
|
"Haiti": "HT", |
||||
|
"Heard Island and McDonald Islands": "HM", |
||||
|
"Holy See (Vatican City State)": "VA", |
||||
|
"Honduras": "HN", |
||||
|
"Hong Kong": "HK", |
||||
|
"Hungary": "HU", |
||||
|
"Iceland": "IS", |
||||
|
"India": "IN", |
||||
|
"Indonesia": "ID", |
||||
|
"Iran, Islamic Republic of": "IR", |
||||
|
"Iraq": "IQ", |
||||
|
"Ireland": "IE", |
||||
|
"Isle of Man": "IM", |
||||
|
"Israel": "IL", |
||||
|
"Italy": "IT", |
||||
|
"Jamaica": "JM", |
||||
|
"Japan": "JP", |
||||
|
"Jersey": "JE", |
||||
|
"Jordan": "JO", |
||||
|
"Kazakhstan": "KZ", |
||||
|
"Kenya": "KE", |
||||
|
"Kiribati": "KI", |
||||
|
"KP": "Korea, Democratic People's Republic of", |
||||
|
"Korea, Republic of": "KR", |
||||
|
"Kuwait": "KW", |
||||
|
"Kyrgyzstan": "KG", |
||||
|
"Lao People's Democratic Republic": "LA", |
||||
|
"Latvia": "LV", |
||||
|
"Lebanon": "LB", |
||||
|
"Lesotho": "LS", |
||||
|
"Liberia": "LR", |
||||
|
"Libya": "LY", |
||||
|
"Liechtenstein": "LI", |
||||
|
"Lithuania": "LT", |
||||
|
"Luxembourg": "LU", |
||||
|
"Macao": "MO", |
||||
|
"MK": "Macedonia, the former Yugoslav Republic of", |
||||
|
"Madagascar": "MG", |
||||
|
"Malawi": "MW", |
||||
|
"Malaysia": "MY", |
||||
|
"Maldives": "MV", |
||||
|
"Mali": "ML", |
||||
|
"Malta": "MT", |
||||
|
"Marshall Islands": "MH", |
||||
|
"Martinique": "MQ", |
||||
|
"Mauritania": "MR", |
||||
|
"Mauritius": "MU", |
||||
|
"Mayotte": "YT", |
||||
|
"Mexico": "MX", |
||||
|
"Micronesia, Federated States of": "FM", |
||||
|
"Moldova, Republic of": "MD", |
||||
|
"Monaco": "MC", |
||||
|
"Mongolia": "MN", |
||||
|
"Montenegro": "ME", |
||||
|
"Montserrat": "MS", |
||||
|
"Morocco": "MA", |
||||
|
"Mozambique": "MZ", |
||||
|
"Myanmar": "MM", |
||||
|
"Namibia": "NA", |
||||
|
"Nauru": "NR", |
||||
|
"Nepal": "NP", |
||||
|
"Netherlands": "NL", |
||||
|
"New Caledonia": "NC", |
||||
|
"New Zealand": "NZ", |
||||
|
"Nicaragua": "NI", |
||||
|
"Niger": "NE", |
||||
|
"Nigeria": "NG", |
||||
|
"Niue": "NU", |
||||
|
"Norfolk Island": "NF", |
||||
|
"Northern Mariana Islands": "MP", |
||||
|
"Norway": "NO", |
||||
|
"Oman": "OM", |
||||
|
"Pakistan": "PK", |
||||
|
"Palau": "PW", |
||||
|
"Palestinian Territory, Occupied": "PS", |
||||
|
"Panama": "PA", |
||||
|
"Papua New Guinea": "PG", |
||||
|
"Paraguay": "PY", |
||||
|
"Peru": "PE", |
||||
|
"Philippines": "PH", |
||||
|
"Pitcairn": "PN", |
||||
|
"Poland": "PL", |
||||
|
"Portugal": "PT", |
||||
|
"Puerto Rico": "PR", |
||||
|
"Qatar": "QA", |
||||
|
"Réunion": "RE", |
||||
|
"Romania": "RO", |
||||
|
"Russian Federation": "RU", |
||||
|
"Rwanda": "RW", |
||||
|
"Saint Barthélemy": "BL", |
||||
|
"SH": "Saint Helena, Ascension and Tristan da Cunha", |
||||
|
"Saint Kitts and Nevis": "KN", |
||||
|
"Saint Lucia": "LC", |
||||
|
"Saint Martin (French part)": "MF", |
||||
|
"Saint Pierre and Miquelon": "PM", |
||||
|
"Saint Vincent and the Grenadines": "VC", |
||||
|
"Samoa": "WS", |
||||
|
"San Marino": "SM", |
||||
|
"Sao Tome and Principe": "ST", |
||||
|
"Saudi Arabia": "SA", |
||||
|
"Senegal": "SN", |
||||
|
"Serbia": "RS", |
||||
|
"Seychelles": "SC", |
||||
|
"Sierra Leone": "SL", |
||||
|
"Singapore": "SG", |
||||
|
"Sint Maarten (Dutch part)": "SX", |
||||
|
"Slovakia": "SK", |
||||
|
"Slovenia": "SI", |
||||
|
"Solomon Islands": "SB", |
||||
|
"Somalia": "SO", |
||||
|
"South Africa": "ZA", |
||||
|
"GS": "South Georgia and the South Sandwich Islands", |
||||
|
"South Sudan": "SS", |
||||
|
"Spain": "ES", |
||||
|
"Sri Lanka": "LK", |
||||
|
"Sudan": "SD", |
||||
|
"Suriname": "SR", |
||||
|
"Svalbard and Jan Mayen": "SJ", |
||||
|
"Swaziland": "SZ", |
||||
|
"Sweden": "SE", |
||||
|
"Switzerland": "CH", |
||||
|
"Syrian Arab Republic": "SY", |
||||
|
"Taiwan, Province of China": "TW", |
||||
|
"Tajikistan": "TJ", |
||||
|
"Tanzania, United Republic of": "TZ", |
||||
|
"Thailand": "TH", |
||||
|
"Timor-Leste": "TL", |
||||
|
"Togo": "TG", |
||||
|
"Tokelau": "TK", |
||||
|
"Tonga": "TO", |
||||
|
"Trinidad and Tobago": "TT", |
||||
|
"Tunisia": "TN", |
||||
|
"Turkey": "TR", |
||||
|
"Turkmenistan": "TM", |
||||
|
"Turks and Caicos Islands": "TC", |
||||
|
"Tuvalu": "TV", |
||||
|
"Uganda": "UG", |
||||
|
"Ukraine": "UA", |
||||
|
"United Arab Emirates": "AE", |
||||
|
"United Kingdom": "GB", |
||||
|
"UM": "United States Minor Outlying Islands", |
||||
|
"Uruguay": "UY", |
||||
|
"Uzbekistan": "UZ", |
||||
|
"Vanuatu": "VU", |
||||
|
"Venezuela, Bolivarian Republic of": "VE", |
||||
|
"Viet Nam": "VN", |
||||
|
"Virgin Islands, British": "VG", |
||||
|
"Virgin Islands, U.S.": "VI", |
||||
|
"Wallis and Futuna": "WF", |
||||
|
"Western Sahara": "EH", |
||||
|
"Yemen": "YE", |
||||
|
"Zambia": "ZM", |
||||
|
"Zimbabwe": "ZW" |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
// Packages
|
||||
|
const gMaps = require('@google/maps') |
||||
|
|
||||
|
const MAPS_API_KEY = 'AIzaSyALfKTQ6AiIoJ8WGDXR3E7IBOwlHoTPfYY' |
||||
|
|
||||
|
// eslint-disable-next-line camelcase
|
||||
|
module.exports = function ({country, zipCode: postal_code}) { |
||||
|
return new Promise(resolve => { |
||||
|
const maps = gMaps.createClient({key: MAPS_API_KEY}) |
||||
|
maps.geocode({ |
||||
|
address: `${postal_code} ${country}` // eslint-disable-line camelcase
|
||||
|
}, (err, res) => { |
||||
|
if (err || res.json.results.length === 0) { |
||||
|
resolve() |
||||
|
} |
||||
|
|
||||
|
const data = res.json.results[0] |
||||
|
const components = {} |
||||
|
data.address_components.forEach(c => { |
||||
|
components[c.types[0]] = c |
||||
|
}) |
||||
|
const state = components.administrative_area_level_1 |
||||
|
const city = components.locality |
||||
|
resolve({state: state && state.long_name, city: city && city.long_name}) |
||||
|
}) |
||||
|
}) |
||||
|
} |
@ -0,0 +1,86 @@ |
|||||
|
const chalk = require('chalk') |
||||
|
const inquirer = require('inquirer') |
||||
|
const stripAnsi = require('strip-ansi') |
||||
|
|
||||
|
/* eslint-disable no-multiple-empty-lines, no-var, no-undef, no-eq-null, eqeqeq, semi */ |
||||
|
inquirer.prompt.prompts.list.prototype.getQuestion = function () { |
||||
|
var message = chalk.bold('> ' + this.opt.message) + ' ' |
||||
|
|
||||
|
// Append the default if available, and if question isn't answered
|
||||
|
if (this.opt.default != null && this.status !== 'answered') { |
||||
|
message += chalk.dim('(' + this.opt.default + ') ') |
||||
|
} |
||||
|
|
||||
|
return message |
||||
|
}; |
||||
|
/* eslint-enable */ |
||||
|
|
||||
|
function getLength(string) { |
||||
|
let biggestLength = 0 |
||||
|
string.split('\n').map(str => { |
||||
|
str = stripAnsi(str) |
||||
|
if (str.length > biggestLength) { |
||||
|
biggestLength = str.length |
||||
|
} |
||||
|
return undefined |
||||
|
}) |
||||
|
return biggestLength |
||||
|
} |
||||
|
|
||||
|
module.exports = async function ({ |
||||
|
message = 'the question', |
||||
|
choices = [{ // eslint-disable-line no-unused-vars
|
||||
|
name: 'something\ndescription\ndetails\netc', |
||||
|
value: 'something unique', |
||||
|
short: 'generally the first line of `name`' |
||||
|
}], |
||||
|
pageSize = 15, // Show 15 lines without scrolling (~4 credit cards)
|
||||
|
separator = true, // puts a blank separator between each choice
|
||||
|
abort = 'end' // wether the `abort` option will be at the `start` or the `end`
|
||||
|
}) { |
||||
|
let biggestLength = 0 |
||||
|
|
||||
|
choices = choices.map(choice => { |
||||
|
if (choice.name) { |
||||
|
const length = getLength(choice.name) |
||||
|
if (length > biggestLength) { |
||||
|
biggestLength = length |
||||
|
} |
||||
|
return choice |
||||
|
} |
||||
|
throw new Error('Invalid choice') |
||||
|
}) |
||||
|
|
||||
|
if (separator === true) { |
||||
|
choices = choices.reduce((prev, curr) => ( |
||||
|
prev.concat(new inquirer.Separator(' '), curr) |
||||
|
), []) |
||||
|
} |
||||
|
|
||||
|
const abortSeparator = new inquirer.Separator('─'.repeat(biggestLength)) |
||||
|
const _abort = { |
||||
|
name: 'Abort', |
||||
|
value: undefined |
||||
|
} |
||||
|
|
||||
|
if (abort === 'start') { |
||||
|
const blankSep = choices.shift() |
||||
|
choices.unshift(abortSeparator) |
||||
|
choices.unshift(_abort) |
||||
|
choices.unshift(blankSep) |
||||
|
} else { |
||||
|
choices.push(abortSeparator) |
||||
|
choices.push(_abort) |
||||
|
} |
||||
|
|
||||
|
const nonce = Date.now() |
||||
|
const answer = await inquirer.prompt({ |
||||
|
name: nonce, |
||||
|
type: 'list', |
||||
|
message, |
||||
|
choices, |
||||
|
pageSize |
||||
|
}) |
||||
|
|
||||
|
return answer[nonce] |
||||
|
} |
@ -0,0 +1,207 @@ |
|||||
|
const ansiEscapes = require('ansi-escapes') |
||||
|
const ansiRegex = require('ansi-regex') |
||||
|
const chalk = require('chalk') |
||||
|
const stripAnsi = require('strip-ansi') |
||||
|
|
||||
|
const ESCAPES = { |
||||
|
LEFT: '\x1b[D', |
||||
|
RIGHT: '\x1b[C', |
||||
|
CTRL_C: '\x03', |
||||
|
BACKSPACE: '\x08', |
||||
|
CTRL_H: '\x7f', |
||||
|
CARRIAGE: '\r' |
||||
|
} |
||||
|
|
||||
|
module.exports = function ({ |
||||
|
label = '', |
||||
|
initialValue = '', |
||||
|
// can be:
|
||||
|
// - `false`, which does nothing
|
||||
|
// - `cc`, for credit cards
|
||||
|
// - `date`, for dates in the mm / yyyy format
|
||||
|
mask = false, |
||||
|
placeholder = '', |
||||
|
abortSequences = new Set(['\x03']), |
||||
|
eraseSequences = new Set([ESCAPES.BACKSPACE, ESCAPES.CTRL_H]), |
||||
|
resolveChars = new Set([ESCAPES.CARRIAGE]), |
||||
|
stdin = process.stdin, |
||||
|
stdout = process.stdout, |
||||
|
// char to print before resolving/rejecting the promise
|
||||
|
// if `false`, nothing will be printed
|
||||
|
trailing = ansiEscapes.eraseLines(1), |
||||
|
// gets called on each keypress;
|
||||
|
// `data` contains the current keypress;
|
||||
|
// `futureValue` contains the current value + the
|
||||
|
// keypress in the correct place
|
||||
|
validateKeypress = (data, futureValue) => true, // eslint-disable-line no-unused-vars
|
||||
|
// get's called before the promise is resolved
|
||||
|
// returning `false` here will prevent the user from submiting the value
|
||||
|
validateValue = data => true, // eslint-disable-line no-unused-vars
|
||||
|
// receives the value of the input and should return a string
|
||||
|
// or false if no autocomplion is available
|
||||
|
autoComplete = value => false, // eslint-disable-line no-unused-vars
|
||||
|
autoCompleteChars = new Set([ |
||||
|
'\t', // tab
|
||||
|
'\x1b[C' // right arrow
|
||||
|
]) |
||||
|
} = {}) { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const isRaw = process.stdin.isRaw |
||||
|
|
||||
|
let value |
||||
|
let caretOffset = 0 |
||||
|
let regex |
||||
|
let suggestion = '' |
||||
|
|
||||
|
stdout.write(label) |
||||
|
value = initialValue |
||||
|
stdout.write(initialValue) |
||||
|
if (mask) { |
||||
|
if (!value) { |
||||
|
value = chalk.gray(placeholder) |
||||
|
caretOffset = 0 - stripAnsi(value).length |
||||
|
stdout.write(value) |
||||
|
stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset))) |
||||
|
} |
||||
|
|
||||
|
regex = placeholder.split('').reduce((prev, curr) => { |
||||
|
if (curr !== ' ' && !prev.includes(curr)) { |
||||
|
if (curr === '/') { |
||||
|
prev.push(' / ') |
||||
|
} else { |
||||
|
prev.push(curr) |
||||
|
} |
||||
|
} |
||||
|
return prev |
||||
|
}, []).join('|') |
||||
|
regex = new RegExp(`(${regex})`, 'g') |
||||
|
} |
||||
|
stdin.setRawMode(true) |
||||
|
stdin.resume() |
||||
|
|
||||
|
function restore() { |
||||
|
stdin.setRawMode(isRaw) |
||||
|
stdin.pause() |
||||
|
stdin.removeListener('data', onData) |
||||
|
if (trailing) { |
||||
|
stdout.write(trailing) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function onData(buffer) { |
||||
|
const data = buffer.toString() |
||||
|
value = stripAnsi(value) |
||||
|
|
||||
|
if (abortSequences.has(data)) { |
||||
|
restore() |
||||
|
return reject(new Error('USER_ABORT')) |
||||
|
} |
||||
|
|
||||
|
if (suggestion !== '' && !caretOffset && autoCompleteChars.has(data)) { |
||||
|
value += stripAnsi(suggestion) |
||||
|
suggestion = '' |
||||
|
} else if (data === ESCAPES.LEFT) { |
||||
|
if (value.length > Math.abs(caretOffset)) { |
||||
|
caretOffset-- |
||||
|
} |
||||
|
} else if (data === ESCAPES.RIGHT) { |
||||
|
if (caretOffset < 0) { |
||||
|
caretOffset++ |
||||
|
} |
||||
|
} else if (eraseSequences.has(data)) { |
||||
|
let char |
||||
|
if ((mask) && value.length > Math.abs(caretOffset)) { |
||||
|
if (value[value.length + caretOffset - 1] === ' ') { |
||||
|
if (value[value.length + caretOffset - 2] === '/') { |
||||
|
caretOffset -= 1 |
||||
|
} |
||||
|
char = placeholder[value.length + caretOffset] |
||||
|
value = value.substr(0, value.length + caretOffset - 2) + char + |
||||
|
value.substr(value.length + caretOffset - 1) |
||||
|
caretOffset-- |
||||
|
} else { |
||||
|
char = placeholder[value.length + caretOffset - 1] |
||||
|
value = value.substr(0, value.length + caretOffset - 1) + char + |
||||
|
value.substr(value.length + caretOffset) |
||||
|
} |
||||
|
caretOffset-- |
||||
|
} else { |
||||
|
value = value.substr(0, value.length + caretOffset - 1) + |
||||
|
value.substr(value.length + caretOffset) |
||||
|
} |
||||
|
suggestion = '' |
||||
|
} else if (resolveChars.has(data)) { |
||||
|
if (validateValue(value)) { |
||||
|
restore() |
||||
|
resolve(value) |
||||
|
} else { |
||||
|
if (mask === 'cc' || mask === 'ccv') { |
||||
|
value = value.replace(/\s/g, '').replace(/(.{4})/g, '$1 ').trim() |
||||
|
value = value.replace(regex, chalk.gray('$1')) |
||||
|
} else if (mask === 'expDate') { |
||||
|
value = value.replace(regex, chalk.gray('$1')) |
||||
|
} |
||||
|
const l = chalk.red(label.replace('-', '✖')) |
||||
|
stdout.write(ansiEscapes.eraseLines(1) + l + value + ansiEscapes.beep) |
||||
|
if (caretOffset) { |
||||
|
process.stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset))) |
||||
|
} |
||||
|
} |
||||
|
return |
||||
|
} else if (!ansiRegex().test(data)) { |
||||
|
let tmp = value.substr(0, value.length + caretOffset) + data + |
||||
|
value.substr(value.length + caretOffset) |
||||
|
|
||||
|
if (mask) { |
||||
|
if (/\d/.test(data) && caretOffset !== 0) { |
||||
|
if (value[value.length + caretOffset + 1] === ' ') { |
||||
|
tmp = value.substr(0, value.length + caretOffset) + data + |
||||
|
value.substr(value.length + caretOffset + 1) |
||||
|
caretOffset += 2 |
||||
|
if (value[value.length + caretOffset] === '/') { |
||||
|
caretOffset += 2 |
||||
|
} |
||||
|
} else { |
||||
|
tmp = value.substr(0, value.length + caretOffset) + data + |
||||
|
value.substr(value.length + caretOffset + 1) |
||||
|
caretOffset++ |
||||
|
} |
||||
|
} else if (/\s/.test(data) && caretOffset < 0) { |
||||
|
caretOffset++ |
||||
|
tmp = value |
||||
|
} else { |
||||
|
return stdout.write(ansiEscapes.beep) |
||||
|
} |
||||
|
value = tmp |
||||
|
} else if (validateKeypress(data, value)) { |
||||
|
value = tmp |
||||
|
if (caretOffset === 0) { |
||||
|
const completion = await autoComplete(value) |
||||
|
if (completion) { |
||||
|
suggestion = chalk.gray(completion) |
||||
|
suggestion += ansiEscapes.cursorBackward(completion.length) |
||||
|
} else { |
||||
|
suggestion = '' |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
return stdout.write(ansiEscapes.beep) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (mask === 'cc' || mask === 'ccv') { |
||||
|
value = value.replace(/\s/g, '').replace(/(.{4})/g, '$1 ').trim() |
||||
|
value = value.replace(regex, chalk.gray('$1')) |
||||
|
} else if (mask === 'expDate') { |
||||
|
value = value.replace(regex, chalk.gray('$1')) |
||||
|
} |
||||
|
|
||||
|
stdout.write(ansiEscapes.eraseLines(1) + label + value + suggestion) |
||||
|
if (caretOffset) { |
||||
|
process.stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset))) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
stdin.on('data', onData) |
||||
|
}) |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
const chalk = require('chalk') |
||||
|
|
||||
|
// the equivalent of <code>, for embedding a cmd
|
||||
|
// eg: Please run ${cmd(woot)}
|
||||
|
|
||||
|
module.exports = cmd => ( |
||||
|
`${chalk.gray('`')}${chalk.cyan(cmd)}${chalk.gray('`')}` |
||||
|
) |
@ -0,0 +1,8 @@ |
|||||
|
const chalk = require('chalk') |
||||
|
|
||||
|
// the equivalent of <code>, for embedding anything
|
||||
|
// you may want to take a look at ./cmd.js
|
||||
|
|
||||
|
module.exports = cmd => ( |
||||
|
`${chalk.gray('`')}${chalk.bold(cmd)}${chalk.gray('`')}` |
||||
|
) |
@ -0,0 +1,7 @@ |
|||||
|
const chalk = require('chalk') |
||||
|
|
||||
|
// prints an error message
|
||||
|
module.exports = msg => { |
||||
|
console.error(`${chalk.red('> Error!')} ${msg}`) |
||||
|
} |
||||
|
|
@ -0,0 +1,7 @@ |
|||||
|
const chalk = require('chalk') |
||||
|
|
||||
|
// prints an informational message
|
||||
|
module.exports = msg => { |
||||
|
console.log(`${chalk.gray('>')} ${msg}`) |
||||
|
} |
||||
|
|
@ -0,0 +1,8 @@ |
|||||
|
const chalk = require('chalk') |
||||
|
|
||||
|
// returns a user param in a nice formatting
|
||||
|
// e.g.: google.com -> "google.com" (in bold)
|
||||
|
|
||||
|
module.exports = param => ( |
||||
|
chalk.bold(`${chalk.gray('"')}${chalk.bold(param)}${chalk.gray('"')}`) |
||||
|
) |
@ -0,0 +1,54 @@ |
|||||
|
const chalk = require('chalk') |
||||
|
|
||||
|
module.exports = (label, { |
||||
|
defaultValue = false, |
||||
|
abortSequences = new Set(['\x03']), |
||||
|
resolveChars = new Set(['\r']), |
||||
|
yesChar = 'y', |
||||
|
noChar = 'n', |
||||
|
stdin = process.stdin, |
||||
|
stdout = process.stdout |
||||
|
} = {}) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const isRaw = process.stdin.isRaw |
||||
|
|
||||
|
stdin.setRawMode(true) |
||||
|
stdin.resume() |
||||
|
|
||||
|
function restore() { |
||||
|
stdin.setRawMode(isRaw) |
||||
|
stdin.pause() |
||||
|
stdin.removeListener('data', onData) |
||||
|
} |
||||
|
|
||||
|
function onData(buffer) { |
||||
|
const data = buffer.toString() |
||||
|
|
||||
|
if (abortSequences.has(data)) { |
||||
|
restore() |
||||
|
return reject(new Error('USER_ABORT')) |
||||
|
} |
||||
|
|
||||
|
if (resolveChars.has(data[0])) { |
||||
|
restore() |
||||
|
resolve(defaultValue) |
||||
|
} else if (data[0].toLowerCase() === yesChar) { |
||||
|
restore() |
||||
|
resolve(true) |
||||
|
} else if (data[0].toLowerCase() === noChar) { |
||||
|
restore() |
||||
|
resolve(false) |
||||
|
} else { |
||||
|
// ignore extraneous input
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const defaultText = defaultValue === null ? |
||||
|
`[${yesChar}|${noChar}]` : |
||||
|
defaultValue ? |
||||
|
`[${chalk.bold(yesChar.toUpperCase())}|${noChar}]` : |
||||
|
`[${yesChar}|${chalk.bold(noChar.toUpperCase())}]` |
||||
|
stdout.write(`${chalk.gray('-')} ${label} ${chalk.gray(defaultText)} `) |
||||
|
stdin.on('data', onData) |
||||
|
}) |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
const ms = require('ms') |
||||
|
const chalk = require('chalk') |
||||
|
|
||||
|
// returns a time delta with the right color
|
||||
|
// example: `[103ms]`
|
||||
|
|
||||
|
module.exports = () => { |
||||
|
const start = new Date() |
||||
|
return () => ( |
||||
|
chalk.gray(`[${ms(new Date() - start)}]`) |
||||
|
) |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
const chalk = require('chalk') |
||||
|
|
||||
|
// prints a success message
|
||||
|
module.exports = msg => { |
||||
|
console.log(`${chalk.cyan('> Success!')} ${msg}`) |
||||
|
} |
@ -0,0 +1,7 @@ |
|||||
|
const chalk = require('chalk') |
||||
|
|
||||
|
// used for including uids in the output
|
||||
|
// example: `(dom_ji13dj2fih4fi2hf)`
|
||||
|
module.exports = id => ( |
||||
|
chalk.gray(`(${id})`) |
||||
|
) |
@ -0,0 +1,15 @@ |
|||||
|
const ora = require('ora') |
||||
|
const chalk = require('chalk') |
||||
|
const {eraseLine} = require('ansi-escapes') |
||||
|
|
||||
|
// prints a spinner followed by the given text
|
||||
|
module.exports = msg => { |
||||
|
const spinner = ora(chalk.gray(msg)) |
||||
|
spinner.color = 'gray' |
||||
|
spinner.start() |
||||
|
|
||||
|
return () => { |
||||
|
spinner.stop() |
||||
|
process.stdout.write(eraseLine) |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue