Browse Source

Prettified everything

master
Leo Lamprecht 8 years ago
parent
commit
4119c17abe
  1. 454
      bin/now-alias.js
  2. 192
      bin/now-billing-add.js
  3. 340
      bin/now-billing.js
  4. 399
      bin/now-certs.js
  5. 690
      bin/now-deploy.js
  6. 325
      bin/now-dns.js
  7. 376
      bin/now-domains.js
  8. 193
      bin/now-list.js
  9. 156
      bin/now-open.js
  10. 214
      bin/now-remove.js
  11. 305
      bin/now-secrets.js
  12. 237
      bin/now-upgrade.js
  13. 122
      bin/now.js
  14. 56
      lib/agent.js
  15. 569
      lib/alias.js
  16. 145
      lib/build-logger.js
  17. 69
      lib/certs.js
  18. 26
      lib/cfg.js
  19. 12
      lib/copy.js
  20. 61
      lib/credit-cards.js
  21. 12
      lib/dns.js
  22. 125
      lib/domain-records.js
  23. 70
      lib/domains.js
  24. 34
      lib/error.js
  25. 18
      lib/errors.js
  26. 263
      lib/get-files.js
  27. 208
      lib/git.js
  28. 60
      lib/hash.js
  29. 2
      lib/ignored.js
  30. 4
      lib/indent.js
  31. 824
      lib/index.js
  32. 31
      lib/is-zeit-world.js
  33. 133
      lib/login.js
  34. 12
      lib/logs.js
  35. 8
      lib/pkg.js
  36. 43
      lib/plans.js
  37. 82
      lib/re-alias.js
  38. 190
      lib/read-metadata.js
  39. 80
      lib/secrets.js
  40. 4
      lib/strlen.js
  41. 30
      lib/test.js
  42. 8
      lib/to-host.js
  43. 6
      lib/ua.js
  44. 48
      lib/utils/billing/geocode.js
  45. 44
      lib/utils/check-path.js
  46. 4
      lib/utils/exit.js
  47. 106
      lib/utils/input/list.js
  48. 75
      lib/utils/input/prompt-bool.js
  49. 294
      lib/utils/input/text.js
  50. 7
      lib/utils/output/cmd.js
  51. 7
      lib/utils/output/code.js
  52. 7
      lib/utils/output/error.js
  53. 7
      lib/utils/output/info.js
  54. 2
      lib/utils/output/logo.js
  55. 7
      lib/utils/output/param.js
  56. 12
      lib/utils/output/stamp.js
  57. 6
      lib/utils/output/success.js
  58. 6
      lib/utils/output/uid.js
  59. 20
      lib/utils/output/wait.js
  60. 42
      lib/utils/prompt-options.js
  61. 10
      lib/utils/to-human-path.js

454
bin/now-alias.js

@ -1,345 +1,397 @@
#!/usr/bin/env node #!/usr/bin/env node
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
const minimist = require('minimist') const minimist = require("minimist");
const table = require('text-table') const table = require("text-table");
const ms = require('ms') const ms = require("ms");
// Ours // Ours
const strlen = require('../lib/strlen') const strlen = require("../lib/strlen");
const NowAlias = require('../lib/alias') const NowAlias = require("../lib/alias");
const login = require('../lib/login') const login = require("../lib/login");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const {error} = require('../lib/error') const { error } = require("../lib/error");
const toHost = require('../lib/to-host') const toHost = require("../lib/to-host");
const {reAlias} = require('../lib/re-alias') const { reAlias } = require("../lib/re-alias");
const exit = require('../lib/utils/exit') const exit = require("../lib/utils/exit");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const promptBool = require('../lib/utils/input/prompt-bool') const promptBool = require("../lib/utils/input/prompt-bool");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config', 'token', 'rules'], string: ["config", "token", "rules"],
boolean: ['help', 'debug'], boolean: ["help", "debug"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
rules: 'r', rules: "r",
debug: 'd', debug: "d",
token: 't' token: "t"
} }
}) });
const subcommand = argv._[0] const subcommand = argv._[0];
// options // options
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now alias`)} <ls | set | rm> <deployment> <alias> ${chalk.bold(`${logo} now alias`)} <ls | set | rm> <deployment> <alias>
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -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 -r ${chalk.bold.underline("RULES_FILE")}, --rules=${chalk.bold.underline("RULES_FILE")} Rules file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} Login token
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} Lists all your aliases: ${chalk.gray("–")} Lists all your aliases:
${chalk.cyan('$ now alias ls')} ${chalk.cyan("$ now alias ls")}
${chalk.gray('–')} Adds a new alias to ${chalk.underline('my-api.now.sh')}: ${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')}`)} ${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: The ${chalk.dim("`.now.sh`")} suffix can be ommited:
${chalk.cyan('$ now alias set api-ownv3nc9f8 my-api')} ${chalk.cyan("$ now alias set api-ownv3nc9f8 my-api")}
The deployment id can be used as the source: The deployment id can be used as the source:
${chalk.cyan('$ now alias set deploymentId my-alias')} ${chalk.cyan("$ now alias set deploymentId my-alias")}
Custom domains work as alias targets: Custom domains work as alias targets:
${chalk.cyan(`$ now alias set ${chalk.underline('api-ownv3nc9f8.now.sh')} ${chalk.underline('my-api.com')}`)} ${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("–")} 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.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.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')}`)} ${chalk.cyan(`$ now alias ${chalk.underline("zeit.ninja")} -r ${chalk.underline("rules.json")}`)}
Export effective routing rules: Export effective routing rules:
${chalk.cyan(`$ now alias ls aliasId --json > ${chalk.underline('rules.json')}`)} ${chalk.cyan(`$ now alias ls aliasId --json > ${chalk.underline("rules.json")}`)}
${chalk.cyan(`$ now alias ls zeit.ninja`)} ${chalk.cyan(`$ now alias ls zeit.ninja`)}
${chalk.gray('–')} Removing an alias: ${chalk.gray("–")} Removing an alias:
${chalk.cyan('$ now alias rm aliasId')} ${chalk.cyan("$ now alias rm aliasId")}
To get the list of alias ids, use ${chalk.dim('`now alias ls`')}. To get the list of alias ids, use ${chalk.dim("`now alias ls`")}.
${chalk.dim('Alias:')} ln ${chalk.dim("Alias:")} ln
`) `
} );
};
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
if (argv.help) { if (argv.help) {
help() help();
exit(0) exit(0);
} else { } else {
const config = cfg.read() const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await run(token) await run(token);
} catch (err) { } catch (err) {
if (err.userError) { if (err.userError) {
error(err.message) error(err.message);
} else { } else {
error(`Unknown error: ${err}\n${err.stack}`) error(`Unknown error: ${err}\n${err.stack}`);
}
exit(1);
} }
exit(1) })
} .catch(e => {
}) error(`Authentication error – ${e.message}`);
.catch(e => { exit(1);
error(`Authentication error – ${e.message}`) });
exit(1)
})
} }
async function run(token) { async function run(token) {
const alias = new NowAlias(apiUrl, token, {debug}) const alias = new NowAlias(apiUrl, token, { debug });
const args = argv._.slice(1) const args = argv._.slice(1);
switch (subcommand) { switch (subcommand) {
case 'ls': case "ls":
case 'list': { case "list": {
if (args.length === 1) { if (args.length === 1) {
const list = await alias.listAliases() const list = await alias.listAliases();
const item = list.find(e => e.uid === argv._[1] || e.alias === argv._[1]) const item = list.find(
e => e.uid === argv._[1] || e.alias === argv._[1]
);
if (!item || !item.rules) { if (!item || !item.rules) {
error(`Could not match path alias for: ${argv._[1]}`) error(`Could not match path alias for: ${argv._[1]}`);
return exit(1) return exit(1);
} }
if (argv.json) { if (argv.json) {
console.log(JSON.stringify({rules: item.rules}, null, 2)) console.log(JSON.stringify({ rules: item.rules }, null, 2));
} else { } else {
const header = [['', 'pathname', 'method', 'dest'].map(s => chalk.dim(s))] const header = [
const text = list.length === 0 ? null : table(header.concat(item.rules.map(rule => { ["", "pathname", "method", "dest"].map(s => chalk.dim(s))
return [ ];
'', const text = list.length === 0
rule.pathname ? rule.pathname : '', ? null
rule.method ? rule.method : '*', : table(
rule.dest header.concat(
] item.rules.map(rule => {
})), {align: ['l', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) return [
"",
console.log(text) rule.pathname ? rule.pathname : "",
rule.method ? rule.method : "*",
rule.dest
];
})
),
{
align: ["l", "l", "l", "l"],
hsep: " ".repeat(2),
stringLength: strlen
}
);
console.log(text);
} }
break break;
} else if (args.length !== 0) { } else if (args.length !== 0) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias ls`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now alias ls`")}`
);
return exit(1);
} }
const start_ = new Date() const start_ = new Date();
const list = await alias.list() const list = await alias.list();
const urls = new Map(list.map(l => [l.uid, l.url])) const urls = new Map(list.map(l => [l.uid, l.url]));
const aliases = await alias.ls() const aliases = await alias.ls();
aliases.sort((a, b) => new Date(b.created) - new Date(a.created)) aliases.sort((a, b) => new Date(b.created) - new Date(a.created));
const current = new Date() const current = new Date();
const header = [['', 'id', 'source', 'url', 'created'].map(s => chalk.dim(s))] const header = [
const text = list.length === 0 ? null : table(header.concat(aliases.map(_alias => { ["", "id", "source", "url", "created"].map(s => chalk.dim(s))
const _url = chalk.underline(`https://${_alias.alias}`) ];
const target = _alias.deploymentId const text = list.length === 0
let _sourceUrl ? null
if (urls.get(target)) { : table(
_sourceUrl = chalk.underline(`https://${urls.get(target)}`) header.concat(
} else if (_alias.rules) { aliases.map(_alias => {
_sourceUrl = chalk.gray(`[${_alias.rules.length} custom rule${_alias.rules.length > 1 ? 's' : ''}]`) const _url = chalk.underline(`https://${_alias.alias}`);
} else { const target = _alias.deploymentId;
_sourceUrl = chalk.gray('<null>') let _sourceUrl;
} if (urls.get(target)) {
_sourceUrl = chalk.underline(`https://${urls.get(target)}`);
const time = chalk.gray(ms(current - new Date(_alias.created)) + ' ago') } else if (_alias.rules) {
return [ _sourceUrl = chalk.gray(
'', `[${_alias.rules.length} custom rule${_alias.rules.length > 1 ? "s" : ""}]`
// we default to `''` because some early aliases didn't );
// have an uid associated } else {
_alias.uid === null ? '' : _alias.uid, _sourceUrl = chalk.gray("<null>");
_sourceUrl, }
_url,
time const time = chalk.gray(
] ms(current - new Date(_alias.created)) + " ago"
})), {align: ['l', 'r', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) );
return [
const elapsed_ = ms(new Date() - start_) "",
console.log(`> ${aliases.length} alias${aliases.length === 1 ? '' : 'es'} found ${chalk.gray(`[${elapsed_}]`)}`) // we default to `''` because some early aliases didn't
// have an uid associated
_alias.uid === null ? "" : _alias.uid,
_sourceUrl,
_url,
time
];
})
),
{
align: ["l", "r", "l", "l"],
hsep: " ".repeat(2),
stringLength: strlen
}
);
const elapsed_ = ms(new Date() - start_);
console.log(
`> ${aliases.length} alias${aliases.length === 1 ? "" : "es"} found ${chalk.gray(`[${elapsed_}]`)}`
);
if (text) { if (text) {
console.log('\n' + text + '\n') console.log("\n" + text + "\n");
} }
break break;
} }
case 'rm': case "rm":
case 'remove': { case "remove": {
const _target = String(args[0]) const _target = String(args[0]);
if (!_target) { if (!_target) {
const err = new Error('No alias id specified') const err = new Error("No alias id specified");
err.userError = true err.userError = true;
throw err throw err;
} }
if (args.length !== 1) { if (args.length !== 1) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias rm <id>`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now alias rm <id>`")}`
);
return exit(1);
} }
const _aliases = await alias.ls() const _aliases = await alias.ls();
const _alias = findAlias(_target, _aliases) const _alias = findAlias(_target, _aliases);
if (!_alias) { if (!_alias) {
const err = new Error(`Alias not found by "${_target}". Run ${chalk.dim('`now alias ls`')} to see your aliases.`) const err = new Error(
err.userError = true `Alias not found by "${_target}". Run ${chalk.dim("`now alias ls`")} to see your aliases.`
throw err );
err.userError = true;
throw err;
} }
try { try {
const confirmation = await confirmDeploymentRemoval(alias, _alias) const confirmation = await confirmDeploymentRemoval(alias, _alias);
if (!confirmation) { if (!confirmation) {
console.log('\n> Aborted') console.log("\n> Aborted");
process.exit(0) process.exit(0);
} }
const start = new Date() const start = new Date();
await alias.rm(_alias) await alias.rm(_alias);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Alias ${chalk.bold(_alias.uid)} removed [${elapsed}]`) console.log(
`${chalk.cyan("> Success!")} Alias ${chalk.bold(_alias.uid)} removed [${elapsed}]`
);
} catch (err) { } catch (err) {
error(err) error(err);
exit(1) exit(1);
} }
break break;
} }
case 'add': case "add":
case 'set': { case "set": {
if (argv.rules) { if (argv.rules) {
await updatePathAlias(alias, argv._[0], argv.rules) await updatePathAlias(alias, argv._[0], argv.rules);
break break;
} }
if (args.length !== 2) { if (args.length !== 2) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias set <id> <domain>`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now alias set <id> <domain>`")}`
);
return exit(1);
} }
await alias.set(String(args[0]), String(args[1])) await alias.set(String(args[0]), String(args[1]));
break break;
} }
default: { default: {
if (argv._.length === 0) { if (argv._.length === 0) {
await reAlias(token, null, help, exit, apiUrl, debug, alias) await reAlias(token, null, help, exit, apiUrl, debug, alias);
break break;
} }
if (argv.rules) { if (argv.rules) {
await updatePathAlias(alias, argv._[0], argv.rules) await updatePathAlias(alias, argv._[0], argv.rules);
} else if (argv._.length === 2) { } else if (argv._.length === 2) {
await alias.set(String(argv._[0]), String(argv._[1])) await alias.set(String(argv._[0]), String(argv._[1]));
} else if (argv._.length >= 3) { } else if (argv._.length >= 3) {
error('Invalid number of arguments') error("Invalid number of arguments");
help() help();
exit(1) exit(1);
} else { } else {
error('Please specify a valid subcommand: ls | set | rm') error("Please specify a valid subcommand: ls | set | rm");
help() help();
exit(1) exit(1);
} }
} }
} }
alias.close() alias.close();
} }
async function confirmDeploymentRemoval(alias, _alias) { async function confirmDeploymentRemoval(alias, _alias) {
const deploymentsList = await alias.list() const deploymentsList = await alias.list();
const urls = new Map(deploymentsList.map(l => [l.uid, l.url])) const urls = new Map(deploymentsList.map(l => [l.uid, l.url]));
const time = chalk.gray(ms(new Date() - new Date(_alias.created)) + ' ago') const time = chalk.gray(ms(new Date() - new Date(_alias.created)) + " ago");
const _sourceUrl = chalk.underline(`https://${urls.get(_alias.deploymentId)}`) const _sourceUrl = chalk.underline(
`https://${urls.get(_alias.deploymentId)}`
);
const tbl = table( const tbl = table(
[[_alias.uid, _sourceUrl, chalk.underline(`https://${_alias.alias}`), time]], [
{align: ['l', 'r', 'l'], hsep: ' '.repeat(6)} [_alias.uid, _sourceUrl, chalk.underline(`https://${_alias.alias}`), time]
) ],
{ align: ["l", "r", "l"], hsep: " ".repeat(6) }
const msg = '> The following alias will be removed permanently\n' + );
` ${tbl} \nAre you sure?`
return await promptBool(msg) const msg = "> The following alias will be removed permanently\n" +
` ${tbl} \nAre you sure?`;
return await promptBool(msg);
} }
function findAlias(alias, list) { function findAlias(alias, list) {
let key let key;
let val let val;
if (/\./.test(alias)) { if (/\./.test(alias)) {
val = toHost(alias) val = toHost(alias);
key = 'alias' key = "alias";
} else { } else {
val = alias val = alias;
key = 'uid' key = "uid";
} }
const _alias = list.find(d => { const _alias = list.find(d => {
if (d[key] === val) { if (d[key] === val) {
if (debug) { if (debug) {
console.log(`> [debug] matched alias ${d.uid} by ${key} ${val}`) console.log(`> [debug] matched alias ${d.uid} by ${key} ${val}`);
} }
return true return true;
} }
// match prefix // match prefix
if (`${val}.now.sh` === d.alias) { if (`${val}.now.sh` === d.alias) {
if (debug) { if (debug) {
console.log(`> [debug] matched alias ${d.uid} by url ${d.host}`) console.log(`> [debug] matched alias ${d.uid} by url ${d.host}`);
} }
return true return true;
} }
return false return false;
}) });
return _alias return _alias;
} }
async function updatePathAlias(alias, aliasName, rules) { async function updatePathAlias(alias, aliasName, rules) {
const start = new Date() const start = new Date();
const res = await alias.updatePathBasedroutes(String(aliasName), rules) const res = await alias.updatePathBasedroutes(String(aliasName), rules);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
if (res.error) { if (res.error) {
const err = new Error(res.error.message) const err = new Error(res.error.message);
err.userError = true err.userError = true;
throw err throw err;
} else { } else {
console.log(`${chalk.cyan('> Success!')} ${res.ruleCount} rules configured for ${chalk.underline(res.alias)} [${elapsed}]`) console.log(
`${chalk.cyan("> Success!")} ${res.ruleCount} rules configured for ${chalk.underline(res.alias)} [${elapsed}]`
);
} }
} }

192
bin/now-billing-add.js

