From 64fe9dbfb2ef3e8c9afbc78c9879ad596c3fb43b Mon Sep 17 00:00:00 2001
From: Scott Nonnenberg <scott@nonnenberg.com>
Date: Mon, 8 Jan 2018 13:19:25 -0800
Subject: [PATCH] Clean logs on start - and eslint/mocha with code coverage
 (#1945)

* Clean logs on startup; install server-side testing/linting

* Add eslint config, make all of app/ conform to its demands

* Add Node.js testing and linting to CI

* Lock project to Node.js 7.9.0, used by Electron 1.7.10

* New eslint error: trailing commas in function argumensts

Node 7.9.0 doesn't like trailing commas, but Electron does

* Move electron to devDependency, tell eslint it's built-in
---
 .eslintignore                   |   17 +
 .eslintrc.js                    |   41 ++
 .gitignore                      |    3 +
 .nvmrc                          |    1 +
 .travis.yml                     |    4 +-
 .yarnclean                      |    3 +
 app/auto_update.js              |   20 +-
 app/config.js                   |   11 +-
 app/locale.js                   |   13 +-
 app/logging.js                  |  204 ++++--
 app/menu.js                     |   43 +-
 app/tray_icon.js                |   52 +-
 app/user_config.js              |    6 +-
 app/window_state.js             |    2 +-
 appveyor.yml                    |    4 +-
 main.js                         |  184 +++---
 package.json                    |   99 +--
 prepare_build.js                |   11 +-
 test/.eslintrc.js               |   17 +
 test/server/app/logging_test.js |  271 ++++++++
 yarn.lock                       | 1092 +++++++++++++++++++++++++++++--
 21 files changed, 1782 insertions(+), 316 deletions(-)
 create mode 100644 .eslintignore
 create mode 100644 .eslintrc.js
 create mode 100644 .nvmrc
 create mode 100644 test/.eslintrc.js
 create mode 100644 test/server/app/logging_test.js

diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 000000000..9569b6efc
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,17 @@
+build/**
+components/**
+dist/**
+libtextsecure/**
+coverage/**
+
+# these aren't ready yet, pulling files in one-by-one
+js/**
+test/**
+/*.js
+!main.js
+!prepare_build.js
+
+# all of these files will be new
+!test/server/**/*.js
+
+# all of app/ is included
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 000000000..f4db62c1e
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,41 @@
+// For reference: https://github.com/airbnb/javascript
+
+module.exports = {
+  settings: {
+    'import/core-modules': [
+      'electron'
+    ]
+  },
+
+  extends: [
+    'airbnb-base',
+  ],
+
+  rules: {
+    'comma-dangle': ['error', {
+        arrays: 'always-multiline',
+        objects: 'always-multiline',
+        imports: 'always-multiline',
+        exports: 'always-multiline',
+        functions: 'never',
+    }],
+
+    // putting params on their own line helps stay within line length limit
+    'function-paren-newline': ['error', 'consistent'],
+
+    // 90 characters allows three+ side-by-side screens on a standard-size monitor
+    'max-len': ['error', {
+      code: 90,
+      ignoreUrls: true,
+    }],
+
+    // it helps readability to put public API at top,
+    'no-use-before-define': 'off',
+
+    // useful for unused or internal fields
+    'no-underscore-dangle': 'off',
+
+    // though we have a logger, we still remap console to log to disk
+    'no-console': 'off',
+  }
+};
diff --git a/.gitignore b/.gitignore
index 357b5f23e..368ae7923 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 node_modules
 .sass-cache
+coverage/*
 build/curve25519_compiled.js
 build/icons/*
 stylesheets/*.css.map
@@ -11,3 +12,5 @@ config/local-*.json
 *.provisionprofile
 release/
 /dev-app-update.yml
+.nyc_output/
+*.sublime*
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..4bc5d6181
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+7.9.0
diff --git a/.travis.yml b/.travis.yml
index acee1fe24..762a4df20 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
 language: node_js
 node_js:
-  - 'node'
+  - '7.9.0'
 os:
   - linux
 dist: trusty
@@ -9,6 +9,8 @@ install:
 script:
   - yarn run generate
   - yarn prepare-build
+  - yarn eslint
+  - yarn test-server
   - ./node_modules/.bin/build --em.environment=$SIGNAL_ENV --config.mac.bundleVersion='$TRAVIS_BUILD_NUMBER' --publish=never
   - ./travis.sh
 env:
diff --git a/.yarnclean b/.yarnclean
index 3d9c26626..9f5015970 100644
--- a/.yarnclean
+++ b/.yarnclean
@@ -39,3 +39,6 @@ Gruntfile.js
 # misc
 *.gz
 *.md
+
+# asset directories
+!nyc/node_modules/istanbul-reports/lib/html/assets
diff --git a/app/auto_update.js b/app/auto_update.js
index ab454b29c..efcb70e67 100644
--- a/app/auto_update.js
+++ b/app/auto_update.js
@@ -1,4 +1,4 @@
-const autoUpdater = require('electron-updater').autoUpdater
+const { autoUpdater } = require('electron-updater');
 const { dialog } = require('electron');
 
 const config = require('./config');
@@ -18,7 +18,7 @@ function checkForUpdates() {
   autoUpdater.checkForUpdates();
 }
 
-var showingDialog = false;
+let showingDialog = false;
 function showUpdateDialog(mainWindow, messages) {
   if (showingDialog) {
     return;
@@ -29,21 +29,21 @@ function showUpdateDialog(mainWindow, messages) {
     type: 'info',
     buttons: [
       messages.autoUpdateRestartButtonLabel.message,
-      messages.autoUpdateLaterButtonLabel.message
+      messages.autoUpdateLaterButtonLabel.message,
     ],
     title: messages.autoUpdateNewVersionTitle.message,
     message: messages.autoUpdateNewVersionMessage.message,
     detail: messages.autoUpdateNewVersionInstructions.message,
     defaultId: LATER_BUTTON,
     cancelId: RESTART_BUTTON,
-  }
+  };
 
-  dialog.showMessageBox(mainWindow, options, function(response) {
-    if (response == RESTART_BUTTON) {
+  dialog.showMessageBox(mainWindow, options, (response) => {
+    if (response === RESTART_BUTTON) {
       // We delay these update calls because they don't seem to work in this
       //   callback - but only if the message box has a parent window.
       // Fixes this bug: https://github.com/WhisperSystems/Signal-Desktop/issues/1864
-      setTimeout(function() {
+      setTimeout(() => {
         windowState.markShouldQuit();
         autoUpdater.quitAndInstall();
       }, 200);
@@ -54,7 +54,7 @@ function showUpdateDialog(mainWindow, messages) {
 }
 
 function onError(error) {
-  console.log("Got an error while updating: ", error.stack);
+  console.log('Got an error while updating: ', error.stack);
 }
 
 function initialize(getMainWindow, messages) {
@@ -66,7 +66,7 @@ function initialize(getMainWindow, messages) {
     return;
   }
 
-  autoUpdater.addListener('update-downloaded', function() {
+  autoUpdater.addListener('update-downloaded', () => {
     showUpdateDialog(getMainWindow(), messages);
   });
   autoUpdater.addListener('error', onError);
@@ -77,5 +77,5 @@ function initialize(getMainWindow, messages) {
 }
 
 module.exports = {
-  initialize
+  initialize,
 };
diff --git a/app/config.js b/app/config.js
index 047d9c15e..d716be53e 100644
--- a/app/config.js
+++ b/app/config.js
@@ -1,9 +1,12 @@
 const path = require('path');
 
+const config = require('config');
+
 const packageJson = require('../package.json');
 
 
 const environment = packageJson.environment || process.env.NODE_ENV || 'development';
+config.environment = environment;
 
 // Set environment vars to configure node-config before requiring it
 process.env.NODE_ENV = environment;
@@ -19,8 +22,6 @@ if (environment === 'production') {
   process.env.SUPPRESS_NO_CONFIG_WARNING = '';
 }
 
-const config = require('config');
-config.environment = environment;
 
 // Log resulting env vars in use by config
 [
@@ -30,9 +31,9 @@ config.environment = environment;
   'ALLOW_CONFIG_MUTATIONS',
   'HOSTNAME',
   'NODE_APP_INSTANCE',
-  'SUPPRESS_NO_CONFIG_WARNING'
-].forEach(function(s) {
-  console.log(s + ' ' + config.util.getEnv(s));
+  'SUPPRESS_NO_CONFIG_WARNING',
+].forEach((s) => {
+  console.log(`${s} ${config.util.getEnv(s)}`);
 });
 
 module.exports = config;
diff --git a/app/locale.js b/app/locale.js
index b3e7bea0e..fddce1f27 100644
--- a/app/locale.js
+++ b/app/locale.js
@@ -1,9 +1,9 @@
 const path = require('path');
 const fs = require('fs');
-const app = require('electron').app;
+const { app } = require('electron');
 const _ = require('lodash');
 
-const logger = require('./logging').getLogger();
+const logging = require('./logging');
 
 function normalizeLocaleName(locale) {
   if (/^en-/.test(locale)) {
@@ -28,7 +28,8 @@ function getLocaleMessages(locale) {
 }
 
 function load() {
-  let english = getLocaleMessages('en');
+  const logger = logging.getLogger();
+  const english = getLocaleMessages('en');
   let appLocale = app.getLocale();
 
   if (process.env.NODE_ENV === 'test') {
@@ -49,7 +50,7 @@ function load() {
     // We start with english, then overwrite that with anything present in locale
     messages = _.merge(english, messages);
   } catch (e) {
-    logger.error('Problem loading messages for locale ' + localeName + ' ' + e.stack);
+    logger.error(`Problem loading messages for locale ${localeName} ${e.stack}`);
     logger.error('Falling back to en locale');
 
     localeName = 'en';
@@ -58,10 +59,10 @@ function load() {
 
   return {
     name: localeName,
-    messages
+    messages,
   };
 }
 
 module.exports = {
-  load: load
+  load,
 };
diff --git a/app/logging.js b/app/logging.js
index 1257c24d0..bad52467a 100644
--- a/app/logging.js
+++ b/app/logging.js
@@ -1,22 +1,31 @@
 const path = require('path');
 const fs = require('fs');
 
-const electron = require('electron')
+const electron = require('electron');
 const bunyan = require('bunyan');
 const mkdirp = require('mkdirp');
 const _ = require('lodash');
+const readFirstLine = require('firstline');
+const readLastLines = require('read-last-lines').read;
 
-
-const app = electron.app;
-const ipc = electron.ipcMain;
+const {
+  app,
+  ipcMain: ipc,
+} = electron;
 const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
-
 let logger;
 
 
-function dropFirst(args) {
-  return Array.prototype.slice.call(args, 1);
-}
+module.exports = {
+  initialize,
+  getLogger,
+  // for tests only:
+  isLineAfterDate,
+  eliminateOutOfDateFiles,
+  eliminateOldEntries,
+  fetchLog,
+  fetch,
+};
 
 function initialize() {
   if (logger) {
@@ -27,38 +36,114 @@ function initialize() {
   const logPath = path.join(basePath, 'logs');
   mkdirp.sync(logPath);
 
-  const logFile = path.join(logPath, 'log.log');
-
-  logger = bunyan.createLogger({
-    name: 'log',
-    streams: [{
-      level: 'debug',
-      stream: process.stdout
-    }, {
-      type: 'rotating-file',
-      path: logFile,
-      period: '1d',
-      count: 3
-    }]
-  });
+  return cleanupLogs(logPath).then(() => {
+    const logFile = path.join(logPath, 'log.log');
+
+    logger = bunyan.createLogger({
+      name: 'log',
+      streams: [{
+        level: 'debug',
+        stream: process.stdout,
+      }, {
+        type: 'rotating-file',
+        path: logFile,
+        period: '1d',
+        count: 3,
+      }],
+    });
 
-  LEVELS.forEach(function(level) {
-    ipc.on('log-' + level, function() {
-      // first parameter is the event, rest are provided arguments
-      var args = dropFirst(arguments);
-      logger[level].apply(logger, args);
+    LEVELS.forEach((level) => {
+      ipc.on(`log-${level}`, (first, ...rest) => {
+        logger[level](...rest);
+      });
     });
-  });
 
-  ipc.on('fetch-log', function(event) {
-    fetch(logPath).then(function(data) {
-      event.sender.send('fetched-log', data);
-    }, function(error) {
-      logger.error('Problem loading log from disk: ' + error.stack);
+    ipc.on('fetch-log', (event) => {
+      fetch(logPath).then((data) => {
+        event.sender.send('fetched-log', data);
+      }, (error) => {
+        logger.error(`Problem loading log from disk: ${error.stack}`);
+      });
     });
   });
 }
 
+function cleanupLogs(logPath) {
+  const now = new Date();
+  const earliestDate = new Date(Date.UTC(
+    now.getUTCFullYear(),
+    now.getUTCMonth(),
+    now.getUTCDate() - 3
+  ));
+
+  return eliminateOutOfDateFiles(logPath, earliestDate).then((remaining) => {
+    const files = _.filter(remaining, file => !file.start && file.end);
+
+    if (!files.length) {
+      return null;
+    }
+
+    return eliminateOldEntries(files, earliestDate);
+  });
+}
+
+function isLineAfterDate(line, date) {
+  if (!line) {
+    return false;
+  }
+
+  try {
+    const data = JSON.parse(line);
+    return (new Date(data.time)).getTime() > date.getTime();
+  } catch (e) {
+    console.log('error parsing log line', e.stack, line);
+    return false;
+  }
+}
+
+function eliminateOutOfDateFiles(logPath, date) {
+  const files = fs.readdirSync(logPath);
+  const paths = files.map(file => path.join(logPath, file));
+
+  return Promise.all(_.map(
+    paths,
+    target => Promise.all([
+      readFirstLine(target),
+      readLastLines(target, 2),
+    ]).then((results) => {
+      const start = results[0];
+      const end = results[1].split('\n');
+
+      const file = {
+        path: target,
+        start: isLineAfterDate(start, date),
+        end: isLineAfterDate(end[end.length - 1], date)
+          || isLineAfterDate(end[end.length - 2], date),
+      };
+
+      if (!file.start && !file.end) {
+        fs.unlinkSync(file.path);
+      }
+
+      return file;
+    })
+  ));
+}
+
+function eliminateOldEntries(files, date) {
+  const earliest = date.getTime();
+
+  return Promise.all(_.map(
+    files,
+    file => fetchLog(file.path).then((lines) => {
+      const recent = _.filter(lines, line => (new Date(line.time)).getTime() >= earliest);
+      const text = _.map(recent, line => JSON.stringify(line)).join('\n');
+
+      return fs.writeFileSync(file.path, `${text}\n`);
+    })
+  ));
+}
+
 function getLogger() {
   if (!logger) {
     throw new Error('Logger hasn\'t been initialized yet!');
@@ -68,18 +153,19 @@ function getLogger() {
 }
 
 function fetchLog(logFile) {
-  return new Promise(function(resolve, reject) {
-    fs.readFile(logFile, { encoding: 'utf8' }, function(err, text) {
+  return new Promise((resolve, reject) => {
+    fs.readFile(logFile, { encoding: 'utf8' }, (err, text) => {
       if (err) {
         return reject(err);
       }
 
       const lines = _.compact(text.split('\n'));
-      const data = _.compact(lines.map(function(line) {
+      const data = _.compact(lines.map((line) => {
         try {
           return _.pick(JSON.parse(line), ['level', 'time', 'msg']);
+        } catch (e) {
+          return null;
         }
-        catch (e) {}
       }));
 
       return resolve(data);
@@ -89,19 +175,17 @@ function fetchLog(logFile) {
 
 function fetch(logPath) {
   const files = fs.readdirSync(logPath);
-  const paths = files.map(function(file) {
-    return path.join(logPath, file)
-  });
+  const paths = files.map(file => path.join(logPath, file));
 
   // creating a manual log entry for the final log result
-  var now = new Date();
+  const now = new Date();
   const fileListEntry = {
     level: 30, // INFO
     time: now.toJSON(),
-    msg: 'Loaded this list of log files from logPath: ' + files.join(', '),
+    msg: `Loaded this list of log files from logPath: ${files.join(', ')}`,
   };
 
-  return Promise.all(paths.map(fetchLog)).then(function(results) {
+  return Promise.all(paths.map(fetchLog)).then((results) => {
     const data = _.flatten(results);
 
     data.push(fileListEntry);
@@ -111,18 +195,14 @@ function fetch(logPath) {
 }
 
 
-function logAtLevel() {
-  const level = arguments[0];
-  const args = Array.prototype.slice.call(arguments, 1);
-
+function logAtLevel(level, ...args) {
   if (logger) {
     // To avoid [Object object] in our log since console.log handles non-strings smoothly
-    const str = args.map(function(item) {
+    const str = args.map((item) => {
       if (typeof item !== 'string') {
         try {
           return JSON.stringify(item);
-        }
-        catch (e) {
+        } catch (e) {
           return item;
         }
       }
@@ -131,20 +211,16 @@ function logAtLevel() {
     });
     logger[level](str.join(' '));
   } else {
-    console._log.apply(console, consoleArgs);
+    console._log(...args);
   }
 }
 
-
-console._log = console.log;
-console.log = _.partial(logAtLevel, 'info');
-console._error = console.error;
-console.error = _.partial(logAtLevel, 'error');
-console._warn = console.warn;
-console.warn = _.partial(logAtLevel, 'warn');
-
-
-module.exports = {
-  initialize,
-  getLogger,
-};
+// This blows up using mocha --watch, so we ensure it is run just once
+if (!console._log) {
+  console._log = console.log;
+  console.log = _.partial(logAtLevel, 'info');
+  console._error = console.error;
+  console.error = _.partial(logAtLevel, 'error');
+  console._warn = console.warn;
+  console.warn = _.partial(logAtLevel, 'warn');
+}
diff --git a/app/menu.js b/app/menu.js
index 4eacc20ef..f12c6a35b 100644
--- a/app/menu.js
+++ b/app/menu.js
@@ -1,18 +1,20 @@
 function createTemplate(options, messages) {
-  const showDebugLog = options.showDebugLog;
-  const showAbout = options.showAbout;
-  const openReleaseNotes = options.openReleaseNotes;
-  const openNewBugForm = options.openNewBugForm;
-  const openSupportPage = options.openSupportPage;
-  const openForums = options.openForums;
+  const {
+    showDebugLog,
+    showAbout,
+    openReleaseNotes,
+    openNewBugForm,
+    openSupportPage,
+    openForums,
+  } = options;
 
-  let template = [{
+  const template = [{
     label: messages.mainMenuFile.message,
     submenu: [
       {
         role: 'quit',
       },
-    ]
+    ],
   },
   {
     label: messages.mainMenuEdit.message,
@@ -43,8 +45,8 @@ function createTemplate(options, messages) {
       },
       {
         role: 'selectall',
-      }
-    ]
+      },
+    ],
   },
   {
     label: messages.mainMenuView.message,
@@ -77,7 +79,7 @@ function createTemplate(options, messages) {
       {
         role: 'toggledevtools',
       },
-    ]
+    ],
   },
   {
     label: messages.mainMenuWindow.message,
@@ -86,7 +88,7 @@ function createTemplate(options, messages) {
       {
         role: 'minimize',
       },
-    ]
+    ],
   },
   {
     label: messages.mainMenuHelp.message,
@@ -118,7 +120,7 @@ function createTemplate(options, messages) {
         label: messages.aboutSignalDesktop.message,
         click: showAbout,
       },
-    ]
+    ],
   }];
 
   if (process.platform === 'darwin') {
@@ -129,8 +131,10 @@ function createTemplate(options, messages) {
 }
 
 function updateForMac(template, messages, options) {
-  const showWindow = options.showWindow;
-  const showAbout = options.showAbout;
+  const {
+    showWindow,
+    showAbout,
+  } = options;
 
   // Remove About item and separator from Help menu, since it's on the first menu
   template[4].submenu.pop();
@@ -162,13 +166,13 @@ function updateForMac(template, messages, options) {
       {
         role: 'quit',
       },
-    ]
+    ],
   });
 
   // Add to Edit menu
   template[1].submenu.push(
     {
-      type: 'separator'
+      type: 'separator',
     },
     {
       label: messages.speech.message,
@@ -179,11 +183,12 @@ function updateForMac(template, messages, options) {
         {
           role: 'stopspeaking',
         },
-      ]
+      ],
     }
   );
 
-  // Add to Window menu
+  // Replace Window menu
+  // eslint-disable-next-line no-param-reassign
   template[3].submenu = [
     {
       accelerator: 'CmdOrCtrl+W',
diff --git a/app/tray_icon.js b/app/tray_icon.js
index 535e9504d..d32ff7652 100644
--- a/app/tray_icon.js
+++ b/app/tray_icon.js
@@ -1,23 +1,24 @@
-const electron = require('electron')
 const path = require('path');
 
-const app = electron.app;
-const Menu = electron.Menu;
-const Tray = electron.Tray;
+const {
+  app,
+  Menu,
+  Tray,
+} = require('electron');
 
 let trayContextMenu = null;
 let tray = null;
 
 function createTrayIcon(getMainWindow, messages) {
-
   // A smaller icon is needed on macOS
   tray = new Tray(
-    process.platform == "darwin" ?
-    path.join(__dirname, '..', 'images', 'icon_16.png') :
-    path.join(__dirname, '..', 'images', 'icon_256.png'));
+    process.platform === 'darwin' ?
+      path.join(__dirname, '..', 'images', 'icon_16.png') :
+      path.join(__dirname, '..', 'images', 'icon_256.png')
+  );
 
-  tray.toggleWindowVisibility = function () {
-    var mainWindow = getMainWindow();
+  tray.toggleWindowVisibility = () => {
+    const mainWindow = getMainWindow();
     if (mainWindow) {
       if (mainWindow.isVisible()) {
         mainWindow.hide();
@@ -33,31 +34,28 @@ function createTrayIcon(getMainWindow, messages) {
       }
     }
     tray.updateContextMenu();
-  }
-
-  tray.updateContextMenu = function () {
+  };
 
-    var mainWindow = getMainWindow();
+  tray.updateContextMenu = () => {
+    const mainWindow = getMainWindow();
 
     // NOTE: we want to have the show/hide entry available in the tray icon
     // context menu, since the 'click' event may not work on all platforms.
     // For details please refer to:
     // https://github.com/electron/electron/blob/master/docs/api/tray.md.
-    trayContextMenu = Menu.buildFromTemplate([
-        {
-          id: 'toggleWindowVisibility',
-          label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message,
-          click: tray.toggleWindowVisibility
-        },
-        {
-          id: 'quit',
-          label: messages.quit.message,
-          click: app.quit.bind(app)
-        }
-    ]);
+    trayContextMenu = Menu.buildFromTemplate([{
+      id: 'toggleWindowVisibility',
+      label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message,
+      click: tray.toggleWindowVisibility,
+    },
+    {
+      id: 'quit',
+      label: messages.quit.message,
+      click: app.quit.bind(app),
+    }]);
 
     tray.setContextMenu(trayContextMenu);
-  }
+  };
 
   tray.on('click', tray.toggleWindowVisibility);
 
diff --git a/app/user_config.js b/app/user_config.js
index 4f77368b5..c0f0bb539 100644
--- a/app/user_config.js
+++ b/app/user_config.js
@@ -1,6 +1,6 @@
 const path = require('path');
 
-const app = require('electron').app;
+const { app } = require('electron');
 const ElectronConfig = require('electron-config');
 
 const config = require('./config');
@@ -10,13 +10,13 @@ const config = require('./config');
 if (config.has('storageProfile')) {
   const userData = path.join(
     app.getPath('appData'),
-    'Signal-' + config.get('storageProfile')
+    `Signal-${config.get('storageProfile')}`
   );
 
   app.setPath('userData', userData);
 }
 
-console.log('userData: ' + app.getPath('userData'));
+console.log(`userData: ${app.getPath('userData')}`);
 
 // this needs to be below our update to the appData path
 const userConfig = new ElectronConfig();
diff --git a/app/window_state.js b/app/window_state.js
index 6d264339c..0cd7dd024 100644
--- a/app/window_state.js
+++ b/app/window_state.js
@@ -10,5 +10,5 @@ function shouldQuit() {
 
 module.exports = {
   shouldQuit,
-  markShouldQuit
+  markShouldQuit,
 };
diff --git a/appveyor.yml b/appveyor.yml
index 671182985..6aac7837e 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -8,10 +8,12 @@ cache:
 install:
   - systeminfo | findstr /C:"OS"
   - set PATH=C:\Ruby23-x64\bin;%PATH%
-  - ps: Install-Product node 6 x64
+  - ps: Install-Product node 7.9.0 x64
   - yarn install
 
 build_script:
+  - yarn eslint
+  - yarn test-server
   - yarn run icon-gen
   - node build\grunt.js
   - type package.json | findstr /v certificateSubjectName > temp.json
diff --git a/main.js b/main.js
index c1e1c86cd..e164f42ef 100644
--- a/main.js
+++ b/main.js
@@ -6,19 +6,25 @@ const _ = require('lodash');
 const electron = require('electron');
 const semver = require('semver');
 
-const BrowserWindow = electron.BrowserWindow;
-const app = electron.app;
-const ipc = electron.ipcMain;
-const Menu = electron.Menu;
-const shell = electron.shell;
+const {
+  BrowserWindow,
+  app,
+  Menu,
+  shell,
+  ipcMain: ipc,
+} = electron;
 
 const packageJson = require('./package.json');
+
+const createTrayIcon = require('./app/tray_icon');
+const createTemplate = require('./app/menu.js');
+const logging = require('./app/logging');
 const autoUpdate = require('./app/auto_update');
 const windowState = require('./app/window_state');
 
 
-const aumid = 'org.whispersystems.' + packageJson.name;
-console.log('setting AUMID to ' + aumid);
+const aumid = `org.whispersystems.${packageJson.name}`;
+console.log(`setting AUMID to ${aumid}`);
 app.setAppUserModelId(aumid);
 
 // Keep a global reference of the window object, if you don't, the window will
@@ -34,7 +40,7 @@ let tray = null;
 const startInTray = process.argv.find(arg => arg === '--start-in-tray');
 const usingTrayIcon = startInTray || process.argv.find(arg => arg === '--use-tray-icon');
 
-const config = require("./app/config");
+const config = require('./app/config');
 
 // Very important to put before the single instance check, since it is based on the
 //   userData directory.
@@ -63,7 +69,7 @@ function showWindow() {
 
 if (!process.mas) {
   console.log('making app single instance');
-  var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) {
+  const shouldQuit = app.makeSingleInstance(() => {
     // Someone tried to run a second instance, we should focus our window
     if (mainWindow) {
       if (mainWindow.isMinimized()) {
@@ -78,19 +84,14 @@ if (!process.mas) {
   if (shouldQuit) {
     console.log('quitting; we are the second instance');
     app.quit();
-    return;
   }
 }
 
-const logging = require('./app/logging');
-
-// This must be after we set up appPath in user_config.js, so we know where logs go
-logging.initialize();
-const logger = logging.getLogger();
-
 let windowConfig = userConfig.get('window');
 const loadLocale = require('./app/locale').load;
 
+// Both of these will be set after app fires the 'ready' event
+let logger;
 let locale;
 
 const WINDOWS_8 = '8.0.0';
@@ -118,20 +119,20 @@ function prepareURL(pathSegments) {
       appInstance: process.env.NODE_APP_INSTANCE,
       polyfillNotifications: polyfillNotifications ? true : undefined, // for stringify()
       proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy,
-    }
-  })
+    },
+  });
 }
 
 function handleUrl(event, target) {
   event.preventDefault();
-  const protocol = url.parse(target).protocol;
+  const { protocol } = url.parse(target);
   if (protocol === 'http:' || protocol === 'https:') {
     shell.openExternal(target);
   }
 }
 
 function captureClicks(window) {
-  window.webContents.on('will-navigate', handleUrl)
+  window.webContents.on('will-navigate', handleUrl);
   window.webContents.on('new-window', handleUrl);
 }
 
@@ -150,11 +151,11 @@ function isVisible(window, bounds) {
 
   // requiring BOUNDS_BUFFER pixels on the left or right side
   const rightSideClearOfLeftBound = (window.x + window.width >= boundsX + BOUNDS_BUFFER);
-  const leftSideClearOfRightBound = (window.x <= boundsX + boundsWidth - BOUNDS_BUFFER);
+  const leftSideClearOfRightBound = (window.x <= (boundsX + boundsWidth) - BOUNDS_BUFFER);
 
   // top can't be offscreen, and must show at least BOUNDS_BUFFER pixels at bottom
   const topClearOfUpperBound = window.y >= boundsY;
-  const topClearOfLowerBound = (window.y <= boundsY + boundsHeight - BOUNDS_BUFFER);
+  const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER);
 
   return rightSideClearOfLeftBound
     && leftSideClearOfRightBound
@@ -162,8 +163,8 @@ function isVisible(window, bounds) {
     && topClearOfLowerBound;
 }
 
-function createWindow () {
-  const screen = electron.screen;
+function createWindow() {
+  const { screen } = electron;
   const windowOptions = Object.assign({
     show: !startInTray, // allow to start minimised in tray
     width: DEFAULT_WIDTH,
@@ -173,8 +174,8 @@ function createWindow () {
     autoHideMenuBar: false,
     webPreferences: {
       nodeIntegration: false,
-      //sandbox: true,
-      preload: path.join(__dirname, 'preload.js')
+      // sandbox: true,
+      preload: path.join(__dirname, 'preload.js'),
     },
     icon: path.join(__dirname, 'images', 'icon_256.png'),
   }, _.pick(windowConfig, ['maximized', 'autoHideMenuBar', 'width', 'height', 'x', 'y']));
@@ -192,7 +193,7 @@ function createWindow () {
     delete windowOptions.autoHideMenuBar;
   }
 
-  const visibleOnAnyScreen = _.some(screen.getAllDisplays(), function(display) {
+  const visibleOnAnyScreen = _.some(screen.getAllDisplays(), (display) => {
     if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) {
       return false;
     }
@@ -225,7 +226,7 @@ function createWindow () {
       width: size[0],
       height: size[1],
       x: position[0],
-      y: position[1]
+      y: position[1],
     };
 
     if (mainWindow.isFullScreen()) {
@@ -243,12 +244,13 @@ function createWindow () {
   mainWindow.on('move', debouncedCaptureStats);
   mainWindow.on('close', captureAndSaveWindowStats);
 
-  mainWindow.on('focus', function() {
+  mainWindow.on('focus', () => {
     mainWindow.flashFrame(false);
   });
 
   // Ingested in preload.js via a sendSync call
-  ipc.on('locale-data', function(event, arg) {
+  ipc.on('locale-data', (event) => {
+    // eslint-disable-next-line no-param-reassign
     event.returnValue = locale.messages;
   });
 
@@ -262,23 +264,21 @@ function createWindow () {
 
   if (config.get('openDevTools')) {
     // Open the DevTools.
-    mainWindow.webContents.openDevTools()
+    mainWindow.webContents.openDevTools();
   }
 
   captureClicks(mainWindow);
 
-  mainWindow.webContents.on('will-navigate', function(e) {
+  mainWindow.webContents.on('will-navigate', (e) => {
     logger.info('will-navigate');
     e.preventDefault();
   });
 
   // Emitted when the window is about to be closed.
-  mainWindow.on('close', function (e) {
-
+  mainWindow.on('close', (e) => {
     // If the application is terminating, just do the default
     if (windowState.shouldQuit()
       || config.environment === 'test' || config.environment === 'test-lib') {
-
       return;
     }
 
@@ -296,26 +296,26 @@ function createWindow () {
   });
 
   // Emitted when the window is closed.
-  mainWindow.on('closed', function () {
+  mainWindow.on('closed', () => {
     // Dereference the window object, usually you would store windows
     // in an array if your app supports multi windows, this is the time
     // when you should delete the corresponding element.
-    mainWindow = null
+    mainWindow = null;
   });
 
-  ipc.on('show-window', function() {
+  ipc.on('show-window', () => {
     showWindow();
   });
 }
 
 function showDebugLog() {
   if (mainWindow) {
-    mainWindow.webContents.send('debug-log')
+    mainWindow.webContents.send('debug-log');
   }
 }
 
 function openReleaseNotes() {
-  shell.openExternal('https://github.com/WhisperSystems/Signal-Desktop/releases/tag/v' + app.getVersion());
+  shell.openExternal(`https://github.com/WhisperSystems/Signal-Desktop/releases/tag/v${app.getVersion()}`);
 }
 
 function openNewBugForm() {
@@ -348,7 +348,7 @@ function showAbout() {
     show: false,
     webPreferences: {
       nodeIntegration: false,
-      preload: path.join(__dirname, 'preload.js')
+      preload: path.join(__dirname, 'preload.js'),
     },
     parent: mainWindow,
   };
@@ -359,11 +359,11 @@ function showAbout() {
 
   aboutWindow.loadURL(prepareURL([__dirname, 'about.html']));
 
-  aboutWindow.on('closed', function () {
+  aboutWindow.on('closed', () => {
     aboutWindow = null;
   });
 
-  aboutWindow.once('ready-to-show', function() {
+  aboutWindow.once('ready-to-show', () => {
     aboutWindow.show();
   });
 }
@@ -372,53 +372,64 @@ function showAbout() {
 // initialization and is ready to create browser windows.
 // Some APIs can only be used after this event occurs.
 let ready = false;
-app.on('ready', function() {
-  logger.info('app ready');
-  ready = true;
+app.on('ready', () => {
+  let loggingSetupError;
+  logging.initialize().catch((error) => {
+    loggingSetupError = error;
+  }).then(() => {
+    logger = logging.getLogger();
+    logger.info('app ready');
+
+    if (loggingSetupError) {
+      logger.error('Problem setting up logging', loggingSetupError.stack);
+    }
 
-  if (!locale) {
-    locale = loadLocale();
-  }
+    if (!locale) {
+      locale = loadLocale();
+    }
 
-  autoUpdate.initialize(getMainWindow, locale.messages);
+    ready = true;
 
-  createWindow();
+    autoUpdate.initialize(getMainWindow, locale.messages);
 
-  if (usingTrayIcon) {
-    const createTrayIcon = require("./app/tray_icon");
-    tray = createTrayIcon(getMainWindow, locale.messages);
-  }
+    createWindow();
 
-  const options = {
-    showDebugLog,
-    showWindow,
-    showAbout,
-    openReleaseNotes,
-    openNewBugForm,
-    openSupportPage,
-    openForums,
-  };
-  const createTemplate = require('./app/menu.js');
-  const template = createTemplate(options, locale.messages);
+    if (usingTrayIcon) {
+      tray = createTrayIcon(getMainWindow, locale.messages);
+    }
 
-  const menu = Menu.buildFromTemplate(template);
-  Menu.setApplicationMenu(menu);
-})
+    const options = {
+      showDebugLog,
+      showWindow,
+      showAbout,
+      openReleaseNotes,
+      openNewBugForm,
+      openSupportPage,
+      openForums,
+    };
+    const template = createTemplate(options, locale.messages);
 
-app.on('before-quit', function() {
+    const menu = Menu.buildFromTemplate(template);
+    Menu.setApplicationMenu(menu);
+  });
+});
+
+app.on('before-quit', () => {
   windowState.markShouldQuit();
 });
 
 // Quit when all windows are closed.
-app.on('window-all-closed', function () {
+app.on('window-all-closed', () => {
   // On OS X it is common for applications and their menu bar
   // to stay active until the user quits explicitly with Cmd + Q
-  if (process.platform !== 'darwin' || config.environment === 'test' || config.environment === 'test-lib') {
-    app.quit()
+  if (process.platform !== 'darwin'
+    || config.environment === 'test'
+    || config.environment === 'test-lib') {
+    app.quit();
   }
-})
+});
 
-app.on('activate', function () {
+app.on('activate', () => {
   if (!ready) {
     return;
   }
@@ -430,46 +441,43 @@ app.on('activate', function () {
   } else {
     createWindow();
   }
-})
-
-// In this file you can include the rest of your app's specific main process
-// code. You can also put them in separate files and require them here.
+});
 
-ipc.on('set-badge-count', function(event, count) {
+ipc.on('set-badge-count', (event, count) => {
   app.setBadgeCount(count);
 });
 
-ipc.on('draw-attention', function(event, count) {
+ipc.on('draw-attention', () => {
   if (process.platform === 'darwin') {
     app.dock.bounce();
-  } else if (process.platform == 'win32') {
+  } else if (process.platform === 'win32') {
     mainWindow.flashFrame(true);
-    setTimeout(function() {
+    setTimeout(() => {
       mainWindow.flashFrame(false);
     }, 1000);
-  } else if (process.platform == 'linux') {
+  } else if (process.platform === 'linux') {
     mainWindow.flashFrame(true);
   }
 });
 
-ipc.on('restart', function(event) {
+ipc.on('restart', () => {
   app.relaunch();
   app.quit();
 });
 
-ipc.on("set-auto-hide-menu-bar", function(event, autoHide) {
+ipc.on('set-auto-hide-menu-bar', (event, autoHide) => {
   if (mainWindow) {
     mainWindow.setAutoHideMenuBar(autoHide);
   }
 });
 
-ipc.on("set-menu-bar-visibility", function(event, visibility) {
+ipc.on('set-menu-bar-visibility', (event, visibility) => {
   if (mainWindow) {
     mainWindow.setMenuBarVisibility(visibility);
   }
 });
 
-ipc.on("close-about", function() {
+ipc.on('close-about', () => {
   if (aboutWindow) {
     aboutWindow.close();
   }
diff --git a/package.json b/package.json
index 5c31e215e..a1dd3bb4a 100644
--- a/package.json
+++ b/package.json
@@ -10,26 +10,6 @@
     "email": "support@whispersystems.org"
   },
   "main": "main.js",
-  "devDependencies": {
-    "asar": "^0.14.0",
-    "bower": "^1.8.2",
-    "electron": "1.7.10",
-    "electron-builder": "^19.49.2",
-    "electron-icon-maker": "^0.0.4",
-    "electron-publisher-s3": "^19.49.0",
-    "grunt": "^1.0.1",
-    "grunt-cli": "^1.2.0",
-    "grunt-contrib-concat": "^1.0.1",
-    "grunt-contrib-copy": "^1.0.0",
-    "grunt-contrib-jshint": "^1.1.0",
-    "grunt-contrib-watch": "^1.0.0",
-    "grunt-exec": "^3.0.0",
-    "grunt-gitinfo": "^0.1.7",
-    "grunt-jscs": "^3.0.1",
-    "grunt-sass": "^2.0.0",
-    "node-sass-import-once": "^1.2.0",
-    "spectron": "^3.7.2"
-  },
   "scripts": {
     "postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
     "test": "grunt test",
@@ -53,7 +33,62 @@
     "release-mac": "npm run build-release -- -m --prepackaged release/mac/Signal*.app --publish=always",
     "release-win": "npm run build-release -- -w --prepackaged release/windows --publish=always",
     "release-lin": "npm run build-release -- -l --prepackaged release/linux && NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh",
-    "release": "npm run release-mac && npm run release-win && npm run release-lin"
+    "release": "npm run release-mac && npm run release-win && npm run release-lin",
+    "test-server": "mocha --recursive test/server",
+    "test-server-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/server",
+    "eslint": "eslint .",
+    "open-coverage": "open coverage/lcov-report/index.html"
+  },
+  "dependencies": {
+    "bunyan": "^1.8.12",
+    "config": "^1.28.1",
+    "electron-config": "^1.0.0",
+    "electron-editor-context-menu": "^1.1.1",
+    "electron-updater": "^2.17.6",
+    "emoji-datasource": "4.0.0",
+    "emoji-datasource-apple": "4.0.0",
+    "emoji-js": "^3.4.0",
+    "emoji-panel": "https://github.com/scottnonnenberg/emoji-panel.git#v0.5.5",
+    "firstline": "^1.2.1",
+    "google-libphonenumber": "^3.0.7",
+    "lodash": "^4.17.4",
+    "mkdirp": "^0.5.1",
+    "node-fetch": "https://github.com/scottnonnenberg/node-fetch.git#3e5f51e08c647ee5f20c43b15cf2d352d61c36b4",
+    "node-notifier": "^5.1.2",
+    "os-locale": "^2.1.0",
+    "proxy-agent": "^2.1.0",
+    "read-last-lines": "^1.3.0",
+    "rimraf": "^2.6.2",
+    "semver": "^5.4.1",
+    "spellchecker": "^3.4.4",
+    "websocket": "^1.0.25"
+  },
+  "devDependencies": {
+    "asar": "^0.14.0",
+    "bower": "^1.8.2",
+    "chai": "^4.1.2",
+    "electron": "1.7.10",
+    "electron-builder": "^19.49.2",
+    "electron-icon-maker": "^0.0.4",
+    "electron-publisher-s3": "^19.49.0",
+    "eslint": "^4.14.0",
+    "eslint-config-airbnb-base": "^12.1.0",
+    "eslint-plugin-import": "^2.8.0",
+    "grunt": "^1.0.1",
+    "grunt-cli": "^1.2.0",
+    "grunt-contrib-concat": "^1.0.1",
+    "grunt-contrib-copy": "^1.0.0",
+    "grunt-contrib-jshint": "^1.1.0",
+    "grunt-contrib-watch": "^1.0.0",
+    "grunt-exec": "^3.0.0",
+    "grunt-gitinfo": "^0.1.7",
+    "grunt-jscs": "^3.0.1",
+    "grunt-sass": "^2.0.0",
+    "mocha": "^4.1.0",
+    "node-sass-import-once": "^1.2.0",
+    "nyc": "^11.4.1",
+    "spectron": "^3.7.2",
+    "tmp": "^0.0.33"
   },
   "build": {
     "appId": "org.whispersystems.signal-desktop",
@@ -169,27 +204,5 @@
       "node_modules/spellchecker/build/Release/*.node",
       "node_modules/websocket/build/Release/*.node"
     ]
-  },
-  "dependencies": {
-    "bunyan": "^1.8.12",
-    "config": "^1.28.1",
-    "electron-config": "^1.0.0",
-    "electron-editor-context-menu": "^1.1.1",
-    "electron-updater": "^2.17.6",
-    "emoji-datasource": "4.0.0",
-    "emoji-datasource-apple": "4.0.0",
-    "emoji-js": "^3.4.0",
-    "emoji-panel": "https://github.com/scottnonnenberg/emoji-panel.git#v0.5.5",
-    "google-libphonenumber": "^3.0.7",
-    "lodash": "^4.17.4",
-    "mkdirp": "^0.5.1",
-    "node-fetch": "https://github.com/scottnonnenberg/node-fetch.git#3e5f51e08c647ee5f20c43b15cf2d352d61c36b4",
-    "node-notifier": "^5.1.2",
-    "os-locale": "^2.1.0",
-    "proxy-agent": "^2.1.0",
-    "rimraf": "^2.6.2",
-    "semver": "^5.4.1",
-    "spellchecker": "^3.4.4",
-    "websocket": "^1.0.25"
   }
 }
diff --git a/prepare_build.js b/prepare_build.js
index 06d894f65..47acc5d57 100644
--- a/prepare_build.js
+++ b/prepare_build.js
@@ -2,7 +2,9 @@ const fs = require('fs');
 const _ = require('lodash');
 
 const packageJson = require('./package.json');
-const version = packageJson.version;
+
+
+const { version } = packageJson;
 const beta = /beta/;
 
 // You might be wondering why this file is necessary. It comes down to our desire to allow
@@ -12,7 +14,7 @@ const beta = /beta/;
 //   adding the ${channel} macro to these values, but Electron-Builder didn't like that.
 
 if (!beta.test(version)) {
-  return;
+  process.exit();
 }
 
 console.log('prepare_build: updating package.json for beta build');
@@ -36,13 +38,12 @@ const PRODUCTION_STARTUP_WM_CLASS = 'Signal';
 const BETA_STARTUP_WM_CLASS = 'Signal Beta';
 
 
-
 // -------
 
 function checkValue(object, objectPath, expected) {
-  const actual = _.get(object, objectPath)
+  const actual = _.get(object, objectPath);
   if (actual !== expected) {
-    throw new Error(objectPath + ' was ' + actual + '; expected ' + expected);
+    throw new Error(`${objectPath} was ${actual}; expected ${expected}`);
   }
 }
 
diff --git a/test/.eslintrc.js b/test/.eslintrc.js
new file mode 100644
index 000000000..3a0872c30
--- /dev/null
+++ b/test/.eslintrc.js
@@ -0,0 +1,17 @@
+// For reference: https://github.com/airbnb/javascript
+
+module.exports = {
+  env: {
+    mocha: true,
+  },
+
+  rules: {
+    // We still get the value of this rule, it just allows for dev deps
+    'import/no-extraneous-dependencies': ['error', {
+      devDependencies: true
+    }],
+
+    // We want to keep each test structured the same, even if its contents are tiny
+    'arrow-body-style': 'off',
+  }
+};
diff --git a/test/server/app/logging_test.js b/test/server/app/logging_test.js
new file mode 100644
index 000000000..865232bfc
--- /dev/null
+++ b/test/server/app/logging_test.js
@@ -0,0 +1,271 @@
+const fs = require('fs');
+const path = require('path');
+
+const tmp = require('tmp');
+const { expect } = require('chai');
+
+const {
+  eliminateOutOfDateFiles,
+  eliminateOldEntries,
+  isLineAfterDate,
+  fetchLog,
+  fetch,
+} = require('../../../app/logging');
+
+describe('app/logging', () => {
+  let basePath;
+  let tmpDir;
+
+  beforeEach(() => {
+    tmpDir = tmp.dirSync({
+      unsafeCleanup: true,
+    });
+    basePath = tmpDir.name;
+  });
+
+  afterEach((done) => {
+    // we need the unsafe option to recursively remove the directory
+    tmpDir.removeCallback(done);
+  });
+
+  describe('#isLineAfterDate', () => {
+    it('returns false if falsy', () => {
+      const actual = isLineAfterDate('', new Date());
+      expect(actual).to.equal(false);
+    });
+    it('returns false if invalid JSON', () => {
+      const actual = isLineAfterDate('{{}', new Date());
+      expect(actual).to.equal(false);
+    });
+    it('returns false if date is invalid', () => {
+      const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
+      const actual = isLineAfterDate(line, new Date('try6'));
+      expect(actual).to.equal(false);
+    });
+    it('returns false if log time is invalid', () => {
+      const line = JSON.stringify({ time: 'try7' });
+      const date = new Date('2018-01-04T19:17:00.000Z');
+      const actual = isLineAfterDate(line, date);
+      expect(actual).to.equal(false);
+    });
+    it('returns false if date before provided date', () => {
+      const line = JSON.stringify({ time: '2018-01-04T19:17:00.000Z' });
+      const date = new Date('2018-01-04T19:17:05.014Z');
+      const actual = isLineAfterDate(line, date);
+      expect(actual).to.equal(false);
+    });
+    it('returns true if date is after provided date', () => {
+      const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
+      const date = new Date('2018-01-04T19:17:00.000Z');
+      const actual = isLineAfterDate(line, date);
+      expect(actual).to.equal(true);
+    });
+  });
+
+  describe('#eliminateOutOfDateFiles', () => {
+    it('deletes an empty file', () => {
+      const date = new Date();
+      const log = '\n';
+      const target = path.join(basePath, 'log.log');
+      fs.writeFileSync(target, log);
+
+      return eliminateOutOfDateFiles(basePath, date).then(() => {
+        expect(fs.existsSync(target)).to.equal(false);
+      });
+    });
+    it('deletes a file with invalid JSON lines', () => {
+      const date = new Date();
+      const log = '{{}\n';
+      const target = path.join(basePath, 'log.log');
+      fs.writeFileSync(target, log);
+
+      return eliminateOutOfDateFiles(basePath, date).then(() => {
+        expect(fs.existsSync(target)).to.equal(false);
+      });
+    });
+    it('deletes a file with all dates before provided date', () => {
+      const date = new Date('2018-01-04T19:17:05.014Z');
+      const contents = [
+        JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
+      ].join('\n');
+      const target = path.join(basePath, 'log.log');
+      fs.writeFileSync(target, contents);
+
+      return eliminateOutOfDateFiles(basePath, date).then(() => {
+        expect(fs.existsSync(target)).to.equal(false);
+      });
+    });
+    it('keeps a file with first line date before provided date', () => {
+      const date = new Date('2018-01-04T19:16:00.000Z');
+      const contents = [
+        JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
+      ].join('\n');
+      const target = path.join(basePath, 'log.log');
+      fs.writeFileSync(target, contents);
+
+      return eliminateOutOfDateFiles(basePath, date).then(() => {
+        expect(fs.existsSync(target)).to.equal(true);
+      });
+    });
+    it('keeps a file with last line date before provided date', () => {
+      const date = new Date('2018-01-04T19:17:01.000Z');
+      const contents = [
+        JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
+      ].join('\n');
+      const target = path.join(basePath, 'log.log');
+      fs.writeFileSync(target, contents);
+
+      return eliminateOutOfDateFiles(basePath, date).then(() => {
+        expect(fs.existsSync(target)).to.equal(true);
+      });
+    });
+  });
+
+  describe('#eliminateOldEntries', () => {
+    it('eliminates all non-parsing entries', () => {
+      const date = new Date('2018-01-04T19:17:01.000Z');
+      const contents = [
+        'random line',
+        JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
+      ].join('\n');
+      const expected = [
+        JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
+      ].join('\n');
+
+      const target = path.join(basePath, 'log.log');
+      const files = [{
+        path: target,
+      }];
+
+      fs.writeFileSync(target, contents);
+
+      return eliminateOldEntries(files, date).then(() => {
+        expect(fs.readFileSync(target, 'utf8')).to.equal(`${expected}\n`);
+      });
+    });
+    it('preserves all lines if before target date', () => {
+      const date = new Date('2018-01-04T19:17:03.000Z');
+      const contents = [
+        'random line',
+        JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
+        JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
+      ].join('\n');
+      const expected = [
+        JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
+      ].join('\n');
+
+      const target = path.join(basePath, 'log.log');
+      const files = [{
+        path: target,
+      }];
+
+      fs.writeFileSync(target, contents);
+
+      return eliminateOldEntries(files, date).then(() => {
+        expect(fs.readFileSync(target, 'utf8')).to.equal(`${expected}\n`);
+      });
+    });
+  });
+
+  describe('#fetchLog', () => {
+    it('returns error if file does not exist', () => {
+      const target = 'random_file';
+      return fetchLog(target).then(() => {
+        throw new Error('Expected an error!');
+      }, (error) => {
+        expect(error).to.have.property('message').that.match(/random_file/);
+      });
+    });
+    it('returns empty array if file has no valid JSON lines', () => {
+      const contents = 'line 1\nline2\n';
+      const expected = [];
+      const target = path.join(basePath, 'test.log');
+
+      fs.writeFileSync(target, contents);
+
+      return fetchLog(target).then((result) => {
+        expect(result).to.deep.equal(expected);
+      });
+    });
+    it('returns just three fields in each returned line', () => {
+      const contents = [
+        JSON.stringify({
+          one: 1,
+          two: 2,
+          level: 1,
+          time: 2,
+          msg: 3,
+        }),
+        JSON.stringify({
+          one: 1,
+          two: 2,
+          level: 2,
+          time: 3,
+          msg: 4,
+        }),
+        '',
+      ].join('\n');
+      const expected = [{
+        level: 1,
+        time: 2,
+        msg: 3,
+      }, {
+        level: 2,
+        time: 3,
+        msg: 4,
+      }];
+
+      const target = path.join(basePath, 'test.log');
+
+      fs.writeFileSync(target, contents);
+
+      return fetchLog(target).then((result) => {
+        expect(result).to.deep.equal(expected);
+      });
+    });
+  });
+
+  describe('#fetch', () => {
+    it('returns single entry if no files', () => {
+      return fetch(basePath).then((results) => {
+        expect(results).to.have.length(1);
+        expect(results[0].msg).to.match(/Loaded this list/);
+      });
+    });
+    it('returns sorted entries from all files', () => {
+      const first = [
+        JSON.stringify({ msg: 2, time: '2018-01-04T19:17:05.014Z' }),
+        '',
+      ].join('\n');
+      const second = [
+        JSON.stringify({ msg: 1, time: '2018-01-04T19:17:00.014Z' }),
+        JSON.stringify({ msg: 3, time: '2018-01-04T19:18:00.014Z' }),
+        '',
+      ].join('\n');
+
+      fs.writeFileSync(path.join(basePath, 'first.log'), first);
+      fs.writeFileSync(path.join(basePath, 'second.log'), second);
+
+      return fetch(basePath).then((results) => {
+        expect(results).to.have.length(4);
+        expect(results[0].msg).to.equal(1);
+        expect(results[1].msg).to.equal(2);
+        expect(results[2].msg).to.equal(3);
+      });
+    });
+  });
+});
diff --git a/yarn.lock b/yarn.lock
index ed01f897e..4afc5b8c5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -34,6 +34,20 @@ abbrev@1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
 
+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@^5.2.1:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822"
+
 agent-base@2, agent-base@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.1.1.tgz#d6de10d5af6132d5bd692427d46fc538539094c7"
@@ -47,7 +61,7 @@ agent-base@^4.1.0:
   dependencies:
     es6-promisify "^5.0.0"
 
-ajv-keywords@^2.1.1:
+ajv-keywords@^2.1.0, ajv-keywords@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
 
@@ -58,7 +72,7 @@ ajv@^4.9.1:
     co "^4.6.0"
     json-stable-stringify "^1.0.1"
 
-ajv@^5.1.0, ajv@^5.5.1:
+ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0, ajv@^5.5.1:
   version "5.5.2"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
   dependencies:
@@ -67,6 +81,14 @@ ajv@^5.1.0, ajv@^5.5.1:
     fast-json-stable-stringify "^2.0.0"
     json-schema-traverse "^0.3.0"
 
+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"
@@ -103,7 +125,7 @@ ansi-styles@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
 
-any-promise@^1.3.0:
+any-promise@^1.0.0, any-promise@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
 
@@ -118,6 +140,12 @@ app-package-builder@2.0.1:
     int64-buffer "^0.1.10"
     rabin-bindings "~1.7.4"
 
+append-transform@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+  dependencies:
+    default-require-extensions "^1.0.0"
+
 aproba@^1.0.3:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.1.tgz#95d3600f07710aa0e9298c726ad5ecf2eacbabab"
@@ -146,6 +174,10 @@ archiver@~2.1.0:
     tar-stream "^1.5.0"
     zip-stream "^1.2.0"
 
+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.4"
   resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
@@ -169,6 +201,16 @@ args@^2.3.0:
     pkginfo "0.4.0"
     string-similarity "1.1.0"
 
+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.1.0"
+  resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
 array-find-index@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
@@ -183,7 +225,11 @@ array-uniq@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
 
-arrify@^1.0.0:
+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, arrify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
 
@@ -219,6 +265,10 @@ assert-plus@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
 
+assertion-error@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
+
 ast-types@0.x.x:
   version "0.10.1"
   resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd"
@@ -235,7 +285,7 @@ async@0.2.x, async@~0.2.9:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
 
-async@^1.5.0, async@~1.5.2:
+async@^1.4.0, async@^1.5.0, async@~1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
@@ -284,7 +334,34 @@ aws4@^1.2.1, aws4@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
 
-babel-runtime@^6.26.0:
+babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+  dependencies:
+    chalk "^1.1.3"
+    esutils "^2.0.2"
+    js-tokens "^3.0.2"
+
+babel-generator@^6.18.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
+  dependencies:
+    babel-messages "^6.23.0"
+    babel-runtime "^6.26.0"
+    babel-types "^6.26.0"
+    detect-indent "^4.0.0"
+    jsesc "^1.3.0"
+    lodash "^4.17.4"
+    source-map "^0.5.6"
+    trim-right "^1.0.1"
+
+babel-messages@^6.23.0:
+  version "6.23.0"
+  resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+  dependencies:
+    babel-runtime "^6.22.0"
+
+babel-runtime@^6.22.0, babel-runtime@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   dependencies:
@@ -298,6 +375,43 @@ babel-runtime@^6.9.2:
     core-js "^2.4.0"
     regenerator-runtime "^0.10.0"
 
+babel-template@^6.16.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+  dependencies:
+    babel-runtime "^6.26.0"
+    babel-traverse "^6.26.0"
+    babel-types "^6.26.0"
+    babylon "^6.18.0"
+    lodash "^4.17.4"
+
+babel-traverse@^6.18.0, babel-traverse@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+  dependencies:
+    babel-code-frame "^6.26.0"
+    babel-messages "^6.23.0"
+    babel-runtime "^6.26.0"
+    babel-types "^6.26.0"
+    babylon "^6.18.0"
+    debug "^2.6.8"
+    globals "^9.18.0"
+    invariant "^2.2.2"
+    lodash "^4.17.4"
+
+babel-types@^6.18.0, babel-types@^6.26.0:
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+  dependencies:
+    babel-runtime "^6.26.0"
+    esutils "^2.0.2"
+    lodash "^4.17.4"
+    to-fast-properties "^1.0.3"
+
+babylon@^6.18.0:
+  version "6.18.0"
+  resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+
 babylon@^6.8.1:
   version "6.17.0"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.0.tgz#37da948878488b9c4e3c4038893fa3314b3fc932"
@@ -417,6 +531,18 @@ brace-expansion@^1.0.0, brace-expansion@^1.1.7:
     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"
+
 buffer-crc32@^0.2.1:
   version "0.2.13"
   resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@@ -467,7 +593,7 @@ builder-util@3.4.4, builder-util@^3.4.4:
     temp-file "^3.0.0"
     tunnel-agent "^0.6.0"
 
-builtin-modules@^1.0.0:
+builtin-modules@^1.0.0, builtin-modules@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
 
@@ -492,6 +618,24 @@ bytes@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
 
+caching-transform@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-1.0.1.tgz#6dbdb2f20f8d8fbce79f3e94e9d1742dcdf5c0a1"
+  dependencies:
+    md5-hex "^1.2.0"
+    mkdirp "^0.5.1"
+    write-file-atomic "^1.1.4"
+
+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-keys@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
@@ -503,6 +647,10 @@ camelcase@4.1.0, camelcase@^4.0.0, camelcase@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
 
+camelcase@^1.0.2:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+
 camelcase@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@@ -519,13 +667,31 @@ caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
 
+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@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c"
+  dependencies:
+    assertion-error "^1.0.1"
+    check-error "^1.0.1"
+    deep-eql "^3.0.0"
+    get-func-name "^2.0.0"
+    pathval "^1.0.0"
+    type-detect "^4.0.0"
+
 chainsaw@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98"
   dependencies:
     traverse ">=0.3.0 <0.4"
 
-chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@~1.1.0, chalk@~1.1.1:
+chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   dependencies:
@@ -535,7 +701,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@~1.1.0, chalk@~1.1.1:
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
   dependencies:
@@ -555,6 +721,10 @@ chardet@^0.4.0:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
 
+check-error@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
+
 chownr@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
@@ -567,6 +737,10 @@ ci-info@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.0.0.tgz#dc5285f2b4e251821683681c381c3388f46ec534"
 
+circular-json@^0.3.1:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+
 cli-boxes@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
@@ -594,6 +768,14 @@ cli@~1.0.0:
     exit "0.1.2"
     glob "^7.1.1"
 
+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.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
@@ -646,6 +828,10 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
+commander@2.11.0:
+  version "2.11.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
+
 commander@^2.9.0, commander@~2.9.0:
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
@@ -658,6 +844,10 @@ comment-parser@^0.3.1:
   dependencies:
     readable-stream "^2.0.4"
 
+commondir@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+
 compare-version@^0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080"
@@ -683,7 +873,7 @@ concat-stream@1.5.0:
     readable-stream "~2.0.0"
     typedarray "~0.0.5"
 
-concat-stream@1.6.0:
+concat-stream@1.6.0, concat-stream@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
   dependencies:
@@ -728,10 +918,18 @@ 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"
 
+contains-path@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+
 content-type@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
 
+convert-source-map@^1.3.0:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
+
 core-js@^2.4.0:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
@@ -764,7 +962,14 @@ cross-spawn@^3.0.0:
     lru-cache "^4.0.1"
     which "^1.2.9"
 
-cross-spawn@^5.0.1:
+cross-spawn@^4:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41"
+  dependencies:
+    lru-cache "^4.0.1"
+    which "^1.2.9"
+
+cross-spawn@^5.0.1, cross-spawn@^5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
   dependencies:
@@ -854,6 +1059,10 @@ dateformat@~1.0.12:
     get-stdin "^4.0.1"
     meow "^3.3.0"
 
+debug-log@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f"
+
 debug@0.7.4:
   version "0.7.4"
   resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
@@ -864,6 +1073,12 @@ debug@2, debug@2.6.9:
   dependencies:
     ms "2.0.0"
 
+debug@3.1.0, debug@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  dependencies:
+    ms "2.0.0"
+
 debug@^2.1.3, debug@^2.2.0, debug@^2.6.8:
   version "2.6.8"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
@@ -876,19 +1091,13 @@ debug@^3.0.0:
   dependencies:
     ms "2.0.0"
 
-debug@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
-  dependencies:
-    ms "2.0.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"
 
-decamelize@^1.1.1, decamelize@^1.1.2:
+decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
 
@@ -904,6 +1113,12 @@ decompress-zip@0.3.0:
     readable-stream "^1.1.8"
     touch "0.0.3"
 
+deep-eql@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
+  dependencies:
+    type-detect "^4.0.0"
+
 deep-equal@*:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -920,6 +1135,12 @@ deepmerge@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.0.1.tgz#25c1c24f110fb914f80001b925264dd77f3f4312"
 
+default-require-extensions@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+  dependencies:
+    strip-bom "^2.0.0"
+
 degenerator@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095"
@@ -928,7 +1149,7 @@ degenerator@^1.0.4:
     escodegen "1.x.x"
     esprima "3.x.x"
 
-del@^2.2.2:
+del@^2.0.2, del@^2.2.2:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
   dependencies:
@@ -956,10 +1177,20 @@ depd@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
 
+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"
+
 dev-null@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/dev-null/-/dev-null-0.1.1.tgz#5a205ce3c2b2ef77b6238d6ba179eb74c6a0e818"
 
+diff@3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
+
 dmg-builder@2.1.9:
   version "2.1.9"
   resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-2.1.9.tgz#3c501b034436134bef464082212e380124a5df79"
@@ -972,6 +1203,19 @@ dmg-builder@2.1.9:
     js-yaml "^3.10.0"
     parse-color "^1.0.0"
 
+doctrine@1.5.0:
+  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"
+
+doctrine@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.2.tgz#68f96ce8efc56cc42651f1faadb4f175273b0075"
+  dependencies:
+    esutils "^2.0.2"
+
 dom-serializer@0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@@ -1298,7 +1542,7 @@ es6-promisify@^5.0.0:
   dependencies:
     es6-promise "^4.0.3"
 
-escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, 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"
 
@@ -1313,6 +1557,105 @@ escodegen@1.x.x:
   optionalDependencies:
     source-map "~0.5.6"
 
+eslint-config-airbnb-base@^12.1.0:
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz#386441e54a12ccd957b0a92564a4bafebd747944"
+  dependencies:
+    eslint-restricted-globals "^0.1.1"
+
+eslint-import-resolver-node@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc"
+  dependencies:
+    debug "^2.6.8"
+    resolve "^1.2.0"
+
+eslint-module-utils@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449"
+  dependencies:
+    debug "^2.6.8"
+    pkg-dir "^1.0.0"
+
+eslint-plugin-import@^2.8.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz#fa1b6ef31fcb3c501c09859c1b86f1fc5b986894"
+  dependencies:
+    builtin-modules "^1.1.1"
+    contains-path "^0.1.0"
+    debug "^2.6.8"
+    doctrine "1.5.0"
+    eslint-import-resolver-node "^0.3.1"
+    eslint-module-utils "^2.1.1"
+    has "^1.0.1"
+    lodash.cond "^4.3.0"
+    minimatch "^3.0.3"
+    read-pkg-up "^2.0.0"
+
+eslint-restricted-globals@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
+
+eslint-scope@^3.7.1:
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+  dependencies:
+    esrecurse "^4.1.0"
+    estraverse "^4.1.1"
+
+eslint-visitor-keys@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
+
+eslint@^4.14.0:
+  version "4.14.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.14.0.tgz#96609768d1dd23304faba2d94b7fefe5a5447a82"
+  dependencies:
+    ajv "^5.3.0"
+    babel-code-frame "^6.22.0"
+    chalk "^2.1.0"
+    concat-stream "^1.6.0"
+    cross-spawn "^5.1.0"
+    debug "^3.1.0"
+    doctrine "^2.0.2"
+    eslint-scope "^3.7.1"
+    eslint-visitor-keys "^1.0.0"
+    espree "^3.5.2"
+    esquery "^1.0.0"
+    esutils "^2.0.2"
+    file-entry-cache "^2.0.0"
+    functional-red-black-tree "^1.0.1"
+    glob "^7.1.2"
+    globals "^11.0.1"
+    ignore "^3.3.3"
+    imurmurhash "^0.1.4"
+    inquirer "^3.0.6"
+    is-resolvable "^1.0.0"
+    js-yaml "^3.9.1"
+    json-stable-stringify-without-jsonify "^1.0.1"
+    levn "^0.3.0"
+    lodash "^4.17.4"
+    minimatch "^3.0.2"
+    mkdirp "^0.5.1"
+    natural-compare "^1.4.0"
+    optionator "^0.8.2"
+    path-is-inside "^1.0.2"
+    pluralize "^7.0.0"
+    progress "^2.0.0"
+    require-uncached "^1.0.3"
+    semver "^5.3.0"
+    strip-ansi "^4.0.0"
+    strip-json-comments "~2.0.1"
+    table "^4.0.1"
+    text-table "~0.2.0"
+
+espree@^3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
+  dependencies:
+    acorn "^5.2.1"
+    acorn-jsx "^3.0.0"
+
 esprima@3.x.x, esprima@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
@@ -1325,7 +1668,20 @@ esprima@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
 
-estraverse@^4.1.0, estraverse@^4.2.0:
+esquery@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+  dependencies:
+    estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+  dependencies:
+    estraverse "^4.1.0"
+    object-assign "^4.0.1"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
 
@@ -1361,6 +1717,18 @@ exit@0.1.2, exit@0.1.x, exit@~0.1.1, exit@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
 
+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-template@^1.0.2:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-1.1.0.tgz#e09efba977bf98f9ee0ed25abd0c692e02aec3fc"
@@ -1381,6 +1749,12 @@ external-editor@^2.0.4:
     iconv-lite "^0.4.17"
     tmp "^0.0.33"
 
+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"
+
 extract-zip@^1.0.3:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.0.tgz#7f400c9607ea866ecab7aa6d54fb978eeb11621a"
@@ -1437,6 +1811,13 @@ figures@^2.0.0:
   dependencies:
     escape-string-regexp "^1.0.5"
 
+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"
+
 file-sync-cmp@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz#a5e7a8ffbfa493b43b923bbd4ca89a53b63b612b"
@@ -1455,6 +1836,28 @@ file-url@^1.1.0:
   dependencies:
     meow "^3.7.0"
 
+filename-regex@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+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-cache-dir@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9"
+  dependencies:
+    commondir "^1.0.1"
+    mkdirp "^0.5.1"
+    pkg-dir "^1.0.0"
+
 find-up@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -1462,7 +1865,7 @@ find-up@^1.0.0:
     path-exists "^2.0.0"
     pinkie-promise "^2.0.0"
 
-find-up@^2.1.0:
+find-up@^2.0.0, find-up@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
   dependencies:
@@ -1474,12 +1877,42 @@ findup-sync@~0.3.0:
   dependencies:
     glob "~5.0.0"
 
+firstline@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/firstline/-/firstline-1.2.1.tgz#b88673c42009f8821fac2926e99720acee924fae"
+
+flat-cache@^1.2.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481"
+  dependencies:
+    circular-json "^0.3.1"
+    del "^2.0.2"
+    graceful-fs "^4.1.2"
+    write "^0.2.1"
+
 for-each@^0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4"
   dependencies:
     is-function "~1.0.0"
 
+for-in@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+  dependencies:
+    for-in "^1.0.1"
+
+foreground-child@^1.5.3, foreground-child@^1.5.6:
+  version "1.5.6"
+  resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9"
+  dependencies:
+    cross-spawn "^4"
+    signal-exit "^3.0.0"
+
 forever-agent@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@@ -1514,7 +1947,7 @@ fs-extra-p@^4.4.5, fs-extra-p@^4.5.0:
     bluebird-lst "^1.0.5"
     fs-extra "^5.0.0"
 
-fs-extra@0.26.7:
+fs-extra@0.26.7, fs-extra@^0.26.5:
   version "0.26.7"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9"
   dependencies:
@@ -1565,6 +1998,15 @@ fs-extra@^5.0.0:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
+fs-promise@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/fs-promise/-/fs-promise-0.5.0.tgz#4347d6bf624655a7061a4319213c393276ad3ef3"
+  dependencies:
+    any-promise "^1.0.0"
+    fs-extra "^0.26.5"
+    mz "^2.3.1"
+    thenify-all "^1.6.0"
+
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1585,6 +2027,14 @@ ftp@~0.3.10:
     readable-stream "1.1.x"
     xregexp "2.0.0"
 
+function-bind@^1.0.2:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+functional-red-black-tree@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+
 gauge@~2.7.1, gauge@~2.7.3:
   version "2.7.4"
   resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -1608,6 +2058,10 @@ 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"
 
+get-func-name@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
+
 get-stdin@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -1641,6 +2095,30 @@ github-from-package@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
 
+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@7.1.2, glob@^7.0.6, glob@^7.1.2:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 glob@^5.0.1, glob@~5.0.0:
   version "5.0.15"
   resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@@ -1696,6 +2174,14 @@ global@~4.3.0:
     min-document "^2.19.0"
     process "~0.5.1"
 
+globals@^11.0.1:
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-11.1.0.tgz#632644457f5f0e3ae711807183700ebf2e4633e4"
+
+globals@^9.18.0:
+  version "9.18.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
 globby@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
@@ -1743,6 +2229,10 @@ graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3,
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
 
+growl@1.10.3:
+  version "1.10.3"
+  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f"
+
 growly@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -1870,6 +2360,16 @@ grunt@^1.0.1:
     path-is-absolute "~1.0.0"
     rimraf "~2.2.8"
 
+handlebars@^4.0.3:
+  version "4.0.11"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
+  dependencies:
+    async "^1.4.0"
+    optimist "^0.6.1"
+    source-map "^0.4.4"
+  optionalDependencies:
+    uglify-js "^2.6"
+
 har-schema@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
@@ -1910,6 +2410,12 @@ has-unicode@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
 
+has@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+  dependencies:
+    function-bind "^1.0.2"
+
 hasha@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1"
@@ -1935,6 +2441,10 @@ hawk@~6.0.2:
     hoek "4.x.x"
     sntp "2.x.x"
 
+he@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
+
 hoek@2.x.x:
   version "2.16.3"
   resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
@@ -2039,6 +2549,10 @@ ieee754@^1.1.4:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
 
+ignore@^3.3.3:
+  version "3.3.7"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
+
 import-lazy@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
@@ -2080,7 +2594,7 @@ ini@^1.3.5:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
 
-inquirer@~3.3.0:
+inquirer@^3.0.6, inquirer@~3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
   dependencies:
@@ -2103,6 +2617,12 @@ int64-buffer@^0.1.10:
   version "0.1.10"
   resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423"
 
+invariant@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+  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"
@@ -2119,6 +2639,10 @@ is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
 
+is-buffer@^1.1.5:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+
 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"
@@ -2131,6 +2655,24 @@ is-ci@^1.0.10:
   dependencies:
     ci-info "^1.0.0"
 
+is-dotfile@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+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"
@@ -2151,6 +2693,12 @@ is-function@^1.0.1, is-function@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5"
 
+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-installed-globally@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
@@ -2162,6 +2710,18 @@ 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.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-number@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+  dependencies:
+    kind-of "^3.0.2"
+
 is-obj@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@@ -2182,6 +2742,14 @@ is-path-inside@^1.0.0:
   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-promise@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
@@ -2190,6 +2758,10 @@ 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.1"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.1.tgz#acca1cd36dbe44b974b924321555a70ba03b1cf4"
+
 is-retry-allowed@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
@@ -2210,7 +2782,7 @@ isarray@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
 
-isarray@^1.0.0, isarray@~1.0.0:
+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"
 
@@ -2222,10 +2794,63 @@ isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
 
+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.x, isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
 
+istanbul-lib-coverage@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
+
+istanbul-lib-hook@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz#8538d970372cb3716d53e55523dd54b557a8d89b"
+  dependencies:
+    append-transform "^0.4.0"
+
+istanbul-lib-instrument@^1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e"
+  dependencies:
+    babel-generator "^6.18.0"
+    babel-template "^6.16.0"
+    babel-traverse "^6.18.0"
+    babel-types "^6.18.0"
+    babylon "^6.18.0"
+    istanbul-lib-coverage "^1.1.1"
+    semver "^5.3.0"
+
+istanbul-lib-report@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz#922be27c13b9511b979bd1587359f69798c1d425"
+  dependencies:
+    istanbul-lib-coverage "^1.1.1"
+    mkdirp "^0.5.1"
+    path-parse "^1.0.5"
+    supports-color "^3.1.2"
+
+istanbul-lib-source-maps@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz#750578602435f28a0c04ee6d7d9e0f2960e62c1c"
+  dependencies:
+    debug "^3.1.0"
+    istanbul-lib-coverage "^1.1.1"
+    mkdirp "^0.5.1"
+    rimraf "^2.6.1"
+    source-map "^0.5.3"
+
+istanbul-reports@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.3.tgz#3b9e1e8defb6d18b1d425da8e8b32c5a163f2d10"
+  dependencies:
+    handlebars "^4.0.3"
+
 jimp@^0.2.27:
   version "0.2.27"
   resolved "https://registry.yarnpkg.com/jimp/-/jimp-0.2.27.tgz#41ef5082d8b63201d54747e04fe8bcacbaf25474"
@@ -2264,7 +2889,11 @@ js-base64@^2.1.8:
   version "2.1.9"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce"
 
-js-yaml@^3.10.0, js-yaml@^3.2.7:
+js-tokens@^3.0.0, js-tokens@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@^3.10.0, js-yaml@^3.2.7, js-yaml@^3.9.1:
   version "3.10.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
   dependencies:
@@ -2338,6 +2967,10 @@ jsdoctypeparser@~1.2.0:
   dependencies:
     lodash "^3.7.0"
 
+jsesc@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
 jshint@~2.9.4:
   version "2.9.4"
   resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.9.4.tgz#5e3ba97848d5290273db514aee47fe24cf592934"
@@ -2359,6 +2992,10 @@ 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-without-jsonify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+
 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"
@@ -2413,6 +3050,18 @@ kew@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b"
 
+kind-of@^3.0.2:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+  dependencies:
+    is-buffer "^1.1.5"
+
 klaw@^1.0.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
@@ -2425,6 +3074,10 @@ latest-version@^3.0.0:
   dependencies:
     package-json "^4.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"
+
 lazy-val@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.2.tgz#d9b07fb1fce54cbc99b3c611de431b83249369b6"
@@ -2445,7 +3098,7 @@ lcid@^1.0.0:
   dependencies:
     invert-kv "^1.0.0"
 
-levn@~0.3.0:
+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:
@@ -2478,6 +3131,15 @@ load-json-file@^1.0.0:
     pinkie-promise "^2.0.0"
     strip-bom "^2.0.0"
 
+load-json-file@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^2.2.0"
+    pify "^2.0.0"
+    strip-bom "^3.0.0"
+
 locate-path@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -2493,6 +3155,10 @@ lodash.clonedeep@^4.3.0, lodash.clonedeep@^4.3.2:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
 
+lodash.cond@^4.3.0:
+  version "4.5.2"
+  resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
+
 lodash.defaults@^4.0.1:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
@@ -2549,6 +3215,16 @@ lodash@~4.6.1:
   version "4.6.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.6.1.tgz#df00c1164ad236b183cfc3887a5e8d38cc63cbbc"
 
+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.1"
+  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+  dependencies:
+    js-tokens "^3.0.0"
+
 loud-rejection@^1.0.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
@@ -2581,6 +3257,16 @@ map-obj@^1.0.0, map-obj@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
 
+md5-hex@^1.2.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-1.3.0.tgz#d2c4afe983c4370662179b8cad145219135046c4"
+  dependencies:
+    md5-o-matic "^0.1.1"
+
+md5-o-matic@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/md5-o-matic/-/md5-o-matic-0.1.1.tgz#822bccd65e117c514fab176b25945d54100a03c3"
+
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -2606,6 +3292,30 @@ meow@^3.1.0, meow@^3.3.0, meow@^3.7.0:
     redent "^1.0.0"
     trim-newlines "^1.0.0"
 
+merge-source-map@^1.0.2:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"
+  dependencies:
+    source-map "^0.6.1"
+
+micromatch@^2.3.11:
+  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.27.0:
   version "1.27.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
@@ -2670,7 +3380,7 @@ mkdirp@0.5.0:
   dependencies:
     minimist "0.0.8"
 
-mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
+mkdirp@0.5.1, mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
@@ -2688,6 +3398,21 @@ mksnapshot@^0.3.0:
     fs-extra "0.26.7"
     request "^2.79.0"
 
+mocha@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794"
+  dependencies:
+    browser-stdout "1.3.0"
+    commander "2.11.0"
+    debug "3.1.0"
+    diff "3.3.1"
+    escape-string-regexp "1.0.5"
+    glob "7.1.2"
+    growl "1.10.3"
+    he "1.1.1"
+    mkdirp "0.5.1"
+    supports-color "4.4.0"
+
 moment@^2.10.6:
   version "2.18.1"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
@@ -2712,6 +3437,14 @@ mv@~2:
     ncp "~2.0.0"
     rimraf "~2.4.0"
 
+mz@^2.3.1:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
+  dependencies:
+    any-promise "^1.0.0"
+    object-assign "^4.0.1"
+    thenify-all "^1.0.0"
+
 nan@^2.0.0, nan@^2.3.2, nan@^2.3.3:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
@@ -2720,6 +3453,10 @@ nan@^2.8.0:
   version "2.8.0"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
 
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
 natural-compare@~1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.2.2.tgz#1f96d60e3141cac1b6d05653ce0daeac763af6aa"
@@ -2843,7 +3580,7 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
-normalize-path@^2.0.0:
+normalize-path@^2.0.0, normalize-path@^2.0.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
   dependencies:
@@ -2893,6 +3630,38 @@ 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"
 
+nyc@^11.4.1:
+  version "11.4.1"
+  resolved "https://registry.yarnpkg.com/nyc/-/nyc-11.4.1.tgz#13fdf7e7ef22d027c61d174758f6978a68f4f5e5"
+  dependencies:
+    archy "^1.0.0"
+    arrify "^1.0.1"
+    caching-transform "^1.0.0"
+    convert-source-map "^1.3.0"
+    debug-log "^1.0.1"
+    default-require-extensions "^1.0.0"
+    find-cache-dir "^0.1.1"
+    find-up "^2.1.0"
+    foreground-child "^1.5.3"
+    glob "^7.0.6"
+    istanbul-lib-coverage "^1.1.1"
+    istanbul-lib-hook "^1.1.0"
+    istanbul-lib-instrument "^1.9.1"
+    istanbul-lib-report "^1.1.2"
+    istanbul-lib-source-maps "^1.2.2"
+    istanbul-reports "^1.1.3"
+    md5-hex "^1.2.0"
+    merge-source-map "^1.0.2"
+    micromatch "^2.3.11"
+    mkdirp "^0.5.0"
+    resolve-from "^2.0.0"
+    rimraf "^2.5.4"
+    signal-exit "^3.0.1"
+    spawn-wrap "^1.4.2"
+    test-exclude "^4.1.1"
+    yargs "^10.0.3"
+    yargs-parser "^8.0.0"
+
 oauth-sign@~0.8.1, oauth-sign@~0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
@@ -2905,6 +3674,13 @@ object-keys@~0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
 
+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"
+
 on-finished@~2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@@ -2927,14 +3703,14 @@ onetime@^2.0.0:
   dependencies:
     mimic-fn "^1.0.0"
 
-optimist@~0.6.1:
+optimist@^0.6.1, 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.1, optionator@^0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
   dependencies:
@@ -3041,6 +3817,15 @@ parse-color@^1.0.0:
   dependencies:
     color-convert "~0.5.0"
 
+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-headers@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.1.tgz#6ae83a7aa25a9d9b700acc28698cd1f1ed7e9536"
@@ -3072,7 +3857,7 @@ path-is-absolute@^1.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.1, 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"
 
@@ -3080,6 +3865,10 @@ path-key@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
 
+path-parse@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
 path-type@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -3088,6 +3877,16 @@ path-type@^1.0.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
+path-type@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+  dependencies:
+    pify "^2.0.0"
+
+pathval@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
+
 pathval@~0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/pathval/-/pathval-0.1.1.tgz#08f911cdca9cce5942880da7817bc0b723b66d82"
@@ -3138,6 +3937,12 @@ pixelmatch@^4.0.0:
   dependencies:
     pngjs "^3.0.0"
 
+pkg-dir@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+  dependencies:
+    find-up "^1.0.0"
+
 pkg-up@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
@@ -3160,6 +3965,10 @@ plist@^2.1.0:
     xmlbuilder "8.2.2"
     xmldom "0.1.x"
 
+pluralize@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
+
 pn@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/pn/-/pn-1.0.0.tgz#1cf5a30b0d806cd18f88fc41a6b5d4ad615b3ba9"
@@ -3195,6 +4004,10 @@ prepend-http@^1.0.1:
   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"
+
 pretty-bytes@^1.0.2:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84"
@@ -3221,6 +4034,10 @@ progress@^1.1.8:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
 
+progress@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
+
 prompt@~0.2.14:
   version "0.2.14"
   resolved "https://registry.yarnpkg.com/prompt/-/prompt-0.2.14.tgz#57754f64f543fd7b0845707c818ece618f05ffdc"
@@ -3295,6 +4112,13 @@ rabin-bindings@~1.7.4:
     nan "^2.8.0"
     prebuild-install "^2.3.0"
 
+randomatic@^1.1.3:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
+  dependencies:
+    is-number "^3.0.0"
+    kind-of "^4.0.0"
+
 raw-body@^2.2.0:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
@@ -3339,6 +4163,12 @@ read-config-file@1.2.1:
     json5 "^0.5.1"
     lazy-val "^1.0.2"
 
+read-last-lines@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/read-last-lines/-/read-last-lines-1.3.0.tgz#0dd170188d46124a23eb1a87156baf46b315ac4b"
+  dependencies:
+    fs-promise "^0.5.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"
@@ -3346,6 +4176,13 @@ read-pkg-up@^1.0.1:
     find-up "^1.0.0"
     read-pkg "^1.0.0"
 
+read-pkg-up@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+  dependencies:
+    find-up "^2.0.0"
+    read-pkg "^2.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"
@@ -3354,6 +4191,14 @@ read-pkg@^1.0.0:
     normalize-package-data "^2.3.2"
     path-type "^1.0.0"
 
+read-pkg@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+  dependencies:
+    load-json-file "^2.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^2.0.0"
+
 read@1.0.x:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
@@ -3416,6 +4261,12 @@ regenerator-runtime@^0.11.0:
   version "0.11.1"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
 
+regex-cache@^0.4.2:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+  dependencies:
+    is-equal-shallow "^0.1.3"
+
 registry-auth-token@^3.0.1:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.2.0.tgz#5bf3bd4608a2dd9242542c44d66ad8a5f9cdd3b0"
@@ -3432,6 +4283,14 @@ remove-trailing-separator@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz#615ebb96af559552d4bf4057c8436d486ab63cc4"
 
+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@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
@@ -3506,10 +4365,25 @@ 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.3:
+  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"
+
 reserved-words@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.1.tgz#6f7c15e5e5614c50da961630da46addc87c0cef2"
 
+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-url@~0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -3518,6 +4392,12 @@ resolve@^1.1.6, resolve@~1.1.0:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
 
+resolve@^1.2.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
+  dependencies:
+    path-parse "^1.0.5"
+
 restore-cursor@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
@@ -3533,11 +4413,17 @@ rgb2hex@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.1.0.tgz#ccd55f860ae0c5c4ea37504b958e442d8d12325b"
 
+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, rimraf@2.x.x, rimraf@^2.2.8, rimraf@~2.2.8:
   version "2.2.8"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
 
-rimraf@^2.6.2:
+rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -3655,7 +4541,7 @@ shellwords@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
 
-signal-exit@^3.0.0, signal-exit@^3.0.2:
+signal-exit@^3.0.0, signal-exit@^3.0.1, signal-exit@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
 
@@ -3673,6 +4559,12 @@ single-line-log@^1.1.2:
   dependencies:
     string-width "^1.0.1"
 
+slice-ansi@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
+  dependencies:
+    is-fullwidth-code-point "^2.0.0"
+
 slide@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
@@ -3746,7 +4638,7 @@ source-map@^0.1.38:
   dependencies:
     amdefine ">=0.0.4"
 
-source-map@^0.4.2:
+source-map@^0.4.2, source-map@^0.4.4:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
   dependencies:
@@ -3756,14 +4648,25 @@ source-map@^0.5.3, source-map@^0.5.6:
   version "0.5.6"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
 
-source-map@^0.6.0:
+source-map@^0.6.0, source-map@^0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
 
-source-map@~0.5.6:
+source-map@~0.5.1, source-map@~0.5.6:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
 
+spawn-wrap@^1.4.2:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c"
+  dependencies:
+    foreground-child "^1.5.6"
+    mkdirp "^0.5.0"
+    os-homedir "^1.0.1"
+    rimraf "^2.6.2"
+    signal-exit "^3.0.2"
+    which "^1.3.0"
+
 spdx-correct@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@@ -3877,7 +4780,7 @@ string-width@^2.0.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^3.0.0"
 
-string-width@^2.1.0:
+string-width@^2.1.0, string-width@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
   dependencies:
@@ -3920,6 +4823,10 @@ strip-bom@^2.0.0:
   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-eof@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@@ -3951,15 +4858,21 @@ sumchecker@^2.0.1, sumchecker@^2.0.2:
   dependencies:
     debug "^2.2.0"
 
+supports-color@4.4.0, supports-color@^4.0.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
+  dependencies:
+    has-flag "^2.0.0"
+
 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@^4.0.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
+supports-color@^3.1.2:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
   dependencies:
-    has-flag "^2.0.0"
+    has-flag "^1.0.0"
 
 supports-color@~5.0.0:
   version "5.0.1"
@@ -3976,6 +4889,17 @@ svg2png@4.1.0:
     pn "^1.0.0"
     yargs "^5.0.0"
 
+table@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
+  dependencies:
+    ajv "^5.2.3"
+    ajv-keywords "^2.1.0"
+    chalk "^2.1.0"
+    lodash "^4.17.4"
+    slice-ansi "1.0.0"
+    string-width "^2.1.1"
+
 tar-fs@^1.13.0:
   version "1.16.0"
   resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.0.tgz#e877a25acbcc51d8c790da1c57c9cf439817b896"
@@ -4026,6 +4950,32 @@ term-size@^1.2.0:
   dependencies:
     execa "^0.7.0"
 
+test-exclude@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26"
+  dependencies:
+    arrify "^1.0.1"
+    micromatch "^2.3.11"
+    object-assign "^4.1.0"
+    read-pkg-up "^1.0.1"
+    require-main-filename "^1.0.1"
+
+text-table@~0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+thenify-all@^1.0.0, thenify-all@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
+  dependencies:
+    thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
+  dependencies:
+    any-promise "^1.0.0"
+
 throttleit@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"
@@ -4084,6 +5034,10 @@ to-double-quotes@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/to-double-quotes/-/to-double-quotes-2.0.0.tgz#aaf231d6fa948949f819301bbab4484d8588e4a7"
 
+to-fast-properties@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
 to-single-quotes@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/to-single-quotes/-/to-single-quotes-2.0.1.tgz#7cc29151f0f5f2c41946f119f5932fe554170125"
@@ -4114,6 +5068,10 @@ trim-newlines@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
 
+trim-right@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
 trim@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
@@ -4140,6 +5098,10 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
+type-detect@^4.0.0:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2"
+
 type-is@~1.6.10:
   version "1.6.15"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
@@ -4157,6 +5119,19 @@ typedarray@^0.0.6, 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.8.29"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
+  dependencies:
+    source-map "~0.5.1"
+    yargs "~3.10.0"
+  optionalDependencies:
+    uglify-to-browserify "~1.0.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"
+
 underscore.string@~3.2.3:
   version "3.2.3"
   resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.2.3.tgz#806992633665d5e5fcb4db1fb3a862eb68e9e6da"
@@ -4366,7 +5341,7 @@ which@1, which@^1.2.9, which@~1.2.1:
   dependencies:
     isexe "^2.0.0"
 
-which@^1.2.10, which@^1.2.12:
+which@^1.2.10, which@^1.2.12, which@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
   dependencies:
@@ -4384,6 +5359,10 @@ widest-line@^1.0.0:
   dependencies:
     string-width "^1.0.1"
 
+window-size@0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
 window-size@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
@@ -4400,6 +5379,10 @@ winston@0.8.x:
     pkginfo "0.3.x"
     stack-trace "0.0.x"
 
+wordwrap@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
 wordwrap@~0.0.2:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
@@ -4427,6 +5410,20 @@ write-file-atomic@^1.1.2:
     imurmurhash "^0.1.4"
     slide "^1.1.5"
 
+write-file-atomic@^1.1.4:
+  version "1.3.4"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
+  dependencies:
+    graceful-fs "^4.1.11"
+    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@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
@@ -4570,6 +5567,15 @@ yargs@^6.6.0:
     y18n "^3.2.1"
     yargs-parser "^4.2.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"
+
 yauzl@2.4.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"