'use strict'; var spawn = require('child_process').spawn; var path = require('path'); var format = require('util').format; var Configstore = require('configstore'); var chalk = require('chalk'); var semverDiff = require('semver-diff'); var latestVersion = require('latest-version'); var isNpm = require('is-npm'); var boxen = require('boxen'); var xdgBasedir = require('xdg-basedir'); var ansiAlign = require('ansi-align'); var ONE_DAY = 1000 * 60 * 60 * 24; function UpdateNotifier(options) { this.options = options = options || {}; options.pkg = options.pkg || {}; // reduce pkg to the essential keys. with fallback to deprecated options // TODO: remove deprecated options at some point far into the future options.pkg = { name: options.pkg.name || options.packageName, version: options.pkg.version || options.packageVersion }; if (!options.pkg.name || !options.pkg.version) { throw new Error('pkg.name and pkg.version required'); } this.packageName = options.pkg.name; this.packageVersion = options.pkg.version; this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY; this.hasCallback = typeof options.callback === 'function'; this.callback = options.callback || function () {}; if (!this.hasCallback) { try { this.config = new Configstore('update-notifier-' + this.packageName, { optOut: false, // init with the current time so the first check is only // after the set interval, so not to bother users right away lastUpdateCheck: Date.now() }); } catch (_) { // expecting error code EACCES or EPERM process.on('exit', function () { var msg = [chalk.yellow(format(' %s update check failed ', options.pkg.name))]; msg.push(format(' Try running with %s or get access ', chalk.cyan('sudo'))); msg.push(' to the local update config store via '); msg.push(chalk.cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir.config))); console.error('\n' + boxen(ansiAlign.center(msg).join('\n'))); }); } } } UpdateNotifier.prototype.check = function () { if (this.hasCallback) { this.checkNpm().then(this.callback.bind(this, null)).catch(this.callback); return; } if ( !this.config || this.config.get('optOut') || 'NO_UPDATE_NOTIFIER' in process.env || process.argv.indexOf('--no-update-notifier') !== -1 ) { return; } this.update = this.config.get('update'); if (this.update) { this.config.del('update'); } // Only check for updates on a set interval if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) { return; } // Spawn a detached process, passing the options as an environment property spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], { detached: true, stdio: 'ignore' }).unref(); }; UpdateNotifier.prototype.checkNpm = function () { return latestVersion(this.packageName).then(function (latestVersion) { return { latest: latestVersion, current: this.packageVersion, type: semverDiff(this.packageVersion, latestVersion) || 'latest', name: this.packageName }; }.bind(this)); }; UpdateNotifier.prototype.notify = function (opts) { if (!process.stdout.isTTY || isNpm || !this.update) { return this; } opts = opts || {}; var message = '\n' + boxen('Update available ' + chalk.dim(this.update.current) + chalk.reset(' → ') + chalk.green(this.update.latest) + ' \nRun ' + chalk.cyan('npm i -g ' + this.packageName) + ' to update', { padding: 1, margin: 1, borderColor: 'yellow', borderStyle: 'round' }); if (opts.defer === undefined) { process.on('exit', function () { console.error(message); }); process.on('SIGINT', function () { console.error('\n' + message); }); } else { console.error(message); } return this; }; module.exports = function (options) { var updateNotifier = new UpdateNotifier(options); updateNotifier.check(); return updateNotifier; }; module.exports.UpdateNotifier = UpdateNotifier;