@ -1,183 +1,185 @@
#!/usr/bin/env node #!/usr/bin/env node
// Packages // Packages
const ansiEscapes = require('ansi-escapes') const ansiEscapes = require("ansi-escapes");
const chalk = require('chalk') const chalk = require("chalk");
const ccValidator = require('credit-card') const ccValidator = require("credit-card");
// Ours // Ours
const textInput = require('../lib/utils/input/text') const textInput = require("../lib/utils/input/text");
const countries = require('../lib/utils/billing/country-list') const countries = require("../lib/utils/billing/country-list");
const cardBrands = require('../lib/utils/billing/card-brands') const cardBrands = require("../lib/utils/billing/card-brands");
const geocode = require('../lib/utils/billing/geocode') const geocode = require("../lib/utils/billing/geocode");
const success = require('../lib/utils/output/success') const success = require("../lib/utils/output/success");
const wait = require('../lib/utils/output/wait') const wait = require("../lib/utils/output/wait");
function rightPad(string, n = 12) { function rightPad(string, n = 12) {
n -= string.length n -= string.length;
return string + ' '.repeat(n > -1 ? n : 0) return string + " ".repeat(n > -1 ? n : 0);
} }
function expDateMiddleware(data) { function expDateMiddleware(data) {
return data return data;
} }
module.exports = function (creditCards) { module.exports = function(creditCards) {
const state = { const state = {
error: undefined, error: undefined,
cardGroupLabel: `> ${chalk.bold('Enter your card details')}`, cardGroupLabel: `> ${chalk.bold("Enter your card details")}`,
name: { name: {
label: rightPad('Full Name'), label: rightPad("Full Name"),
placeholder: 'John Appleseed', placeholder: "John Appleseed",
validateValue: data => data.trim().length > 0 validateValue: data => data.trim().length > 0
}, },
cardNumber: { cardNumber: {
label: rightPad('Number'), label: rightPad("Number"),
mask: 'cc', mask: "cc",
placeholder: '#### #### #### ####', placeholder: "#### #### #### ####",
validateKeypress: (data, value) => ( validateKeypress: (data, value) => /\d/.test(data) && value.length < 19,
/\d/.test(data) && value.length < 19
),
validateValue: data => { validateValue: data => {
data = data.replace(/ /g, '') data = data.replace(/ /g, "");
const type = ccValidator.determineCardType(data) const type = ccValidator.determineCardType(data);
if (!type) { if (!type) {
return false return false;
} }
return ccValidator.isValidCardNumber(data, type) return ccValidator.isValidCardNumber(data, type);
} }
}, },
ccv: { ccv: {
label: rightPad('CCV'), label: rightPad("CCV"),
mask: 'ccv', mask: "ccv",
placeholder: '###', placeholder: "###",
validateValue: data => { validateValue: data => {
const brand = state.cardNumber.brand.toLowerCase() const brand = state.cardNumber.brand.toLowerCase();
return ccValidator.doesCvvMatchType(data, brand) return ccValidator.doesCvvMatchType(data, brand);
} }
}, },
expDate: { expDate: {
label: rightPad('Exp. Date'), label: rightPad("Exp. Date"),
mask: 'expDate', mask: "expDate",
placeholder: 'mm / yyyy', placeholder: "mm / yyyy",
middleware: expDateMiddleware, middleware: expDateMiddleware,
validateValue: data => !ccValidator.isExpired(...data.split(' / ')) validateValue: data => !ccValidator.isExpired(...data.split(" / "))
}, },
addressGroupLabel: `\n> ${chalk.bold('Enter your billing address')}`, addressGroupLabel: `\n> ${chalk.bold("Enter your billing address")}`,
country: { country: {
label: rightPad('Country'), label: rightPad("Country"),
async autoComplete(value) { async autoComplete(value) {
for (const country in countries) { for (const country in countries) {
if (!Object.hasOwnProperty.call(countries, country)) { if (!Object.hasOwnProperty.call(countries, country)) {
continue continue;
} }
if (country.startsWith(value)) { if (country.startsWith(value)) {
return country.substr(value.length) return country.substr(value.length);
} }
} }
return false return false;
}, },
validateValue: value => countries[value] !== undefined validateValue: value => countries[value] !== undefined
}, },
zipCode: { zipCode: {
label: rightPad('ZIP'), label: rightPad("ZIP"),
validadeKeypress: data => data.trim().length > 0, validadeKeypress: data => data.trim().length > 0,
validateValue: data => data.trim().length > 0 validateValue: data => data.trim().length > 0
}, },
state: { state: {
label: rightPad('State'), label: rightPad("State"),
validateValue: data => data.trim().length > 0 validateValue: data => data.trim().length > 0
}, },
city: { city: {
label: rightPad('City'), label: rightPad("City"),
validateValue: data => data.trim().length > 0 validateValue: data => data.trim().length > 0
}, },
address1: { address1: {
label: rightPad('Address'), label: rightPad("Address"),
validateValue: data => data.trim().length > 0 validateValue: data => data.trim().length > 0
} }
} };
async function render() { async function render() {
for (const key in state) { for (const key in state) {
if (!Object.hasOwnProperty.call(state, key)) { if (!Object.hasOwnProperty.call(state, key)) {
continue continue;
} }
const piece = state[key] const piece = state[key];
if (typeof piece === 'string') { if (typeof piece === "string") {
console.log(piece) console.log(piece);
} else if (typeof piece === 'object') { } else if (typeof piece === "object") {
let result let result;
try { try {
result = await textInput({ result = await textInput({
label: '- ' + piece.label, label: "- " + piece.label,
initialValue: piece.initialValue || piece.value, initialValue: piece.initialValue || piece.value,
placeholder: piece.placeholder, placeholder: piece.placeholder,
mask: piece.mask, mask: piece.mask,
validateKeypress: piece.validateKeypress, validateKeypress: piece.validateKeypress,
validateValue: piece.validateValue, validateValue: piece.validateValue,
autoComplete: piece.autoComplete autoComplete: piece.autoComplete
}) });
piece.value = result piece.value = result;
if (key === 'cardNumber') { if (key === "cardNumber") {
let brand = cardBrands[ccValidator.determineCardType(result)] let brand = cardBrands[ccValidator.determineCardType(result)];
piece.brand = brand piece.brand = brand;
if (brand === 'American Express') { if (brand === "American Express") {
state.ccv.placeholder = '#'.repeat(4) state.ccv.placeholder = "#".repeat(4);
} else { } else {
state.ccv.placeholder = '#'.repeat(3) state.ccv.placeholder = "#".repeat(3);
} }
brand = chalk.cyan(`[${brand}]`) brand = chalk.cyan(`[${brand}]`);
const masked = chalk.gray('#### '.repeat(3)) + result.split(' ')[3] const masked = chalk.gray("#### ".repeat(3)) + result.split(" ")[3];
process.stdout.write( process.stdout.write(
`${chalk.cyan('✓')} ${piece.label}${masked} ${brand}\n` `${chalk.cyan("✓")} ${piece.label}${masked} ${brand}\n`
) );
} else if (key === 'ccv') { } else if (key === "ccv") {
process.stdout.write( process.stdout.write(
`${chalk.cyan('✓')} ${piece.label}${'*'.repeat(result.length)}\n` `${chalk.cyan("✓")} ${piece.label}${"*".repeat(result.length)}\n`
) );
} else if (key === 'expDate') { } else if (key === "expDate") {
let text = result.split(' / ') let text = result.split(" / ");
text = text[0] + chalk.gray(' / ') + text[1] text = text[0] + chalk.gray(" / ") + text[1];
process.stdout.write(`${chalk.cyan('✓')} ${piece.label}${text}\n`) process.stdout.write(`${chalk.cyan("✓")} ${piece.label}${text}\n`);
} else if (key === 'zipCode') { } else if (key === "zipCode") {
const stopSpinner = wait(piece.label + result) const stopSpinner = wait(piece.label + result);
const addressInfo = await geocode({ const addressInfo = await geocode({
country: state.country.value, country: state.country.value,
zipCode: result zipCode: result
}) });
if (addressInfo.state) { if (addressInfo.state) {
state.state.initialValue = addressInfo.state state.state.initialValue = addressInfo.state;
} }
if (addressInfo.city) { if (addressInfo.city) {
state.city.initialValue = addressInfo.city state.city.initialValue = addressInfo.city;
} }
stopSpinner() stopSpinner();
process.stdout.write(`${chalk.cyan('✓')} ${piece.label}${result}\n`) process.stdout.write(
`${chalk.cyan("✓")} ${piece.label}${result}\n`
);
} else { } else {
process.stdout.write(`${chalk.cyan('✓')} ${piece.label}${result}\n`) process.stdout.write(
`${chalk.cyan("✓")} ${piece.label}${result}\n`
);
} }
} catch (err) { } catch (err) {
if (err.message === 'USER_ABORT') { if (err.message === "USER_ABORT") {
process.exit(1) process.exit(1);
} else { } else {
console.error(err) console.error(err);
} }
} }
} }
} }
console.log('') // new line console.log(""); // new line
const stopSpinner = wait('Saving card') const stopSpinner = wait("Saving card");
try { try {
const res = await creditCards.add({ const res = await creditCards.add({
@ -190,17 +192,19 @@ module.exports = function (creditCards) {
state: state.state.value, state: state.state.value,
city: state.city.value, city: state.city.value,
address1: state.address1.value address1: state.address1.value
}) });
stopSpinner() stopSpinner();
success(`${state.cardNumber.brand} ending in ${res.last4} was added to your account`) success(
`${state.cardNumber.brand} ending in ${res.last4} was added to your account`
);
} catch (err) { } catch (err) {
stopSpinner() stopSpinner();
const linesToClear = state.error ? 13 : 12 const linesToClear = state.error ? 13 : 12;
process.stdout.write(ansiEscapes.eraseLines(linesToClear)) process.stdout.write(ansiEscapes.eraseLines(linesToClear));
state.error = `${chalk.red('> Error!')} ${err.message} Please make sure the info is correct` state.error = `${chalk.red("> Error!")} ${err.message} Please make sure the info is correct`;
await render() await render();
} }
} }
render().catch(console.error) render().catch(console.error);
} };

340
bin/now-billing.js

@ -1,78 +1,80 @@
#!/usr/bin/env node #!/usr/bin/env node
// Native // Native
const {resolve} = require('path') const { resolve } = require("path");
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
const minimist = require('minimist') const minimist = require("minimist");
const ms = require('ms') const ms = require("ms");
// Ours // Ours
const login = require('../lib/login') const login = require("../lib/login");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const {error} = require('../lib/error') const { error } = require("../lib/error");
const NowCreditCards = require('../lib/credit-cards') const NowCreditCards = require("../lib/credit-cards");
const indent = require('../lib/indent') const indent = require("../lib/indent");
const listInput = require('../lib/utils/input/list') const listInput = require("../lib/utils/input/list");
const success = require('../lib/utils/output/success') const success = require("../lib/utils/output/success");
const promptBool = require('../lib/utils/input/prompt-bool') const promptBool = require("../lib/utils/input/prompt-bool");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'], string: ["config", "token"],
boolean: ['help', 'debug'], boolean: ["help", "debug"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
token: 't' token: "t"
} }
}) });
const subcommand = argv._[0] const subcommand = argv._[0];
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now billing`)} <ls | add | rm | set-default> ${chalk.bold(`${logo} now billing`)} <ls | add | rm | set-default>
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -c ${chalk.bold.underline("FILE")}, --config=${chalk.bold.underline("FILE")} Config file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} Login token
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} Lists all your credit cards: ${chalk.gray("–")} Lists all your credit cards:
${chalk.cyan('$ now billing ls')} ${chalk.cyan("$ now billing ls")}
${chalk.gray('–')} Adds a credit card (interactively): ${chalk.gray("–")} Adds a credit card (interactively):
${chalk.cyan(`$ now billing add`)} ${chalk.cyan(`$ now billing add`)}
${chalk.gray('–')} Removes a credit card: ${chalk.gray("–")} Removes a credit card:
${chalk.cyan(`$ now billing rm <id>`)} ${chalk.cyan(`$ now billing rm <id>`)}
${chalk.gray('–')} If the id is omitted, you can choose interactively ${chalk.gray("–")} If the id is omitted, you can choose interactively
${chalk.gray('–')} Selects your default credit card: ${chalk.gray("–")} Selects your default credit card:
${chalk.cyan(`$ now billing set-default <id>`)} ${chalk.cyan(`$ now billing set-default <id>`)}
${chalk.gray('–')} If the id is omitted, you can choose interactively ${chalk.gray("–")} If the id is omitted, you can choose interactively
`) `
} );
};
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
const exit = code => { const exit = code => {
@ -80,235 +82,247 @@ const exit = code => {
// because there's a node bug where // because there's a node bug where
// stdout writes are asynchronous // stdout writes are asynchronous
// https://github.com/nodejs/node/issues/6456 // https://github.com/nodejs/node/issues/6456
setTimeout(() => process.exit(code || 0), 100) setTimeout(() => process.exit(code || 0), 100);
} };
if (argv.help || !subcommand) { if (argv.help || !subcommand) {
help() help();
exit(0) exit(0);
} else { } else {
const config = cfg.read() const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await run(token) await run(token);
} catch (err) { } catch (err) {
if (err.userError) { if (err.userError) {
error(err.message) error(err.message);
} else { } else {
error(`Unknown error: ${err.stack}`) error(`Unknown error: ${err.stack}`);
}
exit(1);
} }
exit(1) })
} .catch(e => {
}) error(`Authentication error – ${e.message}`);
.catch(e => { exit(1);
error(`Authentication error – ${e.message}`) });
exit(1)
})
} }
// Builds a `choices` object that can be passesd to inquirer.prompt() // Builds a `choices` object that can be passesd to inquirer.prompt()
function buildInquirerChoices(cards) { function buildInquirerChoices(cards) {
return cards.cards.map(card => { return cards.cards.map(card => {
const _default = card.id === cards.defaultCardId ? ' ' + chalk.bold('(default)') : '' const _default = card.id === cards.defaultCardId
const id = `${chalk.cyan(`ID: ${card.id}`)}${_default}` ? " " + chalk.bold("(default)")
const number = `${chalk.gray('#### ').repeat(3)}${card.last4}` : "";
const id = `${chalk.cyan(`ID: ${card.id}`)}${_default}`;
const number = `${chalk.gray("#### ").repeat(3)}${card.last4}`;
const str = [ const str = [
id, id,
indent(card.name, 2), indent(card.name, 2),
indent(`${card.brand} ${number}`, 2) indent(`${card.brand} ${number}`, 2)
].join('\n') ].join("\n");
return { return {
name: str, // Will be displayed by Inquirer name: str, // Will be displayed by Inquirer
value: card.id, // Will be used to identify the answer value: card.id, // Will be used to identify the answer
short: card.id // Will be displayed after the users answers short: card.id // Will be displayed after the users answers
} };
}) });
} }
async function run(token) { async function run(token) {
const start = new Date() const start = new Date();
const creditCards = new NowCreditCards(apiUrl, token, {debug}) const creditCards = new NowCreditCards(apiUrl, token, { debug });
const args = argv._.slice(1) const args = argv._.slice(1);
switch (subcommand) { switch (subcommand) {
case 'ls': case "ls":
case 'list': { case "list": {
const cards = await creditCards.ls() const cards = await creditCards.ls();
const text = cards.cards.map(card => { const text = cards.cards
const _default = card.id === cards.defaultCardId ? ' ' + chalk.bold('(default)') : '' .map(card => {
const id = `${chalk.gray('-')} ${chalk.cyan(`ID: ${card.id}`)}${_default}` const _default = card.id === cards.defaultCardId
const number = `${chalk.gray('#### ').repeat(3)}${card.last4}` ? " " + chalk.bold("(default)")
let address = card.address_line1 : "";
const id = `${chalk.gray("-")} ${chalk.cyan(`ID: ${card.id}`)}${_default}`;
if (card.address_line2) { const number = `${chalk.gray("#### ").repeat(3)}${card.last4}`;
address += `, ${card.address_line2}.` let address = card.address_line1;
} else {
address += '.' if (card.address_line2) {
} address += `, ${card.address_line2}.`;
} else {
address += ".";
}
address += `\n${card.address_city}, ` address += `\n${card.address_city}, `;
if (card.address_state) { if (card.address_state) {
address += `${card.address_state}, ` address += `${card.address_state}, `;
} }
// Stripe is returning a two digit code for the country, // Stripe is returning a two digit code for the country,
// but we want the full country name // but we want the full country name
address += `${card.address_zip}. ${card.address_country}` address += `${card.address_zip}. ${card.address_country}`;
return [ return [
id, id,
indent(card.name, 2), indent(card.name, 2),
indent(`${card.brand} ${number}`, 2), indent(`${card.brand} ${number}`, 2),
indent(address, 2) indent(address, 2)
].join('\n') ].join("\n");
}).join('\n\n') })
.join("\n\n");
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`> ${cards.cards.length} card${cards.cards.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`) console.log(
`> ${cards.cards.length} card${cards.cards.length === 1 ? "" : "s"} found ${chalk.gray(`[${elapsed}]`)}`
);
if (text) { if (text) {
console.log(`\n${text}\n`) console.log(`\n${text}\n`);
} }
break break;
} }
case 'set-default': { case "set-default": {
if (args.length > 1) { if (args.length > 1) {
error('Invalid number of arguments') error("Invalid number of arguments");
return exit(1) return exit(1);
} }
const start = new Date() const start = new Date();
const cards = await creditCards.ls() const cards = await creditCards.ls();
if (cards.cards.length === 0) { if (cards.cards.length === 0) {
error('You have no credit cards to choose from') error("You have no credit cards to choose from");
return exit(0) return exit(0);
} }
let cardId = args[0] let cardId = args[0];
if (cardId === undefined) { if (cardId === undefined) {
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
const message = `Selecting a new default payment card from ${cards.cards.length} total ${chalk.gray(`[${elapsed}]`)}` const message = `Selecting a new default payment card from ${cards.cards.length} total ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(cards) const choices = buildInquirerChoices(cards);
cardId = await listInput({ cardId = await listInput({
message, message,
choices, choices,
separator: true, separator: true,
abort: 'end' abort: "end"
}) });
} }
// Check if the provided cardId (in case the user // Check if the provided cardId (in case the user
// typed `now billing set-default <some-id>`) is valid // typed `now billing set-default <some-id>`) is valid
if (cardId) { if (cardId) {
const label = `Are you sure that you to set this card as the default?` const label = `Are you sure that you to set this card as the default?`;
const confirmation = await promptBool(label) const confirmation = await promptBool(label);
if (!confirmation) { if (!confirmation) {
console.log('Aborted') console.log("Aborted");
break break;
} }
const start = new Date() const start = new Date();
await creditCards.setDefault(cardId) await creditCards.setDefault(cardId);
const card = cards.cards.find(card => card.id === cardId) const card = cards.cards.find(card => card.id === cardId);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
success(`${card.brand} ending in ${card.last4} is now the default ${chalk.gray(`[${elapsed}]`)}`) success(
`${card.brand} ending in ${card.last4} is now the default ${chalk.gray(`[${elapsed}]`)}`
);
} else { } else {
console.log('No changes made') console.log("No changes made");
} }
break break;
} }
case 'rm': case "rm":
case 'remove': { case "remove": {
if (args.length > 1) { if (args.length > 1) {
error('Invalid number of arguments') error("Invalid number of arguments");
return exit(1) return exit(1);
} }
const start = new Date() const start = new Date();
const cards = await creditCards.ls() const cards = await creditCards.ls();
if (cards.cards.length === 0) { if (cards.cards.length === 0) {
error('You have no credit cards to choose from to delete') error("You have no credit cards to choose from to delete");
return exit(0) return exit(0);
} }
let cardId = args[0] let cardId = args[0];
if (cardId === undefined) { if (cardId === undefined) {
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
const message = `Selecting a card to ${chalk.underline('remove')} from ${cards.cards.length} total ${chalk.gray(`[${elapsed}]`)}` const message = `Selecting a card to ${chalk.underline("remove")} from ${cards.cards.length} total ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(cards) const choices = buildInquirerChoices(cards);
cardId = await listInput({ cardId = await listInput({
message, message,
choices, choices,
separator: true, separator: true,
abort: 'start' abort: "start"
}) });
} }
// Shoud check if the provided cardId (in case the user // Shoud check if the provided cardId (in case the user
// typed `now billing rm <some-id>`) is valid // typed `now billing rm <some-id>`) is valid
if (cardId) { if (cardId) {
const label = `Are you sure that you want to remove this card?` const label = `Are you sure that you want to remove this card?`;
const confirmation = await promptBool(label) const confirmation = await promptBool(label);
if (!confirmation) { if (!confirmation) {
console.log('Aborted') console.log("Aborted");
break break;
} }
const start = new Date() const start = new Date();
await creditCards.rm(cardId) await creditCards.rm(cardId);
const deletedCard = cards.cards.find(card => card.id === cardId) const deletedCard = cards.cards.find(card => card.id === cardId);
const remainingCards = cards.cards.filter(card => card.id !== cardId) const remainingCards = cards.cards.filter(card => card.id !== cardId);
let text = `${deletedCard.brand} ending in ${deletedCard.last4} was deleted` let text = `${deletedCard.brand} ending in ${deletedCard.last4} was deleted`;
// ${chalk.gray(`[${elapsed}]`)} // ${chalk.gray(`[${elapsed}]`)}
if (cardId === cards.defaultCardId) { if (cardId === cards.defaultCardId) {
if (remainingCards.length === 0) { if (remainingCards.length === 0) {
// The user deleted the last card in their account // The user deleted the last card in their account
text += `\n${chalk.yellow('Warning!')} You have no default card` text += `\n${chalk.yellow("Warning!")} You have no default card`;
} else { } else {
// We can't guess the current default card – let's ask the API // We can't guess the current default card – let's ask the API
const cards = await creditCards.ls() const cards = await creditCards.ls();
const newDefaultCard = cards.cards.find(card => card.id === cards.defaultCardId) const newDefaultCard = cards.cards.find(
card => card.id === cards.defaultCardId
);
text += `\n${newDefaultCard.brand} ending in ${newDefaultCard.last4} in now default` text += `\n${newDefaultCard.brand} ending in ${newDefaultCard.last4} in now default`;
} }
} }
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
text += ` ${chalk.gray(`[${elapsed}]`)}` text += ` ${chalk.gray(`[${elapsed}]`)}`;
success(text) success(text);
} else { } else {
console.log('No changes made') console.log("No changes made");
} }
break break;
} }
case 'add': { case "add": {
require(resolve(__dirname, 'now-billing-add.js'))(creditCards) require(resolve(__dirname, "now-billing-add.js"))(creditCards);
break break;
} }
default: default:
error('Please specify a valid subcommand: ls | add | rm | set-default') error("Please specify a valid subcommand: ls | add | rm | set-default");
help() help();
exit(1) exit(1);
} }
creditCards.close() creditCards.close();
} }

399
bin/now-certs.js

@ -1,287 +1,342 @@
#!/usr/bin/env node #!/usr/bin/env node
// Native // Native
const path = require('path') const path = require("path");
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
const table = require('text-table') const table = require("text-table");
const minimist = require('minimist') const minimist = require("minimist");
const fs = require('fs-promise') const fs = require("fs-promise");
const ms = require('ms') const ms = require("ms");
// Ours // Ours
const strlen = require('../lib/strlen') const strlen = require("../lib/strlen");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const {handleError, error} = require('../lib/error') const { handleError, error } = require("../lib/error");
const NowCerts = require('../lib/certs') const NowCerts = require("../lib/certs");
const login = require('../lib/login') const login = require("../lib/login");
const exit = require('../lib/utils/exit') const exit = require("../lib/utils/exit");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config', 'token', 'crt', 'key', 'ca'], string: ["config", "token", "crt", "key", "ca"],
boolean: ['help', 'debug'], boolean: ["help", "debug"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
token: 't' token: "t"
} }
}) });
const subcommand = argv._[0] const subcommand = argv._[0];
// options // options
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now certs`)} <ls | create | renew | replace | rm> <cn> ${chalk.bold(`${logo} now certs`)} <ls | create | renew | replace | rm> <cn>
${chalk.dim('Note:')} ${chalk.dim("Note:")}
This command is intended for advanced use only, normally ${chalk.bold('now')} manages your certificates automatically. This command is intended for advanced use only, normally ${chalk.bold("now")} manages your certificates automatically.
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -c ${chalk.bold.underline("FILE")}, --config=${chalk.bold.underline("FILE")} Config file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} Login token
--crt ${chalk.bold.underline('FILE')} Certificate file --crt ${chalk.bold.underline("FILE")} Certificate file
--key ${chalk.bold.underline('FILE')} Certificate key file --key ${chalk.bold.underline("FILE")} Certificate key file
--ca ${chalk.bold.underline('FILE')} CA certificate chain file --ca ${chalk.bold.underline("FILE")} CA certificate chain file
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} Listing all your certificates: ${chalk.gray("–")} Listing all your certificates:
${chalk.cyan('$ now certs ls')} ${chalk.cyan("$ now certs ls")}
${chalk.gray('–')} Creating a new certificate: ${chalk.gray("–")} Creating a new certificate:
${chalk.cyan('$ now certs create domain.com')} ${chalk.cyan("$ now certs create domain.com")}
${chalk.gray('–')} Renewing an existing certificate issued with ${chalk.bold('now')}: ${chalk.gray("–")} Renewing an existing certificate issued with ${chalk.bold("now")}:
${chalk.cyan('$ now certs renew domain.com')} ${chalk.cyan("$ now certs renew domain.com")}
${chalk.gray('–')} Replacing an existing certificate with a user-supplied certificate: ${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')} ${chalk.cyan("$ now certs replace --crt domain.crt --key domain.key --ca ca_chain.crt domain.com")}
`) `
} );
};
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
if (argv.help || !subcommand) { if (argv.help || !subcommand) {
help() help();
exit(0) exit(0);
} else { } else {
const config = cfg.read() const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await run(token) await run(token);
} catch (err) { } catch (err) {
handleError(err) handleError(err);
exit(1) exit(1);
} }
}) })
.catch(e => { .catch(e => {
error(`Authentication error – ${e.message}`) error(`Authentication error – ${e.message}`);
exit(1) exit(1);
}) });
} }
function formatExpirationDate(date) { function formatExpirationDate(date) {
const diff = date - Date.now() const diff = date - Date.now();
return diff < 0 ? chalk.gray(ms(-diff) + ' ago') : chalk.gray('in ' + ms(diff)) return diff < 0
? chalk.gray(ms(-diff) + " ago")
: chalk.gray("in " + ms(diff));
} }
async function run(token) { async function run(token) {
const certs = new NowCerts(apiUrl, token, {debug}) const certs = new NowCerts(apiUrl, token, { debug });
const args = argv._.slice(1) const args = argv._.slice(1);
const start = Date.now() const start = Date.now();
if (subcommand === 'ls' || subcommand === 'list') { if (subcommand === "ls" || subcommand === "list") {
if (args.length !== 0) { if (args.length !== 0) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs ls`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now certs ls`")}`
);
return exit(1);
} }
const list = await certs.ls() const list = await certs.ls();
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`> ${list.length} certificate${list.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`) console.log(
`> ${list.length} certificate${list.length === 1 ? "" : "s"} found ${chalk.gray(`[${elapsed}]`)}`
);
if (list.length > 0) { if (list.length > 0) {
const cur = Date.now() const cur = Date.now();
list.sort((a, b) => { list.sort((a, b) => {
return a.cn.localeCompare(b.cn) return a.cn.localeCompare(b.cn);
}) });
const header = [['', 'id', 'cn', 'created', 'expiration', 'auto-renew'].map(s => chalk.dim(s))] const header = [
const out = table(header.concat(list.map(cert => { ["", "id", "cn", "created", "expiration", "auto-renew"].map(s =>
const cn = chalk.bold(cert.cn) chalk.dim(s))
const time = chalk.gray(ms(cur - new Date(cert.created)) + ' ago') ];
const expiration = formatExpirationDate(new Date(cert.expiration)) const out = table(
return [ header.concat(
'', list.map(cert => {
cert.uid ? cert.uid : 'unknown', const cn = chalk.bold(cert.cn);
cn, const time = chalk.gray(ms(cur - new Date(cert.created)) + " ago");
time, const expiration = formatExpirationDate(new Date(cert.expiration));
expiration, return [
cert.autoRenew ? 'yes' : 'no' "",
] cert.uid ? cert.uid : "unknown",
})), {align: ['l', 'r', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) cn,
time,
expiration,
cert.autoRenew ? "yes" : "no"
];
})
),
{
align: ["l", "r", "l", "l", "l"],
hsep: " ".repeat(2),
stringLength: strlen
}
);
if (out) { if (out) {
console.log('\n' + out + '\n') console.log("\n" + out + "\n");
} }
} }
} else if (subcommand === 'create') { } else if (subcommand === "create") {
if (args.length !== 1) { if (args.length !== 1) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs create <cn>`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now certs create <cn>`")}`
);
return exit(1);
} }
const cn = args[0] const cn = args[0];
let cert let cert;
if (argv.crt || argv.key || argv.ca) { // Issue a custom certificate if (argv.crt || argv.key || argv.ca) {
// Issue a custom certificate
if (!argv.crt || !argv.key) { 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>`')}`) error(
return exit(1) `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 crt = readX509File(argv.crt);
const key = readX509File(argv.key) const key = readX509File(argv.key);
const ca = argv.ca ? readX509File(argv.ca) : '' const ca = argv.ca ? readX509File(argv.ca) : "";
cert = await certs.put(cn, crt, key, ca) cert = await certs.put(cn, crt, key, ca);
} else { // Issue a standard certificate } else {
cert = await certs.create(cn) // Issue a standard certificate
cert = await certs.create(cn);
} }
if (!cert) { if (!cert) {
// Cert is undefined and "Cert is already issued" has been printed to stdout // Cert is undefined and "Cert is already issued" has been printed to stdout
return exit(1) return exit(1);
} }
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Certificate entry ${chalk.bold(cn)} ${chalk.gray(`(${cert.uid})`)} created ${chalk.gray(`[${elapsed}]`)}`) console.log(
} else if (subcommand === 'renew') { `${chalk.cyan("> Success!")} Certificate entry ${chalk.bold(cn)} ${chalk.gray(`(${cert.uid})`)} created ${chalk.gray(`[${elapsed}]`)}`
);
} else if (subcommand === "renew") {
if (args.length !== 1) { if (args.length !== 1) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs renew <id | cn>`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now certs renew <id | cn>`")}`
);
return exit(1);
} }
const cert = await getCertIdCn(certs, args[0]) const cert = await getCertIdCn(certs, args[0]);
if (!cert) { if (!cert) {
return exit(1) return exit(1);
} }
const yes = await readConfirmation(cert, 'The following certificate will be renewed\n') const yes = await readConfirmation(
cert,
"The following certificate will be renewed\n"
);
if (!yes) { if (!yes) {
error('User abort') error("User abort");
return exit(0) return exit(0);
} }
await certs.renew(cert.cn) await certs.renew(cert.cn);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Certificate ${chalk.bold(cert.cn)} ${chalk.gray(`(${cert.uid})`)} renewed ${chalk.gray(`[${elapsed}]`)}`) console.log(
} else if (subcommand === 'replace') { `${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) { 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>`')}`) error(
return exit(1) `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 crt = readX509File(argv.crt);
const key = readX509File(argv.key) const key = readX509File(argv.key);
const ca = argv.ca ? readX509File(argv.ca) : '' const ca = argv.ca ? readX509File(argv.ca) : "";
const cert = await getCertIdCn(certs, args[0]) const cert = await getCertIdCn(certs, args[0]);
if (!cert) { if (!cert) {
return exit(1) return exit(1);
} }
const yes = await readConfirmation(cert, 'The following certificate will be replaced permanently\n') const yes = await readConfirmation(
cert,
"The following certificate will be replaced permanently\n"
);
if (!yes) { if (!yes) {
error('User abort') error("User abort");
return exit(0) return exit(0);
} }
await certs.put(cert.cn, crt, key, ca) await certs.put(cert.cn, crt, key, ca);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Certificate ${chalk.bold(cert.cn)} ${chalk.gray(`(${cert.uid})`)} replaced ${chalk.gray(`[${elapsed}]`)}`) console.log(
} else if (subcommand === 'rm' || subcommand === 'remove') { `${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) { if (args.length !== 1) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs rm <id | cn>`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now certs rm <id | cn>`")}`
);
return exit(1);
} }
const cert = await getCertIdCn(certs, args[0]) const cert = await getCertIdCn(certs, args[0]);
if (!cert) { if (!cert) {
return exit(1) return exit(1);
} }
const yes = await readConfirmation(cert, 'The following certificate will be removed permanently\n') const yes = await readConfirmation(
cert,
"The following certificate will be removed permanently\n"
);
if (!yes) { if (!yes) {
error('User abort') error("User abort");
return exit(0) return exit(0);
} }
await certs.delete(cert.cn) await certs.delete(cert.cn);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Certificate ${chalk.bold(cert.cn)} ${chalk.gray(`(${cert.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}`) console.log(
`${chalk.cyan("> Success!")} Certificate ${chalk.bold(cert.cn)} ${chalk.gray(`(${cert.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}`
);
} else { } else {
error('Please specify a valid subcommand: ls | create | renew | replace | rm') error(
help() "Please specify a valid subcommand: ls | create | renew | replace | rm"
exit(1) );
help();
exit(1);
} }
return certs.close() return certs.close();
} }
process.on('uncaughtException', err => { process.on("uncaughtException", err => {
handleError(err) handleError(err);
exit(1) exit(1);
}) });
function readConfirmation(cert, msg) { function readConfirmation(cert, msg) {
return new Promise(resolve => { return new Promise(resolve => {
const time = chalk.gray(ms(new Date() - new Date(cert.created)) + ' ago') const time = chalk.gray(ms(new Date() - new Date(cert.created)) + " ago");
const tbl = table( const tbl = table([[cert.uid, chalk.bold(cert.cn), time]], {
[[cert.uid, chalk.bold(cert.cn), time]], align: ["l", "r", "l"],
{align: ['l', 'r', 'l'], hsep: ' '.repeat(6)} hsep: " ".repeat(6)
) });
process.stdout.write(`> ${msg}`) process.stdout.write(`> ${msg}`);
process.stdout.write(' ' + tbl + '\n') process.stdout.write(" " + tbl + "\n");
process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/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') process.stdin
}).resume() .on("data", d => {
}) process.stdin.pause();
resolve(d.toString().trim().toLowerCase() === "y");
})
.resume();
});
} }
function readX509File(file) { function readX509File(file) {
return fs.readFileSync(path.resolve(file), 'utf8') return fs.readFileSync(path.resolve(file), "utf8");
} }
async function getCertIdCn(certs, idOrCn) { async function getCertIdCn(certs, idOrCn) {
const list = await certs.ls() const list = await certs.ls();
const thecert = list.filter(cert => { const thecert = list.filter(cert => {
return cert.uid === idOrCn || cert.cn === idOrCn return cert.uid === idOrCn || cert.cn === idOrCn;
})[0] })[0];
if (!thecert) { if (!thecert) {
error(`No certificate found by id or cn "${idOrCn}"`) error(`No certificate found by id or cn "${idOrCn}"`);
return null return null;
} }
return thecert return thecert;
} }

690
bin/now-deploy.js

@ -1,85 +1,81 @@
#!/usr/bin/env node #!/usr/bin/env node
// Native // Native
const {resolve} = require('path') const { resolve } = require("path");
// Packages // Packages
const Progress = require('progress') const Progress = require("progress");
const fs = require('fs-promise') const fs = require("fs-promise");
const bytes = require('bytes') const bytes = require("bytes");
const chalk = require('chalk') const chalk = require("chalk");
const minimist = require('minimist') const minimist = require("minimist");
const ms = require('ms') const ms = require("ms");
const flatten = require('arr-flatten') const flatten = require("arr-flatten");
const dotenv = require('dotenv') const dotenv = require("dotenv");
// Ours // Ours
const copy = require('../lib/copy') const copy = require("../lib/copy");
const login = require('../lib/login') const login = require("../lib/login");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const {version} = require('../lib/pkg') const { version } = require("../lib/pkg");
const Logger = require('../lib/build-logger') const Logger = require("../lib/build-logger");
const Now = require('../lib') const Now = require("../lib");
const toHumanPath = require('../lib/utils/to-human-path') const toHumanPath = require("../lib/utils/to-human-path");
const promptOptions = require('../lib/utils/prompt-options') const promptOptions = require("../lib/utils/prompt-options");
const {handleError, error} = require('../lib/error') const { handleError, error } = require("../lib/error");
const {fromGit, isRepoPath, gitPathParts} = require('../lib/git') const { fromGit, isRepoPath, gitPathParts } = require("../lib/git");
const readMetaData = require('../lib/read-metadata') const readMetaData = require("../lib/read-metadata");
const checkPath = require('../lib/utils/check-path') const checkPath = require("../lib/utils/check-path");
const {reAlias, assignAlias} = require('../lib/re-alias') const { reAlias, assignAlias } = require("../lib/re-alias");
const exit = require('../lib/utils/exit') const exit = require("../lib/utils/exit");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const cmd = require('../lib/utils/output/cmd') const cmd = require("../lib/utils/output/cmd");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: [ string: ["config", "token", "name", "alias"],
'config',
'token',
'name',
'alias'
],
boolean: [ boolean: [
'help', "help",
'version', "version",
'debug', "debug",
'force', "force",
'links', "links",
'login', "login",
'no-clipboard', "no-clipboard",
'forward-npm', "forward-npm",
'docker', "docker",
'npm', "npm",
'static' "static"
], ],
alias: { alias: {
env: 'e', env: "e",
dotenv: 'E', dotenv: "E",
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
version: 'v', version: "v",
force: 'f', force: "f",
token: 't', token: "t",
forceSync: 'F', forceSync: "F",
links: 'l', links: "l",
login: 'L', login: "L",
public: 'p', public: "p",
'no-clipboard': 'C', "no-clipboard": "C",
'forward-npm': 'N', "forward-npm": "N",
name: 'n', name: "n",
alias: 'a' alias: "a"
} }
}) });
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now`)} [options] <command | path> ${chalk.bold(`${logo} now`)} [options] <command | path>
${chalk.dim('Commands:')} ${chalk.dim("Commands:")}
${chalk.dim('Cloud')} ${chalk.dim("Cloud")}
deploy [path] Performs a deployment ${chalk.bold('(default)')} deploy [path] Performs a deployment ${chalk.bold("(default)")}
ls | list [app] List deployments ls | list [app] List deployments
rm | remove [id] Remove a deployment rm | remove [id] Remove a deployment
ln | alias [id] [url] Configures aliases for deployments ln | alias [id] [url] Configures aliases for deployments
@ -90,97 +86,102 @@ const help = () => {
open Open the latest deployment for the project open Open the latest deployment for the project
help [cmd] Displays complete help for [cmd] help [cmd] Displays complete help for [cmd]
${chalk.dim('Administrative')} ${chalk.dim("Administrative")}
billing | cc [cmd] Manages your credit cards and billing methods billing | cc [cmd] Manages your credit cards and billing methods
upgrade | downgrade [plan] Upgrades or downgrades your plan upgrade | downgrade [plan] Upgrades or downgrades your plan
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-v, --version Output the version number -v, --version Output the version number
-n, --name Set the name of the deployment -n, --name Set the name of the deployment
-c ${chalk.underline('FILE')}, --config=${chalk.underline('FILE')} Config file -c ${chalk.underline("FILE")}, --config=${chalk.underline("FILE")} Config file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-f, --force Force a new deployment even if nothing has changed -f, --force Force a new deployment even if nothing has changed
-t ${chalk.underline('TOKEN')}, --token=${chalk.underline('TOKEN')} Login token -t ${chalk.underline("TOKEN")}, --token=${chalk.underline("TOKEN")} Login token
-L, --login Configure login -L, --login Configure login
-l, --links Copy symlinks without resolving their target -l, --links Copy symlinks without resolving their target
-p, --public Deployment is public (${chalk.dim('`/_src`')} is exposed) [on for oss, off for premium] -p, --public Deployment is public (${chalk.dim("`/_src`")} is exposed) [on for oss, off for premium]
-e, --env Include an env var (e.g.: ${chalk.dim('`-e KEY=value`')}). Can appear many times. -e, --env Include an env var (e.g.: ${chalk.dim("`-e KEY=value`")}). Can appear many times.
-E ${chalk.underline('FILE')}, --dotenv=${chalk.underline('FILE')} Include env vars from .env file. Defaults to '.env' -E ${chalk.underline("FILE")}, --dotenv=${chalk.underline("FILE")} Include env vars from .env file. Defaults to '.env'
-C, --no-clipboard Do not attempt to copy URL to clipboard -C, --no-clipboard Do not attempt to copy URL to clipboard
-N, --forward-npm Forward login information to install private npm modules -N, --forward-npm Forward login information to install private npm modules
${chalk.dim('Enforcable Types (when both package.json and Dockerfile exist):')} ${chalk.dim("Enforcable Types (when both package.json and Dockerfile exist):")}
--npm Node.js application --npm Node.js application
--docker Docker container --docker Docker container
--static Static file hosting --static Static file hosting
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} Deploys the current directory ${chalk.gray("–")} Deploys the current directory
${chalk.cyan('$ now')} ${chalk.cyan("$ now")}
${chalk.gray('–')} Deploys a custom path ${chalk.dim('`/usr/src/project`')} ${chalk.gray("–")} Deploys a custom path ${chalk.dim("`/usr/src/project`")}
${chalk.cyan('$ now /usr/src/project')} ${chalk.cyan("$ now /usr/src/project")}
${chalk.gray('–')} Deploys a GitHub repository ${chalk.gray("–")} Deploys a GitHub repository
${chalk.cyan('$ now user/repo#ref')} ${chalk.cyan("$ now user/repo#ref")}
${chalk.gray('–')} Deploys a GitHub, GitLab or Bitbucket repo using its URL ${chalk.gray("–")} Deploys a GitHub, GitLab or Bitbucket repo using its URL
${chalk.cyan('$ now https://gitlab.com/user/repo')} ${chalk.cyan("$ now https://gitlab.com/user/repo")}
${chalk.gray('–')} Deploys with ENV vars ${chalk.gray("–")} Deploys with ENV vars
${chalk.cyan('$ now -e NODE_ENV=production -e MYSQL_PASSWORD=@mysql-password')} ${chalk.cyan("$ now -e NODE_ENV=production -e MYSQL_PASSWORD=@mysql-password")}
${chalk.gray('–')} Displays comprehensive help for the subcommand ${chalk.dim('`list`')} ${chalk.gray("–")} Displays comprehensive help for the subcommand ${chalk.dim("`list`")}
${chalk.cyan('$ now help list')} ${chalk.cyan("$ now help list")}
`) `
} );
};
let path = argv._[0] let path = argv._[0];
if (path) { if (path) {
// if path is relative: resolve // if path is relative: resolve
// if path is absolute: clear up strange `/` etc // if path is absolute: clear up strange `/` etc
path = resolve(process.cwd(), path) path = resolve(process.cwd(), path);
} else { } else {
path = process.cwd() path = process.cwd();
} }
// If the current deployment is a repo // If the current deployment is a repo
const gitRepo = {} const gitRepo = {};
// options // options
let forceNew = argv.force let forceNew = argv.force;
const debug = argv.debug const debug = argv.debug;
const clipboard = !argv['no-clipboard'] const clipboard = !argv["no-clipboard"];
const forwardNpm = argv['forward-npm'] const forwardNpm = argv["forward-npm"];
const forceSync = argv.forceSync const forceSync = argv.forceSync;
const shouldLogin = argv.login const shouldLogin = argv.login;
const followSymlinks = !argv.links const followSymlinks = !argv.links;
const wantsPublic = argv.public const wantsPublic = argv.public;
const deploymentName = argv.name || false const deploymentName = argv.name || false;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
const isTTY = process.stdout.isTTY const isTTY = process.stdout.isTTY;
const quiet = !isTTY const quiet = !isTTY;
const autoAliases = typeof argv.alias === 'undefined' ? false : flatten([argv.alias]) const autoAliases = typeof argv.alias === "undefined"
? false
: flatten([argv.alias]);
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
if (Array.isArray(autoAliases)) { if (Array.isArray(autoAliases)) {
console.log(`${chalk.red('Deprecated!')} The option ${chalk.grey('--alias')} will be removed soon.`) console.log(
console.log('Read more about the new way here: http://bit.ly/2l2v5Fg\n') `${chalk.red("Deprecated!")} The option ${chalk.grey("--alias")} will be removed soon.`
);
console.log("Read more about the new way here: http://bit.ly/2l2v5Fg\n");
} }
// Create a new deployment if user changed // Create a new deployment if user changed
@ -188,307 +189,348 @@ if (Array.isArray(autoAliases)) {
// This should just work fine because it doesn't // This should just work fine because it doesn't
// force a new sync, it just forces a new deployment. // force a new sync, it just forces a new deployment.
if (deploymentName || wantsPublic) { if (deploymentName || wantsPublic) {
forceNew = true forceNew = true;
} }
const config = cfg.read() const config = cfg.read();
const alwaysForwardNpm = config.forwardNpm const alwaysForwardNpm = config.forwardNpm;
if (argv.h || argv.help) { if (argv.h || argv.help) {
help() help();
exit(0) exit(0);
} else if (argv.v || argv.version) { } else if (argv.v || argv.version) {
console.log(version) console.log(version);
process.exit(0) process.exit(0);
} else if (!(argv.token || config.token) || shouldLogin) { } else if (!(argv.token || config.token) || shouldLogin) {
login(apiUrl) login(apiUrl)
.then(token => { .then(token => {
if (shouldLogin) { if (shouldLogin) {
console.log('> Logged in successfully. Token saved in ~/.now.json') console.log("> Logged in successfully. Token saved in ~/.now.json");
process.exit(0) process.exit(0);
} else { } else {
sync(token).catch(err => { sync(token).catch(err => {
error(`Unknown error: ${err}\n${err.stack}`) error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1) process.exit(1);
}) });
} }
}) })
.catch(e => { .catch(e => {
error(`Authentication error – ${e.message}`) error(`Authentication error – ${e.message}`);
process.exit(1) process.exit(1);
}) });
} else { } else {
sync(argv.token || config.token).catch(err => { sync(argv.token || config.token).catch(err => {
error(`Unknown error: ${err}\n${err.stack}`) error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1) process.exit(1);
}) });
} }
async function sync(token) { async function sync(token) {
const start = Date.now() const start = Date.now();
const rawPath = argv._[0] const rawPath = argv._[0];
const stopDeployment = msg => { const stopDeployment = msg => {
error(msg) error(msg);
process.exit(1) process.exit(1);
} };
const isValidRepo = isRepoPath(rawPath) const isValidRepo = isRepoPath(rawPath);
try { try {
await fs.stat(path) await fs.stat(path);
} catch (err) { } catch (err) {
let repo let repo;
if (isValidRepo && isValidRepo !== 'no-valid-url') { if (isValidRepo && isValidRepo !== "no-valid-url") {
const gitParts = gitPathParts(rawPath) const gitParts = gitPathParts(rawPath);
Object.assign(gitRepo, gitParts) Object.assign(gitRepo, gitParts);
const searchMessage = setTimeout(() => { const searchMessage = setTimeout(
console.log(`> Didn't find directory. Searching on ${gitRepo.type}...`) () => {
}, 500) console.log(
`> Didn't find directory. Searching on ${gitRepo.type}...`
);
},
500
);
try { try {
repo = await fromGit(rawPath, debug) repo = await fromGit(rawPath, debug);
} catch (err) {} } catch (err) {}
clearTimeout(searchMessage) clearTimeout(searchMessage);
} }
if (repo) { if (repo) {
// Tell now which directory to deploy // Tell now which directory to deploy
path = repo.path path = repo.path;
// Set global variable for deleting tmp dir later // Set global variable for deleting tmp dir later
// once the deployment has finished // once the deployment has finished
Object.assign(gitRepo, repo) Object.assign(gitRepo, repo);
} else if (isValidRepo === 'no-valid-url') { } else if (isValidRepo === "no-valid-url") {
stopDeployment(`This URL is neither a valid repository from GitHub, nor from GitLab.`) stopDeployment(
`This URL is neither a valid repository from GitHub, nor from GitLab.`
);
} else if (isValidRepo) { } else if (isValidRepo) {
const gitRef = gitRepo.ref ? `with "${chalk.bold(gitRepo.ref)}" ` : '' const gitRef = gitRepo.ref ? `with "${chalk.bold(gitRepo.ref)}" ` : "";
stopDeployment(`There's no repository named "${chalk.bold(gitRepo.main)}" ${gitRef}on ${gitRepo.type}`) stopDeployment(
`There's no repository named "${chalk.bold(gitRepo.main)}" ${gitRef}on ${gitRepo.type}`
);
} else { } else {
stopDeployment(`Could not read directory ${chalk.bold(path)}`) stopDeployment(`Could not read directory ${chalk.bold(path)}`);
} }
} }
// Make sure that directory is deployable // Make sure that directory is deployable
await checkPath(path) await checkPath(path);
if (!quiet) { if (!quiet) {
if (gitRepo.main) { if (gitRepo.main) {
const gitRef = gitRepo.ref ? ` at "${chalk.bold(gitRepo.ref)}" ` : '' const gitRef = gitRepo.ref ? ` at "${chalk.bold(gitRepo.ref)}" ` : "";
console.log(`> Deploying ${gitRepo.type} repository "${chalk.bold(gitRepo.main)}"` + gitRef) console.log(
`> Deploying ${gitRepo.type} repository "${chalk.bold(gitRepo.main)}"` +
gitRef
);
} else { } else {
console.log(`> Deploying ${chalk.bold(toHumanPath(path))}`) console.log(`> Deploying ${chalk.bold(toHumanPath(path))}`);
} }
} }
let deploymentType let deploymentType;
let hasPackage let hasPackage;
let hasDockerfile let hasDockerfile;
let isStatic let isStatic;
if (argv.docker) { if (argv.docker) {
if (debug) { if (debug) {
console.log(`> [debug] Forcing \`deploymentType\` = \`docker\``) console.log(`> [debug] Forcing \`deploymentType\` = \`docker\``);
} }
deploymentType = 'docker' deploymentType = "docker";
} else if (argv.npm) { } else if (argv.npm) {
deploymentType = 'npm' deploymentType = "npm";
} else if (argv.static) { } else if (argv.static) {
if (debug) { if (debug) {
console.log(`> [debug] Forcing static deployment`) console.log(`> [debug] Forcing static deployment`);
} }
deploymentType = 'npm' deploymentType = "npm";
isStatic = true isStatic = true;
} else { } else {
try { try {
await fs.stat(resolve(path, 'package.json')) await fs.stat(resolve(path, "package.json"));
} catch (err) { } catch (err) {
hasPackage = true hasPackage = true;
} }
[hasPackage, hasDockerfile] = await Promise.all([ [hasPackage, hasDockerfile] = await Promise.all([
await (async () => { await (async () => {
try { try {
await fs.stat(resolve(path, 'package.json')) await fs.stat(resolve(path, "package.json"));
} catch (err) { } catch (err) {
return false return false;
} }
return true return true;
})(), })(),
await (async () => { await (async () => {
try { try {
await fs.stat(resolve(path, 'Dockerfile')) await fs.stat(resolve(path, "Dockerfile"));
} catch (err) { } catch (err) {
return false return false;
} }
return true return true;
})() })()
]) ]);
if (hasPackage && hasDockerfile) { if (hasPackage && hasDockerfile) {
if (debug) { if (debug) {
console.log('[debug] multiple manifests found, disambiguating') console.log("[debug] multiple manifests found, disambiguating");
} }
if (isTTY) { if (isTTY) {
try { try {
console.log(`> Two manifests found. Press [${chalk.bold('n')}] to deploy or re-run with --flag`) console.log(
`> Two manifests found. Press [${chalk.bold("n")}] to deploy or re-run with --flag`
);
deploymentType = await promptOptions([ deploymentType = await promptOptions([
['npm', `${chalk.bold('package.json')}\t${chalk.gray(' --npm')} `], [
['docker', `${chalk.bold('Dockerfile')}\t${chalk.gray('--docker')} `] "npm",
]) `${chalk.bold("package.json")}\t${chalk.gray(" --npm")} `
],
[
"docker",
`${chalk.bold("Dockerfile")}\t${chalk.gray("--docker")} `
]
]);
} catch (err) { } catch (err) {
error(err.message) error(err.message);
process.exit(1) process.exit(1);
} }
} else { } else {
error('Ambiguous deployment (`package.json` and `Dockerfile` found). ' + error(
'Please supply `--npm` or `--docker` to disambiguate.') "Ambiguous deployment (`package.json` and `Dockerfile` found). " +
"Please supply `--npm` or `--docker` to disambiguate."
);
} }
} else if (hasPackage) { } else if (hasPackage) {
if (debug) { if (debug) {
console.log('> [debug] `package.json` found, assuming `deploymentType` = `npm`') console.log(
"> [debug] `package.json` found, assuming `deploymentType` = `npm`"
);
} }
deploymentType = 'npm' deploymentType = "npm";
} else if (hasDockerfile) { } else if (hasDockerfile) {
if (debug) { if (debug) {
console.log('> [debug] `Dockerfile` found, assuming `deploymentType` = `docker`') console.log(
"> [debug] `Dockerfile` found, assuming `deploymentType` = `docker`"
);
} }
deploymentType = 'docker' deploymentType = "docker";
} else { } else {
if (debug) { if (debug) {
console.log('> [debug] No manifest files found, assuming static deployment') console.log(
"> [debug] No manifest files found, assuming static deployment"
);
} }
isStatic = true isStatic = true;
} }
} }
const {nowConfig} = await readMetaData(path, { const { nowConfig } = await readMetaData(path, {
deploymentType, deploymentType,
deploymentName, deploymentName,
isStatic, isStatic,
quiet: true quiet: true
}) });
const now = new Now(apiUrl, token, {debug}) const now = new Now(apiUrl, token, { debug });
let dotenvConfig let dotenvConfig;
let dotenvOption let dotenvOption;
if (argv.dotenv) { if (argv.dotenv) {
dotenvOption = argv.dotenv dotenvOption = argv.dotenv;
} else if (nowConfig && nowConfig.dotenv) { } else if (nowConfig && nowConfig.dotenv) {
dotenvOption = nowConfig.dotenv dotenvOption = nowConfig.dotenv;
} }
if (dotenvOption) { if (dotenvOption) {
const dotenvFileName = typeof dotenvOption === 'string' ? dotenvOption : '.env' const dotenvFileName = typeof dotenvOption === "string"
? dotenvOption
: ".env";
if (!fs.existsSync(dotenvFileName)) { if (!fs.existsSync(dotenvFileName)) {
error(`--dotenv flag is set but ${dotenvFileName} file is missing`) error(`--dotenv flag is set but ${dotenvFileName} file is missing`);
return process.exit(1) return process.exit(1);
} }
const dotenvFile = await fs.readFile(dotenvFileName) const dotenvFile = await fs.readFile(dotenvFileName);
dotenvConfig = dotenv.parse(dotenvFile) dotenvConfig = dotenv.parse(dotenvFile);
} }
// Merge `now.env` from package.json with `-e` arguments. // Merge `now.env` from package.json with `-e` arguments.
const pkgEnv = nowConfig && nowConfig.env const pkgEnv = nowConfig && nowConfig.env;
const envs = [ const envs = [
...Object.keys(dotenvConfig || {}).map(k => `${k}=${dotenvConfig[k]}`), ...Object.keys(dotenvConfig || {}).map(k => `${k}=${dotenvConfig[k]}`),
...Object.keys(pkgEnv || {}).map(k => `${k}=${pkgEnv[k]}`), ...Object.keys(pkgEnv || {}).map(k => `${k}=${pkgEnv[k]}`),
...[].concat(argv.env || []) ...[].concat(argv.env || [])
] ];
let secrets let secrets;
const findSecret = async uidOrName => { const findSecret = async uidOrName => {
if (!secrets) { if (!secrets) {
secrets = await now.listSecrets() secrets = await now.listSecrets();
} }
return secrets.filter(secret => { return secrets.filter(secret => {
return secret.name === uidOrName || secret.uid === uidOrName return secret.name === uidOrName || secret.uid === uidOrName;
}) });
} };
const env_ = await Promise.all(envs.map(async kv => { const env_ = await Promise.all(
if (typeof kv !== 'string') { envs.map(async kv => {
error('Env key and value missing') if (typeof kv !== "string") {
return process.exit(1) error("Env key and value missing");
} return process.exit(1);
}
const [key, ...rest] = kv.split('=')
let val
if (rest.length > 0) { const [key, ...rest] = kv.split("=");
val = rest.join('=') let val;
}
if (/[^A-z0-9_]/i.test(key)) { if (rest.length > 0) {
error(`Invalid ${chalk.dim('-e')} key ${chalk.bold(`"${chalk.bold(key)}"`)}. Only letters, digits and underscores are allowed.`) val = rest.join("=");
return process.exit(1) }
}
if (!key) { if (/[^A-z0-9_]/i.test(key)) {
error(`Invalid env option ${chalk.bold(`"${kv}"`)}`) error(
return process.exit(1) `Invalid ${chalk.dim("-e")} key ${chalk.bold(`"${chalk.bold(key)}"`)}. Only letters, digits and underscores are allowed.`
} );
return process.exit(1);
}
if (val === undefined) { if (!key) {
if ((key in process.env)) { error(`Invalid env option ${chalk.bold(`"${kv}"`)}`);
console.log(`> Reading ${chalk.bold(`"${chalk.bold(key)}"`)} from your env (as no value was specified)`) return process.exit(1);
// escape value if it begins with @
val = process.env[key].replace(/^@/, '\\@')
} else {
error(`No value specified for env ${chalk.bold(`"${chalk.bold(key)}"`)} and it was not found in your env.`)
return process.exit(1)
} }
}
if (val[0] === '@') { if (val === undefined) {
const uidOrName = val.substr(1) if (key in process.env) {
const secrets = await findSecret(uidOrName) console.log(
if (secrets.length === 0) { `> Reading ${chalk.bold(`"${chalk.bold(key)}"`)} from your env (as no value was specified)`
if (uidOrName === '') { );
error(`Empty reference provided for env key ${chalk.bold(`"${chalk.bold(key)}"`)}`) // escape value if it begins with @
val = process.env[key].replace(/^@/, "\\@");
} else { } else {
error(`No secret found by uid or name ${chalk.bold(`"${uidOrName}"`)}`) error(
`No value specified for env ${chalk.bold(`"${chalk.bold(key)}"`)} and it was not found in your env.`
);
return process.exit(1);
} }
return process.exit(1)
} else if (secrets.length > 1) {
error(`Ambiguous secret ${chalk.bold(`"${uidOrName}"`)} (matches ${chalk.bold(secrets.length)} secrets)`)
return process.exit(1)
} }
val = {uid: secrets[0].uid} if (val[0] === "@") {
} const uidOrName = val.substr(1);
const secrets = await findSecret(uidOrName);
if (secrets.length === 0) {
if (uidOrName === "") {
error(
`Empty reference provided for env key ${chalk.bold(`"${chalk.bold(key)}"`)}`
);
} else {
error(
`No secret found by uid or name ${chalk.bold(`"${uidOrName}"`)}`
);
}
return process.exit(1);
} else if (secrets.length > 1) {
error(
`Ambiguous secret ${chalk.bold(`"${uidOrName}"`)} (matches ${chalk.bold(secrets.length)} secrets)`
);
return process.exit(1);
}
return [ val = { uid: secrets[0].uid };
key, }
typeof val === 'string' ? val.replace(/^\\@/, '@') : val
]
}))
const env = {} return [key, typeof val === "string" ? val.replace(/^\\@/, "@") : val];
env_ })
.filter(v => Boolean(v)) );
.forEach(([key, val]) => {
const env = {};
env_.filter(v => Boolean(v)).forEach(([key, val]) => {
if (key in env) { if (key in env) {
console.log(`> ${chalk.yellow('NOTE:')} Overriding duplicate env key ${chalk.bold(`"${key}"`)}`) console.log(
`> ${chalk.yellow("NOTE:")} Overriding duplicate env key ${chalk.bold(`"${key}"`)}`
);
} }
env[key] = val env[key] = val;
}) });
try { try {
await now.create(path, { await now.create(path, {
@ -502,138 +544,146 @@ async function sync(token) {
quiet, quiet,
wantsPublic, wantsPublic,
isStatic isStatic
}) });
} catch (err) { } catch (err) {
if (debug) { if (debug) {
console.log(`> [debug] error: ${err}\n${err.stack}`) console.log(`> [debug] error: ${err}\n${err.stack}`);
} }
handleError(err) handleError(err);
process.exit(1) process.exit(1);
} }
const {url} = now const { url } = now;
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
if (isTTY) { if (isTTY) {
if (clipboard) { if (clipboard) {
try { try {
await copy(url) await copy(url);
console.log(`${chalk.cyan('> Ready!')} ${chalk.bold(url)} (copied to clipboard) [${elapsed}]`) console.log(
`${chalk.cyan("> Ready!")} ${chalk.bold(url)} (copied to clipboard) [${elapsed}]`
);
} catch (err) { } catch (err) {
console.log(`${chalk.cyan('> Ready!')} ${chalk.bold(url)} [${elapsed}]`) console.log(
`${chalk.cyan("> Ready!")} ${chalk.bold(url)} [${elapsed}]`
);
} }
} else { } else {
console.log(`> ${url} [${elapsed}]`) console.log(`> ${url} [${elapsed}]`);
} }
} else { } else {
process.stdout.write(url) process.stdout.write(url);
} }
const startU = new Date() const startU = new Date();
const complete = () => { const complete = () => {
if (!quiet) { if (!quiet) {
const elapsedU = ms(new Date() - startU) const elapsedU = ms(new Date() - startU);
console.log(`> Sync complete (${bytes(now.syncAmount)}) [${elapsedU}] `) console.log(`> Sync complete (${bytes(now.syncAmount)}) [${elapsedU}] `);
console.log('> Initializing…') console.log("> Initializing…");
} }
// close http2 agent // close http2 agent
now.close() now.close();
// show build logs // show build logs
printLogs(now.host, token) printLogs(now.host, token);
} };
if (now.syncAmount) { if (now.syncAmount) {
const bar = new Progress('> Upload [:bar] :percent :etas', { const bar = new Progress("> Upload [:bar] :percent :etas", {
width: 20, width: 20,
complete: '=', complete: "=",
incomplete: '', incomplete: "",
total: now.syncAmount total: now.syncAmount
}) });
now.upload() now.upload();
now.on('upload', ({names, data}) => { now.on("upload", ({ names, data }) => {
const amount = data.length const amount = data.length;
if (debug) { if (debug) {
console.log(`> [debug] Uploaded: ${names.join(' ')} (${bytes(data.length)})`) console.log(
`> [debug] Uploaded: ${names.join(" ")} (${bytes(data.length)})`
);
} }
bar.tick(amount) bar.tick(amount);
}) });
now.on('complete', complete) now.on("complete", complete);
now.on('error', err => { now.on("error", err => {
error('Upload failed') error("Upload failed");
handleError(err) handleError(err);
process.exit(1) process.exit(1);
}) });
} else { } else {
if (!quiet) { if (!quiet) {
console.log(`> Initializing…`) console.log(`> Initializing…`);
} }
// close http2 agent // close http2 agent
now.close() now.close();
// show build logs // show build logs
printLogs(now.host, token) printLogs(now.host, token);
} }
} }
function printLogs(host, token) { function printLogs(host, token) {
// log build // log build
const logger = new Logger(host, {debug, quiet}) const logger = new Logger(host, { debug, quiet });
logger.on('error', async err => { logger.on("error", async err => {
if (!quiet) { if (!quiet) {
if (err && err.type === 'BUILD_ERROR') { if (err && err.type === "BUILD_ERROR") {
error(`The build step of your project failed. To retry, run ${cmd('now --force')}.`) error(
`The build step of your project failed. To retry, run ${cmd("now --force")}.`
);
} else { } else {
error('Deployment failed') error("Deployment failed");
} }
} }
if (gitRepo && gitRepo.cleanup) { if (gitRepo && gitRepo.cleanup) {
// Delete temporary directory that contains repository // Delete temporary directory that contains repository
gitRepo.cleanup() gitRepo.cleanup();
if (debug) { if (debug) {
console.log(`> [debug] Removed temporary repo directory`) console.log(`> [debug] Removed temporary repo directory`);
} }
} }
process.exit(1) process.exit(1);
}) });
logger.on('close', async () => { logger.on("close", async () => {
if (Array.isArray(autoAliases)) { if (Array.isArray(autoAliases)) {
const aliasList = autoAliases.filter(item => item !== '') const aliasList = autoAliases.filter(item => item !== "");
if (aliasList.length > 0) { if (aliasList.length > 0) {
for (const alias of aliasList) { for (const alias of aliasList) {
await assignAlias(alias, token, host, apiUrl, debug) await assignAlias(alias, token, host, apiUrl, debug);
} }
} else { } else {
await reAlias(token, host, help, exit, apiUrl, debug) await reAlias(token, host, help, exit, apiUrl, debug);
} }
} }
if (!quiet) { if (!quiet) {
console.log(`${chalk.cyan('> Deployment complete!')}`) console.log(`${chalk.cyan("> Deployment complete!")}`);
} }
if (gitRepo && gitRepo.cleanup) { if (gitRepo && gitRepo.cleanup) {
// Delete temporary directory that contains repository // Delete temporary directory that contains repository
gitRepo.cleanup() gitRepo.cleanup();
if (debug) { if (debug) {
console.log(`> [debug] Removed temporary repo directory`) console.log(`> [debug] Removed temporary repo directory`);
} }
} }
process.exit(0) process.exit(0);
}) });
} }

325
bin/now-dns.js

@ -1,193 +1,224 @@
#!/usr/bin/env node #!/usr/bin/env node
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
const minimist = require('minimist') const minimist = require("minimist");
const ms = require('ms') const ms = require("ms");
const table = require('text-table') const table = require("text-table");
// Ours // Ours
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const DomainRecords = require('../lib/domain-records') const DomainRecords = require("../lib/domain-records");
const indent = require('../lib/indent') const indent = require("../lib/indent");
const login = require('../lib/login') const login = require("../lib/login");
const strlen = require('../lib/strlen') const strlen = require("../lib/strlen");
const {handleError, error} = require('../lib/error') const { handleError, error } = require("../lib/error");
const exit = require('../lib/utils/exit') const exit = require("../lib/utils/exit");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config'], string: ["config"],
boolean: ['help', 'debug'], boolean: ["help", "debug"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
token: 't' token: "t"
} }
}) });
const subcommand = argv._[0] const subcommand = argv._[0];
// options // options
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now dns ls`)} [domain] ${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> <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> MX <value> <mx_priority>
${chalk.bold(`${logo} now dns add`)} <domain> <name> SRV <priority> <weight> <port> <target> ${chalk.bold(`${logo} now dns add`)} <domain> <name> SRV <priority> <weight> <port> <target>
${chalk.bold(`${logo} now dns rm`)} <id> ${chalk.bold(`${logo} now dns rm`)} <id>
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help output usage information -h, --help output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} config file -c ${chalk.bold.underline("FILE")}, --config=${chalk.bold.underline("FILE")} config file
-d, --debug debug mode [off] -d, --debug debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} login token
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} List all your DNS records ${chalk.gray("–")} List all your DNS records
${chalk.cyan('$ now dns ls')} ${chalk.cyan("$ now dns ls")}
${chalk.gray('–')} Add an A record for a subdomain ${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 <YOUR DOMAIN> <SUBDOMAIN NAME> A <RECORD VALUE>")}
${chalk.cyan('$ now dns add zeit.rocks api A 198.51.100.100')} ${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.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 <YOUR DOMAIN> @ MX <RECORD VALUE> <PRIORITY>")}
${chalk.cyan('$ now dns add zeit.rocks @ MX mail.zeit.rocks 10')} ${chalk.cyan("$ now dns add zeit.rocks @ MX mail.zeit.rocks 10")}
`) `
} );
};
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
if (argv.help || !subcommand) { if (argv.help || !subcommand) {
help() help();
exit(0) exit(0);
} else { } else {
const config = cfg.read() const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await run(token) await run(token);
} catch (err) { } catch (err) {
handleError(err) handleError(err);
exit(1) exit(1);
} }
}) })
.catch(e => { .catch(e => {
error(`Authentication error – ${e.message}`) error(`Authentication error – ${e.message}`);
exit(1) exit(1);
}) });
} }
async function run(token) { async function run(token) {
const domainRecords = new DomainRecords(apiUrl, token, {debug}) const domainRecords = new DomainRecords(apiUrl, token, { debug });
const args = argv._.slice(1) const args = argv._.slice(1);
const start = Date.now() const start = Date.now();
if (subcommand === 'ls' || subcommand === 'list') { if (subcommand === "ls" || subcommand === "list") {
if (args.length > 1) { if (args.length > 1) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now dns ls [domain]`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now dns ls [domain]`")}`
);
return exit(1);
} }
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
const res = await domainRecords.ls(args[0]) const res = await domainRecords.ls(args[0]);
const text = [] const text = [];
let count = 0 let count = 0;
res.forEach((records, domain) => { res.forEach((records, domain) => {
count += records.length count += records.length;
if (records.length > 0) { if (records.length > 0) {
const cur = Date.now() const cur = Date.now();
const header = [['', 'id', 'name', 'type', 'value', 'aux', 'created'].map(s => chalk.dim(s))] const header = [
const out = table(header.concat(records.map(record => { ["", "id", "name", "type", "value", "aux", "created"].map(s =>
const time = chalk.gray(ms(cur - new Date(Number(record.created))) + ' ago') chalk.dim(s))
return [ ];
'', const out = table(
record.id, header.concat(
record.name, records.map(record => {
record.type, const time = chalk.gray(
record.value, ms(cur - new Date(Number(record.created))) + " ago"
record.mxPriority || record.priority || '', );
time return [
] "",
})), {align: ['l', 'r', 'l', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) record.id,
text.push(`\n\n${chalk.bold(domain)}\n${indent(out, 2)}`) record.name,
record.type,
record.value,
record.mxPriority || record.priority || "",
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}]`)}`) console.log(
console.log(text.join('')) `> ${count} record${count === 1 ? "" : "s"} found ${chalk.gray(`[${elapsed}]`)}`
} else if (subcommand === 'add') { );
const param = parseAddArgs(args) console.log(text.join(""));
} else if (subcommand === "add") {
const param = parseAddArgs(args);
if (!param) { if (!param) {
error(`Invalid number of arguments. See: ${chalk.cyan('`now dns --help`')} for usage.`) error(
return exit(1) `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 record = await domainRecords.create(param.domain, param.data);
const elapsed = ms(new Date() - start) 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}]`)}`) console.log(
} else if (subcommand === 'rm' || subcommand === 'remove') { `${chalk.cyan("> Success!")} A new DNS record for domain ${chalk.bold(param.domain)} ${chalk.gray(`(${record.uid})`)} created ${chalk.gray(`[${elapsed}]`)}`
);
} else if (subcommand === "rm" || subcommand === "remove") {
if (args.length !== 1) { if (args.length !== 1) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now dns rm <id>`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now dns rm <id>`")}`
);
return exit(1);
} }
const record = await domainRecords.getRecord(args[0]) const record = await domainRecords.getRecord(args[0]);
if (!record) { if (!record) {
error('DNS record not found') error("DNS record not found");
return exit(1) return exit(1);
} }
const yes = await readConfirmation(record, 'The following record will be removed permanently\n') const yes = await readConfirmation(
record,
"The following record will be removed permanently\n"
);
if (!yes) { if (!yes) {
error('User abort') error("User abort");
return exit(0) return exit(0);
} }
await domainRecords.delete(record.domain, record.id) await domainRecords.delete(record.domain, record.id);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Record ${chalk.gray(`${record.id}`)} removed ${chalk.gray(`[${elapsed}]`)}`) console.log(
`${chalk.cyan("> Success!")} Record ${chalk.gray(`${record.id}`)} removed ${chalk.gray(`[${elapsed}]`)}`
);
} else { } else {
error('Please specify a valid subcommand: ls | add | rm') error("Please specify a valid subcommand: ls | add | rm");
help() help();
exit(1) exit(1);
} }
return domainRecords.close() return domainRecords.close();
} }
process.on('uncaughtException', err => { process.on("uncaughtException", err => {
handleError(err) handleError(err);
exit(1) exit(1);
}) });
function parseAddArgs(args) { function parseAddArgs(args) {
if (!args || args.length < 4) { if (!args || args.length < 4) {
return null return null;
} }
const domain = args[0] const domain = args[0];
const name = args[1] === '@' ? '' : args[1] const name = args[1] === "@" ? "" : args[1];
const type = args[2] const type = args[2];
const value = args[3] const value = args[3];
if (!(domain && typeof name === 'string' && type)) { if (!(domain && typeof name === "string" && type)) {
return null return null;
} }
if (type === 'MX') { if (type === "MX") {
if (args.length !== 5) { if (args.length !== 5) {
return null return null;
} }
return { return {
@ -198,10 +229,10 @@ function parseAddArgs(args) {
value, value,
mxPriority: args[4] mxPriority: args[4]
} }
} };
} else if (type === 'SRV') { } else if (type === "SRV") {
if (args.length !== 7) { if (args.length !== 7) {
return null return null;
} }
return { return {
@ -216,11 +247,11 @@ function parseAddArgs(args) {
target: args[6] target: args[6]
} }
} }
} };
} }
if (args.length !== 4) { if (args.length !== 4) {
return null return null;
} }
return { return {
@ -230,27 +261,39 @@ function parseAddArgs(args) {
type, type,
value value
} }
} };
} }
function readConfirmation(record, msg) { function readConfirmation(record, msg) {
return new Promise(resolve => { return new Promise(resolve => {
const time = chalk.gray(ms(new Date() - new Date(Number(record.created))) + ' ago') const time = chalk.gray(
ms(new Date() - new Date(Number(record.created))) + " ago"
);
const tbl = table( const tbl = table(
[[record.id, [
chalk.bold(`${record.name.length > 0 ? record.name + '.' : ''}${record.domain} ${record.type} ${record.value} ${record.mxPriority ? record.mxPriority : ''}`), [
time]], record.id,
{align: ['l', 'r', 'l'], hsep: ' '.repeat(6)} chalk.bold(
) `${record.name.length > 0 ? record.name + "." : ""}${record.domain} ${record.type} ${record.value} ${record.mxPriority ? record.mxPriority : ""}`
),
process.stdout.write(`> ${msg}`) time
process.stdout.write(' ' + tbl + '\n') ]
],
process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`) { align: ["l", "r", "l"], hsep: " ".repeat(6) }
);
process.stdin.on('data', d => {
process.stdin.pause() process.stdout.write(`> ${msg}`);
resolve(d.toString().trim().toLowerCase() === 'y') process.stdout.write(" " + tbl + "\n");
}).resume()
}) 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();
});
} }

