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