Leo Lamprecht
7 years ago
7 changed files with 952 additions and 1 deletions
@ -0,0 +1,187 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
// Packages
|
|||
const chalk = require('chalk') |
|||
const minimist = require('minimist') |
|||
|
|||
// Utilities
|
|||
const login = require('../lib/login') |
|||
const cfg = require('../lib/cfg') |
|||
const error = require('../lib/utils/output/error') |
|||
const NowTeams = require('../lib/teams') |
|||
const logo = require('../lib/utils/output/logo') |
|||
const exit = require('../lib/utils/exit') |
|||
const { handleError } = require('../lib/error') |
|||
const list = require('./teams/list') |
|||
const add = require('./teams/add') |
|||
const change = require('./teams/switch') |
|||
const invite = require('./teams/invite') |
|||
|
|||
const help = () => { |
|||
console.log(` |
|||
${chalk.bold(`${logo} now teams`)} <add | ls | rm | invite> |
|||
|
|||
${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('–')} Add a new team: |
|||
|
|||
${chalk.cyan('$ now teams add')} |
|||
|
|||
${chalk.gray('–')} Switch to a team: |
|||
|
|||
${chalk.cyan(`$ now switch <slug>`)} |
|||
|
|||
${chalk.gray( |
|||
'–' |
|||
)} If your team's url is 'zeit.co/teams/name', 'name' is the slug |
|||
${chalk.gray('–')} If the slug is omitted, you can choose interactively |
|||
|
|||
${chalk.yellow( |
|||
'NOTE:' |
|||
)} When you switch, everything you add, list or remove will be scoped that team! |
|||
|
|||
${chalk.gray('–')} Invite new members (interactively): |
|||
|
|||
${chalk.cyan(`$ now teams invite`)} |
|||
|
|||
${chalk.gray('–')} Invite a specific email: |
|||
|
|||
${chalk.cyan(`$ now teams invite geist@zeit.co`)} |
|||
|
|||
${chalk.gray('–')} Remove a team: |
|||
|
|||
${chalk.cyan(`$ now teams rm <id>`)} |
|||
|
|||
${chalk.gray('–')} If the id is omitted, you can choose interactively |
|||
`)
|
|||
} |
|||
|
|||
let argv |
|||
let debug |
|||
let apiUrl |
|||
let subcommand |
|||
|
|||
const main = async ctx => { |
|||
argv = minimist(ctx.argv.slice(2), { |
|||
string: ['config', 'token'], |
|||
boolean: ['help', 'debug'], |
|||
alias: { |
|||
help: 'h', |
|||
config: 'c', |
|||
debug: 'd', |
|||
token: 't', |
|||
switch: 'change' |
|||
} |
|||
}) |
|||
|
|||
debug = argv.debug |
|||
apiUrl = argv.url || 'https://api.zeit.co' |
|||
|
|||
if (argv.config) { |
|||
cfg.setConfigFile(argv.config) |
|||
} |
|||
|
|||
const isSwitch = argv._[0] && argv._[0] === 'switch' |
|||
|
|||
argv._ = argv._.slice(1) |
|||
subcommand = argv._[0] |
|||
|
|||
if (isSwitch) { |
|||
subcommand = 'switch' |
|||
} |
|||
|
|||
if (argv.help || !subcommand) { |
|||
help() |
|||
exit(0) |
|||
} |
|||
|
|||
const config = await cfg.read({ token: argv.token }) |
|||
let token |
|||
|
|||
try { |
|||
token = config.token || (await login(apiUrl)) |
|||
} catch (err) { |
|||
error(`Authentication error – ${err.message}`) |
|||
exit(1) |
|||
} |
|||
|
|||
try { |
|||
await run({ token, config }) |
|||
} catch (err) { |
|||
if (err.userError) { |
|||
error(err.message) |
|||
} else { |
|||
error(`Unknown error: ${err.stack}`) |
|||
} |
|||
exit(1) |
|||
} |
|||
} |
|||
|
|||
module.exports = async ctx => { |
|||
try { |
|||
await main(ctx) |
|||
} catch (err) { |
|||
handleError(err) |
|||
process.exit(1) |
|||
} |
|||
} |
|||
|
|||
async function run({ token, config: { currentTeam } }) { |
|||
const teams = new NowTeams({ apiUrl, token, debug, currentTeam }) |
|||
const args = argv._ |
|||
|
|||
switch (subcommand) { |
|||
case 'list': |
|||
case 'ls': { |
|||
await list({ |
|||
teams, |
|||
token |
|||
}) |
|||
break |
|||
} |
|||
case 'switch': |
|||
case 'change': { |
|||
await change({ |
|||
teams, |
|||
args, |
|||
token |
|||
}) |
|||
break |
|||
} |
|||
case 'add': |
|||
case 'create': { |
|||
await add({ teams, token }) |
|||
break |
|||
} |
|||
|
|||
case 'invite': { |
|||
await invite({ |
|||
teams, |
|||
args, |
|||
token |
|||
}) |
|||
break |
|||
} |
|||
|
|||
default: { |
|||
let code = 0 |
|||
if (subcommand !== 'help') { |
|||
error('Please specify a valid subcommand: ls | add | rm | set-default') |
|||
code = 1 |
|||
} |
|||
help() |
|||
exit(code) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,135 @@ |
|||
// Packages
|
|||
const chalk = require('chalk') |
|||
|
|||
// Utilities
|
|||
const stamp = require('../../lib/utils/output/stamp') |
|||
const info = require('../../lib/utils/output/info') |
|||
const error = require('../../lib/utils/output/error') |
|||
const wait = require('../../lib/utils/output/wait') |
|||
const rightPad = require('../../lib/utils/output/right-pad') |
|||
const eraseLines = require('../../lib/utils/output/erase-lines') |
|||
const { tick } = require('../../lib/utils/output/chars') |
|||
const success = require('../../lib/utils/output/success') |
|||
const cmd = require('../../lib/utils/output/cmd') |
|||
const note = require('../../lib/utils/output/note') |
|||
const uid = require('../../lib/utils/output/uid') |
|||
const textInput = require('../../lib/utils/input/text') |
|||
const exit = require('../../lib/utils/exit') |
|||
const cfg = require('../../lib/cfg') |
|||
|
|||
function validateSlugKeypress(data, value) { |
|||
// TODO: the `value` here should contain the current value + the keypress
|
|||
// should be fixed on utils/input/text.js
|
|||
return /^[a-zA-Z]+[a-zA-Z0-9_-]*$/.test(value + data) |
|||
} |
|||
|
|||
function gracefulExit() { |
|||
console.log() // Blank line
|
|||
note( |
|||
`Your team is now active for all ${cmd('now')} commands!\n Run ${cmd( |
|||
'now switch' |
|||
)} to change it in the future.` |
|||
) |
|||
return exit() |
|||
} |
|||
|
|||
const teamUrlPrefix = rightPad('Team URL', 14) + chalk.gray('zeit.co/') |
|||
const teamNamePrefix = rightPad('Team Name', 14) |
|||
|
|||
module.exports = async function({ teams, token }) { |
|||
let slug |
|||
let team |
|||
let elapsed |
|||
let stopSpinner |
|||
|
|||
info( |
|||
`Pick a team identifier for its url (e.g.: ${chalk.cyan('`zeit.co/acme`')})` |
|||
) |
|||
do { |
|||
try { |
|||
// eslint-disable-next-line no-await-in-loop
|
|||
slug = await textInput({ |
|||
label: `- ${teamUrlPrefix}`, |
|||
validateKeypress: validateSlugKeypress, |
|||
initialValue: slug, |
|||
valid: team, |
|||
forceLowerCase: true |
|||
}) |
|||
} catch (err) { |
|||
if (err.message === 'USER_ABORT') { |
|||
info('Aborted') |
|||
return exit() |
|||
} |
|||
throw err |
|||
} |
|||
elapsed = stamp() |
|||
stopSpinner = wait(teamUrlPrefix + slug) |
|||
|
|||
let res |
|||
try { |
|||
// eslint-disable-next-line no-await-in-loop
|
|||
res = await teams.create({ slug }) |
|||
stopSpinner() |
|||
team = res |
|||
} catch (err) { |
|||
stopSpinner() |
|||
eraseLines(2) |
|||
error(err.message) |
|||
} |
|||
} while (!team) |
|||
|
|||
eraseLines(2) |
|||
success(`Team created ${uid(team.id)} ${elapsed()}`) |
|||
console.log(chalk.cyan(`${tick} `) + teamUrlPrefix + slug + '\n') |
|||
|
|||
info('Pick a display name for your team') |
|||
let name |
|||
try { |
|||
name = await textInput({ |
|||
label: `- ${teamNamePrefix}`, |
|||
validateValue: value => value.trim().length > 0 |
|||
}) |
|||
} catch (err) { |
|||
if (err.message === 'USER_ABORT') { |
|||
info('No name specified') |
|||
gracefulExit() |
|||
} else { |
|||
throw err |
|||
} |
|||
} |
|||
elapsed = stamp() |
|||
stopSpinner = wait(teamNamePrefix + name) |
|||
const res = await teams.edit({ id: team.id, name }) |
|||
stopSpinner() |
|||
|
|||
eraseLines(2) |
|||
if (res.error) { |
|||
error(res.error.message) |
|||
console.log(`${chalk.red(`✖ ${teamNamePrefix}`)}${name}`) |
|||
exit(1) |
|||
// TODO: maybe we want to ask the user to retry? not sure if
|
|||
// there's a scenario where that would be wanted
|
|||
} |
|||
|
|||
team = Object.assign(team, res) |
|||
|
|||
success(`Team name saved ${elapsed()}`) |
|||
console.log(chalk.cyan(`${tick} `) + teamNamePrefix + team.name + '\n') |
|||
|
|||
stopSpinner = wait('Saving') |
|||
await cfg.merge({ currentTeam: team }) |
|||
stopSpinner() |
|||
|
|||
await require('./invite')({ |
|||
teams, |
|||
args: [], |
|||
token, |
|||
introMsg: |
|||
'Invite your team mates! When done, press enter on an empty field', |
|||
noopMsg: `You can invite team mates later by running ${cmd( |
|||
'now teams invite' |
|||
)}` |
|||
}) |
|||
|
|||
gracefulExit() |
|||
} |
@ -0,0 +1,160 @@ |
|||
// Packages
|
|||
const chalk = require('chalk') |
|||
|
|||
// Utilities
|
|||
const regexes = require('../../lib/utils/input/regexes') |
|||
const wait = require('../../lib/utils/output/wait') |
|||
const cfg = require('../../lib/cfg') |
|||
const fatalError = require('../../lib/utils/fatal-error') |
|||
const cmd = require('../../lib/utils/output/cmd') |
|||
const info = require('../../lib/utils/output/info') |
|||
const stamp = require('../../lib/utils/output/stamp') |
|||
const param = require('../../lib/utils/output/param') |
|||
const { tick } = require('../../lib/utils/output/chars') |
|||
const rightPad = require('../../lib/utils/output/right-pad') |
|||
const textInput = require('../../lib/utils/input/text') |
|||
const eraseLines = require('../../lib/utils/output/erase-lines') |
|||
const success = require('../../lib/utils/output/success') |
|||
const error = require('../../lib/utils/output/error') |
|||
|
|||
function validateEmail(data) { |
|||
return regexes.email.test(data.trim()) || data.length === 0 |
|||
} |
|||
|
|||
const domains = Array.from( |
|||
new Set([ |
|||
'aol.com', |
|||
'gmail.com', |
|||
'google.com', |
|||
'yahoo.com', |
|||
'ymail.com', |
|||
'hotmail.com', |
|||
'live.com', |
|||
'outlook.com', |
|||
'inbox.com', |
|||
'mail.com', |
|||
'gmx.com', |
|||
'icloud.com' |
|||
]) |
|||
) |
|||
|
|||
function emailAutoComplete(value, teamSlug) { |
|||
const parts = value.split('@') |
|||
|
|||
if (parts.length === 2 && parts[1].length > 0) { |
|||
const [, host] = parts |
|||
let suggestion = false |
|||
|
|||
domains.unshift(teamSlug) |
|||
for (const domain of domains) { |
|||
if (domain.startsWith(host)) { |
|||
suggestion = domain.substr(host.length) |
|||
break |
|||
} |
|||
} |
|||
|
|||
domains.shift() |
|||
return suggestion |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
module.exports = async function( |
|||
{ teams, args, token, introMsg, noopMsg = 'No changes made' } = {} |
|||
) { |
|||
const { user, currentTeam } = await cfg.read({ token }) |
|||
|
|||
domains.push(user.email.split('@')[1]) |
|||
|
|||
if (!currentTeam) { |
|||
let err = `You can't run this command under ${param( |
|||
user.username || user.email |
|||
)}.\n` |
|||
err += `${chalk.gray('>')} Run ${cmd('now switch')} to choose to a team.` |
|||
return fatalError(err) |
|||
} |
|||
|
|||
info(introMsg || `Inviting team members to ${chalk.bold(currentTeam.name)}`) |
|||
|
|||
if (args.length > 0) { |
|||
for (const email of args) { |
|||
if (regexes.email.test(email)) { |
|||
const stopSpinner = wait(email) |
|||
const elapsed = stamp() |
|||
// eslint-disable-next-line no-await-in-loop
|
|||
await teams.inviteUser({ teamId: currentTeam.id, email }) |
|||
stopSpinner() |
|||
console.log(`${chalk.cyan(tick)} ${email} ${elapsed()}`) |
|||
} else { |
|||
console.log(`${chalk.red(`✖ ${email}`)} ${chalk.gray('[invalid]')}`) |
|||
} |
|||
} |
|||
return |
|||
} |
|||
|
|||
const inviteUserPrefix = rightPad('Invite User', 14) |
|||
const emails = [] |
|||
let hasError = false |
|||
let email |
|||
do { |
|||
email = '' |
|||
try { |
|||
// eslint-disable-next-line no-await-in-loop
|
|||
email = await textInput({ |
|||
label: `- ${inviteUserPrefix}`, |
|||
validateValue: validateEmail, |
|||
autoComplete: value => emailAutoComplete(value, currentTeam.slug) |
|||
}) |
|||
} catch (err) { |
|||
if (err.message !== 'USER_ABORT') { |
|||
throw err |
|||
} |
|||
} |
|||
let elapsed |
|||
let stopSpinner |
|||
if (email) { |
|||
elapsed = stamp() |
|||
stopSpinner = wait(inviteUserPrefix + email) |
|||
try { |
|||
// eslint-disable-next-line no-await-in-loop
|
|||
await teams.inviteUser({ teamId: currentTeam.id, email }) |
|||
stopSpinner() |
|||
email = `${email} ${elapsed()}` |
|||
emails.push(email) |
|||
console.log(`${chalk.cyan(tick)} ${inviteUserPrefix}${email}`) |
|||
if (hasError) { |
|||
hasError = false |
|||
eraseLines(emails.length + 2) |
|||
info( |
|||
introMsg || |
|||
`Inviting team members to ${chalk.bold(currentTeam.name)}` |
|||
) |
|||
for (const email of emails) { |
|||
console.log(`${chalk.cyan(tick)} ${inviteUserPrefix}${email}`) |
|||
} |
|||
} |
|||
} catch (err) { |
|||
stopSpinner() |
|||
eraseLines(emails.length + 2) |
|||
error(err.message) |
|||
hasError = true |
|||
for (const email of emails) { |
|||
console.log(`${chalk.cyan(tick)} ${inviteUserPrefix}${email}`) |
|||
} |
|||
} |
|||
} |
|||
} while (email !== '') |
|||
|
|||
eraseLines(emails.length + 2) |
|||
|
|||
const n = emails.length |
|||
if (emails.length === 0) { |
|||
info(noopMsg) |
|||
} else { |
|||
success(`Invited ${n} team mate${n > 1 ? 's' : ''}`) |
|||
for (const email of emails) { |
|||
console.log(`${chalk.cyan(tick)} ${inviteUserPrefix}${email}`) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,64 @@ |
|||
// Packages
|
|||
const chalk = require('chalk') |
|||
|
|||
// Utilities
|
|||
const wait = require('../../lib/utils/output/wait') |
|||
const cfg = require('../../lib/cfg') |
|||
const info = require('../../lib/utils/output/info') |
|||
const error = require('../../lib/utils/output/error') |
|||
const { tick: tickChar } = require('../../lib/utils/output/chars') |
|||
const table = require('../../lib/utils/output/table') |
|||
|
|||
module.exports = async function({ teams, token }) { |
|||
const stopSpinner = wait('Fetching teams') |
|||
const list = (await teams.ls()).teams |
|||
let { user, currentTeam } = await cfg.read({ token }) |
|||
const accountIsCurrent = !currentTeam |
|||
stopSpinner() |
|||
|
|||
if (accountIsCurrent) { |
|||
currentTeam = { |
|||
slug: user.username || user.email |
|||
} |
|||
} |
|||
|
|||
const teamList = list.map(({ slug, name }) => { |
|||
return { |
|||
name, |
|||
value: slug, |
|||
current: slug === currentTeam.slug ? tickChar : '' |
|||
} |
|||
}) |
|||
|
|||
teamList.unshift({ |
|||
name: user.email, |
|||
value: user.username || user.email, |
|||
current: (accountIsCurrent && tickChar) || '' |
|||
}) |
|||
|
|||
// Let's bring the current team to the beginning of the list
|
|||
if (!accountIsCurrent) { |
|||
const index = teamList.findIndex( |
|||
choice => choice.value === currentTeam.slug |
|||
) |
|||
const choice = teamList.splice(index, 1)[0] |
|||
teamList.unshift(choice) |
|||
} |
|||
|
|||
// Printing
|
|||
const count = teamList.length |
|||
if (!count) { |
|||
// Maybe should not happen
|
|||
error(`No team found`) |
|||
return |
|||
} |
|||
|
|||
info(`${chalk.bold(count)} team${count > 1 ? 's' : ''} found`) |
|||
console.log() |
|||
|
|||
table( |
|||
['', 'id', 'email / name'], |
|||
teamList.map(team => [team.current, team.value, team.name]), |
|||
[1, 5] |
|||
) |
|||
} |
@ -0,0 +1,126 @@ |
|||
// Packages
|
|||
const chalk = require('chalk') |
|||
|
|||
// Utilities
|
|||
const wait = require('../../lib/utils/output/wait') |
|||
const listInput = require('../../lib/utils/input/list') |
|||
const cfg = require('../../lib/cfg') |
|||
const exit = require('../../lib/utils/exit') |
|||
const success = require('../../lib/utils/output/success') |
|||
const info = require('../../lib/utils/output/info') |
|||
const error = require('../../lib/utils/output/error') |
|||
const param = require('../../lib/utils/output/param') |
|||
|
|||
async function updateCurrentTeam({ cfg, newTeam } = {}) { |
|||
delete newTeam.created |
|||
delete newTeam.creator_id |
|||
await cfg.merge({ currentTeam: newTeam }) |
|||
} |
|||
|
|||
module.exports = async function({ teams, args, token }) { |
|||
let stopSpinner = wait('Fetching teams') |
|||
const list = (await teams.ls()).teams |
|||
let { user, currentTeam } = await cfg.read({ token }) |
|||
const accountIsCurrent = !currentTeam |
|||
stopSpinner() |
|||
|
|||
if (accountIsCurrent) { |
|||
currentTeam = { |
|||
slug: user.username || user.email |
|||
} |
|||
} |
|||
|
|||
if (args.length !== 0) { |
|||
const desiredSlug = args[0] |
|||
|
|||
const newTeam = list.find(team => team.slug === desiredSlug) |
|||
if (newTeam) { |
|||
await updateCurrentTeam({ cfg, newTeam }) |
|||
success(`The team ${chalk.bold(newTeam.name)} is now active!`) |
|||
return exit() |
|||
} |
|||
if (desiredSlug === user.username) { |
|||
stopSpinner = wait('Saving') |
|||
await cfg.remove('currentTeam') |
|||
stopSpinner() |
|||
return success(`Your account (${chalk.bold(desiredSlug)}) is now active!`) |
|||
} |
|||
error(`Could not find membership for team ${param(desiredSlug)}`) |
|||
return exit(1) |
|||
} |
|||
|
|||
const choices = list.map(({ slug, name }) => { |
|||
name = `${slug} (${name})` |
|||
if (slug === currentTeam.slug) { |
|||
name += ` ${chalk.bold('(current)')}` |
|||
} |
|||
|
|||
return { |
|||
name, |
|||
value: slug, |
|||
short: slug |
|||
} |
|||
}) |
|||
|
|||
const suffix = accountIsCurrent ? ` ${chalk.bold('(current)')}` : '' |
|||
|
|||
const userEntryName = user.username |
|||
? `${user.username} (${user.email})${suffix}` |
|||
: user.email |
|||
|
|||
choices.unshift({ |
|||
name: userEntryName, |
|||
value: user.email, |
|||
short: user.username |
|||
}) |
|||
|
|||
// Let's bring the current team to the beginning of the list
|
|||
if (!accountIsCurrent) { |
|||
const index = choices.findIndex(choice => choice.value === currentTeam.slug) |
|||
const choice = choices.splice(index, 1)[0] |
|||
choices.unshift(choice) |
|||
} |
|||
|
|||
let message |
|||
|
|||
if (currentTeam) { |
|||
message = `Switch to:` |
|||
} |
|||
|
|||
const choice = await listInput({ |
|||
message, |
|||
choices, |
|||
separator: false |
|||
}) |
|||
|
|||
// Abort
|
|||
if (!choice) { |
|||
info('No changes made') |
|||
return exit() |
|||
} |
|||
|
|||
const newTeam = list.find(item => item.slug === choice) |
|||
|
|||
// Switch to account
|
|||
if (!newTeam) { |
|||
if (currentTeam.slug === user.username || currentTeam.slug === user.email) { |
|||
info('No changes made') |
|||
return exit() |
|||
} |
|||
stopSpinner = wait('Saving') |
|||
await cfg.remove('currentTeam') |
|||
stopSpinner() |
|||
return success(`Your account (${chalk.bold(choice)}) is now active!`) |
|||
} |
|||
|
|||
if (newTeam.slug === currentTeam.slug) { |
|||
info('No changes made') |
|||
return exit() |
|||
} |
|||
|
|||
stopSpinner = wait('Saving') |
|||
await updateCurrentTeam({ cfg, newTeam }) |
|||
stopSpinner() |
|||
|
|||
success(`The team ${chalk.bold(newTeam.name)} is now active!`) |
|||
} |
@ -0,0 +1,267 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
// Packages
|
|||
const chalk = require('chalk') |
|||
const minimist = require('minimist') |
|||
const ms = require('ms') |
|||
|
|||
// Utilities
|
|||
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 logo = require('../lib/utils/output/logo') |
|||
const { handleError } = require('../lib/error') |
|||
|
|||
const { bold } = chalk |
|||
|
|||
const help = () => { |
|||
console.log(` |
|||
${chalk.bold(`${logo} 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`)} |
|||
`)
|
|||
} |
|||
|
|||
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) |
|||
} |
|||
|
|||
let argv |
|||
let debug |
|||
let apiUrl |
|||
|
|||
const main = async ctx => { |
|||
argv = minimist(ctx.argv.slice(2), { |
|||
string: ['config', 'token'], |
|||
boolean: ['help', 'debug'], |
|||
alias: { |
|||
help: 'h', |
|||
config: 'c', |
|||
debug: 'd', |
|||
token: 't' |
|||
} |
|||
}) |
|||
|
|||
argv._ = argv._.slice(1) |
|||
|
|||
debug = argv.debug |
|||
apiUrl = argv.url || 'https://api.zeit.co' |
|||
|
|||
if (argv.config) { |
|||
cfg.setConfigFile(argv.config) |
|||
} |
|||
|
|||
if (argv.help) { |
|||
help() |
|||
exit(0) |
|||
} |
|||
|
|||
const config = await cfg.read({ token: argv.token }) |
|||
|
|||
let token |
|||
|
|||
try { |
|||
token = config.token || (await login(apiUrl)) |
|||
} catch (err) { |
|||
error(`Authentication error – ${err.message}`) |
|||
exit(1) |
|||
} |
|||
|
|||
try { |
|||
await run({ token, config }) |
|||
} catch (err) { |
|||
if (err.userError) { |
|||
error(err.message) |
|||
} else { |
|||
error(`Unknown error: ${err.stack}`) |
|||
} |
|||
exit(1) |
|||
} |
|||
} |
|||
|
|||
module.exports = async ctx => { |
|||
try { |
|||
await main(ctx) |
|||
} catch (err) { |
|||
handleError(err) |
|||
process.exit(1) |
|||
} |
|||
} |
|||
|
|||
function buildInquirerChoices(current, until) { |
|||
if (until) { |
|||
until = until.split(' ') |
|||
until = ' for ' + chalk.bold(until[0]) + ' more ' + until[1] |
|||
} else { |
|||
until = '' |
|||
} |
|||
|
|||
const currentText = bold('(current)') |
|||
let ossName = `OSS ${bold('FREE')}` |
|||
let premiumName = `Premium ${bold('$15')}` |
|||
let proName = `Pro ${bold('$50')}` |
|||
let advancedName = `Advanced ${bold('$200')}` |
|||
|
|||
switch (current) { |
|||
case 'oss': { |
|||
ossName += indent(currentText, 6) |
|||
break |
|||
} |
|||
case 'premium': { |
|||
premiumName += indent(currentText, 3) |
|||
break |
|||
} |
|||
case 'pro': { |
|||
proName += indent(currentText, 7) |
|||
break |
|||
} |
|||
case 'advanced': { |
|||
advancedName += indent(currentText, 1) |
|||
break |
|||
} |
|||
default: { |
|||
ossName += indent(currentText, 6) |
|||
} |
|||
} |
|||
|
|||
return [ |
|||
{ |
|||
name: ossName, |
|||
value: 'oss', |
|||
short: `OSS ${bold('FREE')}` |
|||
}, |
|||
{ |
|||
name: premiumName, |
|||
value: 'premium', |
|||
short: `Premium ${bold('$15')}` |
|||
}, |
|||
{ |
|||
name: proName, |
|||
value: 'pro', |
|||
short: `Pro ${bold('$50')}` |
|||
}, |
|||
{ |
|||
name: advancedName, |
|||
value: 'advanced', |
|||
short: `Advanced ${bold('$200')}` |
|||
} |
|||
] |
|||
} |
|||
|
|||
async function run({ token, config: { currentTeam, user } }) { |
|||
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, currentTeam }) |
|||
|
|||
let planId = args[0] |
|||
|
|||
if (![undefined, 'oss', 'premium', 'pro', 'advanced'].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) |
|||
|
|||
let message = `For more info, please head to https://zeit.co` |
|||
message = currentTeam |
|||
? `${message}/${currentTeam.slug}/settings/plan` |
|||
: `${message}/account/plan` |
|||
message += `\n> Select a plan for ${bold( |
|||
(currentTeam && currentTeam.slug) || user.username || user.email |
|||
)} ${chalk.gray(`[${elapsed}]`)}` |
|||
const choices = buildInquirerChoices(currentPlan.id, currentPlan.until) |
|||
|
|||
planId = await listInput({ |
|||
message, |
|||
choices, |
|||
separator: false, |
|||
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) { |
|||
if (err.code === 'customer_not_found' || err.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 ${err.message}`) |
|||
} |
|||
plans.close() |
|||
return |
|||
} |
|||
|
|||
if (currentPlan.until && newPlan.id !== 'oss') { |
|||
success( |
|||
`The cancelation has been undone. You're back on the ${chalk.bold( |
|||
`${newPlan.name} plan` |
|||
)}` |
|||
) |
|||
} else if (newPlan.until) { |
|||
success( |
|||
`Your plan will be switched to ${chalk.bold( |
|||
newPlan.name |
|||
)} in ${chalk.bold(newPlan.until)}. Your card will not be charged again` |
|||
) |
|||
} else { |
|||
success(`You're now on the ${chalk.bold(`${newPlan.name} plan`)}`) |
|||
} |
|||
|
|||
plans.close() |
|||
} |
Loading…
Reference in new issue