now-flow
Version:
Add deployment workflows to Zeit now
374 lines (325 loc) • 9.25 kB
JavaScript
// Packages
const chalk = require('chalk')
const mri = require('mri')
const ms = require('ms')
const psl = require('psl')
const table = require('text-table')
const plural = require('pluralize')
// Utilities
const NowDomains = require('../util/domains')
const exit = require('../../../util/exit')
const logo = require('../../../util/output/logo')
const promptBool = require('../../../util/input/prompt-bool')
const strlen = require('../util/strlen')
const toHost = require('../util/to-host')
const { handleError, error } = require('../util/error')
const buy = require('./domains/buy')
/*eslint-disable */
const getProcess = () => process
/*eslint-enable */
const help = () => {
console.log(`
${chalk.bold(`${logo} now domains`)} [options] <command>
${chalk.dim('Commands:')}
ls Show all domains in a list
add [name] Add a new domain that you already own
rm [name] Remove a domain
buy [name] Buy a domain that you don't yet own
${chalk.dim('Options:')}
-h, --help Output usage information
-d, --debug Debug mode [off]
-e, --external Use external DNS server
-f, --force Skip DNS verification
-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
-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 domain that you already own
${chalk.cyan(`$ now domains add ${chalk.underline('domain-name.com')}`)}
Make sure the domain's DNS nameservers are at least 2 of the
ones listed on ${chalk.underline('https://zeit.world')}.
${chalk.yellow('NOTE:')} Running ${chalk.dim(
'`now alias`'
)} will automatically register your domain
if it's configured with these nameservers (no need to ${chalk.dim(
'`domain add`'
)}).
${chalk.gray(
'–'
)} Add a domain using an external nameserver
${chalk.cyan('$ now domain add -e my-app.com')}
`)
}
// Options
let argv
let debug
let apiUrl
let subcommand
const main = async ctx => {
argv = mri(ctx.argv.slice(2), {
string: ['coupon'],
boolean: ['help', 'debug', 'external', 'force'],
alias: {
help: 'h',
coupon: 'c',
debug: 'd',
external: 'e',
force: 'f'
}
})
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}\n${err.stack}`))
}
exit(1)
}
}
module.exports = async ctx => {
try {
await main(ctx)
} catch (err) {
handleError(err)
getProcess().exit(1)
}
}
async function run({ token, sh: { currentTeam, user } }) {
const domain = new NowDomains({ apiUrl, token, debug, currentTeam })
const args = argv._.slice(1)
switch (subcommand) {
case 'ls':
case 'list': {
if (args.length !== 0) {
console.error(error('Invalid number of arguments'))
return exit(1)
}
const start_ = new Date()
const domains = await domain.ls()
domains.sort((a, b) => new Date(b.created) - new Date(a.created))
const current = new Date()
const header = [
['', 'domain', 'dns', 'verified', 'created'].map(s => chalk.dim(s))
]
const out =
domains.length === 0
? null
: table(
header.concat(
domains.map(domain => {
const ns = domain.isExternal ? 'external' : 'zeit.world'
const url = chalk.bold(domain.name)
const time = chalk.gray(
ms(current - new Date(domain.created)) + ' ago'
)
return ['', url, ns, domain.verified, time]
})
),
{
align: ['l', 'l', 'l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen
}
)
const elapsed_ = ms(new Date() - start_)
console.log(
`> ${plural('domain', domains.length, true)} found under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)} ${chalk.gray(`[${elapsed_}]`)}`
)
if (out) {
console.log('\n' + out + '\n')
}
break
}
case 'rm':
case 'remove': {
if (args.length !== 1) {
console.error(error('Invalid number of arguments'))
return exit(1)
}
const _target = String(args[0])
if (!_target) {
const err = new Error('No domain specified')
err.userError = true
throw err
}
const _domains = await domain.ls()
const _domain = findDomain(_target, _domains)
if (!_domain) {
const err = new Error(
`Domain not found by "${_target}". Run ${chalk.dim(
'`now domains ls`'
)} to see your domains.`
)
err.userError = true
throw err
}
try {
const confirmation = (await readConfirmation(
domain,
_domain
)).toLowerCase()
if (confirmation !== 'y' && confirmation !== 'yes') {
console.log('\n> Aborted')
getProcess().exit(0)
}
const start = new Date()
await domain.rm(_domain)
const elapsed = ms(new Date() - start)
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(_domain.name)} removed [${elapsed}]`
)
} catch (err) {
console.error(error(err))
exit(1)
}
break
}
case 'add':
case 'set': {
if (args.length !== 1) {
console.error(error('Invalid number of arguments'))
return exit(1)
}
const name = String(args[0])
const parsedDomain = psl.parse(name)
if (parsedDomain.subdomain) {
const msg =
`You are adding "${name}" as a domain name which seems to contain a subdomain part "${parsedDomain.subdomain}".\n` +
' This is probably wrong unless you really know what you are doing.\n' +
` To add the root domain instead please run: ${chalk.cyan(
'now domain add ' +
(argv.external ? '-e ' : '') +
parsedDomain.domain
)}\n` +
` Continue adding "${name}" as a domain name?`
if (!await promptBool(msg)) {
return exit(1)
}
}
const start = new Date()
const { uid, code, created, verified } = await domain.add(
name,
argv.force,
argv.external
)
const elapsed = ms(new Date() - start)
if (created) {
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
chalk.underline(name)
)} ${chalk.dim(`(${uid})`)} added [${elapsed}]`
)
} else if (verified) {
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
chalk.underline(name)
)} ${chalk.dim(`(${uid})`)} verified [${elapsed}]`
)
} else if (code === 'not_modified') {
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
chalk.underline(name)
)} ${chalk.dim(`(${uid})`)} already exists [${elapsed}]`
)
} else {
console.log(
'> Verification required: Please rerun this command after some time'
)
}
break
}
case 'buy': {
await buy({
domains: domain,
args,
currentTeam,
user,
coupon: argv.coupon
})
break
}
default:
console.error(error('Please specify a valid subcommand: ls | add | rm'))
help()
exit(1)
}
domain.close()
}
async function readConfirmation(domain, _domain) {
return new Promise(resolve => {
const time = chalk.gray(ms(new Date() - new Date(_domain.created)) + ' ago')
const tbl = table([[chalk.bold(_domain.name), time]], {
align: ['r', 'l'],
hsep: ' '.repeat(6)
})
getProcess().stdout.write('> The following domain will be removed permanently\n')
getProcess().stdout.write(' ' + tbl + '\n')
if (_domain.aliases.length > 0) {
getProcess().stdout.write(
`> ${chalk.yellow('Warning!')} This domain's ` +
`${chalk.bold(
plural('alias', _domain.aliases.length, true)
)} ` +
`will be removed. Run ${chalk.dim('`now alias ls`')} to list them.\n`
)
}
if (_domain.certs.length > 0) {
getProcess().stdout.write(
`> ${chalk.yellow('Warning!')} This domain's ` +
`${chalk.bold(
plural('certificate', _domain.certs.length, true)
)} ` +
`will be removed. Run ${chalk.dim('`now cert ls`')} to list them.\n`
)
}
getProcess().stdout.write(
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
)
getProcess().stdin
.on('data', d => {
getProcess().stdin.pause()
resolve(d.toString().trim())
})
.resume()
})
}
function findDomain(val, list) {
return list.find(d => {
if (d.uid === val) {
if (debug) {
console.log(`> [debug] matched domain ${d.uid} by uid`)
}
return true
}
// Match prefix
if (d.name === toHost(val)) {
if (debug) {
console.log(`> [debug] matched domain ${d.uid} by name ${d.name}`)
}
return true
}
return false
})
}