376
bin/now-domains.js

@ -1,291 +1,333 @@
#!/usr/bin/env node #!/usr/bin/env node
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
const minimist = require('minimist') const minimist = require("minimist");
const table = require('text-table') const table = require("text-table");
const ms = require('ms') const ms = require("ms");
// Ours // Ours
const login = require('../lib/login') const login = require("../lib/login");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const {error} = require('../lib/error') const { error } = require("../lib/error");
const toHost = require('../lib/to-host') const toHost = require("../lib/to-host");
const strlen = require('../lib/strlen') const strlen = require("../lib/strlen");
const NowDomains = require('../lib/domains') const NowDomains = require("../lib/domains");
const exit = require('../lib/utils/exit') const exit = require("../lib/utils/exit");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'], string: ["config", "token"],
boolean: ['help', 'debug', 'external', 'force'], boolean: ["help", "debug", "external", "force"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
external: 'e', external: "e",
force: 'f', force: "f",
token: 't' token: "t"
} }
}) });
const subcommand = argv._[0] const subcommand = argv._[0];
// options // options
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now domains`)} <ls | add | rm> <domain> ${chalk.bold(`${logo} now domains`)} <ls | add | rm> <domain>
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -c ${chalk.bold.underline("FILE")}, --config=${chalk.bold.underline("FILE")} Config file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-e, --external Use external DNS server -e, --external Use external DNS server
-f, --force Skip DNS verification -f, --force Skip DNS verification
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} Login token
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} Lists all your domains: ${chalk.gray("–")} Lists all your domains:
${chalk.cyan('$ now domains ls')} ${chalk.cyan("$ now domains ls")}
${chalk.gray('–')} Adds a domain name: ${chalk.gray("–")} Adds a domain name:
${chalk.cyan(`$ now domains add ${chalk.underline('my-app.com')}`)} ${chalk.cyan(`$ now domains add ${chalk.underline("my-app.com")}`)}
Make sure the domain's DNS nameservers are at least 2 of these: 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("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("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("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("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("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("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("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("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("frankfurt.zeit.world")} ${chalk.dim("91.109.245.139")}
${chalk.gray('–')} ${chalk.underline('iowa.zeit.world')} ${chalk.dim('23.236.59.22')} ${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 ${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`')}). if it's configured with these nameservers (no need to ${chalk.dim("`domain add`")}).
For more details head to ${chalk.underline('https://zeit.world')}. For more details head to ${chalk.underline("https://zeit.world")}.
${chalk.gray('–')} Removing a domain: ${chalk.gray("–")} Removing a domain:
${chalk.cyan('$ now domain rm my-app.com')} ${chalk.cyan("$ now domain rm my-app.com")}
or or
${chalk.cyan('$ now domain rm domainId')} ${chalk.cyan("$ now domain rm domainId")}
To get the list of domain ids, use ${chalk.dim('`now domains ls`')}. 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.gray("–")} Adding and verifying a domain name using zeit.world nameservers:
${chalk.cyan('$ now domain add my-app.com')} ${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. 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.gray("–")} Adding and verifying a domain name using an external nameserver:
${chalk.cyan('$ now domain add -e my-app.com')} ${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. and follow the verification instructions if requested. Finally, rerun the same command after completing the verification step.
`) `
} );
};
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
if (argv.help || !subcommand) { if (argv.help || !subcommand) {
help() help();
exit(0) exit(0);
} else { } else {
const config = cfg.read() const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await run(token) await run(token);
} catch (err) { } catch (err) {
if (err.userError) { if (err.userError) {
error(err.message) error(err.message);
} else { } else {
error(`Unknown error: ${err}\n${err.stack}`) error(`Unknown error: ${err}\n${err.stack}`);
}
exit(1);
} }
exit(1) })
} .catch(e => {
}) error(`Authentication error – ${e.message}`);
.catch(e => { exit(1);
error(`Authentication error – ${e.message}`) });
exit(1)
})
} }
async function run(token) { async function run(token) {
const domain = new NowDomains(apiUrl, token, {debug}) const domain = new NowDomains(apiUrl, token, { debug });
const args = argv._.slice(1) const args = argv._.slice(1);
switch (subcommand) { switch (subcommand) {
case 'ls': case "ls":
case 'list': { case "list": {
if (args.length !== 0) { if (args.length !== 0) {
error('Invalid number of arguments') error("Invalid number of arguments");
return exit(1) return exit(1);
} }
const start_ = new Date() const start_ = new Date();
const domains = await domain.ls() const domains = await domain.ls();
domains.sort((a, b) => new Date(b.created) - new Date(a.created)) domains.sort((a, b) => new Date(b.created) - new Date(a.created));
const current = new Date() const current = new Date();
const header = [['', 'id', 'dns', 'url', 'verified', 'created'].map(s => chalk.dim(s))] const header = [
const out = domains.length === 0 ? null : table(header.concat(domains.map(domain => { ["", "id", "dns", "url", "verified", "created"].map(s => chalk.dim(s))
const ns = domain.isExternal ? 'external' : 'zeit.world' ];
const url = chalk.underline(`https://${domain.name}`) const out = domains.length === 0
const time = chalk.gray(ms(current - new Date(domain.created)) + ' ago') ? null
return [ : table(
'', header.concat(
domain.uid, domains.map(domain => {
ns, const ns = domain.isExternal ? "external" : "zeit.world";
url, const url = chalk.underline(`https://${domain.name}`);
domain.verified, const time = chalk.gray(
time ms(current - new Date(domain.created)) + " ago"
] );
})), {align: ['l', 'r', 'l', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) return ["", domain.uid, ns, url, domain.verified, time];
})
const elapsed_ = ms(new Date() - start_) ),
console.log(`> ${domains.length} domain${domains.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed_}]`)}`) {
align: ["l", "r", "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 ${chalk.gray(`[${elapsed_}]`)}`
);
if (out) { if (out) {
console.log('\n' + out + '\n') console.log("\n" + out + "\n");
} }
break break;
} }
case 'rm': case "rm":
case 'remove': { case "remove": {
if (args.length !== 1) { if (args.length !== 1) {
error('Invalid number of arguments') error("Invalid number of arguments");
return exit(1) return exit(1);
} }
const _target = String(args[0]) const _target = String(args[0]);
if (!_target) { if (!_target) {
const err = new Error('No domain specified') const err = new Error("No domain specified");
err.userError = true err.userError = true;
throw err throw err;
} }
const _domains = await domain.ls() const _domains = await domain.ls();
const _domain = findDomain(_target, _domains) const _domain = findDomain(_target, _domains);
if (!_domain) { if (!_domain) {
const err = new Error(`Domain not found by "${_target}". Run ${chalk.dim('`now domains ls`')} to see your domains.`) const err = new Error(
err.userError = true `Domain not found by "${_target}". Run ${chalk.dim("`now domains ls`")} to see your domains.`
throw err );
err.userError = true;
throw err;
} }
try { try {
const confirmation = (await readConfirmation(domain, _domain, _domains)).toLowerCase() const confirmation = (await readConfirmation(
if (confirmation !== 'y' && confirmation !== 'yes') { domain,
console.log('\n> Aborted') _domain,
process.exit(0) _domains
)).toLowerCase();
if (confirmation !== "y" && confirmation !== "yes") {
console.log("\n> Aborted");
process.exit(0);
} }
const start = new Date() const start = new Date();
await domain.rm(_domain.name) await domain.rm(_domain.name);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(_domain.uid)} removed [${elapsed}]`) console.log(
`${chalk.cyan("> Success!")} Domain ${chalk.bold(_domain.uid)} removed [${elapsed}]`
);
} catch (err) { } catch (err) {
error(err) error(err);
exit(1) exit(1);
} }
break break;
} }
case 'add': case "add":
case 'set': { case "set": {
if (args.length !== 1) { if (args.length !== 1) {
error('Invalid number of arguments') error("Invalid number of arguments");
return exit(1) return exit(1);
} }
const start = new Date() const start = new Date();
const name = String(args[0]) const name = String(args[0]);
const {uid, code, created, verified} = await domain.add(name, argv.force, argv.external) const { uid, code, created, verified } = await domain.add(
const elapsed = ms(new Date() - start) name,
argv.force,
argv.external
);
const elapsed = ms(new Date() - start);
if (created) { if (created) {
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} added [${elapsed}]`) console.log(
`${chalk.cyan("> Success!")} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} added [${elapsed}]`
);
} else if (verified) { } else if (verified) {
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} verified [${elapsed}]`) console.log(
} else if (code === 'not_modified') { `${chalk.cyan("> Success!")} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} verified [${elapsed}]`
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} already exists [${elapsed}]`) );
} else if (code === "not_modified") {
console.log(
`${chalk.cyan("> Success!")} Domain ${chalk.bold(chalk.underline(name))} ${chalk.dim(`(${uid})`)} already exists [${elapsed}]`
);
} else { } else {
console.log('> Verification required: Please rerun this command after some time') console.log(
"> Verification required: Please rerun this command after some time"
);
} }
break break;
} }
default: default:
error('Please specify a valid subcommand: ls | add | rm') error("Please specify a valid subcommand: ls | add | rm");
help() help();
exit(1) exit(1);
} }
domain.close() domain.close();
} }
async function readConfirmation(domain, _domain) { async function readConfirmation(domain, _domain) {
return new Promise(resolve => { return new Promise(resolve => {
const time = chalk.gray(ms(new Date() - new Date(_domain.created)) + ' ago') const time = chalk.gray(
ms(new Date() - new Date(_domain.created)) + " ago"
);
const tbl = table( const tbl = table(
[[_domain.uid, chalk.underline(`https://${_domain.name}`), time]], [[_domain.uid, chalk.underline(`https://${_domain.name}`), time]],
{align: ['l', 'r', 'l'], hsep: ' '.repeat(6)} { align: ["l", "r", "l"], hsep: " ".repeat(6) }
) );
process.stdout.write('> The following domain will be removed permanently\n') process.stdout.write(
process.stdout.write(' ' + tbl + '\n') "> The following domain will be removed permanently\n"
);
process.stdout.write(" " + tbl + "\n");
if (_domain.aliases.length > 0) { if (_domain.aliases.length > 0) {
process.stdout.write(`> ${chalk.yellow('Warning!')} This domain's ` + process.stdout.write(
`${chalk.bold(_domain.aliases.length + ' alias' + (_domain.aliases.length === 1 ? '' : 'es'))} ` + `> ${chalk.yellow("Warning!")} This domain's ` +
`will be removed. Run ${chalk.dim('`now alias ls`')} to list.\n`) `${chalk.bold(_domain.aliases.length + " alias" + (_domain.aliases.length === 1 ? "" : "es"))} ` +
`will be removed. Run ${chalk.dim("`now alias ls`")} to list.\n`
);
} }
process.stdout.write(` ${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/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()) process.stdin
}).resume() .on("data", d => {
}) process.stdin.pause();
resolve(d.toString().trim());
})
.resume();
});
} }
function findDomain(val, list) { function findDomain(val, list) {
return list.find(d => { return list.find(d => {
if (d.uid === val) { if (d.uid === val) {
if (debug) { if (debug) {
console.log(`> [debug] matched domain ${d.uid} by uid`) console.log(`> [debug] matched domain ${d.uid} by uid`);
} }
return true return true;
} }
// match prefix // match prefix
if (d.name === toHost(val)) { if (d.name === toHost(val)) {
if (debug) { if (debug) {
console.log(`> [debug] matched domain ${d.uid} by name ${d.name}`) console.log(`> [debug] matched domain ${d.uid} by name ${d.name}`);
} }
return true return true;
} }
return false return false;
}) });
} }

193
bin/now-list.js

@ -1,154 +1,163 @@
#!/usr/bin/env node #!/usr/bin/env node
// Packages // Packages
const fs = require('fs-promise') const fs = require("fs-promise");
const minimist = require('minimist') const minimist = require("minimist");
const chalk = require('chalk') const chalk = require("chalk");
const table = require('text-table') const table = require("text-table");
const ms = require('ms') const ms = require("ms");
// Ours // Ours
const strlen = require('../lib/strlen') const strlen = require("../lib/strlen");
const indent = require('../lib/indent') const indent = require("../lib/indent");
const Now = require('../lib') const Now = require("../lib");
const login = require('../lib/login') const login = require("../lib/login");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const {handleError, error} = require('../lib/error') const { handleError, error } = require("../lib/error");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'], string: ["config", "token"],
boolean: ['help', 'debug'], boolean: ["help", "debug"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
token: 't' token: "t"
} }
}) });
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now list`)} [app] ${chalk.bold(`${logo} now list`)} [app]
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -c ${chalk.bold.underline("FILE")}, --config=${chalk.bold.underline("FILE")} Config file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} Login token
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} List all deployments ${chalk.gray("–")} List all deployments
${chalk.cyan('$ now ls')} ${chalk.cyan("$ now ls")}
${chalk.gray('–')} List all deployments for the app ${chalk.dim('`my-app`')} ${chalk.gray("–")} List all deployments for the app ${chalk.dim("`my-app`")}
${chalk.cyan('$ now ls my-app')} ${chalk.cyan("$ now ls my-app")}
${chalk.dim('Alias:')} ls ${chalk.dim("Alias:")} ls
`) `
} );
};
if (argv.help) { if (argv.help) {
help() help();
process.exit(0) process.exit(0);
} }
const app = argv._[0] const app = argv._[0];
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
const config = cfg.read() const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await list(token) await list(token);
} catch (err) { } catch (err) {
error(`Unknown error: ${err}\n${err.stack}`) error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1) process.exit(1);
} }
}) })
.catch(e => { .catch(e => {
error(`Authentication error – ${e.message}`) error(`Authentication error – ${e.message}`);
process.exit(1) process.exit(1);
}) });
async function list(token) { async function list(token) {
const now = new Now(apiUrl, token, {debug}) const now = new Now(apiUrl, token, { debug });
const start = new Date() const start = new Date();
let deployments let deployments;
try { try {
deployments = await now.list(app) deployments = await now.list(app);
} catch (err) { } catch (err) {
handleError(err) handleError(err);
process.exit(1) process.exit(1);
} }
now.close() now.close();
const apps = new Map() const apps = new Map();
for (const dep of deployments) { for (const dep of deployments) {
const deps = apps.get(dep.name) || [] const deps = apps.get(dep.name) || [];
apps.set(dep.name, deps.concat(dep)) apps.set(dep.name, deps.concat(dep));
} }
const sorted = await sort([...apps]) const sorted = await sort([...apps]);
const current = Date.now() const current = Date.now();
const text = sorted.map(([name, deps]) => { const text = sorted
const t = table(deps.map(({uid, url, created}) => { .map(([name, deps]) => {
const _url = url ? chalk.underline(`https://${url}`) : 'incomplete' const t = table(
const time = chalk.gray(ms(current - created) + ' ago') deps.map(({ uid, url, created }) => {
return [uid, _url, time] const _url = url ? chalk.underline(`https://${url}`) : "incomplete";
}), {align: ['l', 'r', 'l'], hsep: ' '.repeat(6), stringLength: strlen}) const time = chalk.gray(ms(current - created) + " ago");
return chalk.bold(name) + '\n\n' + indent(t, 2) return [uid, _url, time];
}).join('\n\n') }),
{ align: ["l", "r", "l"], hsep: " ".repeat(6), stringLength: strlen }
);
return chalk.bold(name) + "\n\n" + indent(t, 2);
})
.join("\n\n");
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`> ${deployments.length} deployment${deployments.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`) console.log(
`> ${deployments.length} deployment${deployments.length === 1 ? "" : "s"} found ${chalk.gray(`[${elapsed}]`)}`
);
if (text) { if (text) {
console.log('\n' + text + '\n') console.log("\n" + text + "\n");
} }
} }
async function sort(apps) { async function sort(apps) {
let pkg let pkg;
try { try {
const json = await fs.readFile('package.json') const json = await fs.readFile("package.json");
pkg = JSON.parse(json) pkg = JSON.parse(json);
} catch (err) { } catch (err) {
pkg = {} pkg = {};
} }
return apps return apps
.map(([name, deps]) => { .map(([name, deps]) => {
deps = deps.slice().sort((a, b) => { deps = deps.slice().sort((a, b) => {
return b.created - a.created return b.created - a.created;
});
return [name, deps];
}) })
return [name, deps] .sort(([nameA, depsA], [nameB, depsB]) => {
}) if (pkg.name === nameA) {
.sort(([nameA, depsA], [nameB, depsB]) => { return -1;
if (pkg.name === nameA) { }
return -1
}
if (pkg.name === nameB) { if (pkg.name === nameB) {
return 1 return 1;
} }
return depsB[0].created - depsA[0].created return depsB[0].created - depsA[0].created;
}) });
} }

