/*!
 * babel-extract-comments <https://github.com/jonschlinkert/babel-extract-comments>
 *
 * Copyright (c) 2014-2018, Jon Schlinkert.
 * Released under the MIT License.
 */

'use strict';

const fs = require('fs');
const path = require('path');
const babylon = require('babylon');

/**
 * Extract code comments from the given `string`.
 *
 * ```js
 * var extract = require('babel-extract-comments');
 * console.log(extract('// this is a code comment'));
 * // [{ type: 'CommentBlock',
 * //  value: '!\n * babel-extract-comments <https://github.com/jonschlinkert/babel-extract-comments>\n *\n *
 * // Copyright (c) 2014-2018, Jon Schlinkert.\n * Released under the MIT License.\n ',
 * //   start: 0,
 * //   end: 173,
 * //   loc: SourceLocation { start: [Position], end: [Position] } }]
 * ```
 * @param  {String} `string` String of javascript
 * @return {Array} Array of code comment objects.
 * @api public
 */

function extract(str, options) {
  const res = babylon.parse(str, options);
  return res.comments;
}

/**
 * Extract code comments from a JavaScript file.
 *
 * ```js
 * console.log(extract.file('some-file.js'), { cwd: 'some/path' });
 * // [ { type: 'Line',
 * //     value: ' this is a line comment',
 * //     range: [ 0, 25 ],
 * //     loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 25 } } } ]
 * ```
 * @param  {String} `file` Filepath to the file to parse.
 * @param  {Object} `options` Options to pass to [esprima][].
 * @return {Array} Array of code comment objects.
 * @api public
 */

extract.file = function(file, options) {
  const opts = Object.assign({ cwd: process.cwd() }, options);
  const str = fs.readFileSync(path.resolve(opts.cwd, file), 'utf8');
  return extract(str, options);
};

/**
 * Expose `extract`
 */

module.exports = extract;


import angular from 'angular';
import noUiSlider from 'nouislider';
import { module } from './module';

module.directive('noUiSlider', ($timeout, $q, $log) => ({
  restrict: 'AE',
  require: '?ngModel',
  scope: {
    created: '&?sliderCreated',
    options: '=?sliderOptions',
  },
  link: (scope, element, attrs, ngModel) => {
    const htmlElement = element[0];
    let options = angular.copy(scope.options);

    /**
     * Extends the API returned by noUiSlider with the `$on` function which wraps the `on` function
     * to use Angular.
     *
     * @param {Object} api The API instance returned by the `noUiSlider.create()` method
     * @return {Object} The API instance with the added `$on` function
     */
    function extendApi(api) {
      api.$on = (eventName, callback) => {
        const wrappedCallback = () => {
          $timeout(() => {
            callback(api.get());
          });
        };

        api.on(eventName, wrappedCallback);

        return () => {
          api.off(eventName, wrappedCallback);
        };
      };

      return api;
    }

    /**
     * Creates a watcher that calls the function given by the `slider-created` directive attribute.
     * The watcher fires every time the `slider-created` function changes.
     *
     * @param {Object} api The API instance returned by the `noUiSlider.create()` method
     */
    function setCreatedWatcher(api) {
      scope.$watch('created', (newCallback) => {
        if (!angular.isFunction(newCallback)) {
          return;
        }

        newCallback({ api });
      });
    }

    /**
     * Creates a watcher that looks for changes in the `slider-options` directive attribute. When a
     * change is detected the options for the noUiSlider instance are updated. Note that only the
     * 'margin', 'limit', 'step', 'range', 'animate' and 'snap' options can be updated this way (as
     * documented in https://refreshless.com/nouislider/more/#section-update). All other option
     * updates require you to destroy the current instance and create a new one.
     *
     * @param {Object} api The API instance returned by the `noUiSlider.create()` method
     */
    function setOptionsWatcher(api) {
      scope.$watch('options', (newOptions, oldOptions) => {
        if (angular.equals(newOptions, oldOptions)) {
          return;
        }

        options = angular.copy(scope.options);

        api.updateOptions(options);
      });
    }

    /**
     * Add ngModel controls to the directive. This allows the use of ngModel to set and get the
     * value in the slider. It uses the noUiSlider API's get and set functions, so no custom
     * formatters need to be defined for ngModel. The ngModelOptions can be used.
     *
     * @param {Object} api The API instance returned by the `noUiSlider.create()` method
     */
    function bindNgModelControls(api) {
      ngModel.$render = () => {
        api.set(ngModel.$modelValue);
      };

      api.on('update', () => {
        const positions = api.get();
        ngModel.$setViewValue(positions);
      });
    }

    /**
     * A utility function that returns a promise which resolves when ngModel is correctly loaded,
     * using $timeout.
     *
     * @return {Promise} Returns a promise that resolves with `null` when ngModel is null and thus
     * not in use. If the value entered for ngModel is not an array or number, an error is thrown
     * and thus the promise rejects. If the value entered for ngModel is correct, the promise
     * resolves with this value.
     */
    function initializeNgModel() {
      if (ngModel === null) {
        return $q.resolve(null);
      }

      return $q((resolve) => {
        $timeout(() => {
          if (!(angular.isArray(ngModel.$modelValue) || angular.isNumber(ngModel.$modelValue))) {
            throw new Error(`Value provided in ngModel is not a valid noUislider start position. Expected a Number or an Array of Numbers, found: ${ ngModel.$modelValue }`);
          }

          resolve(ngModel.$modelValue);
        });
      });
    }

    /**
     * Creates a noUiSlider instance.
     */
    function createInstance() {
      const api = extendApi(noUiSlider.create(htmlElement, options));

      setCreatedWatcher(api);
      setOptionsWatcher(api);

      if (ngModel !== null) {
        bindNgModelControls(api);
      }
    }

    // Wait for ngModel to be initialized
    initializeNgModel()
      .then(($modelValue) => {
        if ($modelValue !== null) {
          // If ngModel is being used, (over)write the start option for the noUiSlider options
          options.start = $modelValue;
        }

        // Create a noUiSlider instance
        createInstance();
      })
      .catch($log.error);
  },
}));


//chop a string into pronounced syllables
'use strict';

const ones = [
  /^[^aeiou]?ion/,
  /^[^aeiou]?ised/,
  /^[^aeiou]?iled/,

  // -ing, -ent
  /[aeiou][n][gt]$/,

  // -ate, -age
  /\wa[gt]e$/,
];
const all_spaces = / /g;
const ends_with_vowel = /[aeiouy]$/;
const starts_with_e_then_specials = /^e[sm]/;
const starts_with_e = /^e/;
const ends_with_noisy_vowel_combos = /(eo|eu|ia|oa|ua|ui)$/i;
const starts_with_single_vowel_combos = /^(eu)/i;
const aiouy = /[aiouy]/;
const ends_with_ee = /ee$/;
const whitespace_dash = /\s\-/;
const starts_with_consonant_vowel = /^[^aeiouy][h]?[aeiouy]/;
const joining_consonant_vowel = /^[^aeiou][e]([^d]|$)/;
const cvcv_same_consonant = /^([^aeiouy])[aeiouy]\1[aeiouy]/;
const cvcv_same_vowel = /^[^aeiouy]([aeiouy])[^aeiouy]\1/;
const cvcv_known_consonants = /^([tg][aeiouy]){2}/;
const only_one_or_more_c = /^[^aeiouy]+$/;

//suffix fixes
function postprocess(arr) {
  //trim whitespace
  arr = arr.map(function(w) {
    return w.trim();
  });
  arr = arr.filter(function(w) {
    return w !== '';
  });
  // if (arr.length > 2) {
  //   return arr;
  // }
  let l = arr.length;
  if (l > 1) {
    let suffix = arr[l - 2] + arr[l - 1];
    for (let i = 0; i < ones.length; i++) {
      if (suffix.match(ones[i])) {
        arr[l - 2] = arr[l - 2] + arr[l - 1];
        arr.pop();
      }
    }
  }

  // since the open syllable detection is overzealous,
  // sometimes need to rejoin incorrect splits
  if (arr.length > 1) {
    let first_is_open = (arr[0].length === 1 || arr[0].match(starts_with_consonant_vowel)) && arr[0].match(ends_with_vowel);
    let second_is_joining = arr[1].match(joining_consonant_vowel);

    if (first_is_open && second_is_joining) {
      let possible_combination = arr[0] + arr[1];
      let probably_separate_syllables = possible_combination.match(cvcv_same_consonant) || possible_combination.match(cvcv_same_vowel) || possible_combination.match(cvcv_known_consonants);

      if (!probably_separate_syllables) {
        arr[0] = arr[0] + arr[1];
        arr.splice(1, 1);
      }
    }
  }

  if (arr.length > 1) {
    let second_to_last_is_open = arr[arr.length - 2].match(starts_with_consonant_vowel) && arr[arr.length - 2].match(ends_with_vowel);
    let last_is_joining = arr[arr.length - 1].match(joining_consonant_vowel) && ones.every(re => !arr[arr.length - 1].match(re));

    if (second_to_last_is_open && last_is_joining) {
      let possible_combination = arr[arr.length - 2] + arr[arr.length - 1];
      let probably_separate_syllables = possible_combination.match(cvcv_same_consonant) || possible_combination.match(cvcv_same_vowel) || possible_combination.match(cvcv_known_consonants);

      if (!probably_separate_syllables) {
        arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1];
        arr.splice(arr.length - 1, 1);
      }
    }
  }

  if (arr.length > 1) {
    let single = arr[0] + arr[1];
    if (single.match(starts_with_single_vowel_combos)) {
      arr[0] = single;
      arr.splice(1, 1);
    }
  }

  if (arr.length > 1) {
    if (arr[arr.length - 1].match(only_one_or_more_c)) {
      arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1];
      arr.splice(arr.length - 1, 1);
    }
  }

  return arr;
}

