diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..7c19bb4 --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + "es2015-node" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9099689 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..b1e95ea --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,38 @@ +{ + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "env": { + "node": true, + "mocha": true + }, + "extends": [ + "eslint:recommended", + "google" + ], + "rules": { + "comma-dangle": ["error", "always-multiline"], + "quotes": ["error", "single"], + "max-len": ["warn", 100, 2], + "indent": ["error", 2], + "linebreak-style": "error", + "no-multiple-empty-lines": ["error", { "max": 1 }], + "no-trailing-spaces": "error", + "eol-last": "error", + "new-cap": ["error", { "properties": false }], + "arrow-parens": ["error", "as-needed"], + "no-var": "error", + "prefer-const": "warn", + "space-infix-ops": "error", + "space-before-blocks": "error", + "padded-blocks": ["error", "never"], + "object-curly-spacing": ["error", "always"], + "no-multi-spaces": "error", + "block-spacing": ["error", "always"], + "key-spacing": ["error", { "align": "value" }], + "comma-spacing": "error", + "computed-property-spacing": "error", + "keyword-spacing": "error" + } +} diff --git a/.gitignore b/.gitignore index d690531..65d0064 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ node_modules songCache cache settings + +dist diff --git a/.travis.yml b/.travis.yml index c696b00..05c53b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,9 @@ notifications: language: node_js node_js: - "node" +cache: + directories: + - node_modules +after_script: + - "npm run-script coverage" + - "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/bin/nodeplayer b/bin/nodeplayer deleted file mode 100755 index 5966008..0000000 --- a/bin/nodeplayer +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env node - -var argv = require('yargs').argv; -var nodeplayer = require('../'); -var logger = nodeplayer.labeledLogger('core'); - -var core = new nodeplayer.Core(); -core.initModules(argv.u, function() { - logger.info('ready'); -}); diff --git a/db/migrations/000_initial.js b/db/migrations/000_initial.js new file mode 100644 index 0000000..422c285 --- /dev/null +++ b/db/migrations/000_initial.js @@ -0,0 +1,24 @@ +/*eslint-disable func-names*/ +'use strict'; + +exports.up = function(knex) { + return knex.schema + .createTable('songs', function(table) { + table.text('songId').primary(); + table.text('backendName').notNullable(); + table.integer('duration').notNullable(); + table.text('title').notNullable(); + table.text('artist'); + table.text('album'); + table.text('albumArt'); + table.timestamp('createdAt').defaultTo(knex.fn.now()); + }) + + .then(function() { + }); +}; + +exports.down = function(knex) { + return knex.schema + .dropTableIfExists('songs'); +}; diff --git a/index.js b/index.js index 0aca9d5..cb01ee2 100644 --- a/index.js +++ b/index.js @@ -1,107 +1,16 @@ 'use strict'; -var _ = require('underscore'); -var npm = require('npm'); -var async = require('async'); -var labeledLogger = require('./lib/logger'); -var Player = require('./lib/player'); -var nodeplayerConfig = require('./lib/config'); -var config = nodeplayerConfig.getConfig(); - -var logger = labeledLogger('core'); - -function Core() { - this.player = new Player(); +var config = require('./src/config'); +var logger = require('./src/logger'); + +import Player from './src/player'; +import Backend from './src/backends'; +import Plugin from './src/plugins'; + +export { + Player, + Backend, + Plugin, + config, + logger, } - -Core.prototype.checkModule = function(module) { - try { - require.resolve(module); - return true; - } catch (e) { - return false; - } -}; - -Core.prototype.installModule = function(moduleName, callback) { - logger.info('installing module: ' + moduleName); - npm.load({}, function(err) { - npm.commands.install(__dirname, [moduleName], function(err) { - if (err) { - logger.error(moduleName + ' installation failed:', err); - callback(); - } else { - logger.info(moduleName + ' successfully installed'); - callback(); - } - }); - }); -}; - -// make sure all modules are installed, installs missing ones, then calls loadCallback -Core.prototype.installModules = function(modules, moduleType, update, loadCallback) { - async.eachSeries(modules, _.bind(function(moduleShortName, callback) { - var moduleName = 'nodeplayer-' + moduleType + '-' + moduleShortName; - if (!this.checkModule(moduleName) || update) { - // perform install / update - this.installModule(moduleName, callback); - } else { - // skip already installed - callback(); - } - }, this), loadCallback); -}; - -Core.prototype.initModule = function(moduleShortName, moduleType, callback) { - var moduleTypeCapital = moduleType.charAt(0).toUpperCase() + moduleType.slice(1); - var moduleName = 'nodeplayer-' + moduleType + '-' + moduleShortName; - var module = require(moduleName); - - var moduleLogger = labeledLogger(moduleShortName); - module.init(this.player, moduleLogger, _.bind(function(err) { - if (!err) { - this[moduleType + 's'][moduleShortName] = module; - if (moduleType === 'backend') { - this.songsPreparing[moduleShortName] = {}; - } - - moduleLogger.info(moduleType + ' module initialized'); - this.callHooks('on' + moduleTypeCapital + 'Initialized', [moduleShortName]); - } else { - moduleLogger.error('while initializing: ' + err); - this.callHooks('on' + moduleTypeCapital + 'InitError', [moduleShortName]); - } - callback(err); - }, this.player)); -}; - -Core.prototype.initModules = function(update, callback) { - async.eachSeries(['plugin', 'backend'], _.bind(function(moduleType, installCallback) { - // first install missing modules - this.installModules(config[moduleType + 's'], moduleType, update, installCallback); - }, this), _.bind(function() { - // then initialize modules, first all plugins in series, then all backends in parallel - async.eachSeries(['plugin', 'backend'], _.bind(function(moduleType, typeCallback) { - var moduleTypeCapital = moduleType.charAt(0).toUpperCase() + moduleType.slice(1); - - (moduleType === 'plugin' ? async.eachSeries : async.each) - (config[moduleType + 's'], _.bind(function(moduleName, moduleCallback) { - if (this.checkModule('nodeplayer-' + moduleType + '-' + moduleName)) { - this.initModule(moduleName, moduleType, moduleCallback); - } - }, this), _.bind(function(err) { - logger.info('all ' + moduleType + ' modules initialized'); - this.callHooks('on' + moduleTypeCapital + 'sInitialized'); - typeCallback(); - }, this.player)); - }, this), function() { - callback(); - }); - }, this)); -}; - -exports.Player = Player; -exports.labeledLogger = labeledLogger; -exports.config = nodeplayerConfig; - -exports.Core = Core; diff --git a/knexfile.js b/knexfile.js new file mode 100644 index 0000000..e844f45 --- /dev/null +++ b/knexfile.js @@ -0,0 +1,28 @@ +//This file is interpreted as ES5 CommonJS module. +'use strict'; + +const getConfig = require('./src/config').getConfig; + +const ALL_ENVIRONMENTS = Object.assign(getConfig().db, { + pool: { + min: 1, + max: 1 + }, + migrations: { + tableName: 'nodeplayer_migrations', + directory: 'db/migrations' + } +}); + +// Feel free to create any number of other environments. +// The ones below are a best attempt at sensible defaults. +module.exports = { + // Developer's local machine + development: ALL_ENVIRONMENTS, + // Unit and integration test environment + test: ALL_ENVIRONMENTS, + // Shared test/qa/staging/preproduction + staging: ALL_ENVIRONMENTS, + // Production environment + production: ALL_ENVIRONMENTS +}; diff --git a/lib/config.js b/lib/config.js deleted file mode 100644 index fc37d3d..0000000 --- a/lib/config.js +++ /dev/null @@ -1,124 +0,0 @@ -var _ = require('underscore'); -var mkdirp = require('mkdirp'); -var fs = require('fs'); -var os = require('os'); -var path = require('path'); - -function getHomeDir() { - if (process.platform === 'win32') { - return process.env.USERPROFILE; - } else { - return process.env.HOME; - } -} -exports.getHomeDir = getHomeDir; - -function getConfigDir() { - if (process.platform === 'win32') { - return process.env.USERPROFILE + '\\nodeplayer\\config'; - } else { - return process.env.HOME + '/.nodeplayer/config'; - } -} -exports.getConfigDir = getConfigDir; - -function getBaseDir() { - if (process.platform === 'win32') { - return process.env.USERPROFILE + '\\nodeplayer'; - } else { - return process.env.HOME + '/.nodeplayer'; - } -} -exports.getBaseDir = getBaseDir; - -var defaultConfig = {}; - -// backends are sources of music, default backends don't require API keys -defaultConfig.backends = [ - 'youtube' -]; - -// plugins are "everything else", most of the functionality is in plugins -// -// NOTE: ordering is important here, plugins that require another plugin will -// complain if order is wrong. -defaultConfig.plugins = [ - 'storequeue', - 'express', - 'socketio', - 'passport', - 'rest', - 'weblistener' -]; - -defaultConfig.logLevel = 'info'; -defaultConfig.logColorize = true; -defaultConfig.logExceptions = false; // disabled for now because it looks terrible -defaultConfig.logJson = false; - -defaultConfig.songCachePath = getBaseDir() + path.sep + 'song-cache'; -defaultConfig.searchResultCnt = 10; -defaultConfig.playedQueueSize = 100; -defaultConfig.songDelayMs = 1000; // add delay between songs to prevent skips - -defaultConfig.songPrepareTimeout = 10000; // cancel preparation if no progress - -// hostname of the server, may be used as a default value by other plugins -defaultConfig.hostname = os.hostname(); - -exports.getDefaultConfig = function() { - return defaultConfig; -}; - -// path and defaults are optional, if undefined then values corresponding to core config are used -exports.getConfig = function(moduleName, defaults) { - if (process.env.NODE_ENV === 'test') { - // unit tests should always use default config - return (defaults || defaultConfig); - } - - var configPath = getConfigDir() + path.sep + (moduleName || 'core') + '.json'; - - try { - var userConfig = require(configPath); - var config = _.defaults(userConfig, defaults || defaultConfig); - return config; - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - if (!moduleName) { - // only print welcome text for core module first run - console.warn('Welcome to nodeplayer!'); - console.warn('----------------------'); - } - console.warn('\n====================================================================='); - console.warn('We couldn\'t find the user configuration file for module "' + - (moduleName || 'core') + '",'); - console.warn('so a sample configuration file containing default settings ' + - 'will be written into:'); - console.warn(configPath); - - mkdirp.sync(getConfigDir()); - fs.writeFileSync(configPath, JSON.stringify(defaults || defaultConfig, undefined, 4)); - - console.warn('\nFile created. Go edit it NOW!'); - console.warn('Note that the file only needs to contain the configuration ' + - 'variables that'); - console.warn('you want to override from the defaults. Also note that it ' + - 'MUST be valid JSON!'); - console.warn('=====================================================================\n'); - - if (!moduleName) { - // only exit on missing core module config - console.warn('Exiting now. Please re-run nodeplayer when you\'re done ' + - 'configuring!'); - process.exit(0); - } - - return (defaults || defaultConfig); - } else { - console.warn('Unexpected error while loading configuration for module "' + - (moduleName || 'core') + '":'); - console.warn(e); - } - } -}; diff --git a/lib/logger.js b/lib/logger.js deleted file mode 100644 index b1f72f9..0000000 --- a/lib/logger.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; -var config = require('./config').getConfig(); -var winston = require('winston'); - -module.exports = function(label) { - return new (winston.Logger)({ - transports: [ - new (winston.transports.Console)({ - label: label, - level: config.logLevel, - colorize: config.logColorize, - handleExceptions: config.logExceptions, - json: config.logJson - }) - ] - }); -}; diff --git a/lib/player.js b/lib/player.js deleted file mode 100644 index 4b742a1..0000000 --- a/lib/player.js +++ /dev/null @@ -1,531 +0,0 @@ -'use strict'; -var _ = require('underscore'); -var async = require('async'); -var labeledLogger = require('./logger'); - -function Player(options) { - options = options || {}; - - // TODO: some of these should NOT be loaded from config - _.bindAll.apply(_, [this].concat(_.functions(this))); - this.config = options.config || require('./config').getConfig(); - this.logger = options.logger || labeledLogger('core'); - this.playedQueue = options.playedQueue || []; - this.queue = options.queue || []; - this.plugins = options.plugins || {}; - this.backends = options.backends || {}; - this.songsPreparing = options.songsPreparing || {}; - this.volume = options.volume || 1; - this.songEndTimeout = options.songEndTimeout || null; - this.playbackState = { - // TODO: move playbackStart, playbackPosition etc here - }; -} - -// call hook function in all modules -// if any hooks return a truthy value, it is an error and we abort -// be very careful with calling hooks from within a hook, infinite loops are possible -Player.prototype.callHooks = function(hook, argv) { - // _.find() used instead of _.each() because we want to break out as soon - // as a hook returns a truthy value (used to indicate an error, e.g. in form - // of a string) - var err = null; - - this.logger.silly('callHooks(' + hook + - (argv ? ', ' + JSON.stringify(argv) + ')' : ')')); - - _.find(this.plugins, function(plugin) { - if (plugin[hook]) { - err = plugin[hook].apply(null, argv); - return err; - } - }); - - return err; -}; - -// returns number of hook functions attached to given hook -Player.prototype.numHooks = function(hook) { - var cnt = 0; - - _.find(this.plugins, function(plugin) { - if (plugin[hook]) { - cnt++; - } - }); - - return cnt; -}; - -Player.prototype.endOfSong = function() { - var np = this.queue[0]; - - this.logger.info('end of song ' + np.songID); - this.callHooks('onSongEnd', [np]); - - this.playedQueue.push(this.queue[0]); - this.playedQueue = _.last(this.playedQueue, this.config.playedQueueSize); - - this.playbackPosition = null; - this.playbackStart = null; - this.queue[0] = null; - this.songEndTimeout = null; - this.onQueueModify(); -}; - -// start or resume playback of now playing song. -// if pos is undefined, playback continues (or starts from 0 if !playbackPosition) -Player.prototype.startPlayback = function(pos) { - var np = this.queue[0]; - if (!np) { - this.logger.verbose('startPlayback called, but hit end of queue'); - return; - } - - if (!_.isUndefined(pos) && !_.isNull(pos)) { - this.logger.info('playing song: ' + np.songID + ', from pos: ' + pos); - } else { - this.logger.info('playing song: ' + np.songID); - } - - var oldPlaybackStart = this.playbackStart; - this.playbackStart = new Date().getTime(); // song is playing while this is truthy - - // where did the song start playing from at playbackStart? - if (!_.isUndefined(pos) && !_.isNull(pos)) { - this.playbackPosition = pos; - } else if (!this.playbackPosition) { - this.playbackPosition = 0; - } - - if (oldPlaybackStart) { - this.callHooks('onSongSeek', [np]); - } else { - this.callHooks('onSongChange', [np]); - } - - var durationLeft = parseInt(np.duration) - this.playbackPosition + this.config.songDelayMs; - if (this.songEndTimeout) { - this.logger.debug('songEndTimeout was cleared'); - clearTimeout(this.songEndTimeout); - this.songEndTimeout = null; - } - this.songEndTimeout = setTimeout(this.endOfSong, durationLeft); -}; - -Player.prototype.pausePlayback = function() { - // update position - this.playbackPosition += new Date().getTime() - this.playbackStart; - this.playbackStart = null; - - clearTimeout(this.songEndTimeout); - this.songEndTimeout = null; - this.callHooks('onSongPause', [this.nowPlaying]); -}; - -// TODO: proper song object with constructor? -Player.prototype.setPrepareTimeout = function(song) { - if (song.prepareTimeout) { - clearTimeout(song.prepareTimeout); - } - - song.prepareTimeout = setTimeout(_.bind(function() { - this.logger.info('prepare timeout for song: ' + song.songID + ', removing'); - song.cancelPrepare('prepare timeout'); - song.prepareTimeout = null; - }, this), this.config.songPrepareTimeout); - - Object.defineProperty(song, 'prepareTimeout', { - enumerable: false, - writable: true - }); -}; - -Player.prototype.prepareError = function(song, err) { - // remove all instances of this song - for (var i = this.queue.length - 1; i >= 0; i--) { - if (this.queue[i].songID === song.songID && - this.queue[i].backendName === song.backendName) { - if (!song.beingDeleted) { - this.logger.error('preparing song failed! (' + err + '), removing from queue: ' + - song.songID); - this.removeFromQueue(i); - } - } - } - - this.callHooks('onSongPrepareError', [song, err]); -}; - -Player.prototype.prepareProgCallback = function(song, newData, done, asyncCallback) { - /* progress callback - * when this is called, new song data has been flushed to disk */ - - // append new song data to buffer - Object.defineProperty(song, 'songData', { - enumerable: false, - writable: true - }); - if (newData) { - song.songData = song.songData ? Buffer.concat([song.songData, newData]) : newData; - } else if (!song.songData) { - song.songData = new Buffer(0); - } - - // start playback if it hasn't been started yet - if (this.queue[0] && - this.queue[0].backendName === song.backendName && - this.queue[0].songID === song.songID && - !this.playbackStart && newData) { - this.startPlayback(); - } - - // tell plugins that new data is available for this song, and - // whether the song is now fully written to disk or not. - this.callHooks('onPrepareProgress', [song, newData, done]); - - if (done) { - // mark song as prepared - this.callHooks('onSongPrepared', [song]); - - // done preparing, can't cancel anymore - delete(song.cancelPrepare); - - // song data should now be available on disk, don't keep it in memory - this.songsPreparing[song.backendName][song.songID].songData = undefined; - delete(this.songsPreparing[song.backendName][song.songID]); - - // clear prepare timeout - clearTimeout(song.prepareTimeout); - song.prepareTimeout = null; - - asyncCallback(); - } else { - // reset prepare timeout - this.setPrepareTimeout(song); - } -}; - -Player.prototype.prepareErrCallback = function(song, err, asyncCallback) { - /* error callback */ - - // don't let anything run cancelPrepare anymore - delete(song.cancelPrepare); - - // clear prepare timeout - clearTimeout(song.prepareTimeout); - song.prepareTimeout = null; - - // abort preparing more songs; current song will be deleted -> - // onQueueModified is called -> song preparation is triggered again - asyncCallback(true); - - // TODO: investigate this, should probably be above asyncCallback - this.prepareError(song, err); - - song.songData = undefined; - delete(this.songsPreparing[song.backendName][song.songID]); -}; - -// TODO: get rid of the callback hell, use promises? -Player.prototype.prepareSong = function(song, asyncCallback) { - if (!song) { - this.logger.warn('prepareSong() without song'); - asyncCallback(true); - return; - } - if (!this.backends[song.backendName]) { - this.prepareError(song, 'prepareSong() with unknown backend ' + song.backendName); - asyncCallback(true); - return; - } - - if (this.backends[song.backendName].isPrepared(song)) { - // start playback if it hasn't been started yet - if (this.queue[0] && - this.queue[0].backendName === song.backendName && - this.queue[0].songID === song.songID && - !this.playbackStart) { - this.startPlayback(); - } - - // song is already prepared, ok to prepare more songs - asyncCallback(); - } else if (this.songsPreparing[song.backendName][song.songID]) { - // this song is already preparing, so don't yet prepare next song - asyncCallback(true); - } else { - // song is not prepared and not currently preparing: let backend prepare it - this.logger.debug('DEBUG: prepareSong() ' + song.songID); - this.songsPreparing[song.backendName][song.songID] = song; - - song.cancelPrepare = this.backends[song.backendName].prepareSong( - song, - _.partial(this.prepareProgCallback, _, _, _, asyncCallback), - _.partial(this.prepareErrCallback, _, _, asyncCallback) - ); - - this.setPrepareTimeout(song); - } -}; - -// prepare now playing and queued songs for playback -Player.prototype.prepareSongs = function() { - async.series([ - _.bind(function(callback) { - // prepare now-playing song if it exists and if not prepared - if (this.queue[0]) { - this.prepareSong(this.queue[0], callback); - } else { - callback(true); - } - }, this), - _.bind(function(callback) { - // prepare next song in queue if it exists and if not prepared - if (this.queue[1]) { - this.prepareSong(this.queue[1], callback); - } else { - callback(true); - } - }, this) - ]); -}; - -// to be called whenever the queue has been modified -// this function will: -// - play back the first song in the queue if no song is playing -// - call prepareSongs() -Player.prototype.onQueueModify = function() { - this.callHooks('preQueueModify', [this.queue]); - - // set next song as now playing - if (!this.queue[0]) { - this.queue.shift(); - } - - if (!this.queue.length) { - // if the queue is now empty, do nothing - this.callHooks('onEndOfQueue'); - this.logger.info('end of queue, waiting for more songs'); - } else { - // else prepare songs - this.prepareSongs(); - } - this.callHooks('postQueueModify', [this.queue]); -}; - -// find song from queue -Player.prototype.searchQueue = function(backendName, songID) { - for (var i = 0; i < this.queue.length; i++) { - if (this.queue[i].songID === songID && - this.queue[i].backendName === backendName) { - return this.queue[i]; - } - } - - return null; -}; - -// make a search query to backends -Player.prototype.searchBackends = function(query, callback) { - var resultCnt = 0; - var allResults = {}; - - _.each(this.backends, function(backend) { - backend.search(query, _.bind(function(results) { - resultCnt++; - - // make a temporary copy of songlist, clear songlist, check - // each song and add them again if they are ok - var tempSongs = _.clone(results.songs); - allResults[backend.name] = results; - allResults[backend.name].songs = {}; - - _.each(tempSongs, function(song) { - var err = this.callHooks('preAddSearchResult', [song]); - if (!err) { - allResults[backend.name].songs[song.songID] = song; - } else { - this.logger.error('preAddSearchResult hook error: ' + err); - } - }, this); - - // got results from all services? - if (resultCnt >= Object.keys(this.backends).length) { - callback(allResults); - } - }, this), _.bind(function(err) { - resultCnt++; - this.logger.error('error while searching ' + backend.name + ': ' + err); - - // got results from all services? - if (resultCnt >= Object.keys(this.backends).length) { - callback(allResults); - } - }, this)); - }, this); -}; - -// get rid of song in queue -// cnt can be left out for deleting only one song -Player.prototype.removeFromQueue = function(pos, cnt, onlyRemove) { - var retval = []; - if (!cnt) { - cnt = 1; - } - pos = Math.max(0, parseInt(pos)); - - if (!onlyRemove) { - this.callHooks('preSongsRemoved', [pos, cnt]); - } - - // remove songs from queue - if (pos + cnt > 0) { - if (this.queue.length) { - // stop preparing songs we are about to remove - // we want to limit this to this.queue.length if cnt is very large - for (var i = 0; i < Math.min(this.queue.length, pos + cnt); i++) { - var song = this.queue[i]; - - // signal prepareError function not to run removeFromQueue again - // TODO: try getting rid of this ugly hack (beingDeleted)... - // TODO: more non enumerable properties, especially plugins? - Object.defineProperty(song, 'beingDeleted', { - enumerable: false, - writable: true - }); - - song.beingDeleted = true; - if (song.cancelPrepare) { - song.cancelPrepare('song deleted'); - delete(song.cancelPrepare); - } - } - - retval = this.queue.splice(pos, cnt); - - if (pos === 0) { - // now playing was deleted - this.playbackPosition = null; - this.playbackStart = null; - clearTimeout(this.songEndTimeout); - this.songEndTimeout = null; - } - } - } - - if (!onlyRemove) { - this.onQueueModify(); - this.callHooks('postSongsRemoved', [pos, cnt]); - } - - return retval; -}; - -Player.prototype.moveInQueue = function(from, to, cnt) { - if (!cnt || cnt < 1) { - cnt = 1; - } - if (from < 0 || from + cnt > this.queue.length || to + cnt > this.queue.length) { - return null; - } - - this.callHooks('preSongsMoved', [from, to, cnt]); - - var songs = this.removeFromQueue(from, cnt, true); - Array.prototype.splice.apply(this.queue, [to, 0].concat(songs)); - - this.callHooks('sortQueue'); - this.onQueueModify(); - this.callHooks('postSongsMoved', [songs, from, to, cnt]); - - return songs; -}; - -// add songs to the queue, at optional position -Player.prototype.addToQueue = function(songs, pos) { - if (!pos) { - pos = this.queue.length; - } - if (pos < 0) { - pos = 1; - } - pos = Math.min(pos, this.queue.length); - - this.callHooks('preSongsQueued', [songs, pos]); - _.each(songs, function(song) { - // check that required fields are provided - if (!song.title || !song.songID || !song.backendName || !song.duration) { - this.logger.info('required song fields not provided: ' + song.songID); - return; - //return 'required song fields not provided'; // TODO: this ain't gonna work - } - - var err = this.callHooks('preSongQueued', [song]); - if (err) { - this.logger.error('not adding song to queue: ' + err); - } else { - song.timeAdded = new Date().getTime(); - - this.queue.splice(pos++, 0, song); - this.logger.info('added song to queue: ' + song.songID); - this.callHooks('postSongQueued', [song]); - } - }, this); - - this.callHooks('sortQueue'); - this.onQueueModify(); - this.callHooks('postSongsQueued', [songs, pos]); -}; - -Player.prototype.shuffleQueue = function() { - // don't change now playing - var temp = this.queue.shift(); - this.queue = _.shuffle(this.queue); - this.queue.unshift(temp); - - this.callHooks('onQueueShuffled', [this.queue]); - this.onQueueModify(); -}; - -// cnt can be negative to go back or zero to restart current song -Player.prototype.skipSongs = function(cnt) { - this.npIsPlaying = false; - - // TODO: this could be replaced with a splice? - for (var i = 0; i < Math.abs(cnt); i++) { - if (cnt > 0) { - if (this.queue[0]) { - this.playedQueue.push(this.queue[0]); - } - - this.queue.shift(); - } else if (cnt < 0) { - if (this.playedQueue.length) { - this.queue.unshift(this.playedQueue.pop()); - } - } - - // ran out of songs while skipping, stop - if (!this.queue[0]) { - break; - } - } - - this.playedQueue = _.last(this.playedQueue, this.config.playedQueueSize); - - this.playbackPosition = null; - this.playbackStart = null; - clearTimeout(this.songEndTimeout); - this.songEndTimeout = null; - this.onQueueModify(); -}; - -// TODO: userID does not belong into core...? -Player.prototype.setVolume = function(newVol, userID) { - newVol = Math.min(1, Math.max(0, newVol)); - this.volume = newVol; - this.callHooks('onVolumeChange', [newVol, userID]); -}; - -module.exports = Player; diff --git a/package.json b/package.json index 5d5fb32..7c1a15b 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,21 @@ { "name": "nodeplayer", - "version": "0.2.0", + "version": "0.2.1", "description": "simple, modular music player written in node.js", "main": "index.js", "preferGlobal": true, "scripts": { - "start": "./bin/nodeplayer", - "test": "NODE_ENV=test ./node_modules/mocha/bin/mocha", - "coverage": "NODE_ENV=test istanbul cover _mocha -- -R spec" + "start": "node dist/index", + "build": "babel src --out-dir dist --source-maps", + "postinstall": "npm run build", + "watch": "nodemon --exec babel-node src/index.js", + "test": "NODE_ENV=test ./node_modules/mocha/bin/mocha --compilers js:babel-core/register", + "fix": "eslint --fix src", + "lint": "eslint src", + "coverage": "NODE_ENV=test istanbul cover _mocha -- -R spec", + "db:init": "knex migrate:latest", + "db:migrate": "knex migrate:latest", + "db:rollback": "knex migrate:rollback" }, "repository": { "type": "git", @@ -15,24 +23,43 @@ }, "author": "FruitieX", "bin": { - "nodeplayer": "./bin/nodeplayer" + "nodeplayer": "./dist/index.js" }, "license": "MIT", "dependencies": { - "async": "^0.9.0", - "mkdirp": "^0.5.0", - "npm": "^2.7.1", - "underscore": "^1.7.0", - "winston": "^0.9.0", - "yargs": "^3.6.0" + "async": "^2.0.0-rc.6", + "babel-cli": "^6.11.4", + "babel-core": "^6.11.4", + "babel-preset-es2015-node": "^6.1.1", + "bluebird": "^3.4.1", + "boom": "^4.2.0", + "escape-string-regexp": "^1.0.5", + "fluent-ffmpeg": "^2.1.0", + "hapi": "^15.2.0", + "inert": "^4.0.2", + "knex": "^0.12.5", + "lodash": "^4.15.0", + "mkdirp": "^0.5.1", + "mongoose": "^4.4.20", + "node-ffprobe": "^1.2.2", + "node-uuid": "^1.4.7", + "npm": "^3.9.5", + "pg": "^6.1.0", + "sockjs": "^0.3.18", + "sockjs-client": "^1.1.1", + "walk": "^2.3.9", + "winston": "^2.2.0", + "yargs": "^4.7.1" }, "devDependencies": { "chai": "*", + "coveralls": "^2.11.9", + "eslint": "^3.4.0", + "eslint-config-google": "^0.6.0", "istanbul": "*", - "jscs": "^1.11.3", "mocha": "*", - "mocha-jscs": "^1.0.2", - "mocha-jshint": "^1.0.0", - "nodeplayer-backend-dummy": "^0.1.9" + "mocha-eslint": "^3.0.1", + "nodemon": "^1.10.2", + "nodeplayer-backend-dummy": "^0.1.999" } } diff --git a/playlist-wip.md b/playlist-wip.md new file mode 100644 index 0000000..ec9fd56 --- /dev/null +++ b/playlist-wip.md @@ -0,0 +1,46 @@ +# Nodeplayer playlist concepts +## Definitions +### Playlist + +- Generic list of songs that can be added to the playback queue. +- Can *not* be played directly, must *always* be added to playback queue first. +- Core contains playlist management functionality for: + - Creating playlists + - Adding/removing songs to/from playlists + - Editing playlists + - Listing playlists +- Backends can provide playlists, containing songs from that backend. + - These will start out as read-only to keep things simple. +- Plugins can provide autogenerated playlists, for example based on ratings. + - These are also read-only by the user, plugin can modify. + +### Playback queue + +- Contains list of songs that the player will play / has played. +- Keeps track of where playback currently is. +- Keeps track of which playlist a song was added from (if any). This is simply + a field in the song object containing the name and backend of the playlist. + - This makes it possible for clients to display songs differently depending + on which playlist they were added from. + - One time use, 'pseudo-playlists', are also possible. nodeplayer blindly + trusts which playlist a song was added from, the client can decide. + - Pseudo-playlists can be used by clients to implement functionality such + as queuing songs before the rest of the playback queue. This simulates + something like the WinAmp queue, which is always played before the rest + of the playback queue. Let's call this the 'pre-queue'. + - This is done by having the client reserve a certain playlist name + for the pre-queue, say '__prequeue'. + - Pre-queuing songs works like this: The client checks the current + playback queue at the current playback position. + - If the current song was added from a playlist '__prequeue', find + the first song after it that is not part of __prequeue, insert + song before it. + - If the current song is not part of __prequeue, insert song into + the playback queue immediately after the current song and mark it + as __prequeue. + - Implementing pre-queue functionality like this makes playlist handling + code much much simpler. E.g. song preparing doesn't need to care about + a separate queue and playlists, it only needs to care about one array + which is the playback queue. + - Plugins can restrict how the playback queue is managed, e.g. partyplay + only allows appending into __prequeue. diff --git a/src/backends/defaults.js b/src/backends/defaults.js new file mode 100644 index 0000000..3113ddc --- /dev/null +++ b/src/backends/defaults.js @@ -0,0 +1,6 @@ +import Local from './local'; + +const defaultBackends = []; +//defaultBackends.push(Local); + +export default defaultBackends; diff --git a/src/backends/index.js b/src/backends/index.js new file mode 100644 index 0000000..218feb2 --- /dev/null +++ b/src/backends/index.js @@ -0,0 +1,235 @@ +import path from 'path'; +import fs from 'fs'; +import ffmpeg from 'fluent-ffmpeg'; +import config from '../config'; +import labeledLogger from '../logger'; +import mkdirp from 'mkdirp'; + +const coreConfig = config.getConfig(); + +/** + * Super constructor for backends + */ +export default class Backend { + constructor(defaultConfig) { + this.name = this.constructor.name.toLowerCase(); + this.log = labeledLogger(this.name); + this.songsPreparing = {}; + this.coreConfig = coreConfig; + + if (defaultConfig) { + this.config = config.getConfig(this, defaultConfig); + } + } + + /** + * Callback for reporting encoding progress + * @callback encodeCallback + * @param {Error} err - If truthy, an error occurred and preparation cannot continue + * @param {Buffer} bytesWritten - How many new bytes was written to song.data + * @param {Bool} done - True if this was the last chunk + */ + + /** + * Encode stream as opus + * @param {Stream} stream - Input stream + * @param {Number} seek - Skip to this position in song (TODO) + * @param {Song} song - Song object whose audio is being encoded + * @param {encodeCallback} callback - Called when song is ready or on error + * @return {Function} - Can be called to terminate encoding + */ + encodeSong(stream, seek, song, callback) { + const encodedPath = path.join(coreConfig.songCachePath, this.name, + song.songId + '.opus'); + + const command = ffmpeg(stream) + .noVideo() + // .inputFormat('mp3') + // .inputOption('-ac 2') + .audioCodec('libopus') + .audioBitrate('192') + .format('opus') + .on('error', err => { + this.log.error(this.name + ': error while transcoding ' + song.songId + ': ' + err); + delete song.prepare.data; + callback(err); + }); + + const opusStream = command.pipe(null, { end: true }); + opusStream.on('data', chunk => { + // TODO: this could be optimized by using larger buffers + // song.prepare.data = Buffer.concat([song.prepare.data, chunk], song.prepare.data.length + chunk.length); + + if (chunk.length <= song.prepare.data.length - song.prepare.dataPos) { + // If there's room in the buffer, write chunk into it + chunk.copy(song.prepare.data, song.prepare.dataPos); + song.prepare.dataPos += chunk.length; + } else { + // Otherwise allocate more room, then copy chunk into buffer + + // Make absolutely sure that the chunk will fit inside new buffer + const newSize = Math.max(song.prepare.data.length * 2, + song.prepare.data.length + chunk.length); + + this.log.debug('Allocated new song data buffer of size: ' + newSize); + + const buf = new Buffer.allocUnsafe(newSize); + + song.prepare.data.copy(buf); + song.prepare.data = buf; + + chunk.copy(song.prepare.data, song.prepare.dataPos); + song.prepare.dataPos += chunk.length; + } + + callback(null, chunk.length, false); + }); + opusStream.on('end', () => { + mkdirp(path.dirname(encodedPath), err => { + if (err) { + return this.log.error(`error creating directory: ${path.dirname(encodedPath)}: ${err}`); + } + fs.writeFile(encodedPath, song.prepare.data, err => { + if (err) { + return this.log.error(`error writing file to ${encodedPath}: ${err}`); + } + + this.log.verbose('wrote file to ' + encodedPath); + this.log.verbose('transcoding ended for ' + song.songId); + + delete song.prepare; + // TODO: we don't know if transcoding ended successfully or not, + // and there might be a race condition between errCallback deleting + // the file and us trying to move it to the songCache + // TODO: is this still the case? + // (we no longer save incomplete files on disk) + + callback(null, null, true); + }); + }); + }); + + this.log.verbose('transcoding ' + song.songId + '...'); + + // return a function which can be used for terminating encoding + return err => { + command.kill(); + this.log.verbose(this.name + ': canceled preparing: ' + song.songId + ': ' + err); + delete song.prepare; + callback(new Error('canceled preparing: ' + song.songId + ': ' + err)); + }; + } + + /** + * Prepare song for playback + * @param {Song} song - Song to prepare + * @param {encodeCallback} callback - Called when song is ready or on error + */ + prepare(song, callback) { + if (this.songsPreparing[song.songId]) { + // song is preparing, caller can drop this request (previous caller will take care of + // handling once preparation is finished) + callback(null, null, false); + } else if (this.isPrepared(song)) { + // song has already prepared, caller can start playing song + callback(null, null, true); + } else { + // begin preparing song + let cancelEncode = null; + let canceled = false; + + song.prepare = { + data: new Buffer.allocUnsafe(1024 * 1024), + dataPos: 0, + cancel: () => { + canceled = true; + if (cancelEncode) { + cancelEncode(); + } + }, + }; + + this.songsPreparing[song.songId] = song; + + this.getSongStream(song, (err, readStream) => { + if (canceled) { + callback(new Error('song was canceled before encoding started')); + } else if (err) { + callback(new Error(`error while getting song stream: ${err}`)); + } else { + cancelEncode = this.encodeSong(readStream, 0, song, callback); + readStream.on('error', err => { + callback(err); + }); + } + }); + } + } + + /** + * Cancel song preparation if applicable + * @param {Song} song - Song to cancel + */ + cancelPrepare(song) { + if (this.songsPreparing[song.songId]) { + this.log.info('Canceling song preparing: ' + song.songId); + this.songsPreparing[song.songId].cancel(); + } else { + this.log.error('cancelPrepare() called on song not in preparation: ' + song.songId); + } + } + + // dummy functions + + /** + * Callback for reporting song duration + * @callback durationCallback + * @param {Error} err - If truthy, an error occurred + * @param {Number} duration - Duration in milliseconds + */ + + /** + * Returns length of song + * @param {Song} song - Query concerns this song + * @param {durationCallback} callback - Called with duration + */ + getDuration(song, callback) { + const err = 'FATAL: backend does not implement getDuration()!'; + this.log.error(err); + callback(err); + } + + /** + * Synchronously(!) returns whether the song with songId is prepared or not + * @param {Song} song - Query concerns this song + * @return {Boolean} - true if song is prepared, false if not + */ + isPrepared(song) { + this.log.error('FATAL: backend does not implement isPrepared()!'); + return false; + } + + /** + * Get read stream for song + * @param {Song} song - Song to prepare + * @param {streamCallback} callback - Called when read stream is ready or on error + */ + getSongStream(song, callback) { + this.log.error('FATAL: backend does not implement getSongStream()!'); + callback(new Error('FATAL: backend does not implement getSongStream()!')); + } + + /** + * Search for songs + * @param {Object} query - Search terms + * @param {String} [query.artist] - Artist + * @param {String} [query.title] - Title + * @param {String} [query.album] - Album + * @param {Boolean} [query.any] - Match any of the above, otherwise all fields have to match + * @param {Function} callback - Called with error or results + */ + search(query, callback) { + this.log.error('FATAL: backend does not implement search()!'); + callback(new Error('FATAL: backend does not implement search()!')); + } +} diff --git a/src/backends/local.js b/src/backends/local.js new file mode 100644 index 0000000..8112561 --- /dev/null +++ b/src/backends/local.js @@ -0,0 +1,418 @@ +'use strict'; + +const Backend = require('../../').Backend; +const path = require('path'); +const fs = require('fs'); + +import knex from '../db'; + +module.exports = class Local extends Backend { + constructor(callback) { + super(); + + callback(null); + } + + isPrepared(song) { + const filePath = path.join(this.coreConfig.songCachePath, 'local', song.songId + '.opus'); + return fs.existsSync(filePath); + } + + getSongStream(song, callback) { + knex + .first('songs') + .where('songId', song.songId) + .then(song => { + // song id is file path + const filePath = song.songId; + let stream = fs.createReadStream(filePath); + callback(null, stream); + }) + .catch(err => { + callback(err); + }); + } +}; + +/* +const path = require('path'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const mongoose = require('mongoose'); +const async = require('async'); +const walk = require('walk'); +const ffprobe = require('node-ffprobe'); +const _ = require('lodash'); +const escapeStringRegexp = require('escape-string-regexp'); + +import Backend from '../backend'; +*/ + +/* +var probeCallback = (err, probeData, next) => { + var formats = config.importFormats; + if (probeData) { + // ignore camel case rule here as we can't do anything about probeData + //jscs:disable requireCamelCaseOrUpperCaseIdentifiers + if (formats.indexOf(probeData.format.format_name) >= 0) { // Format is supported + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + var song = { + title: '', + artist: '', + album: '', + duration: '0', + }; + + // some tags may be in mixed/all caps, let's convert every tag to lower case + var key; + var keys = Object.keys(probeData.metadata); + var n = keys.length; + var metadata = {}; + while (n--) { + key = keys[n]; + metadata[key.toLowerCase()] = probeData.metadata[key]; + } + + // try a best guess based on filename in case tags are unavailable + var basename = path.basename(probeData.file); + basename = path.basename(probeData.file, path.extname(basename)); + var splitTitle = basename.split(/\s-\s(.+)?/); + + if (!_.isUndefined(metadata.title)) { + song.title = metadata.title; + } else { + song.title = splitTitle[1]; + } + if (!_.isUndefined(metadata.artist)) { + song.artist = metadata.artist; + } else { + song.artist = splitTitle[0]; + } + if (!_.isUndefined(metadata.album)) { + song.album = metadata.album; + } + + song.file = probeData.file; + + song.duration = probeData.format.duration * 1000; + SongModel.update({file: probeData.file}, {'$set':song}, {upsert: true}, + (err, result) => { + if (result == 1) { + self.log.debug('Upserted: ' + probeData.file); + } else { + self.log.error('error while updating db: ' + err); + } + + next(); + }); + } else { + self.log.verbose('format not supported, skipping...'); + next(); + } + } else { + self.log.error('error while probing:' + err); + next(); + } +}; +*/ + +/* +// database model +const SongModel = mongoose.model('Song', { + title: String, + artist: String, + album: String, + albumArt: { + lq: String, + hq: String, + }, + duration: { + type: Number, + required: true, + }, + format: { + type: String, + required: true, + }, + filename: { + type: String, + unique: true, + required: true, + dropDups: true, + }, +}); +*/ + +/** + * Try to guess metadata from file path, + * Assumes the following naming conventions: + * /path/to/music/Album/Artist - Title.ext + * + * @param {String} filePath - Full file path including filename extension + * @param {String} fileExt - Filename extension + * @return {Metadata} Song metadata + */ + +/* +const guessMetadataFromPath = (filePath, fileExt) => { + const fileName = path.basename(filePath, fileExt); + + // split filename at dashes, trim extra whitespace, e.g: + let splitName = fileName.split('-'); + splitName = _.map(splitName, name => { + return name.trim(); + }); + + // TODO: compare album name against music dir, leave empty if equal + return { + artist: splitName[0], + title: splitName[1], + album: path.basename(path.dirname(filePath)), + }; +}; + +export default class Local extends Backend { + constructor(callback) { + super(); + + const self = this; + + // NOTE: no argument passed so we get the core's config + const config = require('../config').getConfig(); + this.config = config; + this.songCachePath = config.songCachePath; + this.importFormats = config.importFormats; + + // make sure all necessary directories exist + mkdirp.sync(path.join(this.songCachePath, 'local', 'incomplete')); + + // connect to the database + mongoose.connect(config.mongo); + + const db = mongoose.connection; + db.on('error', err => { + return callback(err, self); + }); + db.once('open', () => { + return callback(null, self); + }); + + const options = { + followLinks: config.followSymlinks, + }; + + const insertSong = (probeData, done) => { + const guessMetadata = guessMetadataFromPath(probeData.file, probeData.fileext); + + let song = new SongModel({ + title: probeData.metadata.TITLE || guessMetadata.title, + artist: probeData.metadata.ARTIST || guessMetadata.artist, + album: probeData.metadata.ALBUM || guessMetadata.album, + // albumArt: {} // TODO + duration: probeData.format.duration * 1000, + format: probeData.format.format_name, + filename: probeData.file, + }); + + song = song.toObject(); + + delete song._id; + + SongModel.findOneAndUpdate({ + filename: probeData.file, + }, song, { upsert: true }, err => { + if (err) { + self.log.error('while inserting song: ' + probeData.file + ', ' + err); + } + done(); + }); + }; + + // create async.js queue to limit concurrent probes + const q = async.queue((task, done) => { + ffprobe(task.filename, (err, probeData) => { + if (!probeData) { + return done(); + } + + let validStreams = false; + + if (_.includes(self.importFormats, probeData.format.format_name)) { + validStreams = true; + } + + if (validStreams) { + insertSong(probeData, done); + } else { + self.log.info('skipping file of unknown format: ' + task.filename); + done(); + } + }); + }, config.concurrentProbes); + + // walk the filesystem and scan files + // TODO: also check through entire DB to see that all files still exist on the filesystem + // TODO: filter by allowed filename extensions + if (config.rescanAtStart) { + self.log.info('Scanning directory: ' + config.importPath); + const walker = walk.walk(config.importPath, options); + const startTime = new Date(); + let scanned = 0; + walker.on('file', (root, fileStats, next) => { + const filename = path.join(root, fileStats.name); + self.log.verbose('Scanning: ' + filename); + scanned++; + q.push({ + filename: filename, + }); + next(); + }); + walker.on('end', () => { + self.log.verbose('Scanned files: ' + scanned); + self.log.verbose('Done in: ' + + Math.round((new Date() - startTime) / 1000) + ' seconds'); + }); + } + + // TODO: fs watch + // set fs watcher on media directory + // TODO: add a debounce so if the file keeps changing we don't probe it multiple times +// */ + /* + watch(config.importPath, { + recursive: true, + followSymlinks: config.followSymlinks + }, (filename) => { + if (fs.existsSync(filename)) { + self.log.debug(filename + ' modified or created, queued for probing'); + q.unshift({ + filename: filename + }); + } else { + self.log.debug(filename + ' deleted'); + db.collection('songs').remove({file: filename}, (err, items) => { + self.log.debug(filename + ' deleted from db: ' + err + ', ' + items); + }); + } + }); + */ + /* + } + + isPrepared(song) { + const filePath = path.join(this.songCachePath, 'local', song.songId + '.opus'); + return fs.existsSync(filePath); + } + + getDuration(song, callback) { + SongModel.findById(song.songId, (err, item) => { + if (err) { + return callback(err); + } + + callback(null, item.duration); + }); + } + + prepare(song, callback) { + const self = this; + + // TODO: move most of this into common code inside core + if (self.songsPreparing[song.songId]) { + // song is preparing, caller can drop this request (previous caller will take care of + // handling once preparation is finished) + callback(null, null, false); + } else if (self.isPrepared(song)) { + // song has already prepared, caller can start playing song + callback(null, null, true); + } else { + // begin preparing song + let cancelEncode = null; + let canceled = false; + + song.prepare = { + data: new Buffer.allocUnsafe(1024 * 1024), + dataPos: 0, + cancel: () => { + canceled = true; + if (cancelEncode) { + cancelEncode(); + } + }, + }; + + self.songsPreparing[song.songId] = song; + + SongModel.findById(song.songId, (err, item) => { + if (canceled) { + callback(new Error('song was canceled before encoding started')); + } else if (item) { + const readStream = fs.createReadStream(item.filename); + cancelEncode = self.encodeSong(readStream, 0, song, callback); + readStream.on('error', err => { + callback(err); + }); + } else { + callback(new Error('song not found in local db: ' + song.songId)); + } + }); + } + } + + search(query, callback) { + const self = this; + + let q; + if (query.any) { + q = { + $or: [ + { artist: new RegExp(escapeStringRegexp(query.any), 'i') }, + { title: new RegExp(escapeStringRegexp(query.any), 'i') }, + { album: new RegExp(escapeStringRegexp(query.any), 'i') }, + ], + }; + } else { + q = { + $and: [], + }; + + _.keys(query).forEach(key => { + const criterion = {}; + criterion[key] = new RegExp(escapeStringRegexp(query[key]), 'i'); + q.$and.push(criterion); + }); + } + + SongModel.find(q).exec((err, items) => { + if (err) { + return callback(err); + } + + const results = {}; + results.songs = {}; + + const numItems = items.length; + let cur = 0; + items.forEach(song => { + if (Object.keys(results.songs).length <= self.config.searchResultCnt) { + song = song.toObject(); + + results.songs[song._id] = { + artist: song.artist, + title: song.title, + album: song.album, + albumArt: null, // TODO: can we add this? + duration: song.duration, + songId: song._id, + score: self.config.maxScore * (numItems - cur) / numItems, + backendName: 'local', + format: 'opus', + }; + cur++; + } + }); + callback(results); + }); + } +} +*/ diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..66794d7 --- /dev/null +++ b/src/config.js @@ -0,0 +1,143 @@ +const _ = require('lodash'); +const mkdirp = require('mkdirp'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +/* eslint no-console: ["error", { "allow": ["warn"] }] */ + +function getHomeDir() { + if (process.platform === 'win32') { + return process.env.USERPROFILE; + } + + return process.env.HOME; +} +exports.getHomeDir = getHomeDir; + +function getBaseDir() { + if (process.platform === 'win32') { + return path.join(process.env.USERPROFILE, 'nodeplayer'); + } + + return path.join(process.env.HOME, '.nodeplayer'); +} +exports.getBaseDir = getBaseDir; + +const defaultConfig = {}; + +// backends are sources of music +defaultConfig.backends = [ + // 'youtube', +]; + +// plugins are "everything else", most of the functionality is in plugins +// +// NOTE: ordering is important here, plugins that require another plugin will +// complain if order is wrong. +defaultConfig.plugins = [ + // 'weblistener', +]; + +defaultConfig.logLevel = 'info'; +defaultConfig.logColorize = true; +defaultConfig.logExceptions = false; // disabled for now because it looks terrible +defaultConfig.logJson = false; + +defaultConfig.songCachePath = path.join(getBaseDir(), 'song-cache'); +defaultConfig.searchResultCnt = 10; +defaultConfig.playedQueueSize = 100; +defaultConfig.songDelayMs = 1000; // add delay between songs to prevent skips + +defaultConfig.songPrepareTimeout = 10000; // cancel preparation if no progress + +// built-in express plugin +defaultConfig.port = 8080; +defaultConfig.tls = false; +defaultConfig.key = path.join(getBaseDir(), 'nodeplayer-key.pem'); +defaultConfig.cert = path.join(getBaseDir(), 'nodeplayer-cert.pem'); +defaultConfig.ca = path.join(getBaseDir(), 'nodeplayer-ca.pem'); +defaultConfig.requestCert = false; +defaultConfig.rejectUnauthorized = true; + +// built-in local file backend +defaultConfig.db = { + client: 'postgresql', + connection: 'postgres;//postgres@127.0.0.1/nodeplayer', +}; + +defaultConfig.mongo = 'mongodb://localhost:27017/nodeplayer-backend-file'; +defaultConfig.rescanAtStart = false; +defaultConfig.importPath = path.join(getHomeDir(), 'music'); +defaultConfig.importFormats = [ + 'mp3', + 'flac', + 'ogg', + 'opus', +]; +defaultConfig.concurrentProbes = 4; +defaultConfig.followSymlinks = true; +defaultConfig.maxScore = 10; // FIXME: ATM the search algo can return VERY irrelevant results + +// hostname of the server, may be used as a default value by other plugins +defaultConfig.hostname = os.hostname(); + +defaultConfig.queueStorePath = path.join(getBaseDir(), 'stored-queue.json'); + +exports.getDefaultConfig = () => { + return defaultConfig; +}; + +// path and defaults are optional, if undefined then values corresponding to core config are used +exports.getConfig = (module, defaults) => { + if (process.env.NODE_ENV === 'test') { + // unit tests should always use default config + return (defaults || defaultConfig); + } + + const moduleName = module ? module.name : null; + + const configPath = path.join(getBaseDir(), 'config', (moduleName || 'core') + '.json'); + + try { + const userConfig = require(configPath); + const config = _.defaults(userConfig, defaults || defaultConfig); + return config; + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + if (!moduleName) { + // only print welcome text for core module first run + console.warn('Welcome to nodeplayer!'); + console.warn('----------------------'); + } + console.warn('\n====================================================================='); + console.warn('We couldn\'t find the user configuration file for module "' + + (moduleName || 'core') + '",'); + console.warn('so a sample configuration file containing default settings ' + + 'will be written into:'); + console.warn(configPath); + + mkdirp.sync(path.join(getBaseDir(), 'config')); + fs.writeFileSync(configPath, JSON.stringify(defaults || defaultConfig, undefined, 4)); + + console.warn('\nFile created. Go edit it NOW!'); + console.warn('Note that the file only needs to contain the configuration ' + + 'variables that'); + console.warn('you want to override from the defaults. Also note that it ' + + 'MUST be valid JSON!'); + console.warn('=====================================================================\n'); + + if (!moduleName) { + // only exit on missing core module config + console.warn('Exiting now. Please re-run nodeplayer when you\'re done configuring!'); + process.exit(0); + } + + return (defaults || defaultConfig); + } + + console.warn('Unexpected error while loading configuration for module "' + + (moduleName || 'core') + '":'); + console.warn(e); + } +}; diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..3797eee --- /dev/null +++ b/src/db.js @@ -0,0 +1,9 @@ +'use strict'; + +import { getConfig } from './config'; +import knex from 'knex'; + +export default knex({ + client: getConfig().db.client, + connection: getConfig().db.connection, +}); diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..97bbd7e --- /dev/null +++ b/src/index.js @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +import Player from './player'; +const p = new Player(); + +p.init(); diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 0000000..80b9ad8 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,17 @@ +'use strict'; +const config = require('./config').getConfig(); +const winston = require('winston'); + +module.exports = label => { + return new (winston.Logger)({ + transports: [ + new (winston.transports.Console)({ + label: label, + level: config.logLevel, + colorize: config.logColorize, + handleExceptions: config.logExceptions, + json: config.logJson, + }), + ], + }); +}; diff --git a/src/modules.js b/src/modules.js new file mode 100644 index 0000000..b521db3 --- /dev/null +++ b/src/modules.js @@ -0,0 +1,192 @@ +const npm = require('npm'); +const async = require('async'); +const labeledLogger = require('./logger'); +import defaultPlugins from './plugins/defaults'; +import defaultBackends from './backends/defaults'; + +const _ = require('lodash'); +const logger = labeledLogger('modules'); + +const checkModule = module => { + try { + require.resolve(module); + return true; + } catch (e) { + return false; + } +}; + +// install a single module +const installModule = (moduleName, callback) => { + logger.info('installing module: ' + moduleName); + npm.load({}, err => { + npm.commands.install(__dirname, [moduleName], err => { + if (err) { + logger.error(moduleName + ' installation failed:', err); + callback(); + } else { + logger.info(moduleName + ' successfully installed'); + callback(); + } + }); + }); +}; + +// make sure all modules are installed, installs missing ones, then calls done +const installModules = (modules, moduleType, forceUpdate, done) => { + async.eachSeries(modules, (moduleShortName, callback) => { + const moduleName = 'nodeplayer-' + moduleType + '-' + moduleShortName; + if (!checkModule(moduleName) || forceUpdate) { + // perform install / update + installModule(moduleName, callback); + } else { + // skip already installed + callback(); + } + }, done); +}; + +/* +var initModule = (moduleShortName, moduleType, callback) => { + var moduleName = 'nodeplayer-' + moduleType + '-' + moduleShortName; + var module = require(moduleName); + + module.init((err) => { + callback(err, module); + }); +}; +*/ + +exports.loadBackends = (player, backends, forceUpdate, done) => { + async.mapSeries(backends, (moduleName, callback) => { + moduleName = 'nodeplayer-backend-' + moduleName; + + const Backend = require(moduleName); + const backend = new Backend(err => { + // defer execution in case callback was called synchronously + process.nextTick(() => { + if (err) { + backend.log.error('while initializing: ' + err); + return callback(); + } + + player.callHooks('onBackendInitialized', [backend.name]); + backend.log.verbose('backend initialized'); + callback(null, { [backend.name]: backend }); + }); + }); + }, (err, results) => { + done(Object.assign({}, ...results)); + }); +}; + +/* + // first install missing backends + // installModules(backends, 'backend', forceUpdate, () => { + // then initialize all backends in parallel + async.map(backends, (backend, callback) => { + const moduleLogger = labeledLogger(backend); + const moduleName = 'nodeplayer-backend-' + backend; + if (moduleName) { + moduleLogger.verbose('initializing...'); + + const Module = require(moduleName); + const instance = new Module(err => { + if (err) { + moduleLogger.error('while initializing: ' + err); + callback(); + } else { + moduleLogger.verbose('backend initialized'); + player.callHooks('onBackendInitialized', [backend]); + callback(null, instance); + } + }); + } else { + // skip module whose installation failed + moduleLogger.info('not loading backend: ' + backend); + callback(); + } + }, (err, results) => { + logger.info('all backend modules initialized'); + results = _.filter(results, _.identity); + done(results); + }); + }); +}; +*/ + +// TODO: this probably doesn't work +// needs rewrite +exports.loadPlugins = (player, plugins, forceUpdate, done) => { + // first install missing plugins + installModules(plugins, 'plugin', forceUpdate, () => { + // then initialize all plugins in series + async.mapSeries(plugins, (plugin, callback) => { + const moduleLogger = labeledLogger(plugin); + const moduleName = 'nodeplayer-plugin-' + plugin; + if (checkModule(moduleName)) { + moduleLogger.verbose('initializing...'); + + const Module = require(moduleName); + const instance = new Module(player, err => { + if (err) { + moduleLogger.error('while initializing: ' + err); + callback(); + } else { + moduleLogger.verbose('plugin initialized'); + player.callHooks('onPluginInitialized', [plugin]); + callback(null, instance); + } + }); + } else { + // skip module whose installation failed + moduleLogger.info('not loading plugin: ' + plugin); + callback(); + } + }, (err, results) => { + logger.info('all plugin modules initialized'); + results = _.filter(results, _.identity); + done(results); + }); + }); +}; + +exports.loadBuiltinPlugins = (player, done) => { + async.mapSeries(defaultPlugins, (Plugin, callback) => { + const plugin = new Plugin(player, err => { + // defer execution in case callback was called synchronously + process.nextTick(() => { + if (err) { + plugin.log.error('while initializing: ' + err); + return callback(); + } + + plugin.log.verbose('plugin initialized'); + player.callHooks('onPluginInitialized', [plugin.name]); + callback(null, { [plugin.name]: plugin }); + }); + }); + }, (err, results) => { + done(Object.assign({}, ...results)); + }); +}; + +exports.loadBuiltinBackends = (player, done) => { + async.mapSeries(defaultBackends, (Backend, callback) => { + const backend = new Backend(err => { + // defer execution in case callback was called synchronously + process.nextTick(() => { + if (err) { + backend.log.error('while initializing: ' + err); + return callback(); + } + + player.callHooks('onBackendInitialized', [backend.name]); + backend.log.verbose('backend initialized'); + callback(null, { [backend.name]: backend }); + }); + }); + }, (err, results) => { + done(Object.assign({}, ...results)); + }); +}; diff --git a/src/player.js b/src/player.js new file mode 100644 index 0000000..5d71e7a --- /dev/null +++ b/src/player.js @@ -0,0 +1,460 @@ +'use strict'; +const _ = require('lodash'); +const async = require('async'); +const util = require('util'); +const labeledLogger = require('./logger'); +import Queue from './queue'; +const modules = require('./modules'); + +import { getConfig } from './config'; + +export default class Player { + constructor(options = {}) { + this.config = getConfig(); + this.logger = labeledLogger('core'); + this.queue = new Queue(this); + this.nowPlaying = null; + this.play = false; // TODO: integrate with Song? + this.repeat = false; + this.plugins = {}; + this.backends = {}; + this.prepareTimeouts = {}; + this.volume = 1; + this.songEndTimeout = null; + this.pluginVars = {}; + + this.init = this.init.bind(this); + } + + getQueue() { + return this.queue.serialize(); + } + + getState() { + return { + nowPlaying: this.nowPlaying, + play: this.play, + repeat: this.repeat, + volume: this.volume + }; + } + + /** + * Initializes player + */ + init() { + const player = this; + const config = player.config; + const forceUpdate = false; + + // initialize plugins & backends + async.series([ + callback => { + modules.loadBuiltinPlugins(player, plugins => { + player.plugins = plugins; + player.callHooks('onBuiltinPluginsInitialized'); + callback(); + }); + }, callback => { + modules.loadPlugins(player, config.plugins, forceUpdate, results => { + player.plugins = _.extend(player.plugins, results); + player.callHooks('onPluginsInitialized'); + callback(); + }); + }, callback => { + modules.loadBuiltinBackends(player, backends => { + player.backends = backends; + player.callHooks('onBuiltinBackendsInitialized'); + callback(); + }); + }, callback => { + modules.loadBackends(player, config.backends, forceUpdate, results => { + player.backends = _.extend(player.backends, results); + player.callHooks('onBackendsInitialized', [player.backends]); + callback(); + }); + }, + ], () => { + player.logger.info('ready'); + player.callHooks('onReady'); + }); + } + + // call hook function in all modules + // if any hooks return a truthy value, it is an error and we abort + // be very careful with calling hooks from within a hook, infinite loops are possible + callHooks(hook, argv) { + // _.find() used instead of _.each() because we want to break out as soon + // as a hook returns a truthy value (used to indicate an error, e.g. in form + // of a string) + let err = null; + + this.logger.silly('callHooks(' + hook + + (argv ? ', ' + util.inspect(argv) + ')' : ')')); + + _.find(this.plugins, plugin => { + if (plugin.hooks[hook]) { + const fun = plugin.hooks[hook]; + err = fun.apply(null, argv); + return err; + } + }); + + return err; + } + + // returns number of hook functions attached to given hook + numHooks(hook) { + let cnt = 0; + + _.find(this.plugins, plugin => { + if (plugin[hook]) { + cnt++; + } + }); + + return cnt; + } + + /** + * Returns currently playing song + * @return {Song|null} - Song object, null if no now playing song + */ + getNowPlaying() { + return this.nowPlaying ? this.nowPlaying.serialize() : null; + } + + // TODO: handling of pause in a good way? + /** + * Stop playback of current song + * @param {Boolean} [pause=false] - If true, don't reset song position + */ + stopPlayback(pause) { + this.logger.info('playback ' + (pause ? 'paused.' : 'stopped.')); + + clearTimeout(this.songEndTimeout); + this.play = false; + + const np = this.nowPlaying; + if (np) { + const pos = np.playback.startPos + (new Date().getTime() - np.playback.startTime); + + np.playback = { + startTime: 0, + startPos: pause ? pos : 0, + }; + console.log(np.playback); + } + + if (!pause) { + this.nowPlaying = null; + } + + this.callHooks('onStopPlayback', [np ? np.serialize() : null]); + } + + /** + * Start playing now playing song, at optional position + * @param {Number} [position=0] - Position at which playback is started + * @throws {Error} if an error occurred + */ + startPlayback(position) { + console.log('pos', position); + clearTimeout(this.songEndTimeout); + + if (!this.nowPlaying) { + // find first song in queue + this.nowPlaying = this.queue.songs[0]; + + if (!this.nowPlaying) { + throw new Error('queue is empty! not starting playback.'); + } + } + + position = _.isNumber(position) ? position : this.nowPlaying.playback.startPos; + + this.prepareSong(this.nowPlaying, (err) => { + if (err) { + throw new Error('error while preparing now playing: ' + err); + } + + console.log(position); + console.log(this.nowPlaying.playback.startPos); + this.nowPlaying.playbackStarted(position); + + this.callHooks('onStartPlayback', [this.nowPlaying.serialize()]); + + this.logger.info('playback started.'); + this.play = true; + this.songEndTimeout = setTimeout(this.songEnd.bind(this), this.nowPlaying.duration - position); + }); + } + + /** + * Change to song + * @param {String} uuid - UUID of song to change to, if not found in queue, now + * playing is removed, playback stopped + */ + changeSong(uuid) { + this.nowPlaying = this.queue.findSong(uuid); + + if (!this.nowPlaying) { + this.logger.info('song not found: ' + uuid); + throw new Error('song not found', uuid); + } + + this.logger.info('changing song to: ' + uuid); + this.startPlayback(0); + } + + endOfQueue() { + this.logger.info('hit end of queue.'); + + if (this.repeat) { + this.logger.info('repeat is on, restarting playback from start of queue.'); + this.changeSong(this.queue.uuidAtIndex(0)); + } else { + this.stopPlayback(); + } + } + + songEnd() { + const np = this.getNowPlaying(); + const npIndex = np ? this.queue.findSongIndex(np.uuid) : -1; + + this.logger.info('end of song ' + np.uuid); + this.callHooks('onSongEnd', [this.queue.findSong(np.uuid)]); + + const nextSong = this.queue.songs[npIndex + 1]; + if (nextSong) { + this.changeSong(nextSong.uuid); + } else { + this.endOfQueue(); + } + + this.prepareSongs(); + } + + // TODO: move these to song class? + setPrepareTimeout(song) { + const player = this; + + if (song.prepareTimeout) { + clearTimeout(song.prepareTimeout); + } + + song.prepareTimeout = setTimeout(() => { + player.logger.info('prepare timeout for song: ' + song.songId + ', removing'); + song.cancelPrepare('prepare timeout'); + song.prepareTimeout = null; + }, this.config.songPrepareTimeout); + + Object.defineProperty(song, 'prepareTimeout', { + enumerable: false, + writable: true, + }); + } + + clearPrepareTimeout(song) { + // clear prepare timeout + clearTimeout(song.prepareTimeout); + song.prepareTimeout = null; + } + + prepareError(song, err) { + // TODO: mark song as failed + this.callHooks('onSongPrepareError', [song, err]); + } + + prepareProgCallback(song, bytesWritten, done) { + /* progress callback + * when this is called, new song data has been flushed to disk */ + + const np = this.getNowPlaying(); + + /* + * TODO! + // start playback if it hasn't been started yet + if (this.play && this.getNowPlaying() && + np.uuid === song.uuid && + !np.playback.startTime && bytesWritten) { + this.startPlayback(); + } + */ + + // tell plugins that new data is available for this song, and + // whether the song is now fully written to disk or not. + this.callHooks('onPrepareProgress', [song, bytesWritten, done]); + + if (done) { + // mark song as prepared + this.callHooks('onSongPrepared', [song]); + + // done preparing, can't cancel anymore + delete (song.cancelPrepare); + + // song data should now be available on disk, don't keep it in memory + song.backend.songsPreparing[song.songId].songData = undefined; + delete (song.backend.songsPreparing[song.songId]); + + // clear prepare timeout + this.clearPrepareTimeout(song); + } else { + // reset prepare timeout + this.setPrepareTimeout(song); + } + } + + prepareErrCallback(song, err, callback) { + /* error callback */ + + // don't let anything run cancelPrepare anymore + delete (song.cancelPrepare); + + this.clearPrepareTimeout(song); + + // abort preparing more songs; current song will be deleted -> + // onQueueModified is called -> song preparation is triggered again + callback(true); + + // TODO: investigate this, should probably be above callback + this.prepareError(song, err); + + song.songData = undefined; + delete (this.songsPreparing[song.backend.name][song.songId]); + } + + prepareSong(song, callback) { + const self = this; + + if (!song) { + throw new Error('prepareSong() without song'); + } + + if (song.isPrepared()) { + const np = this.getNowPlaying(); + + // start playback if it hasn't been started yet + /* + if (this.play && this.getNowPlaying() && + np.uuid === song.uuid && + !np.playback.startTime) { + this.startPlayback(); + } + */ + + // song is already prepared, ok to prepare more songs + callback(); + } else { + // song is not prepared and not currently preparing: let backend prepare it + this.logger.debug('DEBUG: prepareSong() ' + song.songId); + + song.prepare((err, chunk, done) => { + if (err) { + return callback(err); + } + + if (chunk) { + self.prepareProgCallback(song, chunk, done); + } + + if (done) { + self.clearPrepareTimeout(song); + callback(); + } + }); + + this.setPrepareTimeout(song); + } + } + + /** + * Prepare now playing and next song for playback + */ + prepareSongs() { + const player = this; + + let currentSong; + async.series([ + callback => { + // prepare now-playing song + if (player.getNowPlaying()) { + currentSong = player.queue.findSong(player.getNowPlaying().uuid); + } + + if (currentSong) { + player.prepareSong(currentSong, callback); + } else if (player.queue.getLength()) { + // songs exist in queue, prepare first one + currentSong = player.queue.songs[0]; + player.prepareSong(currentSong, callback); + } else { + // bail out + callback(true); + } + }, + callback => { + // prepare next song in playlist + const nextSong = player.queue.songs[player.queue.findSongIndex(currentSong) + 1]; + if (nextSong) { + player.prepareSong(nextSong, callback); + } else { + // bail out + callback(true); + } + }, + ]); + // TODO where to put this + // player.prepareErrCallback(); + } + + getPlaylists(callback) { + let resultCnt = 0; + const allResults = {}; + const player = this; + + _.each(this.backends, backend => { + if (!backend.getPlaylists) { + resultCnt++; + + // got results from all services? + if (resultCnt >= Object.keys(player.backends).length) { + callback(allResults); + } + return; + } + + backend.getPlaylists((err, results) => { + resultCnt++; + + allResults[backend.name] = results; + + // got results from all services? + if (resultCnt >= Object.keys(player.backends).length) { + callback(allResults); + } + }); + }); + } + + // make a search query to backends + searchBackends(query, done) { + async.mapValues(this.backends, (backend, backendName, callback) => { + backend.search(query, (err, results) => { + if (err) { + this.logger.error('error while searching ' + backend.name + ': ' + err); + results.error = err; + } + + callback(null, results); + }); + }, done); + } + + // TODO: userID does not belong into core...? + setVolume(newVol, userID) { + newVol = Math.min(1, Math.max(0, newVol)); + this.volume = newVol; + this.callHooks('onVolumeChange', [newVol, userID]); + } +} diff --git a/src/plugins/defaults.js b/src/plugins/defaults.js new file mode 100644 index 0000000..cbdff32 --- /dev/null +++ b/src/plugins/defaults.js @@ -0,0 +1,15 @@ +import Server from './server'; +import Rest from './rest'; +import WebSockets from './ws'; +import StoreQueue from './storeQueue'; + +/** + * Export default plugins + */ +const defaultPlugins = []; +defaultPlugins.push(Server); +defaultPlugins.push(Rest); // NOTE: must be initialized after Server +defaultPlugins.push(WebSockets); // NOTE: must be initialized after Server +defaultPlugins.push(StoreQueue); + +export default defaultPlugins; diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 0000000..5b3d021 --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,24 @@ +import config from '../config'; +import labeledLogger from '../logger'; + +const coreConfig = config.getConfig(); + +/** + * Super constructor for plugins + */ +export default class Plugin { + constructor(defaultConfig) { + this.name = this.constructor.name.toLowerCase(); + this.log = labeledLogger(this.name); + this.hooks = {}; + this.coreConfig = coreConfig; + + if (defaultConfig) { + this.config = config.getConfig(this, defaultConfig); + } + } + + registerHook(hook, callback) { + this.hooks[hook] = callback; + } +} diff --git a/src/plugins/rest.js b/src/plugins/rest.js new file mode 100644 index 0000000..9c9c484 --- /dev/null +++ b/src/plugins/rest.js @@ -0,0 +1,583 @@ +'use strict'; + +const _ = require('lodash'); + +const async = require('async'); +const path = require('path'); +import Plugin from '.'; +import Boom from 'boom'; +import Inert from 'inert'; + +export default class Rest extends Plugin { + constructor(player, callback) { + super(); + + if (!player.server) { + return callback('module must be initialized after Server module!'); + } + + this.pendingRequests = {}; + + player.server.register(Inert); + + player.server.route({ + method: 'GET', + path: '/api/v1', + handler: (request, reply) => { + reply('Hello world!\n'); + } + }); + + player.server.route({ + method: 'GET', + path: '/api/v1/queue', + handler: (request, reply) => { + reply(player.getQueue()); + } + }); + + player.server.route({ + method: 'GET', + path: '/api/v1/state', + handler: (request, reply) => { + reply(player.getState()); + } + }); + + player.server.route({ + method: 'POST', + path: '/api/v1/search', + handler: (request, reply) => { + player.searchBackends(request.payload, (err, results) => { + reply(err ? Boom.badImplementation(err) : results); + }); + } + }); + + player.server.route({ + method: 'DELETE', + path: '/api/v1/queue/delete', + handler: (request, reply) => { + const err = player.queue.removeSongs(request.payload.at, Number(request.payload.cnt) || 1); + reply(err ? Boom.badImplementation(err) : { success: true }); + } + }); + + player.server.route({ + method: 'POST', + path: '/api/v1/queue/song', + handler: (request, reply) => { + const at = player.queue.uuidAtIndex(player.queue.getLength() - 1) || null; + const err = player.queue.insertSongs(at, request.payload); + reply(err ? Boom.badImplementation(err) : { success: true }); + } + }); + + player.server.route({ + method: 'POST', + path: '/api/v1/play', + handler: (request, reply) => { + const err = player.startPlayback(); + reply(err ? Boom.badImplementation(err) : { success: true }); + } + }); + + player.server.route({ + method: 'POST', + path: '/api/v1/stop', + handler: (request, reply) => { + const err = player.stopPlayback(); + reply(err ? Boom.badImplementation(err) : { success: true }); + } + }); + + player.server.route({ + method: 'POST', + path: '/api/v1/pause', + handler: (request, reply) => { + const err = player.stopPlayback(true); + reply(err ? Boom.badImplementation(err) : { success: true }); + } + }); + + player.server.route({ + method: 'POST', + path: '/api/v1/seek', + handler: (request, reply) => { + const err = player.startPlayback(request.payload.position); + reply(err ? Boom.badImplementation(err) : { success: true }); + } + }); + + player.server.route({ + method: 'POST', + path: '/api/v1/changeSong', + handler: (request, reply) => { + player.changeSong(request.payload.uuid); + } + }) + player.server.route({ + method: 'POST', + path: '/api/v1/skip', + handler: (request, reply) => { + let nowPlayingIndex = -1; + let cnt = 1; + + if (request.payload && _.isNumber(request.payload.cnt)) { + cnt = request.payload.cnt; + } + + const nowPlaying = player.getNowPlaying(); + if (nowPlaying) { + nowPlayingIndex = player.queue.findSongIndex(nowPlaying.uuid); + } + + const nextSongUuid = player.queue.uuidAtIndex(nowPlayingIndex + cnt); + if (nextSongUuid) { + player.changeSong(nextSongUuid); + } else { + player.endOfQueue(); + } + + reply({ success: true }); + } + }); + + this.registerHook('onPrepareProgress', (song, bytesWritten, done) => { + if (!this.pendingRequests[song.backend.name]) { + return; + } + + _.each(this.pendingRequests[song.backend.name][song.songId], client => { + if (bytesWritten) { + let end = song.prepare.dataPos; + if (client.wishRange[1]) { + end = Math.min(client.wishRange[1], bytesWritten - 1); + } + + // console.log('end: ' + end + '\tclient.serveRange[1]: ' + client.serveRange[1]); + + if (client.serveRange[1] < end) { + // console.log('write'); + client.res.write(song.prepare.data.slice(client.serveRange[1] + 1, end)); + } + + client.serveRange[1] = end; + } + + if (done) { + this.log.debug('done'); + client.res.end(); + } + }); + + if (done) { + this.pendingRequests[song.backend.name][song.songId] = []; + } + }); + + this.registerHook('onBackendInitialized', backendName => { + this.pendingRequests[backendName] = {}; + + console.log('installing backend route'); + // provide API path for music data, might block while song is preparing + player.server.route({ + method: 'GET', + path: `/api/v1/song/${backendName}/{fileName}`, + handler: (request, reply) => { + const extIndex = request.params.fileName.lastIndexOf('.'); + const songId = request.params.fileName.substring(0, extIndex); + const songFormat = request.params.fileName.substring(extIndex + 1); + + const backend = player.backends[backendName]; + const filename = path.join(backendName, songId + '.' + songFormat); + + //const response = reply(); + + //res.setHeader('Content-Type', 'audio/ogg; codecs=opus'); + //res.setHeader('Accept-Ranges', 'bytes'); + //response.header('Content-Type', 'audio/ogg; codecs=opus'); + //response.header('Accept-Ranges', 'audio/ogg; codecs=opus'); + // + const setHeaders = (response) => { + return response.header('Content-Type', 'audio/ogg; codecs=opus'); + }; + + const queuedSong = _.find(player.queue.serialize(), song => { + return song.songId === songId && song.backendName === backendName; + }); + + if (backend.isPrepared({ songId: songId })) { + // song should be available on disk + const response = reply.file(filename, { + confine: this.coreConfig.songCachePath, + }); + + setHeaders(response); + } else if (backend.songsPreparing[songId]) { + reply(Boom.notImplemented('TODO: Fetching in-progress songs not yet supported')); + } else { + reply(Boom.notFound('Song not found')); + } + /* + async.series([ + callback => { + // try finding out length of song + if (queuedSong) { + //res.setHeader('X-Content-Duration', queuedSong.duration / 1000); + response.header('X-Content-Duration', queuedSong.duration / 1000); + callback(); + } else { + backend.getDuration({ songId: songId }, (err, exactDuration) => { + //res.setHeader('X-Content-Duration', exactDuration / 1000); + response.header('X-Content-Duration', exactDuration / 1000); + callback(); + }); + } + }, + callback => { + if (backend.isPrepared({ songId: songId })) { + // song should be available on disk + response.file(filename, { + confine: this.coreConfig.songCachePath, + }); + } else if (backend.songsPreparing[songId]) { + response(Boom.notImplemented('TODO: Fetching in-progress songs not yet supported')); + /* + // song is preparing + const song = backend.songsPreparing[songId]; + + const haveRange = []; + let wishRange = []; + const serveRange = []; + + haveRange[0] = 0; + haveRange[1] = song.prepare.data.length - 1; + + wishRange[0] = 0; + wishRange[1] = null; + + serveRange[0] = 0; + + res.setHeader('Transfer-Encoding', 'chunked'); + + if (request.headers.range) { + // partial request + + wishRange = request.headers.range.substr(request.headers.range.indexOf('=') + 1).split('-'); + + serveRange[0] = wishRange[0]; + + // a best guess for the response header + serveRange[1] = haveRange[1]; + if (wishRange[1]) { + serveRange[1] = Math.min(wishRange[1], haveRange[1]); + } + + res.statusCode = 206; + res.setHeader('Content-Range', 'bytes ' + serveRange[0] + '-' + serveRange[1] + '/*'); + } else { + serveRange[1] = haveRange[1]; + } + + this.log.debug('request with wishRange: ' + wishRange); + + if (!this.pendingRequests[backendName][songId]) { + this.pendingRequests[backendName][songId] = []; + } + + const client = { + res: res, + serveRange: serveRange, + wishRange: wishRange, + filepath: path.join(config.songCachePath, filename), + }; + + // TODO: If we know that we have already flushed data to disk, + // we could open up the read stream already here instead of waiting + // around for the first flush + + // If we can satisfy the start of the requested range, write as + // much as possible to res immediately + if (haveRange[1] >= wishRange[0]) { + client.res.write(song.prepare.data.slice(serveRange[0], serveRange[1] + 1)); + } + + if (serveRange[1] === wishRange[1]) { + client.res.end(); + } else { + // If we couldn't satisfy the entire request, push the client + // into pendingRequests so we can append to the stream later + this.pendingRequests[backendName][songId].push(client); + + request.on('close', () => { + this.pendingRequests[backendName][songId].splice( + this.pendingRequests[backendName][songId].indexOf(client), 1 + ); + }); + } + } else { + response(Boom.notFound('Song not found')); + } + }] + ); + */ + } + }); + }); + + callback(); + + /* + player.app.use((req, res, next) => { + res.sendRes = (err, data) => { + if (err) { + res.status(404).send(err); + } else { + res.send(data || 'ok'); + } + }; + next(); + }); + + player.app.get('/queue', (req, res) => { + const np = player.nowPlaying; + let pos = 0; + if (np) { + if (np.playback.startTime) { + pos = new Date().getTime() - np.playback.startTime + np.playback.startPos; + } else { + pos = np.playback.startPos; + } + } + + res.json({ + songs: player.queue.serialize(), + nowPlaying: np ? np.serialize() : null, + nowPlayingPos: pos, + play: player.play, + }); + }); + + // TODO: error handling + player.app.post('/queue/song', (req, res) => { + const err = player.queue.insertSongs(null, req.body); + + res.sendRes(err); + }); + player.app.post('/queue/song/:at', (req, res) => { + const err = player.queue.insertSongs(req.params.at, req.body); + + res.sendRes(err); + }); + + */ + /* + player.app.post('/queue/move/:pos', (req, res) => { + var err = player.moveInQueue( + Number(req.params.pos), + Number(req.body.to), + Number(req.body.cnt) + ); + sendResponse(res, 'success', err); + }); + */ + + /* + player.app.delete('/queue/song/:at', (req, res) => { + player.removeSongs(req.params.at, Number(req.query.cnt) || 1, res.sendRes); + }); + + player.app.post('/playctl/play', (req, res) => { + player.startPlayback(Number(req.body.position) || 0); + res.sendRes(null, 'ok'); + }); + + player.app.post('/playctl/stop', (req, res) => { + player.stopPlayback(req.query.pause); + res.sendRes(null, 'ok'); + }); + + player.app.post('/playctl/skip', (req, res) => { + player.skipSongs(Number(req.body.cnt)); + res.sendRes(null, 'ok'); + }); + + player.app.post('/playctl/shuffle', (req, res) => { + player.shuffleQueue(); + res.sendRes(null, 'ok'); + }); + + player.app.post('/volume', (req, res) => { + player.setVolume(Number(req.body)); + res.send('success'); + }); + + // search for songs, search terms in query params + player.app.post('/search', (req, res) => { + this.log.verbose('got search request: ' + JSON.stringify(req.body)); + + player.searchBackends(req.body, (err, results) => { + // console.log(results); + res.json(results); + }); + }); + + this.pendingRequests = {}; + const rest = this; + this.registerHook('onPrepareProgress', (song, bytesWritten, done) => { + if (!rest.pendingRequests[song.backend.name]) { + return; + } + + _.each(rest.pendingRequests[song.backend.name][song.songId], client => { + if (bytesWritten) { + let end = song.prepare.dataPos; + if (client.wishRange[1]) { + end = Math.min(client.wishRange[1], bytesWritten - 1); + } + + // console.log('end: ' + end + '\tclient.serveRange[1]: ' + client.serveRange[1]); + + if (client.serveRange[1] < end) { + // console.log('write'); + client.res.write(song.prepare.data.slice(client.serveRange[1] + 1, end)); + } + + client.serveRange[1] = end; + } + + if (done) { + this.log.debug('done'); + client.res.end(); + } + }); + + if (done) { + rest.pendingRequests[song.backend.name][song.songId] = []; + } + }); + + this.registerHook('onBackendInitialized', backendName => { + rest.pendingRequests[backendName] = {}; + + // provide API path for music data, might block while song is preparing + player.app.get('/song/' + backendName + '/:fileName', (req, res, next) => { + const extIndex = req.params.fileName.lastIndexOf('.'); + const songId = req.params.fileName.substring(0, extIndex); + const songFormat = req.params.fileName.substring(extIndex + 1); + + const backend = player.backends[backendName]; + const filename = path.join(backendName, songId + '.' + songFormat); + + res.setHeader('Content-Type', 'audio/ogg; codecs=opus'); + res.setHeader('Accept-Ranges', 'bytes'); + + const queuedSong = _.find(player.queue.serialize(), song => { + return song.songId === songId && song.backendName === backendName; + }); + + async.series([ + callback => { + // try finding out length of song + if (queuedSong) { + res.setHeader('X-Content-Duration', queuedSong.duration / 1000); + callback(); + } else { + backend.getDuration({ songId: songId }, (err, exactDuration) => { + res.setHeader('X-Content-Duration', exactDuration / 1000); + callback(); + }); + } + }, + callback => { + if (backend.isPrepared({ songId: songId })) { + // song should be available on disk + res.sendFile(filename, { + root: config.songCachePath, + }); + } else if (backend.songsPreparing[songId]) { + // song is preparing + const song = backend.songsPreparing[songId]; + + const haveRange = []; + let wishRange = []; + const serveRange = []; + + haveRange[0] = 0; + haveRange[1] = song.prepare.data.length - 1; + + wishRange[0] = 0; + wishRange[1] = null; + + serveRange[0] = 0; + + res.setHeader('Transfer-Encoding', 'chunked'); + + if (req.headers.range) { + // partial request + + wishRange = req.headers.range.substr(req.headers.range.indexOf('=') + 1).split('-'); + + serveRange[0] = wishRange[0]; + + // a best guess for the response header + serveRange[1] = haveRange[1]; + if (wishRange[1]) { + serveRange[1] = Math.min(wishRange[1], haveRange[1]); + } + + res.statusCode = 206; + res.setHeader('Content-Range', 'bytes ' + serveRange[0] + '-' + serveRange[1] + '/*'); + } else { + serveRange[1] = haveRange[1]; + } + + this.log.debug('request with wishRange: ' + wishRange); + + if (!rest.pendingRequests[backendName][songId]) { + rest.pendingRequests[backendName][songId] = []; + } + + const client = { + res: res, + serveRange: serveRange, + wishRange: wishRange, + filepath: path.join(config.songCachePath, filename), + }; + + // TODO: If we know that we have already flushed data to disk, + // we could open up the read stream already here instead of waiting + // around for the first flush + + // If we can satisfy the start of the requested range, write as + // much as possible to res immediately + if (haveRange[1] >= wishRange[0]) { + client.res.write(song.prepare.data.slice(serveRange[0], serveRange[1] + 1)); + } + + if (serveRange[1] === wishRange[1]) { + client.res.end(); + } else { + // If we couldn't satisfy the entire request, push the client + // into pendingRequests so we can append to the stream later + rest.pendingRequests[backendName][songId].push(client); + + req.on('close', () => { + rest.pendingRequests[backendName][songId].splice( + rest.pendingRequests[backendName][songId].indexOf(client), 1 + ); + }); + } + } else { + res.status(404).end('404 song not found'); + } + }] + ); + }); + }); + + callback(null); + */ + } +} diff --git a/src/plugins/server.js b/src/plugins/server.js new file mode 100644 index 0000000..d3b4dc9 --- /dev/null +++ b/src/plugins/server.js @@ -0,0 +1,64 @@ +'use strict'; + +import Plugin from '.'; + +import Hapi from 'hapi'; +//const bodyParser = require('body-parser'); +//const cookieParser = require('cookie-parser'); +//const https = require('https'); +//const http = require('http'); +//const fs = require('fs'); + +export default class Server extends Plugin { + constructor(player, callback) { + super(); + + const server = new Hapi.Server(); + server.connection({ + port: this.coreConfig.port, + routes: { + cors: true + } + }); + + server.start(err => { + if (err) { + return callback(err); + } else { + this.log.info(`listening on port ${this.coreConfig.port}`); + player.server = server; + + callback(); + } + }); + + /* + // NOTE: no argument passed so we get the core's config + player.app = express(); + + let options = {}; + const port = process.env.PORT || config.port; + if (config.tls) { + options = { + tls: config.tls, + key: config.key ? fs.readFileSync(config.key) : undefined, + cert: config.cert ? fs.readFileSync(config.cert) : undefined, + ca: config.ca ? fs.readFileSync(config.ca) : undefined, + requestCert: config.requestCert, + rejectUnauthorized: config.rejectUnauthorized, + }; + // TODO: deprecated! + player.app.set('tls', true); + player.httpServer = https.createServer(options, player.app) + .listen(port); + } else { + player.httpServer = http.createServer(player.app) + .listen(port); + } + + player.app.use(cookieParser()); + player.app.use(bodyParser.json({ limit: '100mb' })); + player.app.use(bodyParser.urlencoded({ extended: true })); + */ + } +} diff --git a/src/plugins/storeQueue.js b/src/plugins/storeQueue.js new file mode 100644 index 0000000..676dc5e --- /dev/null +++ b/src/plugins/storeQueue.js @@ -0,0 +1,80 @@ +'use strict'; + +import Plugin from '.'; +import Song from '../song'; +import fs from 'fs'; + +export default class StoreQueue extends Plugin { + constructor(player, callback) { + super(); + + this.path = this.coreConfig.queueStorePath; + this.initialized = false; + this.player = player; + + this.registerHook('onQueueModify', () => { + if (!this.path || !this.initialized) { + return; + } + + this.storeQueue(); + }); + + this.registerHook('onStartPlayback', () => { + if (!this.path || !this.initialized) { + return; + } + + this.storeQueue(); + }); + + this.registerHook('onBackendsInitialized', (backends) => { + if (this.path && fs.existsSync(this.path)) { + fs.readFile(this.path, (err, data) => { + if (!err) { + data = JSON.parse(data); + + const queue = data.queue; + player.queue.insertSongs(null, queue, true); + + let np = data.nowPlaying; + if (np) { + np = new Song(np, backends[np.backendName], true); + player.nowPlaying = np; + player.startPlayback(data.nowPlaying.playback.curPos); + } + } + + this.initialized = true; + }); + } + process.once('SIGINT', () => { + console.log('SIGINT received, saving queue'); + this.storeQueue(true); + }); + process.once('SIGUSR2', () => { + console.log('SIGUSR2 received, saving queue'); + this.storeQueue(true); + }); + }); + + callback(); + } + + storeQueue(quit) { + let np = this.player.nowPlaying ? this.player.nowPlaying.serialize() : null; + + if (np) { + np.playback.startPos = np.playback.curPos; + } + + fs.writeFile(this.path, JSON.stringify({ + queue: this.player.queue.serialize(), + nowPlaying: np + }, '', 4), () => { + if (quit) { + process.exit(0); + } + }); + } +} diff --git a/src/plugins/ws.js b/src/plugins/ws.js new file mode 100644 index 0000000..77e2cf5 --- /dev/null +++ b/src/plugins/ws.js @@ -0,0 +1,112 @@ +'use strict'; + +import Plugin from '.'; +import Sockjs from 'sockjs'; +import _ from 'lodash'; + +const sockjsOpts = { + sockjs_url: "http://cdn.jsdelivr.net/sockjs/1.0.1/sockjs.min.js" +}; + +export default class WebSockets extends Plugin { + constructor(player, callback) { + super(); + + this.clients = {}; + + const sockjs = Sockjs.createServer(sockjsOpts); + + sockjs.installHandlers(player.server.listener, { + prefix: '/ws' + }); + + sockjs.on('connection', (conn) => { + this.clients[conn.id] = conn; + + const np = player.getNowPlaying(); + let queue = player.queue.serialize(); + + if (np) { + const npPos = player.queue.findSongIndex(np.uuid); + if (npPos > 0) { + queue = queue.slice(npPos - 1); + } + } + + conn.write(JSON.stringify({ + jsonrpc: '2.0', + method: 'sync', + params: { + nowPlaying: np, + queue: queue + } + })); + + conn.on('close', () => { + delete this.clients[conn.id]; + }); + }); + + player.ws = sockjs; + + this.registerHook('onStartPlayback', (song) => { + const np = player.getNowPlaying(); + let queue = player.queue.serialize(); + + if (np) { + const npPos = player.queue.findSongIndex(np.uuid); + if (npPos > 0) { + queue = queue.slice(npPos - 1); + } + } + + this.broadcast({ + jsonrpc: '2.0', + method: 'queue', + params: queue + }); + + this.broadcast({ + jsonrpc: '2.0', + method: 'play', + params: song + }); + }); + + this.registerHook('onStopPlayback', (song) => { + this.broadcast({ + jsonrpc: '2.0', + method: 'stop', + params: song + }); + }); + + this.registerHook('onQueueModify', () => { + const np = player.getNowPlaying(); + let queue = player.queue.serialize(); + + if (np) { + const npPos = player.queue.findSongIndex(np.uuid); + if (npPos > 0) { + queue = queue.slice(npPos - 1); + } + } + + this.broadcast({ + jsonrpc: '2.0', + method: 'queue', + params: queue + }); + }); + + callback(); + } + + broadcast(message) { + // iterate through each client in clients object + _.forOwn(this.clients, (client) => { + // send the message to that client + client.write(JSON.stringify(message)); + }); + } +} diff --git a/src/queue.js b/src/queue.js new file mode 100644 index 0000000..015f79c --- /dev/null +++ b/src/queue.js @@ -0,0 +1,194 @@ +import _ from 'lodash'; +import Song from './song'; + +/** + * Constructor + * @param {Player} player - Parent player object reference + * @throws {Error} in case of errors + */ +export default class Queue { + constructor(player) { + if (!player || !_.isObject(player)) { + throw new Error('Queue constructor called without player reference!'); + } + + this.unshuffledSongs = null; + this.songs = []; + this.player = player; + } + + // TODO: hooks + // TODO: moveSongs + + /** + * Get serialized list of songs in queue + * @return {[SerializedSong]} - List of songs in serialized format + */ + serialize() { + const serialized = _.map(this.songs, song => { + return song.serialize(); + }); + + return serialized; + } + + /** + * Find index of song in queue + * @param {String} at - Look for song with this UUID + * @return {Number} - Index of song, -1 if not found + */ + findSongIndex(at) { + return _.findIndex(this.songs, song => { + return song.uuid === at; + }); + } + + /** + * Find song in queue + * @param {String} at - Look for song with this UUID + * @return {Song|null} - Song object, null if not found + */ + findSong(at) { + return _.find(this.songs, song => { + return song.uuid === at; + }) || null; + } + + /** + * Find song UUID at given index + * @param {Number} index - Look for song at this index + * @return {String|null} - UUID, null if not found + */ + uuidAtIndex(index) { + const song = this.songs[index]; + return song ? song.uuid : null; + } + + /** + * Returns queue length + * @return {Number} - Queue length + */ + getLength() { + return this.songs.length; + } + + /** + * Insert songs into queue + * @param {String | null} at - Insert songs after song with this UUID + * (null = start of queue) + * @param {Song | Object[]} songs - Song or list of songs to insert + * @return {Error} - in case of errors + */ + insertSongs(at, songs, forceUuid) { + let pos; + if (at === null) { + // insert at start of queue + pos = 0; + } else { + // insert song after song with UUID + pos = this.findSongIndex(at); + + if (pos < 0) { + return 'Song with UUID ' + at + ' not found!'; + } + + pos++; // insert after song + } + + if (!_.isArray(songs)) { + songs = [songs]; + } + + // generate Song objects of each song + songs = _.map(songs, song => { + // TODO: this would be best done in the song constructor, + // effectively making it a SerializedSong object deserializer + const backend = this.player.backends[song.backendName]; + if (!backend) { + throw new Error('Song constructor called with invalid backend: ' + song.backendName); + } + + return new Song(song, backend, forceUuid); + }, this); + + // perform insertion + const args = [pos, 0].concat(songs); + Array.prototype.splice.apply(this.songs, args); + + this.player.logger.verbose('Inserted songs:', songs.map((song) => { + return _.pick(song.serialize(), [ + 'backendName', + 'title', + 'artist', + 'album', + 'uuid' + ]); + })); + this.player.callHooks('onQueueModify', [this.serialize()]); + this.player.prepareSongs(); + } + + /** + * Removes songs from queue + * @param {String} at - Start removing at song with this UUID + * @param {Number} cnt - Number of songs to delete + * @return {Song[] | Error} - List of removed songs, Error in case of errors + */ + removeSongs(at, cnt) { + const pos = this.findSongIndex(at); + if (pos < 0) { + return 'Song with UUID ' + at + ' not found!'; + } + + // cancel preparing all songs to be deleted + for (let i = pos; i < pos + cnt && i < this.songs.length; i++) { + const song = this.songs[i]; + if (song.cancelPrepare) { + song.cancelPrepare('Song removed.'); + } + } + + // store index of now playing song + const np = this.player.nowPlaying; + const npIndex = np ? this.findSongIndex(np.uuid) : -1; + + // perform deletion + const removed = this.songs.splice(pos, cnt); + + // was now playing removed? + if (pos <= npIndex && pos + cnt >= npIndex) { + // change to first song after splice + const newNp = this.songs[pos]; + this.player.changeSong(newNp ? newNp.uuid : null); + } else { + this.player.prepareSongs(); + } + + this.player.callHooks('onQueueModify', [this.serialize()]); + return removed; + } + + /** + * Toggle queue shuffling + */ + shuffle() { + if (this.unshuffledSongs) { + // unshuffle + + // restore unshuffled list + this.songs = this.unshuffledSongs; + + this.unshuffledSongs = null; + } else { + // shuffle + + // store copy of current songs array + this.unshuffledSongs = this.songs.slice(); + + this.songs = _.shuffle(this.songs); + } + + this.player.callHooks('onQueueModify', [this.serialize()]); + this.player.prepareSongs(); + } +} diff --git a/src/song.js b/src/song.js new file mode 100644 index 0000000..fabc770 --- /dev/null +++ b/src/song.js @@ -0,0 +1,132 @@ +const _ = require('lodash'); +const uuid = require('node-uuid'); + +/** + * Constructor + * @param {Song} song - Song details + * @param {Backend} backend - Backend providing the audio + * @throws {Error} in case of errors + */ +export default class Song { + constructor(song, backend, forceUuid) { + // make sure we have a reference to backend + if (!backend || !_.isObject(backend)) { + throw new Error('Song constructor called with invalid backend: ' + backend); + } + + if (!song.duration || !_.isNumber(song.duration)) { + throw new Error('Song constructor called without duration!'); + } + if (!song.title || !_.isString(song.title)) { + throw new Error('Song constructor called without title!'); + } + if (!song.songId || !_.isString(song.songId)) { + throw new Error('Song constructor called without songId!'); + } + if (!song.score || !_.isNumber(song.score)) { + throw new Error('Song constructor called without score!'); + } + if (!song.format || !_.isString(song.format)) { + throw new Error('Song constructor called without format!'); + } + + if (forceUuid) { + this.uuid = song.uuid; + } else { + this.uuid = uuid.v4(); + } + + this.title = song.title; + this.artist = song.artist; + this.album = song.album; + this.albumArt = { + lq: song.albumArt ? song.albumArt.lq : null, + hq: song.albumArt ? song.albumArt.hq : null, + }; + this.duration = song.duration; + this.songId = song.songId; + this.score = song.score; + this.format = song.format; + + this.playback = { + startTime: null, + startPos: null, + }; + + // NOTE: internally to the Song we store a reference to the backend. + // However when accessing the Song from the outside, we return only the + // backend's name inside a backendName field. + // + // Any functions requiring access to the backend should be implemented as + // members of the Song (e.g. isPrepared, prepareSong) + this.backend = backend; + + // optional fields + this.playlist = song.playlist; + } + + /** + * Set playback status as started at specified optional position + * @param {Number} [pos] - position to start playing at + */ + playbackStarted(pos) { + console.log(pos); + this.playback = { + startTime: new Date(), + startPos: pos || null, + }; + } + + isPlaying() { + return !!this.playback.startTime; + } + + /** + * Return serialized details of the song + * @return {SerializedSong} - serialized Song object + */ + serialize() { + return { + uuid: this.uuid, + title: this.title, + artist: this.artist, + album: this.album, + albumArt: this.albumArt, + duration: this.duration, + songId: this.songId, + score: this.score, + format: this.format, + backendName: this.backend.name, + playlist: this.playlist, + playback: { + startTime: this.playback.startTime, + startPos: this.playback.startPos, + curPos: this.playback.startPos + (this.playback.startTime ? + new Date().getTime() - this.playback.startTime.getTime() : null) + } + }; + } + + /** + * Synchronously(!) returns whether the song is prepared or not + * @return {Boolean} - true if song is prepared, false if not + */ + isPrepared() { + return this.backend.isPrepared(this); + } + + /** + * Prepare song for playback + * @param {encodeCallback} callback - Called when song is ready or on error + */ + prepare(callback) { + this.backend.prepare(this, callback); + } + + /** + * Cancel song preparation if applicable + */ + cancelPrepare() { + this.backend.cancelPrepare(this); + } +} diff --git a/test/eslint.spec.js b/test/eslint.spec.js new file mode 100644 index 0000000..7d5a344 --- /dev/null +++ b/test/eslint.spec.js @@ -0,0 +1,9 @@ +const lint = require('mocha-eslint'); + +const paths = [ + 'bin', + 'src', + 'test', +]; + +lint(paths); diff --git a/test/jscs.spec.js b/test/jscs.spec.js deleted file mode 100644 index 97f9c2f..0000000 --- a/test/jscs.spec.js +++ /dev/null @@ -1 +0,0 @@ -require('mocha-jscs')(); diff --git a/test/jshint.spec.js b/test/jshint.spec.js deleted file mode 100644 index ee8b829..0000000 --- a/test/jshint.spec.js +++ /dev/null @@ -1 +0,0 @@ -require('mocha-jshint')(); diff --git a/test/test.js b/test/test.js index 51437f9..3d193f0 100644 --- a/test/test.js +++ b/test/test.js @@ -1,18 +1,20 @@ 'use strict'; -/*jshint expr: true*/ -var should = require('chai').should(); -var _ = require('underscore'); -var Player = require('../lib/player'); -var dummyBackend = require('nodeplayer-backend-dummy'); -var exampleQueue = require('./exampleQueue.json'); +require('chai').should(); +// var _ = require('underscore'); +// var Player = require('../src/player'); +// var dummyBackend = require('nodeplayer-backend-dummy'); +// var exampleQueue = require('./exampleQueue.json'); process.env.NODE_ENV = 'test'; +/* var dummyClone = function(obj) { return JSON.parse(JSON.stringify(obj)); }; +*/ +/* var dummyLogger = { silly: _.noop, debug: _.noop, @@ -21,15 +23,19 @@ var dummyLogger = { warn: _.noop, error: _.noop, }; +*/ +/* describe('exampleQueue', function() { it('should contain at least 5 items', function() { exampleQueue.length.should.be.above(5); }); }); +*/ // TODO: test error cases also describe('Player', function() { + /* describe('#setVolume()', function() { var player; @@ -52,7 +58,9 @@ describe('Player', function() { player.volume.should.equal(0.5).and.be.a('number'); }); }); + */ + /* describe('#skipSongs()', function() { var player; var playedQueueSize = 3; // TODO: better handling of config variables here @@ -558,4 +566,5 @@ describe('Player', function() { player.queue.should.deep.equal(exampleQueue); }); }); + */ }); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..7424a25 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4209 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 +abbrev@~1.0.9, abbrev@1, abbrev@1.0.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + +accept@2.x.x: + version "2.1.3" + resolved "https://registry.yarnpkg.com/accept/-/accept-2.1.3.tgz#ab0f5bda4c449bbe926aea607b3522562f5acf86" + dependencies: + boom "4.x.x" + hoek "4.x.x" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.3.tgz#1a3e850b428e73ba6b09d1cc527f5aaad4d03ef1" + +ajv-keywords@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.1.1.tgz#02550bc605a3e576041565628af972e06c549d50" + +ajv@^4.7.0: + version "4.8.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.8.2.tgz#65486936ca36fea39a1504332a78bebd5d447bdc" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ammo@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ammo/-/ammo-2.0.2.tgz#366c55f7bc4f2f24218ed3a4dd4b8df135c2e6ca" + dependencies: + boom "3.x.x" + hoek "4.x.x" + +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi@^0.3.0, ansi@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" + +ansicolors@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + +ansistyles@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" + +anymatch@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" + dependencies: + arrify "^1.0.0" + micromatch "^2.1.5" + +ap@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ap/-/ap-0.2.0.tgz#ae0942600b29912f0d2b14ec60c45e8f330b6110" + +aproba@^1.0.3, aproba@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.0.4.tgz#2713680775e7614c8ba186c065d4e2e52d1072c0" + +archy@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + +are-we-there-yet@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz#80e470e95a084794fe1899262c5667c6e88de1b3" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.0 || ^1.1.13" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b" + +array-index@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-index/-/array-index-1.0.0.tgz#ec56a749ee103e4e08c790b9c353df16055b97f9" + dependencies: + debug "^2.2.0" + es6-symbol "^3.0.2" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asap@^2.0.0, asap@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assertion-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async-some@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/async-some/-/async-some-1.0.2.tgz#4d8a81620d5958791b5b98f802d3207776e95509" + dependencies: + dezalgo "^1.0.2" + +async@^0.9.0, async@0.9.x: + version "0.9.2" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" + +async@^1.4.0, async@1.x: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.0.0-rc.6, async@^2.0.1, async@>=0.2.9, async@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/async/-/async-2.1.2.tgz#612a4ab45ef42a70cde806bad86ee6db047e8385" + dependencies: + lodash "^4.14.0" + +async@~0.2.6: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + +async@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755" + +b64@3.x.x: + version "3.0.2" + resolved "https://registry.yarnpkg.com/b64/-/b64-3.0.2.tgz#7a9d60466adf7b8de114cbdf651a5fdfcc90894d" + +babel-cli@^6.11.4: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.18.0.tgz#92117f341add9dead90f6fa7d0a97c0cc08ec186" + dependencies: + babel-core "^6.18.0" + babel-polyfill "^6.16.0" + babel-register "^6.18.0" + babel-runtime "^6.9.0" + commander "^2.8.1" + convert-source-map "^1.1.0" + fs-readdir-recursive "^1.0.0" + glob "^5.0.5" + lodash "^4.2.0" + output-file-sync "^1.1.0" + path-is-absolute "^1.0.0" + slash "^1.0.0" + source-map "^0.5.0" + v8flags "^2.0.10" + optionalDependencies: + chokidar "^1.0.0" + +babel-code-frame@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.16.0.tgz#f90e60da0862909d3ce098733b5d3987c97cb8de" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^2.0.0" + +babel-core@^6.11.4, babel-core@^6.18.0: + version "6.18.2" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.18.2.tgz#d8bb14dd6986fa4f3566a26ceda3964fa0e04e5b" + dependencies: + babel-code-frame "^6.16.0" + babel-generator "^6.18.0" + babel-helpers "^6.16.0" + babel-messages "^6.8.0" + babel-register "^6.18.0" + babel-runtime "^6.9.1" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.11.0" + convert-source-map "^1.1.0" + debug "^2.1.1" + json5 "^0.5.0" + lodash "^4.2.0" + minimatch "^3.0.2" + path-is-absolute "^1.0.0" + private "^0.1.6" + slash "^1.0.0" + source-map "^0.5.0" + +babel-generator@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.18.0.tgz#e4f104cb3063996d9850556a45aae4a022060a07" + dependencies: + babel-messages "^6.8.0" + babel-runtime "^6.9.0" + babel-types "^6.18.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.2.0" + source-map "^0.5.0" + +babel-helper-call-delegate@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.18.0.tgz#05b14aafa430884b034097ef29e9f067ea4133bd" + dependencies: + babel-helper-hoist-variables "^6.18.0" + babel-runtime "^6.0.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + +babel-helper-function-name@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.18.0.tgz#68ec71aeba1f3e28b2a6f0730190b754a9bf30e6" + dependencies: + babel-helper-get-function-arity "^6.18.0" + babel-runtime "^6.0.0" + babel-template "^6.8.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + +babel-helper-get-function-arity@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.18.0.tgz#a5b19695fd3f9cdfc328398b47dafcd7094f9f24" + dependencies: + babel-runtime "^6.0.0" + babel-types "^6.18.0" + +babel-helper-hoist-variables@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.18.0.tgz#a835b5ab8b46d6de9babefae4d98ea41e866b82a" + dependencies: + babel-runtime "^6.0.0" + babel-types "^6.18.0" + +babel-helper-regex@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.18.0.tgz#ae0ebfd77de86cb2f1af258e2cc20b5fe893ecc6" + dependencies: + babel-runtime "^6.9.0" + babel-types "^6.18.0" + lodash "^4.2.0" + +babel-helpers@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.16.0.tgz#1095ec10d99279460553e67eb3eee9973d3867e3" + dependencies: + babel-runtime "^6.0.0" + babel-template "^6.16.0" + +babel-messages@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.8.0.tgz#bf504736ca967e6d65ef0adb5a2a5f947c8e0eb9" + dependencies: + babel-runtime "^6.0.0" + +babel-plugin-transform-es2015-destructuring@6.x: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.18.0.tgz#a08fb89415ab82058649558bedb7bf8dafa76ba5" + dependencies: + babel-runtime "^6.9.0" + +babel-plugin-transform-es2015-function-name@6.x: + version "6.9.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.9.0.tgz#8c135b17dbd064e5bba56ec511baaee2fca82719" + dependencies: + babel-helper-function-name "^6.8.0" + babel-runtime "^6.9.0" + babel-types "^6.9.0" + +babel-plugin-transform-es2015-modules-commonjs@6.x: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.18.0.tgz#c15ae5bb11b32a0abdcc98a5837baa4ee8d67bcc" + dependencies: + babel-plugin-transform-strict-mode "^6.18.0" + babel-runtime "^6.0.0" + babel-template "^6.16.0" + babel-types "^6.18.0" + +babel-plugin-transform-es2015-parameters@6.x: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.18.0.tgz#9b2cfe238c549f1635ba27fc1daa858be70608b1" + dependencies: + babel-helper-call-delegate "^6.18.0" + babel-helper-get-function-arity "^6.18.0" + babel-runtime "^6.9.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + +babel-plugin-transform-es2015-shorthand-properties@6.x: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.18.0.tgz#e2ede3b7df47bf980151926534d1dd0cbea58f43" + dependencies: + babel-runtime "^6.0.0" + babel-types "^6.18.0" + +babel-plugin-transform-es2015-spread@6.x: + version "6.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.8.0.tgz#0217f737e3b821fa5a669f187c6ed59205f05e9c" + dependencies: + babel-runtime "^6.0.0" + +babel-plugin-transform-es2015-sticky-regex@6.x: + version "6.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.8.0.tgz#e73d300a440a35d5c64f5c2a344dc236e3df47be" + dependencies: + babel-helper-regex "^6.8.0" + babel-runtime "^6.0.0" + babel-types "^6.8.0" + +babel-plugin-transform-es2015-unicode-regex@6.x: + version "6.11.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.11.0.tgz#6298ceabaad88d50a3f4f392d8de997260f6ef2c" + dependencies: + babel-helper-regex "^6.8.0" + babel-runtime "^6.0.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-strict-mode@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.18.0.tgz#df7cf2991fe046f44163dcd110d5ca43bc652b9d" + dependencies: + babel-runtime "^6.0.0" + babel-types "^6.18.0" + +babel-polyfill@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.16.0.tgz#2d45021df87e26a374b6d4d1a9c65964d17f2422" + dependencies: + babel-runtime "^6.9.1" + core-js "^2.4.0" + regenerator-runtime "^0.9.5" + +babel-preset-es2015-node@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-preset-es2015-node/-/babel-preset-es2015-node-6.1.1.tgz#60b23157024b0cfebf3a63554cb05ee035b4e55f" + dependencies: + babel-plugin-transform-es2015-destructuring "6.x" + babel-plugin-transform-es2015-function-name "6.x" + babel-plugin-transform-es2015-modules-commonjs "6.x" + babel-plugin-transform-es2015-parameters "6.x" + babel-plugin-transform-es2015-shorthand-properties "6.x" + babel-plugin-transform-es2015-spread "6.x" + babel-plugin-transform-es2015-sticky-regex "6.x" + babel-plugin-transform-es2015-unicode-regex "6.x" + semver "5.x" + +babel-register@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.18.0.tgz#892e2e03865078dd90ad2c715111ec4449b32a68" + dependencies: + babel-core "^6.18.0" + babel-runtime "^6.11.6" + core-js "^2.4.0" + home-or-tmp "^2.0.0" + lodash "^4.2.0" + mkdirp "^0.5.1" + source-map-support "^0.4.2" + +babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.9.0, babel-runtime@^6.9.1: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.18.0.tgz#0f4177ffd98492ef13b9f823e9994a02584c9078" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.9.5" + +babel-template@^6.16.0, babel-template@^6.8.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.16.0.tgz#e149dd1a9f03a35f817ddbc4d0481988e7ebc8ca" + dependencies: + babel-runtime "^6.9.0" + babel-traverse "^6.16.0" + babel-types "^6.16.0" + babylon "^6.11.0" + lodash "^4.2.0" + +babel-traverse@^6.16.0, babel-traverse@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.18.0.tgz#5aeaa980baed2a07c8c47329cd90c3b90c80f05e" + dependencies: + babel-code-frame "^6.16.0" + babel-messages "^6.8.0" + babel-runtime "^6.9.0" + babel-types "^6.18.0" + babylon "^6.11.0" + debug "^2.2.0" + globals "^9.0.0" + invariant "^2.2.0" + lodash "^4.2.0" + +babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.8.0, babel-types@^6.9.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.18.0.tgz#1f7d5a73474c59eb9151b2417bbff4e4fce7c3f8" + dependencies: + babel-runtime "^6.9.1" + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^1.0.1" + +babylon@^6.11.0: + version "6.13.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.13.1.tgz#adca350e088f0467647157652bafead6ddb8dfdb" + +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +bcrypt-pbkdf@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" + dependencies: + tweetnacl "^0.14.3" + +binary-extensions@^1.0.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.7.0.tgz#6c1610db163abfb34edfe42fa423343a1e01185d" + +bl@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.1.2.tgz#fdca871a99713aa00d19e3bbba41c44787a65398" + dependencies: + readable-stream "~2.0.5" + +block-stream@*, block-stream@0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^3.4.1, bluebird@^3.4.6: + version "3.4.6" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f" + +bluebird@2.10.2: + version "2.10.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.10.2.tgz#024a5517295308857f14f91f1106fc3b555f446b" + +boom, boom@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.2.0.tgz#c1a74174b11fbba223f6162d4fd8851a1b82a536" + dependencies: + hoek "4.x.x" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +boom@3.x.x: + version "3.2.2" + resolved "https://registry.yarnpkg.com/boom/-/boom-3.2.2.tgz#0f0cc5d04adc5003b8c7d71f42cca7271fef0e78" + dependencies: + hoek "4.x.x" + +brace-expansion@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + +bson@~0.5.4, bson@~0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/bson/-/bson-0.5.6.tgz#e03de80a692c28fca4396f0d14c97069bd2b73a6" + +buffer-shims@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + +buffer-writer@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-1.0.1.tgz#22a936901e3029afcd7547eb4487ceb697a3bf08" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtins@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-0.0.7.tgz#355219cd6cf18dbe7c01cc7fd2dce765cfdc549a" + +call@3.x.x: + version "3.0.3" + resolved "https://registry.yarnpkg.com/call/-/call-3.0.3.tgz#e4748ddbbb7f41ae40cee055f8b270b733bf7c8d" + dependencies: + boom "3.x.x" + hoek "4.x.x" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +catbox-memory@2.x.x: + version "2.0.4" + resolved "https://registry.yarnpkg.com/catbox-memory/-/catbox-memory-2.0.4.tgz#433e255902caf54233d1286429c8f4df14e822d5" + dependencies: + hoek "4.x.x" + +catbox@7.x.x: + version "7.1.2" + resolved "https://registry.yarnpkg.com/catbox/-/catbox-7.1.2.tgz#46721b1c99967513fd7b7e9451706a05edfed5ad" + dependencies: + boom "3.x.x" + hoek "4.x.x" + joi "9.x.x" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chai@*: + version "3.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" + dependencies: + assertion-error "^1.0.1" + deep-eql "^0.1.3" + type-detect "^1.0.0" + +chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +char-spinner@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081" + +chmodr@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chmodr/-/chmodr-1.0.2.tgz#04662b932d0f02ec66deaa2b0ea42811968e3eb9" + +chokidar@^1.0.0, chokidar@^1.4.3: + version "1.6.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +chownr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +circular-json@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.0.3, cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + +cmd-shim@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" + dependencies: + graceful-fs "^4.1.2" + mkdirp "~0.5.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +colors@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +columnify@~1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" + dependencies: + strip-ansi "^3.0.0" + wcwidth "^1.0.0" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.2.0, commander@^2.8.1, commander@^2.9.0, commander@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.4.6, concat-stream@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + +config-chain@~1.1.10, config-chain@~1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +configstore@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" + dependencies: + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + object-assign "^4.0.1" + os-tmpdir "^1.0.0" + osenv "^0.1.0" + uuid "^2.0.1" + write-file-atomic "^1.1.2" + xdg-basedir "^2.0.0" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +content@3.x.x: + version "3.0.3" + resolved "https://registry.yarnpkg.com/content/-/content-3.0.3.tgz#000f8a01371b95c66afe99be9390fa6cb91aa87a" + dependencies: + boom "4.x.x" + +convert-source-map@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" + +core-js@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +coveralls@^2.11.9: + version "2.11.15" + resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-2.11.15.tgz#37d3474369d66c14f33fa73a9d25cee6e099fca0" + dependencies: + js-yaml "3.6.1" + lcov-parse "0.0.10" + log-driver "1.2.5" + minimist "1.2.0" + request "2.75.0" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +cryptiles@3.x.x: + version "3.1.1" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.1.tgz#86a9203f7367a0e9324bc7555ff0fcf5f81979ee" + dependencies: + boom "4.x.x" + +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + +d@^0.1.1, d@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" + dependencies: + es5-ext "~0.10.2" + +dashdash@^1.12.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.0.tgz#29e486c5418bf0f356034a993d51686a33e84141" + dependencies: + assert-plus "^1.0.0" + +debug@^2.1.1, debug@^2.1.3, debug@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.2.tgz#94cb466ef7d6d2c7e5245cdd6e4104f2d0d70d30" + dependencies: + ms "0.7.2" + +debug@~2.2.0, debug@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + +decamelize@^1.0.0, decamelize@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-eql@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" + dependencies: + type-detect "0.1.1" + +deep-extend@~0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + dependencies: + clone "^1.0.2" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +detect-file@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63" + dependencies: + fs-exists-sync "^0.1.0" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +dezalgo@^1.0.0, dezalgo@^1.0.1, dezalgo@^1.0.2, dezalgo@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + dependencies: + asap "^2.0.0" + wrappy "1" + +diff@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" + +doctrine@^1.2.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +duplexer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + +duplexify@^3.2.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.0.tgz#1aa773002e1578457e9d9d4a50b0ccaaebcbd604" + dependencies: + end-of-stream "1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +editor@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742" + +end-of-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.0.0.tgz#d4596e702734a93e40e9af864319eabd99ff2f0e" + dependencies: + once "~1.3.0" + +error-ex@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9" + dependencies: + is-arrayish "^0.2.1" + +es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7: + version "0.10.12" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac" + dependencies: + d "^0.1.1" + es5-ext "^0.10.7" + es6-symbol "3" + +es6-map@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-set "~0.1.3" + es6-symbol "~3.1.0" + event-emitter "~0.3.4" + +es6-promise@^3.0.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + +es6-promise@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.2.1.tgz#ec56233868032909207170c39448e24449dd1fc4" + +es6-set@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-symbol "3" + event-emitter "~0.3.4" + +es6-symbol@^3.0.2, es6-symbol@~3.1, es6-symbol@~3.1.0, es6-symbol@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + +es6-weak-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81" + dependencies: + d "^0.1.1" + es5-ext "^0.10.8" + es6-iterator "2" + es6-symbol "3" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escodegen@1.8.x: + version "1.8.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" + dependencies: + esprima "^2.7.1" + estraverse "^1.9.1" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.2.0" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-config-google@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.6.0.tgz#c542ec18fb3247983ac16bba31662d01625b763f" + dependencies: + eslint-config-xo "^0.13.0" + +eslint-config-xo@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/eslint-config-xo/-/eslint-config-xo-0.13.0.tgz#f916765432ba67d2fc7a7177b8bcfef3f6eb0564" + +eslint@^3.0.0, eslint@^3.4.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.10.0.tgz#4a90079046b3a89099eaa47787eafeb081e78209" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.4.6" + debug "^2.1.1" + doctrine "^1.2.2" + escope "^3.6.0" + espree "^3.3.1" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.2.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~1.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.3.2.tgz#dbf3fadeb4ecb4d4778303e50103b3d36c88b89c" + dependencies: + acorn "^4.0.1" + acorn-jsx "^3.0.0" + +esprima@^2.6.0, esprima@^2.7.1, esprima@2.7.x: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esrecurse@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + dependencies: + estraverse "~4.1.0" + object-assign "^4.0.1" + +estraverse@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" + +estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +estraverse@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +event-emitter@~0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5" + dependencies: + d "~0.1.1" + es5-ext "~0.10.7" + +event-stream@~3.3.0: + version "3.3.4" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + +eventsource@~0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" + dependencies: + original ">=0.0.5" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + dependencies: + os-homedir "^1.0.1" + +extend@^3.0.0, extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +eyes@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + +fast-levenshtein@~2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz#bd33145744519ab1c36c3ee9f31f08e9079b67f2" + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.0.tgz#d9ccf0e789e7db725d74bc4877d23aa42972ac50" + dependencies: + websocket-driver ">=0.5.1" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +filename-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +findup-sync@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12" + dependencies: + detect-file "^0.1.0" + is-glob "^2.0.1" + micromatch "^2.3.7" + resolve-dir "^0.1.0" + +flagged-respawn@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-0.3.2.tgz#ff191eddcd7088a675b2610fffc976be9b8074b5" + +flat-cache@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.1.tgz#6c837d6225a7de5659323740b36d5361f71691ff" + dependencies: + circular-json "^0.3.0" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +fluent-ffmpeg@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.0.tgz#e6ab85e75ba8e49119a3900cd9df10d39831d392" + dependencies: + async ">=0.2.9" + which "^1.1.1" + +for-in@^0.1.5: + version "0.1.6" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" + +for-own@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.4.tgz#0149b41a39088c7515f51ebe1c1386d45f935072" + dependencies: + for-in "^0.1.5" + +foreachasync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~1.0.0-rc4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" + dependencies: + async "^2.0.1" + combined-stream "^1.0.5" + mime-types "^2.1.11" + +form-data@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.0.0.tgz#6f0aebadcc5da16c13e1ecc11137d85f9b883b25" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.11" + +form-data@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +from@~0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.3.tgz#ef63ac2062ac32acf7862e0d40b44b896f22f3bc" + +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + +fs-readdir-recursive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" + +fs-vacuum@~1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.9.tgz#4f90193ab8ea02890995bcd4e804659a5d366b2d" + dependencies: + graceful-fs "^4.1.2" + path-is-inside "^1.0.1" + rimraf "^2.5.2" + +fs-write-stream-atomic@~1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.8.tgz#e49aaddf288f87d46ff9e882f216a13abc40778b" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.0.15" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.0.15.tgz#fa63f590f3c2ad91275e4972a6cea545fb0aae44" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.29" + +fstream-ignore@^1.0.0, fstream-ignore@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream-npm@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fstream-npm/-/fstream-npm-1.1.1.tgz#6b9175db6239a83d8209e232426c494dbb29690c" + dependencies: + fstream-ignore "^1.0.0" + inherits "2" + +fstream-npm@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fstream-npm/-/fstream-npm-1.2.0.tgz#d2c3c89101346982d64e57091c38487bda916fce" + dependencies: + fstream-ignore "^1.0.0" + inherits "2" + +fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.10.tgz#604e8a92fe26ffd9f6fae30399d4984e1ab22822" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +gauge@~1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-1.2.7.tgz#e9cec5483d3d4ee0ef44b60a7d99e4935e136d93" + dependencies: + ansi "^0.3.0" + has-unicode "^2.0.0" + lodash.pad "^4.1.0" + lodash.padend "^4.1.0" + lodash.padstart "^4.1.0" + +gauge@~2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.6.0.tgz#d35301ad18e96902b4751dcbbe40f4218b942a46" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-color "^0.1.7" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +generic-pool@^2.4.2: + version "2.4.6" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.4.6.tgz#f1b55e572167dba2fe75d5aa91ebb1e9f72642d7" + +generic-pool@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.4.2.tgz#886bc5bf0beb7db96e81bcbba078818de5a62683" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +getpass@^0.1.1: + version "0.1.6" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" + dependencies: + assert-plus "^1.0.0" + +github-url-from-git@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/github-url-from-git/-/github-url-from-git-1.4.0.tgz#285e6b520819001bde128674704379e4ff03e0de" + +github-url-from-username-repo@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/github-url-from-username-repo/-/github-url-from-username-repo-1.0.2.tgz#7dd79330d2abe69c10c2cef79714c97215791dfa" + +glob-all@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.1.0.tgz#8913ddfb5ee1ac7812656241b03d5217c64b02ab" + dependencies: + glob "^7.0.5" + yargs "~1.2.6" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@^5.0.15, glob@^5.0.5: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^6.0.0: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@~7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + +global-prefix@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.4.tgz#05158db1cde2dd491b455e290eb3ab8bfc45c6e1" + dependencies: + ini "^1.3.4" + is-windows "^0.2.0" + osenv "^0.1.3" + which "^1.2.10" + +globals@^9.0.0, globals@^9.2.0: + version "9.13.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.13.0.tgz#d97706b61600d8dbe94708c367d3fdcf48470b8f" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +got@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca" + dependencies: + duplexify "^3.2.0" + infinity-agent "^2.0.0" + is-redirect "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + nested-error-stacks "^1.0.0" + object-assign "^3.0.0" + prepend-http "^1.0.0" + read-all-stream "^3.0.0" + timed-out "^2.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@~4.1.6, graceful-fs@~4.1.9: + version "4.1.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.10.tgz#f2d720c22092f743228775c75e3612632501f131" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +growl@1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" + +handlebars@^4.0.1: + version "4.0.5" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.5.tgz#92c6ed6bb164110c50d4d8d0fbddc70806c6f8e7" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + +hapi@^15.2.0: + version "15.2.0" + resolved "https://registry.yarnpkg.com/hapi/-/hapi-15.2.0.tgz#5704ca2c04b6386c03caf9ee901f1de080316d23" + dependencies: + accept "2.x.x" + ammo "2.x.x" + boom "4.x.x" + call "3.x.x" + catbox "7.x.x" + catbox-memory "2.x.x" + cryptiles "3.x.x" + heavy "4.x.x" + hoek "4.x.x" + iron "4.x.x" + items "2.x.x" + joi "9.x.x" + mimos "3.x.x" + podium "^1.2.x" + shot "3.x.x" + statehood "5.x.x" + subtext "^4.3.x" + topo "2.x.x" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-color@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-unicode@^2.0.0, has-unicode@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +heavy@4.x.x: + version "4.0.2" + resolved "https://registry.yarnpkg.com/heavy/-/heavy-4.0.2.tgz#dbb66cda5f017a594fc6c8301df6999ea8d533f0" + dependencies: + boom "3.x.x" + hoek "4.x.x" + joi "9.x.x" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hoek@4.x.x: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.1.0.tgz#4a4557460f69842ed463aa00628cc26d2683afa7" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +hooks-fixed@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hooks-fixed/-/hooks-fixed-1.2.0.tgz#0d2772d4d7d685ff9244724a9f0b5b2559aac96b" + +hosted-git-info@^2.1.4, hosted-git-info@^2.1.5, hosted-git-info@~2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iferr@^0.1.5, iferr@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +ignore-by-default@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + +ignore@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.0.tgz#8d88f03c3002a0ac52114db25d2c673b0bf1e435" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +inert: + version "4.0.2" + resolved "https://registry.yarnpkg.com/inert/-/inert-4.0.2.tgz#f26094988e653f81c84a690664781546f8d75928" + dependencies: + ammo "2.x.x" + boom "3.x.x" + hoek "4.x.x" + items "2.x.x" + joi "9.x.x" + lru-cache "4.0.x" + +infinity-agent@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/infinity-agent/-/infinity-agent-2.0.3.tgz#45e0e2ff7a9eb030b27d62b74b3744b7a7ac4216" + +inflight@^1.0.4, inflight@~1.0.4, inflight@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@^1.3.4, ini@~1.3.0, ini@~1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +init-package-json@~1.9.4: + version "1.9.4" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.9.4.tgz#b4053d0b40f0cf842a41966937cb3dc0f534e856" + dependencies: + glob "^6.0.0" + npm-package-arg "^4.0.0" + promzard "^0.3.0" + read "~1.0.1" + read-package-json "1 || 2" + semver "2.x || 3.x || 4 || 5" + validate-npm-package-license "^3.0.1" + validate-npm-package-name "^2.0.1" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +interpret@^0.6.5: + version "0.6.6" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-0.6.6.tgz#fecd7a18e7ce5ca6abfb953e1f86213a49f1625b" + +interpret@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" + +invariant@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.1.tgz#b097010547668c7e337028ebe816ebe36c8a8d54" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +iron@4.x.x: + version "4.0.4" + resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.4.tgz#c1f8cc4c91454194ab8920d9247ba882e528061a" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.0.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-dotfile@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + +is-number@^2.0.2, is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + +is-stream@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + +isarray@^1.0.0, isarray@~1.0.0, isarray@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isemail@2.x.x: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-2.2.1.tgz#0353d3d9a62951080c262c2aa0a42b8ea8e9e2a6" + +isexe@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isstream@~0.1.2, isstream@0.1.x: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +istanbul@*: + version "0.4.5" + resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" + dependencies: + abbrev "1.0.x" + async "1.x" + escodegen "1.8.x" + esprima "2.7.x" + glob "^5.0.15" + handlebars "^4.0.1" + js-yaml "3.x" + mkdirp "0.5.x" + nopt "3.x" + once "1.x" + resolve "1.1.x" + supports-color "^3.1.0" + which "^1.1.1" + wordwrap "^1.0.0" + +items@2.x.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198" + +jju@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jju/-/jju-1.3.0.tgz#dadd9ef01924bc728b03f2f7979bdbd62f7a2aaa" + +jodid25519@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" + dependencies: + jsbn "~0.1.0" + +joi@9.x.x: + version "9.2.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-9.2.0.tgz#3385ac790192130cbe230e802ec02c9215bbfeda" + dependencies: + hoek "4.x.x" + isemail "2.x.x" + items "2.x.x" + moment "2.x.x" + topo "2.x.x" + +js-tokens@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" + +js-yaml@^3.5.1, js-yaml@3.x: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +js-yaml@3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-parse-helpfulerror@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz#13f14ce02eed4e981297b64eb9e3b932e2dd13dc" + dependencies: + jju "^1.1.0" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json3@^3.3.2, json3@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + +json5@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.0.tgz#9b20715b026cbe3778fd769edccd822d8332a5b2" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonpointer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" + +jsprim@^1.2.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" + dependencies: + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +kareem@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.1.3.tgz#0877610d8879c38da62d1dbafde4e17f2692f041" + +kind-of@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.0.4.tgz#7b8ecf18a4e17f8269d73b501c9f232c96887a74" + dependencies: + is-buffer "^1.0.2" + +knex@^0.12.5: + version "0.12.6" + resolved "https://registry.yarnpkg.com/knex/-/knex-0.12.6.tgz#a255f0ea03af2c2c94687a622c08acc1a9463c0e" + dependencies: + babel-runtime "^6.11.6" + bluebird "^3.4.6" + chalk "^1.0.0" + commander "^2.2.0" + debug "^2.1.3" + generic-pool "^2.4.2" + inherits "~2.0.1" + interpret "^0.6.5" + liftoff "~2.2.0" + lodash "^4.6.0" + minimist "~1.1.0" + mkdirp "^0.5.0" + node-uuid "^1.4.7" + pg-connection-string "^0.1.3" + readable-stream "^1.1.12" + tildify "~1.0.0" + v8flags "^2.0.2" + +latest-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb" + dependencies: + package-json "^1.0.0" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +lcov-parse@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-0.0.10.tgz#1b0b8ff9ac9c7889250582b70b71315d9da6d9a3" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +liftoff@~2.2.0: + version "2.2.5" + resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.2.5.tgz#998c2876cff484b103e4423b93d356da44734c91" + dependencies: + extend "^3.0.0" + findup-sync "^0.4.2" + flagged-respawn "^0.3.2" + rechoir "^0.6.2" + resolve "^1.1.7" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +lockfile@~1.0.1, lockfile@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.2.tgz#97e1990174f696cbe0a3acd58a43b84aa30c7c83" + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._basecreate@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" + +lodash._baseuniq@~4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" + dependencies: + lodash._createset "~4.0.0" + lodash._root "~3.0.0" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._createassigner@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" + dependencies: + lodash._bindcallback "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash.restparam "^3.0.0" + +lodash._createset@~4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash._root@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + +lodash.assign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" + dependencies: + lodash._baseassign "^3.0.0" + lodash._createassigner "^3.0.0" + lodash.keys "^3.0.0" + +lodash.assign@^4.0.3, lodash.assign@^4.0.6: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.clonedeep@~4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + +lodash.create@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" + dependencies: + lodash._baseassign "^3.0.0" + lodash._basecreate "^3.0.0" + lodash._isiterateecall "^3.0.0" + +lodash.defaults@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c" + dependencies: + lodash.assign "^3.0.0" + lodash.restparam "^3.0.0" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.pad@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/lodash.pad/-/lodash.pad-4.5.1.tgz#4330949a833a7c8da22cc20f6a26c4d59debba70" + +lodash.padend@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" + +lodash.padstart@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" + +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + +lodash.union@~4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + +lodash.uniq@~4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash.without@~4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" + +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.6.0: + version "4.16.6" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.6.tgz#d22c9ac660288f3843e16ba7d2b5d06cca27d777" + +log-driver@1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.0.tgz#6b26248c42f6d4fa4b0d8542f78edfcde35642a8" + dependencies: + js-tokens "^2.0.0" + +lowercase-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + +lru-cache@~4.0.1, lru-cache@4.0.x: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.1.tgz#1343955edaf2e37d9b9e7ee7241e27c4b9fb72be" + dependencies: + pseudomap "^1.0.1" + yallist "^2.0.0" + +map-stream@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + +micromatch@^2.1.5, micromatch@^2.3.7: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +mime-db@~1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.24.0.tgz#e2d13f939f0016c6e4e9ad25a8652f126c467f0c" + +mime-db@1.x.x: + version "1.25.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" + +mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.7: + version "2.1.12" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.12.tgz#152ba256777020dd4663f54c2e7bc26381e71729" + dependencies: + mime-db "~1.24.0" + +mimos@3.x.x: + version "3.0.3" + resolved "https://registry.yarnpkg.com/mimos/-/mimos-3.0.3.tgz#b9109072ad378c2b72f6a0101c43ddfb2b36641f" + dependencies: + hoek "4.x.x" + mime-db "1.x.x" + +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@~3.0.3, "minimatch@2 || 3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" + +minimist@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de" + +minimist@^1.2.0, minimist@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +minimist@~1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@^0.5.0, mkdirp@^0.5.1, "mkdirp@>=0.5 0", mkdirp@~0.5.0, mkdirp@~0.5.1, mkdirp@0.5.1, mkdirp@0.5.x: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha-eslint@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mocha-eslint/-/mocha-eslint-3.0.1.tgz#ae72abc561cb289d2e6c143788ee182aca1a04db" + dependencies: + chalk "^1.1.0" + eslint "^3.0.0" + glob-all "^3.0.1" + replaceall "^0.1.6" + +mocha@*: + version "3.1.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.1.2.tgz#51f93b432bf7e1b175ffc22883ccd0be32dba6b5" + dependencies: + browser-stdout "1.3.0" + commander "2.9.0" + debug "2.2.0" + diff "1.4.0" + escape-string-regexp "1.0.5" + glob "7.0.5" + growl "1.9.2" + json3 "3.3.2" + lodash.create "3.1.1" + mkdirp "0.5.1" + supports-color "3.1.2" + +moment@2.x.x: + version "2.16.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.16.0.tgz#f38f2c97c9889b0ee18fc6cc392e1e443ad2da8e" + +mongodb-core@2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.0.13.tgz#f9394b588dce0e579482e53d74dbc7d7a9d4519c" + dependencies: + bson "~0.5.6" + require_optional "~1.0.0" + +mongodb@2.2.11: + version "2.2.11" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.11.tgz#a828b036fe6a437a35e723af5f81781c4976306c" + dependencies: + es6-promise "3.2.1" + mongodb-core "2.0.13" + readable-stream "2.1.5" + +mongoose@^4.4.20: + version "4.6.7" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.6.7.tgz#fc82b4851620e8d994b883fb3fd03b401566d693" + dependencies: + async "2.1.2" + bson "~0.5.4" + hooks-fixed "1.2.0" + kareem "1.1.3" + mongodb "2.2.11" + mpath "0.2.1" + mpromise "0.5.5" + mquery "2.0.0" + ms "0.7.1" + muri "1.1.1" + regexp-clone "0.0.1" + sliced "1.0.1" + +mpath@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.2.1.tgz#3a4e829359801de96309c27a6b2e102e89f9e96e" + +mpromise@0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mpromise/-/mpromise-0.5.5.tgz#f5b24259d763acc2257b0a0c8c6d866fd51732e6" + +mquery@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-2.0.0.tgz#b5abc850b90dffc3e10ae49b4b6e7a479752df22" + dependencies: + bluebird "2.10.2" + debug "2.2.0" + regexp-clone "0.0.1" + sliced "0.0.5" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +muri@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/muri/-/muri-1.1.1.tgz#64bd904eaf8ff89600c994441fad3c5195905ac2" + +mute-stream@~0.0.4: + version "0.0.6" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +nan@^2.3.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +nested-error-stacks@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz#19f619591519f096769a5ba9a86e6eeec823c3cf" + dependencies: + inherits "~2.0.1" + +nigel@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/nigel/-/nigel-2.0.2.tgz#93a1866fb0c52d87390aa75e2b161f4b5c75e5b1" + dependencies: + hoek "4.x.x" + vise "2.x.x" + +node-ffprobe@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/node-ffprobe/-/node-ffprobe-1.2.2.tgz#ab2e7152c9d2b7e296fce2f77a97ef249baa6d33" + +node-gyp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.4.0.tgz#dda558393b3ecbbe24c9e6b8703c71194c63fa36" + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + minimatch "^3.0.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3" + osenv "0" + path-array "^1.0.0" + request "2" + rimraf "2" + semver "2.x || 3.x || 4 || 5" + tar "^2.0.0" + which "1" + +node-pre-gyp@^0.6.29: + version "0.6.31" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.31.tgz#d8a00ddaa301a940615dbcc8caad4024d58f6017" + dependencies: + mkdirp "~0.5.1" + nopt "~3.0.6" + npmlog "^4.0.0" + rc "~1.1.6" + request "^2.75.0" + rimraf "~2.5.4" + semver "~5.3.0" + tar "~2.2.1" + tar-pack "~3.3.0" + +node-uuid@^1.4.7, node-uuid@~1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f" + +nodemon@^1.10.2: + version "1.11.0" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c" + dependencies: + chokidar "^1.4.3" + debug "^2.2.0" + es6-promise "^3.0.2" + ignore-by-default "^1.0.0" + lodash.defaults "^3.1.2" + minimatch "^3.0.0" + ps-tree "^1.0.1" + touch "1.0.0" + undefsafe "0.0.3" + update-notifier "0.5.0" + +nodeplayer-backend-dummy@^0.1.999: + version "0.1.999" + resolved "https://registry.yarnpkg.com/nodeplayer-backend-dummy/-/nodeplayer-backend-dummy-0.1.999.tgz#9a15c522a6cf06770e7a6ee6d610319bb75e5665" + dependencies: + nodeplayer "*" + underscore "^1.8.2" + +nodeplayer@*: + version "0.2.0" + resolved "https://registry.yarnpkg.com/nodeplayer/-/nodeplayer-0.2.0.tgz#a2cddfe4002197dbcd37e1b32d93600331b3fbdd" + dependencies: + async "^0.9.0" + mkdirp "^0.5.0" + npm "^2.7.1" + underscore "^1.7.0" + winston "^0.9.0" + yargs "^3.6.0" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + dependencies: + abbrev "1" + +nopt@~3.0.6, "nopt@2 || 3", nopt@3.x: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +normalize-git-url@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/normalize-git-url/-/normalize-git-url-3.0.2.tgz#8e5f14be0bdaedb73e07200310aa416c27350fc4" + +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, "normalize-package-data@~1.0.1 || ^2.0.0", normalize-package-data@~2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" + +npm-cache-filename@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/npm-cache-filename/-/npm-cache-filename-1.0.2.tgz#ded306c5b0bfc870a9e9faf823bc5f283e05ae11" + +npm-install-checks@~1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-1.0.7.tgz#6d91aeda0ac96801f1ed7aadee116a6c0a086a57" + dependencies: + npmlog "0.1 || 1 || 2" + semver "^2.3.0 || 3.x || 4 || 5" + +npm-install-checks@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-3.0.0.tgz#d4aecdfd51a53e3723b7b2f93b2ee28e307bc0d7" + dependencies: + semver "^2.3.0 || 3.x || 4 || 5" + +"npm-package-arg@^3.0.0 || ^4.0.0", npm-package-arg@^4.0.0, npm-package-arg@^4.1.1, npm-package-arg@~4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-4.2.0.tgz#809bc61cabf54bd5ff94f6165c89ba8ee88c115c" + dependencies: + hosted-git-info "^2.1.5" + semver "^5.1.0" + +npm-package-arg@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-4.1.1.tgz#86d9dca985b4c5e5d59772dfd5de6919998a495a" + dependencies: + hosted-git-info "^2.1.4" + semver "4 || 5" + +npm-registry-client@~7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-7.2.1.tgz#c792266b088cc313f8525e7e35248626c723db75" + dependencies: + concat-stream "^1.5.2" + graceful-fs "^4.1.6" + normalize-package-data "~1.0.1 || ^2.0.0" + npm-package-arg "^3.0.0 || ^4.0.0" + once "^1.3.3" + request "^2.74.0" + retry "^0.10.0" + semver "2 >=2.2.1 || 3.x || 4 || 5" + slide "^1.1.3" + optionalDependencies: + npmlog "~2.0.0 || ~3.1.0" + +npm-user-validate@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-0.1.5.tgz#52465d50c2d20294a57125b996baedbf56c5004b" + +npm@^2.7.1: + version "2.15.11" + resolved "https://registry.yarnpkg.com/npm/-/npm-2.15.11.tgz#350588fba9cd8d384cf9a6e8dc0fef0f94992b7c" + dependencies: + abbrev "~1.0.9" + ansi "~0.3.1" + ansicolors "~0.3.2" + ansistyles "~0.1.3" + archy "~1.0.0" + async-some "~1.0.2" + block-stream "0.0.9" + char-spinner "~1.0.1" + chmodr "~1.0.2" + chownr "~1.0.1" + cmd-shim "~2.0.2" + columnify "~1.5.4" + config-chain "~1.1.10" + dezalgo "~1.0.3" + editor "~1.0.0" + fs-vacuum "~1.2.9" + fs-write-stream-atomic "~1.0.8" + fstream "~1.0.10" + fstream-npm "~1.1.1" + github-url-from-git "~1.4.0" + github-url-from-username-repo "~1.0.2" + glob "~7.0.6" + graceful-fs "~4.1.6" + hosted-git-info "~2.1.5" + inflight "~1.0.4" + inherits "~2.0.3" + ini "~1.3.4" + init-package-json "~1.9.4" + lockfile "~1.0.1" + lru-cache "~4.0.1" + minimatch "~3.0.3" + mkdirp "~0.5.1" + node-gyp "~3.4.0" + nopt "~3.0.6" + normalize-git-url "~3.0.2" + normalize-package-data "~2.3.5" + npm-cache-filename "~1.0.2" + npm-install-checks "~1.0.7" + npm-package-arg "~4.1.0" + npm-registry-client "~7.2.1" + npm-user-validate "~0.1.5" + npmlog "~2.0.4" + once "~1.4.0" + opener "~1.4.1" + osenv "~0.1.3" + path-is-inside "~1.0.0" + read "~1.0.7" + read-installed "~4.0.3" + read-package-json "~2.0.4" + readable-stream "~2.1.5" + realize-package-specifier "~3.0.1" + request "~2.74.0" + retry "~0.10.0" + rimraf "~2.5.4" + semver "~5.1.0" + sha "~2.0.1" + slide "~1.1.6" + sorted-object "~2.0.0" + spdx-license-ids "~1.2.2" + strip-ansi "~3.0.1" + tar "~2.2.1" + text-table "~0.2.0" + uid-number "0.0.6" + umask "~1.1.0" + validate-npm-package-license "~3.0.1" + validate-npm-package-name "~2.2.2" + which "~1.2.11" + wrappy "~1.0.2" + write-file-atomic "~1.1.4" + +npm@^3.9.5: + version "3.10.10" + resolved "https://registry.yarnpkg.com/npm/-/npm-3.10.10.tgz#5b1d577e4c8869d6c8603bc89e9cd1637303e46e" + dependencies: + abbrev "~1.0.9" + ansicolors "~0.3.2" + ansistyles "~0.1.3" + aproba "~1.0.4" + archy "~1.0.0" + asap "~2.0.5" + chownr "~1.0.1" + cmd-shim "~2.0.2" + columnify "~1.5.4" + config-chain "~1.1.11" + dezalgo "~1.0.3" + editor "~1.0.0" + fs-vacuum "~1.2.9" + fs-write-stream-atomic "~1.0.8" + fstream "~1.0.10" + fstream-npm "~1.2.0" + glob "~7.1.0" + graceful-fs "~4.1.9" + has-unicode "~2.0.1" + hosted-git-info "~2.1.5" + iferr "~0.1.5" + inflight "~1.0.5" + inherits "~2.0.3" + ini "~1.3.4" + init-package-json "~1.9.4" + lockfile "~1.0.2" + lodash._baseuniq "~4.6.0" + lodash.clonedeep "~4.5.0" + lodash.union "~4.6.0" + lodash.uniq "~4.5.0" + lodash.without "~4.4.0" + mkdirp "~0.5.1" + node-gyp "~3.4.0" + nopt "~3.0.6" + normalize-git-url "~3.0.2" + normalize-package-data "~2.3.5" + npm-cache-filename "~1.0.2" + npm-install-checks "~3.0.0" + npm-package-arg "~4.2.0" + npm-registry-client "~7.2.1" + npm-user-validate "~0.1.5" + npmlog "~4.0.0" + once "~1.4.0" + opener "~1.4.2" + osenv "~0.1.3" + path-is-inside "~1.0.2" + read "~1.0.7" + read-cmd-shim "~1.0.1" + read-installed "~4.0.3" + read-package-json "~2.0.4" + read-package-tree "~5.1.5" + readable-stream "~2.1.5" + realize-package-specifier "~3.0.3" + request "~2.75.0" + retry "~0.10.0" + rimraf "~2.5.4" + semver "~5.3.0" + sha "~2.0.1" + slide "~1.1.6" + sorted-object "~2.0.1" + strip-ansi "~3.0.1" + tar "~2.2.1" + text-table "~0.2.0" + uid-number "0.0.6" + umask "~1.1.0" + unique-filename "~1.1.0" + unpipe "~1.0.0" + validate-npm-package-name "~2.2.2" + which "~1.2.11" + wrappy "~1.0.2" + write-file-atomic "~1.2.0" + +npmlog@^4.0.0, npmlog@~4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.0.tgz#e094503961c70c1774eb76692080e8d578a9f88f" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.6.0" + set-blocking "~2.0.0" + +"npmlog@~2.0.0 || ~3.1.0", "npmlog@0 || 1 || 2 || 3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-3.1.2.tgz#2d46fa874337af9498a2f12bb43d8d0be4a36873" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.6.0" + set-blocking "~2.0.0" + +npmlog@~2.0.4, "npmlog@0.1 || 1 || 2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-2.0.4.tgz#98b52530f2514ca90d09ec5b22c8846722375692" + dependencies: + ansi "~0.3.1" + are-we-there-yet "~1.1.2" + gauge "~1.2.5" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +once@^1.3.0, once@^1.3.3, once@~1.4.0, once@1.x: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +once@~1.3.0, once@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +opener@~1.4.1, opener@~1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.2.tgz#b32582080042af8680c389a499175b4c54fff523" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.1, optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +original@>=0.0.5: + version "1.0.0" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" + dependencies: + url-parse "1.0.x" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.0, osenv@^0.1.3, osenv@~0.1.3, osenv@0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.3.tgz#83cf05c6d6458fc4d5ac6362ea325d92f2754217" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +output-file-sync@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" + dependencies: + graceful-fs "^4.1.4" + mkdirp "^0.5.1" + object-assign "^4.1.0" + +package-json@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" + dependencies: + got "^3.2.0" + registry-url "^3.0.0" + +packet-reader@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-0.2.0.tgz#819df4d010b82d5ea5671f8a1a3acf039bcd7700" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +path-array@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-array/-/path-array-1.0.1.tgz#7e2f0f35f07a2015122b868b7eac0eb2c4fec271" + dependencies: + array-index "^1.0.0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1, path-is-inside@~1.0.0, path-is-inside@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +pause-stream@0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + dependencies: + through "~2.3" + +pez@2.x.x: + version "2.1.3" + resolved "https://registry.yarnpkg.com/pez/-/pez-2.1.3.tgz#e53ebcbf48961b4aa1bb2b68cfd1eb20c9898039" + dependencies: + b64 "3.x.x" + boom "4.x.x" + content "3.x.x" + hoek "4.x.x" + nigel "2.x.x" + +pg-connection-string@^0.1.3, pg-connection-string@0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7" + +pg-pool@1.*: + version "1.5.0" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-1.5.0.tgz#d789756ccb90cd389fc5a395e0ca9f2d2c558d48" + dependencies: + generic-pool "2.4.2" + object-assign "4.1.0" + +pg-types@1.*: + version "1.11.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.11.0.tgz#aae91a82d952b633bb88d006350a166daaf6ea90" + dependencies: + ap "~0.2.0" + postgres-array "~1.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.0" + postgres-interval "~1.0.0" + +pg@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-6.1.0.tgz#4ebc58100a79187b6b98fa5caf1675d669926b41" + dependencies: + buffer-writer "1.0.1" + packet-reader "0.2.0" + pg-connection-string "0.1.3" + pg-pool "1.*" + pg-types "1.*" + pgpass "1.x" + semver "4.3.2" + +pgpass@1.x: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.1.tgz#0de8b5bef993295d90a7e17d976f568dcd25d49f" + dependencies: + split "^1.0.0" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pkginfo@0.3.x: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" + +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + +podium@^1.2.x: + version "1.2.3" + resolved "https://registry.yarnpkg.com/podium/-/podium-1.2.3.tgz#5c95b7cc2f5c87dd324e0ad4a9363ac62d66b371" + dependencies: + hoek "4.x.x" + items "2.x.x" + joi "9.x.x" + +postgres-array@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-1.0.0.tgz#48c2e82935b178bf805e0dff689d137eec2bfe6b" + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + +postgres-date@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.3.tgz#e2d89702efdb258ff9d9cee0fe91bd06975257a8" + +postgres-interval@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.0.2.tgz#7261438d862b412921c6fdb7617668424b73a6ed" + dependencies: + xtend "^4.0.0" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +private@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.6.tgz#55c6a976d0f9bafb9924851350fe47b9b5fbb7c1" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +promzard@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" + dependencies: + read "1" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + +ps-tree@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" + dependencies: + event-stream "~3.3.0" + +pseudomap@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" + +qs@~6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" + +querystringify@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" + +randomatic@^1.1.3: + version "1.1.5" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.5.tgz#5e9ef5f2d573c67bd2b8124ae90b5156e457840b" + dependencies: + is-number "^2.0.2" + kind-of "^3.0.2" + +rc@^1.0.1, rc@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~1.0.4" + +read-all-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" + dependencies: + pinkie-promise "^2.0.0" + readable-stream "^2.0.0" + +read-cmd-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.1.tgz#2d5d157786a37c055d22077c32c53f8329e91c7b" + dependencies: + graceful-fs "^4.1.2" + +read-installed@~4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067" + dependencies: + debuglog "^1.0.1" + read-package-json "^2.0.0" + readdir-scoped-modules "^1.0.0" + semver "2 || 3 || 4 || 5" + slide "~1.1.3" + util-extend "^1.0.1" + optionalDependencies: + graceful-fs "^4.1.2" + +read-package-json@^2.0.0, read-package-json@~2.0.4, "read-package-json@1 || 2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.4.tgz#61ed1b2256ea438d8008895090be84b8e799c853" + dependencies: + glob "^6.0.0" + json-parse-helpfulerror "^1.0.2" + normalize-package-data "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.2" + +read-package-tree@~5.1.5: + version "5.1.5" + resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.1.5.tgz#ace7e6381c7684f970aaa98fc7c5d2b666addab6" + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + once "^1.3.0" + read-package-json "^2.0.0" + readdir-scoped-modules "^1.0.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read@~1.0.1, read@~1.0.7, read@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" + dependencies: + mute-stream "~0.0.4" + +readable-stream@^1.1.12: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, "readable-stream@1 || 2": + version "2.2.1" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.1.tgz#c459a6687ad6195f936b959870776edef27a7655" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readable-stream@~2.0.0, readable-stream@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readable-stream@~2.1.4, readable-stream@~2.1.5, readable-stream@2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readdir-scoped-modules@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +realize-package-specifier@~3.0.1, realize-package-specifier@~3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/realize-package-specifier/-/realize-package-specifier-3.0.3.tgz#d0def882952b8de3f67eba5e91199661271f41f4" + dependencies: + dezalgo "^1.0.1" + npm-package-arg "^4.1.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +regenerate@^1.2.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" + +regenerator-runtime@^0.9.5: + version "0.9.6" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029" + +regex-cache@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" + dependencies: + is-equal-shallow "^0.1.3" + is-primitive "^2.0.0" + +regexp-clone@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +registry-url@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + dependencies: + rc "^1.0.1" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" + dependencies: + is-finite "^1.0.0" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +replaceall@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/replaceall/-/replaceall-0.1.6.tgz#81d81ac7aeb72d7f5c4942adf2697a3220688d8e" + +request@^2.74.0, request@^2.75.0, request@2: + version "2.78.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.78.0.tgz#e1c8dec346e1c81923b24acdb337f11decabe9cc" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + +request@~2.74.0: + version "2.74.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.74.0.tgz#7693ca768bbb0ea5c8ce08c084a45efa05b892ab" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + bl "~1.1.2" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~1.0.0-rc4" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.7" + oauth-sign "~0.8.1" + qs "~6.2.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + +request@~2.75.0, request@2.75.0: + version "2.75.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.75.0.tgz#d2b8268a286da13eaa5d01adf5d18cc90f657d93" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + bl "~1.1.2" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.0.0" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.7" + oauth-sign "~0.8.1" + qs "~6.2.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + +require_optional@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.0.tgz#52a86137a849728eb60a55533617f8f914f59abf" + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +require-uncached@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +requires-port@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" + dependencies: + expand-tilde "^1.2.2" + global-modules "^0.2.3" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + +resolve@^1.1.6, resolve@^1.1.7, resolve@1.1.x: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +retry@^0.10.0, retry@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.0.tgz#649e15ca408422d98318161935e7f7d652d435dd" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@^2.2.8, rimraf@^2.5.2, rimraf@~2.5.1, rimraf@~2.5.4, rimraf@2: + version "2.5.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" + dependencies: + glob "^7.0.5" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + dependencies: + semver "^5.0.3" + +"semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@~5.3.0, "semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@4 || 5", semver@5.x: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +semver@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19" + +semver@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +sha@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/sha/-/sha-2.0.1.tgz#6030822fbd2c9823949f8f72ed6411ee5cf25aae" + dependencies: + graceful-fs "^4.1.2" + readable-stream "^2.0.2" + +shelljs@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.5.tgz#2eef7a50a21e1ccf37da00df767ec69e30ad0675" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shot@3.x.x: + version "3.3.2" + resolved "https://registry.yarnpkg.com/shot/-/shot-3.3.2.tgz#691c2611759decc20487b20d25cc299f39e5f9b7" + dependencies: + hoek "4.x.x" + joi "9.x.x" + +signal-exit@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.1.tgz#5a4c884992b63a7acd9badb7894c3ee9cfccad81" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +sliced@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-0.0.5.tgz#5edc044ca4eb6f7816d50ba2fc63e25d8fe4707f" + +sliced@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + +slide@^1.1.3, slide@^1.1.5, slide@~1.1.3, slide@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +sockjs: + version "0.3.18" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.18.tgz#d9b289316ca7df77595ef299e075f0f937eb4207" + dependencies: + faye-websocket "^0.10.0" + uuid "^2.0.2" + +sockjs-client: + version "1.1.1" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.1.tgz#284843e9a9784d7c474b1571b3240fca9dda4bb0" + dependencies: + debug "^2.2.0" + eventsource "~0.1.6" + faye-websocket "~0.11.0" + inherits "^2.0.1" + json3 "^3.3.2" + url-parse "^1.1.1" + +sorted-object@~2.0.0, sorted-object@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/sorted-object/-/sorted-object-2.0.1.tgz#7d631f4bd3a798a24af1dffcfbfe83337a5df5fc" + +source-map-support@^0.4.2: + version "0.4.6" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.6.tgz#32552aa64b458392a85eab3b0b5ee61527167aeb" + dependencies: + source-map "^0.5.3" + +source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.0, source-map@^0.5.3, source-map@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + +source-map@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" + dependencies: + amdefine ">=0.0.4" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2, spdx-license-ids@~1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +split@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.0.tgz#c4395ce683abcd254bc28fe1dabb6e5c27dcffae" + dependencies: + through "2" + +split@0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.1.tgz#30e1a5d329244974a1af61511339d595af6638b0" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jodid25519 "^1.0.0" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stack-trace@0.0.x: + version "0.0.9" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695" + +statehood@5.x.x: + version "5.0.0" + resolved "https://registry.yarnpkg.com/statehood/-/statehood-5.0.0.tgz#ce2285aabeae398ae87cbba746184b7599b8fa31" + dependencies: + boom "3.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + iron "4.x.x" + items "2.x.x" + joi "9.x.x" + +stream-combiner@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + dependencies: + duplexer "~0.1.1" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" + dependencies: + strip-ansi "^3.0.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1, strip-ansi@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-json-comments@~1.0.1, strip-json-comments@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" + +subtext@^4.3.x: + version "4.3.0" + resolved "https://registry.yarnpkg.com/subtext/-/subtext-4.3.0.tgz#dfac90492ec35669fd6e00c6e5d938b06d7ccfbb" + dependencies: + boom "4.x.x" + content "3.x.x" + hoek "4.x.x" + pez "2.x.x" + wreck "10.x.x" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.1.0, supports-color@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" + dependencies: + has-flag "^1.0.0" + +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +tar-pack@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae" + dependencies: + debug "~2.2.0" + fstream "~1.0.10" + fstream-ignore "~1.0.5" + once "~1.3.3" + readable-stream "~2.1.4" + rimraf "~2.5.1" + tar "~2.2.1" + uid-number "~0.0.6" + +tar@^2.0.0, tar@~2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +through@^2.3.6, through@~2.3, through@~2.3.1, through@2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tildify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.0.0.tgz#2a021db5e8fbde0a8f8b4df37adaa8fb1d39d7dd" + dependencies: + user-home "^1.0.0" + +timed-out@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" + +to-fast-properties@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" + +topo@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" + dependencies: + hoek "4.x.x" + +touch@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-1.0.0.tgz#449cbe2dbae5a8c8038e30d71fa0ff464947c4de" + dependencies: + nopt "~1.0.10" + +tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +tryit@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.3.tgz#3da382f670f25ded78d7b3d1792119bca0b7132d" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-detect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" + +type-detect@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" + +typedarray@~0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uglify-js@^2.6: + version "2.7.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.4.tgz#a295a0de12b6a650c031c40deb0dc40b14568bd2" + dependencies: + async "~0.2.6" + source-map "~0.5.1" + uglify-to-browserify "~1.0.0" + yargs "~3.10.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uid-number@~0.0.6, uid-number@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +umask@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" + +undefsafe@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f" + +underscore@^1.7.0, underscore@^1.8.2: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +unique-filename@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +update-notifier@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc" + dependencies: + chalk "^1.0.0" + configstore "^1.0.0" + is-npm "^1.0.0" + latest-version "^1.0.0" + repeating "^1.1.2" + semver-diff "^2.0.0" + string-length "^1.0.0" + +url-parse@^1.1.1: + version "1.1.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a" + dependencies: + querystringify "0.0.x" + requires-port "1.0.x" + +url-parse@1.0.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b" + dependencies: + querystringify "0.0.x" + requires-port "1.0.x" + +user-home@^1.0.0, user-home@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util-extend@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" + +uuid@^2.0.1, uuid@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + +v8flags@^2.0.10, v8flags@^2.0.2: + version "2.0.11" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.0.11.tgz#bca8f30f0d6d60612cc2c00641e6962d42ae6881" + dependencies: + user-home "^1.1.1" + +validate-npm-package-license@^3.0.1, validate-npm-package-license@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +validate-npm-package-name@^2.0.1, validate-npm-package-name@~2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-2.2.2.tgz#f65695b22f7324442019a3c7fa39a6e7fd299085" + dependencies: + builtins "0.0.7" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +vise@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/vise/-/vise-2.0.2.tgz#6b08e8fb4cb76e3a50cd6dd0ec37338e811a0d39" + dependencies: + hoek "4.x.x" + +walk@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.9.tgz#31b4db6678f2ae01c39ea9fb8725a9031e558a7b" + dependencies: + foreachasync "^3.0.0" + +wcwidth@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + dependencies: + defaults "^1.0.3" + +websocket-driver@>=0.5.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + dependencies: + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which@^1.1.1, which@^1.2.10, which@~1.2.11, which@1: + version "1.2.12" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" + dependencies: + isexe "^1.1.1" + +wide-align@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" + dependencies: + string-width "^1.0.1" + +window-size@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" + +window-size@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +winston@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-0.9.0.tgz#b5726e6c42291e305e36286ce7ae9f3b74a527a8" + dependencies: + async "0.9.x" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + pkginfo "0.3.x" + stack-trace "0.0.x" + +winston@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.0.tgz#207faaab6fccf3fe493743dd2b03dbafc7ceb78c" + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + stack-trace "0.0.x" + +wordwrap@^1.0.0, wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wrap-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.0.0.tgz#7d30f8f873f9a5bbc3a64dabc8d177e071ae426f" + dependencies: + string-width "^1.0.1" + +wrappy@~1.0.2, wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +wreck@10.x.x: + version "10.0.0" + resolved "https://registry.yarnpkg.com/wreck/-/wreck-10.0.0.tgz#98ab882f85e16a526332507f101f5a7841162278" + dependencies: + boom "4.x.x" + hoek "4.x.x" + +write-file-atomic@^1.1.2, write-file-atomic@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.2.0.tgz#14c66d4e4cb3ca0565c28cf3b7a6f3e4d5938fab" + dependencies: + graceful-fs "^4.1.2" + imurmurhash "^0.1.4" + slide "^1.1.5" + +write-file-atomic@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.1.4.tgz#b1f52dc2e8dc0e3cb04d187a25f758a38a90ca3b" + dependencies: + graceful-fs "^4.1.2" + imurmurhash "^0.1.4" + slide "^1.1.5" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +xdg-basedir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" + dependencies: + os-homedir "^1.0.0" + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.0, y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.0.0.tgz#306c543835f09ee1a4cb23b7bce9ab341c91cdd4" + +yargs-parser@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4" + dependencies: + camelcase "^3.0.0" + lodash.assign "^4.0.6" + +yargs@^3.6.0: + version "3.32.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" + dependencies: + camelcase "^2.0.1" + cliui "^3.0.3" + decamelize "^1.1.1" + os-locale "^1.4.0" + string-width "^1.0.1" + window-size "^0.1.4" + y18n "^3.2.0" + +yargs@^4.7.1: + version "4.8.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0" + dependencies: + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + lodash.assign "^4.0.3" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.1" + which-module "^1.0.0" + window-size "^0.2.0" + y18n "^3.2.1" + yargs-parser "^2.4.1" + +yargs@~1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b" + dependencies: + minimist "^0.1.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" +