156
bin/now-open.js

@ -1,141 +1,143 @@
#!/usr/bin/env node #!/usr/bin/env node
// Packages // Packages
const fs = require('fs-promise') const fs = require("fs-promise");
const minimist = require('minimist') const minimist = require("minimist");
const chalk = require('chalk') const chalk = require("chalk");
const opn = require('opn') const opn = require("opn");
// Ours // Ours
const Now = require('../lib') const Now = require("../lib");
const login = require('../lib/login') const login = require("../lib/login");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const {handleError, error} = require('../lib/error') const { handleError, error } = require("../lib/error");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'], string: ["config", "token"],
boolean: ['help', 'debug'], boolean: ["help", "debug"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
token: 't' token: "t"
} }
}) });
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now open`)} ${chalk.bold(`${logo} now open`)}
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -c ${chalk.bold.underline("FILE")}, --config=${chalk.bold.underline("FILE")} Config file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} Login token
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} Open latest deployment for current project ${chalk.gray("–")} Open latest deployment for current project
${chalk.cyan('$ now open')} ${chalk.cyan("$ now open")}
`) `
} );
};
if (argv.help) { if (argv.help) {
help() help();
process.exit(0) process.exit(0);
} }
const app = argv._[0] const app = argv._[0];
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
const config = cfg.read() const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await open(token) await open(token);
} catch (err) { } catch (err) {
error(`Unknown error: ${err}\n${err.stack}`) error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1) process.exit(1);
} }
}) })
.catch(e => { .catch(e => {
error(`Authentication error – ${e.message}`) error(`Authentication error – ${e.message}`);
process.exit(1) process.exit(1);
}) });
async function open(token) { async function open(token) {
const now = new Now(apiUrl, token, {debug}) const now = new Now(apiUrl, token, { debug });
let deployments let deployments;
try { try {
deployments = await now.list(app) deployments = await now.list(app);
} catch (err) { } catch (err) {
handleError(err) handleError(err);
process.exit(1) process.exit(1);
} }
now.close() now.close();
const apps = new Map() const apps = new Map();
for (const dep of deployments) { for (const dep of deployments) {
const deps = apps.get(dep.name) || [] const deps = apps.get(dep.name) || [];
apps.set(dep.name, deps.concat(dep)) apps.set(dep.name, deps.concat(dep));
} }
let pkg let pkg;
try { try {
const json = await fs.readFile('package.json') const json = await fs.readFile("package.json");
pkg = JSON.parse(json) pkg = JSON.parse(json);
} catch (err) { } catch (err) {
pkg = {} pkg = {};
} }
const [currentProjectDeployments] = await getCurrentProjectDeployments([...apps], pkg) const [currentProjectDeployments] = await getCurrentProjectDeployments(
[...apps],
pkg
);
if (typeof currentProjectDeployments === 'undefined') { if (typeof currentProjectDeployments === "undefined") {
console.log(`no deployments found for ${chalk.bold(pkg.name)}`) console.log(`no deployments found for ${chalk.bold(pkg.name)}`);
process.exit(0) process.exit(0);
} }
const sorted = await sortByCreation([...currentProjectDeployments]) const sorted = await sortByCreation([...currentProjectDeployments]);
const latestDeploy = sorted[0] const latestDeploy = sorted[0];
try { try {
const url = `https://${latestDeploy.url}` const url = `https://${latestDeploy.url}`;
console.log(`Opening the latest deployment for ${chalk.bold(pkg.name)}...`) console.log(`Opening the latest deployment for ${chalk.bold(pkg.name)}...`);
console.log(`Here's the URL: ${chalk.underline(url)}`) console.log(`Here's the URL: ${chalk.underline(url)}`);
opn(url) opn(url);
process.exit(0) process.exit(0);
} catch (err) { } catch (err) {
error(`Unknown error: ${err}\n${err.stack}`) error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1) process.exit(1);
} }
} }
async function getCurrentProjectDeployments(apps, pkg) { async function getCurrentProjectDeployments(apps, pkg) {
return apps return apps.filter(app => pkg.name === app[0]).map(app => app[1]);
.filter(app => pkg.name === app[0])
.map(app => app[1])
} }
async function sortByCreation(deps) { async function sortByCreation(deps) {
return deps return deps.sort((depA, depB) => {
.sort((depA, depB) => { return depB.created - depA.created;
return depB.created - depA.created });
})
} }

214
bin/now-remove.js

@ -1,184 +1,202 @@
#!/usr/bin/env node #!/usr/bin/env node
// Packages // Packages
const minimist = require('minimist') const minimist = require("minimist");
const chalk = require('chalk') const chalk = require("chalk");
const ms = require('ms') const ms = require("ms");
const table = require('text-table') const table = require("text-table");
const isURL = require('is-url') const isURL = require("is-url");
// Ours // Ours
const Now = require('../lib') const Now = require("../lib");
const login = require('../lib/login') const login = require("../lib/login");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const {handleError, error} = require('../lib/error') const { handleError, error } = require("../lib/error");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'], string: ["config", "token"],
boolean: ['help', 'debug', 'hard', 'yes'], boolean: ["help", "debug", "hard", "yes"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
token: 't', token: "t",
yes: 'y' yes: "y"
} }
}) });
const ids = argv._ const ids = argv._;
// options // options
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now remove`)} deploymentId|deploymentName [...deploymentId|deploymentName] ${chalk.bold(`${logo} now remove`)} deploymentId|deploymentName [...deploymentId|deploymentName]
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -c ${chalk.bold.underline("FILE")}, --config=${chalk.bold.underline("FILE")} Config file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} Login token
-y, --yes Skip confirmation -y, --yes Skip confirmation
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} Remove a deployment identified by ${chalk.dim('`deploymentId`')}: ${chalk.gray("–")} Remove a deployment identified by ${chalk.dim("`deploymentId`")}:
${chalk.cyan('$ now rm deploymentId')} ${chalk.cyan("$ now rm deploymentId")}
${chalk.gray('–')} Remove all deployments with name ${chalk.dim('`my-app`')}: ${chalk.gray("–")} Remove all deployments with name ${chalk.dim("`my-app`")}:
${chalk.cyan('$ now rm my-app')} ${chalk.cyan("$ now rm my-app")}
${chalk.gray('–')} Remove two deployments with IDs ${chalk.dim('`eyWt6zuSdeus`')} and ${chalk.dim('`uWHoA9RQ1d1o`')}: ${chalk.gray("–")} Remove two deployments with IDs ${chalk.dim("`eyWt6zuSdeus`")} and ${chalk.dim("`uWHoA9RQ1d1o`")}:
${chalk.cyan('$ now rm eyWt6zuSdeus uWHoA9RQ1d1o')} ${chalk.cyan("$ now rm eyWt6zuSdeus uWHoA9RQ1d1o")}
${chalk.dim('Alias:')} rm ${chalk.dim("Alias:")} rm
`) `
} );
};
if (argv.help || ids.length === 0) { if (argv.help || ids.length === 0) {
help() help();
process.exit(0) process.exit(0);
} }
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
const hard = argv.hard || false const hard = argv.hard || false;
const skipConfirmation = argv.yes || false const skipConfirmation = argv.yes || false;
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
const config = cfg.read() const config = cfg.read();
function readConfirmation(matches) { function readConfirmation(matches) {
return new Promise(resolve => { return new Promise(resolve => {
process.stdout.write(`> The following deployment${matches.length === 1 ? '' : 's'} will be removed permanently:\n`) process.stdout.write(
`> The following deployment${matches.length === 1 ? "" : "s"} will be removed permanently:\n`
);
const tbl = table( const tbl = table(
matches.map(depl => { matches.map(depl => {
const time = chalk.gray(ms(new Date() - depl.created) + ' ago') const time = chalk.gray(ms(new Date() - depl.created) + " ago");
const url = depl.url ? chalk.underline(`https://${depl.url}`) : '' const url = depl.url ? chalk.underline(`https://${depl.url}`) : "";
return [depl.uid, url, time] return [depl.uid, url, time];
}), }),
{align: ['l', 'r', 'l'], hsep: ' '.repeat(6)} { align: ["l", "r", "l"], hsep: " ".repeat(6) }
) );
process.stdout.write(tbl + '\n') process.stdout.write(tbl + "\n");
for (const depl of matches) { for (const depl of matches) {
for (const alias of depl.aliases) { for (const alias of depl.aliases) {
process.stdout.write( process.stdout.write(
`> ${chalk.yellow('Warning!')} Deployment ${chalk.bold(depl.uid)} ` + `> ${chalk.yellow("Warning!")} Deployment ${chalk.bold(depl.uid)} ` +
`is an alias for ${chalk.underline(`https://${alias.alias}`)} and will be removed.\n` `is an alias for ${chalk.underline(`https://${alias.alias}`)} and will be removed.\n`
) );
} }
} }
process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/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()) process.stdin
}).resume() .on("data", d => {
}) process.stdin.pause();
resolve(d.toString().trim());
})
.resume();
});
} }
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await remove(token) await remove(token);
} catch (err) { } catch (err) {
error(`Unknown error: ${err}\n${err.stack}`) error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1) process.exit(1);
} }
}) })
.catch(e => { .catch(e => {
error(`Authentication error – ${e.message}`) error(`Authentication error – ${e.message}`);
process.exit(1) process.exit(1);
}) });
async function remove(token) { async function remove(token) {
const now = new Now(apiUrl, token, {debug}) const now = new Now(apiUrl, token, { debug });
const deployments = await now.list() const deployments = await now.list();
const matches = deployments.filter(d => { const matches = deployments.filter(d => {
return ids.find(id => { return ids.find(id => {
// Normalize URL by removing slash from the end // Normalize URL by removing slash from the end
if (isURL(id) && id.slice(-1) === '/') { if (isURL(id) && id.slice(-1) === "/") {
id = id.slice(0, -1) id = id.slice(0, -1);
} }
// `url` should match the hostname of the deployment // `url` should match the hostname of the deployment
let u = id.replace(/^https:\/\//i, '') let u = id.replace(/^https:\/\//i, "");
if (u.indexOf('.') === -1) { if (u.indexOf(".") === -1) {
// `.now.sh` domain is implied if just the subdomain is given // `.now.sh` domain is implied if just the subdomain is given
u += '.now.sh' u += ".now.sh";
} }
return d.uid === id || d.name === id || d.url === u return d.uid === id || d.name === id || d.url === u;
}) });
}) });
if (matches.length === 0) { if (matches.length === 0) {
error(`Could not find any deployments matching ${ids.map(id => chalk.bold(`"${id}"`)).join(', ')}. Run ${chalk.dim(`\`now ls\``)} to list.`) error(
return process.exit(1) `Could not find any deployments matching ${ids
.map(id => chalk.bold(`"${id}"`))
.join(", ")}. Run ${chalk.dim(`\`now ls\``)} to list.`
);
return process.exit(1);
} }
const aliases = await Promise.all(matches.map(depl => now.listAliases(depl.uid))) const aliases = await Promise.all(
matches.map(depl => now.listAliases(depl.uid))
);
for (let i = 0; i < matches.length; i++) { for (let i = 0; i < matches.length; i++) {
matches[i].aliases = aliases[i] matches[i].aliases = aliases[i];
} }
try { try {
if (!skipConfirmation) { if (!skipConfirmation) {
const confirmation = (await readConfirmation(matches)).toLowerCase() const confirmation = (await readConfirmation(matches)).toLowerCase();
if (confirmation !== 'y' && confirmation !== 'yes') { if (confirmation !== "y" && confirmation !== "yes") {
console.log('\n> Aborted') console.log("\n> Aborted");
process.exit(0) process.exit(0);
} }
} }
const start = new Date() const start = new Date();
await Promise.all(matches.map(depl => now.remove(depl.uid, {hard}))) await Promise.all(matches.map(depl => now.remove(depl.uid, { hard })));
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} [${elapsed}]`) console.log(`${chalk.cyan("> Success!")} [${elapsed}]`);
console.log(table(matches.map(depl => { console.log(
return [`Deployment ${chalk.bold(depl.uid)} removed`] table(
}))) matches.map(depl => {
return [`Deployment ${chalk.bold(depl.uid)} removed`];
})
)
);
} catch (err) { } catch (err) {
handleError(err) handleError(err);
process.exit(1) process.exit(1);
} }
now.close() now.close();
} }

305
bin/now-secrets.js

@ -1,233 +1,268 @@
#!/usr/bin/env node #!/usr/bin/env node
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
const table = require('text-table') const table = require("text-table");
const minimist = require('minimist') const minimist = require("minimist");
const ms = require('ms') const ms = require("ms");
// Ours // Ours
const strlen = require('../lib/strlen') const strlen = require("../lib/strlen");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const {handleError, error} = require('../lib/error') const { handleError, error } = require("../lib/error");
const NowSecrets = require('../lib/secrets') const NowSecrets = require("../lib/secrets");
const login = require('../lib/login') const login = require("../lib/login");
const exit = require('../lib/utils/exit') const exit = require("../lib/utils/exit");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'], string: ["config", "token"],
boolean: ['help', 'debug', 'base64'], boolean: ["help", "debug", "base64"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
base64: 'b', base64: "b",
token: 't' token: "t"
} }
}) });
const subcommand = argv._[0] const subcommand = argv._[0];
// options // options
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now secrets`)} <ls | add | rename | rm> <secret> ${chalk.bold(`${logo} now secrets`)} <ls | add | rename | rm> <secret>
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-b, --base64 Treat value as base64-encoded -b, --base64 Treat value as base64-encoded
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -c ${chalk.bold.underline("FILE")}, --config=${chalk.bold.underline("FILE")} Config file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} Login token
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} Lists all your secrets: ${chalk.gray("–")} Lists all your secrets:
${chalk.cyan('$ now secrets ls')} ${chalk.cyan("$ now secrets ls")}
${chalk.gray('–')} Adds a new secret: ${chalk.gray("–")} Adds a new secret:
${chalk.cyan('$ now secrets add my-secret "my value"')} ${chalk.cyan('$ now secrets add my-secret "my value"')}
${chalk.gray('–')} Once added, a secret's value can't be retrieved in plaintext anymore ${chalk.gray("–")} Once added, a secret's value can't be retrieved in plaintext anymore
${chalk.gray('–')} If the secret's value is more than one word, wrap it in quotes ${chalk.gray("–")} If the secret's value is more than one word, wrap it in quotes
${chalk.gray('–')} Actually, when in doubt, wrap your value in quotes ${chalk.gray("–")} Actually, when in doubt, wrap your value in quotes
${chalk.gray('–')} Exposes a secret as an env variable: ${chalk.gray("–")} Exposes a secret as an env variable:
${chalk.cyan(`$ now -e MY_SECRET=${chalk.bold('@my-secret')}`)} ${chalk.cyan(`$ now -e MY_SECRET=${chalk.bold("@my-secret")}`)}
Notice the ${chalk.cyan.bold('`@`')} symbol which makes the value a secret reference. Notice the ${chalk.cyan.bold("`@`")} symbol which makes the value a secret reference.
${chalk.gray('–')} Renames a secret: ${chalk.gray("–")} Renames a secret:
${chalk.cyan(`$ now secrets rename my-secret my-renamed-secret`)} ${chalk.cyan(`$ now secrets rename my-secret my-renamed-secret`)}
${chalk.gray('–')} Removes a secret: ${chalk.gray("–")} Removes a secret:
${chalk.cyan(`$ now secrets rm my-secret`)} ${chalk.cyan(`$ now secrets rm my-secret`)}
`) `
} );
};
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
if (argv.help || !subcommand) { if (argv.help || !subcommand) {
help() help();
exit(0) exit(0);
} else { } else {
const config = cfg.read() const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await run(token) await run(token);
} catch (err) { } catch (err) {
handleError(err) handleError(err);
exit(1) exit(1);
} }
}) })
.catch(e => { .catch(e => {
error(`Authentication error – ${e.message}`) error(`Authentication error – ${e.message}`);
exit(1) exit(1);
}) });
} }
async function run(token) { async function run(token) {
const secrets = new NowSecrets(apiUrl, token, {debug}) const secrets = new NowSecrets(apiUrl, token, { debug });
const args = argv._.slice(1) const args = argv._.slice(1);
const start = Date.now() const start = Date.now();
if (subcommand === 'ls' || subcommand === 'list') { if (subcommand === "ls" || subcommand === "list") {
if (args.length !== 0) { if (args.length !== 0) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret ls`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now secret ls`")}`
);
return exit(1);
} }
const list = await secrets.ls() const list = await secrets.ls();
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`> ${list.length} secret${list.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`) console.log(
`> ${list.length} secret${list.length === 1 ? "" : "s"} found ${chalk.gray(`[${elapsed}]`)}`
);
if (list.length > 0) { if (list.length > 0) {
const cur = Date.now() const cur = Date.now();
const header = [['', 'id', 'name', 'created'].map(s => chalk.dim(s))] const header = [["", "id", "name", "created"].map(s => chalk.dim(s))];
const out = table(header.concat(list.map(secret => { const out = table(
return [ header.concat(
'', list.map(secret => {
secret.uid, return [
chalk.bold(secret.name), "",
chalk.gray(ms(cur - new Date(secret.created)) + ' ago') secret.uid,
] chalk.bold(secret.name),
})), {align: ['l', 'r', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) chalk.gray(ms(cur - new Date(secret.created)) + " ago")
];
})
),
{
align: ["l", "r", "l", "l"],
hsep: " ".repeat(2),
stringLength: strlen
}
);
if (out) { if (out) {
console.log('\n' + out + '\n') console.log("\n" + out + "\n");
} }
} }
return secrets.close() return secrets.close();
} }
if (subcommand === 'rm' || subcommand === 'remove') { if (subcommand === "rm" || subcommand === "remove") {
if (args.length !== 1) { if (args.length !== 1) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rm <id | name>`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now secret rm <id | name>`")}`
);
return exit(1);
} }
const list = await secrets.ls() const list = await secrets.ls();
const theSecret = list.filter(secret => { const theSecret = list.filter(secret => {
return secret.uid === args[0] || secret.name === args[0] return secret.uid === args[0] || secret.name === args[0];
})[0] })[0];
if (theSecret) { if (theSecret) {
const yes = await readConfirmation(theSecret) const yes = await readConfirmation(theSecret);
if (!yes) { if (!yes) {
error('User abort') error("User abort");
return exit(0) return exit(0);
} }
} else { } else {
error(`No secret found by id or name "${args[0]}"`) error(`No secret found by id or name "${args[0]}"`);
return exit(1) return exit(1);
} }
const secret = await secrets.rm(args[0]) const secret = await secrets.rm(args[0]);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}`) console.log(
return secrets.close() `${chalk.cyan("> Success!")} Secret ${chalk.bold(secret.name)} ${chalk.gray(`(${secret.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}`
);
return secrets.close();
} }
if (subcommand === 'rename') { if (subcommand === "rename") {
if (args.length !== 2) { if (args.length !== 2) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret rename <old-name> <new-name>`')}`) error(
return exit(1) `Invalid number of arguments. Usage: ${chalk.cyan("`now secret rename <old-name> <new-name>`")}`
);
return exit(1);
} }
const secret = await secrets.rename(args[0], args[1]) const secret = await secrets.rename(args[0], args[1]);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(secret.oldName)} ${chalk.gray(`(${secret.uid})`)} renamed to ${chalk.bold(args[1])} ${chalk.gray(`[${elapsed}]`)}`) console.log(
return secrets.close() `${chalk.cyan("> Success!")} Secret ${chalk.bold(secret.oldName)} ${chalk.gray(`(${secret.uid})`)} renamed to ${chalk.bold(args[1])} ${chalk.gray(`[${elapsed}]`)}`
);
return secrets.close();
} }
if (subcommand === 'add' || subcommand === 'set') { if (subcommand === "add" || subcommand === "set") {
if (args.length !== 2) { if (args.length !== 2) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now secret add <name> <value>`')}`) error(
`Invalid number of arguments. Usage: ${chalk.cyan("`now secret add <name> <value>`")}`
);
if (args.length > 2) { if (args.length > 2) {
const example = chalk.cyan(`$ now secret add ${args[0]}`) const example = chalk.cyan(`$ now secret add ${args[0]}`);
console.log(`> If your secret has spaces, make sure to wrap it in quotes. Example: \n ${example} `) console.log(
`> If your secret has spaces, make sure to wrap it in quotes. Example: \n ${example} `
);
} }
return exit(1) return exit(1);
} }
const [name, value_] = args const [name, value_] = args;
let value let value;
if (argv.base64) { if (argv.base64) {
value = {base64: value_} value = { base64: value_ };
} else { } else {
value = value_ value = value_;
} }
const secret = await secrets.add(name, value) const secret = await secrets.add(name, value);
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
console.log(`${chalk.cyan('> Success!')} Secret ${chalk.bold(name.toLowerCase())} ${chalk.gray(`(${secret.uid})`)} added ${chalk.gray(`[${elapsed}]`)}`) console.log(
return secrets.close() `${chalk.cyan("> Success!")} Secret ${chalk.bold(name.toLowerCase())} ${chalk.gray(`(${secret.uid})`)} added ${chalk.gray(`[${elapsed}]`)}`
);
return secrets.close();
} }
error('Please specify a valid subcommand: ls | add | rename | rm') error("Please specify a valid subcommand: ls | add | rename | rm");
help() help();
exit(1) exit(1);
} }
process.on('uncaughtException', err => { process.on("uncaughtException", err => {
handleError(err) handleError(err);
exit(1) exit(1);
}) });
function readConfirmation(secret) { function readConfirmation(secret) {
return new Promise(resolve => { return new Promise(resolve => {
const time = chalk.gray(ms(new Date() - new Date(secret.created)) + ' ago') const time = chalk.gray(ms(new Date() - new Date(secret.created)) + " ago");
const tbl = table( const tbl = table([[secret.uid, chalk.bold(secret.name), time]], {
[[secret.uid, chalk.bold(secret.name), time]], align: ["l", "r", "l"],
{align: ['l', 'r', 'l'], hsep: ' '.repeat(6)} hsep: " ".repeat(6)
) });
process.stdout.write('> The following secret will be removed permanently\n') process.stdout.write(
process.stdout.write(' ' + tbl + '\n') "> The following secret will be removed permanently\n"
);
process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`) process.stdout.write(" " + tbl + "\n");
process.stdin.on('data', d => { process.stdout.write(
process.stdin.pause() `${chalk.bold.red("> Are you sure?")} ${chalk.gray("[y/N] ")}`
resolve(d.toString().trim().toLowerCase() === 'y') );
}).resume()
}) process.stdin
.on("data", d => {
process.stdin.pause();
resolve(d.toString().trim().toLowerCase() === "y");
})
.resume();
});
} }

237
bin/now-upgrade.js

@ -1,67 +1,69 @@
#!/usr/bin/env node #!/usr/bin/env node
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
const minimist = require('minimist') const minimist = require("minimist");
const ms = require('ms') const ms = require("ms");
const stripAnsi = require('strip-ansi') const stripAnsi = require("strip-ansi");
// Ours // Ours
const login = require('../lib/login') const login = require("../lib/login");
const cfg = require('../lib/cfg') const cfg = require("../lib/cfg");
const NowPlans = require('../lib/plans') const NowPlans = require("../lib/plans");
const indent = require('../lib/indent') const indent = require("../lib/indent");
const listInput = require('../lib/utils/input/list') const listInput = require("../lib/utils/input/list");
const code = require('../lib/utils/output/code') const code = require("../lib/utils/output/code");
const error = require('../lib/utils/output/error') const error = require("../lib/utils/output/error");
const success = require('../lib/utils/output/success') const success = require("../lib/utils/output/success");
const cmd = require('../lib/utils/output/cmd') const cmd = require("../lib/utils/output/cmd");
const logo = require('../lib/utils/output/logo') const logo = require("../lib/utils/output/logo");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'], string: ["config", "token"],
boolean: ['help', 'debug'], boolean: ["help", "debug"],
alias: { alias: {
help: 'h', help: "h",
config: 'c', config: "c",
debug: 'd', debug: "d",
token: 't' token: "t"
} }
}) });
const help = () => { const help = () => {
console.log(` console.log(
`
${chalk.bold(`${logo} now upgrade`)} [plan] ${chalk.bold(`${logo} now upgrade`)} [plan]
${chalk.dim('Options:')} ${chalk.dim("Options:")}
-h, --help Output usage information -h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file -c ${chalk.bold.underline("FILE")}, --config=${chalk.bold.underline("FILE")} Config file
-d, --debug Debug mode [off] -d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token -t ${chalk.bold.underline("TOKEN")}, --token=${chalk.bold.underline("TOKEN")} Login token
${chalk.dim('Examples:')} ${chalk.dim("Examples:")}
${chalk.gray('–')} List available plans and pick one interactively ${chalk.gray("–")} List available plans and pick one interactively
${chalk.cyan('$ now upgrade')} ${chalk.cyan("$ now upgrade")}
${chalk.yellow('NOTE:')} ${chalk.gray('Make sure you have a payment method, or add one:')} ${chalk.yellow("NOTE:")} ${chalk.gray("Make sure you have a payment method, or add one:")}
${chalk.cyan(`$ now billing add`)} ${chalk.cyan(`$ now billing add`)}
${chalk.gray('–')} Pick a specific plan (premium): ${chalk.gray("–")} Pick a specific plan (premium):
${chalk.cyan(`$ now upgrade premium`)} ${chalk.cyan(`$ now upgrade premium`)}
`) `
} );
};
// options // options
const debug = argv.debug const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co' const apiUrl = argv.url || "https://api.zeit.co";
if (argv.config) { if (argv.config) {
cfg.setConfigFile(argv.config) cfg.setConfigFile(argv.config);
} }
const exit = code => { const exit = code => {
@ -69,141 +71,150 @@ const exit = code => {
// because there's a node bug where // because there's a node bug where
// stdout writes are asynchronous // stdout writes are asynchronous
// https://github.com/nodejs/node/issues/6456 // https://github.com/nodejs/node/issues/6456
setTimeout(() => process.exit(code || 0), 100) setTimeout(() => process.exit(code || 0), 100);
} };
if (argv.help) { if (argv.help) {
help() help();
exit(0) exit(0);
} else { } else {
const config = cfg.read() const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl)) Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => { .then(async token => {
try { try {
await run(token) await run(token);
} catch (err) { } catch (err) {
if (err.userError) { if (err.userError) {
error(err.message) error(err.message);
} else { } else {
error(`Unknown error: ${err.stack}`) error(`Unknown error: ${err.stack}`);
}
exit(1);
} }
exit(1) })
} .catch(e => {
}) error(`Authentication error – ${e.message}`);
.catch(e => { exit(1);
error(`Authentication error – ${e.message}`) });
exit(1)
})
} }
function buildInquirerChoices(current, until) { function buildInquirerChoices(current, until) {
if (until) { if (until) {
until = until.split(' ') until = until.split(" ");
until = ' for ' + chalk.bold(until[0]) + ' more ' + until[1] until = " for " + chalk.bold(until[0]) + " more " + until[1];
} else { } else {
until = '' until = "";
} }
const ossTitle = current === 'oss' ? const ossTitle = current === "oss"
`oss FREE ${' '.repeat(28)} (current)` : ? `oss FREE ${" ".repeat(28)} (current)`
'oss FREE' : "oss FREE";
const premiumTitle = current === 'premium' ? const premiumTitle = current === "premium"
`premium $15/mo ${' '.repeat(24 - stripAnsi(until).length)} (current${until})` : ? `premium $15/mo ${" ".repeat(24 - stripAnsi(until).length)} (current${until})`
'premium $15/mo' : "premium $15/mo";
return [ return [
{ {
name: [ name: [
ossTitle, ossTitle,
indent('✓ All code is public and open-source', 2), indent("✓ All code is public and open-source", 2),
indent('✓ 20 deploys per month | 1GB monthly bandwidth', 2), indent("✓ 20 deploys per month | 1GB monthly bandwidth", 2),
indent('✓ 1GB FREE storage | 1MB size limit per file', 2) indent("✓ 1GB FREE storage | 1MB size limit per file", 2)
].join('\n'), ].join("\n"),
value: 'oss', value: "oss",
short: 'oss FREE' short: "oss FREE"
}, },
{ {
name: [ name: [
premiumTitle, premiumTitle,
indent('✓ All code is private and secure', 2), indent("✓ All code is private and secure", 2),
indent('✓ 1000 deploys per month | 50GB monthly bandwidth', 2), indent("✓ 1000 deploys per month | 50GB monthly bandwidth", 2),
indent('✓ 100GB storage | No filesize limit', 2) indent("✓ 100GB storage | No filesize limit", 2)
].join('\n'), ].join("\n"),
value: 'premium', value: "premium",
short: 'premium $15/mo' short: "premium $15/mo"
} }
] ];
} }
async function run(token) { async function run(token) {
const args = argv._ const args = argv._;
if (args.length > 1) { if (args.length > 1) {
error('Invalid number of arguments') error("Invalid number of arguments");
return exit(1) return exit(1);
} }
const start = new Date() const start = new Date();
const plans = new NowPlans(apiUrl, token, {debug}) const plans = new NowPlans(apiUrl, token, { debug });
let planId = args[0] let planId = args[0];
if (![undefined, 'oss', 'premium'].includes(planId)) { if (![undefined, "oss", "premium"].includes(planId)) {
error(`Invalid plan name – should be ${code('oss')} or ${code('premium')}`) error(`Invalid plan name – should be ${code("oss")} or ${code("premium")}`);
return exit(1) return exit(1);
} }
const currentPlan = await plans.getCurrent() const currentPlan = await plans.getCurrent();
if (planId === undefined) { if (planId === undefined) {
const elapsed = ms(new Date() - start) const elapsed = ms(new Date() - start);
let message = `To manage this from the web UI, head to https://zeit.co/account\n` let message = `To manage this from the web UI, head to https://zeit.co/account\n`;
message += `> Selecting a plan for your account ${chalk.gray(`[${elapsed}]`)}` message += `> Selecting a plan for your account ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(currentPlan.id, currentPlan.until) const choices = buildInquirerChoices(currentPlan.id, currentPlan.until);
planId = await listInput({ planId = await listInput({
message, message,
choices, choices,
separator: true, separator: true,
abort: 'end' abort: "end"
}) });
} }
if (planId === undefined || (planId === currentPlan.id && currentPlan.until === undefined)) { if (
return console.log('No changes made') planId === undefined ||
(planId === currentPlan.id && currentPlan.until === undefined)
) {
return console.log("No changes made");
} }
let newPlan let newPlan;
try { try {
newPlan = await plans.set(planId) newPlan = await plans.set(planId);
} catch (err) { } catch (err) {
let errorBody let errorBody;
if (err.res && err.res.status === 400) { if (err.res && err.res.status === 400) {
errorBody = err.res.json() errorBody = err.res.json();
} else { } else {
const message = 'A network error has occurred. Please retry.' const message = "A network error has occurred. Please retry.";
errorBody = {message} errorBody = { message };
} }
const _err = (await errorBody).error const _err = (await errorBody).error;
const {code, message} = _err const { code, message } = _err;
if (code === 'customer_not_found' || code === 'source_not_found') { if (code === "customer_not_found" || code === "source_not_found") {
error(`You have no payment methods available. Run ${cmd('now billing add')} to add one`) error(
`You have no payment methods available. Run ${cmd("now billing add")} to add one`
);
} else { } else {
error(`An unknow error occured. Please try again later ${message}`) error(`An unknow error occured. Please try again later ${message}`);
} }
plans.close() plans.close();
return return;
} }
if (currentPlan.until && newPlan.id === 'premium') { if (currentPlan.until && newPlan.id === "premium") {
success(`The cancelation has been undone. You're back on the ${chalk.bold('Premium plan')}`) success(
`The cancelation has been undone. You're back on the ${chalk.bold("Premium plan")}`
);
} else if (newPlan.until) { } else if (newPlan.until) {
success(`Your plan will be switched to OSS in ${chalk.bold(newPlan.until)}. Your card will not be charged again`) success(
`Your plan will be switched to OSS in ${chalk.bold(newPlan.until)}. Your card will not be charged again`
);
} else { } else {
success(`You're now on the ${chalk.bold('Premium plan')}`) success(`You're now on the ${chalk.bold("Premium plan")}`);
} }
plans.close() plans.close();
} }

122
bin/now.js

@ -1,102 +1,102 @@
#!/usr/bin/env node #!/usr/bin/env node
// Native // Native
const {resolve} = require('path') const { resolve } = require("path");
// Packages // Packages
const nodeVersion = require('node-version') const nodeVersion = require("node-version");
const updateNotifier = require('update-notifier') const updateNotifier = require("update-notifier");
const chalk = require('chalk') const chalk = require("chalk");
// Ours // Ours
const {error} = require('../lib/error') const { error } = require("../lib/error");
const pkg = require('../lib/pkg') const pkg = require("../lib/pkg");
// Throw an error if node version is too low // Throw an error if node version is too low
if (nodeVersion.major < 6) { if (nodeVersion.major < 6) {
error('Now requires at least version 6 of Node. Please upgrade!') error("Now requires at least version 6 of Node. Please upgrade!");
process.exit(1) process.exit(1);
} }
if (!process.pkg) { if (!process.pkg) {
const notifier = updateNotifier({pkg}) const notifier = updateNotifier({ pkg });
const update = notifier.update const update = notifier.update;
if (update) { if (update) {
let message = `Update available! ${chalk.red(update.current)}${chalk.green(update.latest)} \n` let message = `Update available! ${chalk.red(update.current)}${chalk.green(update.latest)} \n`;
message += `Run ${chalk.magenta('npm i -g now')} to update!\n` message += `Run ${chalk.magenta("npm i -g now")} to update!\n`;
message += `${chalk.magenta('Changelog:')} https://github.com/zeit/now-cli/releases/tag/${update.latest}` message += `${chalk.magenta("Changelog:")} https://github.com/zeit/now-cli/releases/tag/${update.latest}`;
notifier.notify({message}) notifier.notify({ message });
} }
} }
// This command will be run if no other sub command is specified // This command will be run if no other sub command is specified
const defaultCommand = 'deploy' const defaultCommand = "deploy";
const commands = new Set([ const commands = new Set([
defaultCommand, defaultCommand,
'help', "help",
'list', "list",
'ls', "ls",
'rm', "rm",
'remove', "remove",
'alias', "alias",
'aliases', "aliases",
'ln', "ln",
'domain', "domain",
'domains', "domains",
'dns', "dns",
'cert', "cert",
'certs', "certs",
'secret', "secret",
'secrets', "secrets",
'cc', "cc",
'billing', "billing",
'upgrade', "upgrade",
'downgrade', "downgrade",
'open' "open"
]) ]);
const aliases = new Map([ const aliases = new Map([
['ls', 'list'], ["ls", "list"],
['rm', 'remove'], ["rm", "remove"],
['ln', 'alias'], ["ln", "alias"],
['aliases', 'alias'], ["aliases", "alias"],
['domain', 'domains'], ["domain", "domains"],
['cert', 'certs'], ["cert", "certs"],
['secret', 'secrets'], ["secret", "secrets"],
['cc', 'billing'], ["cc", "billing"],
['downgrade', 'upgrade'] ["downgrade", "upgrade"]
]) ]);
let cmd = defaultCommand let cmd = defaultCommand;
const args = process.argv.slice(2) const args = process.argv.slice(2);
const index = args.findIndex(a => commands.has(a)) const index = args.findIndex(a => commands.has(a));
if (index > -1) { if (index > -1) {
cmd = args[index] cmd = args[index];
args.splice(index, 1) args.splice(index, 1);
if (cmd === 'help') { if (cmd === "help") {
if (index < args.length && commands.has(args[index])) { if (index < args.length && commands.has(args[index])) {
cmd = args[index] cmd = args[index];
args.splice(index, 1) args.splice(index, 1);
} else { } else {
cmd = defaultCommand cmd = defaultCommand;
} }
args.unshift('--help') args.unshift("--help");
} }
cmd = aliases.get(cmd) || cmd cmd = aliases.get(cmd) || cmd;
} }
const bin = resolve(__dirname, 'now-' + cmd + '.js') const bin = resolve(__dirname, "now-" + cmd + ".js");
// Prepare process.argv for subcommand // Prepare process.argv for subcommand
process.argv = process.argv.slice(0, 2).concat(args) process.argv = process.argv.slice(0, 2).concat(args);
// Load sub command // Load sub command
// With custom parameter to make "pkg" happy // With custom parameter to make "pkg" happy
require(bin, 'may-exclude') require(bin, "may-exclude");

56
lib/agent.js

@ -1,10 +1,10 @@
// Native // Native
const {parse} = require('url') const { parse } = require("url");
const http = require('http') const http = require("http");
const https = require('https') const https = require("https");
// Packages // Packages
const fetch = require('node-fetch') const fetch = require("node-fetch");
/** /**
* Returns a `fetch` version with a similar * Returns a `fetch` version with a similar
@ -18,66 +18,66 @@ const fetch = require('node-fetch')
*/ */
module.exports = class Agent { module.exports = class Agent {
constructor(url, {tls = true, debug} = {}) { constructor(url, { tls = true, debug } = {}) {
this._url = url this._url = url;
const parsed = parse(url) const parsed = parse(url);
this._protocol = parsed.protocol this._protocol = parsed.protocol;
this._debug = debug this._debug = debug;
if (tls) { if (tls) {
this._initAgent() this._initAgent();
} }
} }
_initAgent() { _initAgent() {
const module = this._protocol === 'https:' ? https : http const module = this._protocol === "https:" ? https : http;
const agent = this._agent = new module.Agent({ const agent = (this._agent = new module.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 10000, keepAliveMsecs: 10000,
maxSockets: 8 maxSockets: 8
}).on('error', err => this._onError(err, agent)) }).on("error", err => this._onError(err, agent)));
} }
_onError(err, agent) { _onError(err, agent) {
if (this._debug) { if (this._debug) {
console.log(`> [debug] agent connection error ${err}\n${err.stack}`) console.log(`> [debug] agent connection error ${err}\n${err.stack}`);
} }
if (this._agent === agent) { if (this._agent === agent) {
this._agent = null this._agent = null;
} }
} }
fetch(path, opts = {}) { fetch(path, opts = {}) {
if (!this._agent) { if (!this._agent) {
if (this._debug) { if (this._debug) {
console.log('> [debug] re-initializing agent') console.log("> [debug] re-initializing agent");
} }
this._initAgent() this._initAgent();
} }
const {body} = opts const { body } = opts;
if (this._agent) { if (this._agent) {
opts.agent = this._agent opts.agent = this._agent;
} }
if (body && typeof body === 'object' && typeof body.pipe !== 'function') { if (body && typeof body === "object" && typeof body.pipe !== "function") {
opts.headers['Content-Type'] = 'application/json' opts.headers["Content-Type"] = "application/json";
opts.body = JSON.stringify(body) opts.body = JSON.stringify(body);
} }
if (opts.body && typeof body.pipe !== 'function') { if (opts.body && typeof body.pipe !== "function") {
opts.headers['Content-Length'] = Buffer.byteLength(opts.body) opts.headers["Content-Length"] = Buffer.byteLength(opts.body);
} }
return fetch(this._url + path, opts) return fetch(this._url + path, opts);
} }
close() { close() {
if (this._debug) { if (this._debug) {
console.log('> [debug] closing agent') console.log("> [debug] closing agent");
} }
if (this._agent) { if (this._agent) {
this._agent.destroy() this._agent.destroy();
} }
} }
} };

