now-flow
Version:
Add deployment workflows to Zeit now
368 lines (309 loc) • 8.96 kB
JavaScript
// Packages
const chalk = require('chalk')
const mri = require('mri')
const ms = require('ms')
const plural = require('pluralize')
// Utilities
const { handleError, error } = require('../util/error')
const NowCreditCards = require('../util/credit-cards')
const indent = require('../util/indent')
const listInput = require('../../../util/input/list')
const success = require('../../../util/output/success')
const promptBool = require('../../../util/input/prompt-bool')
const info = require('../../../util/output/info')
const logo = require('../../../util/output/logo')
const addBilling = require('./billing/add')
const exit = require('../../../util/exit')
/*eslint-disable */
const getProcess = () => process
/*eslint-enable */
const help = () => {
console.log(`
${chalk.bold(`${logo} now billing`)} [options] <command>
${chalk.dim('Options:')}
ls Show all of your credit cards
add Add a new credit card
rm [id] Remove a credit card
set-default [id] Make a credit card your default one
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-T, --team Set a custom team scope
${chalk.dim('Examples:')}
${chalk.gray('–')} Add a new credit card (interactively)
${chalk.cyan('$ now billing add')}
`)
}
let argv
let debug
let apiUrl
let subcommand
const main = async ctx => {
argv = mri(ctx.argv.slice(2), {
boolean: ['help', 'debug'],
alias: {
help: 'h',
debug: 'd'
}
})
argv._ = argv._.slice(1)
debug = argv.debug
apiUrl = ctx.apiUrl
subcommand = argv._[0]
if (argv.help || !subcommand) {
help()
await exit(0)
}
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
try {
await run({ token, sh })
} catch (err) {
if (err.userError) {
console.error(error(err.message))
} else {
console.error(error(`Unknown error: ${err.stack}`))
}
exit(1)
}
}
module.exports = async ctx => {
try {
await main(ctx)
} catch (err) {
handleError(err)
getProcess().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, sh: { currentTeam, user } }) {
const start = new Date()
const creditCards = new NowCreditCards({ apiUrl, token, debug, currentTeam })
const args = argv._.slice(1)
switch (subcommand) {
case 'ls':
case 'list': {
let cards
try {
cards = await creditCards.ls()
} catch (err) {
console.error(error(err.message))
return
}
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}, `
}
// 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(
`> ${
plural('card', cards.cards.length, true)
} found under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)} ${chalk.gray(`[${elapsed}]`)}`
)
if (text) {
console.log(`\n${text}\n`)
}
break
}
case 'set-default': {
if (args.length > 1) {
console.error(error('Invalid number of arguments'))
return exit(1)
}
const start = new Date()
let cards
try {
cards = await creditCards.ls()
} catch (err) {
console.error(error(err.message))
return
}
if (cards.cards.length === 0) {
console.error(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 for ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)} ${chalk.gray(`[${elapsed}]`)}`
const choices = buildInquirerChoices(cards)
cardId = await listInput({
message,
choices,
separator: true,
abort: 'end'
})
}
// 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, {
trailing: '\n'
})
if (!confirmation) {
info('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) {
console.error(error('Invalid number of arguments'))
return exit(1)
}
const start = new Date()
let cards
try {
cards = await creditCards.ls()
} catch (err) {
console.error(error(err.message))
return
}
if (cards.cards.length === 0) {
console.error(error(
`You have no credit cards to choose from to delete under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
))
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'
)} under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)} ${chalk.gray(`[${elapsed}]`)}`
const choices = buildInquirerChoices(cards)
cardId = await listInput({
message,
choices,
separator: true,
abort: 'start'
})
}
// Shoud 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)
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 for ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
}
}
const elapsed = ms(new Date() - start)
text += ` ${chalk.gray(`[${elapsed}]`)}`
success(text)
} else {
console.log('No changes made')
}
break
}
case 'add': {
addBilling({
creditCards,
currentTeam,
user
})
break
}
default:
console.error(error('Please specify a valid subcommand: ls | add | rm | set-default'))
help()
exit(1)
}
creditCards.close()
}