Browse Source

Add `logs`, `teams`, `switch`, `scale`, new `ls` and much more (#468)

* Feature/teams (#25)

* Add the skeleton of `now teams`

* Add support for "complex" command aliases

This adds the ability to set, for example, `now switch` as an alias for `now teams switch`.
Subsequent commands and arguments are taken into account: `now switch zeit --debug` will be parsed to `now teams switch zeit --debug`.

* `now switch` => `now teams switch`

* Extract `rightPad`

* Extract `eraseLines`

* Extract `✓`

* Text input: add `valid` option

* Text input: add `forceLowerCase` option

* Add preliminary `now teams add`

* Make the linter happy

* Extract `> NOTE: ...`

* Add missing labels

* Fix typos

* Add missing parameters

* Change the section label after inviting all specified team mates

* Call the API after each email submission

* Show the elapsed time after each `inviteUser` api call

* Handle user aborts

* We don't need `args` for `now teams add`

* Add missing `await`

* Extract regex

* `process.exit()` => `exit(1)`

* `prompt-bool` is an `input` util, not an `output` one

* Add the ability to delete a key from the config file

* Add `fatal-error`

* Add `now teams invite`

* This shouldn't be black

* Save the username in `~/.now.json` upon login

* Save the token and userId instead of token and email

* Fix typo

* Save more info about the user to `~/.now.json` upon login

* `~/.now.json`: Persist the current time when login in

* Add `user` helper

* `user.userId` => `user.id`

* Tweak code organization

* Add caching system to `.now.json`

* Automatically switch to a team after its creation

* Introduce the concept of `inactive` teams

* Use bold for `payment method`

* Remove duplicated code

* Add line breaks

* Auto complete with the first match

* Remove placeholder stuff

* Add the user's email to the list of suggestions

* FIx bad merge

* Add `now switch`

* Make `now teams invite` more reliable and faster

* Shut up XO

* Improve autocompletion

* Fix TypeError

* Make stuff pretty

* Not sure how this got overwritten

* Feature/domains (#26)

* Add the skeleton of `now teams`

* Add support for "complex" command aliases

This adds the ability to set, for example, `now switch` as an alias for `now teams switch`.
Subsequent commands and arguments are taken into account: `now switch zeit --debug` will be parsed to `now teams switch zeit --debug`.

* `now switch` => `now teams switch`

* Extract `rightPad`

* Extract `eraseLines`

* Extract `✓`

* Text input: add `valid` option

* Text input: add `forceLowerCase` option

* Add preliminary `now teams add`

* Make the linter happy

* Extract `> NOTE: ...`

* Add missing labels

* Fix typos

* Add missing parameters

* Change the section label after inviting all specified team mates

* Call the API after each email submission

* Show the elapsed time after each `inviteUser` api call

* Handle user aborts

* We don't need `args` for `now teams add`

* Add missing `await`

* Extract regex

* `process.exit()` => `exit(1)`

* `prompt-bool` is an `input` util, not an `output` one

* Add the ability to delete a key from the config file

* Add `fatal-error`

* Add `now teams invite`

* This shouldn't be black

* Save the username in `~/.now.json` upon login

* Save the token and userId instead of token and email

* Fix typo

* Save more info about the user to `~/.now.json` upon login

* `~/.now.json`: Persist the current time when login in

* Add `user` helper

* `user.userId` => `user.id`

* Tweak code organization

* Add caching system to `.now.json`

* Automatically switch to a team after its creation

* Introduce the concept of `inactive` teams

* Use bold for `payment method`

* Remove duplicated code

* Add line breaks

* Auto complete with the first match

* Remove placeholder stuff

* Add the user's email to the list of suggestions

* FIx bad merge

* Add `now switch`

* Make `now teams invite` more reliable and faster

* Shut up XO

* Improve autocompletion

* Fix TypeError

* Make stuff pretty

* Not sure how this got overwritten

* `prompt-bool` is an `input` util, not an `output` one

* Make stuff pretty

* Not sure how this got overwritten

* Add domains.status, price and buy

* Add `now domains buy`

* Add the ability to buy a domain when running `now alias`

* Logs (#27)

* Added `logs` sub command

* add missing dependencies

* use utils/output/logo

* logs: fix wrong reference

* logs: fix buffer time

* sort build logs (#19)

* logs: use lib/logs

* lib/logs: fix

* logs: resolve url to id

* logs: default to follow

* logs: don't resolve URL to id

* logs: revert to default unfollow

* logs: add since option and until option

* logs: fix default number of logs

* fix logs auth

* logs: listen ready event

* logs: new endpoint

* log: remove v query param

* logs: default to not include access logs

* fix styles of now-logs

* logs: remove relative time

* Fix bad merge conflict

* Add `now scale` (#28)

* Inital drafts fro `now-scale`

* More final draft

* sketch new `now ls` format

* Add sketch for `now ls --all`

* Placeholder for `now scale ls`

* "Prettify" and improve scale command

Signed-off-by: Jarmo Isotalo <jamo@isotalo.fi>

* Adopt to now-list api changes

* Improve now-list --all colors

Signed-off-by: Jarmo Isotalo <jamo@isotalo.fi>

* Add now scale ls

Signed-off-by: Jarmo Isotalo <jamo@isotalo.fi>

* Prettify

* Show auto correctly

* Add partial match scale for now alias

* Make alias to match scale before uptading alias and presumably a bunch of unrelated style changes

* Replace spinners with help text

* Make the list :nice:

* Make now-list great again

* Final touches

* Allow --all only when app is defined

* Add progress tracking to scale

* Correctly use --all for > 0 deployments found [1s] and improve scale info ux

* Show --all info if we are hiding stuff

* Fixes

* Refactor scale info and unfreeze

* Fixes

* Proper progress bar

* Fix bad merge

* Fix auth for now-scale

* logs: fix reading config

* Fix reference

* Small ux tweaks

* Improve now alias ux

* Fix a ton of lint errors

* Fix scaling up and alias ux

* Fix lint errors + prettify

* Make `bin/now-scale.js` available via `now scale`

* Fix errornous syntax for domains list

* And use correct header for domains list

* Update now-scale help to match new spec

* `await` for `cfg.read()` on `cfg.remove()`

* Update scale command

* Cleanu
p

* Fetch the teams from the api on teams.ls()

Plus prettier shit

* Run prettier hooks manually

* Make `now switch` perfect

* Rm unused variables

* Lint

* Ruin ux but lint

* Consume `POST /teams`

* Consume `PATCH /teams/:id`

* Fix/teams support (#29)

* Add teams support for lib/index.js

* Consume `POST /teams/:id/members`

* Make `now teams create` and `now teams invite` perfect

* Add a way to not send `?teamId` if necessary

* Add `?teamId=${currentTeam.id}` support to all comamnds and subcommands

* Display the username only if it's available

* Consume the preoduction endpoits for domain purchase

* Fix typo

* Fix grammar

* Fix grammar

* Remove useless require

* Display the user name/team slug on creation/list commands

* Remove use of old, now undefined variable

* Show domains in bold on `now domains ls`

* `user.userId` => `user.uid`

* Remove console.log

* Show a better messsage upon unexpected `domains.buy()` error

* typo

* Consume new `/plan` API and fix plan check

* Update `now upgrade` – consume new APIs and expose new plans

* Fix `now ugprade` info message

* `now cc`: consume new APIs and fix error messages

* Add team/user context to `now alias` when buying a domain

* Fix wording on `now domains buy`

* Add stamp to domain purchase

* Improve scale ux

* Remove ToS prompt upon login

* Fix `prompt-bool` trailing issues

* Remove useless `require`

* Allow `now switch <username>`

* Make `now help` better

* This shouldn't be here

* Make `now switch` incredible

* Remove old stuff from ~/.now.json

* Add comments

* `now team` => `now teams`

* 5.0.0

* Fix linter

* Fix DNS

* Parse subdomain

* FIx lint

* drop IDs for certs

* Make now ls look nice also when noTTY

* Make now list look nice when colors are not supported

* Mane certs ls look nice when we have no colers

* Now ls --all can also take uniq url as a parameter

* Improve now ls --all

* Now ls -all takes alias as an argument
master
Matheus Fernandes 8 years ago
committed by GitHub
parent
commit
e59e7616c7
  1. 64
      bin/domains/buy.js
  2. 70
      bin/now-alias.js
  3. 49
      bin/now-billing-add.js
  4. 101
      bin/now-billing.js
  5. 117
      bin/now-certs.js
  6. 131
      bin/now-deploy.js
  7. 49
      bin/now-dns.js
  8. 57
      bin/now-domains.js
  9. 165
      bin/now-list.js
  10. 270
      bin/now-logs.js
  11. 47
      bin/now-open.js
  12. 78
      bin/now-remove.js
  13. 309
      bin/now-scale.js
  14. 47
      bin/now-secrets.js
  15. 142
      bin/now-teams.js
  16. 147
      bin/now-upgrade.js
  17. 20
      bin/now.js
  18. 127
      bin/teams/add.js
  19. 160
      bin/teams/invite.js
  20. 126
      bin/teams/switch.js
  21. 202
      lib/alias.js
  22. 73
      lib/cfg.js
  23. 18
      lib/credit-cards.js
  24. 92
      lib/domains.js
  25. 242
      lib/index.js
  26. 39
      lib/login.js
  27. 2
      lib/logs.js
  28. 44
      lib/plans.js
  29. 6
      lib/re-alias.js
  30. 72
      lib/scale-info.js
  31. 43
      lib/scale.js
  32. 141
      lib/teams.js
  33. 0
      lib/unfreeze.js
  34. 54
      lib/user.js
  35. 25
      lib/utils/domains/treat-buy-error.js
  36. 7
      lib/utils/fatal-error.js
  37. 3
      lib/utils/input/regexes.js
  38. 50
      lib/utils/input/text.js
  39. 3
      lib/utils/output/chars.js
  40. 3
      lib/utils/output/erase-lines.js
  41. 6
      lib/utils/output/note.js
  42. 4
      lib/utils/output/right-pad.js
  43. 23
      lib/utils/url.js
  44. 12
      package.json

64
bin/domains/buy.js

@ -0,0 +1,64 @@
const { italic, bold } = require('chalk');
const error = require('../../lib/utils/output/error');
const wait = require('../../lib/utils/output/wait');
const cmd = require('../../lib/utils/output/cmd');
const param = require('../../lib/utils/output/param');
const info = require('../../lib/utils/output/info');
const uid = require('../../lib/utils/output/uid');
const success = require('../../lib/utils/output/success');
const stamp = require('../../lib/utils/output/stamp');
const promptBool = require('../../lib/utils/input/prompt-bool');
const eraseLines = require('../../lib/utils/output/erase-lines');
const treatBuyError = require('../../lib/utils/domains/treat-buy-error');
module.exports = async function({domains, args, currentTeam, user}) {
const name = args[0];
let elapsed
if (!name) {
return error(`Missing domain name. Run ${cmd('now domains help')}`);
}
const nameParam = param(name);
elapsed = stamp()
let stopSpinner = wait(`Checking availability for ${nameParam}`);
const price = await domains.price(name);
const available = await domains.status(name);
stopSpinner();
if (! available) {
return error(`The domain ${nameParam} is ${italic('unavailable')}! ${elapsed()}`);
}
info(`The domain ${nameParam} is ${italic('available')}! ${elapsed()}`);
const confirmation = await promptBool(`Buy now for ${bold(`$${price}`)} (${
bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
})?`);
eraseLines(1);
if (!confirmation) {
return info('Aborted');
}
stopSpinner = wait('Purchasing');
elapsed = stamp()
let domain;
try {
domain = await domains.buy(name);
} catch (err) {
stopSpinner();
return treatBuyError(err);
}
stopSpinner();
success(`Domain purchased and created ${uid(domain.uid)} ${elapsed()}`);
info(
`You may now use your domain as an alias to your deployments. Run ${cmd('now alias help')}`
);
};

70
bin/now-alias.js

@ -9,6 +9,7 @@ const ms = require('ms');
// Ours
const strlen = require('../lib/strlen');
const NowAlias = require('../lib/alias');
const NowDomains = require('../lib/domains');
const login = require('../lib/login');
const cfg = require('../lib/cfg');
const { error } = require('../lib/error');
@ -104,29 +105,33 @@ if (argv.help) {
help();
exit(0);
} else {
const config = cfg.read();
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
exit(1);
}
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await run(token);
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err}\n${err.stack}`);
}
exit(1);
try {
await run({token, config});
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err}\n${err.stack}`);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
exit(1);
});
}
});
}
async function run(token) {
const alias = new NowAlias(apiUrl, token, { debug });
async function run({token, config: {currentTeam, user}}) {
const alias = new NowAlias({apiUrl, token, debug, currentTeam });
const domains = new NowDomains({apiUrl, token, debug, currentTeam });
const args = argv._.slice(1);
switch (subcommand) {
@ -229,7 +234,11 @@ async function run(token) {
const elapsed_ = ms(new Date() - start_);
console.log(
`> ${aliases.length} alias${aliases.length === 1 ? '' : 'es'} found ${chalk.gray(`[${elapsed_}]`)}`
`> ${aliases.length} alias${aliases.length === 1 ? '' : 'es'} found ${chalk.gray(`[${elapsed_}]`)} under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`
);
if (text) {
@ -259,7 +268,11 @@ async function run(token) {
if (!_alias) {
const err = new Error(
`Alias not found by "${_target}". Run ${chalk.dim('`now alias ls`')} to see your aliases.`
`Alias not found by "${_target}" under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}. Run ${chalk.dim('`now alias ls`')} to see your aliases.`
);
err.userError = true;
throw err;
@ -288,7 +301,7 @@ async function run(token) {
case 'add':
case 'set': {
if (argv.rules) {
await updatePathAlias(alias, argv._[0], argv.rules);
await updatePathAlias(alias, argv._[0], argv.rules, domains);
break;
}
if (args.length !== 2) {
@ -297,7 +310,7 @@ async function run(token) {
);
return exit(1);
}
await alias.set(String(args[0]), String(args[1]));
await alias.set(String(args[0]), String(args[1]), currentTeam, user);
break;
}
default: {
@ -307,9 +320,9 @@ async function run(token) {
}
if (argv.rules) {
await updatePathAlias(alias, argv._[0], argv.rules);
await updatePathAlias(alias, argv._[0], argv.rules, domains);
} else if (argv._.length === 2) {
await alias.set(String(argv._[0]), String(argv._[1]));
await alias.set(String(argv._[0]), String(argv._[1]), domains, currentTeam, user);
} else if (argv._.length >= 3) {
error('Invalid number of arguments');
help();
@ -322,6 +335,7 @@ async function run(token) {
}
}
domains.close()
alias.close();
}
@ -343,7 +357,9 @@ async function confirmDeploymentRemoval(alias, _alias) {
const msg = '> The following alias will be removed permanently\n' +
` ${tbl} \nAre you sure?`;
return promptBool(msg);
return promptBool(msg, {
trailing: '\n'
});
}
function findAlias(alias, list) {
@ -382,9 +398,9 @@ function findAlias(alias, list) {
return _alias;
}
async function updatePathAlias(alias, aliasName, rules) {
async function updatePathAlias(alias, aliasName, rules, domains) {
const start = new Date();
const res = await alias.updatePathBasedroutes(String(aliasName), rules);
const res = await alias.updatePathBasedroutes(String(aliasName), rules, domains);
const elapsed = ms(new Date() - start);
if (res.error) {
const err = new Error(res.error.message);

49
bin/now-billing-add.js

@ -12,29 +12,30 @@ const cardBrands = require('../lib/utils/billing/card-brands');
const geocode = require('../lib/utils/billing/geocode');
const success = require('../lib/utils/output/success');
const wait = require('../lib/utils/output/wait');
function rightPad(string, n = 12) {
n -= string.length;
return string + ' '.repeat(n > -1 ? n : 0);
}
const { tick } = require('../lib/utils/output/chars');
const rightPad = require('../lib/utils/output/right-pad');
function expDateMiddleware(data) {
return data;
}
module.exports = function(creditCards) {
module.exports = function({creditCards, currentTeam, user}) {
const state = {
error: undefined,
cardGroupLabel: `> ${chalk.bold('Enter your card details')}`,
cardGroupLabel: `> ${chalk.bold(`Enter your card details for ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`)}`,
name: {
label: rightPad('Full Name'),
label: rightPad('Full Name', 12),
placeholder: 'John Appleseed',
validateValue: data => data.trim().length > 0
},
cardNumber: {
label: rightPad('Number'),
label: rightPad('Number', 12),
mask: 'cc',
placeholder: '#### #### #### ####',
validateKeypress: (data, value) => /\d/.test(data) && value.length < 19,
@ -49,7 +50,7 @@ module.exports = function(creditCards) {
},
ccv: {
label: rightPad('CCV'),
label: rightPad('CCV', 12),
mask: 'ccv',
placeholder: '###',
validateValue: data => {
@ -59,7 +60,7 @@ module.exports = function(creditCards) {
},
expDate: {
label: rightPad('Exp. Date'),
label: rightPad('Exp. Date', 12),
mask: 'expDate',
placeholder: 'mm / yyyy',
middleware: expDateMiddleware,
@ -69,7 +70,7 @@ module.exports = function(creditCards) {
addressGroupLabel: `\n> ${chalk.bold('Enter your billing address')}`,
country: {
label: rightPad('Country'),
label: rightPad('Country', 12),
async autoComplete(value) {
for (const country in countries) {
if (!Object.hasOwnProperty.call(countries, country)) {
@ -85,23 +86,23 @@ module.exports = function(creditCards) {
},
zipCode: {
label: rightPad('ZIP'),
label: rightPad('ZIP', 12),
validadeKeypress: data => data.trim().length > 0,
validateValue: data => data.trim().length > 0
},
state: {
label: rightPad('State'),
label: rightPad('State', 12),
validateValue: data => data.trim().length > 0
},
city: {
label: rightPad('City'),
label: rightPad('City', 12),
validateValue: data => data.trim().length > 0
},
address1: {
label: rightPad('Address'),
label: rightPad('Address', 12),
validateValue: data => data.trim().length > 0
}
};
@ -141,16 +142,16 @@ module.exports = function(creditCards) {
brand = chalk.cyan(`[${brand}]`);
const masked = chalk.gray('#### '.repeat(3)) + result.split(' ')[3];
process.stdout.write(
`${chalk.cyan('✓')} ${piece.label}${masked} ${brand}\n`
`${chalk.cyan(tick)} ${piece.label}${masked} ${brand}\n`
);
} else if (key === 'ccv') {
process.stdout.write(
`${chalk.cyan('✓')} ${piece.label}${'*'.repeat(result.length)}\n`
`${chalk.cyan(tick)} ${piece.label}${'*'.repeat(result.length)}\n`
);
} else if (key === 'expDate') {
let text = result.split(' / ');
text = text[0] + chalk.gray(' / ') + text[1];
process.stdout.write(`${chalk.cyan('✓')} ${piece.label}${text}\n`);
process.stdout.write(`${chalk.cyan(tick)} ${piece.label}${text}\n`);
} else if (key === 'zipCode') {
const stopSpinner = wait(piece.label + result);
const addressInfo = await geocode({
@ -165,11 +166,11 @@ module.exports = function(creditCards) {
}
stopSpinner();
process.stdout.write(
`${chalk.cyan('✓')} ${piece.label}${result}\n`
`${chalk.cyan(tick)} ${piece.label}${result}\n`
);
} else {
process.stdout.write(
`${chalk.cyan('✓')} ${piece.label}${result}\n`
`${chalk.cyan(tick)} ${piece.label}${result}\n`
);
}
} catch (err) {
@ -198,7 +199,11 @@ module.exports = function(creditCards) {
});
stopSpinner();
success(
`${state.cardNumber.brand} ending in ${res.last4} was added to your account`
`${state.cardNumber.brand} ending in ${res.last4} was added to ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`
);
} catch (err) {
stopSpinner();

101
bin/now-billing.js

@ -89,25 +89,27 @@ if (argv.help || !subcommand) {
help();
exit(0);
} else {
const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await run(token);
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err.stack}`);
}
exit(1);
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
exit(1);
}
try {
await run({token, config});
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err.stack}`);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
exit(1);
});
}
});
}
// Builds a `choices` object that can be passesd to inquirer.prompt()
@ -132,15 +134,21 @@ function buildInquirerChoices(cards) {
});
}
async function run(token) {
async function run({token, config: {currentTeam, user}}) {
const start = new Date();
const creditCards = new NowCreditCards(apiUrl, token, { debug });
const creditCards = new NowCreditCards({apiUrl, token, debug, currentTeam });
const args = argv._.slice(1);
switch (subcommand) {
case 'ls':
case 'list': {
const cards = await creditCards.ls();
let cards
try {
cards = await creditCards.ls();
} catch (err) {
error(err.message)
return
}
const text = cards.cards
.map(card => {
const _default = card.id === cards.defaultCardId
@ -177,7 +185,11 @@ async function run(token) {
const elapsed = ms(new Date() - start);
console.log(
`> ${cards.cards.length} card${cards.cards.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`
`> ${cards.cards.length} card${cards.cards.length === 1 ? '' : 's'} found under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
} ${chalk.gray(`[${elapsed}]`)}`
);
if (text) {
console.log(`\n${text}\n`);
@ -193,7 +205,14 @@ async function run(token) {
}
const start = new Date();
const cards = await creditCards.ls();
let cards
try {
cards = await creditCards.ls();
} catch (err) {
error(err.message)
return
}
if (cards.cards.length === 0) {
error('You have no credit cards to choose from');
@ -204,7 +223,11 @@ async function run(token) {
if (cardId === undefined) {
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 for ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
} ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(cards);
cardId = await listInput({
@ -219,7 +242,9 @@ async function run(token) {
// typed `now billing set-default <some-id>`) is valid
if (cardId) {
const label = `Are you sure that you to set this card as the default?`;
const confirmation = await promptBool(label);
const confirmation = await promptBool(label, {
trailing: '\n'
});
if (!confirmation) {
console.log('Aborted');
break;
@ -247,10 +272,20 @@ async function run(token) {
}
const start = new Date();
const cards = await creditCards.ls();
let cards
try {
cards = await creditCards.ls();
} catch (err) {
error(err.message)
return
}
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 under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`);
return exit(0);
}
@ -258,7 +293,11 @@ async function run(token) {
if (cardId === undefined) {
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')} under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
} ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(cards);
cardId = await listInput({
@ -298,7 +337,11 @@ async function run(token) {
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 for ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`;
}
}
@ -313,7 +356,7 @@ async function run(token) {
}
case 'add': {
require(resolve(__dirname, 'now-billing-add.js'))(creditCards);
require(resolve(__dirname, 'now-billing-add.js'))({creditCards, currentTeam, user});
break;
}

117
bin/now-certs.js

@ -9,9 +9,11 @@ const table = require('text-table');
const minimist = require('minimist');
const fs = require('fs-promise');
const ms = require('ms');
const printf = require('printf');
require('epipebomb')();
const supportsColor = require('supports-color');
// Ours
const strlen = require('../lib/strlen');
const cfg = require('../lib/cfg');
const { handleError, error } = require('../lib/error');
const NowCerts = require('../lib/certs');
@ -22,12 +24,7 @@ const logo = require('../lib/utils/output/logo');
const argv = minimist(process.argv.slice(2), {
string: ['config', 'token', 'crt', 'key', 'ca'],
boolean: ['help', 'debug'],
alias: {
help: 'h',
config: 'c',
debug: 'd',
token: 't'
}
alias: { help: 'h', config: 'c', debug: 'd', token: 't' }
});
const subcommand = argv._[0];
@ -85,21 +82,24 @@ if (argv.help || !subcommand) {
help();
exit(0);
} else {
const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await run(token);
} catch (err) {
handleError(err);
exit(1);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
exit(1);
});
}
try {
await run({ token, config });
} catch (err) {
handleError(err);
exit(1);
}
});
}
function formatExpirationDate(date) {
@ -109,8 +109,8 @@ function formatExpirationDate(date) {
: chalk.gray('in ' + ms(diff));
}
async function run(token) {
const certs = new NowCerts(apiUrl, token, { debug });
async function run({ token, config: { currentTeam, user } }) {
const certs = new NowCerts({ apiUrl, token, debug, currentTeam });
const args = argv._.slice(1);
const start = Date.now();
@ -126,7 +126,7 @@ async function run(token) {
const elapsed = ms(new Date() - start);
console.log(
`> ${list.length} certificate${list.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`
`> ${list.length} certificate${list.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)} under ${chalk.bold((currentTeam && currentTeam.slug) || user.username || user.email)}`
);
if (list.length > 0) {
@ -134,36 +134,37 @@ async function run(token) {
list.sort((a, b) => {
return a.cn.localeCompare(b.cn);
});
const header = [
['', 'id', 'cn', 'created', 'expiration', 'auto-renew'].map(s =>
chalk.dim(s))
];
const out = table(
header.concat(
list.map(cert => {
const cn = chalk.bold(cert.cn);
const time = chalk.gray(ms(cur - new Date(cert.created)) + ' ago');
const expiration = formatExpirationDate(new Date(cert.expiration));
return [
'',
cert.uid ? cert.uid : 'unknown',
cn,
time,
expiration,
cert.autoRenew ? 'yes' : 'no'
];
})
),
{
align: ['l', 'r', 'l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen
}
const maxCnLength =
list.reduce((acc, i) => {
return Math.max(acc, (i.cn && i.cn.length) || 0);
}, 0) + 1;
console.log(
chalk.dim(
printf(
` %-${maxCnLength}s %-8s %-10s %-10s`,
'cn',
'created',
'expiration',
'auto-renew'
)
)
);
if (out) {
console.log('\n' + out + '\n');
}
list.forEach(cert => {
const cn = chalk.bold(cert.cn);
const time = chalk.gray(ms(cur - new Date(cert.created)) + ' ago');
const expiration = formatExpirationDate(new Date(cert.expiration));
const autoRenew = cert.autoRenew ? 'yes' : 'no';
let spec;
if (supportsColor) {
spec = ` %-${maxCnLength + 9}s %-18s %-20s %-20s\n`;
} else {
spec = ` %-${maxCnLength}s %-8s %-10s %-10s\n`;
}
process.stdout.write(printf(spec, cn, time, expiration, autoRenew));
});
}
} else if (subcommand === 'create') {
if (args.length !== 1) {
@ -209,7 +210,7 @@ async function run(token) {
return exit(1);
}
const cert = await getCertIdCn(certs, args[0]);
const cert = await getCertIdCn(certs, args[0], currentTeam, user);
if (!cert) {
return exit(1);
}
@ -240,7 +241,7 @@ async function run(token) {
const key = readX509File(argv.key);
const ca = argv.ca ? readX509File(argv.ca) : '';
const cert = await getCertIdCn(certs, args[0]);
const cert = await getCertIdCn(certs, args[0], currentTeam, user);
if (!cert) {
return exit(1);
}
@ -266,7 +267,7 @@ async function run(token) {
return exit(1);
}
const cert = await getCertIdCn(certs, args[0]);
const cert = await getCertIdCn(certs, args[0], currentTeam, user);
if (!cert) {
return exit(1);
}
@ -327,14 +328,16 @@ function readX509File(file) {
return fs.readFileSync(path.resolve(file), 'utf8');
}
async function getCertIdCn(certs, idOrCn) {
async function getCertIdCn(certs, idOrCn, currentTeam, user) {
const list = await certs.ls();
const thecert = list.filter(cert => {
return cert.uid === idOrCn || cert.cn === idOrCn;
})[0];
if (!thecert) {
error(`No certificate found by id or cn "${idOrCn}"`);
error(
`No certificate found by id or cn "${idOrCn}" under ${chalk.bold((currentTeam && currentTeam.slug) || user.username || user.email)}`
);
return null;
}

131
bin/now-deploy.js

@ -35,6 +35,7 @@ const info = require('../lib/utils/output/info');
const wait = require('../lib/utils/output/wait');
const NowPlans = require('../lib/plans');
const promptBool = require('../lib/utils/input/prompt-bool');
const note = require('../lib/utils/output/note');
const argv = minimist(process.argv.slice(2), {
string: ['config', 'token', 'name', 'alias'],
@ -80,21 +81,24 @@ const help = () => {
${chalk.dim('Cloud')}
deploy [path] Performs a deployment ${chalk.bold('(default)')}
ls | list [app] List deployments
rm | remove [id] Remove a deployment
ln | alias [id] [url] Configures aliases for deployments
domains [name] Manages your domain names
certs [cmd] Manages your SSL certificates
secrets [name] Manages your secret environment variables
dns [name] Manages your DNS records
open Open the latest deployment for the project
help [cmd] Displays complete help for [cmd]
deploy [path] Performs a deployment ${chalk.bold('(default)')}
ls | list [app] List deployments
rm | remove [id] Remove a deployment
ln | alias [id] [url] Configures aliases for deployments
domains [name] Manages your domain names
certs [cmd] Manages your SSL certificates
secrets [name] Manages your secret environment variables
dns [name] Manages your DNS records
logs [url] Displays the logs for a deployment
scale [args] Scales the instance count of a deployment
help [cmd] Displays complete help for [cmd]
${chalk.dim('Administrative')}
billing | cc [cmd] Manages your credit cards and billing methods
upgrade | downgrade [plan] Upgrades or downgrades your plan
billing | cc [cmd] Manages your credit cards and billing methods
upgrade | downgrade [plan] Upgrades or downgrades your plan
teams [team] Manages your teams
switch Switches between teams and your account
${chalk.dim('Options:')}
@ -197,44 +201,48 @@ if (deploymentName || wantsPublic) {
forceNew = true;
}
const config = cfg.read();
const alwaysForwardNpm = config.forwardNpm;
if (argv.h || argv.help) {
help();
exit(0);
} else if (argv.v || argv.version) {
console.log(version);
process.exit(0);
} else if (!(argv.token || config.token) || shouldLogin) {
login(apiUrl)
.then(token => {
if (shouldLogin) {
console.log('> Logged in successfully. Token saved in ~/.now.json');
process.exit(0);
} else {
sync(token).catch(err => {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
});
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
let alwaysForwardNpm;
Promise.resolve().then(async () => {
const config = await cfg.read();
alwaysForwardNpm = config.forwardNpm;
if (argv.h || argv.help) {
help();
exit(0);
} else if (argv.v || argv.version) {
console.log(version);
process.exit(0);
} else if (!(argv.token || config.token) || shouldLogin) {
let token;
try {
token = await login(apiUrl);
} catch (err) {
error(`Authentication error – ${err.message}`);
process.exit(1);
}
if (shouldLogin) {
console.log('> Logged in successfully. Token saved in ~/.now.json');
process.exit(0);
} else {
sync({token, config}).catch(err => {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
});
}
} else {
sync({token: argv.token || config.token, config}).catch(err => {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
});
} else {
sync(argv.token || config.token).catch(err => {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
});
}
}
});
async function sync(token) {
async function sync({token, config: {currentTeam, user}}) {
const start = Date.now();
const rawPath = argv._[0];
const planPromise = new NowPlans(apiUrl, token, { debug }).getCurrent();
const planPromise = new NowPlans({apiUrl, token, debug, currentTeam }).getCurrent();
const stopDeployment = msg => {
error(msg);
@ -301,11 +309,18 @@ async function sync(token) {
if (gitRepo.main) {
const gitRef = gitRepo.ref ? ` at "${chalk.bold(gitRepo.ref)}" ` : '';
console.log(
`> Deploying ${gitRepo.type} repository "${chalk.bold(gitRepo.main)}"` +
gitRef
`> Deploying ${gitRepo.type} repository "${chalk.bold(gitRepo.main)}" ${gitRef} under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`
);
} else {
console.log(`> Deploying ${chalk.bold(toHumanPath(path))}`);
console.log(`> Deploying ${chalk.bold(toHumanPath(path))} under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`);
}
}
@ -420,7 +435,7 @@ async function sync(token) {
quiet: true
});
const now = new Now(apiUrl, token, { debug });
const now = new Now({apiUrl, token, debug, currentTeam });
let dotenvConfig;
let dotenvOption;
@ -536,9 +551,7 @@ async function sync(token) {
const env = {};
env_.filter(v => Boolean(v)).forEach(([key, val]) => {
if (key in env) {
console.log(
`> ${chalk.yellow('NOTE:')} Overriding duplicate env key ${chalk.bold(`"${key}"`)}`
);
note(`Overriding duplicate env key ${chalk.bold(`"${key}"`)}`);
}
env[key] = val;
@ -601,7 +614,7 @@ async function sync(token) {
now.close();
// Show build logs
printLogs(now.host, token);
printLogs(now.host, token, currentTeam);
};
const plan = await planPromise;
@ -609,13 +622,17 @@ async function sync(token) {
if (plan.id === 'oss') {
if (isTTY) {
info(
`You are on the OSS plan. Your code will be made ${chalk.bold('public')}.`
`${
chalk.bold(
(currentTeam && `${currentTeam.slug} is`) || `You (${user.username || user.email}) are`
)
} on the OSS plan. Your code will be made ${chalk.bold('public')}.`
);
let proceed;
try {
const label = 'Are you sure you want to proceed with the deployment?';
proceed = await promptBool(label, { trailing: eraseLines(2) });
proceed = await promptBool(label, { trailing: eraseLines(1) });
} catch (err) {
if (err.message === 'USER_ABORT') {
proceed = false;
@ -676,11 +693,11 @@ async function sync(token) {
now.close();
// Show build logs
printLogs(now.host, token);
printLogs(now.host, token, currentTeam);
}
}
function printLogs(host, token) {
function printLogs(host, token, currentTeam) {
// Log build
const logger = new Logger(host, token, { debug, quiet });
@ -714,7 +731,7 @@ function printLogs(host, token) {
const assignments = [];
for (const alias of aliasList) {
assignments.push(assignAlias(alias, token, host, apiUrl, debug));
assignments.push(assignAlias(alias, token, host, apiUrl, debug, currentTeam));
}
await Promise.all(assignments);

49
bin/now-dns.js

@ -76,25 +76,28 @@ if (argv.help || !subcommand) {
help();
exit(0);
} else {
const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await run(token);
} catch (err) {
handleError(err);
exit(1);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
exit(1);
});
}
try {
await run({token, config});
} catch (err) {
handleError(err);
exit(1);
}
});
}
async function run(token) {
const domainRecords = new DomainRecords(apiUrl, token, { debug });
async function run({token, config: {currentTeam, user}}) {
const domainRecords = new DomainRecords({apiUrl, token, debug, currentTeam });
const args = argv._.slice(1);
const start = Date.now();
@ -145,7 +148,11 @@ async function run(token) {
}
});
console.log(
`> ${count} record${count === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`
`> ${count} record${count === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)} under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`
);
console.log(text.join(''));
} else if (subcommand === 'add') {
@ -159,7 +166,11 @@ async function run(token) {
const record = await domainRecords.create(param.domain, param.data);
const elapsed = ms(new Date() - start);
console.log(
`${chalk.cyan('> Success!')} A new DNS record for domain ${chalk.bold(param.domain)} ${chalk.gray(`(${record.uid})`)} created ${chalk.gray(`[${elapsed}]`)}`
`${chalk.cyan('> Success!')} A new DNS record for domain ${chalk.bold(param.domain)} ${chalk.gray(`(${record.uid})`)} created ${chalk.gray(`[${elapsed}]`)} (${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
})`
);
} else if (subcommand === 'rm' || subcommand === 'remove') {
if (args.length !== 1) {
@ -177,7 +188,7 @@ async function run(token) {
const yes = await readConfirmation(
record,
'The following record will be removed permanently\n'
'The following record will be removed permanently \n'
);
if (!yes) {
error('User abort');

57
bin/now-domains.js

@ -1,5 +1,8 @@
#!/usr/bin/env node
// Native
const {resolve} = require('path')
// Packages
const chalk = require('chalk');
const minimist = require('minimist');
@ -111,29 +114,31 @@ if (argv.help || !subcommand) {
help();
exit(0);
} else {
const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await run(token);
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err}\n${err.stack}`);
}
exit(1);
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
exit(1);
}
try {
await run({token, config});
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err}\n${err.stack}`);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
exit(1);
});
}
});
}
async function run(token) {
const domain = new NowDomains(apiUrl, token, { debug });
async function run({token, config: {currentTeam, user}}) {
const domain = new NowDomains({apiUrl, token, debug, currentTeam });
const args = argv._.slice(1);
switch (subcommand) {
@ -149,7 +154,7 @@ async function run(token) {
domains.sort((a, b) => new Date(b.created) - new Date(a.created));
const current = new Date();
const header = [
['', 'id', 'dns', 'url', 'verified', 'created'].map(s => chalk.dim(s))
['', 'id', 'dns', 'domain', 'verified', 'created'].map(s => chalk.dim(s))
];
const out = domains.length === 0
? null
@ -157,7 +162,7 @@ async function run(token) {
header.concat(
domains.map(domain => {
const ns = domain.isExternal ? 'external' : 'zeit.world';
const url = chalk.underline(`https://${domain.name}`);
const url = chalk.bold(domain.name);
const time = chalk.gray(
ms(current - new Date(domain.created)) + ' ago'
);
@ -173,7 +178,11 @@ async function run(token) {
const elapsed_ = ms(new Date() - start_);
console.log(
`> ${domains.length} domain${domains.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed_}]`)}`
`> ${domains.length} domain${domains.length === 1 ? '' : 's'} found under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
} ${chalk.gray(`[${elapsed_}]`)}`
);
if (out) {
@ -264,6 +273,10 @@ async function run(token) {
}
break;
}
case 'buy': {
await require(resolve(__dirname, 'domains', 'buy.js'))({domains: domain, args, currentTeam, user});
break;
}
default:
error('Please specify a valid subcommand: ls | add | rm');
help();

165
bin/now-list.js

@ -4,12 +4,12 @@
const fs = require('fs-promise');
const minimist = require('minimist');
const chalk = require('chalk');
const table = require('text-table');
const ms = require('ms');
const printf = require('printf');
require('epipebomb')();
const supportsColor = require('supports-color');
// Ours
const strlen = require('../lib/strlen');
const indent = require('../lib/indent');
const Now = require('../lib');
const login = require('../lib/login');
const cfg = require('../lib/cfg');
@ -18,7 +18,7 @@ const logo = require('../lib/utils/output/logo');
const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'],
boolean: ['help', 'debug'],
boolean: ['help', 'debug', 'all'],
alias: {
help: 'h',
config: 'c',
@ -69,26 +69,33 @@ if (argv.config) {
cfg.setConfigFile(argv.config);
}
const config = cfg.read();
Promise.resolve().then(async () => {
const config = await cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await list(token);
} catch (err) {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
process.exit(1);
});
}
try {
await list({ token, config });
} catch (err) {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
}
});
async function list(token) {
const now = new Now(apiUrl, token, { debug });
async function list({ token, config: { currentTeam, user } }) {
const now = new Now({ apiUrl, token, debug, currentTeam });
const start = new Date();
if (argv.all && !app) {
console.log('> You must define an app when using `--all`');
process.exit(1);
}
let deployments;
try {
deployments = await now.list(app);
@ -97,40 +104,122 @@ async function list(token) {
process.exit(1);
}
if (!deployments || (Array.isArray(deployments) && deployments.length <= 0)) {
const match = await now.findDeployment(app);
if (match !== null && typeof match !== 'undefined') {
deployments = Array.of(match);
}
}
if (!deployments || (Array.isArray(deployments) && deployments.length <= 0)) {
const aliases = await now.listAliases();
const item = aliases.find(e => e.uid === app || e.alias === app);
const match = await now.findDeployment(item.deploymentId);
if (match !== null && typeof match !== 'undefined') {
deployments = Array.of(match);
}
}
now.close();
const apps = new Map();
if (argv.all) {
await Promise.all(
deployments.map(async ({ uid }, i) => {
deployments[i].instances = await now.listInstances(uid);
})
);
}
for (const dep of deployments) {
const deps = apps.get(dep.name) || [];
apps.set(dep.name, deps.concat(dep));
}
const sorted = await sort([...apps]);
const current = Date.now();
const text = sorted
.map(([name, deps]) => {
const t = table(
deps.map(({ uid, url, created }) => {
const _url = url ? chalk.underline(`https://${url}`) : 'incomplete';
const time = chalk.gray(ms(current - created) + ' ago');
return [uid, _url, time];
}),
{ 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 urlLength =
deployments.reduce((acc, i) => {
return Math.max(acc, (i.url && i.url.length) || 0);
}, 0) + 5;
const timeNow = new Date();
console.log(
`> ${deployments.length} deployment${deployments.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`
`> ${deployments.length} deployment${deployments.length === 1 ? '' : 's'} found under ${chalk.bold((currentTeam && currentTeam.slug) || user.username || user.email)} ${chalk.grey('[' + ms(timeNow - start) + ']')}`
);
if (text) {
console.log('\n' + text + '\n');
let shouldShowAllInfo = false;
for (const app of apps) {
shouldShowAllInfo =
app[1].length > 5 ||
app.find(depl => {
return depl.scale && depl.scale.current > 1;
});
if (shouldShowAllInfo) {
break;
}
}
if (!argv.all && shouldShowAllInfo) {
console.log(
`> To expand the list and see instances run ${chalk.cyan('`now ls --all [app]`')}`
);
}
console.log();
sorted.forEach(([name, deps]) => {
const listedDeployments = argv.all ? deps : deps.slice(0, 5);
console.log(
`${chalk.bold(name)} ${chalk.gray('(' + listedDeployments.length + ' of ' + deps.length + ' total)')}`
);
const urlSpec = `%-${urlLength}s`;
console.log(
printf(
` ${chalk.grey(urlSpec + ' %8s %-16s %8s')}`,
'url',
'inst #',
'state',
'age'
)
);
listedDeployments.forEach(dep => {
let state = dep.state;
let extraSpaceForState = 0;
if (state === null || typeof state === 'undefined') {
state = 'DEPLOYMENT_ERROR';
}
if (/ERROR/.test(state)) {
state = chalk.red(state);
extraSpaceForState = 10;
} else if (state === 'FROZEN') {
state = chalk.grey(state);
extraSpaceForState = 10;
}
let spec;
if (supportsColor) {
spec = ` %-${urlLength + 10}s %8s %-${extraSpaceForState + 16}s %8s`;
} else {
spec = ` %-${urlLength + 1}s %8s %-${16}s %8s`;
}
console.log(
printf(
spec,
chalk.underline(dep.url),
dep.scale.current,
state,
ms(timeNow - dep.created)
)
);
if (Array.isArray(dep.instances) && dep.instances.length > 0) {
dep.instances.forEach(i => {
console.log(
printf(` %-${urlLength + 10}s`, ` - ${chalk.underline(i.url)}`)
);
});
console.log();
}
});
console.log();
});
}
async function sort(apps) {

270
bin/now-logs.js

@ -0,0 +1,270 @@
#!/usr/bin/env node
const qs = require('querystring');
const minimist = require('minimist');
const chalk = require('chalk');
const dateformat = require('dateformat');
const io = require('socket.io-client');
const Now = require('../lib');
const login = require('../lib/login');
const cfg = require('../lib/cfg');
const { handleError, error } = require('../lib/error');
const logo = require('../lib/utils/output/logo');
const { compare, deserialize } = require('../lib/logs');
const { maybeURL, normalizeURL } = require('../lib/utils/url');
const argv = minimist(process.argv.slice(2), {
string: ['config', 'query', 'since', 'token', 'until'],
boolean: ['help', 'all', 'debug', 'f'],
alias: {
help: 'h',
all: 'a',
config: 'c',
debug: 'd',
token: 't',
query: 'q'
}
});
let deploymentIdOrURL = argv._[0];
const help = () => {
console.log(
`
${chalk.bold(`${logo} now logs`)} <deploymentId|url>
${chalk.dim('Options:')}
-h, --help output usage information
-a, --all include access logs
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} config file
-d, --debug debug mode [off]
-f wait for additional data [off]
-n ${chalk.bold.underline('NUMBER')} number of logs [1000]
-q ${chalk.bold.underline('QUERY')}, --query=${chalk.bold.underline('QUERY')} search query
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} login token
--since=${chalk.bold.underline('SINCE')} only return logs after date (ISO 8601)
--until=${chalk.bold.underline('UNTIL')} only return logs before date (ISO 8601), ignored if the f option is enbled.
${chalk.dim('Examples:')}
${chalk.gray('–')} Print logs for the deployment ${chalk.dim('`deploymentId`')}
${chalk.cyan('$ now logs deploymentId')}
`
);
};
if (argv.help || !deploymentIdOrURL) {
help();
process.exit(0);
}
// Options
const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co';
if (argv.config) {
cfg.setConfigFile(argv.config);
}
const limit = typeof argv.n === 'number' ? argv.n : 1000;
const query = argv.query || '';
const follow = argv.f;
const types = argv.all ? [] : ['command', 'stdout', 'stderr', 'exit'];
let since;
try {
since = argv.since ? toSerial(argv.since) : null;
} catch (err) {
error(`Invalid date string: ${argv.since}`);
process.exit(1);
}
let until;
try {
until = argv.until ? toSerial(argv.until) : null;
} catch (err) {
error(`Invalid date string: ${argv.until}`);
process.exit(1);
}
if (maybeURL(deploymentIdOrURL)) {
deploymentIdOrURL = normalizeURL(deploymentIdOrURL);
}
Promise.resolve()
.then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || login(apiUrl);
} catch (err) {
error(`Authentication error – ${err.message}`);
process.exit(1);
}
await printLogs({token, config});
})
.catch(err => {
error(`Unknown error: ${err.stack}`);
process.exit(1);
});
async function printLogs({token, config: {currentTeam}}) {
let buf = [];
let init = false;
let lastLog;
if (!follow) {
onLogs(await fetchLogs({token, currentTeam, since, until }));
return;
}
const isURL = deploymentIdOrURL.includes('.');
const q = qs.stringify({
deploymentId: isURL ? '' : deploymentIdOrURL,
host: isURL ? deploymentIdOrURL : '',
types: types.join(','),
query
});
const socket = io(`https://log-io.zeit.co?${q}`);
socket.on('connect', () => {
if (debug) {
console.log('> [debug] Socket connected');
}
});
socket.on('auth', callback => {
if (debug) {
console.log('> [debug] Socket authenticate');
}
callback(token);
});
socket.on('ready', () => {
if (debug) {
console.log('> [debug] Socket ready');
}
// For the case socket reconnected
const _since = lastLog ? lastLog.serial : since;
fetchLogs({token, currentTeam, since: _since }).then(logs => {
init = true;
const m = {};
logs.concat(buf.map(b => b.log)).forEach(l => {
m[l.id] = l;
});
buf = [];
onLogs(Object.values(m));
});
});
socket.on('logs', l => {
const log = deserialize(l);
let timer;
if (init) {
// Wait for other logs for a while
// and sort them in the correct order
timer = setTimeout(() => {
buf.sort((a, b) => compare(a.log, b.log));
const idx = buf.findIndex(b => b.log.id === log.id);
buf.slice(0, idx + 1).forEach(b => {
clearTimeout(b.timer);
onLog(b.log);
});
buf = buf.slice(idx + 1);
}, 300);
}
buf.push({ log, timer });
});
socket.on('disconnect', () => {
if (debug) {
console.log('> [debug] Socket disconnect');
}
init = false;
});
socket.on('error', err => {
if (debug) {
console.log('> [debug] Socket error', err.stack);
}
});
function onLogs(logs) {
logs.sort(compare).forEach(onLog);
}
function onLog(log) {
lastLog = log;
printLog(log);
}
}
function printLog(log) {
let data;
const obj = log.object;
if (log.type === 'request') {
data =
`REQ "${obj.method} ${obj.uri} ${obj.protocol}"` +
` ${obj.remoteAddr} - ${obj.remoteUser || ''}` +
` "${obj.referer || ''}" "${obj.userAgent}"`;
} else if (log.type === 'response') {
data =
`RES "${obj.method} ${obj.uri} ${obj.protocol}"` +
` ${obj.status} ${obj.bodyBytesSent}`;
} else {
data = obj
? JSON.stringify(obj, null, 2)
: (log.text || '').replace(/\n$/, '');
}
const date = dateformat(log.date, 'mm/dd hh:MM TT');
data.split('\n').forEach((line, i) => {
if (i === 0) {
console.log(`${chalk.dim(date)} ${line}`);
} else {
console.log(`${repeat(' ', date.length)} ${line}`);
}
});
}
async function fetchLogs({token, currentTeam, since, until } = {}) {
const now = new Now({apiUrl, token, debug, currentTeam });
let logs;
try {
logs = await now.logs(deploymentIdOrURL, {
types,
limit,
query,
since,
until
});
} catch (err) {
handleError(err);
process.exit(1);
} finally {
now.close();
}
return logs.map(deserialize);
}
function repeat(s, n) {
return new Array(n + 1).join(s);
}
function toSerial(datestr) {
const t = Date.parse(datestr);
if (isNaN(t)) {
throw new TypeError('Invalid date string');
}
const pidLen = 19;
const seqLen = 19;
return t + repeat('0', pidLen + seqLen);
}

47
bin/now-open.js

@ -61,24 +61,27 @@ if (argv.config) {
cfg.setConfigFile(argv.config);
}
const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await open(token);
} catch (err) {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
process.exit(1);
});
}
try {
await open({token, config});
} catch (err) {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
}
});
async function open(token) {
const now = new Now(apiUrl, token, { debug });
async function open({token, config: {currentTeam, user}}) {
const now = new Now({apiUrl, token, debug, currentTeam });
let deployments;
try {
@ -111,7 +114,11 @@ async function open(token) {
);
if (typeof currentProjectDeployments === 'undefined') {
console.log(`no deployments found for ${chalk.bold(pkg.name)}`);
console.log(`No deployments found for ${chalk.bold(pkg.name)} under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`);
process.exit(0);
}
@ -121,7 +128,11 @@ async function open(token) {
try {
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)}... under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}`);
console.log(`Here's the URL: ${chalk.underline(url)}`);
opn(url);

78
bin/now-remove.js

@ -1,18 +1,18 @@
#!/usr/bin/env node
// Packages
const minimist = require('minimist');
const chalk = require('chalk');
const ms = require('ms');
const table = require('text-table');
const isURL = require('is-url');
const minimist = require('minimist')
const chalk = require('chalk')
const ms = require('ms')
const table = require('text-table')
// Ours
const Now = require('../lib');
const login = require('../lib/login');
const cfg = require('../lib/cfg');
const { handleError, error } = require('../lib/error');
const logo = require('../lib/utils/output/logo');
const Now = require('../lib')
const login = require('../lib/login')
const cfg = require('../lib/cfg')
const {handleError, error} = require('../lib/error')
const logo = require('../lib/utils/output/logo')
const {normalizeURL} = require('../lib/utils/url')
const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'],
@ -76,7 +76,24 @@ if (argv.config) {
cfg.setConfigFile(argv.config);
}
const config = cfg.read();
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = (await argv.token) || config.token || login(apiUrl);
} catch (err) {
error(`Authentication error – ${err.message}`);
process.exit(1);
}
try {
await remove({token, config});
} catch (err) {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
}
});
function readConfirmation(matches) {
return new Promise(resolve => {
@ -116,43 +133,16 @@ function readConfirmation(matches) {
});
}
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await remove(token);
} catch (err) {
error(`Unknown error: ${err}\n${err.stack}`);
process.exit(1);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
process.exit(1);
});
async function remove(token) {
const now = new Now(apiUrl, token, { debug });
async function remove({token, config: {currentTeam}}) {
const now = new Now({apiUrl, token, debug, currentTeam });
const deployments = await now.list();
const matches = deployments.filter(d => {
return ids.find(id => {
// Normalize URL by removing slash from the end
if (isURL(id) && id.slice(-1) === '/') {
id = id.slice(0, -1);
}
// `url` should match the hostname of the deployment
let u = id.replace(/^https:\/\//i, '');
if (u.indexOf('.') === -1) {
// `.now.sh` domain is implied if just the subdomain is given
u += '.now.sh';
}
return d.uid === id || d.name === id || d.url === u;
});
});
return ids.some(id => {
return d.uid === id || d.name === id || d.url === normalizeURL(id)
})
})
if (matches.length === 0) {
error(

309
bin/now-scale.js

@ -0,0 +1,309 @@
#!/usr/bin/env node
// Packages
const chalk = require('chalk');
const isURL = require('is-url');
const minimist = require('minimist');
const ms = require('ms');
const printf = require('printf');
require('epipebomb')();
const supportsColor = require('supports-color');
// Ours
const cfg = require('../lib/cfg');
const { handleError, error } = require('../lib/error');
const NowScale = require('../lib/scale');
const login = require('../lib/login');
const exit = require('../lib/utils/exit');
const logo = require('../lib/utils/output/logo');
const info = require('../lib/scale-info');
const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'],
boolean: ['help', 'debug'],
alias: { help: 'h', config: 'c', debug: 'd', token: 't' }
});
let id = argv._[0];
const scaleArg = argv._[1];
const optionalScaleArg = argv._[2];
// Options
const help = () => {
console.log(
`
${chalk.bold(`${logo} now scale`)} ls
${chalk.bold(`${logo} now scale`)} <url>
${chalk.bold(`${logo} now scale`)} <url> <min> [max]
${chalk.dim('Options:')}
-h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file
-d, --debug Debug mode [off]
${chalk.dim('Examples:')}
${chalk.gray('–')} Create an deployment with 3 instances, never sleeps:
${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh 3')}
${chalk.gray('–')} Create an automatically scaling deployment:
${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh 1 5')}
${chalk.gray('–')} Create an automatically scaling deployment without specifying max:
${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh 1 auto')}
${chalk.gray('–')} Create an automatically scaling deployment without specifying min or max:
${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh auto')}
${chalk.gray('–')} Create an deployment that is always active and never "sleeps":
${chalk.cyan('$ now scale my-deployment-ntahoeato.now.sh 1')}
`
);
};
// Options
const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co';
if (argv.config) {
cfg.setConfigFile(argv.config);
}
if (argv.help) {
help();
exit(0);
} else {
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
exit(1);
}
try {
await run({ token, config });
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err}\n${err.stack}`);
}
exit(1);
}
});
}
function guessParams() {
if (Number.isInteger(scaleArg) && !optionalScaleArg) {
return { min: scaleArg, max: scaleArg };
} else if (Number.isInteger(scaleArg) && Number.isInteger(optionalScaleArg)) {
return { min: scaleArg, max: optionalScaleArg };
} else if (Number.isInteger(scaleArg) && optionalScaleArg === 'auto') {
return { min: scaleArg, max: 'auto' };
} else if (!scaleArg && !optionalScaleArg) {
return { min: 1, max: 'auto' };
}
help();
process.exit(1);
}
async function run({ token, config: { currentTeam } }) {
const scale = new NowScale({ apiUrl, token, debug, currentTeam });
const start = Date.now();
if (id === 'ls') {
await list(scale);
process.exit(0);
} else if (id === 'info') {
await info(scale);
process.exit(0);
} else if (id) {
// Normalize URL by removing slash from the end
if (isURL(id) && id.slice(-1) === '/') {
id = id.slice(0, -1);
}
} else {
error('Please specify a deployment: now scale <id|url>');
help();
exit(1);
}
const deployments = await scale.list();
const match = deployments.find(d => {
// `url` should match the hostname of the deployment
let u = id.replace(/^https:\/\//i, '');
if (u.indexOf('.') === -1) {
// `.now.sh` domain is implied if just the subdomain is given
u += '.now.sh';
}
return d.uid === id || d.name === id || d.url === u;
});
if (!match) {
error(`Could not find any deployments matching ${id}`);
return process.exit(1);
}
const { min, max } = guessParams();
if (
!(Number.isInteger(min) || min === 'auto') &&
!(Number.isInteger(max) || max === 'auto')
) {
help();
return exit(1);
}
const {
max: currentMax,
min: currentMin,
current: currentCurrent
} = match.scale;
if (
max === currentMax &&
min === currentMin &&
Number.isInteger(min) &&
currentCurrent >= min &&
Number.isInteger(max) &&
currentCurrent <= max
) {
console.log(`> Done`);
return;
}
if ((match.state === 'FROZEN' || match.scale.current === 0) && min > 0) {
console.log(
`> Deployment is currently in 0 replicas, preparing deployment for scaling...`
);
if (match.scale.max < 1) {
await scale.setScale(match.uid, { min: 0, max: 1 });
}
await scale.unfreeze(match);
}
const { min: newMin, max: newMax } = await scale.setScale(match.uid, {
min,
max
});
const elapsed = ms(new Date() - start);
const currentReplicas = match.scale.current;
const log = console.log;
log(`> ${chalk.cyan('Success!')} Configured scaling rules [${elapsed}]`);
log();
log(
`${chalk.bold(match.url)} (${chalk.gray(currentReplicas)} ${chalk.gray('current')})`
);
log(printf('%6s %s', 'min', chalk.bold(newMin)));
log(printf('%6s %s', 'max', chalk.bold(newMax)));
log(printf('%6s %s', 'auto', chalk.bold(newMin === newMax ? '✖' : '✔')));
log();
await info(scale, match.url);
scale.close();
}
async function list(scale) {
let deployments;
try {
const app = argv._[1];
deployments = await scale.list(app);
} catch (err) {
handleError(err);
process.exit(1);
}
scale.close();
const apps = new Map();
for (const dep of deployments) {
const deps = apps.get(dep.name) || [];
apps.set(dep.name, deps.concat(dep));
}
const timeNow = new Date();
const urlLength =
deployments.reduce((acc, i) => {
return Math.max(acc, (i.url && i.url.length) || 0);
}, 0) + 5;
for (const app of apps) {
const depls = argv.all ? app[1] : app[1].slice(0, 5);
console.log(
`${chalk.bold(app[0])} ${chalk.gray('(' + depls.length + ' of ' + app[1].length + ' total)')}`
);
console.log();
const urlSpec = `%-${urlLength}s`;
console.log(
printf(
` ${chalk.grey(urlSpec + ' %8s %8s %8s %8s %8s')}`,
'url',
'cur',
'min',
'max',
'auto',
'age'
)
);
for (const instance of depls) {
if (instance.scale.current > 0) {
let spec;
if (supportsColor) {
spec = ` %-${urlLength + 10}s %8s %8s %8s %8s %8s`;
} else {
spec = ` %-${urlLength + 1}s %8s %8s %8s %8s %8s`;
}
console.log(
printf(
spec,
chalk.underline(instance.url),
instance.scale.current,
instance.scale.min,
instance.scale.max,
instance.scale.max === instance.scale.min ? '✖' : '✔',
ms(timeNow - instance.created)
)
);
} else {
let spec;
if (supportsColor) {
spec = ` %-${urlLength + 10}s ${chalk.gray('%8s %8s %8s %8s %8s')}`;
} else {
spec = ` %-${urlLength + 1}s ${chalk.gray('%8s %8s %8s %8s %8s')}`;
}
console.log(
printf(
spec,
chalk.underline(instance.url),
instance.scale.current,
instance.scale.min,
instance.scale.max,
instance.scale.max === instance.scale.min ? '✖' : '✔',
ms(timeNow - instance.created)
)
);
}
}
console.log();
}
}
process.on('uncaughtException', err => {
handleError(err);
exit(1);
});

47
bin/now-secrets.js

@ -86,25 +86,28 @@ if (argv.help || !subcommand) {
help();
exit(0);
} else {
const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await run(token);
} catch (err) {
handleError(err);
exit(1);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
exit(1);
});
}
try {
await run({token, config});
} catch (err) {
handleError(err);
exit(1);
}
});
}
async function run(token) {
const secrets = new NowSecrets(apiUrl, token, { debug });
async function run({token, config: {currentTeam, user}}) {
const secrets = new NowSecrets({apiUrl, token, debug, currentTeam });
const args = argv._.slice(1);
const start = Date.now();
@ -120,7 +123,11 @@ async function run(token) {
const elapsed = ms(new Date() - start);
console.log(
`> ${list.length} secret${list.length === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`
`> ${list.length} secret${list.length === 1 ? '' : 's'} found under ${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
} ${chalk.gray(`[${elapsed}]`)}`
);
if (list.length > 0) {
@ -226,7 +233,11 @@ async function run(token) {
const elapsed = ms(new Date() - start);
console.log(
`${chalk.cyan('> Success!')} Secret ${chalk.bold(name.toLowerCase())} ${chalk.gray(`(${secret.uid})`)} added ${chalk.gray(`[${elapsed}]`)}`
`${chalk.cyan('> Success!')} Secret ${chalk.bold(name.toLowerCase())} ${chalk.gray(`(${secret.uid})`)} added (${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
}) ${chalk.gray(`[${elapsed}]`)}`
);
return secrets.close();
}

142
bin/now-teams.js

@ -0,0 +1,142 @@
#!/usr/bin/env node
// Native
const { resolve } = require('path');
// Packages
const chalk = require('chalk');
const minimist = require('minimist');
// Ours
const login = require('../lib/login');
const cfg = require('../lib/cfg');
const error = require('../lib/utils/output/error');
const NowTeams = require('../lib/teams');
const logo = require('../lib/utils/output/logo');
const exit = require('../lib/utils/exit');
const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'],
boolean: ['help', 'debug'],
alias: {
help: 'h',
config: 'c',
debug: 'd',
token: 't',
switch: 'change'
}
});
const subcommand = argv._[0];
const help = () => {
console.log(
`
${chalk.bold(`${logo} now teams`)} <add | ls | rm | invite>
${chalk.dim('Options:')}
-h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} Config file
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} Login token
${chalk.dim('Examples:')}
${chalk.gray('–')} Add a new team:
${chalk.cyan('$ now teams add')}
${chalk.gray('–')} Switch to a team:
${chalk.cyan(`$ now switch <id>`)}
${chalk.gray('–')} If the id is omitted, you can choose interactively
${chalk.yellow('NOTE:')} When you switch, everything you add, list or remove will be scoped that team!
${chalk.gray('–')} Invite new members (interactively):
${chalk.cyan(`$ now teams invite`)}
${chalk.gray('–')} Invite a specific email:
${chalk.cyan(`$ now teams invite geist@zeit.co`)}
${chalk.gray('–')} Remove a team:
${chalk.cyan(`$ now teams rm <id>`)}
${chalk.gray('–')} If the id is omitted, you can choose interactively
`
);
};
// Options
const debug = argv.debug;
const apiUrl = argv.url || 'https://api.zeit.co';
if (argv.config) {
cfg.setConfigFile(argv.config);
}
if (argv.help || !subcommand) {
help();
exit(0);
} else {
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
exit(1);
}
try {
await run({token, config});
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err.stack}`);
}
exit(1);
}
});
}
async function run({token, config: {currentTeam}}) {
const teams = new NowTeams({ apiUrl, token, debug, currentTeam });
const args = argv._.slice(1);
switch (subcommand) {
case 'switch':
case 'change': {
await require(resolve(__dirname, 'teams', 'switch.js'))(teams, args);
break;
}
case 'add':
case 'create': {
await require(resolve(__dirname, 'teams', 'add.js'))(teams);
break;
}
case 'invite': {
await require(resolve(__dirname, 'teams', 'invite.js'))(teams, args);
break;
}
default: {
let code = 0;
if (subcommand !== 'help') {
error('Please specify a valid subcommand: ls | add | rm | set-default');
code = 1;
}
help();
exit(code);
}
}
}

147
bin/now-upgrade.js

@ -4,7 +4,6 @@
const chalk = require('chalk');
const minimist = require('minimist');
const ms = require('ms');
const stripAnsi = require('strip-ansi');
// Ours
const login = require('../lib/login');
@ -18,6 +17,8 @@ const success = require('../lib/utils/output/success');
const cmd = require('../lib/utils/output/cmd');
const logo = require('../lib/utils/output/logo');
const {bold} = chalk
const argv = minimist(process.argv.slice(2), {
string: ['config', 'token'],
boolean: ['help', 'debug'],
@ -78,25 +79,28 @@ if (argv.help) {
help();
exit(0);
} else {
const config = cfg.read();
Promise.resolve(argv.token || config.token || login(apiUrl))
.then(async token => {
try {
await run(token);
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err.stack}`);
}
exit(1);
Promise.resolve().then(async () => {
const config = await cfg.read();
let token;
try {
token = argv.token || config.token || (await login(apiUrl));
} catch (err) {
error(`Authentication error – ${err.message}`);
exit(1);
}
try {
await run({token, config});
} catch (err) {
if (err.userError) {
error(err.message);
} else {
error(`Unknown error: ${err.stack}`);
}
})
.catch(e => {
error(`Authentication error – ${e.message}`);
exit(1);
});
}
});
}
function buildInquirerChoices(current, until) {
@ -106,37 +110,60 @@ function buildInquirerChoices(current, until) {
} else {
until = '';
}
const ossTitle = current === 'oss'
? `oss FREE ${' '.repeat(28)} (current)`
: 'oss FREE';
const premiumTitle = current === 'premium'
? `premium $15/mo ${' '.repeat(24 - stripAnsi(until).length)} (current${until})`
: 'premium $15/mo';
const currentText = bold('(current)')
let ossName = `OSS ${bold('FREE')}`
let premiumName = `Premium ${bold('$15')}`
let proName = `Pro ${bold('$50')}`
let advancedName = `Advanced ${bold('$200')}`
switch (current) {
case 'oss': {
ossName += indent(currentText, 6)
break
}
case 'premium': {
premiumName += indent(currentText, 3)
break
}
case 'pro': {
proName += indent(currentText, 7)
break
}
case 'advanced': {
advancedName += indent(currentText, 1)
break
}
default: {
ossName += indent(currentText, 6)
}
}
return [
{
name: [
ossTitle,
indent('✓ All code is public and open-source', 2),
indent('✓ 20 deploys per month | 1GB monthly bandwidth', 2),
indent('✓ 1GB FREE storage | 1MB size limit per file', 2)
].join('\n'),
name: ossName,
value: 'oss',
short: 'oss FREE'
short: `OSS ${bold('FREE')}`
},
{
name: [
premiumTitle,
indent('✓ All code is private and secure', 2),
indent('✓ 1000 deploys per month | 50GB monthly bandwidth', 2),
indent('✓ 100GB storage | No filesize limit', 2)
].join('\n'),
name: premiumName,
value: 'premium',
short: 'premium $15/mo'
}
short: `Premium ${bold('$15')}`
},
{
name: proName,
value: 'pro',
short: `Pro ${bold('$50')}`
},
{
name: advancedName,
value: 'advanced',
short: `Advanced ${bold('$200')}`
},
];
}
async function run(token) {
async function run({token, config: {currentTeam, user}}) {
const args = argv._;
if (args.length > 1) {
error('Invalid number of arguments');
@ -144,11 +171,11 @@ async function run(token) {
}
const start = new Date();
const plans = new NowPlans(apiUrl, token, { debug });
const plans = new NowPlans({ apiUrl, token, debug, currentTeam });
let planId = args[0];
if (![undefined, 'oss', 'premium'].includes(planId)) {
if (![undefined, 'oss', 'premium', 'pro', 'advanced'].includes(planId)) {
error(`Invalid plan name – should be ${code('oss')} or ${code('premium')}`);
return exit(1);
}
@ -158,14 +185,21 @@ async function run(token) {
if (planId === undefined) {
const elapsed = ms(new Date() - start);
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}]`)}`;
let message = `For more info, please head to https://zeit.co`;
message = currentTeam ?
`${message}/${currentTeam.slug}/settings/plan` :
`${message}/account/plan`
message += `\n> Select a plan for ${
bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
} ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(currentPlan.id, currentPlan.until);
planId = await listInput({
message,
choices,
separator: true,
separator: false,
abort: 'end'
});
}
@ -182,38 +216,27 @@ async function run(token) {
try {
newPlan = await plans.set(planId);
} catch (err) {
let errorBody;
if (err.res && err.res.status === 400) {
errorBody = err.res.json();
} else {
const message = 'A network error has occurred. Please retry.';
errorBody = { message };
}
const _err = (await errorBody).error;
const { code, message } = _err;
if (code === 'customer_not_found' || code === 'source_not_found') {
if (err.code === 'customer_not_found' || err.code === 'source_not_found') {
error(
`You have no payment methods available. Run ${cmd('now billing add')} to add one`
);
} else {
error(`An unknow error occured. Please try again later ${message}`);
error(`An unknow error occured. Please try again later ${err.message}`);
}
plans.close();
return;
}
if (currentPlan.until && newPlan.id === 'premium') {
if (currentPlan.until && newPlan.id !== 'oss') {
success(
`The cancelation has been undone. You're back on the ${chalk.bold('Premium plan')}`
`The cancelation has been undone. You're back on the ${chalk.bold(`${newPlan.name} plan`)}`
);
} else if (newPlan.until) {
success(
`Your plan will be switched to OSS in ${chalk.bold(newPlan.until)}. Your card will not be charged again`
`Your plan will be switched to ${chalk.bold(newPlan.name)} in ${chalk.bold(newPlan.until)}. Your card will not be charged again`
);
} else {
success(`You're now on the ${chalk.bold('Premium plan')}`);
success(`You're now on the ${chalk.bold(`${newPlan.name} plan`)}`);
}
plans.close();

20
bin/now.js

@ -47,7 +47,13 @@ const commands = new Set([
'billing',
'upgrade',
'downgrade',
'open'
'open',
'team',
'teams',
'switch',
'log',
'logs',
'scale'
]);
const aliases = new Map([
@ -59,11 +65,14 @@ const aliases = new Map([
['cert', 'certs'],
['secret', 'secrets'],
['cc', 'billing'],
['downgrade', 'upgrade']
['downgrade', 'upgrade'],
['team', 'teams'],
['switch', 'teams switch'],
['log', 'logs']
]);
let cmd = defaultCommand;
const args = process.argv.slice(2);
let args = process.argv.slice(2);
const index = args.findIndex(a => commands.has(a));
if (index > -1) {
@ -82,6 +91,11 @@ if (index > -1) {
}
cmd = aliases.get(cmd) || cmd;
if (cmd.includes(' ')) {
const parts = cmd.split(' ');
cmd = parts.shift();
args = [].concat(parts, args);
}
}
// Don't throw a useless error message when running `now help help`

127
bin/teams/add.js

@ -0,0 +1,127 @@
// Packages
const chalk = require('chalk');
// Ours
const stamp = require('../../lib/utils/output/stamp');
const info = require('../../lib/utils/output/info');
const error = require('../../lib/utils/output/error');
const wait = require('../../lib/utils/output/wait');
const rightPad = require('../../lib/utils/output/right-pad');
const eraseLines = require('../../lib/utils/output/erase-lines');
const { tick } = require('../../lib/utils/output/chars');
const success = require('../../lib/utils/output/success');
const cmd = require('../../lib/utils/output/cmd');
const note = require('../../lib/utils/output/note');
const uid = require('../../lib/utils/output/uid');
const textInput = require('../../lib/utils/input/text');
const exit = require('../../lib/utils/exit');
const cfg = require('../../lib/cfg');
function validateSlugKeypress(data, value) {
// TODO: the `value` here should contain the current value + the keypress
// should be fixed on utils/input/text.js
return /^[a-zA-Z]+[a-zA-Z0-9_-]*$/.test(value + data);
}
function gracefulExit() {
console.log(); // Blank line
note(
`Your team is now active for all ${cmd('now')} commands!\n Run ${cmd('now switch')} to change it in the future.`
);
return exit();
}
const teamUrlPrefix = rightPad('Team URL', 14) + chalk.gray('zeit.co/');
const teamNamePrefix = rightPad('Team Name', 14);
module.exports = async function(teams) {
let slug;
let team;
let elapsed;
let stopSpinner;
info(
`Pick a team identifier for its url (e.g.: ${chalk.cyan('`zeit.co/acme`')})`
);
do {
try {
// eslint-disable-next-line no-await-in-loop
slug = await textInput({
label: `- ${teamUrlPrefix}`,
validateKeypress: validateSlugKeypress,
initialValue: slug,
valid: team,
forceLowerCase: true
});
} catch (err) {
if (err.message === 'USER_ABORT') {
info('Aborted');
return exit();
}
throw err;
}
elapsed = stamp();
stopSpinner = wait(teamUrlPrefix + slug);
let res
try {
// eslint-disable-next-line no-await-in-loop
res = await teams.create({ slug });
stopSpinner();
team = res
} catch (err) {
stopSpinner();
eraseLines(2);
error(err.message)
}
} while (!team);
eraseLines(2);
success(`Team created ${uid(team.id)} ${elapsed()}`);
console.log(chalk.cyan(`${tick} `) + teamUrlPrefix + slug + '\n');
info('Pick a display name for your team');
let name;
try {
name = await textInput({
label: `- ${teamNamePrefix}`,
validateValue: value => value.trim().length > 0
});
} catch (err) {
if (err.message === 'USER_ABORT') {
info('No name specified');
gracefulExit();
} else {
throw err;
}
}
elapsed = stamp();
stopSpinner = wait(teamNamePrefix + name);
const res = await teams.edit({ id: team.id, name });
stopSpinner();
eraseLines(2);
if (res.error) {
error(res.error.message);
console.log(`${chalk.red(`${teamNamePrefix}`)}${name}`);
exit(1);
// TODO: maybe we want to ask the user to retry? not sure if
// there's a scenario where that would be wanted
}
team = Object.assign(team, res);
success(`Team name saved ${elapsed()}`);
console.log(chalk.cyan(`${tick} `) + teamNamePrefix + team.name + '\n');
stopSpinner = wait('Saving');
await cfg.merge({ currentTeam: team });
stopSpinner();
await require('./invite')(teams, [], {
introMsg: 'Invite your team mates! When done, press enter on an empty field',
noopMsg: `You can invite team mates later by running ${cmd('now teams invite')}`
});
gracefulExit();
};

160
bin/teams/invite.js

@ -0,0 +1,160 @@
// Packages
const chalk = require('chalk');
// Ours
const regexes = require('../../lib/utils/input/regexes');
const wait = require('../../lib/utils/output/wait');
const cfg = require('../../lib/cfg');
const fatalError = require('../../lib/utils/fatal-error');
const cmd = require('../../lib/utils/output/cmd');
const info = require('../../lib/utils/output/info');
const stamp = require('../../lib/utils/output/stamp');
const param = require('../../lib/utils/output/param');
const { tick } = require('../../lib/utils/output/chars');
const rightPad = require('../../lib/utils/output/right-pad');
const textInput = require('../../lib/utils/input/text');
const eraseLines = require('../../lib/utils/output/erase-lines');
const success = require('../../lib/utils/output/success');
const error = require('../../lib/utils/output/error');
function validateEmail(data) {
return regexes.email.test(data.trim()) || data.length === 0;
}
const domains = Array.from(
new Set([
'aol.com',
'gmail.com',
'google.com',
'yahoo.com',
'ymail.com',
'hotmail.com',
'live.com',
'outlook.com',
'inbox.com',
'mail.com',
'gmx.com',
'icloud.com'
])
);
function emailAutoComplete(value, teamSlug) {
const parts = value.split('@');
if (parts.length === 2 && parts[1].length > 0) {
const [, host] = parts;
let suggestion = false;
domains.unshift(teamSlug);
for (const domain of domains) {
if (domain.startsWith(host)) {
suggestion = domain.substr(host.length);
break;
}
}
domains.shift();
return suggestion;
}
return false;
}
module.exports = async function(
teams,
args,
{
introMsg,
noopMsg = 'No changes made'
} = {}
) {
const { user, currentTeam } = await cfg.read();
domains.push(user.email.split('@')[1]);
if (!currentTeam) {
let err = `You can't run this command under ${param(user.username || user.email)}.\n`;
err += `${chalk.gray('>')} Run ${cmd('now switch')} to choose to a team.`;
return fatalError(err);
}
info(introMsg || `Inviting team members to ${chalk.bold(currentTeam.name)}`);
if (args.length > 0) {
for (const email of args) {
if (regexes.email.test(email)) {
const stopSpinner = wait(email);
const elapsed = stamp();
// eslint-disable-next-line no-await-in-loop
await teams.inviteUser({ teamId: currentTeam.id, email });
stopSpinner();
console.log(`${chalk.cyan(tick)} ${email} ${elapsed()}`);
} else {
console.log(`${chalk.red(`${email}`)} ${chalk.gray('[invalid]')}`);
}
}
return;
}
const inviteUserPrefix = rightPad('Invite User', 14);
const emails = [];
let hasError = false
let email;
do {
email = '';
try {
// eslint-disable-next-line no-await-in-loop
email = await textInput({
label: `- ${inviteUserPrefix}`,
validateValue: validateEmail,
autoComplete: value => emailAutoComplete(value, currentTeam.slug)
});
} catch (err) {
if (err.message !== 'USER_ABORT') {
throw err;
}
}
let elapsed;
let stopSpinner;
if (email) {
elapsed = stamp();
stopSpinner = wait(inviteUserPrefix + email);
try {
// eslint-disable-next-line no-await-in-loop
await teams.inviteUser({ teamId: currentTeam.id, email });
stopSpinner();
email = `${email} ${elapsed()}`;
emails.push(email);
console.log(`${chalk.cyan(tick)} ${inviteUserPrefix}${email}`);
if (hasError) {
hasError = false
eraseLines(emails.length + 2);
info(introMsg || `Inviting team members to ${chalk.bold(currentTeam.name)}`);
for (const email of emails) {
console.log(`${chalk.cyan(tick)} ${inviteUserPrefix}${email}`);
}
}
} catch (err) {
stopSpinner()
eraseLines(emails.length + 2);
error(err.message)
hasError = true
for (const email of emails) {
console.log(`${chalk.cyan(tick)} ${inviteUserPrefix}${email}`);
}
}
}
} while (email !== '');
eraseLines(emails.length + 2);
const n = emails.length;
if (emails.length === 0) {
info(noopMsg);
} else {
success(`Invited ${n} team mate${n > 1 ? 's' : ''}`);
for (const email of emails) {
console.log(`${chalk.cyan(tick)} ${inviteUserPrefix}${email}`);
}
}
};

126
bin/teams/switch.js

@ -0,0 +1,126 @@
const chalk = require('chalk');
const wait = require('../../lib/utils/output/wait');
const listInput = require('../../lib/utils/input/list');
const cfg = require('../../lib/cfg');
const exit = require('../../lib/utils/exit');
const success = require('../../lib/utils/output/success');
const info = require('../../lib/utils/output/info');
const error = require('../../lib/utils/output/error');
const param = require('../../lib/utils/output/param');
async function updateCurrentTeam({ cfg, newTeam } = {}) {
delete newTeam.created;
delete newTeam.creator_id;
await cfg.merge({ currentTeam: newTeam });
}
module.exports = async function(teams, args) {
let stopSpinner = wait('Fetching teams');
const list = (await teams.ls()).teams;
let { user, currentTeam } = await cfg.read();
const accountIsCurrent = !currentTeam;
stopSpinner();
if (accountIsCurrent) {
currentTeam = {
slug: user.username || user.email
};
}
if (args.length !== 0) {
const desiredSlug = args[0];
const newTeam = list.find(team => team.slug === desiredSlug);
if (newTeam) {
await updateCurrentTeam({ cfg, newTeam });
success(`The team ${chalk.bold(newTeam.name)} is now active!`);
return exit();
}
if (desiredSlug === user.username) {
stopSpinner = wait('Saving');
await cfg.remove('currentTeam');
stopSpinner();
return success(`Your account (${chalk.bold(desiredSlug)}) is now active!`);
}
error(`Could not find membership for team ${param(desiredSlug)}`);
return exit(1);
}
const choices = list.map(({ slug, name }) => {
name = `${slug} (${name})`;
if (slug === currentTeam.slug) {
name += ` ${chalk.bold('(current)')}`;
}
return {
name,
value: slug,
short: slug
};
});
const suffix = accountIsCurrent ? ` ${chalk.bold('(current)')}` : '';
const userEntryName = user.username ?
`${user.username} (${user.email})${suffix}` :
user.email
choices.unshift({
name: userEntryName,
value: user.email,
short: user.username
});
// Let's bring the current team to the beginning of the list
if (!accountIsCurrent) {
const index = choices.findIndex(
choice => choice.value === currentTeam.slug
);
const choice = choices.splice(index, 1)[0];
choices.unshift(choice);
}
let message;
if (currentTeam) {
message = `Switch to:`;
}
const choice = await listInput({
message,
choices,
separator: false
});
// Abort
if (!choice) {
info('No changes made');
return exit();
}
const newTeam = list.find(item => item.slug === choice);
// Switch to account
if (!newTeam) {
if (currentTeam.slug === user.username || currentTeam.slug === user.email) {
info('No changes made')
return exit()
}
stopSpinner = wait('Saving');
await cfg.remove('currentTeam');
stopSpinner();
return success(`Your account (${chalk.bold(choice)}) is now active!`);
}
if (newTeam.slug === currentTeam.slug) {
info('No changes made')
return exit();
}
stopSpinner = wait('Saving');
await updateCurrentTeam({ cfg, newTeam });
stopSpinner();
success(`The team ${chalk.bold(newTeam.name)} is now active!`);
};

202
lib/alias.js

@ -2,16 +2,27 @@
const { readFileSync } = require('fs');
const publicSuffixList = require('psl');
const minimist = require('minimist');
const ms = require('ms');
const chalk = require('chalk');
// Ours
const promptBool = require('../lib/utils/input/prompt-bool');
const info = require('../lib/utils/output/info');
const param = require('../lib/utils/output/param');
const wait = require('../lib/utils/output/wait');
const success = require('../lib/utils/output/success');
const uid = require('../lib/utils/output/uid');
const eraseLines = require('../lib/utils/output/erase-lines');
const stamp = require('../lib/utils/output/stamp');
const error = require('../lib/utils/output/error');
const treatBuyError = require('../lib/utils/domains/treat-buy-error');
const scaleInfo = require('./scale-info');
const { DOMAIN_VERIFICATION_ERROR } = require('./errors');
const isZeitWorld = require('./is-zeit-world');
const resolve4 = require('./dns');
const toHost = require('./to-host');
const exit = require('./utils/exit');
const copy = require('./copy');
const toHost = require('./to-host');
const resolve4 = require('./dns');
const isZeitWorld = require('./is-zeit-world');
const { DOMAIN_VERIFICATION_ERROR } = require('./errors');
const Now = require('./');
const argv = minimist(process.argv.slice(2), {
@ -97,8 +108,8 @@ module.exports = class Alias extends Now {
return depl;
}
async updatePathBasedroutes(alias, rules) {
alias = await this.maybeSetUpDomain(alias);
async updatePathBasedroutes(alias, rules, domains) {
alias = await this.maybeSetUpDomain(alias, domains);
return this.upsertPathAlias(alias, rules);
}
@ -179,7 +190,6 @@ module.exports = class Alias extends Now {
}
// Try again, but now having provisioned the certificate
return this.upsertPathAlias(alias, rules);
}
@ -216,7 +226,7 @@ module.exports = class Alias extends Now {
}
}
async set(deployment, alias) {
async set(deployment, alias, domains, currentTeam, user) {
const depl = await this.findDeployment(deployment);
if (!depl) {
const err = new Error(
@ -230,10 +240,13 @@ module.exports = class Alias extends Now {
if (aliasDepl && aliasDepl.rules) {
if (isTTY) {
try {
const msg = `> Path alias excists with ${aliasDepl.rules.length} rule${aliasDepl.rules.length > 1 ? 's' : ''}.\n` +
const msg =
`> Path alias exists 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`;
const confirmation = await promptBool(msg);
const confirmation = await promptBool(msg, {
trailing: '\n'
});
if (!confirmation) {
console.log('\n> Aborted');
@ -249,8 +262,70 @@ module.exports = class Alias extends Now {
}
}
alias = await this.maybeSetUpDomain(alias);
let aliasedDeployment = null;
let shouldScaleDown = false;
if (aliasDepl) {
aliasedDeployment = await this.findDeployment(aliasDepl.deploymentId);
if (
aliasedDeployment &&
aliasedDeployment.scale.current > depl.scale.current &&
aliasedDeployment.scale.current >= aliasedDeployment.scale.min &&
aliasedDeployment.scale.current <= aliasedDeployment.scale.max
) {
shouldScaleDown = true;
console.log(
`> Alias ${alias} points to ${chalk.bold(aliasedDeployment.url)} (${chalk.bold(aliasedDeployment.scale.current + ' instances')})`
);
console.log(
`> Scaling ${depl.url} to ${chalk.bold(aliasedDeployment.scale.current + ' instances')} atomically`
);
if (depl.scale.max < 1) {
if (this._debug) {
console.log(
'Updating max scale to 1 so that deployment may be unfrozen.'
);
}
await this.setScale(depl.uid, {
min: depl.scale.min,
max: Math.max(aliasedDeployment.scale.max, 1)
});
}
if (depl.scale.current < 1) {
if (this._debug) {
console.log(`> Deployment ${depl.url} is frozen, unfreezing...`);
}
await this.unfreeze(depl);
if (this._debug) {
console.log(
`> Deployment is now unfrozen, scaling it to match current instance count`
);
}
}
// Scale it to current limit
if (this._debug) {
console.log(`> Scaling deployment to match current scale.`);
}
await this.setScale(depl.uid, {
min: aliasedDeployment.scale.current,
max: aliasedDeployment.scale.current
});
await scaleInfo(this, depl.url);
if (this._debug) {
console.log(`> Updating scaling rules for deployment.`);
}
await this.setScale(depl.uid, {
min: aliasedDeployment.scale.min,
max: aliasedDeployment.scale.max
});
}
}
alias = await this.maybeSetUpDomain(alias, domains, currentTeam, user);
const aliasTime = Date.now();
const newAlias = await this.createAlias(depl, alias);
if (!newAlias) {
throw new Error(
@ -259,17 +334,13 @@ module.exports = class Alias extends Now {
}
const { created, uid } = newAlias;
if (created) {
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} now points to ${chalk.bold(depl.url)}! ${chalk.grey('[' + ms(Date.now() - aliasTime) + ']')}`;
if (isTTY && clipboard) {
let append;
try {
await copy(pretty);
append = '[copied to clipboard]';
await copy(depl.url);
} catch (err) {
append = '';
} finally {
console.log(`${output} ${append}`);
console.log(output);
}
} else {
console.log(output);
@ -279,6 +350,13 @@ module.exports = class Alias extends Now {
`${chalk.cyan('> Success!')} Alias already exists ${chalk.dim(`(${uid})`)}.`
);
}
if (aliasedDeployment && shouldScaleDown) {
const scaleDown = Date.now();
await this.setScale(aliasedDeployment.uid, { min: 0, max: 1 });
console.log(
`> Scaled ${chalk.gray(aliasedDeployment.url)} down to 1 instance ${chalk.gray('[' + ms(Date.now() - scaleDown) + ']')}`
);
}
}
createAlias(depl, alias) {
@ -402,11 +480,7 @@ module.exports = class Alias extends Now {
const res = await this._fetch(`/domains/${domain}/records`, {
method: 'POST',
body: {
type,
name: name === '' ? name : '*',
value: 'alias.zeit.co'
}
body: { type, name: name === '' ? name : '*', value: 'alias.zeit.co' }
});
if (this._debug) {
@ -427,7 +501,13 @@ module.exports = class Alias extends Now {
});
}
async maybeSetUpDomain(alias) {
async maybeSetUpDomain(alias, domains, currentTeam, user) {
const gracefulExit = () => {
this.close();
domains.close();
// eslint-disable-next-line unicorn/no-process-exit
process.exit();
};
// Make alias lowercase
alias = alias.toLowerCase();
@ -455,6 +535,78 @@ module.exports = class Alias extends Now {
console.log(
`> ${chalk.bold(chalk.underline(alias))} is a custom domain.`
);
let stopSpinner = wait('Fetching domain info');
let elapsed = stamp()
const parsed = publicSuffixList.parse(alias)
const pricePromise = domains.price(parsed.domain);
const canBePurchased = await domains.status(parsed.domain);
const aliasParam = param(parsed.domain);
stopSpinner();
if (canBePurchased) {
const price = await pricePromise;
info(
`The domain ${aliasParam} is ${chalk.bold('available for purchase')}! ${elapsed()}`
);
const confirmation = await promptBool(
`Buy now for ${chalk.bold(`$${price}`)} (${
chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)
})?`
)
eraseLines(1);
if (!confirmation) {
info('Aborted');
gracefulExit();
}
elapsed = stamp()
stopSpinner = wait('Purchasing');
let domain;
try {
domain = await domains.buy(alias);
} catch (err) {
stopSpinner();
treatBuyError(err);
gracefulExit();
}
stopSpinner();
success(`Domain purchased and created ${uid(domain.uid)} ${elapsed()}`);
stopSpinner = wait('Verifying nameservers');
let domainInfo;
try {
domainInfo = await this.setupDomain(alias);
} catch (err) {
if (this._debug) {
console.log(
'> [debug] Error while trying to setup the domain',
err
);
}
}
stopSpinner();
if (!domainInfo.verified) {
let { tld } = publicSuffixList.parse(alias);
tld = param(`.${tld}`);
error(
'The nameservers are pending propagation. Please try again shortly'
);
info(
`The ${tld} servers might take some extra time to reflect changes`
);
gracefulExit();
}
}
console.log(
`> Verifying the DNS settings for ${chalk.bold(chalk.underline(alias))} (see ${chalk.underline('https://zeit.world')} for help)`
);
@ -643,6 +795,6 @@ module.exports = class Alias extends Now {
return bail(err);
}
}
});
}, { retries: 5 });
}
};

73
lib/cfg.js

@ -4,6 +4,13 @@ const path = require('path');
// Packages
const fs = require('fs-promise');
const ms = require('ms');
// Ours
const { get: getUser } = require('./user');
// `8h` is arbitrarily used based on the average sleep time
const TTL = ms('8h');
let file = process.env.NOW_JSON
? path.resolve(process.env.NOW_JSON)
@ -13,13 +20,62 @@ function setConfigFile(nowjson) {
file = path.resolve(nowjson);
}
function read() {
function save(data) {
fs.writeFileSync(file, JSON.stringify(data, null, 2));
}
/**
* Reads the config file
*
* Optionally, always queries the API to get the user info even if the
* config file is not present
*
* @param {Boolean} force [false] Queries the API even if the config
* file is not present. If `true`, `token`
* *must* be specified
* @param {String} token Will be used to autenticate in the API
* if `force` is `true`
* @param {String} apiUrl URL of the API to be used
* @return {Object}
*/
async function read({ force = false, token, apiUrl } = {}) {
let existing = null;
try {
existing = fs.readFileSync(file, 'utf8');
existing = JSON.parse(existing);
} catch (err) {}
return existing || {};
if (!existing && force && token) {
const user = await getUser({ token, apiUrl });
if (user) {
return {
token,
user: {
uid: user.uid,
username: user.username,
email: user.email
}
};
}
return {};
}
if (!existing) {
return {};
}
if (!existing.lastUpdate || Date.now() - existing.lastUpdate > TTL) {
// TODO: update `teams` info
const token = existing.token;
const user = await getUser({ token });
if (user) {
existing.user = user;
existing.lastUpdate = Date.now();
save(existing);
}
}
return existing;
}
/**
@ -29,14 +85,21 @@ function read() {
* (atomic)
* @param {Object} data
*/
async function merge(data) {
const cfg = Object.assign({}, await read(), data);
save(cfg);
}
function merge(data) {
const cfg = Object.assign({}, read(), data);
// Removes a key from the config and store the result
async function remove(key) {
const cfg = await read();
delete cfg[key];
fs.writeFileSync(file, JSON.stringify(cfg, null, 2));
}
module.exports = {
setConfigFile,
read,
merge
merge,
remove
};

18
lib/credit-cards.js

@ -4,14 +4,18 @@ const Now = require('../lib');
module.exports = class CreditCards extends Now {
async ls() {
const res = await this._fetch('/www/user/cards');
const res = await this._fetch('/cards');
const body = await res.json();
if (res.status !== 200) {
const e = new Error(body.error.message)
e.code = body.error.code
throw e
}
return body;
}
async setDefault(cardId) {
await this._fetch('/www/user/cards/default', {
await this._fetch('/cards/default', {
method: 'PUT',
body: { cardId }
});
@ -19,7 +23,7 @@ module.exports = class CreditCards extends Now {
}
async rm(cardId) {
await this._fetch(`/www/user/cards/${encodeURIComponent(cardId)}`, {
await this._fetch(`/cards/${encodeURIComponent(cardId)}`, {
method: 'DELETE'
});
return true;
@ -45,16 +49,16 @@ module.exports = class CreditCards extends Now {
try {
const stripeToken = (await stripe.tokens.create({ card })).id;
const res = await this._fetch('/www/user/cards', {
const res = await this._fetch('/cards', {
method: 'POST',
body: { stripeToken }
});
const body = await res.json();
if (body.card && body.card.id) {
if (body && body.id) {
resolve({
last4: body.card.last4
last4: body.last4
});
} else if (body.error && body.error.message) {
reject(new Error(body.error.message));

92
lib/domains.js

@ -1,8 +1,12 @@
// Native
const { encode: encodeQuery } = require('querystring');
// Packages
const chalk = require('chalk');
// Ours
const Now = require('../lib');
const cfg = require('../lib/cfg');
const isZeitWorld = require('./is-zeit-world');
const { DNS_VERIFICATION_ERROR } = require('./errors');
@ -78,4 +82,92 @@ module.exports = class Domains extends Now {
err3.userError = true;
throw err3;
}
async status(name) {
if (!name) {
throw new Error('`domain` is not defined');
}
const query = encodeQuery({ name });
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} GET /domains/status?${query}`);
}
const res = await this._fetch(`/domains/status?${query}`);
const json = await res.json();
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} GET /domains/status?${query}`);
}
return json.available;
});
}
async price(name) {
if (!name) {
throw new Error('`domain` is not defined');
}
const query = encodeQuery({ name });
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} GET /domains/price?${query}`);
}
const res = await this._fetch(`/domains/price?${query}`);
const json = await res.json();
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} GET /domains/price?${query}`);
}
return json.price;
});
}
async buy(name) {
const { token } = await cfg.read();
if (!name) {
throw new Error('`name` is not defined');
}
const rawBody = { name };
if (name.startsWith('test')) {
rawBody.dev = true;
}
const body = JSON.stringify(rawBody);
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} GET /domains/buy`);
}
const res = await this._fetch(`/domains/buy`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`
},
body
});
const json = await res.json();
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} GET /domains/buy`);
}
if ([400, 403, 500, 503].includes(res.status)) {
const e = new Error();
e.code = json.error.code;
e.message = json.error.message
return bail(e);
}
return json;
});
}
};

242
lib/index.js

@ -2,8 +2,11 @@
const { homedir } = require('os');
const { resolve: resolvePath, join: joinPaths } = require('path');
const EventEmitter = require('events');
const qs = require('querystring');
const {parse: parseUrl} = require('url')
// Packages
const fetch = require('node-fetch');
const bytes = require('bytes');
const chalk = require('chalk');
const resumer = require('resumer');
@ -18,6 +21,7 @@ const ua = require('./ua');
const hash = require('./hash');
const Agent = require('./agent');
const readMetaData = require('./read-metadata');
const toHost = require('./to-host');
// How many concurrent HTTP/2 stream uploads
const MAX_CONCURRENT = 10;
@ -27,13 +31,14 @@ const IS_WIN = process.platform.startsWith('win');
const SEP = IS_WIN ? '\\' : '/';
module.exports = class Now extends EventEmitter {
constructor(url, token, { forceNew = false, debug = false }) {
constructor({apiUrl, token, currentTeam, forceNew = false, debug = false }) {
super();
this._token = token;
this._debug = debug;
this._forceNew = forceNew;
this._agent = new Agent(url, { debug });
this._agent = new Agent(apiUrl, { debug });
this._onRetry = this._onRetry.bind(this);
this.currentTeam = currentTeam
}
async create(
@ -345,15 +350,16 @@ module.exports = class Now extends EventEmitter {
);
}
return bail(responseError(res));
return bail(await responseError(res));
}
this.emit('upload', file);
},
{ retries: 3, randomize: true, onRetry: this._onRetry }
))
)
)
)
.then(() => parts.length ? uploadChunk() : this.emit('complete'))
.then(() => (parts.length ? uploadChunk() : this.emit('complete')))
.catch(err => this.emit('error', err));
};
@ -401,7 +407,7 @@ module.exports = class Now extends EventEmitter {
}
if (res.status !== 200) {
throw new Error('Fetching deployment list failed');
throw new Error('Fetching deployment url failed');
}
return res.json();
@ -412,6 +418,129 @@ module.exports = class Now extends EventEmitter {
return deployments;
}
async listInstances(deploymentId) {
const { instances } = await this.retry(
async bail => {
if (this._debug) {
console.time(`> [debug] /deployments/${deploymentId}/instances`);
}
const res = await this._fetch(
`/now/deployments/${deploymentId}/instances`
);
if (this._debug) {
console.timeEnd(`> [debug] /deployments/${deploymentId}/instances`);
}
// No retry on 4xx
if (res.status >= 400 && res.status < 500) {
if (this._debug) {
console.log('> [debug] bailing on listing due to %s', res.status);
}
return bail(responseError(res));
}
if (res.status !== 200) {
throw new Error('Fetching instances list failed');
}
return res.json();
},
{ retries: 3, minTimeout: 2500, onRetry: this._onRetry }
);
return instances;
}
async findDeployment(deployment) {
const list = await this.list();
let key;
let val;
if (/\./.test(deployment)) {
val = toHost(deployment);
key = 'url';
} else {
val = deployment;
key = 'uid';
}
const depl = list.find(d => {
if (d[key] === val) {
if (this._debug) {
console.log(`> [debug] matched deployment ${d.uid} by ${key} ${val}`);
}
return true;
}
// Match prefix
if (`${val}.now.sh` === d.url) {
if (this._debug) {
console.log(`> [debug] matched deployment ${d.uid} by url ${d.url}`);
}
return true;
}
return false;
});
return depl;
}
async logs(deploymentIdOrURL, { types, limit, query, since, until } = {}) {
const q = qs.stringify({
types: types.join(','),
limit,
q: query,
since,
until
});
const { logs } = await this.retry(
async bail => {
if (this._debug) {
console.time('> [debug] /logs');
}
const url = `/now/deployments/${encodeURIComponent(deploymentIdOrURL)}/logs?${q}`;
const res = await this._fetch(url);
if (this._debug) {
console.timeEnd('> [debug] /logs');
}
// No retry on 4xx
if (res.status >= 400 && res.status < 500) {
if (this._debug) {
console.log(
'> [debug] bailing on printing logs due to %s',
res.status
);
}
return bail(await responseError(res));
}
if (res.status !== 200) {
throw new Error('Fetching deployment logs failed');
}
return res.json();
},
{
retries: 3,
minTimeout: 2500,
onRetry: this._onRetry
}
);
return logs;
}
async listAliases(deploymentId) {
return this.retry(async () => {
const res = await this._fetch(
@ -577,13 +706,13 @@ module.exports = class Now extends EventEmitter {
body.error &&
body.error.code === 'verification_failed'
) {
const err = new Error(body.error.message);
err.userError = true;
return bail(err);
throw new Error(body.error.message);
} else if (res.status !== 200) {
throw new Error(body.error.message);
}
if (!body.verified) throw new Error('verification failed, retrying')
return body;
});
}
@ -689,7 +818,7 @@ module.exports = class Now extends EventEmitter {
if (this._debug) {
console.log('> [debug] bailing on removal due to %s', res.status);
}
return bail(responseError(res));
return bail(await responseError(res));
}
if (res.status !== 200) {
@ -740,11 +869,79 @@ module.exports = class Now extends EventEmitter {
}
_fetch(_url, opts = {}) {
if (!opts.useCurrentTeam && this.currentTeam) {
const parsedUrl = parseUrl(_url, true)
const query = parsedUrl.query
query.teamId = this.currentTeam.id
_url = `${parsedUrl.pathname}?${qs.encode(query)}`
delete opts.useCurrentTeam
}
opts.headers = opts.headers || {};
opts.headers.authorization = `Bearer ${this._token}`;
opts.headers['user-agent'] = ua;
return this._agent.fetch(_url, opts);
}
setScale(nameOrId, scale) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(
`> [debug] #${attempt} POST /deployments/${nameOrId}/instances`
);
}
const res = await this._fetch(`/now/deployments/${nameOrId}/instances`, {
method: 'POST',
body: scale
});
if (this._debug) {
console.timeEnd(
`> [debug] #${attempt} POST /deployments/${nameOrId}/instances`
);
}
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
const body = await res.json();
if (res.status !== 200) {
if (res.status === 404 || res.status === 400) {
const err = new Error(body.error.message);
err.userError = true;
return bail(err);
}
if (body.error && body.error.message) {
const err = new Error(body.error.message);
err.userError = true;
return bail(err);
}
throw new Error(`Error occurred while scaling. Please try again later`);
}
return body;
});
}
async unfreeze(depl) {
return this.retry(async bail => {
const res = await fetch(`https://${depl.url}`);
if ([500, 502, 503].includes(res.status)) {
const err = new Error('Unfreeze failed. Try again later.');
bail(err);
}
});
}
async getPlanMax() {
return 10;
}
};
function toRelative(path, base) {
@ -758,12 +955,33 @@ function toRelative(path, base) {
return relative.replace(/\\/g, '/');
}
function responseError(res) {
const err = new Error('Response error');
async function responseError(res) {
let message;
let userError;
if (res.status >= 400 && res.status < 500) {
let body;
try {
body = await res.json();
} catch (err) {
body = {};
}
// Some APIs wrongly return `err` instead of `error`
message = (body.error || body.err || {}).message;
userError = true;
} else {
userError = false;
}
const err = new Error(message || 'Response error');
err.status = res.status;
err.userError = userError;
if (res.status === 429) {
const retryAfter = res.headers.get('Retry-After');
if (retryAfter) {
err.retryAfter = parseInt(retryAfter, 10);
}

39
lib/login.js

@ -13,8 +13,6 @@ const ora = require('ora');
const pkg = require('./pkg');
const ua = require('./ua');
const cfg = require('./cfg');
const info = require('./utils/output/info');
const promptBool = require('./utils/input/prompt-bool');
async function getVerificationData(url, email) {
const tokenName = `Now CLI ${os.platform()}-${os.arch()} ${pkg.version} (${os.hostname()})`;
@ -70,14 +68,6 @@ async function register(url, { retryEmail = false } = {}) {
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.')}`
);
if (!await promptBool('Continue?')) {
info('Aborted.');
process.exit(); // eslint-disable-line unicorn/no-process-exit
}
if (!validate(email)) {
return register(url, { retryEmail: true });
}
@ -96,8 +86,7 @@ async function register(url, { retryEmail = false } = {}) {
process.stdout.write('\n');
const spinner = ora({
text: 'Waiting for confirmation...',
color: 'black'
text: 'Waiting for confirmation...'
}).start();
let final;
@ -112,16 +101,38 @@ async function register(url, { retryEmail = false } = {}) {
} while (!final);
/* eslint-enable no-await-in-loop */
let user;
try {
user = (await (await fetch(`${url}/www/user`, {
headers: {
Authorization: `Bearer ${final}`
}
})).json()).user;
} catch (err) {
spinner.stop();
throw new Error(`Couldn't retrieve user details ${err.message}`);
}
spinner.text = 'Confirmed email address!';
spinner.stopAndPersist('✔');
process.stdout.write('\n');
return { email, token: final };
return {
token: final,
user: {
uid: user.uid,
username: user.username,
email: user.email
},
lastUpdate: Date.now()
};
}
module.exports = async function(url) {
const loginData = await register(url);
cfg.merge(loginData);
await cfg.merge(loginData);
await cfg.remove('currentTeam') // Make sure to logout the team too
await cfg.remove('email') // Remove old schema from previus versions
return loginData.token;
};

2
lib/logs.js

@ -6,7 +6,7 @@ exports.compare = function(a, b) {
exports.deserialize = function(log) {
return Object.assign({}, log, {
data: new Date(log.date),
date: new Date(log.date),
created: new Date(log.created)
});
};

44
lib/plans.js

@ -2,46 +2,56 @@ const ms = require('ms');
const Now = require('../lib');
async function parsePlan(res) {
async function parsePlan(json) {
const { subscription } = json
let id;
let until;
const { subscription } = await res.json();
let name
if (subscription) {
id = subscription.plan.id;
if (subscription.cancel_at_period_end) {
until = ms(
new Date(subscription.current_period_end * 1000) - new Date(),
{ long: true }
);
const planItems = subscription.items.data;
const mainPlan = planItems.find(d => d.plan.metadata.is_main_plan === '1');
if (mainPlan) {
id = mainPlan.plan.id
name = mainPlan.plan.name
if (subscription.cancel_at_period_end) {
until = ms(
new Date(subscription.current_period_end * 1000) - new Date(),
{ long: true }
);
}
} else {
id = 'oss'
}
} else {
id = 'oss';
}
return { id, until };
return { id, name, until };
}
module.exports = class Plans extends Now {
async getCurrent() {
const res = await this._fetch('/www/user/plan');
return parsePlan(res);
const res = await this._fetch('/plan');
const json = await res.json()
return parsePlan(json);
}
async set(plan) {
const res = await this._fetch('/www/user/plan', {
const res = await this._fetch('/plan', {
method: 'PUT',
body: { plan }
});
const json = await res.json()
if (res.ok) {
return parsePlan(res);
return parsePlan(json);
}
const err = new Error(res.statusText);
err.res = res;
const err = new Error(json.error.message);
err.code = json.error.code
throw err;
}
};

6
lib/re-alias.js

@ -10,8 +10,8 @@ const { error } = require('./error');
const readMetaData = require('./read-metadata');
const NowAlias = require('./alias');
exports.assignAlias = async (autoAlias, token, deployment, apiUrl, debug) => {
const aliases = new NowAlias(apiUrl, token, { debug });
exports.assignAlias = async (autoAlias, token, deployment, apiUrl, debug, currentTeam) => {
const aliases = new NowAlias({apiUrl, token, debug, currentTeam });
console.log(
`> Assigning alias ${chalk.bold.underline(autoAlias)} to deployment...`
);
@ -83,7 +83,7 @@ exports.reAlias = async (token, host, help, exit, apiUrl, debug, alias) => {
const assignments = [];
for (const pointer of pointers) {
assignments.push(exports.assignAlias(pointer, token, host, apiUrl, debug));
assignments.push(exports.assignAlias(pointer, token, host, apiUrl, debug, alias.currentTeam));
}
await Promise.all(assignments);

72
lib/scale-info.js

@ -0,0 +1,72 @@
const linelog = require('single-line-log').stdout;
const range = require('lodash.range');
const ms = require('ms');
const chalk = require('chalk');
const retry = require('async-retry');
function barify(cur, tot) {
return (
'[' +
range(0, cur).map(() => '=').join('') +
range(cur, tot).map(() => '-').join('') +
']'
);
}
module.exports = async function(now, url) {
const match = await now.findDeployment(url);
const { min, max, current } = match.scale;
let targetReplicaCount = min;
if (current < min) {
targetReplicaCount = min;
} else if (current > max) {
targetReplicaCount = max;
} else {
console.log(`> Nothing to do, already scaled.`);
return;
}
if (targetReplicaCount === 0) {
console.log(`> Scaled to 0 instances`);
return;
}
const startTime = Date.now();
let barcurr = current;
const end = Math.max(current, max);
linelog(
`> Scaling to ${chalk.bold(String(targetReplicaCount) + (targetReplicaCount === 1 ? ' instance' : ' instances'))}: ` +
barify(barcurr, end)
);
const instances = await retry(
async () => {
const res = await now.listInstances(match.uid);
if (barcurr !== res.length) {
barcurr = res.length;
linelog(
`> Scaling to ${chalk.bold(String(targetReplicaCount) + (targetReplicaCount === 1 ? ' instance' : ' instances'))}: ` +
barify(barcurr, end)
);
if (barcurr === targetReplicaCount) {
linelog.clear();
linelog(
`> Scaled to ${chalk.bold(String(targetReplicaCount) + (targetReplicaCount === 1 ? ' instance' : ' instances'))}: ${chalk.gray('[' + ms(Date.now() - startTime) + ']')}\n`
);
return res;
}
}
throw new Error('Not ready yet');
},
{ retries: 5000, minTimeout: 10, maxTimeout: 20 }
);
process.nextTick(() => {
instances.forEach(inst => {
console.log(` - ${chalk.underline(inst.url)}`);
});
});
};

43
lib/scale.js

@ -0,0 +1,43 @@
// Ours
const Now = require('../lib');
module.exports = class Scale extends Now {
getInstances(id) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(
`> [debug] #${attempt} GET /deployments/${id}/instances`
);
}
const res = await this._fetch(`/now/deployments/${id}/instances`, {
method: 'GET'
});
if (this._debug) {
console.timeEnd(
`> [debug] #${attempt} GET /deployments/${id}/instances`
);
}
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
const body = await res.json();
if (res.status !== 200) {
if (res.status === 404 || res.status === 400) {
const err = new Error(body.error.message);
err.userError = true;
return bail(err);
}
throw new Error(body.error.message);
}
return body;
});
}
};

141
lib/teams.js

@ -0,0 +1,141 @@
const Now = require('../lib');
module.exports = class Teams extends Now {
async create({ slug }) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} POST /teams}`);
}
const res = await this._fetch(`/teams`, {
method: 'POST',
body: {
slug
}
});
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} POST /teams`);
}
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
const body = await res.json();
if (res.status === 400) {
const e = new Error(body.error.message)
e.code = body.error.code
return bail(e)
} else if (res.status !== 200) {
const e = new Error(body.error.message)
e.code = body.error.code
throw e
}
return body
});
}
async edit({ id, slug, name }) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} PATCH /teams/${id}}`);
}
const payload = {}
if (name) {
payload.name = name
}
if (slug) {
payload.slug = slug
}
const res = await this._fetch(`/teams/${id}`, {
method: 'PATCH',
body: payload
});
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} PATCH /teams/${id}`);
}
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
const body = await res.json();
if (res.status === 400) {
const e = new Error(body.error.message)
e.code = body.error.code
return bail(e)
} else if (res.status !== 200) {
const e = new Error(body.error.message)
e.code = body.error.code
throw e
}
return body
});
}
async inviteUser({teamId, email}) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} POST /teams/${teamId}/members}`);
}
const res = await this._fetch(`/teams/${teamId}/members`, {
method: 'POST',
body: {
email
}
});
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} POST /teams/${teamId}/members}`);
}
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
const body = await res.json();
if (res.status === 400) {
const e = new Error(body.error.message)
e.code = body.error.code
return bail(e)
} else if (res.status !== 200) {
const e = new Error(body.error.message)
e.code = body.error.code
throw e
}
return body
});
}
async ls() {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} GET /teams}`);
}
const res = await this._fetch(`/teams`);
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} GET /teams`);
}
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
return res.json();
});
}
};

0
lib/unfreeze.js

54
lib/user.js

@ -0,0 +1,54 @@
const _fetch = require('node-fetch');
function _filter(data) {
data = data.user;
return {
userId: data.uid,
username: data.username,
email: data.email
};
}
/**
* Gets all the info we have about an user
*
* @param {Object} fetch Optionally, _our_ `fetch` can be passed here
* @param {String} token Only necessary if `fetch` is undefined
* @param {String} apiUrl Only necessary if `fetch` is undefined
* @param {Boolean} filter If `true`, the `filter` used will to the data
* before returning
* @return {Object}
*/
async function get(
{ fetch, token, apiUrl = 'https://api.zeit.co', filter = true } = {}
) {
let headers = {};
const endpoint = '/www/user';
const url = fetch ? endpoint : apiUrl + endpoint;
if (!fetch) {
headers = {
Authorization: `Bearer ${token}`
};
fetch = _fetch;
}
try {
const res = await fetch(url, { headers });
const json = await res.json();
if (filter) {
return _filter(json);
}
return json;
} catch (err) {
return {};
}
}
module.exports = {
get,
filter: _filter
};

25
lib/utils/domains/treat-buy-error.js

@ -0,0 +1,25 @@
const error = require('../output/error');
module.exports = function (err) {
switch (err.code) {
case 'invalid_domain': {
error('Invalid domain')
break
}
case 'not_available': {
error('Domain can\'t be purchased at this time')
break
}
case 'service_unavailabe': {
error('Purchase failed – Service unavailable')
break
}
case 'unexpected_error': {
error('Purchase failed – Unexpected error')
break
}
default: {
error(err.message)
}
}
}

7
lib/utils/fatal-error.js

@ -0,0 +1,7 @@
const error = require('./output/error');
const exit = require('./exit');
module.exports = (msg, code = 1) => {
error(msg);
exit(code);
};

3
lib/utils/input/regexes.js

@ -0,0 +1,3 @@
module.exports = {
email: /.+@.+\..+$/
};

50
lib/utils/input/text.js

@ -3,6 +3,8 @@ const ansiRegex = require('ansi-regex');
const chalk = require('chalk');
const stripAnsi = require('strip-ansi');
const eraseLines = require('../output/erase-lines');
const ESCAPES = {
LEFT: '\u001B[D',
RIGHT: '\u001B[C',
@ -16,34 +18,39 @@ module.exports = function(
{
label = '',
initialValue = '',
// If false, the `- label` will be printed as `✖ label` in red
// Until the first keypress
valid = true,
// Can be:
// - `false`, which does nothing
// - `cc`, for credit cards
// - `date`, for dates in the mm / yyyy format
mask = false,
placeholder = '',
abortSequences = new Set(['\u0003']),
abortSequences = new Set(['\x03']),
eraseSequences = new Set([ESCAPES.BACKSPACE, ESCAPES.CTRL_H]),
resolveChars = new Set([ESCAPES.CARRIAGE]),
stdin = process.stdin,
stdout = process.stdout,
// Char to print before resolving/rejecting the promise
// if `false`, nothing will be printed
// If `false`, nothing will be printed
trailing = ansiEscapes.eraseLines(1),
// Gets called on each keypress;
// `data` contains the current keypress;
// `futureValue` contains the current value + the
// keypress in the correct place
// Keypress in the correct place
validateKeypress = (data, futureValue) => true, // eslint-disable-line no-unused-vars
// get's called before the promise is resolved
// returning `false` here will prevent the user from submiting the value
// Get's called before the promise is resolved
// Returning `false` here will prevent the user from submiting the value
validateValue = data => true, // eslint-disable-line no-unused-vars
// receives the value of the input and should return a string
// or false if no autocomplion is available
// Receives the value of the input and should return a string
// Or false if no autocomplion is available
autoComplete = value => false, // eslint-disable-line no-unused-vars
// tab
// right arrow
autoCompleteChars = new Set(['\t', '\u001B[C'])
// Tab
// Right arrow
autoCompleteChars = new Set(['\t', '\x1b[C']),
// If true, converts everything the user types to to lowercase
forceLowerCase = false
} = {}
) {
return new Promise((resolve, reject) => {
@ -54,9 +61,15 @@ module.exports = function(
let regex;
let suggestion = '';
stdout.write(label);
if (valid) {
stdout.write(label);
} else {
const _label = label.replace('-', '✖');
stdout.write(chalk.red(_label));
}
value = initialValue;
stdout.write(initialValue);
if (mask) {
if (!value) {
value = chalk.gray(placeholder);
@ -96,7 +109,7 @@ module.exports = function(
}
async function onData(buffer) {
const data = buffer.toString();
let data = buffer.toString();
value = stripAnsi(value);
if (abortSequences.has(data)) {
@ -104,6 +117,10 @@ module.exports = function(
return reject(new Error('USER_ABORT'));
}
if (forceLowerCase) {
data = data.toLowerCase();
}
if (suggestion !== '' && !caretOffset && autoCompleteChars.has(data)) {
value += stripAnsi(suggestion);
suggestion = '';
@ -150,10 +167,10 @@ module.exports = function(
} else if (mask === 'expDate') {
value = value.replace(regex, chalk.gray('$1'));
}
const l = chalk.red(label.replace('-', '✖'));
stdout.write(
ansiEscapes.eraseLines(1) + l + value + ansiEscapes.beep
);
eraseLines(1);
stdout.write(l + value + ansiEscapes.beep);
if (caretOffset) {
process.stdout.write(
ansiEscapes.cursorBackward(Math.abs(caretOffset))
@ -212,7 +229,8 @@ module.exports = function(
value = value.replace(regex, chalk.gray('$1'));
}
stdout.write(ansiEscapes.eraseLines(1) + label + value + suggestion);
eraseLines(1);
stdout.write(label + value + suggestion);
if (caretOffset) {
process.stdout.write(ansiEscapes.cursorBackward(Math.abs(caretOffset)));
}

3
lib/utils/output/chars.js

@ -0,0 +1,3 @@
module.exports = {
tick: '✓'
};

3
lib/utils/output/erase-lines.js

@ -0,0 +1,3 @@
const ansiEscapes = require('ansi-escapes');
module.exports = n => process.stdout.write(ansiEscapes.eraseLines(n));

6
lib/utils/output/note.js

@ -0,0 +1,6 @@
const chalk = require('chalk');
// Prints a note
module.exports = msg => {
console.log(`${chalk.yellow('> NOTE:')} ${msg}`);
};

4
lib/utils/output/right-pad.js

@ -0,0 +1,4 @@
module.exports = (string, n = 0) => {
n -= string.length;
return string + ' '.repeat(n > -1 ? n : 0);
};

23
lib/utils/url.js

@ -0,0 +1,23 @@
const isURL = require('is-url')
exports.maybeURL = id => {
// E.g, "appname-asdf"
return id.includes('-')
}
exports.normalizeURL = u => {
// Normalize URL by removing slash from the end
if (isURL(u) && u.slice(-1) === '/') {
u = u.slice(0, -1)
}
// `url` should match the hostname of the deployment
u = u.replace(/^https:\/\//i, '')
if (!u.includes('.')) {
// `.now.sh` domain is implied if just the subdomain is given
u += '.now.sh'
}
return u
}

12
package.json

@ -1,6 +1,6 @@
{
"name": "now",
"version": "4.11.2",
"version": "5.0.0",
"description": "The command line interface for Now",
"repository": "zeit/now-cli",
"license": "MIT",
@ -8,7 +8,7 @@
"build"
],
"scripts": {
"precommit": "lint-staged",
"precommit": "xo --quiet && lint-staged",
"lint": "xo",
"test": "npm run build && npm run lint && ava",
"prepublish": "npm run build",
@ -60,29 +60,35 @@
"copy-paste": "1.3.0",
"credit-card": "3.0.1",
"cross-spawn": "5.1.0",
"dateformat": "2.0.0",
"docker-file-parser": "1.0.1",
"dotenv": "4.0.0",
"download": "5.0.3",
"email-prompt": "0.2.0",
"email-validator": "1.0.7",
"epipebomb": "1.0.0",
"fs-promise": "2.0.2",
"glob": "7.1.1",
"ignore": "3.2.7",
"ini": "1.3.4",
"inquirer": "3.0.6",
"is-url": "1.2.2",
"lodash.range": "3.2.0",
"minimist": "1.2.0",
"ms": "1.0.0",
"node-fetch": "1.6.3",
"opn": "4.0.2",
"ora": "1.2.0",
"progress": "2.0.0",
"printf": "0.2.5",
"progress": "^2.0.0",
"psl": "1.1.18",
"resumer": "0.0.0",
"single-line-log": "1.1.2",
"socket.io-client": "1.7.3",
"split-array": "1.0.1",
"strip-ansi": "3.0.1",
"stripe": "4.17.1",
"supports-color": "3.2.3",
"text-table": "0.2.0",
"tmp-promise": "1.0.3",
"update-notifier": "2.1.0"

Loading…
Cancel
Save