569
lib/alias.js

@ -1,462 +1,513 @@
// Packages // Packages
const {readFileSync} = require('fs') const { readFileSync } = require("fs");
const publicSuffixList = require('psl') const publicSuffixList = require("psl");
const minimist = require('minimist') const minimist = require("minimist");
const chalk = require('chalk') const chalk = require("chalk");
// Ours // Ours
const promptBool = require('../lib/utils/input/prompt-bool') const promptBool = require("../lib/utils/input/prompt-bool");
const exit = require('./utils/exit') const exit = require("./utils/exit");
const copy = require('./copy') const copy = require("./copy");
const toHost = require('./to-host') const toHost = require("./to-host");
const resolve4 = require('./dns') const resolve4 = require("./dns");
const isZeitWorld = require('./is-zeit-world') const isZeitWorld = require("./is-zeit-world");
const {DOMAIN_VERIFICATION_ERROR} = require('./errors') const { DOMAIN_VERIFICATION_ERROR } = require("./errors");
const Now = require('./') const Now = require("./");
const argv = minimist(process.argv.slice(2), { const argv = minimist(process.argv.slice(2), {
boolean: ['no-clipboard'], boolean: ["no-clipboard"],
alias: {'no-clipboard': 'C'} alias: { "no-clipboard": "C" }
}) });
const isTTY = process.stdout.isTTY const isTTY = process.stdout.isTTY;
const clipboard = !argv['no-clipboard'] const clipboard = !argv["no-clipboard"];
const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/ const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/;
module.exports = class Alias extends Now { module.exports = class Alias extends Now {
async ls(deployment) { async ls(deployment) {
if (deployment) { if (deployment) {
const target = await this.findDeployment(deployment) const target = await this.findDeployment(deployment);
if (!target) { if (!target) {
const err = new Error(`Aliases not found by "${deployment}". Run ${chalk.dim('`now alias ls`')} to see your aliases.`) const err = new Error(
err.userError = true `Aliases not found by "${deployment}". Run ${chalk.dim("`now alias ls`")} to see your aliases.`
throw err );
err.userError = true;
throw err;
} }
return this.listAliases(target.uid) return this.listAliases(target.uid);
} }
return this.listAliases() return this.listAliases();
} }
async rm(_alias) { async rm(_alias) {
return this.retry(async bail => { return this.retry(async bail => {
const res = await this._fetch(`/now/aliases/${_alias.uid}`, { const res = await this._fetch(`/now/aliases/${_alias.uid}`, {
method: 'DELETE' method: "DELETE"
}) });
if (res.status === 403) { if (res.status === 403) {
return bail(new Error('Unauthorized')) return bail(new Error("Unauthorized"));
} }
if (res.status !== 200) { if (res.status !== 200) {
const err = new Error('Deletion failed. Try again later.') const err = new Error("Deletion failed. Try again later.");
throw err throw err;
} }
}) });
} }
async findDeployment(deployment) { async findDeployment(deployment) {
const list = await this.list() const list = await this.list();
let key let key;
let val let val;
if (/\./.test(deployment)) { if (/\./.test(deployment)) {
val = toHost(deployment) val = toHost(deployment);
key = 'url' key = "url";
} else { } else {
val = deployment val = deployment;
key = 'uid' key = "uid";
} }
const depl = list.find(d => { const depl = list.find(d => {
if (d[key] === val) { if (d[key] === val) {
if (this._debug) { if (this._debug) {
console.log(`> [debug] matched deployment ${d.uid} by ${key} ${val}`) console.log(`> [debug] matched deployment ${d.uid} by ${key} ${val}`);
} }
return true return true;
} }
// match prefix // match prefix
if (`${val}.now.sh` === d.url) { if (`${val}.now.sh` === d.url) {
if (this._debug) { if (this._debug) {
console.log(`> [debug] matched deployment ${d.uid} by url ${d.url}`) console.log(`> [debug] matched deployment ${d.uid} by url ${d.url}`);
} }
return true return true;
} }
return false return false;
}) });
return depl return depl;
} }
async updatePathBasedroutes(alias, rules) { async updatePathBasedroutes(alias, rules) {
alias = await this.maybeSetUpDomain(alias) alias = await this.maybeSetUpDomain(alias);
return await this.upsertPathAlias(alias, rules) return await this.upsertPathAlias(alias, rules);
} }
async upsertPathAlias(alias, rules) { async upsertPathAlias(alias, rules) {
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] /now/aliases #${attempt}`) console.time(`> [debug] /now/aliases #${attempt}`);
} }
const rulesData = this.readRulesFile(rules) const rulesData = this.readRulesFile(rules);
const ruleCount = rulesData.rules.length const ruleCount = rulesData.rules.length;
const res = await this._fetch(`/now/aliases`, { const res = await this._fetch(`/now/aliases`, {
method: 'POST', method: "POST",
body: {alias, rules: rulesData.rules} body: { alias, rules: rulesData.rules }
}) });
const body = await res.json() const body = await res.json();
body.ruleCount = ruleCount body.ruleCount = ruleCount;
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] /now/aliases #${attempt}`) console.timeEnd(`> [debug] /now/aliases #${attempt}`);
} }
// 409 conflict is returned if it already exists // 409 conflict is returned if it already exists
if (res.status === 409) { if (res.status === 409) {
return {uid: body.error.uid} return { uid: body.error.uid };
} }
if (res.status === 422) { if (res.status === 422) {
return body return body;
} }
// no retry on authorization problems // no retry on authorization problems
if (res.status === 403) { if (res.status === 403) {
const code = body.error.code const code = body.error.code;
if (code === 'custom_domain_needs_upgrade') { if (code === "custom_domain_needs_upgrade") {
const err = new Error(`Custom domains are only enabled for premium accounts. Please upgrade by running ${chalk.gray('`')}${chalk.cyan('now upgrade')}${chalk.gray('`')}.`) const err = new Error(
err.userError = true `Custom domains are only enabled for premium accounts. Please upgrade by running ${chalk.gray("`")}${chalk.cyan("now upgrade")}${chalk.gray("`")}.`
return bail(err) );
err.userError = true;
return bail(err);
} }
if (code === 'alias_in_use') { if (code === "alias_in_use") {
const err = new Error(`The alias you are trying to configure (${chalk.underline(chalk.bold(alias))}) is already in use by a different account.`) const err = new Error(
err.userError = true `The alias you are trying to configure (${chalk.underline(chalk.bold(alias))}) is already in use by a different account.`
return bail(err) );
err.userError = true;
return bail(err);
} }
if (code === 'forbidden') { if (code === "forbidden") {
const err = new Error('The domain you are trying to use as an alias is already in use by a different account.') const err = new Error(
err.userError = true "The domain you are trying to use as an alias is already in use by a different account."
return bail(err) );
err.userError = true;
return bail(err);
} }
return bail(new Error('Authorization error')) return bail(new Error("Authorization error"));
} }
// all other errors // all other errors
if (body.error) { if (body.error) {
const code = body.error.code const code = body.error.code;
if (code === 'cert_missing') { if (code === "cert_missing") {
console.log(`> Provisioning certificate for ${chalk.underline(chalk.bold(alias))}`) console.log(
`> Provisioning certificate for ${chalk.underline(chalk.bold(alias))}`
);
try { try {
await this.createCert(alias) await this.createCert(alias);
} catch (err) { } catch (err) {
// we bail to avoid retrying the whole process // we bail to avoid retrying the whole process
// of aliasing which would involve too many // of aliasing which would involve too many
// retries on certificate provisioning // retries on certificate provisioning
return bail(err) return bail(err);
} }
// try again, but now having provisioned the certificate // try again, but now having provisioned the certificate
return this.upsertPathAlias(alias, rules) return this.upsertPathAlias(alias, rules);
} }
if (code === 'cert_expired') { if (code === "cert_expired") {
console.log(`> Renewing certificate for ${chalk.underline(chalk.bold(alias))}`) console.log(
`> Renewing certificate for ${chalk.underline(chalk.bold(alias))}`
);
try { try {
await this.createCert(alias, {renew: true}) await this.createCert(alias, { renew: true });
} catch (err) { } catch (err) {
return bail(err) return bail(err);
} }
} }
return bail(new Error(body.error.message)) return bail(new Error(body.error.message));
} }
// the two expected succesful cods are 200 and 304 // the two expected succesful cods are 200 and 304
if (res.status !== 200 && res.status !== 304) { if (res.status !== 200 && res.status !== 304) {
throw new Error('Unhandled error') throw new Error("Unhandled error");
} }
return body return body;
}) });
} }
readRulesFile(rules) { readRulesFile(rules) {
try { try {
const rulesJson = readFileSync(rules, 'utf8') const rulesJson = readFileSync(rules, "utf8");
return JSON.parse(rulesJson) return JSON.parse(rulesJson);
} catch (err) { } catch (err) {
console.error(`Reading rules file ${rules} failed: ${err}`) console.error(`Reading rules file ${rules} failed: ${err}`);
} }
} }
async set(deployment, alias) { async set(deployment, alias) {
const depl = await this.findDeployment(deployment) const depl = await this.findDeployment(deployment);
if (!depl) { if (!depl) {
const err = new Error(`Deployment not found by "${deployment}". Run ${chalk.dim('`now ls`')} to see your deployments.`) const err = new Error(
err.userError = true `Deployment not found by "${deployment}". Run ${chalk.dim("`now ls`")} to see your deployments.`
throw err );
err.userError = true;
throw err;
} }
const aliasDepl = (await this.listAliases()).find(e => e.alias === alias) const aliasDepl = (await this.listAliases()).find(e => e.alias === alias);
if (aliasDepl && aliasDepl.rules) { if (aliasDepl && aliasDepl.rules) {
if (isTTY) { if (isTTY) {
try { try {
const msg = `> Path alias excists with ${aliasDepl.rules.length} rule${aliasDepl.rules.length > 1 ? 's' : ''}.\n` + const msg = `> Path alias excists with ${aliasDepl.rules.length} rule${aliasDepl.rules.length > 1 ? "s" : ""}.\n` +
`> Are you sure you want to update ${alias} to be a normal alias?\n` `> Are you sure you want to update ${alias} to be a normal alias?\n`;
const confirmation = await promptBool(msg) const confirmation = await promptBool(msg);
if (!confirmation) { if (!confirmation) {
console.log('\n> Aborted') console.log("\n> Aborted");
return exit(1) return exit(1);
} }
} catch (err) { } catch (err) {
console.log(err) console.log(err);
} }
} else { } else {
console.log(`Overwriting path alias with ${aliasDepl.rules.length} rule${aliasDepl.rules.length > 1 ? 's' : ''} to be a normal alias.`) console.log(
`Overwriting path alias with ${aliasDepl.rules.length} rule${aliasDepl.rules.length > 1 ? "s" : ""} to be a normal alias.`
);
} }
} }
alias = await this.maybeSetUpDomain(alias) alias = await this.maybeSetUpDomain(alias);
const newAlias = await this.createAlias(depl, alias) const newAlias = await this.createAlias(depl, alias);
if (!newAlias) { if (!newAlias) {
throw new Error(`Unexpected error occurred while setting up alias: ${JSON.stringify(newAlias)}`) throw new Error(
`Unexpected error occurred while setting up alias: ${JSON.stringify(newAlias)}`
);
} }
const {created, uid} = newAlias const { created, uid } = newAlias;
if (created) { if (created) {
const pretty = `https://${alias}` const pretty = `https://${alias}`;
const output = `${chalk.cyan('> Success!')} Alias created ${chalk.dim(`(${uid})`)}:\n${chalk.bold(chalk.underline(pretty))} now points to ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)}` const output = `${chalk.cyan("> Success!")} Alias created ${chalk.dim(`(${uid})`)}:\n${chalk.bold(chalk.underline(pretty))} now points to ${chalk.bold(`https://${depl.url}`)} ${chalk.dim(`(${depl.uid})`)}`;
if (isTTY && clipboard) { if (isTTY && clipboard) {
let append let append;
try { try {
await copy(pretty) await copy(pretty);
append = '[copied to clipboard]' append = "[copied to clipboard]";
} catch (err) { } catch (err) {
append = '' append = "";
} finally { } finally {
console.log(`${output} ${append}`) console.log(`${output} ${append}`);
} }
} else { } else {
console.log(output) console.log(output);
} }
} else { } else {
console.log(`${chalk.cyan('> Success!')} Alias already exists ${chalk.dim(`(${uid})`)}.`) console.log(
`${chalk.cyan("> Success!")} Alias already exists ${chalk.dim(`(${uid})`)}.`
);
} }
} }
createAlias(depl, alias) { createAlias(depl, alias) {
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`) console.time(
`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`
);
} }
const res = await this._fetch(`/now/deployments/${depl.uid}/aliases`, { const res = await this._fetch(`/now/deployments/${depl.uid}/aliases`, {
method: 'POST', method: "POST",
body: {alias} body: { alias }
}) });
const body = await res.json() const body = await res.json();
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`) console.timeEnd(
`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`
);
} }
// 409 conflict is returned if it already exists // 409 conflict is returned if it already exists
if (res.status === 409) { if (res.status === 409) {
return {uid: body.error.uid} return { uid: body.error.uid };
} }
// no retry on authorization problems // no retry on authorization problems
if (res.status === 403) { if (res.status === 403) {
const code = body.error.code const code = body.error.code;
if (code === 'custom_domain_needs_upgrade') { if (code === "custom_domain_needs_upgrade") {
const err = new Error(`Custom domains are only enabled for premium accounts. Please upgrade by running ${chalk.gray('`')}${chalk.cyan('now upgrade')}${chalk.gray('`')}.`) const err = new Error(
err.userError = true `Custom domains are only enabled for premium accounts. Please upgrade by running ${chalk.gray("`")}${chalk.cyan("now upgrade")}${chalk.gray("`")}.`
return bail(err) );
err.userError = true;
return bail(err);
} }
if (code === 'alias_in_use') { if (code === "alias_in_use") {
const err = new Error(`The alias you are trying to configure (${chalk.underline(chalk.bold(alias))}) is already in use by a different account.`) const err = new Error(
err.userError = true `The alias you are trying to configure (${chalk.underline(chalk.bold(alias))}) is already in use by a different account.`
return bail(err) );
err.userError = true;
return bail(err);
} }
if (code === 'forbidden') { if (code === "forbidden") {
const err = new Error('The domain you are trying to use as an alias is already in use by a different account.') const err = new Error(
err.userError = true "The domain you are trying to use as an alias is already in use by a different account."
return bail(err) );
err.userError = true;
return bail(err);
} }
return bail(new Error('Authorization error')) return bail(new Error("Authorization error"));
} }
// all other errors // all other errors
if (body.error) { if (body.error) {
const code = body.error.code const code = body.error.code;
if (code === 'deployment_not_found') { if (code === "deployment_not_found") {
return bail(new Error('Deployment not found')) return bail(new Error("Deployment not found"));
} }
if (code === 'cert_missing') { if (code === "cert_missing") {
console.log(`> Provisioning certificate for ${chalk.underline(chalk.bold(alias))}`) console.log(
`> Provisioning certificate for ${chalk.underline(chalk.bold(alias))}`
);
try { try {
await this.createCert(alias) await this.createCert(alias);
} catch (err) { } catch (err) {
// we bail to avoid retrying the whole process // we bail to avoid retrying the whole process
// of aliasing which would involve too many // of aliasing which would involve too many
// retries on certificate provisioning // retries on certificate provisioning
return bail(err) return bail(err);
} }
// try again, but now having provisioned the certificate // try again, but now having provisioned the certificate
return this.createAlias(depl, alias) return this.createAlias(depl, alias);
} }
if (code === 'cert_expired') { if (code === "cert_expired") {
console.log(`> Renewing certificate for ${chalk.underline(chalk.bold(alias))}`) console.log(
`> Renewing certificate for ${chalk.underline(chalk.bold(alias))}`
);
try { try {
await this.createCert(alias, {renew: true}) await this.createCert(alias, { renew: true });
} catch (err) { } catch (err) {
return bail(err) return bail(err);
} }
} }
return bail(new Error(body.error.message)) return bail(new Error(body.error.message));
} }
// the two expected succesful cods are 200 and 304 // the two expected succesful cods are 200 and 304
if (res.status !== 200 && res.status !== 304) { if (res.status !== 200 && res.status !== 304) {
throw new Error('Unhandled error') throw new Error("Unhandled error");
} }
return body return body;
}) });
} }
async setupRecord(domain, name) { async setupRecord(domain, name) {
await this.setupDomain(domain) await this.setupDomain(domain);
if (this._debug) { if (this._debug) {
console.log(`> [debug] Setting up record "${name}" for "${domain}"`) console.log(`> [debug] Setting up record "${name}" for "${domain}"`);
} }
const type = name === '' ? 'ALIAS' : 'CNAME' const type = name === "" ? "ALIAS" : "CNAME";
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] /domains/${domain}/records #${attempt}`) console.time(`> [debug] /domains/${domain}/records #${attempt}`);
} }
const res = await this._fetch(`/domains/${domain}/records`, { const res = await this._fetch(`/domains/${domain}/records`, {
method: 'POST', method: "POST",
body: { body: {
type, type,
name: name === '' ? name : '*', name: name === "" ? name : "*",
value: 'alias.zeit.co' value: "alias.zeit.co"
} }
}) });
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] /domains/${domain}/records #${attempt}`) console.timeEnd(`> [debug] /domains/${domain}/records #${attempt}`);
} }
if (res.status === 403) { if (res.status === 403) {
return bail(new Error('Unauthorized')) return bail(new Error("Unauthorized"));
} }
const body = await res.json() const body = await res.json();
if (res.status !== 200) { if (res.status !== 200) {
throw new Error(body.error.message) throw new Error(body.error.message);
} }
return body return body;
}) });
} }
async maybeSetUpDomain(alias) { async maybeSetUpDomain(alias) {
// make alias lowercase // make alias lowercase
alias = alias.toLowerCase() alias = alias.toLowerCase();
// trim leading and trailing dots // trim leading and trailing dots
// for example: `google.com.` => `google.com` // for example: `google.com.` => `google.com`
alias = alias alias = alias.replace(/^\.+/, "").replace(/\.+$/, "");
.replace(/^\.+/, '')
.replace(/\.+$/, '')
// evaluate the alias // evaluate the alias
if (/\./.test(alias)) { if (/\./.test(alias)) {
alias = toHost(alias) alias = toHost(alias);
} else { } else {
if (this._debug) { if (this._debug) {
console.log(`> [debug] suffixing \`.now.sh\` to alias ${alias}`) console.log(`> [debug] suffixing \`.now.sh\` to alias ${alias}`);
} }
alias = `${alias}.now.sh` alias = `${alias}.now.sh`;
} }
if (!domainRegex.test(alias)) { if (!domainRegex.test(alias)) {
const err = new Error(`Invalid alias "${alias}"`) const err = new Error(`Invalid alias "${alias}"`);
err.userError = true err.userError = true;
throw err throw err;
} }
if (!/\.now\.sh$/.test(alias)) { if (!/\.now\.sh$/.test(alias)) {
console.log(`> ${chalk.bold(chalk.underline(alias))} is a custom domain.`) console.log(
console.log(`> Verifying the DNS settings for ${chalk.bold(chalk.underline(alias))} (see ${chalk.underline('https://zeit.world')} for help)`) `> ${chalk.bold(chalk.underline(alias))} is a custom domain.`
);
const _domain = publicSuffixList.parse(alias).domain console.log(
const _domainInfo = await this.getDomain(_domain) `> Verifying the DNS settings for ${chalk.bold(chalk.underline(alias))} (see ${chalk.underline("https://zeit.world")} for help)`
const domainInfo = _domainInfo && !_domainInfo.error ? _domainInfo : undefined );
const {domain, nameservers} = domainInfo ? {domain: _domain} : await this.getNameservers(alias)
const usingZeitWorld = domainInfo ? !domainInfo.isExternal : isZeitWorld(nameservers) const _domain = publicSuffixList.parse(alias).domain;
let skipDNSVerification = false const _domainInfo = await this.getDomain(_domain);
const domainInfo = _domainInfo && !_domainInfo.error
? _domainInfo
: undefined;
const { domain, nameservers } = domainInfo
? { domain: _domain }
: await this.getNameservers(alias);
const usingZeitWorld = domainInfo
? !domainInfo.isExternal
: isZeitWorld(nameservers);
let skipDNSVerification = false;
if (this._debug) { if (this._debug) {
if (domainInfo) { if (domainInfo) {
console.log(`> [debug] Found domain ${domain} with verified:${domainInfo.verified}`) console.log(
`> [debug] Found domain ${domain} with verified:${domainInfo.verified}`
);
} else { } else {
console.log(`> [debug] Found domain ${domain} and nameservers ${nameservers}`) console.log(
`> [debug] Found domain ${domain} and nameservers ${nameservers}`
);
} }
} }
if (!usingZeitWorld && domainInfo) { if (!usingZeitWorld && domainInfo) {
if (domainInfo.verified) { if (domainInfo.verified) {
skipDNSVerification = true skipDNSVerification = true;
} else if (domainInfo.uid) { } else if (domainInfo.uid) {
const {verified, created} = await this.setupDomain(domain, {isExternal: true}) const { verified, created } = await this.setupDomain(domain, {
isExternal: true
});
if (!(created && verified)) { if (!(created && verified)) {
const e = new Error(`> Failed to verify the ownership of ${domain}, please refer to 'now domain --help'.`) const e = new Error(
e.userError = true `> Failed to verify the ownership of ${domain}, please refer to 'now domain --help'.`
throw e );
e.userError = true;
throw e;
} }
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(domain))} verified`) console.log(
`${chalk.cyan("> Success!")} Domain ${chalk.bold(chalk.underline(domain))} verified`
);
} }
} }
try { try {
if (!skipDNSVerification) { if (!skipDNSVerification) {
await this.verifyOwnership(alias) await this.verifyOwnership(alias);
} }
} catch (err) { } catch (err) {
if (err.userError) { if (err.userError) {
@ -465,110 +516,134 @@ module.exports = class Alias extends Now {
// configuration (if we can!) // configuration (if we can!)
try { try {
if (usingZeitWorld) { if (usingZeitWorld) {
console.log(`> Detected ${chalk.bold(chalk.underline('zeit.world'))} nameservers! Configuring records.`) console.log(
const record = alias.substr(0, alias.length - domain.length) `> Detected ${chalk.bold(chalk.underline("zeit.world"))} nameservers! Configuring records.`
);
const record = alias.substr(0, alias.length - domain.length);
// lean up trailing and leading dots // lean up trailing and leading dots
const _record = record const _record = record.replace(/^\./, "").replace(/\.$/, "");
.replace(/^\./, '') const _domain = domain.replace(/^\./, "").replace(/\.$/, "");
.replace(/\.$/, '')
const _domain = domain if (_record === "") {
.replace(/^\./, '') await this.setupRecord(_domain, "*");
.replace(/\.$/, '')
if (_record === '') {
await this.setupRecord(_domain, '*')
} }
await this.setupRecord(_domain, _record) await this.setupRecord(_domain, _record);
this.recordSetup = true this.recordSetup = true;
console.log('> DNS Configured! Verifying propagation…') console.log("> DNS Configured! Verifying propagation…");
try { try {
await this.retry(() => this.verifyOwnership(alias), {retries: 10, maxTimeout: 8000}) await this.retry(() => this.verifyOwnership(alias), {
retries: 10,
maxTimeout: 8000
});
} catch (err2) { } catch (err2) {
const e = new Error('> We configured the DNS settings for your alias, but we were unable to ' + const e = new Error(
'verify that they\'ve propagated. Please try the alias again later.') "> We configured the DNS settings for your alias, but we were unable to " +
e.userError = true "verify that they've propagated. Please try the alias again later."
throw e );
e.userError = true;
throw e;
} }
} else { } else {
console.log(`> Resolved IP: ${err.ip ? `${chalk.underline(err.ip)} (unknown)` : chalk.dim('none')}`) console.log(
console.log(`> Nameservers: ${nameservers && nameservers.length ? nameservers.map(ns => chalk.underline(ns)).join(', ') : chalk.dim('none')}`) `> Resolved IP: ${err.ip ? `${chalk.underline(err.ip)} (unknown)` : chalk.dim("none")}`
throw err );
console.log(
`> Nameservers: ${nameservers && nameservers.length ? nameservers
.map(ns => chalk.underline(ns))
.join(", ") : chalk.dim("none")}`
);
throw err;
} }
} catch (e) { } catch (e) {
if (e.userError) { if (e.userError) {
throw e throw e;
} }
throw err throw err;
} }
} else { } else {
throw err throw err;
} }
} }
if (!usingZeitWorld && !skipDNSVerification) { if (!usingZeitWorld && !skipDNSVerification) {
if (this._debug) { if (this._debug) {
console.log(`> [debug] Trying to register a non-ZeitWorld domain ${domain} for the current user`) console.log(
`> [debug] Trying to register a non-ZeitWorld domain ${domain} for the current user`
);
} }
const {uid, verified, created} = await this.setupDomain(domain, {isExternal: true}) const { uid, verified, created } = await this.setupDomain(domain, {
isExternal: true
});
if (!(created && verified)) { if (!(created && verified)) {
const e = new Error(`> Failed to verify the ownership of ${domain}, please refer to 'now domain --help'.`) const e = new Error(
e.userError = true `> Failed to verify the ownership of ${domain}, please refer to 'now domain --help'.`
throw e );
e.userError = true;
throw e;
} }
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(domain))} ${chalk.dim(`(${uid})`)} added`) console.log(
`${chalk.cyan("> Success!")} Domain ${chalk.bold(chalk.underline(domain))} ${chalk.dim(`(${uid})`)} added`
);
} }
console.log(`> Verification ${chalk.bold('OK')}!`) console.log(`> Verification ${chalk.bold("OK")}!`);
} }
return alias return alias;
} }
verifyOwnership(domain) { verifyOwnership(domain) {
return this.retry(async bail => { return this.retry(async bail => {
const targets = await resolve4('alias.zeit.co') const targets = await resolve4("alias.zeit.co");
if (targets.length <= 0) { if (targets.length <= 0) {
return bail(new Error('Unable to resolve alias.zeit.co')) return bail(new Error("Unable to resolve alias.zeit.co"));
} }
let ips = [] let ips = [];
try { try {
ips = await resolve4(domain) ips = await resolve4(domain);
} catch (err) { } catch (err) {
if (err.code === 'ENODATA' || err.code === 'ESERVFAIL' || err.code === 'ENOTFOUND') { if (
err.code === "ENODATA" ||
err.code === "ESERVFAIL" ||
err.code === "ENOTFOUND"
) {
// not errors per se, just absence of records // not errors per se, just absence of records
if (this._debug) { if (this._debug) {
console.log(`> [debug] No records found for "${domain}"`) console.log(`> [debug] No records found for "${domain}"`);
} }
const err = new Error(DOMAIN_VERIFICATION_ERROR) const err = new Error(DOMAIN_VERIFICATION_ERROR);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} }
throw err throw err;
} }
if (ips.length <= 0) { if (ips.length <= 0) {
const err = new Error(DOMAIN_VERIFICATION_ERROR) const err = new Error(DOMAIN_VERIFICATION_ERROR);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} }
for (const ip of ips) { for (const ip of ips) {
if (targets.indexOf(ip) === -1) { if (targets.indexOf(ip) === -1) {
const err = new Error(`The domain ${domain} has an A record ${chalk.bold(ip)} that doesn't resolve to ${chalk.bold(chalk.underline('alias.zeit.co'))}.\n> ` + DOMAIN_VERIFICATION_ERROR) const err = new Error(
err.ip = ip `The domain ${domain} has an A record ${chalk.bold(ip)} that doesn't resolve to ${chalk.bold(chalk.underline("alias.zeit.co"))}.\n> ` +
err.userError = true DOMAIN_VERIFICATION_ERROR
return bail(err) );
err.ip = ip;
err.userError = true;
return bail(err);
} }
} }
}) });
} }
} };

