Browse Source

Added all team features and ability to upgrade

master
Leo Lamprecht 7 years ago
parent
commit
1b83978ac4
  1. 187
      src/providers/sh/commands/bin/teams.js
  2. 135
      src/providers/sh/commands/bin/teams/add.js
  3. 160
      src/providers/sh/commands/bin/teams/invite.js
  4. 64
      src/providers/sh/commands/bin/teams/list.js
  5. 126
      src/providers/sh/commands/bin/teams/switch.js
  6. 267
      src/providers/sh/commands/bin/upgrade.js
  7. 14
      src/providers/sh/index.js

187
src/providers/sh/commands/bin/teams.js

@ -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)
}
}
}

135
src/providers/sh/commands/bin/teams/add.js

@ -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()
}

160
src/providers/sh/commands/bin/teams/invite.js

@ -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}`)
}
}
}

64
src/providers/sh/commands/bin/teams/list.js

@ -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]
)
}

126
src/providers/sh/commands/bin/teams/switch.js

@ -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!`)
}

267
src/providers/sh/commands/bin/upgrade.js

@ -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()
}

14
src/providers/sh/index.js

@ -14,7 +14,10 @@ module.exports = {
'remove',
'whoami',
'secrets',
'logs'
'logs',
'upgrade',
'teams',
'switch'
]),
get deploy() {
return require('./deploy')
@ -57,5 +60,14 @@ module.exports = {
},
get logs() {
return require('./commands/bin/logs')
},
get upgrade() {
return require('./commands/bin/upgrade')
},
get teams() {
return require('./commands/bin/teams')
},
get switch() {
return require('./commands/bin/teams')
}
}

Loading…
Cancel
Save