From ac8256fcb8d8f93acf235e0cf01ada01c3e6178f Mon Sep 17 00:00:00 2001 From: nkzawa Date: Thu, 14 Apr 2016 14:20:01 +0900 Subject: [PATCH 1/6] bin/now -> bin/now-deploy --- bin/{now => now-deploy} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename bin/{now => now-deploy} (97%) diff --git a/bin/now b/bin/now-deploy similarity index 97% rename from bin/now rename to bin/now-deploy index fb4fb4e..d69ddea 100755 --- a/bin/now +++ b/bin/now-deploy @@ -16,7 +16,12 @@ import ms from 'ms'; const argv = minimist(process.argv.slice(2)); const help = () => { console.log(` - 𝚫 now [options] + 𝚫 now [options] + + Commands: + + list output list of instances + ls alias of list Options: From 1577f471deefef59887e8a43daec392aa733d982 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Thu, 14 Apr 2016 14:22:31 +0900 Subject: [PATCH 2/6] add base command --- bin/now | 38 ++++++++++++++++++++++++++++++++++++++ bin/now-list | 3 +++ 2 files changed, 41 insertions(+) create mode 100755 bin/now create mode 100755 bin/now-list diff --git a/bin/now b/bin/now new file mode 100755 index 0000000..ed777ff --- /dev/null +++ b/bin/now @@ -0,0 +1,38 @@ +#!/usr/bin/env node +import minimist from 'minimist'; +import { resolve } from 'path'; +import { spawn } from 'child_process'; +import checkUpdate from '../lib/check-update'; + +const argv = minimist(process.argv.slice(2)); + +// options +const debug = argv.debug || argv.d; + +// auto-update checking +const update = checkUpdate({ debug }); +const exit = (code) => { + update.then(() => process.exit(code)); + // don't wait for updates more than a second + // when the process really wants to exit + setTimeout(() => process.exit(code), 1000); +}; + +const commands = new Set(['deploy', 'list', 'ls']); +const aliases = new Map([['ls', 'list']]); + +let cmd = argv._[0]; +let args; + +if (commands.has(cmd)) { + cmd = aliases.get(cmd) || cmd; + args = process.argv.slice(3); +} else { + cmd = 'deploy'; + args = process.argv.slice(2); +} + +const bin = resolve(__dirname, 'now-' + cmd); +const proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] }); +proc.on('close', (code) => exit(code)); +proc.on('error', () => exit(1)); diff --git a/bin/now-list b/bin/now-list new file mode 100755 index 0000000..0aa9f4d --- /dev/null +++ b/bin/now-list @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +console.log('!list', process.argv); From 0bd3127687c667ec85604816f29db8692e759f0f Mon Sep 17 00:00:00 2001 From: nkzawa Date: Thu, 14 Apr 2016 14:24:30 +0900 Subject: [PATCH 3/6] now-deploy: don't check update --- bin/now-deploy | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/bin/now-deploy b/bin/now-deploy index d69ddea..331056e 100755 --- a/bin/now-deploy +++ b/bin/now-deploy @@ -5,7 +5,6 @@ import { resolve } from 'path'; import login from '../lib/login'; import * as cfg from '../lib/cfg'; import { version } from '../../package'; -import checkUpdate from '../lib/check-update'; import Logger from '../lib/build-logger'; import bytes from 'bytes'; import chalk from 'chalk'; @@ -51,43 +50,35 @@ const force = argv.f || argv.force; const forceSync = argv.F || argv.forceSync; const shouldLogin = argv.L || argv.login; -// auto-update checking const config = cfg.read(); -const update = checkUpdate({ debug }); -const exit = (code) => { - update.then(() => process.exit(code)); - // don't wait for updates more than a second - // when the process really wants to exit - setTimeout(() => process.exit(code), 1000); -}; if (argv.h || argv.help) { help(); - exit(0); + process.exit(0); } else if (argv.v || argv.version) { console.log(chalk.bold('𝚫 now'), version); - exit(0); + process.exit(0); } else if (!config.token || shouldLogin) { login() .then((token) => { if (shouldLogin) { console.log('> Logged in successfully. Token saved in ~/.now.json'); - exit(0); + process.exit(0); } else { sync(token).catch((err) => { error(`Unknown error: ${err.stack}`); - exit(1); + process.exit(1); }); } }) .catch((e) => { error(`Authentication error – ${e.message}`); - exit(1); + process.exit(1); }); } else { sync(config.token).catch((err) => { error(`Unknown error: ${err.stack}`); - exit(1); + process.exit(1); }); } @@ -154,7 +145,7 @@ async function sync (token) { now.on('error', (err) => { error('Upload failed'); handleError(err); - exit(1); + process.exit(1); }); } else { console.log('> Sync complete (cached)'); @@ -172,11 +163,11 @@ function printLogs (host) { const logger = new Logger(host); logger.on('error', () => { console.log('> Connection error.'); - exit(1); + process.exit(1); }); logger.on('close', () => { console.log(`${chalk.cyan('> Deployment complete!')}`); - exit(0); + process.exit(0); }); } @@ -198,7 +189,7 @@ function handleError (err) { } else { error(`Unexpected error. Please try later. (${err.message})`); } - exit(1); + process.exit(1); } function error (err) { From 7903aa69a6f22e8c6077a650543d399479f91230 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Sat, 16 Apr 2016 01:23:37 +0900 Subject: [PATCH 4/6] add url option for development --- bin/now-deploy | 33 +++++---------------------------- bin/now-list | 43 ++++++++++++++++++++++++++++++++++++++++++- lib/agent.js | 6 +++--- lib/error.js | 25 +++++++++++++++++++++++++ lib/index.js | 32 +++++++++++++++++++++++++++++--- lib/login.js | 20 +++++++++----------- 6 files changed, 113 insertions(+), 46 deletions(-) create mode 100644 lib/error.js diff --git a/bin/now-deploy b/bin/now-deploy index 331056e..e96743b 100755 --- a/bin/now-deploy +++ b/bin/now-deploy @@ -11,6 +11,7 @@ import chalk from 'chalk'; import minimist from 'minimist'; import Now from '../lib'; import ms from 'ms'; +import { handleError, error } from '../lib/error'; const argv = minimist(process.argv.slice(2)); const help = () => { @@ -49,6 +50,7 @@ const clipboard = !(argv.noClipboard || argv.C); const force = argv.f || argv.force; const forceSync = argv.F || argv.forceSync; const shouldLogin = argv.L || argv.login; +const apiUrl = argv.url || 'https://api.now.sh'; const config = cfg.read(); @@ -59,7 +61,7 @@ if (argv.h || argv.help) { console.log(chalk.bold('𝚫 now'), version); process.exit(0); } else if (!config.token || shouldLogin) { - login() + login(apiUrl) .then((token) => { if (shouldLogin) { console.log('> Logged in successfully. Token saved in ~/.now.json'); @@ -87,13 +89,13 @@ async function sync (token) { console.log(`> Deploying "${path}"`); - const now = new Now(token, { debug }); + const now = new Now(apiUrl, token, { debug }); try { await now.create(path, { forceNew: force, forceSync: forceSync }); } catch (err) { handleError(err); - return; + process.exit(1); } const { url } = now; @@ -170,28 +172,3 @@ function printLogs (host) { process.exit(0); }); } - -function handleError (err) { - if (403 === err.status) { - error('Authentication error. Run `now -L` or `now --login` to log-in again.'); - } else if (429 === err.status) { - if (null != err.retryAfter) { - error('Rate limit exceeded error. Try again in ' + - ms(err.retryAfter * 1000, { long: true }) + - ', or upgrade your account: https://zeit.co/now#pricing'); - } else { - error('Rate limit exceeded error. Please try later.'); - } - } else if (err.userError) { - error(err.message); - } else if (500 === err.status) { - error('Unexpected server error. Please retry.'); - } else { - error(`Unexpected error. Please try later. (${err.message})`); - } - process.exit(1); -} - -function error (err) { - console.error(`> \u001b[31mError!\u001b[39m ${err}`); -} diff --git a/bin/now-list b/bin/now-list index 0aa9f4d..25310d3 100755 --- a/bin/now-list +++ b/bin/now-list @@ -1,3 +1,44 @@ #!/usr/bin/env node -console.log('!list', process.argv); +import minimist from 'minimist'; +import Now from '../lib'; +import login from '../lib/login'; +import * as cfg from '../lib/cfg'; +import { handleError, error } from '../lib/error'; + +const argv = minimist(process.argv.slice(2)); +const app = argv._[0]; + +// options +const debug = argv.debug || argv.d; +const apiUrl = argv.url || 'https://api.now.sh'; + +const config = cfg.read(); + +Promise.resolve(config.token || login(apiUrl)) +.then(async (token) => { + try { + await list(token); + } catch (err) { + error(`Unknown error: ${err.stack}`); + process.exit(1); + } +}) +.catch((e) => { + error(`Authentication error – ${e.message}`); + process.exit(1); +}); + +async function list (token) { + const now = new Now(apiUrl, token, { debug }); + + let deployments; + try { + deployments = await now.list(app); + } catch (err) { + handleError(err); + process.exit(1); + } + + console.log(deployments); +} diff --git a/lib/agent.js b/lib/agent.js index 2b55266..84debbc 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -12,8 +12,8 @@ import fetch from 'node-fetch'; */ export default class Agent { - constructor (host, { debug } = {}) { - this._host = host; + constructor (url, { debug } = {}) { + this._url = url; this._debug = debug; this._initAgent(); } @@ -54,7 +54,7 @@ export default class Agent { opts.headers['Content-Length'] = Buffer.byteLength(opts.body); } - return fetch(`https://${this._host}${path}`, opts); + return fetch(this._url + path, opts); } close () { diff --git a/lib/error.js b/lib/error.js new file mode 100644 index 0000000..acaab62 --- /dev/null +++ b/lib/error.js @@ -0,0 +1,25 @@ +import ms from 'ms'; + +export function handleError (err) { + if (403 === err.status) { + error('Authentication error. Run `now -L` or `now --login` to log-in again.'); + } else if (429 === err.status) { + if (null != err.retryAfter) { + error('Rate limit exceeded error. Try again in ' + + ms(err.retryAfter * 1000, { long: true }) + + ', or upgrade your account: https://zeit.co/now#pricing'); + } else { + error('Rate limit exceeded error. Please try later.'); + } + } else if (err.userError) { + error(err.message); + } else if (500 === err.status) { + error('Unexpected server error. Please retry.'); + } else { + error(`Unexpected error. Please try later. (${err.message})`); + } +} + +export function error (err) { + console.error(`> \u001b[31mError!\u001b[39m ${err}`); +} diff --git a/lib/index.js b/lib/index.js index a34fbe2..773dd97 100644 --- a/lib/index.js +++ b/lib/index.js @@ -20,12 +20,12 @@ const IS_WIN = /^win/.test(process.platform); const SEP = IS_WIN ? '\\' : '/'; export default class Now extends EventEmitter { - constructor (token, { forceNew = false, debug = false }) { + constructor (url, token, { forceNew = false, debug = false }) { super(); this._token = token; this._debug = debug; this._forceNew = forceNew; - this._agent = new Agent('api.now.sh', { debug }); + this._agent = new Agent(url, { debug }); this._onRetry = this._onRetry.bind(this); } @@ -159,6 +159,32 @@ export default class Now extends EventEmitter { uploadChunk(); } + async list (app) { + const query = app ? `?app=${encodeURIComponent(app)}` : ''; + + const { deployments } = await retry(async (bail) => { + if (this._debug) console.time('> [debug] /list'); + const res = await this._fetch('/list' + query); + if (this._debug) console.timeEnd('> [debug] /list'); + + // no retry on 4xx + if (400 <= res.status && 500 > res.status) { + if (this._debug) { + console.log('> [debug] bailing on listing due to %s', res.status); + } + return bail(responseError(res)); + } + + if (200 !== res.status) { + throw new Error('Fetching deployment list failed'); + } + + return res.json(); + }, { retries: 3, minTimeout: 2500, onRetry: this._onRetry }); + + return deployments; + } + _onRetry (err) { if (this._debug) { console.log(`> [debug] Retrying: ${err.stack}`); @@ -190,7 +216,7 @@ export default class Now extends EventEmitter { return this._syncAmount; } - async _fetch (_url, opts) { + async _fetch (_url, opts = {}) { opts.headers = opts.headers || {}; opts.headers.authorization = `Bearer ${this._token}`; return await this._agent.fetch(_url, opts); diff --git a/lib/login.js b/lib/login.js index 1024aa3..8a831d0 100644 --- a/lib/login.js +++ b/lib/login.js @@ -3,8 +3,6 @@ import fetch from 'node-fetch'; import * as cfg from './cfg'; import { stringify as stringifyQuery } from 'querystring'; -const URL = 'https://api.now.sh/registration'; - const stdin = process.openStdin(); function readEmail () { @@ -17,9 +15,9 @@ function readEmail () { }); } -async function getVerificationToken (email) { +async function getVerificationToken (url, email) { const data = JSON.stringify({ email }); - const res = await fetch(URL, { + const res = await fetch(url + '/registration', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -36,13 +34,13 @@ async function getVerificationToken (email) { return body.token; } -async function verify (email, verificationToken) { +async function verify (url, email, verificationToken) { const query = { email, token: verificationToken }; - const res = await fetch(`${URL}/verify?${stringifyQuery(query)}`); + const res = await fetch(`${url}/registration/verify?${stringifyQuery(query)}`); const body = await res.json(); return body.token; } @@ -53,9 +51,9 @@ function sleep (ms) { }); } -async function register () { +async function register (url) { const email = await readEmail(); - const verificationToken = await getVerificationToken(email); + const verificationToken = await getVerificationToken(url, email); console.log(`> Please follow the link sent to ${chalk.bold(email)} to log in.`); process.stdout.write('> Waiting for confirmation..'); @@ -64,7 +62,7 @@ async function register () { do { await sleep(2500); try { - final = await verify(email, verificationToken); + final = await verify(url, email, verificationToken); } catch (e) {} process.stdout.write('.'); } while (!final); @@ -74,8 +72,8 @@ async function register () { return { email, token: final }; } -export default async function () { - const loginData = await register(); +export default async function (url) { + const loginData = await register(url); cfg.merge(loginData); return loginData.token; } From 5df1bc613213a5c34f2e0d4996819f2a440b571a Mon Sep 17 00:00:00 2001 From: nkzawa Date: Sat, 16 Apr 2016 17:32:42 +0900 Subject: [PATCH 5/6] now-list: format output --- bin/now-list | 26 +++++++++++++++++++++++++- package.json | 7 ++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/bin/now-list b/bin/now-list index 25310d3..667e760 100755 --- a/bin/now-list +++ b/bin/now-list @@ -1,6 +1,9 @@ #!/usr/bin/env node import minimist from 'minimist'; +import chalk from 'chalk'; +import table from 'text-table'; +import ms from 'ms'; import Now from '../lib'; import login from '../lib/login'; import * as cfg from '../lib/cfg'; @@ -40,5 +43,26 @@ async function list (token) { process.exit(1); } - console.log(deployments); + now.close(); + + const apps = new Map(); + for (const dep of deployments) { + const deps = apps.get(dep.name) || []; + apps.set(dep.name, deps.concat(dep)); + } + + const current = Date.now(); + const text = [...apps].map(([name, deps]) => { + const t = table(deps.map(({ uid, url, created }) => { + const time = ms(current - created, { long: true }) + ' ago'; + return [ uid, time, `https://${url}` ]; + }), { align: ['l', 'r', 'l'] }); + return chalk.bold(name) + '\n\n' + indent(t, 2).split('\n').join('\n\n'); + }).join('\n\n'); + + if (text) console.log('\n' + text + '\n'); +} + +function indent (text, n) { + return text.split('\n').map((l) => ' '.repeat(n) + l).join('\n'); } diff --git a/package.json b/package.json index 2461c5b..8b408b3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "now": "./build/bin/now" }, "dependencies": { + "ansi-escapes": "1.3.0", "arr-flatten": "1.0.1", "array-unique": "0.2.1", "babel-runtime": "6.6.1", @@ -20,15 +21,15 @@ "fs-promise": "0.4.1", "graceful-fs": "4.1.3", "minimatch": "3.0.0", + "minimist": "1.2.0", "ms": "0.7.1", "node-fetch": "1.3.3", "progress": "1.1.8", "resumer": "0.0.0", "retry": "0.9.0", + "socket.io-client": "1.4.5", "split-array": "1.0.1", - "minimist": "1.2.0", - "ansi-escapes": "1.3.0", - "socket.io-client": "1.4.5" + "text-table": "0.2.0" }, "devDependencies": { "alpha-sort": "1.0.2", From 063110008b5348074cd79bebf2a5ef16c03bd493 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Sat, 16 Apr 2016 17:44:10 +0900 Subject: [PATCH 6/6] resume stdin only when reading email --- lib/login.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/login.js b/lib/login.js index 8a831d0..337661a 100644 --- a/lib/login.js +++ b/lib/login.js @@ -3,15 +3,13 @@ import fetch from 'node-fetch'; import * as cfg from './cfg'; import { stringify as stringifyQuery } from 'querystring'; -const stdin = process.openStdin(); - function readEmail () { return new Promise((resolve, reject) => { process.stdout.write('> Enter your email address: '); - stdin.on('data', (d) => { - stdin.destroy(); + process.stdin.on('data', (d) => { + process.stdin.pause(); resolve(d.toString().trim()); - }); + }).resume(); }); }