145
lib/build-logger.js

@ -1,153 +1,156 @@
// Native // Native
const EventEmitter = require('events') const EventEmitter = require("events");
// Packages // Packages
const ansi = require('ansi-escapes') const ansi = require("ansi-escapes");
const io = require('socket.io-client') const io = require("socket.io-client");
const chalk = require('chalk') const chalk = require("chalk");
const {compare, deserialize} = require('./logs') const { compare, deserialize } = require("./logs");
class Lines { class Lines {
constructor(maxLines = 100) { constructor(maxLines = 100) {
this.max = maxLines this.max = maxLines;
this.buf = [] this.buf = [];
} }
write(str) { write(str) {
const {max, buf} = this const { max, buf } = this;
if (buf.length === max) { if (buf.length === max) {
process.stdout.write(ansi.eraseLines(max + 1)) process.stdout.write(ansi.eraseLines(max + 1));
buf.shift() buf.shift();
buf.forEach(line => console.log(line)) buf.forEach(line => console.log(line));
} }
buf.push(str) buf.push(str);
console.log(str) console.log(str);
} }
reset() { reset() {
this.buf = [] this.buf = [];
} }
} }
module.exports = class Logger extends EventEmitter { module.exports = class Logger extends EventEmitter {
constructor(host, {debug = false, quiet = false} = {}) { constructor(host, { debug = false, quiet = false } = {}) {
super() super();
this.host = host this.host = host;
this.debug = debug this.debug = debug;
this.quiet = quiet this.quiet = quiet;
// readyState // readyState
this.building = false this.building = false;
this.socket = io(`https://io.now.sh/states?host=${host}&v=2`) this.socket = io(`https://io.now.sh/states?host=${host}&v=2`);
this.socket.once('error', this.onSocketError.bind(this)) this.socket.once("error", this.onSocketError.bind(this));
this.socket.on('state', this.onState.bind(this)) this.socket.on("state", this.onState.bind(this));
this.socket.on('logs', this.onLog.bind(this)) this.socket.on("logs", this.onLog.bind(this));
this.socket.on('backend', this.onComplete.bind(this)) this.socket.on("backend", this.onComplete.bind(this));
this.lines = new Lines(10) this.lines = new Lines(10);
// log buffer // log buffer
this.buf = [] this.buf = [];
} }
onState(state) { onState(state) {
// console.log(state) // console.log(state)
if (!state.id) { if (!state.id) {
console.error('> Deployment not found') console.error("> Deployment not found");
this.emit('error') this.emit("error");
return return;
} }
if (state.error) { if (state.error) {
this.emit('error', state) this.emit("error", state);
return return;
} }
if (state.backend) { if (state.backend) {
this.onComplete() this.onComplete();
return return;
} }
if (state.logs) { if (state.logs) {
state.logs.forEach(this.onLog, this) state.logs.forEach(this.onLog, this);
} }
} }
onLog(log) { onLog(log) {
if (!this.building) { if (!this.building) {
if (!this.quiet) { if (!this.quiet) {
console.log('> Building') console.log("> Building");
} }
this.building = true this.building = true;
} }
if (this.quiet) { if (this.quiet) {
return return;
} }
log = deserialize(log) log = deserialize(log);
const timer = setTimeout(() => { const timer = setTimeout(
this.buf.sort((a, b) => compare(a.log, b.log)) () => {
const idx = this.buf.findIndex(b => b.log.id === log.id) + 1 this.buf.sort((a, b) => compare(a.log, b.log));
for (const b of this.buf.slice(0, idx)) { const idx = this.buf.findIndex(b => b.log.id === log.id) + 1;
clearTimeout(b.timer) for (const b of this.buf.slice(0, idx)) {
this.printLog(b.log) clearTimeout(b.timer);
} this.printLog(b.log);
this.buf = this.buf.slice(idx) }
}, 300) this.buf = this.buf.slice(idx);
},
300
);
this.buf.push({log, timer}) this.buf.push({ log, timer });
} }
onComplete() { onComplete() {
this.socket.disconnect() this.socket.disconnect();
if (this.building) { if (this.building) {
this.building = false this.building = false;
} }
this.buf.sort((a, b) => compare(a.log, b.log)) this.buf.sort((a, b) => compare(a.log, b.log));
// flush all buffer // flush all buffer
for (const b of this.buf) { for (const b of this.buf) {
clearTimeout(b.timer) clearTimeout(b.timer);
this.printLog(b.log) this.printLog(b.log);
} }
this.buf = [] this.buf = [];
this.emit('close') this.emit("close");
} }
onSocketError(err) { onSocketError(err) {
if (this.debug) { if (this.debug) {
console.log(`> [debug] Socket error ${err}\n${err.stack}`) console.log(`> [debug] Socket error ${err}\n${err.stack}`);
} }
} }
printLog(log) { printLog(log) {
const data = log.object ? JSON.stringify(log.object) : log.text const data = log.object ? JSON.stringify(log.object) : log.text;
if (log.type === 'command') { if (log.type === "command") {
console.log(`${chalk.gray('>')}${data}`) console.log(`${chalk.gray(">")}${data}`);
this.lines.reset() this.lines.reset();
} else if (log.type === 'stderr') { } else if (log.type === "stderr") {
data.split('\n').forEach(v => { data.split("\n").forEach(v => {
if (v.length > 0) { if (v.length > 0) {
console.error(chalk.gray(`> ${v}`)) console.error(chalk.gray(`> ${v}`));
} }
}) });
this.lines.reset() this.lines.reset();
} else if (log.type === 'stdout') { } else if (log.type === "stdout") {
data.split('\n').forEach(v => { data.split("\n").forEach(v => {
if (v.length > 0) { if (v.length > 0) {
this.lines.write(`${chalk.gray('>')} ${v}`) this.lines.write(`${chalk.gray(">")} ${v}`);
} }
}) });
} }
} }
} };

69
lib/certs.js

@ -1,102 +1,101 @@
// Ours // Ours
const Now = require('../lib') const Now = require("../lib");
module.exports = class Certs extends Now { module.exports = class Certs extends Now {
ls() { ls() {
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} GET now/certs`) console.time(`> [debug] #${attempt} GET now/certs`);
} }
const res = await this._fetch('/now/certs') const res = await this._fetch("/now/certs");
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} GET now/certs`) console.timeEnd(`> [debug] #${attempt} GET now/certs`);
} }
const body = await res.json() const body = await res.json();
return body.certs return body.certs;
}) });
} }
create(cn) { create(cn) {
return this.createCert(cn) return this.createCert(cn);
} }
renew(cn) { renew(cn) {
return this.createCert(cn, {renew: true}) return this.createCert(cn, { renew: true });
} }
put(cn, crt, key, ca) { put(cn, crt, key, ca) {
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} PUT now/certs`) console.time(`> [debug] #${attempt} PUT now/certs`);
} }
const res = await this._fetch('/now/certs', { const res = await this._fetch("/now/certs", {
method: 'PUT', method: "PUT",
body: { body: {
domains: [cn], domains: [cn],
ca, ca,
cert: crt, cert: crt,
key key
} }
}) });
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} PUT now/certs`) console.timeEnd(`> [debug] #${attempt} PUT now/certs`);
} }
if (res.status === 403) { if (res.status === 403) {
return bail(new Error('Unauthorized')) return bail(new Error("Unauthorized"));
} }
const body = await res.json() const body = await res.json();
if (res.status !== 200) { if (res.status !== 200) {
if (res.status === 404 || res.status === 400) { if (res.status === 404 || res.status === 400) {
const err = new Error(body.error.message) const err = new Error(body.error.message);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} }
throw new Error(body.error.message) throw new Error(body.error.message);
} }
return body return body;
}) });
} }
delete(cn) { delete(cn) {
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} DELETE now/certs/${cn}`) console.time(`> [debug] #${attempt} DELETE now/certs/${cn}`);
} }
const res = await this._fetch(`/now/certs/${cn}`, {method: 'DELETE'}) const res = await this._fetch(`/now/certs/${cn}`, { method: "DELETE" });
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} DELETE now/certs/${cn}`) console.timeEnd(`> [debug] #${attempt} DELETE now/certs/${cn}`);
} }
if (res.status === 403) { if (res.status === 403) {
return bail(new Error('Unauthorized')) return bail(new Error("Unauthorized"));
} }
const body = await res.json() const body = await res.json();
if (res.status !== 200) { if (res.status !== 200) {
if (res.status === 404 || res.status === 400) { if (res.status === 404 || res.status === 400) {
const err = new Error(body.error.message) const err = new Error(body.error.message);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} }
throw new Error(body.error.message) throw new Error(body.error.message);
} }
return body return body;
}) });
} }
} };

26
lib/cfg.js

@ -1,23 +1,25 @@
// Native // Native
const {homedir} = require('os') const { homedir } = require("os");
const path = require('path') const path = require("path");
// Packages // Packages
const fs = require('fs-promise') const fs = require("fs-promise");
let file = process.env.NOW_JSON ? path.resolve(process.env.NOW_JSON) : path.resolve(homedir(), '.now.json') let file = process.env.NOW_JSON
? path.resolve(process.env.NOW_JSON)
: path.resolve(homedir(), ".now.json");
function setConfigFile(nowjson) { function setConfigFile(nowjson) {
file = path.resolve(nowjson) file = path.resolve(nowjson);
} }
function read() { function read() {
let existing = null let existing = null;
try { try {
existing = fs.readFileSync(file, 'utf8') existing = fs.readFileSync(file, "utf8");
existing = JSON.parse(existing) existing = JSON.parse(existing);
} catch (err) {} } catch (err) {}
return existing || {} return existing || {};
} }
/** /**
@ -29,12 +31,12 @@ function read() {
*/ */
function merge(data) { function merge(data) {
const cfg = Object.assign({}, read(), data) const cfg = Object.assign({}, read(), data);
fs.writeFileSync(file, JSON.stringify(cfg, null, 2)) fs.writeFileSync(file, JSON.stringify(cfg, null, 2));
} }
module.exports = { module.exports = {
setConfigFile, setConfigFile,
read, read,
merge merge
} };

12
lib/copy.js

@ -1,16 +1,16 @@
// Packages // Packages
const {copy: _copy} = require('copy-paste') const { copy: _copy } = require("copy-paste");
function copy(text) { function copy(text) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
_copy(text, err => { _copy(text, err => {
if (err) { if (err) {
return reject(err) return reject(err);
} }
resolve() resolve();
}) });
}) });
} }
module.exports = copy module.exports = copy;

61
lib/credit-cards.js

@ -1,33 +1,34 @@
const stripe = require('stripe')('pk_live_alyEi3lN0kSwbdevK0nrGwTw') const stripe = require("stripe")("pk_live_alyEi3lN0kSwbdevK0nrGwTw");
const Now = require('../lib') const Now = require("../lib");
module.exports = class CreditCards extends Now { module.exports = class CreditCards extends Now {
async ls() { async ls() {
const res = await this._fetch('/www/user/cards') const res = await this._fetch("/www/user/cards");
const body = await res.json() const body = await res.json();
return body return body;
} }
async setDefault(cardId) { async setDefault(cardId) {
await this._fetch('/www/user/cards/default', { await this._fetch("/www/user/cards/default", {
method: 'PUT', method: "PUT",
body: {cardId} body: { cardId }
}) });
return true return true;
} }
async rm(cardId) { async rm(cardId) {
await this._fetch(`/www/user/cards/${encodeURIComponent(cardId)}`, {method: 'DELEtE'}) await this._fetch(`/www/user/cards/${encodeURIComponent(cardId)}`, {
return true method: "DELEtE"
});
return true;
} }
/* eslint-disable camelcase */ /* eslint-disable camelcase */
add(card) { add(card) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const expDateParts = card.expDate.split(' / ') const expDateParts = card.expDate.split(" / ");
card = { card = {
name: card.name, name: card.name,
number: card.cardNumber, number: card.cardNumber,
@ -37,34 +38,34 @@ module.exports = class CreditCards extends Now {
address_state: card.state, address_state: card.state,
address_city: card.city, address_city: card.city,
address_line1: card.address1 address_line1: card.address1
} };
card.exp_month = expDateParts[0] card.exp_month = expDateParts[0];
card.exp_year = expDateParts[1] card.exp_year = expDateParts[1];
try { try {
const stripeToken = (await stripe.tokens.create({card})).id const stripeToken = (await stripe.tokens.create({ card })).id;
const res = await this._fetch('/www/user/cards', { const res = await this._fetch("/www/user/cards", {
method: 'POST', method: "POST",
body: {stripeToken} body: { stripeToken }
}) });
const body = await res.json() const body = await res.json();
if (body.card && body.card.id) { if (body.card && body.card.id) {
resolve({ resolve({
last4: body.card.last4 last4: body.card.last4
}) });
} else if (body.error && body.error.message) { } else if (body.error && body.error.message) {
reject({message: body.error.message}) reject({ message: body.error.message });
} else { } else {
reject('Unknown error') reject("Unknown error");
} }
} catch (err) { } catch (err) {
reject({ reject({
message: err.message || 'Unknown error' message: err.message || "Unknown error"
}) });
} }
}) });
} }
} };

12
lib/dns.js

@ -1,15 +1,15 @@
// Packages // Packages
const dns = require('dns') const dns = require("dns");
function resolve4(host) { function resolve4(host) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
return dns.resolve4(host, (err, answer) => { return dns.resolve4(host, (err, answer) => {
if (err) { if (err) {
return reject(err) return reject(err);
} }
resolve(answer) resolve(answer);
}) });
}) });
} }
module.exports = resolve4 module.exports = resolve4;

125
lib/domain-records.js

@ -1,130 +1,139 @@
// Ours // Ours
const Now = require('../lib') const Now = require("../lib");
module.exports = class DomainRecords extends Now { module.exports = class DomainRecords extends Now {
async getRecord(id) { async getRecord(id) {
const all = (await this.ls()).entries() const all = (await this.ls()).entries();
for (const [domain, records] of all) { for (const [domain, records] of all) {
for (const record of records) { for (const record of records) {
if (record.id === id) { if (record.id === id) {
record.domain = domain record.domain = domain;
return record return record;
} }
} }
} }
return null return null;
} }
async ls(dom) { async ls(dom) {
let domains let domains;
if (dom) { if (dom) {
domains = [dom] domains = [dom];
} else { } else {
const ret = await this.listDomains() const ret = await this.listDomains();
domains = ret.filter(x => !x.isExternal).map(x => x.name).sort((a, b) => a.localeCompare(b)) domains = ret
.filter(x => !x.isExternal)
.map(x => x.name)
.sort((a, b) => a.localeCompare(b));
} }
const records = new Map() const records = new Map();
for (const domain of domains) { for (const domain of domains) {
const body = await this.retry(async (bail, attempt) => { const body = await this.retry(async (bail, attempt) => {
const url = `/domains/${domain}/records` const url = `/domains/${domain}/records`;
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} GET ${url}`) console.time(`> [debug] #${attempt} GET ${url}`);
} }
const res = await this._fetch(url) const res = await this._fetch(url);
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} GET ${url}`) console.timeEnd(`> [debug] #${attempt} GET ${url}`);
} }
const body = await res.json() const body = await res.json();
if (res.status === 404 && body.code === 'not_found') { if (res.status === 404 && body.code === "not_found") {
return bail(new Error(body.message)) return bail(new Error(body.message));
} else if (res.status !== 200) { } else if (res.status !== 200) {
throw new Error(`Failed to get DNS records for domain "${domain}"`) throw new Error(`Failed to get DNS records for domain "${domain}"`);
} }
return body return body;
}) });
records.set(domain, body.records.sort((a, b) => a.slug.localeCompare(b.slug))) records.set(
domain,
body.records.sort((a, b) => a.slug.localeCompare(b.slug))
);
} }
return records return records;
} }
create(domain, data) { create(domain, data) {
const url = `/domains/${domain}/records` const url = `/domains/${domain}/records`;
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} POST ${url}`) console.time(`> [debug] #${attempt} POST ${url}`);
} }
const res = await this._fetch(url, { const res = await this._fetch(url, {
method: 'POST', method: "POST",
body: data body: data
}) });
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} POST ${url}`) console.timeEnd(`> [debug] #${attempt} POST ${url}`);
} }
const body = await res.json() const body = await res.json();
if (res.status === 400) { if (res.status === 400) {
return bail(new Error(body.error ? body.error.message : 'Unknown error')) return bail(
new Error(body.error ? body.error.message : "Unknown error")
);
} else if (res.status === 403) { } else if (res.status === 403) {
const err = new Error(`Not authorized to access the domain "${domain}"`) const err = new Error(
err.userError = true `Not authorized to access the domain "${domain}"`
return bail(err) );
err.userError = true;
return bail(err);
} else if (res.status === 404) { } else if (res.status === 404) {
let err let err;
if (body.error.code === 'not_found') { if (body.error.code === "not_found") {
err = new Error(`The domain "${domain}" was not found`) err = new Error(`The domain "${domain}" was not found`);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} }
} }
if (res.status !== 200) { if (res.status !== 200) {
throw new Error(body.error ? body.error.message : 'Unknown error') throw new Error(body.error ? body.error.message : "Unknown error");
} }
return body return body;
}) });
} }
delete(domain, recordId) { delete(domain, recordId) {
const url = `/domains/${domain}/records/${recordId}` const url = `/domains/${domain}/records/${recordId}`;
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} DELETE ${url}`) console.time(`> [debug] #${attempt} DELETE ${url}`);
} }
const res = await this._fetch(url, {method: 'DELETE'}) const res = await this._fetch(url, { method: "DELETE" });
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} DELETE ${url}`) console.timeEnd(`> [debug] #${attempt} DELETE ${url}`);
} }
const body = await res.json() const body = await res.json();
if (res.status === 403) { if (res.status === 403) {
const err = new Error(`Not authorized to access domain ${domain}`) const err = new Error(`Not authorized to access domain ${domain}`);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} else if (res.status === 404) { } else if (res.status === 404) {
let err let err;
if (body.error.code === 'not_found') { if (body.error.code === "not_found") {
err = new Error(body.error.message) err = new Error(body.error.message);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} }
} }
if (res.status !== 200) { if (res.status !== 200) {
throw new Error(body.error ? body.error.message : 'Unkown error') throw new Error(body.error ? body.error.message : "Unkown error");
} }
return body return body;
}) });
} }
} };

70
lib/domains.js

@ -1,77 +1,81 @@
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
// Ours // Ours
const Now = require('../lib') const Now = require("../lib");
const isZeitWorld = require('./is-zeit-world') const isZeitWorld = require("./is-zeit-world");
const {DNS_VERIFICATION_ERROR} = require('./errors') const { DNS_VERIFICATION_ERROR } = require("./errors");
const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/ const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/;
module.exports = class Domains extends Now { module.exports = class Domains extends Now {
async ls() { async ls() {
return await this.listDomains() return await this.listDomains();
} }
async rm(name) { async rm(name) {
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} DELETE /domains/${name}`) console.time(`> [debug] #${attempt} DELETE /domains/${name}`);
} }
const res = await this._fetch(`/domains/${name}`, {method: 'DELETE'}) const res = await this._fetch(`/domains/${name}`, { method: "DELETE" });
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} DELETE /domains/${name}`) console.timeEnd(`> [debug] #${attempt} DELETE /domains/${name}`);
} }
if (res.status === 403) { if (res.status === 403) {
return bail(new Error('Unauthorized')) return bail(new Error("Unauthorized"));
} }
if (res.status !== 200) { if (res.status !== 200) {
const body = await res.json() const body = await res.json();
throw new Error(body.error.message) throw new Error(body.error.message);
} }
}) });
} }
async add(domain, skipVerification, isExternal) { async add(domain, skipVerification, isExternal) {
if (!domainRegex.test(domain)) { if (!domainRegex.test(domain)) {
const err = new Error(`The supplied value ${chalk.bold(`"${domain}"`)} is not a valid domain.`) const err = new Error(
err.userError = true `The supplied value ${chalk.bold(`"${domain}"`)} is not a valid domain.`
throw err );
err.userError = true;
throw err;
} }
if (skipVerification || isExternal) { if (skipVerification || isExternal) {
return this.setupDomain(domain, {isExternal}) return this.setupDomain(domain, { isExternal });
} }
let ns let ns;
try { try {
console.log('> Verifying nameservers…') console.log("> Verifying nameservers…");
const res = await this.getNameservers(domain) const res = await this.getNameservers(domain);
ns = res.nameservers ns = res.nameservers;
} catch (err) { } catch (err) {
const err2 = new Error(`Unable to fetch nameservers for ${chalk.underline(chalk.bold(domain))}.`) const err2 = new Error(
err2.userError = true `Unable to fetch nameservers for ${chalk.underline(chalk.bold(domain))}.`
throw err2 );
err2.userError = true;
throw err2;
} }
if (isZeitWorld(ns)) { if (isZeitWorld(ns)) {
console.log(`> Verification ${chalk.bold('OK')}!`) console.log(`> Verification ${chalk.bold("OK")}!`);
return this.setupDomain(domain) return this.setupDomain(domain);
} }
if (this._debug) { if (this._debug) {
console.log(`> [debug] Supplied domain "${domain}" has non-zeit nameservers`) console.log(
`> [debug] Supplied domain "${domain}" has non-zeit nameservers`
);
} }
const err3 = new Error(DNS_VERIFICATION_ERROR) const err3 = new Error(DNS_VERIFICATION_ERROR);
err3.userError = true err3.userError = true;
throw err3 throw err3;
} }
};
}

34
lib/error.js

@ -1,33 +1,37 @@
// Packages // Packages
const ms = require('ms') const ms = require("ms");
const chalk = require('chalk') const chalk = require("chalk");
const error = require('./utils/output/error') const error = require("./utils/output/error");
function handleError(err) { function handleError(err) {
if (err.status === 403) { if (err.status === 403) {
error('Authentication error. Run `now -L` or `now --login` to log-in again.') error(
"Authentication error. Run `now -L` or `now --login` to log-in again."
);
} else if (err.status === 429) { } else if (err.status === 429) {
if (err.retryAfter === 'never') { if (err.retryAfter === "never") {
error(err.message) error(err.message);
} else if (err.retryAfter === null) { } else if (err.retryAfter === null) {
error('Rate limit exceeded error. Please try later.') error("Rate limit exceeded error. Please try later.");
} else { } else {
error('Rate limit exceeded error. Try again in ' + error(
ms(err.retryAfter * 1000, {long: true}) + "Rate limit exceeded error. Try again in " +
', or upgrade your account by runnung ' + ms(err.retryAfter * 1000, { long: true }) +
`${chalk.gray('`')}${chalk.cyan('now upgrade')}${chalk.gray('`')}`) ", or upgrade your account by runnung " +
`${chalk.gray("`")}${chalk.cyan("now upgrade")}${chalk.gray("`")}`
);
} }
} else if (err.userError) { } else if (err.userError) {
error(err.message) error(err.message);
} else if (err.status === 500) { } else if (err.status === 500) {
error('Unexpected server error. Please retry.') error("Unexpected server error. Please retry.");
} else { } else {
error(`Unexpected error. Please try later. (${err.message})`) error(`Unexpected error. Please try later. (${err.message})`);
} }
} }
module.exports = { module.exports = {
handleError, handleError,
error error
} };

18
lib/errors.js

@ -1,17 +1,17 @@
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
const DNS_VERIFICATION_ERROR = `Please make sure that your nameservers point to ${chalk.underline('zeit.world')}. const DNS_VERIFICATION_ERROR = `Please make sure that your nameservers point to ${chalk.underline("zeit.world")}.
> Examples: (full list at ${chalk.underline('https://zeit.world')}) > Examples: (full list at ${chalk.underline("https://zeit.world")})
> ${chalk.gray('-')} ${chalk.underline('california.zeit.world')} ${chalk.dim('173.255.215.107')} > ${chalk.gray("-")} ${chalk.underline("california.zeit.world")} ${chalk.dim("173.255.215.107")}
> ${chalk.gray('-')} ${chalk.underline('newark.zeit.world')} ${chalk.dim('173.255.231.87')} > ${chalk.gray("-")} ${chalk.underline("newark.zeit.world")} ${chalk.dim("173.255.231.87")}
> ${chalk.gray('-')} ${chalk.underline('london.zeit.world')} ${chalk.dim('178.62.47.76')} > ${chalk.gray("-")} ${chalk.underline("london.zeit.world")} ${chalk.dim("178.62.47.76")}
> ${chalk.gray('-')} ${chalk.underline('singapore.zeit.world')} ${chalk.dim('119.81.97.170')}` > ${chalk.gray("-")} ${chalk.underline("singapore.zeit.world")} ${chalk.dim("119.81.97.170")}`;
const DOMAIN_VERIFICATION_ERROR = DNS_VERIFICATION_ERROR + const DOMAIN_VERIFICATION_ERROR = DNS_VERIFICATION_ERROR +
`\n> Alternatively, ensure it resolves to ${chalk.underline('alias.zeit.co')} via ${chalk.dim('CNAME')} / ${chalk.dim('ALIAS')}.` `\n> Alternatively, ensure it resolves to ${chalk.underline("alias.zeit.co")} via ${chalk.dim("CNAME")} / ${chalk.dim("ALIAS")}.`;
module.exports = { module.exports = {
DNS_VERIFICATION_ERROR, DNS_VERIFICATION_ERROR,
DOMAIN_VERIFICATION_ERROR DOMAIN_VERIFICATION_ERROR
} };

263
lib/get-files.js