let syllables = function(str) {
  let all = [];

  if (str.match(' ')) {
    return str.split(all_spaces).map(function(s) {
      return syllables(s);
    });
  }

  //method is nested because it's called recursively
  let doer = function(w) {
    let vow = /[aeiouy]$/;
    let chars = w.split('');
    let before = '';
    let after = '';
    let current = '';
    for (let i = 0; i < chars.length; i++) {
      before = chars.slice(0, i).join('');
      current = chars[i];
      after = chars.slice(i + 1, chars.length).join('');
      let candidate = before + chars[i];

      //it's a consonant that comes after a vowel
      if (before.match(ends_with_vowel) && !current.match(ends_with_vowel)) {
        if (after.match(starts_with_e_then_specials)) {
          candidate += 'e';
          after = after.replace(starts_with_e, '');
        }
        all.push(candidate);
        return doer(after);
      }

      //unblended vowels ('noisy' vowel combinations)
      if (candidate.match(ends_with_noisy_vowel_combos)) { //'io' is noisy, not in 'ion'
        all.push(before);
        all.push(current);
        return doer(after); //recursion
      }

      // if candidate is followed by a CV, assume consecutive open syllables
      if (candidate.match(ends_with_vowel) && after.match(starts_with_consonant_vowel)) {
        all.push(candidate);
        return doer(after);
      }
    }
    //if still running, end last syllable
    if (str.match(aiouy) || str.match(ends_with_ee)) { //allow silent trailing e
      all.push(w);
    } else {
      all[all.length - 1] = (all[all.length - 1] || '') + w; //append it to the last one
    }
    return null;
  };

  str.split(whitespace_dash).forEach(function(s) {
    doer(s);
  });
  all = postprocess(all);

  //for words like 'tree' and 'free'
  if (all.length === 0) {
    all = [str];
  }
  //filter blanks
  all = all.filter(function(s) {
    return s !== '' && s !== null && s !== undefined;
  });

  return all;
};

// console.log(syllables('civilised'));

module.exports = syllables;


/*************************************************************************

   chrome-remote-multiplex

   https://github.com/johnspackman/chrome-remote-multiplex

   Copyright:
     2017 Zenesis Limited

   License:
     MIT: https://opensource.org/licenses/MIT

   Authors:
     * John Spackman <john.spackman@zenesis.com> (johnspackman)

************************************************************************ */

const Protocol = require('chrome-remote-interface').Protocol;
const EventEmitter = require('events');
const WebSocket = require('ws');
const Express = require('express');
const Http = require('http');
const Url = require('url');
const Dot = require('dot');

var PACKAGE = require("../package.json");

class Logger {
  constructor() {
    this.mode = "info";
  }

  log(...args) {
    console.log(...args);
  }

  info(...args) {
    this.log(...args);
  }

  error(...args) {
    this.log(...args);
  }

  debug(...args) {
    if (this.mode === "debug")
      this.log(...args);
  }
}

const LOG = new Logger();

/**
 * Used by ProxyDomain-derived classes to create an API method called apiName; the API
 * can have methods `send` and `reply` which are bound to `self`; a promise is created
 * in `api.replied` which is satisfied after the `reply` method has been called
 *
 * @param self
 * @param apiName
 * @param api
 * @returns
 */
function addApi(self, apiName, api) {
  for (var name in api) {
    var fn = api[name];
    if (typeof fn === "function")
      api[name] = api[name].bind(self);
  }

  var tmp = null;
  api.replied = new Promise((resolve, reject) => {
    tmp = { resolve, reject };
  });
  api.replied.resolve = tmp.resolve;
  api.replied.reject = tmp.reject;

  self[apiName] = api;
}

/**
 * ProxyDomain is the base class for creating a proxy of one of the protocol domains;
 * by default, all requests for domains are just bounced to the server and back, with
 * events being broadcast to all clients.  The ProxyDomain class provides a mechanism
 * for the multiplexer to intercept that communication (eg Runtime domain has to collate
 * responses)
 */
class ProxyDomain extends EventEmitter {

  constructor(remoteDebuggerProxy) {
    super();
    this.remoteDebuggerProxy = remoteDebuggerProxy;
  }

  onEvent(data) {
  }
}

/**
 * ProxyDomain implementation for most domains; it collects enable/disable into a single
 * enable/disable with a reference count
 */
class DefaultProxyDomain extends ProxyDomain {
  constructor(remoteDebuggerProxy) {
    super(remoteDebuggerProxy);
    this._enabled = 0;

    addApi(this, "enable", {
      send(devtoolsClient, data, sendImpl) {
        var t = this;
        this._enabled++;
        if (t._enabled === 1) {
          sendImpl(data);
        } else {
          t.enable.replied.then(() => {
            return devtoolsClient.sendMessageToClient({ id: data.id, result: {} }, false);
          });
        }

        return false;
      },

      reply(devtoolsClient, data) {
        var t = this;
        return devtoolsClient.sendMessageToClient(data).then(() => {
          t.enable.replied.resolve(data);
        });
      }
    });

    addApi(this, "disable", {
      send(devtoolsClient, data, sendImpl) {
        this._enabled--;
        if (this._enabled === 0) {
          sendImpl(data);
        } else {
          t.disable.replied.then((data) => {
            return devtoolsClient.sendMessageToClient({ id: data.id, result: {} }, false);
          });
        }

        return false;
      }
    });
  }
}

/**
 * Implementation of ProxyDomain for the Runtime domain.  Execution contexts have
 * to be tracked because the remote debug server will only publish them to us once,
 * so we have to store them and publish them to each client that attaches to us.
 */
class RuntimeProxyDomain extends ProxyDomain {
  constructor(remoteDebuggerProxy) {
    super(remoteDebuggerProxy);
    this._enabled = 0;
    this._executionContexts = {};

    addApi(this, "enable", {
      send(devtoolsClient, data, sendImpl) {
        var t = this;
        this._enabled++;
        if (t._enabled === 1) {
          sendImpl(data);
        } else {
          t.enable.replied.then(() => {
            var promises = [];
            promises.push(devtoolsClient.sendMessageToClient({ id: data.id, result: {} }, false));
            for (var id in t._executionContexts)
              promises.push(devtoolsClient.sendMessageToClient(t._executionContexts[id], true));

            return Promise.all(promises);
          });
        }

        return false;
      },

      reply(devtoolsClient, data) {
        var t = this;
        return devtoolsClient.sendMessageToClient(data).then(() => {
          t.enable.replied.resolve(data);
        });
      },

      replied: null
    });

    addApi(this, "disable", {
      send(devtoolsClient, data, sendImpl) {
        this._enabled--;
        if (this._enabled === 0) {
          sendImpl(data);
        }

        return false;
      }
    });

  }

  onEvent(data) {
    var t = this;

    if (data.method === "Runtime.executionContextCreated") {
      t._executionContexts[data.params.context.id] = data;
    } else if (data.method === "Runtime.executionContextDestroyed") {
      delete t._executionContexts[data.params.contextId];
    } else if (data.method === "Runtime.executionContextsCleared") {
      t._executionContexts = {};
    }
  }
}

/**
 * Represents a single connection from a Devtools client
 */
class DevtoolsClient extends EventEmitter {
  constructor(remoteDebuggerProxy, ws) {
    super();
    const t = this;
    this.remoteDebuggerProxy = remoteDebuggerProxy;
    this.ws = ws;
    ws.on("close", () => remoteDebuggerProxy.detach(this));
    ws.on('message', function (data) {
      const message = JSON.parse(data);
      t.onMessageFromClient(message);
    });
  }

  close() {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }

  onMessageFromClient(data) {
    const t = this;
    return t.remoteDebuggerProxy.sendMessageToServer(this, data);
  }

  sendMessageToClient(data, broadcast) {
    const t = this;

    return promisify((cb) => {
      LOG.debug("REPLY: " + JSON.stringify(data));
      t.ws.send(JSON.stringify(data), cb);
    });
  }
}

/**
 * Represents the single connection to the remote devtools debugger (ie Headless Chrome),
 * and tracks the multiple DevtoolsClient instances which attach to it
 */
class RemoteDebuggerProxy extends EventEmitter {
  constructor(multiplexServer, target) {
    super();
    this.multiplexServer = multiplexServer;
    this.target = target;
    this.devtoolsClients = [];
    this._idMap = {};
    this._nextCommandId = 1;
    this._domainProxies = {
        Runtime: new RuntimeProxyDomain(this)
    };
  }

  /**
   * Connects to the remote debugger instance (ie Chrome Headless)
   */
  connect() {
    const t = this;
    Protocol(t.options).then((data) => {
      data.descriptor.domains.forEach((domain) => {
        if (t._domainProxies[domain.domain] !== undefined)
          return;
        var cmdEnable = null;
        var cmdDisable = null;
        domain.commands.forEach((command) => {
          if (command.name === "enable")
            cmdEnable = command;
          else if (command.name === "disable")
            cmdDisable = command;
        });
        if (cmdEnable || cmdDisable) {
          LOG.debug("Creating default domain for " + domain.domain);
          t._domainProxies[domain.domain] = new DefaultProxyDomain(this);
        }
      });
    });
    return new Promise((resolve, reject) => {
      var url = t.target.originalWebSocketDebuggerUrl;
      url = url.replace(/localhost\:/g, t.multiplexServer.options.remoteClientHostname + ":");
      var ws = t.ws = new WebSocket(url);
      ws.on('open', resolve);
      ws.on('close', function (code) {
        t.emit('disconnect');
      });
      ws.on('error', reject);
      ws.on('message', function (data) {
        const message = JSON.parse(data);
        t.onMessageFromServer(message);
      });
    });
  }

  /**
   * Closes the connection to the server and clients
   */
  close() {
    if (this.ws) {
      var ws = this.ws;
      this.ws = null;
      ws.close();
      this.devtoolsClients.forEach((client) => client.close());
      this.emit("close");
    }
  }

  /**
   * Returns true if there are no devtools clients
   */
  isUnused() {
    return this.devtoolsClients.length == 0;
  }

  /**
   * Detaches a DevTools client
   */
  detach(devtoolsClient) {
    for (var i = 0; i < this.devtoolsClients.length; i++)
      if (this.devtoolsClients[i] === devtoolsClient) {
        this.devtoolsClients.splice(i, 1);
        break;
      }
    this.target.numberOfClients = this.devtoolsClients.length;
    if (!!this.ws && this.autoClose && this.devtoolsClients.length == 0)
      this.closeTarget().then(() => this.close());
  }

  /**
   * Attaches a DevTools client
   */
  attach(devtoolsClient) {
    this.devtoolsClients.push(devtoolsClient);
    this.target.numberOfClients = this.devtoolsClients.length;
  }

  /**
   * Shutsdown the target
   */
  closeTarget() {
    var t = this;
    return httpGet({
      hostname: t.multiplexServer.options.remoteClientHostname,
      port: t.multiplexServer.options.remoteClientPort,
      path: "/json/close/" + t.target.id,
      method: 'GET'
    });
  }

  /**
   * Upgrade request from express
   */
  upgrade(request, socket, head) {
    const t = this;
    const wss = new WebSocket.Server({ noServer: true });

    wss.handleUpgrade(request, socket, head, (ws) => {
      var devtoolsClient = new DevtoolsClient(t, ws);
      t.attach(devtoolsClient);
      wss.emit('connection', ws);
    });
  }

