Browse Source
* 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 argumentmaster
Matheus Fernandes
8 years ago
committed by
GitHub
44 changed files with 2921 additions and 519 deletions
@ -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')}` |
|||
); |
|||
}; |
@ -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); |
|||
} |
@ -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); |
|||
}); |
@ -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); |
|||
} |
|||
} |
|||
} |
@ -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(); |
|||
}; |
@ -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}`); |
|||
} |
|||
} |
|||
}; |
@ -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!`); |
|||
}; |
@ -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)}`); |
|||
}); |
|||
}); |
|||
}; |
@ -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; |
|||
}); |
|||
} |
|||
}; |
@ -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,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 |
|||
}; |
@ -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) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
const error = require('./output/error'); |
|||
const exit = require('./exit'); |
|||
|
|||
module.exports = (msg, code = 1) => { |
|||
error(msg); |
|||
exit(code); |
|||
}; |
@ -0,0 +1,3 @@ |
|||
module.exports = { |
|||
email: /.+@.+\..+$/ |
|||
}; |
@ -0,0 +1,3 @@ |
|||
module.exports = { |
|||
tick: '✓' |
|||
}; |
@ -0,0 +1,3 @@ |
|||
const ansiEscapes = require('ansi-escapes'); |
|||
|
|||
module.exports = n => process.stdout.write(ansiEscapes.eraseLines(n)); |
@ -0,0 +1,6 @@ |
|||
const chalk = require('chalk'); |
|||
|
|||
// Prints a note
|
|||
module.exports = msg => { |
|||
console.log(`${chalk.yellow('> NOTE:')} ${msg}`); |
|||
}; |
@ -0,0 +1,4 @@ |
|||
module.exports = (string, n = 0) => { |
|||
n -= string.length; |
|||
return string + ' '.repeat(n > -1 ? n : 0); |
|||
}; |
@ -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 |
|||
} |
Loading…
Reference in new issue