@ -1,15 +1,15 @@
// Native // Native
const {resolve} = require('path') const { resolve } = require("path");
// Packages // Packages
const flatten = require('arr-flatten') const flatten = require("arr-flatten");
const unique = require('array-unique') const unique = require("array-unique");
const ignore = require('ignore') const ignore = require("ignore");
const _glob = require('glob') const _glob = require("glob");
const {stat, readdir, readFile} = require('fs-promise') const { stat, readdir, readFile } = require("fs-promise");
// Ours // Ours
const IGNORED = require('./ignored') const IGNORED = require("./ignored");
/** /**
* Returns a list of files in the given * Returns a list of files in the given
@ -24,84 +24,95 @@ const IGNORED = require('./ignored')
* @return {Array} comprehensive list of paths to sync * @return {Array} comprehensive list of paths to sync
*/ */
async function npm(path, pkg, nowConfig = null, { async function npm(
limit = null, path,
hasNowJson = false, pkg,
debug = false nowConfig = null,
} = {}) { {
const whitelist = (nowConfig && nowConfig.files) || pkg.files limit = null,
hasNowJson = false,
debug = false
} = {}
) {
const whitelist = (nowConfig && nowConfig.files) || pkg.files;
// the package.json `files` whitelist still // the package.json `files` whitelist still
// honors ignores: https://docs.npmjs.com/files/package.json#files // honors ignores: https://docs.npmjs.com/files/package.json#files
const search_ = whitelist || ['.'] const search_ = whitelist || ["."];
// convert all filenames into absolute paths // convert all filenames into absolute paths
const search = Array.prototype.concat.apply([], (await Promise.all(search_.map(file => glob(file, {cwd: path, absolute: true, dot: true}))))) const search = Array.prototype.concat.apply(
[],
await Promise.all(
search_.map(file => glob(file, { cwd: path, absolute: true, dot: true }))
)
);
// always include the "main" file // always include the "main" file
if (pkg.main) { if (pkg.main) {
search.push(require.resolve(resolve(path, pkg.main), 'may-exclude')) // pkg: may-exclude suppresses warnings search.push(require.resolve(resolve(path, pkg.main), "may-exclude")); // pkg: may-exclude suppresses warnings
} }
// compile list of ignored patterns and files // compile list of ignored patterns and files
const npmIgnore = await maybeRead(resolve(path, '.npmignore'), null) const npmIgnore = await maybeRead(resolve(path, ".npmignore"), null);
const gitIgnore = npmIgnore === null ? const gitIgnore = npmIgnore === null
await maybeRead(resolve(path, '.gitignore')) : ? await maybeRead(resolve(path, ".gitignore"))
null : null;
const filter = ignore().add( const filter = ignore()
IGNORED + .add(
'\n' + IGNORED + "\n" + clearRelative(npmIgnore === null ? gitIgnore : npmIgnore)
clearRelative(npmIgnore === null ? gitIgnore : npmIgnore) )
).createFilter() .createFilter();
const prefixLength = path.length + 1 const prefixLength = path.length + 1;
// the package.json `files` whitelist still // the package.json `files` whitelist still
// honors npmignores: https://docs.npmjs.com/files/package.json#files // honors npmignores: https://docs.npmjs.com/files/package.json#files
// but we don't ignore if the user is explicitly listing files // but we don't ignore if the user is explicitly listing files
// under the now namespace, or using files in combination with gitignore // under the now namespace, or using files in combination with gitignore
const overrideIgnores = (pkg.now && pkg.now.files) || (gitIgnore !== null && pkg.files) const overrideIgnores = (pkg.now && pkg.now.files) ||
const accepts = overrideIgnores ? (gitIgnore !== null && pkg.files);
() => true : const accepts = overrideIgnores
file => { ? () => true
const relativePath = file.substr(prefixLength) : file => {
const relativePath = file.substr(prefixLength);
if (relativePath === '') {
return true if (relativePath === "") {
} return true;
}
const accepted = filter(relativePath) const accepted = filter(relativePath);
if (!accepted && debug) { if (!accepted && debug) {
console.log('> [debug] ignoring "%s"', file) console.log('> [debug] ignoring "%s"', file);
} }
return accepted return accepted;
} };
// locate files // locate files
if (debug) { if (debug) {
console.time(`> [debug] locating files ${path}`) console.time(`> [debug] locating files ${path}`);
} }
const files = await explode(search, { const files = await explode(search, {
accepts, accepts,
limit, limit,
debug debug
}) });
if (debug) { if (debug) {
console.timeEnd(`> [debug] locating files ${path}`) console.timeEnd(`> [debug] locating files ${path}`);
} }
// always include manifest as npm does not allow ignoring it // always include manifest as npm does not allow ignoring it
// source: https://docs.npmjs.com/files/package.json#files // source: https://docs.npmjs.com/files/package.json#files
files.push(asAbsolute('package.json', path)) files.push(asAbsolute("package.json", path));
if (hasNowJson) { if (hasNowJson) {
files.push(asAbsolute('now.json', path)) files.push(asAbsolute("now.json", path));
} }
// get files // get files
return unique(files) return unique(files);
} }
/** /**
@ -112,13 +123,13 @@ async function npm(path, pkg, nowConfig = null, {
* @param {String} parent full path * @param {String} parent full path
*/ */
const asAbsolute = function (path, parent) { const asAbsolute = function(path, parent) {
if (path[0] === '/') { if (path[0] === "/") {
return path return path;
} }
return resolve(parent, path) return resolve(parent, path);
} };
/** /**
* Returns a list of files in the given * Returns a list of files in the given
@ -133,81 +144,89 @@ const asAbsolute = function (path, parent) {
* @return {Array} comprehensive list of paths to sync * @return {Array} comprehensive list of paths to sync
*/ */
async function docker(path, nowConfig = null, { async function docker(
limit = null, path,
hasNowJson = false, nowConfig = null,
debug = false {
} = {}) { limit = null,
const whitelist = nowConfig && nowConfig.files hasNowJson = false,
debug = false
} = {}
) {
const whitelist = nowConfig && nowConfig.files;
// base search path // base search path
// the now.json `files` whitelist still // the now.json `files` whitelist still
// honors ignores: https://docs.npmjs.com/files/package.json#files // honors ignores: https://docs.npmjs.com/files/package.json#files
const search_ = whitelist || ['.'] const search_ = whitelist || ["."];
// convert all filenames into absolute paths // convert all filenames into absolute paths
const search = search_.map(file => asAbsolute(file, path)) const search = search_.map(file => asAbsolute(file, path));
// compile list of ignored patterns and files // compile list of ignored patterns and files
const dockerIgnore = await maybeRead(resolve(path, '.dockerignore'), null) const dockerIgnore = await maybeRead(resolve(path, ".dockerignore"), null);
const filter = ignore().add( const filter = ignore()
IGNORED + .add(
'\n' + IGNORED +
clearRelative(dockerIgnore === null ? "\n" +
await maybeRead(resolve(path, '.gitignore')) : clearRelative(
dockerIgnore) dockerIgnore === null
).createFilter() ? await maybeRead(resolve(path, ".gitignore"))
: dockerIgnore
const prefixLength = path.length + 1 )
const accepts = function (file) { )
const relativePath = file.substr(prefixLength) .createFilter();
if (relativePath === '') { const prefixLength = path.length + 1;
return true const accepts = function(file) {
const relativePath = file.substr(prefixLength);
if (relativePath === "") {
return true;
} }
const accepted = filter(relativePath) const accepted = filter(relativePath);
if (!accepted && debug) { if (!accepted && debug) {
console.log('> [debug] ignoring "%s"', file) console.log('> [debug] ignoring "%s"', file);
} }
return accepted return accepted;
} };
// locate files // locate files
if (debug) { if (debug) {
console.time(`> [debug] locating files ${path}`) console.time(`> [debug] locating files ${path}`);
} }
const files = await explode(search, {accepts, limit, debug}) const files = await explode(search, { accepts, limit, debug });
if (debug) { if (debug) {
console.timeEnd(`> [debug] locating files ${path}`) console.timeEnd(`> [debug] locating files ${path}`);
} }
// always include manifest as npm does not allow ignoring it // always include manifest as npm does not allow ignoring it
// source: https://docs.npmjs.com/files/package.json#files // source: https://docs.npmjs.com/files/package.json#files
files.push(asAbsolute('Dockerfile', path)) files.push(asAbsolute("Dockerfile", path));
if (hasNowJson) { if (hasNowJson) {
files.push(asAbsolute('now.json', path)) files.push(asAbsolute("now.json", path));
} }
// get files // get files
return unique(files) return unique(files);
} }
const glob = async function (pattern, options) { const glob = async function(pattern, options) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
_glob(pattern, options, (error, files) => { _glob(pattern, options, (error, files) => {
if (error) { if (error) {
reject(error) reject(error);
} else { } else {
resolve(files) resolve(files);
} }
}) });
}) });
} };
/** /**
* Explodes directories into a full list of files. * Explodes directories into a full list of files.
@ -223,52 +242,54 @@ const glob = async function (pattern, options) {
* @return {Array} of {String}s of full paths * @return {Array} of {String}s of full paths
*/ */
async function explode(paths, {accepts, debug}) { async function explode(paths, { accepts, debug }) {
const list = async file => { const list = async file => {
let path = file let path = file;
let s let s;
if (!accepts(file)) { if (!accepts(file)) {
return null return null;
} }
try { try {
s = await stat(path) s = await stat(path);
} catch (e) { } catch (e) {
// in case the file comes from `files` or `main` // in case the file comes from `files` or `main`
// and it wasn't specified with `.js` by the user // and it wasn't specified with `.js` by the user
path = file + '.js' path = file + ".js";
try { try {
s = await stat(path) s = await stat(path);
} catch (e2) { } catch (e2) {
if (debug) { if (debug) {
console.log('> [debug] ignoring invalid file "%s"', file) console.log('> [debug] ignoring invalid file "%s"', file);
} }
return null return null;
} }
} }
if (s.isDirectory()) { if (s.isDirectory()) {
const all = await readdir(file) const all = await readdir(file);
return many(all.map(subdir => asAbsolute(subdir, file))) return many(all.map(subdir => asAbsolute(subdir, file)));
} else if (!s.isFile()) { } else if (!s.isFile()) {
if (debug) { if (debug) {
console.log('> [debug] ignoring special file "%s"', file) console.log('> [debug] ignoring special file "%s"', file);
} }
return null return null;
} }
return path return path;
} };
const many = async all => { const many = async all => {
return await Promise.all(all.map(async file => { return await Promise.all(
return await list(file) all.map(async file => {
})) return await list(file);
} })
);
return flatten((await many(paths))).filter(v => v !== null) };
return flatten(await many(paths)).filter(v => v !== null);
} }
/** /**
@ -277,24 +298,24 @@ async function explode(paths, {accepts, debug}) {
* @return {String} results or `''` * @return {String} results or `''`
*/ */
const maybeRead = async function (path, default_ = '') { const maybeRead = async function(path, default_ = "") {
try { try {
return await readFile(path, 'utf8') return await readFile(path, "utf8");
} catch (err) { } catch (err) {
return default_ return default_;
} }
} };
/** /**
* Remove leading `./` from the beginning of ignores * Remove leading `./` from the beginning of ignores
* because our parser doesn't like them :| * because our parser doesn't like them :|
*/ */
const clearRelative = function (str) { const clearRelative = function(str) {
return str.replace(/(\n|^)\.\//g, '$1') return str.replace(/(\n|^)\.\//g, "$1");
} };
module.exports = { module.exports = {
npm, npm,
docker docker
} };

208
lib/git.js

@ -1,211 +1,209 @@
// Native // Native
const path = require('path') const path = require("path");
const url = require('url') const url = require("url");
const childProcess = require('child_process') const childProcess = require("child_process");
// Packages // Packages
const fs = require('fs-promise') const fs = require("fs-promise");
const download = require('download') const download = require("download");
const tmp = require('tmp-promise') const tmp = require("tmp-promise");
const isURL = require('is-url') const isURL = require("is-url");
const cloneRepo = (parts, tmpDir) => new Promise((resolve, reject) => { const cloneRepo = (parts, tmpDir) =>
let host new Promise((resolve, reject) => {
let host;
switch (parts.type) {
case 'GitLab': switch (parts.type) {
host = `gitlab.com` case "GitLab":
break host = `gitlab.com`;
case 'Bitbucket': break;
host = `bitbucket.org` case "Bitbucket":
break host = `bitbucket.org`;
default: break;
host = `github.com` default:
} host = `github.com`;
}
const url = `https://${host}/${parts.main}` const url = `https://${host}/${parts.main}`;
const ref = parts.ref || (parts.type === 'Bitbucket' ? 'default' : 'master') const ref = parts.ref ||
const cmd = `git clone ${url} --single-branch ${ref}` (parts.type === "Bitbucket" ? "default" : "master");
const cmd = `git clone ${url} --single-branch ${ref}`;
childProcess.exec(cmd, {cwd: tmpDir.path}, (err, stdout) => { childProcess.exec(cmd, { cwd: tmpDir.path }, (err, stdout) => {
if (err) { if (err) {
reject(err) reject(err);
} }
resolve(stdout) resolve(stdout);
}) });
}) });
const renameRepoDir = async (pathParts, tmpDir) => { const renameRepoDir = async (pathParts, tmpDir) => {
const tmpContents = await fs.readdir(tmpDir.path) const tmpContents = await fs.readdir(tmpDir.path);
const oldTemp = path.join(tmpDir.path, tmpContents[0]) const oldTemp = path.join(tmpDir.path, tmpContents[0]);
const newTemp = path.join(tmpDir.path, pathParts.main.replace('/', '-')) const newTemp = path.join(tmpDir.path, pathParts.main.replace("/", "-"));
await fs.rename(oldTemp, newTemp) await fs.rename(oldTemp, newTemp);
tmpDir.path = newTemp tmpDir.path = newTemp;
return tmpDir return tmpDir;
} };
const downloadRepo = async repoPath => { const downloadRepo = async repoPath => {
const pathParts = gitPathParts(repoPath) const pathParts = gitPathParts(repoPath);
const tmpDir = await tmp.dir({ const tmpDir = await tmp.dir({
// We'll remove it manually once deployment is done // We'll remove it manually once deployment is done
keep: true, keep: true,
// Recursively remove directory when calling respective method // Recursively remove directory when calling respective method
unsafeCleanup: true unsafeCleanup: true
}) });
let gitInstalled = true let gitInstalled = true;
try { try {
await cloneRepo(pathParts, tmpDir) await cloneRepo(pathParts, tmpDir);
} catch (err) { } catch (err) {
gitInstalled = false gitInstalled = false;
} }
if (gitInstalled) { if (gitInstalled) {
return await renameRepoDir(pathParts, tmpDir) return await renameRepoDir(pathParts, tmpDir);
} }
let url let url;
switch (pathParts.type) { switch (pathParts.type) {
case 'GitLab': { case "GitLab": {
const ref = pathParts.ref ? `?ref=${pathParts.ref}` : '' const ref = pathParts.ref ? `?ref=${pathParts.ref}` : "";
url = `https://gitlab.com/${pathParts.main}/repository/archive.tar` + ref url = `https://gitlab.com/${pathParts.main}/repository/archive.tar` + ref;
break break;
} }
case 'Bitbucket': case "Bitbucket":
url = `https://bitbucket.org/${pathParts.main}/get/${pathParts.ref || 'default'}.zip` url = `https://bitbucket.org/${pathParts.main}/get/${pathParts.ref || "default"}.zip`;
break break;
default: default:
url = `https://api.github.com/repos/${pathParts.main}/tarball/${pathParts.ref}` url = `https://api.github.com/repos/${pathParts.main}/tarball/${pathParts.ref}`;
} }
try { try {
await download(url, tmpDir.path, { await download(url, tmpDir.path, {
extract: true extract: true
}) });
} catch (err) { } catch (err) {
tmpDir.cleanup() tmpDir.cleanup();
return false return false;
} }
return await renameRepoDir(pathParts, tmpDir) return await renameRepoDir(pathParts, tmpDir);
} };
const capitalizePlatform = name => { const capitalizePlatform = name => {
const names = { const names = {
github: 'GitHub', github: "GitHub",
gitlab: 'GitLab', gitlab: "GitLab",
bitbucket: 'Bitbucket' bitbucket: "Bitbucket"
} };
return names[name] return names[name];
} };
const splittedURL = fullURL => { const splittedURL = fullURL => {
const parsedURL = url.parse(fullURL) const parsedURL = url.parse(fullURL);
const pathParts = parsedURL.path.split('/') const pathParts = parsedURL.path.split("/");
pathParts.shift() pathParts.shift();
// Set path to repo... // Set path to repo...
const main = pathParts[0] + '/' + pathParts[1] const main = pathParts[0] + "/" + pathParts[1];
// ...and then remove it from the parts // ...and then remove it from the parts
pathParts.splice(0, 2) pathParts.splice(0, 2);
// Assign Git reference // Assign Git reference
let ref = pathParts.length >= 2 ? pathParts[1] : '' let ref = pathParts.length >= 2 ? pathParts[1] : "";
// Firstly be sure that we haven know the ref type // Firstly be sure that we haven know the ref type
if (pathParts[0]) { if (pathParts[0]) {
// Then shorten the SHA of the commit // Then shorten the SHA of the commit
if (pathParts[0] === 'commit' || pathParts[0] === 'commits') { if (pathParts[0] === "commit" || pathParts[0] === "commits") {
ref = ref.substring(0, 7) ref = ref.substring(0, 7);
} }
} }
// We're deploying master by default, // We're deploying master by default,
// so there's no need to indicate it explicitly // so there's no need to indicate it explicitly
if (ref === 'master') { if (ref === "master") {
ref = '' ref = "";
} }
return { return {
main, main,
ref, ref,
type: capitalizePlatform(parsedURL.host.split('.')[0]) type: capitalizePlatform(parsedURL.host.split(".")[0])
} };
} };
const gitPathParts = main => { const gitPathParts = main => {
let ref = '' let ref = "";
if (isURL(main)) { if (isURL(main)) {
return splittedURL(main) return splittedURL(main);
} }
if (main.split('/')[1].includes('#')) { if (main.split("/")[1].includes("#")) {
const parts = main.split('#') const parts = main.split("#");
ref = parts[1] ref = parts[1];
main = parts[0] main = parts[0];
} }
return { return {
main, main,
ref, ref,
type: capitalizePlatform('github') type: capitalizePlatform("github")
} };
} };
const isRepoPath = path => { const isRepoPath = path => {
if (!path) { if (!path) {
return false return false;
} }
const allowedHosts = [ const allowedHosts = ["github.com", "gitlab.com", "bitbucket.org"];
'github.com',
'gitlab.com',
'bitbucket.org'
]
if (isURL(path)) { if (isURL(path)) {
const urlParts = url.parse(path) const urlParts = url.parse(path);
const slashSplitted = urlParts.path.split('/').filter(n => n) const slashSplitted = urlParts.path.split("/").filter(n => n);
const notBare = slashSplitted.length >= 2 const notBare = slashSplitted.length >= 2;
if (allowedHosts.includes(urlParts.host) && notBare) { if (allowedHosts.includes(urlParts.host) && notBare) {
return true return true;
} }
return 'no-valid-url' return "no-valid-url";
} }
return /[^\s\\]\/[^\s\\]/g.test(path) return /[^\s\\]\/[^\s\\]/g.test(path);
} };
const fromGit = async (path, debug) => { const fromGit = async (path, debug) => {
let tmpDir = false let tmpDir = false;
try { try {
tmpDir = await downloadRepo(path) tmpDir = await downloadRepo(path);
} catch (err) { } catch (err) {
if (debug) { if (debug) {
console.log(`Could not download "${path}" repo from GitHub`) console.log(`Could not download "${path}" repo from GitHub`);
} }
} }
return tmpDir return tmpDir;
} };
module.exports = { module.exports = {
gitPathParts, gitPathParts,
isRepoPath, isRepoPath,
fromGit fromGit
} };

60
lib/hash.js

@ -1,11 +1,11 @@
// Native // Native
const {createHash} = require('crypto') const { createHash } = require("crypto");
const path = require('path') const path = require("path");
// Packages // Packages
const {readFile} = require('fs-promise') const { readFile } = require("fs-promise");
/** /**
* Computes hashes for the contents of each file given. * Computes hashes for the contents of each file given.
* *
* @param {Array} of {String} full paths * @param {Array} of {String} full paths
@ -13,28 +13,30 @@ const {readFile} = require('fs-promise')
*/ */
async function hashes(files, isStatic, pkg) { async function hashes(files, isStatic, pkg) {
const map = new Map() const map = new Map();
await Promise.all(files.map(async name => { await Promise.all(
const filename = path.basename(name) files.map(async name => {
let data const filename = path.basename(name);
let data;
if (isStatic && filename === 'package.json') {
const packageString = JSON.stringify(pkg, null, 2) if (isStatic && filename === "package.json") {
data = Buffer.from(packageString) const packageString = JSON.stringify(pkg, null, 2);
} else { data = Buffer.from(packageString);
data = await readFile(name) } else {
} data = await readFile(name);
}
const h = hash(data)
const entry = map.get(h) const h = hash(data);
if (entry) { const entry = map.get(h);
entry.names.push(name) if (entry) {
} else { entry.names.push(name);
map.set(hash(data), {names: [name], data}) } else {
} map.set(hash(data), { names: [name], data });
})) }
return map })
);
return map;
} }
/** /**
@ -45,9 +47,7 @@ async function hashes(files, isStatic, pkg) {
*/ */
function hash(buf) { function hash(buf) {
return createHash('sha1') return createHash("sha1").update(buf).digest("hex");
.update(buf)
.digest('hex')
} }
module.exports = hashes module.exports = hashes;

2
lib/ignored.js

@ -14,4 +14,4 @@ module.exports = `.hg
npm-debug.log npm-debug.log
config.gypi config.gypi
node_modules node_modules
CVS` CVS`;

4
lib/indent.js

@ -1,5 +1,5 @@
function indent(text, n) { function indent(text, n) {
return text.split('\n').map(l => ' '.repeat(n) + l).join('\n') return text.split("\n").map(l => " ".repeat(n) + l).join("\n");
} }
module.exports = indent module.exports = indent;

824
lib/index.js

File diff suppressed because it is too large

31
lib/is-zeit-world.js

@ -3,17 +3,17 @@
*/ */
const nameservers = new Set([ const nameservers = new Set([
'california.zeit.world', "california.zeit.world",
'london.zeit.world', "london.zeit.world",
'newark.zeit.world', "newark.zeit.world",
'sydney.zeit.world', "sydney.zeit.world",
'iowa.zeit.world', "iowa.zeit.world",
'dallas.zeit.world', "dallas.zeit.world",
'amsterdam.zeit.world', "amsterdam.zeit.world",
'paris.zeit.world', "paris.zeit.world",
'frankfurt.zeit.world', "frankfurt.zeit.world",
'singapore.zeit.world' "singapore.zeit.world"
]) ]);
/** /**
* Given an array of nameservers (ie: as returned * Given an array of nameservers (ie: as returned
@ -21,9 +21,10 @@ const nameservers = new Set([
* zeit.world's. * zeit.world's.
*/ */
function isZeitWorld(ns) { function isZeitWorld(ns) {
return ns.length > 1 && ns.every(host => { return ns.length > 1 &&
return nameservers.has(host) ns.every(host => {
}) return nameservers.has(host);
});
} }
module.exports = isZeitWorld module.exports = isZeitWorld;

133
lib/login.js

@ -1,116 +1,125 @@
// Native // Native
const os = require('os') const os = require("os");
// Packages // Packages
const {stringify: stringifyQuery} = require('querystring') const { stringify: stringifyQuery } = require("querystring");
const chalk = require('chalk') const chalk = require("chalk");
const fetch = require('node-fetch') const fetch = require("node-fetch");
const {validate} = require('email-validator') const { validate } = require("email-validator");
const readEmail = require('email-prompt') const readEmail = require("email-prompt");
const ora = require('ora') const ora = require("ora");
// Ours // Ours
const pkg = require('./pkg') const pkg = require("./pkg");
const ua = require('./ua') const ua = require("./ua");
const cfg = require('./cfg') const cfg = require("./cfg");
const info = require('./utils/output/info') const info = require("./utils/output/info");
const promptBool = require('./utils/input/prompt-bool') const promptBool = require("./utils/input/prompt-bool");
async function getVerificationData(url, email) { async function getVerificationData(url, email) {
const tokenName = `Now CLI ${os.platform()}-${os.arch()} ${pkg.version} (${os.hostname()})` const tokenName = `Now CLI ${os.platform()}-${os.arch()} ${pkg.version} (${os.hostname()})`;
const data = JSON.stringify({email, tokenName}) const data = JSON.stringify({ email, tokenName });
const res = await fetch(`${url}/now/registration`, { const res = await fetch(`${url}/now/registration`, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
'Content-Length': Buffer.byteLength(data), "Content-Length": Buffer.byteLength(data),
'User-Agent': ua "User-Agent": ua
}, },
body: data body: data
}) });
if (res.status !== 200) { if (res.status !== 200) {
throw new Error('Verification error') throw new Error("Verification error");
} }
const body = await res.json() const body = await res.json();
return body return body;
} }
async function verify(url, email, verificationToken) { async function verify(url, email, verificationToken) {
const query = { const query = {
email, email,
token: verificationToken token: verificationToken
} };
const res = await fetch(`${url}/now/registration/verify?${stringifyQuery(query)}`, { const res = await fetch(
headers: {'User-Agent': ua} `${url}/now/registration/verify?${stringifyQuery(query)}`,
}) {
const body = await res.json() headers: { "User-Agent": ua }
return body.token }
);
const body = await res.json();
return body.token;
} }
function sleep(ms) { function sleep(ms) {
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(resolve, ms) setTimeout(resolve, ms);
}) });
} }
async function register(url, {retryEmail = false} = {}) { async function register(url, { retryEmail = false } = {}) {
let email let email;
try { try {
email = await readEmail({invalid: retryEmail}) email = await readEmail({ invalid: retryEmail });
} catch (err) { } catch (err) {
process.stdout.write('\n') process.stdout.write("\n");
throw err throw err;
} }
process.stdout.write('\n') process.stdout.write("\n");
info(`By continuing, you declare that you agree with ${chalk.bold('https://zeit.co/terms')} and ${chalk.bold('https://zeit.co/privacy.')}`) info(
if (!await promptBool('Continue?')) { `By continuing, you declare that you agree with ${chalk.bold("https://zeit.co/terms")} and ${chalk.bold("https://zeit.co/privacy.")}`
info('Aborted.') );
process.exit() // eslint-disable-line unicorn/no-process-exit if (!await promptBool("Continue?")) {
info("Aborted.");
process.exit(); // eslint-disable-line unicorn/no-process-exit
} }
if (!validate(email)) { if (!validate(email)) {
return register(url, {retryEmail: true}) return register(url, { retryEmail: true });
} }
const {token, securityCode} = await getVerificationData(url, email) const { token, securityCode } = await getVerificationData(url, email);
console.log(`> Please follow the link sent to ${chalk.bold(email)} to log in.`) console.log(
`> Please follow the link sent to ${chalk.bold(email)} to log in.`
);
if (securityCode) { if (securityCode) {
console.log(`> Verify that the provided security code in the email matches ${chalk.cyan(chalk.bold(securityCode))}.`) console.log(
`> Verify that the provided security code in the email matches ${chalk.cyan(chalk.bold(securityCode))}.`
);
} }
process.stdout.write('\n') process.stdout.write("\n");
const spinner = ora({ const spinner = ora({
text: 'Waiting for confirmation...', text: "Waiting for confirmation...",
color: 'black' color: "black"
}).start() }).start();
let final let final;
do { do {
await sleep(2500) await sleep(2500);
try { try {
final = await verify(url, email, token) final = await verify(url, email, token);
} catch (err) {} } catch (err) {}
} while (!final) } while (!final);
spinner.text = 'Confirmed email address!' spinner.text = "Confirmed email address!";
spinner.stopAndPersist('✔') spinner.stopAndPersist("✔");
process.stdout.write('\n') process.stdout.write("\n");
return {email, token: final} return { email, token: final };
} }
module.exports = async function (url) { module.exports = async function(url) {
const loginData = await register(url) const loginData = await register(url);
cfg.merge(loginData) cfg.merge(loginData);
return loginData.token return loginData.token;
} };

12
lib/logs.js

@ -1,12 +1,12 @@
exports.compare = function (a, b) { exports.compare = function(a, b) {
return a.serial.localeCompare(b.serial) || return a.serial.localeCompare(b.serial) ||
// for the case serials are a same value on old logs // for the case serials are a same value on old logs
a.created.getTime() - b.created.getTime() a.created.getTime() - b.created.getTime();
} };
exports.deserialize = function (log) { exports.deserialize = function(log) {
return Object.assign({}, log, { return Object.assign({}, log, {
data: new Date(log.date), data: new Date(log.date),
created: new Date(log.created) created: new Date(log.created)
}) });
} };

8
lib/pkg.js

@ -1,8 +1,8 @@
let pkg let pkg;
try { try {
pkg = require('../package.json') pkg = require("../package.json");
} catch (err) { } catch (err) {
pkg = require('../../package.json') pkg = require("../../package.json");
} }
module.exports = pkg module.exports = pkg;

43
lib/plans.js

@ -1,48 +1,47 @@
const ms = require('ms') const ms = require("ms");
const Now = require('../lib') const Now = require("../lib");
async function parsePlan(res) { async function parsePlan(res) {
let id let id;
let until let until;
const {subscription} = await res.json() const { subscription } = await res.json();
if (subscription) { if (subscription) {
id = subscription.plan.id id = subscription.plan.id;
if (subscription.cancel_at_period_end) { if (subscription.cancel_at_period_end) {
until = ms( until = ms(
new Date(subscription.current_period_end * 1000) - new Date(), new Date(subscription.current_period_end * 1000) - new Date(),
{long: true} { long: true }
) );
} }
} else { } else {
id = 'oss' id = "oss";
} }
return {id, until} return { id, until };
} }
module.exports = class Plans extends Now { module.exports = class Plans extends Now {
async getCurrent() { async getCurrent() {
const res = await this._fetch('/www/user/plan') const res = await this._fetch("/www/user/plan");
return await parsePlan(res) return await parsePlan(res);
} }
async set(plan) { async set(plan) {
const res = await this._fetch('/www/user/plan', { const res = await this._fetch("/www/user/plan", {
method: 'PUT', method: "PUT",
body: {plan} body: { plan }
}) });
if (res.ok) { if (res.ok) {
return await parsePlan(res) return await parsePlan(res);
} }
const err = new Error(res.statusText) const err = new Error(res.statusText);
err.res = res err.res = res;
throw err throw err;
} }
} };

82
lib/re-alias.js

@ -1,80 +1,86 @@
// Native // Native
const {join} = require('path') const { join } = require("path");
// Packages // Packages
const fs = require('fs-promise') const fs = require("fs-promise");
const chalk = require('chalk') const chalk = require("chalk");
// Ours // Ours
const {error} = require('./error') const { error } = require("./error");
const readMetaData = require('./read-metadata') const readMetaData = require("./read-metadata");
const NowAlias = require('./alias') const NowAlias = require("./alias");
exports.assignAlias = async (autoAlias, token, deployment, apiUrl, debug) => { exports.assignAlias = async (autoAlias, token, deployment, apiUrl, debug) => {
const aliases = new NowAlias(apiUrl, token, {debug}) const aliases = new NowAlias(apiUrl, token, { debug });
console.log(`> Assigning alias ${chalk.bold.underline(autoAlias)} to deployment...`) console.log(
`> Assigning alias ${chalk.bold.underline(autoAlias)} to deployment...`
);
// Assign alias // Assign alias
await aliases.set(String(deployment), String(autoAlias)) await aliases.set(String(deployment), String(autoAlias));
} };
exports.reAlias = async (token, host, help, exit, apiUrl, debug, alias) => { exports.reAlias = async (token, host, help, exit, apiUrl, debug, alias) => {
const path = process.cwd() const path = process.cwd();
const configFiles = { const configFiles = {
pkg: join(path, 'package.json'), pkg: join(path, "package.json"),
nowJSON: join(path, 'now.json') nowJSON: join(path, "now.json")
} };
if (!fs.existsSync(configFiles.pkg) && !fs.existsSync(configFiles.nowJSON)) { if (!fs.existsSync(configFiles.pkg) && !fs.existsSync(configFiles.nowJSON)) {
error(`Couldn't find a now.json or package.json file with an alias list in it`) error(
return `Couldn't find a now.json or package.json file with an alias list in it`
);
return;
} }
const {nowConfig, name} = await readMetaData(path, { const { nowConfig, name } = await readMetaData(path, {
deploymentType: 'npm', // hard coding settings… deploymentType: "npm", // hard coding settings…
quiet: true // `quiet` quiet: true // `quiet`
}) });
if (!host) { if (!host) {
const lastAlias = await alias.last(name) const lastAlias = await alias.last(name);
host = lastAlias.url host = lastAlias.url;
} }
if (!nowConfig) { if (!nowConfig) {
help() help();
return exit(0) return exit(0);
} }
let pointers = [] let pointers = [];
if (nowConfig.alias) { if (nowConfig.alias) {
const value = nowConfig.alias const value = nowConfig.alias;
if (typeof value === 'string') { if (typeof value === "string") {
pointers.push(value) pointers.push(value);
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
pointers = pointers.concat(nowConfig.alias) pointers = pointers.concat(nowConfig.alias);
} else { } else {
error(`Property ${chalk.grey('aliases')} is not a valid array or string`) error(`Property ${chalk.grey("aliases")} is not a valid array or string`);
return exit(1) return exit(1);
} }
} }
if (nowConfig.aliases && Array.isArray(nowConfig.aliases)) { if (nowConfig.aliases && Array.isArray(nowConfig.aliases)) {
console.log(`${chalk.red('Deprecated!')} The property ${chalk.grey('aliases')} will be ` + console.log(
`removed from the config file soon.`) `${chalk.red("Deprecated!")} The property ${chalk.grey("aliases")} will be ` +
console.log('Read more about the new way here: http://bit.ly/2l2v5Fg\n') `removed from the config file soon.`
);
console.log("Read more about the new way here: http://bit.ly/2l2v5Fg\n");
pointers = pointers.concat(nowConfig.aliases) pointers = pointers.concat(nowConfig.aliases);
} }
if (pointers.length === 0) { if (pointers.length === 0) {
help() help();
return exit(0) return exit(0);
} }
for (const pointer of pointers) { for (const pointer of pointers) {
await exports.assignAlias(pointer, token, host, apiUrl, debug) await exports.assignAlias(pointer, token, host, apiUrl, debug);
} }
} };

190
lib/read-metadata.js

@ -1,42 +1,45 @@
const {basename, resolve: resolvePath} = require('path') const { basename, resolve: resolvePath } = require("path");
const chalk = require('chalk') const chalk = require("chalk");
const {readFile, exists} = require('fs-promise') const { readFile, exists } = require("fs-promise");
const {parse: parseDockerfile} = require('docker-file-parser') const { parse: parseDockerfile } = require("docker-file-parser");
const listPackage = { const listPackage = {
scripts: { scripts: {
start: `NODE_ENV='production' serve ./content` start: `NODE_ENV='production' serve ./content`
}, },
dependencies: { dependencies: {
serve: '4.0.1' serve: "4.0.1"
} }
} };
module.exports = readMetaData module.exports = readMetaData;
async function readMetaData(path, { async function readMetaData(
deploymentType = 'npm', path,
deploymentName, {
quiet = false, deploymentType = "npm",
strict = true, deploymentName,
isStatic = false quiet = false,
}) { strict = true,
let pkg = {} isStatic = false
let nowConfig = null }
let hasNowJson = false ) {
let pkg = {};
let nowConfig = null;
let hasNowJson = false;
let name let name;
let description let description;
try { try {
nowConfig = JSON.parse(await readFile(resolvePath(path, 'now.json'))) nowConfig = JSON.parse(await readFile(resolvePath(path, "now.json")));
hasNowJson = true hasNowJson = true;
} catch (err) { } catch (err) {
// if the file doesn't exist then that's fine; any other error bubbles up // if the file doesn't exist then that's fine; any other error bubbles up
if (err.code !== 'ENOENT') { if (err.code !== "ENOENT") {
const e = Error(`Failed to read JSON in "${path}/now.json"`) const e = Error(`Failed to read JSON in "${path}/now.json"`);
e.userError = true e.userError = true;
throw e throw e;
} }
} }
@ -44,113 +47,130 @@ async function readMetaData(path, {
// user can specify the type of deployment explicitly in the `now.json` file // user can specify the type of deployment explicitly in the `now.json` file
// when both a package.json and Dockerfile exist // when both a package.json and Dockerfile exist
if (nowConfig.type) { if (nowConfig.type) {
deploymentType = nowConfig.type deploymentType = nowConfig.type;
} else if (nowConfig.type === undefined && !await exists(resolvePath(path, 'package.json'))) { } else if (
deploymentType = 'static' nowConfig.type === undefined &&
!await exists(resolvePath(path, "package.json"))
) {
deploymentType = "static";
} }
if (nowConfig.name) { if (nowConfig.name) {
deploymentName = nowConfig.name deploymentName = nowConfig.name;
} }
} }
if (deploymentType === 'static') { if (deploymentType === "static") {
isStatic = true isStatic = true;
deploymentType = 'npm' deploymentType = "npm";
} }
if (deploymentType === 'npm') { if (deploymentType === "npm") {
if (isStatic) { if (isStatic) {
pkg = listPackage pkg = listPackage;
} else { } else {
try { try {
pkg = JSON.parse(await readFile(resolvePath(path, 'package.json'))) pkg = JSON.parse(await readFile(resolvePath(path, "package.json")));
} catch (err) { } catch (err) {
const e = Error(`Failed to read JSON in "${path}/package.json"`) const e = Error(`Failed to read JSON in "${path}/package.json"`);
e.userError = true e.userError = true;
throw e throw e;
} }
} }
if (strict && (!pkg.scripts || (!pkg.scripts.start && !pkg.scripts['now-start']))) { if (
const e = Error('Missing `start` (or `now-start`) script in `package.json`. ' + strict &&
'See: https://docs.npmjs.com/cli/start.') (!pkg.scripts || (!pkg.scripts.start && !pkg.scripts["now-start"]))
e.userError = true ) {
throw e const e = Error(
"Missing `start` (or `now-start`) script in `package.json`. " +
"See: https://docs.npmjs.com/cli/start."
);
e.userError = true;
throw e;
} }
if (!deploymentName) { if (!deploymentName) {
if (typeof pkg.name === 'string') { if (typeof pkg.name === "string") {
name = pkg.name name = pkg.name;
} else { } else {
name = basename(path) name = basename(path);
if (!quiet && !isStatic) { if (!quiet && !isStatic) {
console.log(`> No \`name\` in \`package.json\`, using ${chalk.bold(name)}`) console.log(
`> No \`name\` in \`package.json\`, using ${chalk.bold(name)}`
);
} }
} }
} }
description = pkg.description description = pkg.description;
} else if (deploymentType === 'docker') { } else if (deploymentType === "docker") {
let docker let docker;
try { try {
const dockerfile = await readFile(resolvePath(path, 'Dockerfile'), 'utf8') const dockerfile = await readFile(
docker = parseDockerfile(dockerfile, {includeComments: true}) resolvePath(path, "Dockerfile"),
"utf8"
);
docker = parseDockerfile(dockerfile, { includeComments: true });
} catch (err) { } catch (err) {
const e = Error(`Failed to parse "${path}/Dockerfile"`) const e = Error(`Failed to parse "${path}/Dockerfile"`);
e.userError = true e.userError = true;
throw e throw e;
} }
if (strict && docker.length <= 0) { if (strict && docker.length <= 0) {
const e = Error('No commands found in `Dockerfile`') const e = Error("No commands found in `Dockerfile`");
e.userError = true e.userError = true;
throw e throw e;
} }
const labels = {} const labels = {};
docker docker.filter(cmd => cmd.name === "LABEL").forEach(({ args }) => {
.filter(cmd => cmd.name === 'LABEL')
.forEach(({args}) => {
for (const key in args) { for (const key in args) {
if (!{}.hasOwnProperty.call(args, key)) { if (!{}.hasOwnProperty.call(args, key)) {
continue continue;
} }
// unescape and convert into string // unescape and convert into string
try { try {
labels[key] = JSON.parse(args[key]) labels[key] = JSON.parse(args[key]);
} catch (err) { } catch (err) {
const e = Error(`Error parsing value for LABEL ${key} in \`Dockerfile\``) const e = Error(
e.userError = true `Error parsing value for LABEL ${key} in \`Dockerfile\``
throw e );
e.userError = true;
throw e;
} }
} }
}) });
if (!deploymentName) { if (!deploymentName) {
if (labels.name) { if (labels.name) {
name = labels.name name = labels.name;
} else { } else {
name = basename(path) name = basename(path);
if (!quiet) { if (!quiet) {
if (hasNowJson) { if (hasNowJson) {
console.log(`> No \`name\` LABEL in \`Dockerfile\` or \`name\` field in \`now.json\`, using ${chalk.bold(name)}`) console.log(
`> No \`name\` LABEL in \`Dockerfile\` or \`name\` field in \`now.json\`, using ${chalk.bold(name)}`
);
} else { } else {
console.log(`> No \`name\` LABEL in \`Dockerfile\`, using ${chalk.bold(name)}`) console.log(
`> No \`name\` LABEL in \`Dockerfile\`, using ${chalk.bold(name)}`
);
} }
} }
} }
} }
description = labels.description description = labels.description;
} else { } else {
throw new TypeError(`Unsupported "deploymentType": ${deploymentType}`) throw new TypeError(`Unsupported "deploymentType": ${deploymentType}`);
} }
if (deploymentName) { if (deploymentName) {
name = deploymentName name = deploymentName;
} }
if (pkg.now) { if (pkg.now) {
@ -158,13 +178,15 @@ async function readMetaData(path, {
// file, then fail hard and let the user know that they need to pick one or the // file, then fail hard and let the user know that they need to pick one or the
// other // other
if (hasNowJson) { if (hasNowJson) {
const e = new Error('You have a `now` configuration field' + const e = new Error(
'inside `package.json`, but configuration is also present' + "You have a `now` configuration field" +
'in `now.json`! Please ensure there\'s a single source of configuration by removing one') "inside `package.json`, but configuration is also present" +
e.userError = true "in `now.json`! Please ensure there's a single source of configuration by removing one"
throw e );
e.userError = true;
throw e;
} else { } else {
nowConfig = pkg.now nowConfig = pkg.now;
} }
} }
@ -175,5 +197,5 @@ async function readMetaData(path, {
pkg, pkg,
nowConfig, nowConfig,
hasNowJson hasNowJson
} };
} }

80
lib/secrets.js

@ -1,115 +1,117 @@
// Ours // Ours
const Now = require('../lib') const Now = require("../lib");
module.exports = class Secrets extends Now { module.exports = class Secrets extends Now {
ls() { ls() {
return this.listSecrets() return this.listSecrets();
} }
rm(nameOrId) { rm(nameOrId) {
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`) console.time(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`);
} }
const res = await this._fetch(`/now/secrets/${nameOrId}`, {method: 'DELETE'}) const res = await this._fetch(`/now/secrets/${nameOrId}`, {
method: "DELETE"
});
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`) console.timeEnd(`> [debug] #${attempt} DELETE /secrets/${nameOrId}`);
} }
if (res.status === 403) { if (res.status === 403) {
return bail(new Error('Unauthorized')) return bail(new Error("Unauthorized"));
} }
const body = await res.json() const body = await res.json();
if (res.status !== 200) { if (res.status !== 200) {
if (res.status === 404 || res.status === 400) { if (res.status === 404 || res.status === 400) {
const err = new Error(body.error.message) const err = new Error(body.error.message);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} }
throw new Error(body.error.message) throw new Error(body.error.message);
} }
return body return body;
}) });
} }
add(name, value) { add(name, value) {
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} POST /secrets`) console.time(`> [debug] #${attempt} POST /secrets`);
} }
const res = await this._fetch('/now/secrets', { const res = await this._fetch("/now/secrets", {
method: 'POST', method: "POST",
body: { body: {
name, name,
value: value.toString() value: value.toString()
} }
}) });
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} POST /secrets`) console.timeEnd(`> [debug] #${attempt} POST /secrets`);
} }
if (res.status === 403) { if (res.status === 403) {
return bail(new Error('Unauthorized')) return bail(new Error("Unauthorized"));
} }
const body = await res.json() const body = await res.json();
if (res.status !== 200) { if (res.status !== 200) {
if (res.status === 404 || res.status === 400) { if (res.status === 404 || res.status === 400) {
const err = new Error(body.error.message) const err = new Error(body.error.message);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} }
throw new Error(body.error.message) throw new Error(body.error.message);
} }
return body return body;
}) });
} }
rename(nameOrId, newName) { rename(nameOrId, newName) {
return this.retry(async (bail, attempt) => { return this.retry(async (bail, attempt) => {
if (this._debug) { if (this._debug) {
console.time(`> [debug] #${attempt} PATCH /secrets/${nameOrId}`) console.time(`> [debug] #${attempt} PATCH /secrets/${nameOrId}`);
} }
const res = await this._fetch(`/now/secrets/${nameOrId}`, { const res = await this._fetch(`/now/secrets/${nameOrId}`, {
method: 'PATCH', method: "PATCH",
body: { body: {
name: newName name: newName
} }
}) });
if (this._debug) { if (this._debug) {
console.timeEnd(`> [debug] #${attempt} PATCH /secrets/${nameOrId}`) console.timeEnd(`> [debug] #${attempt} PATCH /secrets/${nameOrId}`);
} }
if (res.status === 403) { if (res.status === 403) {
return bail(new Error('Unauthorized')) return bail(new Error("Unauthorized"));
} }
const body = await res.json() const body = await res.json();
if (res.status !== 200) { if (res.status !== 200) {
if (res.status === 404 || res.status === 400) { if (res.status === 404 || res.status === 400) {
const err = new Error(body.error.message) const err = new Error(body.error.message);
err.userError = true err.userError = true;
return bail(err) return bail(err);
} }
throw new Error(body.error.message) throw new Error(body.error.message);
} }
return body return body;
}) });
} }
} };