  /**
   * Handles a message from the remote debugger
   */
  onMessageFromServer(data) {
    const t = this;

    LOG.debug('SERVER: %s...', JSON.stringify(data));

    // Map API IDs back to client'ss original ID
    if (data.id) {
      var map = t._idMap[data.id];
      if (map) {
        data.id = map.originalId;

        // Offer to domain specific proxies
        var [ domainName, methodName ] = splitName(map.data.method);
        var domainProxy = this._domainProxies[domainName];
        var api = domainProxy && domainProxy[methodName];
        if (api && typeof api.reply === "function") {
          var result = api.reply(map.devtoolsClient, data);
          if (result !== undefined)
            return result;
        }

        // Just bounce it straight on
        return map.devtoolsClient.sendMessageToClient(data);
      } else {
        LOG.error("Server message for id " + data.id + " not matched");
        return;
      }
    }

    // Handle events - offer to domain specific proxy
    var [ domainName, methodName ] = splitName(data.method);
    var domainProxy = this._domainProxies[domainName];
    if (domainProxy) {
      var result = domainProxy.onEvent(data);
      if (result !== undefined)
        return result;
    }

    // Pass event on
    this.devtoolsClients.forEach(function(devtoolsClient) {
      devtoolsClient.sendMessageToClient(data, true);
    });
  }

  /**
   * Allows a DevToolsClient to send a message to the server; handles mapping the
   * client's own ID to a global one for the server
   */
  sendMessageToServer(devtoolsClient, data) {
    const t = this;

    LOG.debug('CLIENT: %s...', JSON.stringify(data));

    // Offer the domain proxy the opportunity to handle the method call
    if (data.method) {
      var [ domainName, methodName ] = splitName(data.method);
      var domainProxy = this._domainProxies[domainName];
      var api = domainProxy && domainProxy[methodName];
      if (api && typeof api.send === "function") {
        var result = api.send(devtoolsClient, data, doSend);
        if (result !== undefined)
          return result;
      }
    }

    // Just send it
    return doSend(data);

    function doSend(data) {
      if (data.id !== undefined) {
        var newId = t._nextCommandId++;
        t._idMap[newId] = {
            newId,
            originalId: data.id,
            data: data,
            devtoolsClient
        };
        data.id = newId;
      }

      return promisify((cb) => {
        t.ws.send(JSON.stringify(data), cb);
      });
    }
  }
}

/**
 * This is the main server instance that clients connect to; it contains the ExpressJS server,
 * communicates with the remote debug server, and establishes RemoteDebuggerProxy instances
 * for each remote target
 *
 */

const DEFAULT_DEVTOOLS_URL = "https://chrome-devtools-frontend.appspot.com/serve_file/@4b9102f9588fb6cf639a6165fd4777658d5ade0d/inspector.html?";

export default class MultiplexServer extends EventEmitter {

  constructor(options) {
    super();
    options = options||{};
    if (options.logging === "debug")
      LOG.mode = "debug";
    var remoteClient = options.remoteClient;
    if (remoteClient !== undefined) {
      var m = remoteClient.match(/^([^:]+)(:([0-9]+))?$/);
      if (m) {
        options.remoteClientHostname = m[1];
        options.remoteClientPort = m[3];
      } else {
        throw new Error("Cannot interpret remoteClient - found " + remoteClient + ", expected something like 'localhost:9222'");
      }
    }
    this.options = {
      listenPort: options.listenPort || 9223,
      remoteClientHostname: options.remoteClientHostname||"localhost",
      remoteClientPort: options.remoteClientPort||9222
    };
    this.options.remoteClient = this.options.remoteClientHostname + ":" + this.options.remoteClientPort;
  }

  /**
   * Starts the HTTP server
   */
  listen() {
    const t = this;

    const app = Express();

    function reportHttpError(req, err) {
      LOG.error("Error in " + req.method + " " + req.originalUrl + ": " + err);
    }

    /*
     * Web page that provides links to debug
     */
    (function() {
// https://chrome-devtools-frontend.appspot.com/serve_file/@4b9102f9588fb6cf639a6165fd4777658d5ade0d/inspector.html?ws=localhost:9223/devtools/page/1e371c5b-25ef-4ee9-9ef6-3ca11d9d59ee&remoteFrontend=true

      var template = Dot.template(
`

Headless proxy

    {{~ it.multiplex.targets :target }}

              {{= it.title(target) }}

    {{~}}

`);

      app.get('/', function (req, res) {
        t.refreshTargets()
          .then(function() {
            res.send(template({
              multiplex: t,
              DEFAULT_DEVTOOLS_URL: DEFAULT_DEVTOOLS_URL,
              url: function(target) {
                return DEFAULT_DEVTOOLS_URL + target.webSocketDebuggerUrl.replace(/^ws:\/\//, "ws=/") + "&remoteFrontend=true";
              },
              title: function(target) {
                var str = target.title;
                if (target.autoClose)
                  str += " (set to auto-close)";
                return str;
              }
            }));
          })
          .catch(reportHttpError.bind(this, req));
      });
    })();

    function getContentType(response) {
      var contentType = response.headers["content-type"];
      if (contentType) {
        var pos = contentType.indexOf(';');
        contentType = contentType.substring(0, pos);
      }
      return contentType;
    }

    // Gets JSON from the remote server
    function getJson(path) {
      return httpGet({
        hostname: t.options.remoteClientHostname,
        port: t.options.remoteClientPort,
        path: path,
        method: 'GET'
      }).then((obj) => {
        var contentType = getContentType(obj.response);
        if (contentType !== "application/json")
          LOG.warn("Expecting JSON from " + path + " but found wrong content type: " + contentType);

        try {
          return JSON.parse(obj.data);
        } catch(ex) {
          LOG.warn("Cannot parse JSON returned from " + path);
          return null;
        }
      });
    }

    // Gets data from the remote server and copies it to the client
    function copyToClient(req, res) {
      return httpGet({
        hostname: t.options.remoteClientHostname,
        port: t.options.remoteClientPort,
        path: req.originalUrl,
        method: 'GET'
      }).then((obj) => {
        var contentType = getContentType(obj.response);
        if (contentType)
          res.set("Content-Type", contentType);
        res.send(obj.data);
      });
    }

    // REST API: list targets
    app.get(["/json", "/json/list"], (req, res) => {
      t.refreshTargets()
        .then(function() {
          res.set("Content-Type", "application/json");
          res.send(JSON.stringify(t.targets, null, 2));
        })
        .catch(reportHttpError.bind(this, req));
                                                                                });

    // REST API: create a new target
    app.get('/json/new', (req, res) => {
      return getJson("/json/new").then(function(target) {
        if (target)
          target = t._addTarget(target);
        res.set("Content-Type", "application/json");
        res.send(JSON.stringify(target, null, 2));
      });
    });

    // REST API: close a target
    app.get('/json/close/*', (req, res) => {
      return httpGet({
        hostname: t.options.remoteClientHostname,
        port: t.options.remoteClientPort,
        path: req.originalUrl,
        method: 'GET'
      }).then((obj) => {
        var id = req.originalUrl.match(/\/json\/close\/(.*)$/)[1];
        var proxy = t.proxies[id];
        if (proxy)
          proxy.close();

        var contentType = getContentType(obj.response);
        if (contentType)
          res.set("Content-Type", contentType);
        res.send(obj.data);
      });
    });

    // REST API: auto-close a target
    app.get('/json/auto-close/*', (req, res) => {
      t.refreshTargets()
        .then(() => {
          var id = req.originalUrl.match(/\/json\/auto-close\/(.*)$/)[1];
          var proxy = t._proxies[id];
          if (proxy) {
            if (proxy.isUnused()) {
              proxy.closeTarget().close();
              LOG.info("Closing target " + id + " due to /json/auto-close")
              res.send("Target is closing");
            } else {
              proxy.autoClose = true;
              t.targetsById[id].autoClose = true;
              LOG.info("Marking target " + id + " to auto close")
              res.send("Target set to auto close");
            }
          } else {
            var target = t.targetsById[id];
            if (target) {
              target.autoClose = true;
              LOG.info("Marking target " + id + " to auto close after first use")
              res.send("Target will close after first use");
            } else
              res.status(500).send("Unrecognised target id " + id);
          }
        });
    });

    // REST API: get version numbers
    app.get('/json/version', (req, res) => {
      return getJson(req.originalUrl).then(function(json) {
        json["Chrome-Remote-Multiplex-Version"] = PACKAGE.version;
        res.set("Content-Type", "application/json");
        res.send(JSON.stringify(json, null, 2));
      });
    });

    app.get('/json/protocol', copyToClient);
    app.get('/json/activate', copyToClient);

    const webServer = this.webServer = Http.createServer(app);
    const proxies = this._proxies = {};

    // Upgrade the connection from ExpressJS
    webServer.on('connection', (socket) => {
      LOG.debug("WEB: connection");
    });
    webServer.on('request', (req, res) => {
      LOG.debug("WEB: request");
    });
    webServer.on('upgrade', (request, socket, head) => {
      LOG.debug("WEB: upgrade");
      const pathname = Url.parse(request.url).pathname;
      var m = pathname.match(/\/([^\/]+)$/);
      var uuid = (m && m[1])||null;

      if (!uuid) {
        LOG.error("Cannot find UUID in " + pathname);
        socket.destroy();
        return;
      }

      t.refreshTargets().then(function() {
        var target = t.targetsById[uuid];
        if (!target) {
          LOG.error("Cannot find target for " + uuid);
          socket.destroy();
          return;
        }

        var proxy = proxies[uuid];
        if (!proxy) {
          if (!target.webSocketDebuggerUrl || target.type !== "page") {
            LOG.error("Target " + uuid + " not eligable for connection");
            socket.destroy();
            return;
          }
          proxy = proxies[uuid] = new RemoteDebuggerProxy(t, target);
          proxy.connect().then(function() {
            return proxy.upgrade(request, socket, head);
          });
          proxy.on('close', () => {
            delete t.targetsById[uuid];
            for (var i = 0; i < t.targets.length; i++)
              if (t.targets[i].id === uuid) {
                t.targets.splice(i, 1);
                break;
              }
          });
          if (target.autoClose)
            proxy.autoClose = true;

        } else {
          return proxy.upgrade(request, socket, head);
        }
      });
    });

    return new Promise((resolve, reject) => {
      LOG.debug("Starting Express server");
      webServer.listen(t.options.listenPort, resolve);
    })
  }

  /**
   * Shuts down
   */
  close() {
    this.webServer.close();
    this.webServer = null;
  }

  /**
   * Rediscovers all targets at the remote server that can be connected to
   */
  refreshTargets() {
    var t = this;
    return httpGet({
      hostname: t.options.remoteClientHostname,
      port: t.options.remoteClientPort,
      path: '/json',
      method: 'GET'
    }).then(function(obj) {
      var json = null;
      if (!obj.data) {
        LOG.debug("No data received from " + t.options.remoteClient);
        return;
      }
      try {
        json = JSON.parse(obj.data);
      } catch(ex) {
        LOG.error("Error while parsing JSON from " + t.options.remoteClient + ": " + ex);
        return;
      }
      var oldTargets = t.targets;
      var oldTargetsById = t.targetsById||{};
      t.targets = json;
      t.targetsById = {};
      for (var i = 0; i < t.targets.length; i++) {
        var target = t.targets[i];
        t.targets[i] = t._addTarget(target, oldTargetsById[target.id]);
      }
    });
  }

  /**
   * Adds a target
   */
  _addTarget(src, target) {
    const RESERVED_KEYWORDS = [
      "description", "id", "title", "type", "url", "devtoolsFrontendUrl", "webSocketDebuggerUrl",
      "originalDevtoolsFrontendUrl", "originalWebSocketDebuggerUrl" ];

    var t = this;
    if (!target)
      target = {};

    for (var name in src)
      target[name] = src[name];

    if (target.devtoolsFrontendUrl)
      target.originalDevtoolsFrontendUrl = target.devtoolsFrontendUrl;
    target.devtoolsFrontendUrl = "/devtools/inspector.html?ws=localhost:" + t.options.listenPort + "/devtools/page/" + target.id;
    if (target.webSocketDebuggerUrl)
      target.originalWebSocketDebuggerUrl = target.webSocketDebuggerUrl;
    target.webSocketDebuggerUrl = "ws://localhost:" + t.options.listenPort + "/devtools/page/" + target.id;
    target.title += " (proxied)";

    if (target.numberOfClients === undefined)
      target.numberOfClients = 0;

    return t.targetsById[target.id] = target;
  }
}

/**
 * API for remote control of the MultiplexServer
 */
export class ClientApi {

