6 changed files with 1186 additions and 249 deletions
@ -0,0 +1,389 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
// Native
|
|||
const path = require('path') |
|||
|
|||
// Packages
|
|||
const chalk = require('chalk') |
|||
const table = require('text-table') |
|||
const minimist = require('minimist') |
|||
const fs = require('fs-extra') |
|||
const ms = require('ms') |
|||
const printf = require('printf') |
|||
require('epipebomb')() |
|||
const supportsColor = require('supports-color') |
|||
|
|||
// Ours
|
|||
const cfg = require('../lib/cfg') |
|||
const { handleError, error } = require('../lib/error') |
|||
const NowCerts = require('../lib/certs') |
|||
const login = require('../lib/login') |
|||
const exit = require('../lib/utils/exit') |
|||
const logo = require('../lib/utils/output/logo') |
|||
|
|||
const help = () => { |
|||
console.log(` |
|||
${chalk.bold(`${logo} now certs`)} <ls | create | renew | replace | rm> <cn> |
|||
|
|||
${chalk.dim('Note:')} |
|||
|
|||
This command is intended for advanced use only, normally ${chalk.bold( |
|||
'now' |
|||
)} manages your certificates automatically. |
|||
|
|||
${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 |
|||
--crt ${chalk.bold.underline('FILE')} Certificate file |
|||
--key ${chalk.bold.underline('FILE')} Certificate key file |
|||
--ca ${chalk.bold.underline('FILE')} CA certificate chain file |
|||
|
|||
${chalk.dim('Examples:')} |
|||
|
|||
${chalk.gray('–')} Listing all your certificates: |
|||
|
|||
${chalk.cyan('$ now certs ls')} |
|||
|
|||
${chalk.gray('–')} Creating a new certificate: |
|||
|
|||
${chalk.cyan('$ now certs create domain.com')} |
|||
|
|||
${chalk.gray('–')} Renewing an existing certificate issued with ${chalk.bold( |
|||
'now' |
|||
)}: |
|||
|
|||
${chalk.cyan('$ now certs renew domain.com')} |
|||
|
|||
${chalk.gray( |
|||
'–' |
|||
)} Replacing an existing certificate with a user-supplied certificate: |
|||
|
|||
${chalk.cyan( |
|||
'$ now certs replace --crt domain.crt --key domain.key --ca ca_chain.crt domain.com' |
|||
)} |
|||
`)
|
|||
} |
|||
|
|||
// Options
|
|||
let argv |
|||
let debug |
|||
let apiUrl |
|||
let subcommand |
|||
|
|||
const main = async ctx => { |
|||
argv = minimist(ctx.argv.slice(2), { |
|||
string: ['config', 'token', 'crt', 'key', 'ca'], |
|||
boolean: ['help', 'debug'], |
|||
alias: { help: 'h', config: 'c', debug: 'd', token: 't' } |
|||
}) |
|||
|
|||
argv._ = argv._.slice(1) |
|||
|
|||
apiUrl = argv.url || 'https://api.zeit.co' |
|||
debug = argv.debug |
|||
subcommand = argv._[0] |
|||
|
|||
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) { |
|||
handleError(err) |
|||
exit(1) |
|||
} |
|||
} |
|||
|
|||
module.exports = async ctx => { |
|||
try { |
|||
await main(ctx) |
|||
} catch (err) { |
|||
handleError(err) |
|||
process.exit(1) |
|||
} |
|||
} |
|||
|
|||
function formatExpirationDate(date) { |
|||
const diff = date - Date.now() |
|||
return diff < 0 |
|||
? chalk.gray(ms(-diff) + ' ago') |
|||
: chalk.gray('in ' + ms(diff)) |
|||
} |
|||
|
|||
async function run({ token, config: { currentTeam, user } }) { |
|||
const certs = new NowCerts({ apiUrl, token, debug, currentTeam }) |
|||
const args = argv._.slice(1) |
|||
const start = Date.now() |
|||
|
|||
if (subcommand === 'ls' || subcommand === 'list') { |
|||
if (args.length !== 0) { |
|||
error( |
|||
`Invalid number of arguments. Usage: ${chalk.cyan('`now certs ls`')}` |
|||
) |
|||
return exit(1) |
|||
} |
|||
|
|||
const list = await certs.ls() |
|||
const elapsed = ms(new Date() - start) |
|||
|
|||
console.log( |
|||
`> ${list.length} certificate${list.length === 1 |
|||
? '' |
|||
: 's'} found ${chalk.gray(`[${elapsed}]`)} under ${chalk.bold( |
|||
(currentTeam && currentTeam.slug) || user.username || user.email |
|||
)}` |
|||
) |
|||
|
|||
if (list.length > 0) { |
|||
const cur = Date.now() |
|||
list.sort((a, b) => { |
|||
return a.cn.localeCompare(b.cn) |
|||
}) |
|||
|
|||
const maxCnLength = |
|||
list.reduce((acc, i) => { |
|||
return Math.max(acc, (i.cn && i.cn.length) || 0) |
|||
}, 0) + 1 |
|||
|
|||
console.log( |
|||
chalk.dim( |
|||
printf( |
|||
` %-${maxCnLength}s %-8s %-10s %-10s`, |
|||
'cn', |
|||
'created', |
|||
'expiration', |
|||
'auto-renew' |
|||
) |
|||
) |
|||
) |
|||
|
|||
list.forEach(cert => { |
|||
const cn = chalk.bold(cert.cn) |
|||
const time = chalk.gray(ms(cur - new Date(cert.created)) + ' ago') |
|||
const expiration = formatExpirationDate(new Date(cert.expiration)) |
|||
const autoRenew = cert.autoRenew ? 'yes' : 'no' |
|||
let spec |
|||
if (supportsColor) { |
|||
spec = ` %-${maxCnLength + 9}s %-18s %-20s %-20s\n` |
|||
} else { |
|||
spec = ` %-${maxCnLength}s %-8s %-10s %-10s\n` |
|||
} |
|||
process.stdout.write(printf(spec, cn, time, expiration, autoRenew)) |
|||
}) |
|||
} |
|||
} else if (subcommand === 'create') { |
|||
if (args.length !== 1) { |
|||
error( |
|||
`Invalid number of arguments. Usage: ${chalk.cyan( |
|||
'`now certs create <cn>`' |
|||
)}` |
|||
) |
|||
return exit(1) |
|||
} |
|||
const cn = args[0] |
|||
let cert |
|||
|
|||
if (argv.crt || argv.key || argv.ca) { |
|||
// Issue a custom certificate
|
|||
if (!argv.crt || !argv.key) { |
|||
error( |
|||
`Missing required arguments for a custom certificate entry. Usage: ${chalk.cyan( |
|||
'`now certs create --crt DOMAIN.CRT --key DOMAIN.KEY [--ca CA.CRT] <id | cn>`' |
|||
)}` |
|||
) |
|||
return exit(1) |
|||
} |
|||
|
|||
const crt = readX509File(argv.crt) |
|||
const key = readX509File(argv.key) |
|||
const ca = argv.ca ? readX509File(argv.ca) : '' |
|||
|
|||
cert = await certs.put(cn, crt, key, ca) |
|||
} else { |
|||
// Issue a standard certificate
|
|||
cert = await certs.create(cn) |
|||
} |
|||
if (!cert) { |
|||
// Cert is undefined and "Cert is already issued" has been printed to stdout
|
|||
return exit(1) |
|||
} |
|||
const elapsed = ms(new Date() - start) |
|||
console.log( |
|||
`${chalk.cyan('> Success!')} Certificate entry ${chalk.bold( |
|||
cn |
|||
)} ${chalk.gray(`(${cert.uid})`)} created ${chalk.gray(`[${elapsed}]`)}` |
|||
) |
|||
} else if (subcommand === 'renew') { |
|||
if (args.length !== 1) { |
|||
error( |
|||
`Invalid number of arguments. Usage: ${chalk.cyan( |
|||
'`now certs renew <id | cn>`' |
|||
)}` |
|||
) |
|||
return exit(1) |
|||
} |
|||
|
|||
const cert = await getCertIdCn(certs, args[0], currentTeam, user) |
|||
if (!cert) { |
|||
return exit(1) |
|||
} |
|||
const yes = await readConfirmation( |
|||
cert, |
|||
'The following certificate will be renewed\n' |
|||
) |
|||
|
|||
if (!yes) { |
|||
error('User abort') |
|||
return exit(0) |
|||
} |
|||
|
|||
await certs.renew(cert.cn) |
|||
const elapsed = ms(new Date() - start) |
|||
console.log( |
|||
`${chalk.cyan('> Success!')} Certificate ${chalk.bold( |
|||
cert.cn |
|||
)} ${chalk.gray(`(${cert.uid})`)} renewed ${chalk.gray(`[${elapsed}]`)}` |
|||
) |
|||
} else if (subcommand === 'replace') { |
|||
if (!argv.crt || !argv.key) { |
|||
error( |
|||
`Invalid number of arguments. Usage: ${chalk.cyan( |
|||
'`now certs replace --crt DOMAIN.CRT --key DOMAIN.KEY [--ca CA.CRT] <id | cn>`' |
|||
)}` |
|||
) |
|||
return exit(1) |
|||
} |
|||
|
|||
const crt = readX509File(argv.crt) |
|||
const key = readX509File(argv.key) |
|||
const ca = argv.ca ? readX509File(argv.ca) : '' |
|||
|
|||
const cert = await getCertIdCn(certs, args[0], currentTeam, user) |
|||
if (!cert) { |
|||
return exit(1) |
|||
} |
|||
const yes = await readConfirmation( |
|||
cert, |
|||
'The following certificate will be replaced permanently\n' |
|||
) |
|||
if (!yes) { |
|||
error('User abort') |
|||
return exit(0) |
|||
} |
|||
|
|||
await certs.put(cert.cn, crt, key, ca) |
|||
const elapsed = ms(new Date() - start) |
|||
console.log( |
|||
`${chalk.cyan('> Success!')} Certificate ${chalk.bold( |
|||
cert.cn |
|||
)} ${chalk.gray(`(${cert.uid})`)} replaced ${chalk.gray(`[${elapsed}]`)}` |
|||
) |
|||
} else if (subcommand === 'rm' || subcommand === 'remove') { |
|||
if (args.length !== 1) { |
|||
error( |
|||
`Invalid number of arguments. Usage: ${chalk.cyan( |
|||
'`now certs rm <id | cn>`' |
|||
)}` |
|||
) |
|||
return exit(1) |
|||
} |
|||
|
|||
const cert = await getCertIdCn(certs, args[0], currentTeam, user) |
|||
if (!cert) { |
|||
return exit(1) |
|||
} |
|||
const yes = await readConfirmation( |
|||
cert, |
|||
'The following certificate will be removed permanently\n' |
|||
) |
|||
if (!yes) { |
|||
error('User abort') |
|||
return exit(0) |
|||
} |
|||
|
|||
await certs.delete(cert.cn) |
|||
const elapsed = ms(new Date() - start) |
|||
console.log( |
|||
`${chalk.cyan('> Success!')} Certificate ${chalk.bold( |
|||
cert.cn |
|||
)} ${chalk.gray(`(${cert.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}` |
|||
) |
|||
} else { |
|||
error( |
|||
'Please specify a valid subcommand: ls | create | renew | replace | rm' |
|||
) |
|||
help() |
|||
exit(1) |
|||
} |
|||
return certs.close() |
|||
} |
|||
|
|||
process.on('uncaughtException', err => { |
|||
handleError(err) |
|||
exit(1) |
|||
}) |
|||
|
|||
function readConfirmation(cert, msg) { |
|||
return new Promise(resolve => { |
|||
const time = chalk.gray(ms(new Date() - new Date(cert.created)) + ' ago') |
|||
const tbl = table([[cert.uid, chalk.bold(cert.cn), time]], { |
|||
align: ['l', 'r', 'l'], |
|||
hsep: ' '.repeat(6) |
|||
}) |
|||
|
|||
process.stdout.write(`> ${msg}`) |
|||
process.stdout.write(' ' + tbl + '\n') |
|||
|
|||
process.stdout.write( |
|||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}` |
|||
) |
|||
|
|||
process.stdin |
|||
.on('data', d => { |
|||
process.stdin.pause() |
|||
resolve(d.toString().trim().toLowerCase() === 'y') |
|||
}) |
|||
.resume() |
|||
}) |
|||
} |
|||
|
|||
function readX509File(file) { |
|||
return fs.readFileSync(path.resolve(file), 'utf8') |
|||
} |
|||
|
|||
async function getCertIdCn(certs, idOrCn, currentTeam, user) { |
|||
const list = await certs.ls() |
|||
const thecert = list.filter(cert => { |
|||
return cert.uid === idOrCn || cert.cn === idOrCn |
|||
})[0] |
|||
|
|||
if (!thecert) { |
|||
error( |
|||
`No certificate found by id or cn "${idOrCn}" under ${chalk.bold( |
|||
(currentTeam && currentTeam.slug) || user.username || user.email |
|||
)}` |
|||
) |
|||
return null |
|||
} |
|||
|
|||
return thecert |
|||
} |
@ -0,0 +1,349 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
// Packages
|
|||
const chalk = require('chalk') |
|||
const minimist = require('minimist') |
|||
const ms = require('ms') |
|||
const table = require('text-table') |
|||
|
|||
// Ours
|
|||
const cfg = require('../lib/cfg') |
|||
const DomainRecords = require('../lib/domain-records') |
|||
const indent = require('../lib/indent') |
|||
const login = require('../lib/login') |
|||
const strlen = require('../lib/strlen') |
|||
const { handleError, error } = require('../lib/error') |
|||
const exit = require('../lib/utils/exit') |
|||
const logo = require('../lib/utils/output/logo') |
|||
|
|||
const help = () => { |
|||
console.log(` |
|||
${chalk.bold(`${logo} now dns ls`)} [domain] |
|||
${chalk.bold( |
|||
`${logo} now dns add` |
|||
)} <domain> <name> <A | AAAA | ALIAS | CNAME | TXT> <value> |
|||
${chalk.bold(`${logo} now dns add`)} <domain> <name> MX <value> <mx_priority> |
|||
${chalk.bold( |
|||
`${logo} now dns add` |
|||
)} <domain> <name> SRV <priority> <weight> <port> <target> |
|||
${chalk.bold(`${logo} now dns rm`)} <id> |
|||
|
|||
${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 all your DNS records |
|||
|
|||
${chalk.cyan('$ now dns ls')} |
|||
|
|||
${chalk.gray('–')} Add an A record for a subdomain |
|||
|
|||
${chalk.cyan( |
|||
'$ now dns add <YOUR DOMAIN> <SUBDOMAIN NAME> A <RECORD VALUE>' |
|||
)} |
|||
${chalk.cyan('$ now dns add zeit.rocks api A 198.51.100.100')} |
|||
|
|||
${chalk.gray('–')} Add an MX record (@ as a name refers to the domain) |
|||
|
|||
${chalk.cyan( |
|||
'$ now dns add <YOUR DOMAIN> @ MX <RECORD VALUE> <PRIORITY>' |
|||
)} |
|||
${chalk.cyan('$ now dns add zeit.rocks @ MX mail.zeit.rocks 10')} |
|||
`)
|
|||
} |
|||
|
|||
// Options
|
|||
let argv |
|||
let debug |
|||
let apiUrl |
|||
let subcommand |
|||
|
|||
const main = async ctx => { |
|||
argv = minimist(ctx.argv.slice(2), { |
|||
string: ['config'], |
|||
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' |
|||
subcommand = argv._[0] |
|||
|
|||
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) { |
|||
handleError(err) |
|||
exit(1) |
|||
} |
|||
} |
|||
|
|||
module.exports = async ctx => { |
|||
try { |
|||
await main(ctx) |
|||
} catch (err) { |
|||
handleError(err) |
|||
process.exit(1) |
|||
} |
|||
} |
|||
|
|||
async function run({ token, config: { currentTeam, user } }) { |
|||
const domainRecords = new DomainRecords({ apiUrl, token, debug, currentTeam }) |
|||
const args = argv._.slice(1) |
|||
const start = Date.now() |
|||
|
|||
if (subcommand === 'ls' || subcommand === 'list') { |
|||
if (args.length > 1) { |
|||
error( |
|||
`Invalid number of arguments. Usage: ${chalk.cyan( |
|||
'`now dns ls [domain]`' |
|||
)}` |
|||
) |
|||
return exit(1) |
|||
} |
|||
|
|||
const elapsed = ms(new Date() - start) |
|||
const res = await domainRecords.ls(args[0]) |
|||
const text = [] |
|||
let count = 0 |
|||
res.forEach((records, domain) => { |
|||
count += records.length |
|||
if (records.length > 0) { |
|||
const cur = Date.now() |
|||
const header = [ |
|||
['', 'id', 'name', 'type', 'value', 'aux', 'created'].map(s => |
|||
chalk.dim(s) |
|||
) |
|||
] |
|||
const out = table( |
|||
header.concat( |
|||
records.map(record => { |
|||
const time = chalk.gray( |
|||
ms(cur - new Date(Number(record.created))) + ' ago' |
|||
) |
|||
const aux = (() => { |
|||
if (record.mxPriority !== undefined) return record.mxPriority |
|||
if (record.priority !== undefined) return record.priority |
|||
return '' |
|||
})() |
|||
return [ |
|||
'', |
|||
record.id, |
|||
record.name, |
|||
record.type, |
|||
record.value, |
|||
aux, |
|||
time |
|||
] |
|||
}) |
|||
), |
|||
{ |
|||
align: ['l', 'r', 'l', 'l', 'l', 'l'], |
|||
hsep: ' '.repeat(2), |
|||
stringLength: strlen |
|||
} |
|||
) |
|||
text.push(`\n\n${chalk.bold(domain)}\n${indent(out, 2)}`) |
|||
} |
|||
}) |
|||
console.log( |
|||
`> ${count} record${count === 1 ? '' : 's'} found ${chalk.gray( |
|||
`[${elapsed}]` |
|||
)} under ${chalk.bold( |
|||
(currentTeam && currentTeam.slug) || user.username || user.email |
|||
)}` |
|||
) |
|||
console.log(text.join('')) |
|||
} else if (subcommand === 'add') { |
|||
const param = parseAddArgs(args) |
|||
if (!param) { |
|||
error( |
|||
`Invalid number of arguments. See: ${chalk.cyan( |
|||
'`now dns --help`' |
|||
)} for usage.` |
|||
) |
|||
return exit(1) |
|||
} |
|||
const record = await domainRecords.create(param.domain, param.data) |
|||
const elapsed = ms(new Date() - start) |
|||
console.log( |
|||
`${chalk.cyan('> Success!')} A new DNS record for domain ${chalk.bold( |
|||
param.domain |
|||
)} ${chalk.gray(`(${record.uid})`)} created ${chalk.gray( |
|||
`[${elapsed}]` |
|||
)} (${chalk.bold( |
|||
(currentTeam && currentTeam.slug) || user.username || user.email |
|||
)})` |
|||
) |
|||
} else if (subcommand === 'rm' || subcommand === 'remove') { |
|||
if (args.length !== 1) { |
|||
error( |
|||
`Invalid number of arguments. Usage: ${chalk.cyan('`now dns rm <id>`')}` |
|||
) |
|||
return exit(1) |
|||
} |
|||
|
|||
const record = await domainRecords.getRecord(args[0]) |
|||
if (!record) { |
|||
error('DNS record not found') |
|||
return exit(1) |
|||
} |
|||
|
|||
const yes = await readConfirmation( |
|||
record, |
|||
'The following record will be removed permanently \n' |
|||
) |
|||
if (!yes) { |
|||
error('User abort') |
|||
return exit(0) |
|||
} |
|||
|
|||
await domainRecords.delete(record.domain, record.id) |
|||
const elapsed = ms(new Date() - start) |
|||
console.log( |
|||
`${chalk.cyan('> Success!')} Record ${chalk.gray( |
|||
`${record.id}` |
|||
)} removed ${chalk.gray(`[${elapsed}]`)}` |
|||
) |
|||
} else { |
|||
error('Please specify a valid subcommand: ls | add | rm') |
|||
help() |
|||
exit(1) |
|||
} |
|||
return domainRecords.close() |
|||
} |
|||
|
|||
process.on('uncaughtException', err => { |
|||
handleError(err) |
|||
exit(1) |
|||
}) |
|||
|
|||
function parseAddArgs(args) { |
|||
if (!args || args.length < 4) { |
|||
return null |
|||
} |
|||
|
|||
const domain = args[0] |
|||
const name = args[1] === '@' ? '' : args[1].toString() |
|||
const type = args[2] |
|||
const value = args[3] |
|||
|
|||
if (!(domain && typeof name === 'string' && type)) { |
|||
return null |
|||
} |
|||
|
|||
if (type === 'MX') { |
|||
if (args.length !== 5) { |
|||
return null |
|||
} |
|||
|
|||
return { |
|||
domain, |
|||
data: { |
|||
name, |
|||
type, |
|||
value, |
|||
mxPriority: args[4] |
|||
} |
|||
} |
|||
} else if (type === 'SRV') { |
|||
if (args.length !== 7) { |
|||
return null |
|||
} |
|||
|
|||
return { |
|||
domain, |
|||
data: { |
|||
name, |
|||
type, |
|||
srv: { |
|||
priority: value, |
|||
weight: args[4], |
|||
port: args[5], |
|||
target: args[6] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (args.length !== 4) { |
|||
return null |
|||
} |
|||
|
|||
return { |
|||
domain, |
|||
data: { |
|||
name, |
|||
type, |
|||
value |
|||
} |
|||
} |
|||
} |
|||
|
|||
function readConfirmation(record, msg) { |
|||
return new Promise(resolve => { |
|||
const time = chalk.gray( |
|||
ms(new Date() - new Date(Number(record.created))) + ' ago' |
|||
) |
|||
const tbl = table( |
|||
[ |
|||
[ |
|||
record.id, |
|||
chalk.bold( |
|||
`${record.name.length > 0 |
|||
? record.name + '.' |
|||
: ''}${record.domain} ${record.type} ${record.value} ${record.mxPriority |
|||
? record.mxPriority |
|||
: ''}` |
|||
), |
|||
time |
|||
] |
|||
], |
|||
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) } |
|||
) |
|||
|
|||
process.stdout.write(`> ${msg}`) |
|||
process.stdout.write(' ' + tbl + '\n') |
|||
|
|||
process.stdout.write( |
|||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}` |
|||
) |
|||
|
|||
process.stdin |
|||
.on('data', d => { |
|||
process.stdin.pause() |
|||
resolve(d.toString().trim().toLowerCase() === 'y') |
|||
}) |
|||
.resume() |
|||
}) |
|||
} |
@ -0,0 +1,432 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
// Native
|
|||
const { resolve } = require('path') |
|||
|
|||
// Packages
|
|||
const chalk = require('chalk') |
|||
const minimist = require('minimist') |
|||
const ms = require('ms') |
|||
const psl = require('psl') |
|||
const table = require('text-table') |
|||
|
|||
// Ours
|
|||
const NowDomains = require('../lib/domains') |
|||
const cfg = require('../lib/cfg') |
|||
const exit = require('../lib/utils/exit') |
|||
const login = require('../lib/login') |
|||
const logo = require('../lib/utils/output/logo') |
|||
const promptBool = require('../lib/utils/input/prompt-bool') |
|||
const strlen = require('../lib/strlen') |
|||
const toHost = require('../lib/to-host') |
|||
const { handleError, error } = require('../lib/error') |
|||
|
|||
const help = () => { |
|||
console.log(` |
|||
${chalk.bold(`${logo} now domains`)} <ls | add | rm | buy> <domain> |
|||
|
|||
${chalk.dim('Options:')} |
|||
|
|||
-h, --help Output usage information |
|||
-d, --debug Debug mode [off] |
|||
-e, --external Use external DNS server |
|||
-f, --force Skip DNS verification |
|||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( |
|||
'TOKEN' |
|||
)} Login token |
|||
|
|||
${chalk.dim('Examples:')} |
|||
|
|||
${chalk.gray('–')} Lists all your domains: |
|||
|
|||
${chalk.cyan('$ now domains ls')} |
|||
|
|||
${chalk.gray('–')} Buy a new domain: |
|||
|
|||
${chalk.cyan(`$ now domains buy ${chalk.underline('domain-name.com')}`)} |
|||
|
|||
${chalk.gray('–')} Adds a domain name: |
|||
|
|||
${chalk.cyan(`$ now domains add ${chalk.underline('domain-name.com')}`)} |
|||
|
|||
Make sure the domain's DNS nameservers are at least 2 of these: |
|||
|
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'california.zeit.world' |
|||
)} ${chalk.dim('173.255.215.107')} |
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'london.zeit.world' |
|||
)} ${chalk.dim('178.62.47.76')} |
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'newark.zeit.world' |
|||
)} ${chalk.dim('173.255.231.87')} |
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'amsterdam.zeit.world' |
|||
)} ${chalk.dim('188.226.197.55')} |
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'dallas.zeit.world' |
|||
)} ${chalk.dim('173.192.101.194')} |
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'paris.zeit.world' |
|||
)} ${chalk.dim('37.123.115.172')} |
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'singapore.zeit.world' |
|||
)} ${chalk.dim('119.81.97.170')} |
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'sydney.zeit.world' |
|||
)} ${chalk.dim('52.64.171.200')} |
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'frankfurt.zeit.world' |
|||
)} ${chalk.dim('91.109.245.139')} |
|||
${chalk.gray('–')} ${chalk.underline( |
|||
'iowa.zeit.world' |
|||
)} ${chalk.dim('23.236.59.22')} |
|||
|
|||
${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`' |
|||
)}). |
|||
|
|||
For more details head to ${chalk.underline('https://zeit.world')}. |
|||
|
|||
${chalk.gray('–')} Removing a domain: |
|||
|
|||
${chalk.cyan('$ now domain rm my-app.com')} |
|||
|
|||
or |
|||
|
|||
${chalk.cyan('$ now domain rm domainId')} |
|||
|
|||
To get the list of domain ids, use ${chalk.dim('`now domains ls`')}. |
|||
|
|||
${chalk.gray( |
|||
'–' |
|||
)} Adding and verifying a domain name using zeit.world nameservers: |
|||
|
|||
${chalk.cyan('$ now domain add my-app.com')} |
|||
|
|||
The command will tell you if the domain was verified succesfully. In case the domain was not verified succesfully you should retry adding the domain after some time. |
|||
|
|||
${chalk.gray( |
|||
'–' |
|||
)} Adding and verifying a domain name using an external nameserver: |
|||
|
|||
${chalk.cyan('$ now domain add -e my-app.com')} |
|||
|
|||
and follow the verification instructions if requested. Finally, rerun the same command after completing the verification step. |
|||
`)
|
|||
} |
|||
|
|||
// Options
|
|||
let argv |
|||
let debug |
|||
let apiUrl |
|||
let subcommand |
|||
|
|||
const main = async ctx => { |
|||
argv = minimist(ctx.argv.slice(2), { |
|||
string: ['coupon', 'token'], |
|||
boolean: ['help', 'debug', 'external', 'force'], |
|||
alias: { |
|||
help: 'h', |
|||
coupon: 'c', |
|||
debug: 'd', |
|||
external: 'e', |
|||
force: 'f', |
|||
token: 't' |
|||
} |
|||
}) |
|||
|
|||
argv._ = argv._.slice(1) |
|||
|
|||
debug = argv.debug |
|||
apiUrl = argv.url || 'https://api.zeit.co' |
|||
subcommand = argv._[0] |
|||
|
|||
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}\n${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, user } }) { |
|||
const domain = new NowDomains({ apiUrl, token, debug, currentTeam }) |
|||
const args = argv._.slice(1) |
|||
|
|||
switch (subcommand) { |
|||
case 'ls': |
|||
case 'list': { |
|||
if (args.length !== 0) { |
|||
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( |
|||
`> ${domains.length} domain${domains.length === 1 |
|||
? '' |
|||
: 's'} 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) { |
|||
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') |
|||
process.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.uid |
|||
)} removed [${elapsed}]` |
|||
) |
|||
} catch (err) { |
|||
error(err) |
|||
exit(1) |
|||
} |
|||
break |
|||
} |
|||
case 'add': |
|||
case 'set': { |
|||
if (args.length !== 1) { |
|||
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 require(resolve(__dirname, 'domains', 'buy.js'))({ |
|||
domains: domain, |
|||
args, |
|||
currentTeam, |
|||
user, |
|||
coupon: argv.coupon |
|||
}) |
|||
break |
|||
} |
|||
default: |
|||
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.underline(`https://${_domain.name}`), time]], { |
|||
align: ['r', 'l'], |
|||
hsep: ' '.repeat(6) |
|||
}) |
|||
|
|||
process.stdout.write('> The following domain will be removed permanently\n') |
|||
process.stdout.write(' ' + tbl + '\n') |
|||
|
|||
if (_domain.aliases.length > 0) { |
|||
process.stdout.write( |
|||
`> ${chalk.yellow('Warning!')} This domain's ` + |
|||
`${chalk.bold( |
|||
_domain.aliases.length + |
|||
' alias' + |
|||
(_domain.aliases.length === 1 ? '' : 'es') |
|||
)} ` +
|
|||
`will be removed. Run ${chalk.dim('`now alias ls`')} to list them.\n` |
|||
) |
|||
} |
|||
if (_domain.certs.length > 0) { |
|||
process.stdout.write( |
|||
`> ${chalk.yellow('Warning!')} This domain's ` + |
|||
`${chalk.bold( |
|||
_domain.certs.length + |
|||
' certificate' + |
|||
(_domain.certs.length === 1 ? '' : 's') |
|||
)} ` +
|
|||
`will be removed. Run ${chalk.dim('`now cert ls`')} to list them.\n` |
|||
) |
|||
} |
|||
|
|||
process.stdout.write( |
|||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}` |
|||
) |
|||
|
|||
process.stdin |
|||
.on('data', d => { |
|||
process.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 |
|||
}) |
|||
} |
@ -1,247 +0,0 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
// Packages
|
|||
const minimist = require('minimist') |
|||
const chalk = require('chalk') |
|||
const ms = require('ms') |
|||
const printf = require('printf') |
|||
require('epipebomb')() |
|||
const supportsColor = require('supports-color') |
|||
|
|||
// Ours
|
|||
const Now = require('../lib') |
|||
const login = require('../lib/login') |
|||
const cfg = require('../lib/cfg') |
|||
const { handleError, error } = require('../lib/error') |
|||
const logo = require('../lib/utils/output/logo') |
|||
const sort = require('../lib/sort-deployments') |
|||
|
|||
const argv = minimist(process.argv.slice(2), { |
|||
string: ['config', 'token'], |
|||
boolean: ['help', 'debug', 'all'], |
|||
alias: { |
|||
help: 'h', |
|||
config: 'c', |
|||
debug: 'd', |
|||
token: 't' |
|||
} |
|||
}) |
|||
|
|||
const help = () => { |
|||
console.log(` |
|||
${chalk.bold(`${logo} now list`)} [app] |
|||
|
|||
${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 all deployments |
|||
|
|||
${chalk.cyan('$ now ls')} |
|||
|
|||
${chalk.gray('–')} List all deployments for the app ${chalk.dim('`my-app`')} |
|||
|
|||
${chalk.cyan('$ now ls my-app')} |
|||
|
|||
${chalk.dim('Alias:')} ls |
|||
`)
|
|||
} |
|||
|
|||
if (argv.help) { |
|||
help() |
|||
process.exit(0) |
|||
} |
|||
|
|||
const app = argv._[0] |
|||
|
|||
// Options
|
|||
const debug = argv.debug |
|||
const apiUrl = argv.url || 'https://api.zeit.co' |
|||
|
|||
if (argv.config) { |
|||
cfg.setConfigFile(argv.config) |
|||
} |
|||
|
|||
Promise.resolve() |
|||
.then(async () => { |
|||
const config = await cfg.read({ token: argv.token }) |
|||
|
|||
let token |
|||
try { |
|||
token = config.token || (await login(apiUrl)) |
|||
} catch (err) { |
|||
error(`Authentication error – ${err.message}`) |
|||
process.exit(1) |
|||
} |
|||
|
|||
if (!config.token) { |
|||
console.log( |
|||
`> Logged in successfully. Token saved to ${chalk.bold('~/.now.json')}.` |
|||
) |
|||
process.exit(0) |
|||
} |
|||
|
|||
try { |
|||
await list({ token, config }) |
|||
} catch (err) { |
|||
error(`Unknown error: ${err}\n${err.stack}`) |
|||
process.exit(1) |
|||
} |
|||
}) |
|||
.catch(err => { |
|||
handleError(err) |
|||
process.exit(1) |
|||
}) |
|||
|
|||
async function list({ token, config: { currentTeam, user } }) { |
|||
const now = new Now({ apiUrl, token, debug, currentTeam }) |
|||
const start = new Date() |
|||
|
|||
if (argv.all && !app) { |
|||
console.log('> You must define an app when using `--all`') |
|||
process.exit(1) |
|||
} |
|||
let deployments |
|||
try { |
|||
deployments = await now.list(app) |
|||
} catch (err) { |
|||
handleError(err) |
|||
process.exit(1) |
|||
} |
|||
|
|||
if (!deployments || (Array.isArray(deployments) && deployments.length <= 0)) { |
|||
const match = await now.findDeployment(app) |
|||
if (match !== null && typeof match !== 'undefined') { |
|||
deployments = Array.of(match) |
|||
} |
|||
} |
|||
if (!deployments || (Array.isArray(deployments) && deployments.length <= 0)) { |
|||
const aliases = await now.listAliases() |
|||
|
|||
const item = aliases.find(e => e.uid === app || e.alias === app) |
|||
if (item) { |
|||
const match = await now.findDeployment(item.deploymentId) |
|||
if (match !== null && typeof match !== 'undefined') { |
|||
deployments = Array.of(match) |
|||
} |
|||
} |
|||
} |
|||
|
|||
now.close() |
|||
|
|||
const apps = new Map() |
|||
|
|||
if (argv.all) { |
|||
await Promise.all( |
|||
deployments.map(async ({ uid }, i) => { |
|||
deployments[i].instances = await now.listInstances(uid) |
|||
}) |
|||
) |
|||
} |
|||
|
|||
for (const dep of deployments) { |
|||
const deps = apps.get(dep.name) || [] |
|||
apps.set(dep.name, deps.concat(dep)) |
|||
} |
|||
|
|||
const sorted = await sort([...apps]) |
|||
|
|||
const urlLength = |
|||
deployments.reduce((acc, i) => { |
|||
return Math.max(acc, (i.url && i.url.length) || 0) |
|||
}, 0) + 5 |
|||
const timeNow = new Date() |
|||
console.log( |
|||
`> ${deployments.length} deployment${deployments.length === 1 |
|||
? '' |
|||
: 's'} found under ${chalk.bold( |
|||
(currentTeam && currentTeam.slug) || user.username || user.email |
|||
)} ${chalk.grey('[' + ms(timeNow - start) + ']')}` |
|||
) |
|||
|
|||
let shouldShowAllInfo = false |
|||
for (const app of apps) { |
|||
shouldShowAllInfo = |
|||
app[1].length > 5 || |
|||
app.find(depl => { |
|||
return depl.scale && depl.scale.current > 1 |
|||
}) |
|||
if (shouldShowAllInfo) { |
|||
break |
|||
} |
|||
} |
|||
if (!argv.all && shouldShowAllInfo) { |
|||
console.log( |
|||
`> To expand the list and see instances run ${chalk.cyan( |
|||
'`now ls --all [app]`' |
|||
)}` |
|||
) |
|||
} |
|||
console.log() |
|||
sorted.forEach(([name, deps]) => { |
|||
const listedDeployments = argv.all ? deps : deps.slice(0, 5) |
|||
console.log( |
|||
`${chalk.bold(name)} ${chalk.gray( |
|||
'(' + listedDeployments.length + ' of ' + deps.length + ' total)' |
|||
)}` |
|||
) |
|||
const urlSpec = `%-${urlLength}s` |
|||
console.log( |
|||
printf( |
|||
` ${chalk.grey(urlSpec + ' %8s %-16s %8s')}`, |
|||
'url', |
|||
'inst #', |
|||
'state', |
|||
'age' |
|||
) |
|||
) |
|||
listedDeployments.forEach(dep => { |
|||
let state = dep.state |
|||
let extraSpaceForState = 0 |
|||
if (state === null || typeof state === 'undefined') { |
|||
state = 'DEPLOYMENT_ERROR' |
|||
} |
|||
if (/ERROR/.test(state)) { |
|||
state = chalk.red(state) |
|||
extraSpaceForState = 10 |
|||
} else if (state === 'FROZEN') { |
|||
state = chalk.grey(state) |
|||
extraSpaceForState = 10 |
|||
} |
|||
let spec |
|||
if (supportsColor) { |
|||
spec = ` %-${urlLength + 10}s %8s %-${extraSpaceForState + 16}s %8s` |
|||
} else { |
|||
spec = ` %-${urlLength + 1}s %8s %-${16}s %8s` |
|||
} |
|||
|
|||
console.log( |
|||
printf( |
|||
spec, |
|||
chalk.underline(dep.url), |
|||
dep.scale ? dep.scale.current : '✖', |
|||
state, |
|||
ms(timeNow - dep.created) |
|||
) |
|||
) |
|||
if (Array.isArray(dep.instances) && dep.instances.length > 0) { |
|||
dep.instances.forEach(i => { |
|||
console.log( |
|||
printf(` %-${urlLength + 10}s`, ` - ${chalk.underline(i.url)}`) |
|||
) |
|||
}) |
|||
console.log() |
|||
} |
|||
}) |
|||
console.log() |
|||
}) |
|||
} |
Loading…
Reference in new issue