4
lib/strlen.js

@ -1,5 +1,5 @@
function strlen(str) { function strlen(str) {
return str.replace(/\x1b[^m]*m/g, '').length return str.replace(/\x1b[^m]*m/g, "").length;
} }
module.exports = strlen module.exports = strlen;

30
lib/test.js

@ -1,22 +1,22 @@
// Native // Native
const {resolve} = require('path') const { resolve } = require("path");
// Ours // Ours
const {npm: getFiles} = require('./get-files') const { npm: getFiles } = require("./get-files");
getFiles(resolve('../mng-test/files-in-package')) getFiles(resolve("../mng-test/files-in-package"))
.then(files => { .then(files => {
console.log(files) console.log(files);
getFiles(resolve('../mng-test/files-in-package-ignore')) getFiles(resolve("../mng-test/files-in-package-ignore"))
.then(files2 => { .then(files2 => {
console.log('ignored: ') console.log("ignored: ");
console.log(files2) console.log(files2);
})
.catch(err => {
console.log(err.stack);
});
}) })
.catch(err => { .catch(err => {
console.log(err.stack) console.log(err.stack);
}) });
})
.catch(err => {
console.log(err.stack)
})

8
lib/to-host.js

@ -1,5 +1,5 @@
// Native // Native
const {parse} = require('url') const { parse } = require("url");
/** /**
* Converts a valid deployment lookup parameter to a hostname. * Converts a valid deployment lookup parameter to a hostname.
@ -9,12 +9,12 @@ const {parse} = require('url')
function toHost(url) { function toHost(url) {
if (/^https?:\/\//.test(url)) { if (/^https?:\/\//.test(url)) {
return parse(url).host return parse(url).host;
} }
// remove any path if present // remove any path if present
// `a.b.c/` => `a.b.c` // `a.b.c/` => `a.b.c`
return url.replace(/(\/\/)?([^/]+)(.*)/, '$2') return url.replace(/(\/\/)?([^/]+)(.*)/, "$2");
} }
module.exports = toHost module.exports = toHost;

6
lib/ua.js

@ -1,7 +1,7 @@
// Native // Native
const os = require('os') const os = require("os");
// Ours // Ours
const {version} = require('./pkg') const { version } = require("./pkg");
module.exports = `now ${version} node-${process.version} ${os.platform()} (${os.arch()})` module.exports = `now ${version} node-${process.version} ${os.platform()} (${os.arch()})`;

48
lib/utils/billing/geocode.js

@ -1,27 +1,33 @@
// Packages // Packages
const gMaps = require('@google/maps') const gMaps = require("@google/maps");
const MAPS_API_KEY = 'AIzaSyALfKTQ6AiIoJ8WGDXR3E7IBOwlHoTPfYY' const MAPS_API_KEY = "AIzaSyALfKTQ6AiIoJ8WGDXR3E7IBOwlHoTPfYY";
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
module.exports = function ({country, zipCode: postal_code}) { module.exports = function({ country, zipCode: postal_code }) {
return new Promise(resolve => { return new Promise(resolve => {
const maps = gMaps.createClient({key: MAPS_API_KEY}) const maps = gMaps.createClient({ key: MAPS_API_KEY });
maps.geocode({ maps.geocode(
address: `${postal_code} ${country}` // eslint-disable-line camelcase {
}, (err, res) => { address: `${postal_code} ${country}` // eslint-disable-line camelcase
if (err || res.json.results.length === 0) { },
resolve() (err, res) => {
} if (err || res.json.results.length === 0) {
resolve();
}
const data = res.json.results[0] const data = res.json.results[0];
const components = {} const components = {};
data.address_components.forEach(c => { data.address_components.forEach(c => {
components[c.types[0]] = c components[c.types[0]] = c;
}) });
const state = components.administrative_area_level_1 const state = components.administrative_area_level_1;
const city = components.locality const city = components.locality;
resolve({state: state && state.long_name, city: city && city.long_name}) resolve({
}) state: state && state.long_name,
}) city: city && city.long_name
} });
}
);
});
};

44
lib/utils/check-path.js

@ -1,49 +1,49 @@
// Native // Native
const os = require('os') const os = require("os");
const path = require('path') const path = require("path");
const checkPath = async dir => { const checkPath = async dir => {
if (!dir) { if (!dir) {
return return;
} }
const home = os.homedir() const home = os.homedir();
let location let location;
const paths = { const paths = {
home, home,
desktop: path.join(home, 'Desktop'), desktop: path.join(home, "Desktop"),
downloads: path.join(home, 'Downloads') downloads: path.join(home, "Downloads")
} };
for (const locationPath in paths) { for (const locationPath in paths) {
if (!{}.hasOwnProperty.call(paths, locationPath)) { if (!{}.hasOwnProperty.call(paths, locationPath)) {
continue continue;
} }
if (dir === paths[locationPath]) { if (dir === paths[locationPath]) {
location = locationPath location = locationPath;
} }
} }
if (!location) { if (!location) {
return return;
} }
let locationName let locationName;
switch (location) { switch (location) {
case 'home': case "home":
locationName = 'user directory' locationName = "user directory";
break break;
case 'downloads': case "downloads":
locationName = 'downloads directory' locationName = "downloads directory";
break break;
default: default:
locationName = location locationName = location;
} }
throw new Error(`You're trying to deploy your ${locationName}.`) throw new Error(`You're trying to deploy your ${locationName}.`);
} };
module.exports = checkPath module.exports = checkPath;

4
lib/utils/exit.js

@ -4,5 +4,5 @@ module.exports = code => {
// stdout writes are asynchronous // stdout writes are asynchronous
// https://github.com/nodejs/node/issues/6456 // https://github.com/nodejs/node/issues/6456
/* eslint-disable unicorn/no-process-exit */ /* eslint-disable unicorn/no-process-exit */
setTimeout(() => process.exit(code || 0), 100) setTimeout(() => process.exit(code || 0), 100);
} };

106
lib/utils/input/list.js

@ -1,86 +1,92 @@
const chalk = require('chalk') const chalk = require("chalk");
const inquirer = require('inquirer') const inquirer = require("inquirer");
const stripAnsi = require('strip-ansi') const stripAnsi = require("strip-ansi");
/* eslint-disable no-multiple-empty-lines, no-var, no-undef, no-eq-null, eqeqeq, semi */ /* eslint-disable no-multiple-empty-lines, no-var, no-undef, no-eq-null, eqeqeq, semi */
inquirer.prompt.prompts.list.prototype.getQuestion = function () { inquirer.prompt.prompts.list.prototype.getQuestion = function() {
var message = chalk.bold('> ' + this.opt.message) + ' ' var message = chalk.bold("> " + this.opt.message) + " ";
// Append the default if available, and if question isn't answered // Append the default if available, and if question isn't answered
if (this.opt.default != null && this.status !== 'answered') { if (this.opt.default != null && this.status !== "answered") {
message += chalk.dim('(' + this.opt.default + ') ') message += chalk.dim("(" + this.opt.default + ") ");
} }
return message return message;
}; };
/* eslint-enable */ /* eslint-enable */
function getLength(string) { function getLength(string) {
let biggestLength = 0 let biggestLength = 0;
string.split('\n').map(str => { string.split("\n").map(str => {
str = stripAnsi(str) str = stripAnsi(str);
if (str.length > biggestLength) { if (str.length > biggestLength) {
biggestLength = str.length biggestLength = str.length;
} }
return undefined return undefined;
}) });
return biggestLength return biggestLength;
} }
module.exports = async function ({ module.exports = async function(
message = 'the question', {
choices = [{ // eslint-disable-line no-unused-vars message = "the question",
name: 'something\ndescription\ndetails\netc', // eslint-disable-line no-unused-vars
value: 'something unique', choices = [
short: 'generally the first line of `name`' {
}], name: "something\ndescription\ndetails\netc",
pageSize = 15, // Show 15 lines without scrolling (~4 credit cards) value: "something unique",
separator = true, // puts a blank separator between each choice short: "generally the first line of `name`"
abort = 'end' // wether the `abort` option will be at the `start` or the `end` }
}) { ],
let biggestLength = 0 pageSize = 15, // Show 15 lines without scrolling (~4 credit cards)
separator = true, // puts a blank separator between each choice
abort = "end" // wether the `abort` option will be at the `start` or the `end`
}
) {
let biggestLength = 0;
choices = choices.map(choice => { choices = choices.map(choice => {
if (choice.name) { if (choice.name) {
const length = getLength(choice.name) const length = getLength(choice.name);
if (length > biggestLength) { if (length > biggestLength) {
biggestLength = length biggestLength = length;
} }
return choice return choice;
} }
throw new Error('Invalid choice') throw new Error("Invalid choice");
}) });
if (separator === true) { if (separator === true) {
choices = choices.reduce((prev, curr) => ( choices = choices.reduce(
prev.concat(new inquirer.Separator(' '), curr) (prev, curr) => prev.concat(new inquirer.Separator(" "), curr),
), []) []
);
} }
const abortSeparator = new inquirer.Separator('─'.repeat(biggestLength)) const abortSeparator = new inquirer.Separator("─".repeat(biggestLength));
const _abort = { const _abort = {
name: 'Abort', name: "Abort",
value: undefined value: undefined
} };
if (abort === 'start') { if (abort === "start") {
const blankSep = choices.shift() const blankSep = choices.shift();
choices.unshift(abortSeparator) choices.unshift(abortSeparator);
choices.unshift(_abort) choices.unshift(_abort);
choices.unshift(blankSep) choices.unshift(blankSep);
} else { } else {
choices.push(abortSeparator) choices.push(abortSeparator);
choices.push(_abort) choices.push(_abort);
} }
const nonce = Date.now() const nonce = Date.now();
const answer = await inquirer.prompt({ const answer = await inquirer.prompt({
name: nonce, name: nonce,
type: 'list', type: "list",
message, message,
choices, choices,
pageSize pageSize
}) });
return answer[nonce] return answer[nonce];
} };

75
lib/utils/input/prompt-bool.js

@ -1,56 +1,59 @@
const chalk = require('chalk') const chalk = require("chalk");
module.exports = (label, { module.exports = (
defaultValue = false, label,
abortSequences = new Set(['\x03']), {
resolveChars = new Set(['\r']), defaultValue = false,
yesChar = 'y', abortSequences = new Set(["\x03"]),
noChar = 'n', resolveChars = new Set(["\r"]),
stdin = process.stdin, yesChar = "y",
stdout = process.stdout, noChar = "n",
trailing = '\n' stdin = process.stdin,
} = {}) => { stdout = process.stdout,
trailing = "\n"
} = {}
) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const isRaw = process.stdin.isRaw const isRaw = process.stdin.isRaw;
stdin.setRawMode(true) stdin.setRawMode(true);
stdin.resume() stdin.resume();
function restore() { function restore() {
console.log(trailing) console.log(trailing);
stdin.setRawMode(isRaw) stdin.setRawMode(isRaw);
stdin.pause() stdin.pause();
stdin.removeListener('data', onData) stdin.removeListener("data", onData);
} }
function onData(buffer) { function onData(buffer) {
const data = buffer.toString() const data = buffer.toString();
if (abortSequences.has(data)) { if (abortSequences.has(data)) {
restore() restore();
return reject(new Error('USER_ABORT')) return reject(new Error("USER_ABORT"));
} }
if (resolveChars.has(data[0])) { if (resolveChars.has(data[0])) {
restore() restore();
resolve(defaultValue) resolve(defaultValue);
} else if (data[0].toLowerCase() === yesChar) { } else if (data[0].toLowerCase() === yesChar) {
restore() restore();
resolve(true) resolve(true);
} else if (data[0].toLowerCase() === noChar) { } else if (data[0].toLowerCase() === noChar) {
restore() restore();
resolve(false) resolve(false);
} else { } else {
// ignore extraneous input // ignore extraneous input
} }
} }
const defaultText = defaultValue === null ? const defaultText = defaultValue === null
`[${yesChar}|${noChar}]` : ? `[${yesChar}|${noChar}]`
defaultValue ? : defaultValue
`[${chalk.bold(yesChar.toUpperCase())}|${noChar}]` : ? `[${chalk.bold(yesChar.toUpperCase())}|${noChar}]`
`[${yesChar}|${chalk.bold(noChar.toUpperCase())}]` : `[${yesChar}|${chalk.bold(noChar.toUpperCase())}]`;
stdout.write(`${chalk.gray('-')} ${label} ${chalk.gray(defaultText)} `) stdout.write(`${chalk.gray("-")} ${label} ${chalk.gray(defaultText)} `);
stdin.on('data', onData) stdin.on("data", onData);
}) });
} };

294
lib/utils/input/text.js

@ -1,207 +1,223 @@
const ansiEscapes = require('ansi-escapes') const ansiEscapes = require("ansi-escapes");
const ansiRegex = require('ansi-regex') const ansiRegex = require("ansi-regex");
const chalk = require('chalk') const chalk = require("chalk");
const stripAnsi = require('strip-ansi') const stripAnsi = require("strip-ansi");
const ESCAPES = { const ESCAPES = {
LEFT: '\x1b[D', LEFT: "\x1b[D",
RIGHT: '\x1b[C', RIGHT: "\x1b[C",
CTRL_C: '\x03', CTRL_C: "\x03",
BACKSPACE: '\x08', BACKSPACE: "\x08",
CTRL_H: '\x7f', CTRL_H: "\x7f",
CARRIAGE: '\r' CARRIAGE: "\r"
} };
module.exports = function ({ module.exports = function(
label = '', {
initialValue = '', label = "",
// can be: initialValue = "",
// - `false`, which does nothing // can be:
// - `cc`, for credit cards // - `false`, which does nothing
// - `date`, for dates in the mm / yyyy format // - `cc`, for credit cards
mask = false, // - `date`, for dates in the mm / yyyy format
placeholder = '', mask = false,
abortSequences = new Set(['\x03']), placeholder = "",
eraseSequences = new Set([ESCAPES.BACKSPACE, ESCAPES.CTRL_H]), abortSequences = new Set(["\x03"]),
resolveChars = new Set([ESCAPES.CARRIAGE]), eraseSequences = new Set([ESCAPES.BACKSPACE, ESCAPES.CTRL_H]),
stdin = process.stdin, resolveChars = new Set([ESCAPES.CARRIAGE]),
stdout = process.stdout, stdin = process.stdin,
// char to print before resolving/rejecting the promise stdout = process.stdout,
// if `false`, nothing will be printed // char to print before resolving/rejecting the promise
trailing = ansiEscapes.eraseLines(1), // if `false`, nothing will be printed
// gets called on each keypress; trailing = ansiEscapes.eraseLines(1),
// `data` contains the current keypress; // gets called on each keypress;
// `futureValue` contains the current value + the // `data` contains the current keypress;
// keypress in the correct place // `futureValue` contains the current value + the
validateKeypress = (data, futureValue) => true, // eslint-disable-line no-unused-vars // keypress in the correct place
// get's called before the promise is resolved validateKeypress = (data, futureValue) => true, // eslint-disable-line no-unused-vars
// returning `false` here will prevent the user from submiting the value // get's called before the promise is resolved
validateValue = data => true, // eslint-disable-line no-unused-vars // returning `false` here will prevent the user from submiting the value
// receives the value of the input and should return a string validateValue = data => true, // eslint-disable-line no-unused-vars
// or false if no autocomplion is available // receives the value of the input and should return a string
autoComplete = value => false, // eslint-disable-line no-unused-vars // or false if no autocomplion is available
autoCompleteChars = new Set([ autoComplete = value => false, // eslint-disable-line no-unused-vars
'\t', // tab // tab
'\x1b[C' // right arrow // right arrow
]) autoCompleteChars = new Set(["\t", "\x1b[C"])
} = {}) { } = {}
) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const isRaw = process.stdin.isRaw const isRaw = process.stdin.isRaw;
let value let value;
let caretOffset = 0 let caretOffset = 0;
let regex let regex;
let suggestion = '' let suggestion = "";
stdout.write(label) stdout.write(label);
value = initialValue value = initialValue;
stdout.write(initialValue) stdout.write(initialValue);
if (mask) { if (mask) {
if (!value) { if (!value) {
value = chalk.gray(placeholder) value = chalk.gray(placeholder);
caretOffset = 0 - stripAnsi(value).length caretOffset = 0 - stripAnsi(value).length;
stdout.write(value) stdout.write(value);
stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset))) stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset)));
} }
regex = placeholder.split('').reduce((prev, curr) => { regex = placeholder
if (curr !== ' ' && !prev.includes(curr)) { .split("")
if (curr === '/') { .reduce(
prev.push(' / ') (prev, curr) => {
} else { if (curr !== " " && !prev.includes(curr)) {
prev.push(curr) if (curr === "/") {
} prev.push(" / ");
} } else {
return prev prev.push(curr);
}, []).join('|') }
regex = new RegExp(`(${regex})`, 'g') }
return prev;
},
[]
)
.join("|");
regex = new RegExp(`(${regex})`, "g");
} }
stdin.setRawMode(true) stdin.setRawMode(true);
stdin.resume() stdin.resume();
function restore() { function restore() {
stdin.setRawMode(isRaw) stdin.setRawMode(isRaw);
stdin.pause() stdin.pause();
stdin.removeListener('data', onData) stdin.removeListener("data", onData);
if (trailing) { if (trailing) {
stdout.write(trailing) stdout.write(trailing);
} }
} }
async function onData(buffer) { async function onData(buffer) {
const data = buffer.toString() const data = buffer.toString();
value = stripAnsi(value) value = stripAnsi(value);
if (abortSequences.has(data)) { if (abortSequences.has(data)) {
restore() restore();
return reject(new Error('USER_ABORT')) return reject(new Error("USER_ABORT"));
} }
if (suggestion !== '' && !caretOffset && autoCompleteChars.has(data)) { if (suggestion !== "" && !caretOffset && autoCompleteChars.has(data)) {
value += stripAnsi(suggestion) value += stripAnsi(suggestion);
suggestion = '' suggestion = "";
} else if (data === ESCAPES.LEFT) { } else if (data === ESCAPES.LEFT) {
if (value.length > Math.abs(caretOffset)) { if (value.length > Math.abs(caretOffset)) {
caretOffset-- caretOffset--;
} }
} else if (data === ESCAPES.RIGHT) { } else if (data === ESCAPES.RIGHT) {
if (caretOffset < 0) { if (caretOffset < 0) {
caretOffset++ caretOffset++;
} }
} else if (eraseSequences.has(data)) { } else if (eraseSequences.has(data)) {
let char let char;
if ((mask) && value.length > Math.abs(caretOffset)) { if (mask && value.length > Math.abs(caretOffset)) {
if (value[value.length + caretOffset - 1] === ' ') { if (value[value.length + caretOffset - 1] === " ") {
if (value[value.length + caretOffset - 2] === '/') { if (value[value.length + caretOffset - 2] === "/") {
caretOffset -= 1 caretOffset -= 1;
} }
char = placeholder[value.length + caretOffset] char = placeholder[value.length + caretOffset];
value = value.substr(0, value.length + caretOffset - 2) + char + value = value.substr(0, value.length + caretOffset - 2) +
value.substr(value.length + caretOffset - 1) char +
caretOffset-- value.substr(value.length + caretOffset - 1);
caretOffset--;
} else { } else {
char = placeholder[value.length + caretOffset - 1] char = placeholder[value.length + caretOffset - 1];
value = value.substr(0, value.length + caretOffset - 1) + char + value = value.substr(0, value.length + caretOffset - 1) +
value.substr(value.length + caretOffset) char +
value.substr(value.length + caretOffset);
} }
caretOffset-- caretOffset--;
} else { } else {
value = value.substr(0, value.length + caretOffset - 1) + value = value.substr(0, value.length + caretOffset - 1) +
value.substr(value.length + caretOffset) value.substr(value.length + caretOffset);
} }
suggestion = '' suggestion = "";
} else if (resolveChars.has(data)) { } else if (resolveChars.has(data)) {
if (validateValue(value)) { if (validateValue(value)) {
restore() restore();
resolve(value) resolve(value);
} else { } else {
if (mask === 'cc' || mask === 'ccv') { if (mask === "cc" || mask === "ccv") {
value = value.replace(/\s/g, '').replace(/(.{4})/g, '$1 ').trim() value = value.replace(/\s/g, "").replace(/(.{4})/g, "$1 ").trim();
value = value.replace(regex, chalk.gray('$1')) value = value.replace(regex, chalk.gray("$1"));
} else if (mask === 'expDate') { } else if (mask === "expDate") {
value = value.replace(regex, chalk.gray('$1')) value = value.replace(regex, chalk.gray("$1"));
} }
const l = chalk.red(label.replace('-', '✖')) const l = chalk.red(label.replace("-", "✖"));
stdout.write(ansiEscapes.eraseLines(1) + l + value + ansiEscapes.beep) stdout.write(
ansiEscapes.eraseLines(1) + l + value + ansiEscapes.beep
);
if (caretOffset) { if (caretOffset) {
process.stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset))) process.stdout.write(
ansiEscapes.cursorBackward(Math.abs(caretOffset))
);
} }
} }
return return;
} else if (!ansiRegex().test(data)) { } else if (!ansiRegex().test(data)) {
let tmp = value.substr(0, value.length + caretOffset) + data + let tmp = value.substr(0, value.length + caretOffset) +
value.substr(value.length + caretOffset) data +
value.substr(value.length + caretOffset);
if (mask) { if (mask) {
if (/\d/.test(data) && caretOffset !== 0) { if (/\d/.test(data) && caretOffset !== 0) {
if (value[value.length + caretOffset + 1] === ' ') { if (value[value.length + caretOffset + 1] === " ") {
tmp = value.substr(0, value.length + caretOffset) + data + tmp = value.substr(0, value.length + caretOffset) +
value.substr(value.length + caretOffset + 1) data +
caretOffset += 2 value.substr(value.length + caretOffset + 1);
if (value[value.length + caretOffset] === '/') { caretOffset += 2;
caretOffset += 2 if (value[value.length + caretOffset] === "/") {
caretOffset += 2;
} }
} else { } else {
tmp = value.substr(0, value.length + caretOffset) + data + tmp = value.substr(0, value.length + caretOffset) +
value.substr(value.length + caretOffset + 1) data +
caretOffset++ value.substr(value.length + caretOffset + 1);
caretOffset++;
} }
} else if (/\s/.test(data) && caretOffset < 0) { } else if (/\s/.test(data) && caretOffset < 0) {
caretOffset++ caretOffset++;
tmp = value tmp = value;
} else { } else {
return stdout.write(ansiEscapes.beep) return stdout.write(ansiEscapes.beep);
} }
value = tmp value = tmp;
} else if (validateKeypress(data, value)) { } else if (validateKeypress(data, value)) {
value = tmp value = tmp;
if (caretOffset === 0) { if (caretOffset === 0) {
const completion = await autoComplete(value) const completion = await autoComplete(value);
if (completion) { if (completion) {
suggestion = chalk.gray(completion) suggestion = chalk.gray(completion);
suggestion += ansiEscapes.cursorBackward(completion.length) suggestion += ansiEscapes.cursorBackward(completion.length);
} else { } else {
suggestion = '' suggestion = "";
} }
} }
} else { } else {
return stdout.write(ansiEscapes.beep) return stdout.write(ansiEscapes.beep);
} }
} }
if (mask === 'cc' || mask === 'ccv') { if (mask === "cc" || mask === "ccv") {
value = value.replace(/\s/g, '').replace(/(.{4})/g, '$1 ').trim() value = value.replace(/\s/g, "").replace(/(.{4})/g, "$1 ").trim();
value = value.replace(regex, chalk.gray('$1')) value = value.replace(regex, chalk.gray("$1"));
} else if (mask === 'expDate') { } else if (mask === "expDate") {
value = value.replace(regex, chalk.gray('$1')) value = value.replace(regex, chalk.gray("$1"));
} }
stdout.write(ansiEscapes.eraseLines(1) + label + value + suggestion) stdout.write(ansiEscapes.eraseLines(1) + label + value + suggestion);
if (caretOffset) { if (caretOffset) {
process.stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset))) process.stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset)));
} }
} }
stdin.on('data', onData) stdin.on("data", onData);
}) });
} };

7
lib/utils/output/cmd.js

@ -1,8 +1,7 @@
const chalk = require('chalk') const chalk = require("chalk");
// the equivalent of <code>, for embedding a cmd // the equivalent of <code>, for embedding a cmd
// eg: Please run ${cmd(woot)} // eg: Please run ${cmd(woot)}
module.exports = cmd => ( module.exports = cmd =>
`${chalk.gray('`')}${chalk.cyan(cmd)}${chalk.gray('`')}` `${chalk.gray("`")}${chalk.cyan(cmd)}${chalk.gray("`")}`;
)

7
lib/utils/output/code.js

@ -1,8 +1,7 @@
const chalk = require('chalk') const chalk = require("chalk");
// the equivalent of <code>, for embedding anything // the equivalent of <code>, for embedding anything
// you may want to take a look at ./cmd.js // you may want to take a look at ./cmd.js
module.exports = cmd => ( module.exports = cmd =>
`${chalk.gray('`')}${chalk.bold(cmd)}${chalk.gray('`')}` `${chalk.gray("`")}${chalk.bold(cmd)}${chalk.gray("`")}`;
)

7
lib/utils/output/error.js

@ -1,7 +1,6 @@
const chalk = require('chalk') const chalk = require("chalk");
// prints an error message // prints an error message
module.exports = msg => { module.exports = msg => {
console.error(`${chalk.red('> Error!')} ${msg}`) console.error(`${chalk.red("> Error!")} ${msg}`);
} };

7
lib/utils/output/info.js

@ -1,7 +1,6 @@
const chalk = require('chalk') const chalk = require("chalk");
// prints an informational message // prints an informational message
module.exports = msg => { module.exports = msg => {
console.log(`${chalk.gray('>')} ${msg}`) console.log(`${chalk.gray(">")} ${msg}`);
} };

2
lib/utils/output/logo.js

@ -1 +1 @@
module.exports = process.platform === 'win32' ? 'Δ' : '𝚫' module.exports = process.platform === "win32" ? "Δ" : "𝚫";

7
lib/utils/output/param.js

@ -1,8 +1,7 @@
const chalk = require('chalk') const chalk = require("chalk");
// returns a user param in a nice formatting // returns a user param in a nice formatting
// e.g.: google.com -> "google.com" (in bold) // e.g.: google.com -> "google.com" (in bold)
module.exports = param => ( module.exports = param =>
chalk.bold(`${chalk.gray('"')}${chalk.bold(param)}${chalk.gray('"')}`) chalk.bold(`${chalk.gray('"')}${chalk.bold(param)}${chalk.gray('"')}`);
)

12
lib/utils/output/stamp.js

@ -1,12 +1,10 @@
const ms = require('ms') const ms = require("ms");
const chalk = require('chalk') const chalk = require("chalk");
// returns a time delta with the right color // returns a time delta with the right color
// example: `[103ms]` // example: `[103ms]`
module.exports = () => { module.exports = () => {
const start = new Date() const start = new Date();
return () => ( return () => chalk.gray(`[${ms(new Date() - start)}]`);
chalk.gray(`[${ms(new Date() - start)}]`) };
)
}

6
lib/utils/output/success.js

@ -1,6 +1,6 @@
const chalk = require('chalk') const chalk = require("chalk");
// prints a success message // prints a success message
module.exports = msg => { module.exports = msg => {
console.log(`${chalk.cyan('> Success!')} ${msg}`) console.log(`${chalk.cyan("> Success!")} ${msg}`);
} };

6
lib/utils/output/uid.js

@ -1,7 +1,5 @@
const chalk = require('chalk') const chalk = require("chalk");
// used for including uids in the output // used for including uids in the output
// example: `(dom_ji13dj2fih4fi2hf)` // example: `(dom_ji13dj2fih4fi2hf)`
module.exports = id => ( module.exports = id => chalk.gray(`(${id})`);
chalk.gray(`(${id})`)
)

20
lib/utils/output/wait.js

@ -1,15 +1,15 @@
const ora = require('ora') const ora = require("ora");
const chalk = require('chalk') const chalk = require("chalk");
const {eraseLine} = require('ansi-escapes') const { eraseLine } = require("ansi-escapes");
// prints a spinner followed by the given text // prints a spinner followed by the given text
module.exports = msg => { module.exports = msg => {
const spinner = ora(chalk.gray(msg)) const spinner = ora(chalk.gray(msg));
spinner.color = 'gray' spinner.color = "gray";
spinner.start() spinner.start();
return () => { return () => {
spinner.stop() spinner.stop();
process.stdout.write(eraseLine) process.stdout.write(eraseLine);
} };
} };

42
lib/utils/prompt-options.js

@ -1,35 +1,35 @@
// Packages // Packages
const chalk = require('chalk') const chalk = require("chalk");
module.exports = function (opts) { module.exports = function(opts) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
opts.forEach(([, text], i) => { opts.forEach(([, text], i) => {
console.log(`${chalk.gray('>')} [${chalk.bold(i + 1)}] ${text}`) console.log(`${chalk.gray(">")} [${chalk.bold(i + 1)}] ${text}`);
}) });
const ondata = v => { const ondata = v => {
const s = v.toString() const s = v.toString();
const cleanup = () => { const cleanup = () => {
process.stdin.setRawMode(false) process.stdin.setRawMode(false);
process.stdin.removeListener('data', ondata) process.stdin.removeListener("data", ondata);
} };
if (s === '\u0003') { if (s === "\u0003") {
cleanup() cleanup();
reject(new Error('Aborted')) reject(new Error("Aborted"));
return return;
} }
const n = Number(s) const n = Number(s);
if (opts[n - 1]) { if (opts[n - 1]) {
cleanup() cleanup();
resolve(opts[n - 1][0]) resolve(opts[n - 1][0]);
} }
} };
process.stdin.setRawMode(true) process.stdin.setRawMode(true);
process.stdin.resume() process.stdin.resume();
process.stdin.on('data', ondata) process.stdin.on("data", ondata);
}) });
} };

10
lib/utils/to-human-path.js

@ -1,9 +1,9 @@
// Native // Native
const {resolve} = require('path') const { resolve } = require("path");
const {homedir} = require('os') const { homedir } = require("os");
// cleaned-up `$HOME` (i.e.: no trailing slash) // cleaned-up `$HOME` (i.e.: no trailing slash)
const HOME = resolve(homedir()) const HOME = resolve(homedir());
/** /**
* Attempts to show the given path in * Attempts to show the given path in
@ -12,7 +12,7 @@ const HOME = resolve(homedir())
*/ */
function toHumanPath(path) { function toHumanPath(path) {
return path.replace(HOME, '~') return path.replace(HOME, "~");
} }
module.exports = toHumanPath module.exports = toHumanPath;

Loading…
Cancel
Save