  constructor(options) {
    options = options||{};
    var remoteClient = options.remoteClient;
    if (remoteClient !== undefined) {
      var m = remoteClient.match(/^([^:]+)(:([0-9]+))?$/);
      if (m) {
        options.remoteClientHostname = m[1];
        options.remoteClientPort = m[3];
      } else {
        throw new Error("Cannot interpret remoteClient - found " + remoteClient + ", expected something like 'localhost:9222'");
      }
    }
    this.options = {
      remoteClientHostname: options.remoteClientHostname||"localhost",
      remoteClientPort: options.remoteClientPort||9222
    };
    this.options.remoteClient = this.options.remoteClientHostname + ":" + this.options.remoteClientPort;
  }

  /**
   * Enables auto close for a specific target
   */
  autoClose(id) {
    return httpGet({
      hostname: this.options.remoteClientHostname,
      port: this.options.remoteClientPort,
      path: '/json/auto-close/' + id,
      method: 'GET'
    }).then((obj) => obj && obj.data);
  }
}

/**
 * Does a simple HTTP GET
 * @return Promise - payload is the response context
 */
function httpGet(options) {
  return new Promise((resolve, reject) => {
    var req = Http.request(options, function(response) {
      var str = '';

      response.on('data', function (chunk) {
        str += chunk;
      });

      response.on('end', function () {
        resolve({ data: str, response: response });
      });
    });
    req.on('error', reject);
    req.end();
  });
}

/**
 * Simple promisify
 */
function promisify(fn) {
  return new Promise((resolve, reject) => {
    fn(function(err, value) {
      if (err)
        reject(err);
      else
        resolve(value);
    });
  });
}

/**
 * Splits a string into domain and method
 */
function splitName(method) {
  var pos = method.indexOf('.');
  if (pos < 0)
    return [ null, method ];
  var domainName = method.substring(0, pos);
  var methodName = method.substring(pos + 1);
  return [ domainName, methodName ];
}


'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

/*************************************************************************

   chrome-remote-multiplex

   https://github.com/johnspackman/chrome-remote-multiplex

   Copyright:
     2017 Zenesis Limited

   License:
     MIT: https://opensource.org/licenses/MIT

   Authors:
     * John Spackman <john.spackman@zenesis.com> (johnspackman)

************************************************************************ */

var Protocol = require('chrome-remote-interface').Protocol;
var EventEmitter = require('events');
var WebSocket = require('ws');
var Express = require('express');
var Http = require('http');
var Url = require('url');
var Dot = require('dot');

var PACKAGE = require("../package.json");

var Logger = function () {
  function Logger() {
    _classCallCheck(this, Logger);

    this.mode = "info";
  }

  _createClass(Logger, [{
    key: 'log',
    value: function log() {
      var _console;

      (_console = console).log.apply(_console, arguments);
    }
  }, {
    key: 'info',
    value: function info() {
      this.log.apply(this, arguments);
    }
  }, {
    key: 'error',
    value: function error() {
      this.log.apply(this, arguments);
    }
  }, {
    key: 'debug',
    value: function debug() {
      if (this.mode === "debug") this.log.apply(this, arguments);
    }
  }]);

  return Logger;
}();

var LOG = new Logger();

/**
 * Used by ProxyDomain-derived classes to create an API method called apiName; the API
 * can have methods `send` and `reply` which are bound to `self`; a promise is created
 * in `api.replied` which is satisfied after the `reply` method has been called
 *
 * @param self
 * @param apiName
 * @param api
 * @returns
 */
function addApi(self, apiName, api) {
  for (var name in api) {
    var fn = api[name];
    if (typeof fn === "function") api[name] = api[name].bind(self);
  }

  var tmp = null;
  api.replied = new Promise(function (resolve, reject) {
    tmp = { resolve: resolve, reject: reject };
  });
  api.replied.resolve = tmp.resolve;
  api.replied.reject = tmp.reject;

  self[apiName] = api;
}

/**
 * ProxyDomain is the base class for creating a proxy of one of the protocol domains;
 * by default, all requests for domains are just bounced to the server and back, with
 * events being broadcast to all clients.  The ProxyDomain class provides a mechanism
 * for the multiplexer to intercept that communication (eg Runtime domain has to collate
 * responses)
 */

var ProxyDomain = function (_EventEmitter) {
  _inherits(ProxyDomain, _EventEmitter);

  function ProxyDomain(remoteDebuggerProxy) {
    _classCallCheck(this, ProxyDomain);

    var _this = _possibleConstructorReturn(this, (ProxyDomain.__proto__ || Object.getPrototypeOf(ProxyDomain)).call(this));

    _this.remoteDebuggerProxy = remoteDebuggerProxy;
    return _this;
  }

  _createClass(ProxyDomain, [{
    key: 'onEvent',
    value: function onEvent(data) {}
  }]);

  return ProxyDomain;
}(EventEmitter);

/**
 * ProxyDomain implementation for most domains; it collects enable/disable into a single
 * enable/disable with a reference count
 */

var DefaultProxyDomain = function (_ProxyDomain) {
  _inherits(DefaultProxyDomain, _ProxyDomain);

  function DefaultProxyDomain(remoteDebuggerProxy) {
    _classCallCheck(this, DefaultProxyDomain);

    var _this2 = _possibleConstructorReturn(this, (DefaultProxyDomain.__proto__ || Object.getPrototypeOf(DefaultProxyDomain)).call(this, remoteDebuggerProxy));

    _this2._enabled = 0;

    addApi(_this2, "enable", {
      send: function send(devtoolsClient, data, sendImpl) {
        var t = this;
        this._enabled++;
        if (t._enabled === 1) {
          sendImpl(data);
        } else {
          t.enable.replied.then(function () {
            return devtoolsClient.sendMessageToClient({ id: data.id, result: {} }, false);
          });
        }

        return false;
      },
      reply: function reply(devtoolsClient, data) {
        var t = this;
        return devtoolsClient.sendMessageToClient(data).then(function () {
          t.enable.replied.resolve(data);
        });
      }
    });

    addApi(_this2, "disable", {
      send: function send(devtoolsClient, data, sendImpl) {
        this._enabled--;
        if (this._enabled === 0) {
          sendImpl(data);
        } else {
          t.disable.replied.then(function (data) {
            return devtoolsClient.sendMessageToClient({ id: data.id, result: {} }, false);
          });
        }

        return false;
      }
    });
    return _this2;
  }

  return DefaultProxyDomain;
}(ProxyDomain);

/**
 * Implementation of ProxyDomain for the Runtime domain.  Execution contexts have
 * to be tracked because the remote debug server will only publish them to us once,
 * so we have to store them and publish them to each client that attaches to us.
 */

var RuntimeProxyDomain = function (_ProxyDomain2) {
  _inherits(RuntimeProxyDomain, _ProxyDomain2);

  function RuntimeProxyDomain(remoteDebuggerProxy) {
    _classCallCheck(this, RuntimeProxyDomain);

    var _this3 = _possibleConstructorReturn(this, (RuntimeProxyDomain.__proto__ || Object.getPrototypeOf(RuntimeProxyDomain)).call(this, remoteDebuggerProxy));

    _this3._enabled = 0;
    _this3._executionContexts = {};

    addApi(_this3, "enable", {
      send: function send(devtoolsClient, data, sendImpl) {
        var t = this;
        this._enabled++;
        if (t._enabled === 1) {
          sendImpl(data);
        } else {
          t.enable.replied.then(function () {
            var promises = [];
            promises.push(devtoolsClient.sendMessageToClient({ id: data.id, result: {} }, false));
            for (var id in t._executionContexts) {
              promises.push(devtoolsClient.sendMessageToClient(t._executionContexts[id], true));
            }return Promise.all(promises);
          });
        }

        return false;
      },
      reply: function reply(devtoolsClient, data) {
        var t = this;
        return devtoolsClient.sendMessageToClient(data).then(function () {
          t.enable.replied.resolve(data);
        });
      },

      replied: null
    });

    addApi(_this3, "disable", {
      send: function send(devtoolsClient, data, sendImpl) {
        this._enabled--;
        if (this._enabled === 0) {
          sendImpl(data);
        }

        return false;
      }
    });

    return _this3;
  }

  _createClass(RuntimeProxyDomain, [{
    key: 'onEvent',
    value: function onEvent(data) {
      var t = this;

      if (data.method === "Runtime.executionContextCreated") {
        t._executionContexts[data.params.context.id] = data;
      } else if (data.method === "Runtime.executionContextDestroyed") {
        delete t._executionContexts[data.params.contextId];
      } else if (data.method === "Runtime.executionContextsCleared") {
        t._executionContexts = {};
      }
    }
  }]);

  return RuntimeProxyDomain;
}(ProxyDomain);

/**
 * Represents a single connection from a Devtools client
 */

var DevtoolsClient = function (_EventEmitter2) {
  _inherits(DevtoolsClient, _EventEmitter2);

  function DevtoolsClient(remoteDebuggerProxy, ws) {
    _classCallCheck(this, DevtoolsClient);

    var _this4 = _possibleConstructorReturn(this, (DevtoolsClient.__proto__ || Object.getPrototypeOf(DevtoolsClient)).call(this));

    var t = _this4;
    _this4.remoteDebuggerProxy = remoteDebuggerProxy;
    _this4.ws = ws;
    ws.on("close", function () {
      return remoteDebuggerProxy.detach(_this4);
    });
    ws.on('message', function (data) {
      var message = JSON.parse(data);
      t.onMessageFromClient(message);
    });
    return _this4;
  }

  _createClass(DevtoolsClient, [{
    key: 'close',
    value: function close() {
      if (this.ws) {
        this.ws.close();
        this.ws = null;
      }
    }
  }, {
    key: 'onMessageFromClient',
    value: function onMessageFromClient(data) {
      var t = this;
      return t.remoteDebuggerProxy.sendMessageToServer(this, data);
    }
  }, {
    key: 'sendMessageToClient',
    value: function sendMessageToClient(data, broadcast) {
      var t = this;

      return promisify(function (cb) {
        LOG.debug("REPLY: " + JSON.stringify(data));
        t.ws.send(JSON.stringify(data), cb);
      });
    }
  }]);

  return DevtoolsClient;
}(EventEmitter);

/**
 * Represents the single connection to the remote devtools debugger (ie Headless Chrome),
 * and tracks the multiple DevtoolsClient instances which attach to it
 */

var RemoteDebuggerProxy = function (_EventEmitter3) {
  _inherits(RemoteDebuggerProxy, _EventEmitter3);

  function RemoteDebuggerProxy(multiplexServer, target) {
    _classCallCheck(this, RemoteDebuggerProxy);

    var _this5 = _possibleConstructorReturn(this, (RemoteDebuggerProxy.__proto__ || Object.getPrototypeOf(RemoteDebuggerProxy)).call(this));

    _this5.multiplexServer = multiplexServer;
    _this5.target = target;
    _this5.devtoolsClients = [];
    _this5._idMap = {};
    _this5._nextCommandId = 1;
    _this5._domainProxies = {
      Runtime: new RuntimeProxyDomain(_this5)
    };
    return _this5;
  }

  /**
   * Connects to the remote debugger instance (ie Chrome Headless)
   */

  _createClass(RemoteDebuggerProxy, [{
    key: 'connect',
    value: function connect() {
      var _this6 = this;

      var t = this;
      Protocol(t.options).then(function (data) {
        data.descriptor.domains.forEach(function (domain) {
          if (t._domainProxies[domain.domain] !== undefined) return;
          var cmdEnable = null;
          var cmdDisable = null;
          domain.commands.forEach(function (command) {
            if (command.name === "enable") cmdEnable = command;else if (command.name === "disable") cmdDisable = command;
          });
          if (cmdEnable || cmdDisable) {
            LOG.debug("Creating default domain for " + domain.domain);
            t._domainProxies[domain.domain] = new DefaultProxyDomain(_this6);
          }
        });
      });
      return new Promise(function (resolve, reject) {
        var url = t.target.originalWebSocketDebuggerUrl;
        url = url.replace(/localhost\:/g, t.multiplexServer.options.remoteClientHostname + ":");
        var ws = t.ws = new WebSocket(url);
        ws.on('open', resolve);
        ws.on('close', function (code) {
          t.emit('disconnect');
        });
        ws.on('error', reject);
        ws.on('message', function (data) {
          var message = JSON.parse(data);
          t.onMessageFromServer(message);
        });
      });
    }

    /**
     * Closes the connection to the server and clients
     */

  }, {
    key: 'close',
    value: function close() {
      if (this.ws) {
        var ws = this.ws;
        this.ws = null;
        ws.close();
        this.devtoolsClients.forEach(function (client) {
          return client.close();
        });
        this.emit("close");
      }
    }

    /**
     * Returns true if there are no devtools clients
     */

  }, {
    key: 'isUnused',
    value: function isUnused() {
      return this.devtoolsClients.length == 0;
    }

    /**
     * Detaches a DevTools client
     */

  }, {
    key: 'detach',
    value: function detach(devtoolsClient) {
      var _this7 = this;

      for (var i = 0; i < this.devtoolsClients.length; i++) {
        if (this.devtoolsClients[i] === devtoolsClient) {
          this.devtoolsClients.splice(i, 1);
          break;
        }
      }this.target.numberOfClients = this.devtoolsClients.length;
      if (!!this.ws && this.autoClose && this.devtoolsClients.length == 0) this.closeTarget().then(function () {
        return _this7.close();
      });
    }

    /**
     * Attaches a DevTools client
     */

  }, {
    key: 'attach',
    value: function attach(devtoolsClient) {
      this.devtoolsClients.push(devtoolsClient);
      this.target.numberOfClients = this.devtoolsClients.length;
    }

    /**
     * Shutsdown the target
     */

  }, {
    key: 'closeTarget',
    value: function closeTarget() {
      var t = this;
      return httpGet({
        hostname: t.multiplexServer.options.remoteClientHostname,
        port: t.multiplexServer.options.remoteClientPort,
        path: "/json/close/" + t.target.id,
        method: 'GET'
      });
    }

    /**
     * Upgrade request from express
     */

  }, {
    key: 'upgrade',
    value: function upgrade(request, socket, head) {
      var t = this;
      var wss = new WebSocket.Server({ noServer: true });

      wss.handleUpgrade(request, socket, head, function (ws) {
        var devtoolsClient = new DevtoolsClient(t, ws);
        t.attach(devtoolsClient);
        wss.emit('connection', ws);
      });
    }

    /**
     * Handles a message from the remote debugger
     */

  }, {
    key: 'onMessageFromServer',
    value: function onMessageFromServer(data) {
      var t = this;

      LOG.debug('SERVER: %s...', JSON.stringify(data));

      // Map API IDs back to client'ss original ID
      if (data.id) {
        var map = t._idMap[data.id];
        if (map) {
          data.id = map.originalId;

          // Offer to domain specific proxies

          var _splitName = splitName(map.data.method),
              _splitName2 = _slicedToArray(_splitName, 2),
              domainName = _splitName2[0],
              methodName = _splitName2[1];

          var domainProxy = this._domainProxies[domainName];
          var api = domainProxy && domainProxy[methodName];
          if (api && typeof api.reply === "function") {
            var result = api.reply(map.devtoolsClient, data);
            if (result !== undefined) return result;
          }

          // Just bounce it straight on
          return map.devtoolsClient.sendMessageToClient(data);
        } else {
          LOG.error("Server message for id " + data.id + " not matched");
          return;
        }
      }

      // Handle events - offer to domain specific proxy

      var _splitName3 = splitName(data.method),
          _splitName4 = _slicedToArray(_splitName3, 2),
          domainName = _splitName4[0],
          methodName = _splitName4[1];

      var domainProxy = this._domainProxies[domainName];
      if (domainProxy) {
        var result = domainProxy.onEvent(data);
        if (result !== undefined) return result;
      }

      // Pass event on
      this.devtoolsClients.forEach(function (devtoolsClient) {
        devtoolsClient.sendMessageToClient(data, true);
      });
    }

    /**
     * Allows a DevToolsClient to send a message to the server; handles mapping the
     * client's own ID to a global one for the server
     */

  }, {
    key: 'sendMessageToServer',
    value: function sendMessageToServer(devtoolsClient, data) {
      var t = this;

      LOG.debug('CLIENT: %s...', JSON.stringify(data));

      // Offer the domain proxy the opportunity to handle the method call
      if (data.method) {
        var _splitName5 = splitName(data.method),
            _splitName6 = _slicedToArray(_splitName5, 2),
            domainName = _splitName6[0],
            methodName = _splitName6[1];

        var domainProxy = this._domainProxies[domainName];
        var api = domainProxy && domainProxy[methodName];
        if (api && typeof api.send === "function") {
          var result = api.send(devtoolsClient, data, doSend);
          if (result !== undefined) return result;
        }
      }

      // Just send it
      return doSend(data);

      function doSend(data) {
        if (data.id !== undefined) {
          var newId = t._nextCommandId++;
          t._idMap[newId] = {
            newId: newId,
            originalId: data.id,
            data: data,
            devtoolsClient: devtoolsClient
          };
          data.id = newId;
        }

        return promisify(function (cb) {
          t.ws.send(JSON.stringify(data), cb);
        });
      }
    }
  }]);

  return RemoteDebuggerProxy;
}(EventEmitter);

/**
 * This is the main server instance that clients connect to; it contains the ExpressJS server,
 * communicates with the remote debug server, and establishes RemoteDebuggerProxy instances
 * for each remote target
 *
 */

var DEFAULT_DEVTOOLS_URL = "https://chrome-devtools-frontend.appspot.com/serve_file/@4b9102f9588fb6cf639a6165fd4777658d5ade0d/inspector.html?";

var MultiplexServer = function (_EventEmitter4) {
  _inherits(MultiplexServer, _EventEmitter4);

  function MultiplexServer(options) {
    _classCallCheck(this, MultiplexServer);

    var _this8 = _possibleConstructorReturn(this, (MultiplexServer.__proto__ || Object.getPrototypeOf(MultiplexServer)).call(this));

    options = options || {};
    if (options.logging === "debug") LOG.mode = "debug";
    var remoteClient = options.remoteClient;
    if (remoteClient !== undefined) {
      var m = remoteClient.match(/^([^:]+)(:([0-9]+))?$/);
      if (m) {
        options.remoteClientHostname = m[1];
        options.remoteClientPort = m[3];
      } else {
        throw new Error("Cannot interpret remoteClient - found " + remoteClient + ", expected something like 'localhost:9222'");
      }
    }
    _this8.options = {
      listenPort: options.listenPort || 9223,
      remoteClientHostname: options.remoteClientHostname || "localhost",
      remoteClientPort: options.remoteClientPort || 9222
    };
    _this8.options.remoteClient = _this8.options.remoteClientHostname + ":" + _this8.options.remoteClientPort;
    return _this8;
  }

  /**
   * Starts the HTTP server
   */

  _createClass(MultiplexServer, [{
    key: 'listen',
    value: function listen() {
      var _this9 = this;

      var t = this;

      var app = Express();

      function reportHttpError(req, err) {
        LOG.error("Error in " + req.method + " " + req.originalUrl + ": " + err);
      }

      /*
       * Web page that provides links to debug
       */
      (function () {
        // https://chrome-devtools-frontend.appspot.com/serve_file/@4b9102f9588fb6cf639a6165fd4777658d5ade0d/inspector.html?ws=localhost:9223/devtools/page/1e371c5b-25ef-4ee9-9ef6-3ca11d9d59ee&remoteFrontend=true

        var template = Dot.template('<html><body>\n  <h1>Headless proxy</h1>\n  <ul>\n    {{~ it.multiplex.targets :target }}\n          <li>\n            <a href="{{= it.url(target) }}">\n              {{= it.title(target) }}\n            </a>\n          </li>\n    {{~}}\n  </ul>\n</body></html>');

        app.get('/', function (req, res) {
          t.refreshTargets().then(function () {
            res.send(template({
              multiplex: t,
              DEFAULT_DEVTOOLS_URL: DEFAULT_DEVTOOLS_URL,
              url: function url(target) {
                return DEFAULT_DEVTOOLS_URL + target.webSocketDebuggerUrl.replace(/^ws:\/\//, "ws=/") + "&remoteFrontend=true";
              },
              title: function title(target) {
                var str = target.title;
                if (target.autoClose) str += " (set to auto-close)";
                return str;
              }
            }));
          }).catch(reportHttpError.bind(this, req));
        });
      })();

      function getContentType(response) {
        var contentType = response.headers["content-type"];
        if (contentType) {
          var pos = contentType.indexOf(';');
          contentType = contentType.substring(0, pos);
        }
        return contentType;
      }

      // Gets JSON from the remote server
      function getJson(path) {
        return httpGet({
          hostname: t.options.remoteClientHostname,
          port: t.options.remoteClientPort,
          path: path,
          method: 'GET'
        }).then(function (obj) {
          var contentType = getContentType(obj.response);
          if (contentType !== "application/json") LOG.warn("Expecting JSON from " + path + " but found wrong content type: " + contentType);

          try {
            return JSON.parse(obj.data);
          } catch (ex) {
            LOG.warn("Cannot parse JSON returned from " + path);
            return null;
          }
        });
      }

      // Gets data from the remote server and copies it to the client
      function copyToClient(req, res) {
        return httpGet({
          hostname: t.options.remoteClientHostname,
          port: t.options.remoteClientPort,
          path: req.originalUrl,
          method: 'GET'
        }).then(function (obj) {
          var contentType = getContentType(obj.response);
          if (contentType) res.set("Content-Type", contentType);
          res.send(obj.data);
        });
      }

      // REST API: list targets
      app.get(["/json", "/json/list"], function (req, res) {
        t.refreshTargets().then(function () {
          res.set("Content-Type", "application/json");
          res.send(JSON.stringify(t.targets, null, 2));
        }).catch(reportHttpError.bind(_this9, req));
      });

      // REST API: create a new target
      app.get('/json/new', function (req, res) {
        return getJson("/json/new").then(function (target) {
          if (target) target = t._addTarget(target);
          res.set("Content-Type", "application/json");
          res.send(JSON.stringify(target, null, 2));
        });
      });

      // REST API: close a target
      app.get('/json/close/*', function (req, res) {
        return httpGet({
          hostname: t.options.remoteClientHostname,
          port: t.options.remoteClientPort,
          path: req.originalUrl,
          method: 'GET'
        }).then(function (obj) {
          var id = req.originalUrl.match(/\/json\/close\/(.*)$/)[1];
          var proxy = t.proxies[id];
          if (proxy) proxy.close();

          var contentType = getContentType(obj.response);
          if (contentType) res.set("Content-Type", contentType);
          res.send(obj.data);
        });
      });

      // REST API: auto-close a target
      app.get('/json/auto-close/*', function (req, res) {
        t.refreshTargets().then(function () {
          var id = req.originalUrl.match(/\/json\/auto-close\/(.*)$/)[1];
          var proxy = t._proxies[id];
          if (proxy) {
            if (proxy.isUnused()) {
              proxy.closeTarget().close();
              LOG.info("Closing target " + id + " due to /json/auto-close");
              res.send("Target is closing");
            } else {
              proxy.autoClose = true;
              t.targetsById[id].autoClose = true;
              LOG.info("Marking target " + id + " to auto close");
              res.send("Target set to auto close");
            }
          } else {
            var target = t.targetsById[id];
            if (target) {
              target.autoClose = true;
              LOG.info("Marking target " + id + " to auto close after first use");
              res.send("Target will close after first use");
            } else res.status(500).send("Unrecognised target id " + id);
          }
        });
      });

      // REST API: get version numbers
      app.get('/json/version', function (req, res) {
        return getJson(req.originalUrl).then(function (json) {
          json["Chrome-Remote-Multiplex-Version"] = PACKAGE.version;
          res.set("Content-Type", "application/json");
          res.send(JSON.stringify(json, null, 2));
        });
      });

      app.get('/json/protocol', copyToClient);
      app.get('/json/activate', copyToClient);

      var webServer = this.webServer = Http.createServer(app);
      var proxies = this._proxies = {};

      // Upgrade the connection from ExpressJS
      webServer.on('connection', function (socket) {
        LOG.debug("WEB: connection");
      });
      webServer.on('request', function (req, res) {
        LOG.debug("WEB: request");
      });
      webServer.on('upgrade', function (request, socket, head) {
        LOG.debug("WEB: upgrade");
        var pathname = Url.parse(request.url).pathname;
        var m = pathname.match(/\/([^\/]+)$/);
        var uuid = m && m[1] || null;

        if (!uuid) {
          LOG.error("Cannot find UUID in " + pathname);
          socket.destroy();
          return;
        }

        t.refreshTargets().then(function () {
          var target = t.targetsById[uuid];
          if (!target) {
            LOG.error("Cannot find target for " + uuid);
            socket.destroy();
            return;
          }

          var proxy = proxies[uuid];
          if (!proxy) {
            if (!target.webSocketDebuggerUrl || target.type !== "page") {
              LOG.error("Target " + uuid + " not eligable for connection");
              socket.destroy();
              return;
            }
            proxy = proxies[uuid] = new RemoteDebuggerProxy(t, target);
            proxy.connect().then(function () {
              return proxy.upgrade(request, socket, head);
            });
            proxy.on('close', function () {
              delete t.targetsById[uuid];
              for (var i = 0; i < t.targets.length; i++) {
                if (t.targets[i].id === uuid) {
                  t.targets.splice(i, 1);
                  break;
                }
              }
            });
            if (target.autoClose) proxy.autoClose = true;
          } else {
            return proxy.upgrade(request, socket, head);
          }
        });
      });

      return new Promise(function (resolve, reject) {
        LOG.debug("Starting Express server");
        webServer.listen(t.options.listenPort, resolve);
      });
    }

    /**
     * Shuts down
     */

  }, {
    key: 'close',
    value: function close() {
      this.webServer.close();
      this.webServer = null;
    }

    /**
     * Rediscovers all targets at the remote server that can be connected to
     */

  }, {
    key: 'refreshTargets',
    value: function refreshTargets() {
      var t = this;
      return httpGet({
        hostname: t.options.remoteClientHostname,
        port: t.options.remoteClientPort,
        path: '/json',
        method: 'GET'
      }).then(function (obj) {
        var json = null;
        if (!obj.data) {
          LOG.debug("No data received from " + t.options.remoteClient);
          return;
        }
        try {
          json = JSON.parse(obj.data);
        } catch (ex) {
          LOG.error("Error while parsing JSON from " + t.options.remoteClient + ": " + ex);
          return;
        }
        var oldTargets = t.targets;
        var oldTargetsById = t.targetsById || {};
        t.targets = json;
        t.targetsById = {};
        for (var i = 0; i < t.targets.length; i++) {
          var target = t.targets[i];
          t.targets[i] = t._addTarget(target, oldTargetsById[target.id]);
        }
      });
    }

    /**
     * Adds a target
     */

  }, {
    key: '_addTarget',
    value: function _addTarget(src, target) {
      var RESERVED_KEYWORDS = ["description", "id", "title", "type", "url", "devtoolsFrontendUrl", "webSocketDebuggerUrl", "originalDevtoolsFrontendUrl", "originalWebSocketDebuggerUrl"];

      var t = this;
      if (!target) target = {};

      for (var name in src) {
        target[name] = src[name];
      }if (target.devtoolsFrontendUrl) target.originalDevtoolsFrontendUrl = target.devtoolsFrontendUrl;
      target.devtoolsFrontendUrl = "/devtools/inspector.html?ws=localhost:" + t.options.listenPort + "/devtools/page/" + target.id;
      if (target.webSocketDebuggerUrl) target.originalWebSocketDebuggerUrl = target.webSocketDebuggerUrl;
      target.webSocketDebuggerUrl = "ws://localhost:" + t.options.listenPort + "/devtools/page/" + target.id;
      target.title += " (proxied)";

      if (target.numberOfClients === undefined) target.numberOfClients = 0;

      return t.targetsById[target.id] = target;
    }
  }]);

  return MultiplexServer;
}(EventEmitter);

/**
 * API for remote control of the MultiplexServer
 */

exports.default = MultiplexServer;

var ClientApi = exports.ClientApi = function () {
  function ClientApi(options) {
    _classCallCheck(this, ClientApi);

    options = options || {};
    var remoteClient = options.remoteClient;
    if (remoteClient !== undefined) {
      var m = remoteClient.match(/^([^:]+)(:([0-9]+))?$/);
      if (m) {
        options.remoteClientHostname = m[1];
        options.remoteClientPort = m[3];
      } else {
        throw new Error("Cannot interpret remoteClient - found " + remoteClient + ", expected something like 'localhost:9222'");
      }
    }
    this.options = {
      remoteClientHostname: options.remoteClientHostname || "localhost",
      remoteClientPort: options.remoteClientPort || 9222
    };
    this.options.remoteClient = this.options.remoteClientHostname + ":" + this.options.remoteClientPort;
  }

  /**
   * Enables auto close for a specific target
   */

  _createClass(ClientApi, [{
    key: 'autoClose',
    value: function autoClose(id) {
      return httpGet({
        hostname: this.options.remoteClientHostname,
        port: this.options.remoteClientPort,
        path: '/json/auto-close/' + id,
        method: 'GET'
      }).then(function (obj) {
        return obj && obj.data;
      });
    }
  }]);

  return ClientApi;
}();

/**
 * Does a simple HTTP GET
 * @return Promise - payload is the response context
 */

function httpGet(options) {
  return new Promise(function (resolve, reject) {
    var req = Http.request(options, function (response) {
      var str = '';

      response.on('data', function (chunk) {
        str += chunk;
      });

      response.on('end', function () {
        resolve({ data: str, response: response });
      });
    });
    req.on('error', reject);
    req.end();
  });
}

/**
 * Simple promisify
 */
function promisify(fn) {
  return new Promise(function (resolve, reject) {
    fn(function (err, value) {
      if (err) reject(err);else resolve(value);
    });
  });
}

/**
 * Splits a string into domain and method
 */
function splitName(method) {
  var pos = method.indexOf('.');
  if (pos < 0) return [null, method];
  var domainName = method.substring(0, pos);
  var methodName = method.substring(pos + 1);
  return [domainName, methodName];
}
//# sourceMappingURL=multiplex.js.map


/*
 * Taken and modified from https://github.com/bubkoo/ascii-progress.
 */

var ansi = require('ansi.js');
var newlineEvent = require('on-new-line');
var stripAnsi = require('strip-ansi');

var stream = process.stdout;

stream.rows = stream.rows || 40;
stream.columns = stream.columns || 80;
newlineEvent(stream);

var placeholder = '\uFFFC';
var rendering = false;
var instances = [];
var cursorRow;
var existing = false;

function beginUpdate() {
    rendering = true;
}

function endUpdate() {
    rendering = false;
}

function isUpdating() {
    return rendering === true;
}

function init(cursorPosition) {
    cursorRow = cursorPosition.row;
}

stream.on('before:newlines', function (count) {
    updateRows(count);

    if (!existing) {
        cursorRow = cursorRow + count;
        if (stream.rows < cursorRow) {
            cursorRow = stream.rows;
        }
    }
});

function updateRows(count) {

    if (isUpdating() || instances.length === 0) {
        return;
    }

    var current = {row: cursorRow, col: 1};
    if (!current) {
        return;
    }

    // did not reach the end, the screen need not scroll up
    if (current.row < stream.rows) {
        return;
    }

    var minRow = 1;
    var cursor = instances[0].cursor;

    beginUpdate();

    instances.forEach(function (instance) {

        if (instance.rendered && (!instance.completed || instance.tough)) {
            // clear the rendered bar
            instance.clear();
            instance.origin.row = Math.max(minRow, instance.origin.row - count);
            minRow += instance.rows;
        } else if (instance.rendered
            && instance.completed
            && !instance.tough
            && !instance.archived
            && !instance.clean) {

            instance.clear();
            instance.origin.row = -instance.rows;
            instance.colorize(instance.output);
            instance.archived = true;
        }
    });

    // append empty row for the new lines, the screen will scroll up,
    // then we can move the bars to their's new position.
    cursor
        .moveTo(current.row, current.col)
        .write(repeatChar(count, '\n'));

    instances.forEach(function (instance) {
        if (instance.rendered && (!instance.completed || instance.tough)) {
            instance.colorize(instance.output);
        }
    });

    cursor.moveTo(current.row - count, current.col);

    endUpdate();
}

function ProgressBar(options) {

    options = options || {};

    this.cursor = ansi(stream);
    this.total = options.total || 100;
    this.current = options.current || 0;
    this.width = options.width || 60;
    this.fixedWidth = options.fixedWidth;

    if (typeof this.width === 'string') {
        if (this.width.endsWith('%')) {
            this.width = parseFloat(this.width) / 100 % 1;
        } else {
            this.width = parseFloat(this.width);
        }
    }

    this.tough = !!options.tough;
    this.clean = !!options.clean;
    this.chars = {
        blank: options.blank || '—',
        filled: options.filled || '▇'
    };

    // callback on completed
    this.callback = options.callback;

    this.setSchema(options.schema);
    this.snoop();

    instances.push(this);
}

// exports
// -------

exports.ProgressBar = ProgressBar;
exports.init = init;

// proto
// -----

ProgressBar.prototype.setSchema = function (schema, refresh) {
    this.schema = schema || ' [:bar] :current/:total :percent :elapseds :etas';

    if (refresh) {
        existing = true;
        this.compile(refresh);
        existing = false;
    }
};

ProgressBar.prototype.tick = function (delta, tokens) {

    var type = typeof delta;

    if (type === 'object') {
        tokens = delta;
        delta = 1;
    } else if (type === 'undefined') {
        delta = 1;
    } else {
        delta = parseFloat(delta);
        if (isNaN(delta) || !isFinite(delta)) {
            delta = 1;
        }
    }

    if (this.completed && delta >= 0) {
        return;
    }

    if (!this.start) {
        this.start = new Date;
    }

    this.current += delta;
    this.compile(tokens);
    this.snoop();
};

ProgressBar.prototype.update = function (ratio, tokens, created) {
    existing = !created;
    var completed = Math.floor(ratio * this.total);
    var delta = completed - this.current;

    this.tick(delta, tokens);
    existing = false;
};

ProgressBar.prototype.compile = function (tokens) {

    var ratio = this.current / this.total;

    ratio = Math.min(Math.max(ratio, 0), 1);

    var chars = this.chars;
    var schema = this.schema;
    var percent = ratio * 100;
    var elapsed = new Date - this.start;

    var eta;
    if (this.current <= 0) {
        eta = '-';
    } else {
        eta = percent === 100 ? 0 : elapsed * this.total / this.current;
        eta = formatTime(eta);
    }

    var output = schema
        .replace(/:total/g, this.total)
        .replace(/:current/g, this.current)
        .replace(/:elapsed/g, formatTime(elapsed))
        .replace(/:eta/g, eta)
        .replace(/:percent/g, toFixed(percent, 0) + '%');

    if (tokens && typeof tokens === 'object') {
        for (var key in tokens) {
            if (tokens.hasOwnProperty(key)) {
                output = output.replace(new RegExp(':' + key, 'g'), ('' + tokens[key]) || placeholder);
            }
        }
    }

    var raw = bleach(output);
    var cols = process.stdout.columns;
    var width = this.width;

    width = width < 1 ? cols * width : width;
    if (!this.fixedWidth) {
        width = Math.min(width, Math.max(0, cols - bareLength(raw)));
    }

    var length = Math.round(width * ratio);
    var filled = repeatChar(length, chars.filled);
    var blank = repeatChar(width - length, chars.blank);

    raw = combine(raw, filled, blank, true);
    output = combine(output, filled, blank, false);

    // without color and font styles
    this.raw = raw;
    // row count of the progress bar
    this.rows = raw.split('\n').length;

    this.render(output);
};

ProgressBar.prototype.render = function (output) {

    if (this.output === output) {
        return;
    }

    var current = {row: cursorRow, col: 1};
    if (!current) {
        return;
    }

    beginUpdate();

    this.savePos = current;
    if (!this.origin) {
        this.origin = current;
    }

    if (this.origin.row === stream.rows) {

        this.cursor.write(repeatChar(this.rows, '\n'));

        instances.forEach(function (instance) {
            if (instance.origin) {
                instance.origin.row -= this.rows;
            }
        }, this);
    }

    this.clear();
    this.colorize(output);

    // move the cursor to the current position.
    if (this.rendered) {
        this.cursor.moveTo(current.row, current.col);
    }

    this.output = output;
    this.rendered = true;

    endUpdate();
};

ProgressBar.prototype.colorize = function (output) {

    var charsLeft = process.stdout.columns - 1;

    function writeChars(chars) {
        if (!charsLeft) {
            return;
        }

        if (stripAnsi(chars).length > charsLeft) {
            var courser = 0;
            var chunk = "";
            while(stripAnsi(chunk).length <= charsLeft) {
                chunk += chars.charAt(courser);
                courser++;
            }
            cursor.write(chunk);
        } else {
            cursor.write(chars);
        }
        charsLeft = Math.max(0, charsLeft - stripAnsi(chars).length);
    }

    var cursor = this.cursor;
    var parts = output.split(/(\.[A-Za-z]+)/g);
    var content = '';
    var matches = [];

    cursor.moveTo(this.origin.row, this.origin.col);

    function write() {

        //console.log(content)
        //console.log(matches)

        var hasFg = false;
        var hasBg = false;
        var gradient = null;

        matches.forEach(function (match) {

            if (match.method === 'gradient') {
                gradient = match;
                return;
            }

            var host = match.isBg
                ? cursor.bg : match.isFont
                    ? cursor.font
                    : cursor.fg;

            if (match.isBg) {
                hasBg = true;
            } else {
                hasFg = true;
            }

            host[match.method]();
        });

        content = content.replace(new RegExp(placeholder, 'g'), '');

        if (content) {

            if (gradient) {

                var color1 = gradient.color1;
                var color2 = gradient.color2;

                for (var i = 0, l = content.length; i < l; i++) {

                    var color = i === 0
                        ? color1 : i === l - 1
                            ? color2
                            : interpolate(color1, color2, (i + 1) / l);

                    cursor.fg.rgb(color.r, color.g, color.b);
                    writeChars(content[i]);
                    cursor.fg.reset();
                }
            } else {
                writeChars(content);
            }
        }

        // reset font style
        matches.forEach(function (match) {
            if (match.isFont) {
                cursor.font['reset' + ucFirst(match.method)]();
            }
        });

        // reset foreground
        if (hasFg) {
            cursor.fg.reset();
        }

        // reset background
        if (hasBg) {
            cursor.bg.reset();
        }

        matches = [];
        content = '';
    }

    for (var i = 0, l = parts.length; i < l; i++) {

        var part = parts[i];
        var match = null;

        if (!part) {
            continue;
        }

        if (part.startsWith('.')) {

            if (part === '.gradient') {
                if (parts[i + 1]) {
                    match = parseGradient(parts[i + 1]);

                    parts[i + 1] = parts[i + 1].replace(/^\((.+),(.+)\)/, '');
                }
            } else {
                match = parseMethod(cursor, part);
            }
        }

        if (match) {
            if (match.suffix) {
                if (i < l - 1) {
                    parts[i + 1] += match.suffix;
                } else {
                    // the last one
                    writeChars(match.suffix);
                }
            }

            matches.push(match);
        } else {

            if (matches.length) {
                write();
            }

            content += part;
        }
    }

    write();

    cursor.write('\n');
};

ProgressBar.prototype.clear = function () {

    if (this.output) {
        this.cursor.moveTo(this.origin.row, this.origin.col);
        for (var i = 0; i < this.rows; i++) {
            this.cursor
                .eraseLine()
                .moveDown();
        }
        this.cursor.moveTo(this.origin.row, this.origin.col);
    }
};

ProgressBar.prototype.snoop = function () {

    this.completed = this.current >= this.total;

    if (this.completed) {
        this.terminate();
    }

    return this.completed;
};

ProgressBar.prototype.terminate = function () {

    if (this.clean && this.rendered) {
        this.clear();
        //var lines = this.raw.split('\n');
        //for (var i = 0; i < this.rows; i++) {
        //  this.cursor
        //    .deleteLine()
        //    .moveDown();
        //}
    }

    this.callback && this.callback(this);
    if (this.savePos) {
        this.cursor.moveTo(this.savePos.row, this.savePos.col);
    }
};

// helpers
// -------

function toFixed(value, precision) {

    var power = Math.pow(10, precision);

    return (Math.round(value * power) / power).toFixed(precision);
}

function formatTime(ms) {
    return isNaN(ms) || !isFinite(ms)
        ? '0.0'
        : toFixed(ms / 1000, 1);
}

function lcFirst(str) {
    return str.charAt(0).toLowerCase() + str.substring(1);
}

function ucFirst(str) {
    return str.charAt(0).toUpperCase() + str.substring(1);
}

function repeatChar(count, char) {
    return new Array(parseInt(count, 10) + 1).join(char);
}

function parseMethod(cursor, str) {

    str = str.substr(1);

    return parseColor(cursor, str)
        || parseFont(cursor, str)
        || parseGradient(str);
}

function parseColor(cursor, str) {

    var match = str.match(/^(bgR|r)ed/)
        || str.match(/^(bgB|b)lue/)
        || str.match(/^(bgC|c)yan/)
        || str.match(/^(bgG|g)rey/)
        || str.match(/^(bgW|w)hite/)
        || str.match(/^(bgB|b)lack/)
        || str.match(/^(bgG|g)reen/)
        || str.match(/^(bgY|y)ellow/)
        || str.match(/^(bgM|m)agenta/)
        || str.match(/^(bgB|b)right(Black|Red|Green|Yellow|Blue|Magenta|Cyan|White)/);

    if (match) {

        var method = match[0];
        var suffix = str.substr(method.length);
        var isBg = method.startsWith('bg');

        if (isBg) {
            method = lcFirst(method.substr(2));
        }

        if (typeof cursor[method] === 'function') {
            return {
                isBg: isBg,
                method: method,
                suffix: suffix
            };
        }
    }
}

function parseFont(cursor, str) {

    var match = str.match(/^bold|italic|underline|inverse/);
    if (match) {

        var method = match[0];
        var suffix = str.substr(method.length);

        if (typeof cursor[method] === 'function') {
            return {
                isFont: true,
                method: method,
                suffix: suffix
            };
        }
    }
}

function parseGradient(str) {

    var match = str.match(/^\((.+),(.+)\)/);
    if (match) {

        var color1 = match[1].trim();
        var color2 = match[2].trim();

        color1 = color1.startsWith('#') ? hex2rgb(color1) : name2rgb(color1);
        color2 = color1.startsWith('#') ? hex2rgb(color2) : name2rgb(color2);

        if (color1 && color2) {
            return {
                method: 'gradient',
                color1: color1,
                color2: color2
            };
        }
    }
}

function interpolate(color1, color2, percent) {

    return {
        r: atPercent(color1.r, color2.r, percent),
        g: atPercent(color1.g, color2.g, percent),
        b: atPercent(color1.b, color2.b, percent)
    };
}

function atPercent(a, b, percent) {
    return a + Math.round((b - a) * percent);
}

function hex2rgb(color) {

    var c = color.substring(1);
    var r = c.substring(0, 2);
    var g = c.substring(2, 4);
    var b = c.substring(4, 6);

    return {
        r: parseInt(r, 16),
        g: parseInt(g, 16),
        b: parseInt(b, 16)
    };
}

function name2rgb(name) {
    var hex = {
        red: '#ff0000',
        blue: '#0000ff',
        cyan: '#00ffff',
        grey: '#808080',
        white: '#ffffff',
        black: '#000000',
        green: '#008000',
        yellow: '#ffff00',
        magenta: '#ff00ff'
    }[name];

    return hex ? hex2rgb(hex) : null;
}

function bleach(output) {
    return output
        .replace(/\.(bgR|r)ed/g, '')
        .replace(/\.(bgB|b)lue/g, '')
        .replace(/\.(bgC|c)yan/g, '')
        .replace(/\.(bgG|g)rey/g, '')
        .replace(/\.(bgW|w)hite/g, '')
        .replace(/\.(bgB|b)lack/g, '')
        .replace(/\.(bgG|g)reen/g, '')
        .replace(/\.(bgY|y)ellow/g, '')
        .replace(/\.(bgM|m)agenta/g, '')
        // bright
        .replace(/\.(bgB|b)right(Black|Red|Green|Yellow|Blue|Magenta|Cyan|White)/g, '')
        // font style
        .replace(/\.bold|italic|underline|inverse/g, '')
        // gradient
        .replace(/\.gradient\((.+),(.+)\)/g, '');
}

function combine(output, filled, blank, bare) {

    var bar = filled + blank;

    if (!bare) {
        bar = bar || placeholder;
        blank = blank || placeholder;
        filled = filled || placeholder;
    }

    return output
        .replace(/:filled/g, filled)
        .replace(/:blank/g, blank)
        .replace(/:bar/g, bar);
}

function bareLength(output) {
    var str = output
        .replace(/:filled/g, '')
        .replace(/:blank/g, '')
        .replace(/:bar/g, '');

    return str.length;
}


/*
 * grunt
 * http://gruntjs.com/
 *
 * Copyright (c) 2016 "Cowboy" Ben Alman
 * Licensed under the MIT license.
 * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
 */

'use strict';

// Nodejs libs.
var spawn = require('child_process').spawn;
var nodeUtil = require('util');
var path = require('path');

// The module to be exported.
var util = module.exports = {};

util.namespace = require('getobject');

// External libs.
util.hooker = require('hooker');
util.async = require('async');
// Dont pollute other lodash: https://github.com/gruntjs/grunt-legacy-util/issues/17
var _ = util._ = require('lodash').runInContext();
var which = require('which').sync;
// Instead of process.exit. See https://github.com/cowboy/node-exit
util.exit = require('exit');

// Mixin Underscore.string methods.
_.str = require('underscore.string');
_.mixin(_.str.exports());

// Return a function that normalizes the given function either returning a
// value or accepting a "done" callback that accepts a single value.
util.callbackify = function(fn) {
  return function callbackable() {
    // Invoke original function, getting its result.
    var result = fn.apply(this, arguments);
    // If the same number or less arguments were specified than fn accepts,
    // assume the "done" callback was already handled.
    var length = arguments.length;
    if (length === fn.length) { return; }
    // Otherwise, if the last argument is a function, assume it is a "done"
    // callback and call it.
    var done = arguments[length - 1];
    if (typeof done === 'function') { done(result); }
  };
};

// Create a new Error object, with an origError property that will be dumped
// if grunt was run with the --debug=9 option.
util.error = function(err, origError) {
  if (!nodeUtil.isError(err)) { err = new Error(err); }
  if (origError) { err.origError = origError; }
  return err;
};

// The line feed char for the current system.
util.linefeed = process.platform === 'win32' ? '\r\n' : '\n';

// Normalize linefeeds in a string.
util.normalizelf = function(str) {
  return str.replace(/\r\n|\n/g, util.linefeed);
};

// What "kind" is a value?
// I really need to rework https://github.com/cowboy/javascript-getclass
var kindsOf = {};
'Number String Boolean Function RegExp Array Date Error'.split(' ').forEach(function(k) {
  kindsOf['[object ' + k + ']'] = k.toLowerCase();
});
util.kindOf = function(value) {
  // Null or undefined.
  if (value == null) { return String(value); }
  // Everything else.
  return kindsOf[kindsOf.toString.call(value)] || 'object';
};

// Coerce something to an Array.
util.toArray = _.toArray;

// Return the string `str` repeated `n` times.
util.repeat = function(n, str) {
  return new Array(n + 1).join(str || ' ');
};

// Given str of "a/b", If n is 1, return "a" otherwise "b".
util.pluralize = function(n, str, separator) {
  var parts = str.split(separator || '/');
  return n === 1 ? (parts[0] || '') : (parts[1] || '');
};

// Recurse through objects and arrays, executing fn for each non-object.
util.recurse = function(value, fn, fnContinue) {
  function recurse(value, fn, fnContinue, state) {
    var error;
    if (state.objs.indexOf(value) !== -1) {
      error = new Error('Circular reference detected (' + state.path + ')');
      error.path = state.path;
      throw error;
    }
    var obj, key;
    if (fnContinue && fnContinue(value) === false) {
      // Skip value if necessary.
      return value;
    } else if (util.kindOf(value) === 'array') {
      // If value is an array, recurse.
      return value.map(function(item, index) {
        return recurse(item, fn, fnContinue, {
          objs: state.objs.concat([value]),
          path: state.path + '[' + index + ']',
        });
      });
    } else if (util.kindOf(value) === 'object' && !Buffer.isBuffer(value)) {
      // If value is an object, recurse.
      obj = {};
      for (key in value) {
        obj[key] = recurse(value[key], fn, fnContinue, {
          objs: state.objs.concat([value]),
          path: state.path + (/\W/.test(key) ? '["' + key + '"]' : '.' + key),
        });
      }
      return obj;
    } else {
      // Otherwise pass value into fn and return.
      return fn(value);
    }
  }
  return recurse(value, fn, fnContinue, {objs: [], path: ''});
};

// Spawn a child process, capturing its stdout and stderr.
util.spawn = function(opts, done) {
  // Build a result object and pass it (among other things) into the
  // done function.
  var callDone = function(code, stdout, stderr) {
    // Remove trailing whitespace (newline)
    stdout = _.rtrim(stdout);
    stderr = _.rtrim(stderr);
    // Create the result object.
    var result = {
      stdout: stdout,
      stderr: stderr,
      code: code,
      toString: function() {
        if (code === 0) {
          return stdout;
        } else if ('fallback' in opts) {
          return opts.fallback;
        } else if (opts.grunt) {
          // grunt.log.error uses standard out, to be fixed in 0.5.
          return stderr || stdout;
        }
        return stderr;
      }
    };
    // On error (and no fallback) pass an error object, otherwise pass null.
    done(code === 0 || 'fallback' in opts ? null : new Error(stderr), result, code);
  };

  var cmd, args;
  var pathSeparatorRe = /[\\\/]/g;
  if (opts.grunt) {
    cmd = process.execPath;
    args = process.execArgv.concat(process.argv[1], opts.args);
  } else {
    // On Windows, child_process.spawn will only file .exe files in the PATH,
    // not other executable types (grunt issue #155).
    try {
      if (!pathSeparatorRe.test(opts.cmd)) {
        // Only use which if cmd has no path component.
        cmd = which(opts.cmd);
      } else {
        cmd = opts.cmd.replace(pathSeparatorRe, path.sep);
      }
    } catch (err) {
      callDone(127, '', String(err));
      return;
    }
    args = opts.args || [];
  }

  var child = spawn(cmd, args, opts.opts);
  var stdout = new Buffer('');
  var stderr = new Buffer('');
  if (child.stdout) {
    child.stdout.on('data', function(buf) {
      stdout = Buffer.concat([stdout, new Buffer(buf)]);
    });
  }
  if (child.stderr) {
    child.stderr.on('data', function(buf) {
      stderr = Buffer.concat([stderr, new Buffer(buf)]);
    });
  }
  child.on('close', function(code) {
    callDone(code, stdout.toString(), stderr.toString());
  });
  return child;
};