diff --git a/bin/now b/bin/now index 2743c06..cb56858 100755 --- a/bin/now +++ b/bin/now @@ -2,14 +2,14 @@ import program from 'commander'; import Progress from 'progress'; import copy from '../lib/copy'; +import * as cfg from '../lib/cfg'; import { resolve } from 'path'; import login from '../lib/login'; +import checkUpdate from '../lib/check-update'; import bytes from 'bytes'; import chalk from 'chalk'; import Now from '../lib'; -import fs from 'fs'; import ms from 'ms'; -import os from 'os'; program .usage('[options]') @@ -29,38 +29,37 @@ if (path) { path = process.cwd(); } -let config; +const debug = !!program.debug; +const clipboard = !program.noClipboard; +const config = cfg.read(); +const update = checkUpdate({ debug }); +const exit = (code) => update.then(() => process.exit(code)); -try { - config = fs.readFileSync(resolve(os.homedir(), '.now.json'), 'utf8'); - config = JSON.parse(config); -} catch (err) {} - -if (!config || !config.token || program.login) { +if (!config.token || program.login) { login() .then((token) => { if (program.login) { console.log('> Logged in successfully. Token saved in ~/.now.json'); - process.exit(0); + exit(0); } else { sync(token).catch((err) => { error(`Unknown error: ${err.stack}`); + exit(1); }); } }) .catch((e) => { error(`Authentication error – ${e.message}`); - process.exit(1); + exit(1); }); } else { sync(config.token).catch((err) => { error(`Unknown error: ${err.stack}`); + exit(1); }); } async function sync (token) { - const debug = !!program.debug; - const clipboard = !program.noClipboard; const start = Date.now(); console.log(`> Deploying "${path}"`); @@ -71,6 +70,7 @@ async function sync (token) { await now.create(path, { forceNew: program.force }); } catch (err) { handleError(err); + return; } const { url } = now; @@ -92,7 +92,7 @@ async function sync (token) { const elapsed_u = ms(new Date() - start_u); console.log(`> Sync complete (${bytes(now.syncAmount)}) [${elapsed_u}] `); now.close(); - process.exit(0); + exit(0); }; if (now.syncAmount) { @@ -118,24 +118,26 @@ async function sync (token) { now.on('error', (err) => { error('Upload failed'); handleError(err); - process.exit(1); + exit(1); }); } else { console.log('> Sync complete (cached)'); now.close(); - process.exit(0); + exit(0); } } function handleError (err) { if (403 === err.status) { error('Authentication error. Run `now -L` or `now --login` to log-in again.'); + } 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); + exit(1); } function error (err) { diff --git a/lib/check-update.js b/lib/check-update.js new file mode 100644 index 0000000..5926529 --- /dev/null +++ b/lib/check-update.js @@ -0,0 +1,115 @@ +import ms from 'ms'; +import * as cfg from './cfg'; +import pkg from '../../package'; // relative to `build/` :\ +import request from 'https'; +import chalk from 'chalk'; + +const TEN_MINUTES = ms('1s'); + +/** + * Configures auto updates. + * Sets up a `exit` listener to report them. + */ + +export default function checkUpdate (opts = {}) { + let updateData; + + const update = check(opts).then((data) => { + updateData = data; + + // forces the `exit` event upon Ctrl + C + process.on('SIGINT', () => { + // clean up output after ^C + process.stdout.write('\n'); + process.exit(1); + }); + }, (err) => console.error(err.stack)); + + process.on('exit', (code) => { + if (updateData) { + const { current, latest, at } = updateData; + const ago = ms(Date.now() - at); + console.log(`> ${chalk.white.bgRed('UPDATE NEEDED')} ` + + `Current: ${current} – ` + + `Latest ${chalk.bold(latest)} (released ${ago} ago)`); + } + }); + + return update; +} + +function check ({ debug = false, debounce = TEN_MINUTES, timeout = 1000 }) { + return new Promise((resolve, reject) => { + const { _last_update_check } = cfg.read(); + if (_last_update_check && _last_update_check + debounce > Date.now()) { + if (debug) { + const ago = ms(Date.now() - _last_update_check); + console.log(`> [debug] Skipping update. Last check ${ago} ago.`); + } + return; + } + + if (debug) console.log(`> [debug] Checking for updates. Timeout in ${ms(timeout)}.`); + + let timer; + let req = request.get('https://registry.npmjs.org/now', (res) => { + if (200 !== res.statusCode) { + if (debug) console.log(`> [debug] Update check error. NPM ${res.statusCode}.`); + resolve(false); + return; + } + + res.resume(); + + const bufs = []; + + res.on('data', (buf) => bufs.push(buf)); + + res.on('error', (err) => { + if (debug) console.log(`> [debug] Update check error: ${err.message}.`); + resolve(false); + }); + + res.on('end', () => { + clearTimeout(timer); + const buf = Buffer.concat(bufs); + let data; + + try { + data = JSON.parse(buf.toString('utf8')); + } catch (err) { + if (debug) console.log(`> [debug] Update check JSON parse error: ${err.message}.`); + resolve(false); + return; + } + + const { latest } = data['dist-tags']; + const current = pkg.version; + + if (latest !== pkg.version) { + if (debug) console.log(`> [debug] Needs update. Current ${current}, latest ${latest}`); + resolve({ + latest, + current, + at: new Date(data.time[latest]) + }); + } else { + if (debug) console.log(`> [debug] Up to date (${pkg.version}).`); + resolve(false); + } + + cfg.merge({ _last_update_check: Date.now() }); + }); + }) + .on('error', (err) => { + if (debug) console.log(`> [debug] Update check error: ${err.message}.`); + resolve(false); + }); + + timer = setTimeout(() => { + if (debug) console.log(`> [debug] Aborting update check after ${ms(timeout)}.`); + req.abort(); + resolve(false); + }, timeout); + }); +} diff --git a/lib/index.js b/lib/index.js index f31fa94..c861bab 100644 --- a/lib/index.js +++ b/lib/index.js @@ -34,12 +34,16 @@ export default class Now extends EventEmitter { pkg = await readFile(resolve(path, 'package.json')); pkg = JSON.parse(pkg); } catch (err) { - throw new Error(`Failed to read JSON in "${path}/package.json"`); + const e = Error(`Failed to read JSON in "${path}/package.json"`); + e.userError = true; + throw e; } if (!pkg.scripts || !pkg.scripts.start) { - throw new Error('Missing `start` script in `package.json`. ' + + const e = Error('Missing `start` script in `package.json`. ' + 'See: https://docs.npmjs.com/cli/start.'); + e.userError = true; + throw e; } if (this._debug) console.time('> [debug] Getting files');