#!/usr/bin/env node // Packages const chalk = require('chalk') const minimist = require('minimist') const table = require('text-table') const ms = require('ms') const printf = require('printf') require('epipebomb')() const supportsColor = require('supports-color') // Ours const strlen = require('../lib/strlen') const NowAlias = require('../lib/alias') const NowDomains = require('../lib/domains') const login = require('../lib/login') const cfg = require('../lib/cfg') const { handleError, error } = require('../lib/error') const toHost = require('../lib/to-host') const { reAlias } = require('../lib/re-alias') const exit = require('../lib/utils/exit') const info = require('../lib/utils/output/info') const logo = require('../lib/utils/output/logo') const promptBool = require('../lib/utils/input/prompt-bool') const argv = minimist(process.argv.slice(2), { string: ['config', 'token', 'rules'], boolean: ['help', 'debug'], alias: { help: 'h', config: 'c', rules: 'r', debug: 'd', token: 't' } }) const subcommand = argv._[0] const grayWidth = 10 const underlineWidth = 11 // Options const help = () => { console.log(` ${chalk.bold(`${logo} now alias`)} ${chalk.dim('Options:')} -h, --help Output usage information -c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline( 'FILE' )} Config file -r ${chalk.bold.underline('RULES_FILE')}, --rules=${chalk.bold.underline( 'RULES_FILE' )} Rules file -d, --debug Debug mode [off] -t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline( 'TOKEN' )} Login token ${chalk.dim('Examples:')} ${chalk.gray('–')} Lists all your aliases: ${chalk.cyan('$ now alias ls')} ${chalk.gray('–')} Adds a new alias to ${chalk.underline('my-api.now.sh')}: ${chalk.cyan( `$ now alias set ${chalk.underline( 'api-ownv3nc9f8.now.sh' )} ${chalk.underline('my-api.now.sh')}` )} The ${chalk.dim('`.now.sh`')} suffix can be ommited: ${chalk.cyan('$ now alias set api-ownv3nc9f8 my-api')} The deployment id can be used as the source: ${chalk.cyan('$ now alias set deploymentId my-alias')} Custom domains work as alias targets: ${chalk.cyan( `$ now alias set ${chalk.underline( 'api-ownv3nc9f8.now.sh' )} ${chalk.underline('my-api.com')}` )} ${chalk.dim('–')} The subcommand ${chalk.dim( '`set`' )} is the default and can be skipped. ${chalk.dim('–')} ${chalk.dim( '`http(s)://`' )} in the URLs is unneeded / ignored. ${chalk.gray('–')} Add and modify path based aliases for ${chalk.underline( 'zeit.ninja' )}: ${chalk.cyan( `$ now alias ${chalk.underline('zeit.ninja')} -r ${chalk.underline( 'rules.json' )}` )} Export effective routing rules: ${chalk.cyan( `$ now alias ls aliasId --json > ${chalk.underline('rules.json')}` )} ${chalk.cyan(`$ now alias ls zeit.ninja`)} ${chalk.gray('–')} Removing an alias: ${chalk.cyan('$ now alias rm aliasId')} To get the list of alias ids, use ${chalk.dim('`now alias ls`')}. ${chalk.dim('Alias:')} ln `) } // Options const debug = argv.debug const apiUrl = argv.url || 'https://api.zeit.co' if (argv.config) { cfg.setConfigFile(argv.config) } if (argv.help) { help() exit(0) } else { 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}`) 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) } }) .catch(err => { handleError(err) process.exit(1) }) } async function run({ token, config: { currentTeam, user } }) { const alias = new NowAlias({ apiUrl, token, debug, currentTeam }) const domains = new NowDomains({ apiUrl, token, debug, currentTeam }) const args = argv._.slice(1) switch (subcommand) { case 'ls': case 'list': { if (args.length === 1) { const list = await alias.listAliases() const item = list.find( e => e.uid === argv._[1] || e.alias === argv._[1] ) if (!item || !item.rules) { error(`Could not match path alias for: ${argv._[1]}`) return exit(1) } if (argv.json) { console.log(JSON.stringify({ rules: item.rules }, null, 2)) } else { const header = [ ['', 'pathname', 'method', 'dest'].map(s => chalk.dim(s)) ] const text = list.length === 0 ? null : table( header.concat( item.rules.map(rule => { return [ '', rule.pathname ? rule.pathname : '', rule.method ? rule.method : '*', rule.dest ] }) ), { align: ['l', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen } ) console.log(text) } break } else if (args.length !== 0) { error( `Invalid number of arguments. Usage: ${chalk.cyan('`now alias ls`')}` ) return exit(1) } const start_ = new Date() const aliases = await alias.ls() aliases.sort((a, b) => new Date(b.created) - new Date(a.created)) const current = new Date() const sourceUrlLength = aliases.reduce((acc, i) => { return Math.max(acc, (i.deployment && i.deployment.url.length) || 0) }, 0) + 9 const aliasLength = aliases.reduce((acc, i) => { return Math.max(acc, (i.alias && i.alias.length) || 0) }, 0) + 8 const elapsed_ = ms(new Date() - start_) console.log( `> ${aliases.length} alias${aliases.length === 1 ? '' : 'es'} found ${chalk.gray(`[${elapsed_}]`)} under ${chalk.bold( (currentTeam && currentTeam.slug) || user.username || user.email )}` ) console.log() if (supportsColor) { const urlSpecHeader = `%-${sourceUrlLength + 1}s` const aliasSpecHeader = `%-${aliasLength + 1}s` console.log( printf( ` ${chalk.gray(urlSpecHeader + ' ' + aliasSpecHeader + ' %5s')}`, 'source', 'url', 'age' ) ) } else { const urlSpecHeader = `%-${sourceUrlLength}s` const aliasSpecHeader = `%-${aliasLength}s` console.log( printf( ` ${urlSpecHeader} ${aliasSpecHeader} %5s`, 'source', 'url', 'age' ) ) } let text = '' aliases.forEach(_alias => { let urlSpec = sourceUrlLength let aliasSpec = aliasLength let ageSpec = 5 const _url = chalk.underline(_alias.alias) let _sourceUrl if (supportsColor) { aliasSpec += underlineWidth ageSpec += grayWidth } if (_alias.deployment) { _sourceUrl = chalk.underline(_alias.deployment.url) if (supportsColor) { urlSpec += grayWidth } } else if (_alias.rules) { _sourceUrl = chalk.gray( `[${_alias.rules.length} custom rule${_alias.rules.length > 1 ? 's' : ''}]` ) if (supportsColor) { urlSpec += underlineWidth } } else { _sourceUrl = chalk.gray('') } const time = chalk.gray(ms(current - new Date(_alias.created))) text += printf( ` %-${urlSpec}s %-${aliasSpec}s %${ageSpec}s\n`, _sourceUrl, _url, time ) }) console.log(text) break } case 'rm': case 'remove': { const _target = String(args[0]) if (!_target) { const err = new Error('No alias id specified') err.userError = true throw err } if (args.length !== 1) { error( `Invalid number of arguments. Usage: ${chalk.cyan( '`now alias rm `' )}` ) return exit(1) } const _aliases = await alias.ls() const _alias = findAlias(_target, _aliases) if (!_alias) { const err = new Error( `Alias not found by "${_target}" under ${chalk.bold( (currentTeam && currentTeam.slug) || user.username || user.email )}. Run ${chalk.dim('`now alias ls`')} to see your aliases.` ) err.userError = true throw err } try { const confirmation = await confirmDeploymentRemoval(alias, _alias) if (!confirmation) { info('Aborted') return process.exit(0) } const start = new Date() await alias.rm(_alias) const elapsed = ms(new Date() - start) console.log( `${chalk.cyan('> Success!')} Alias ${chalk.bold( _alias.uid )} removed [${elapsed}]` ) } catch (err) { error(err) exit(1) } break } case 'add': case 'set': { if (argv.rules) { await updatePathAlias(alias, argv._[0], argv.rules, domains) break } if (args.length !== 2) { error( `Invalid number of arguments. Usage: ${chalk.cyan( '`now alias set `' )}` ) return exit(1) } await alias.set( String(args[0]), String(args[1]), domains, currentTeam, user ) break } default: { if (argv._.length === 0) { await reAlias( token, null, null, help, exit, apiUrl, debug, alias, currentTeam, user ) break } if (argv.rules) { await updatePathAlias(alias, argv._[0], argv.rules, domains) break } if (argv._.length === 1) { await reAlias( token, null, String(argv._[0]), help, exit, apiUrl, debug, alias, currentTeam, user ) break } else if (argv._.length === 2) { await alias.set( String(argv._[0]), String(argv._[1]), domains, currentTeam, user ) } else if (argv._.length >= 3) { error('Invalid number of arguments') help() exit(1) } else { error('Please specify a valid subcommand: ls | set | rm') help() exit(1) } } } domains.close() alias.close() } async function confirmDeploymentRemoval(alias, _alias) { const time = chalk.gray(ms(new Date() - new Date(_alias.created)) + ' ago') const _sourceUrl = _alias.deployment ? chalk.underline(_alias.deployment.url) : null const tbl = table( [ [ _alias.uid, ...(_sourceUrl ? [_sourceUrl] : []), chalk.underline(_alias.alias), time ] ], { hsep: ' '.repeat(6) } ) const msg = '> The following alias will be removed permanently\n' + ` ${tbl} \nAre you sure?` return promptBool(msg, { trailing: '\n' }) } function findAlias(alias, list) { let key let val if (/\./.test(alias)) { val = toHost(alias) key = 'alias' } else { val = alias key = 'uid' } const _alias = list.find(d => { if (d[key] === val) { if (debug) { console.log(`> [debug] matched alias ${d.uid} by ${key} ${val}`) } return true } // Match prefix if (`${val}.now.sh` === d.alias) { if (debug) { console.log(`> [debug] matched alias ${d.uid} by url ${d.host}`) } return true } return false }) return _alias } async function updatePathAlias(alias, aliasName, rules, domains) { const start = new Date() const res = await alias.updatePathBasedroutes( String(aliasName), rules, domains ) const elapsed = ms(new Date() - start) if (res.error) { const err = new Error(res.error.message) err.userError = true throw err } else { console.log( `${chalk.cyan( '> Success!' )} ${res.ruleCount} rules configured for ${chalk.underline( res.alias )} [${elapsed}]` ) } }