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