[
  {
    "path": ".babelrc",
    "content": "{\n  \"blacklist\": [\"useStrict\"]\n}"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: http://EditorConfig.org\n\nroot = true;\n\n[*]\n#  Ensure there's no lingering whitespace\ntrim_trailing_whitespace = true\n# Ensure a newline at the end of each file\ninsert_final_newline = true\n\n[*.js]\n# Unix-style newlines\nend_of_line = lf\ncharset = utf-8\nindent_style = space\nindent_size = 2"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"rules\": {\n    \"strict\": 0,\n    \"quotes\": [2, \"single\"]\n  },\n  \"env\": {\n    \"browser\": false,\n    \"node\": true\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\n# Commenting this out is preferred by some people, see\n# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-\nnode_modules\nbower_components\ncoverage\ntmp\n\n# Users Environment Variables\n.lock-wscript\n"
  },
  {
    "path": ".jscsrc",
    "content": "{\n  \"preset\": \"google\",\n  \"maximumLineLength\": null,\n  \"esnext\": true\n}\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - \"0.10\"\n  - \"0.12\"\n  - \"io.js\"\nsudo: false\nscript: \"gulp coverage\"\nafter_success:\n  - npm install -g codeclimate-test-reporter\n  - codeclimate-test-reporter < coverage/lcov.info\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "### [0.0.1](https://github.com/risq/investigator/releases/tag/v0.0.1)\n\n- The first release"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 risq <valentin.ledrapier@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# investigator\nInteractive and asynchronous logging tool for Node.js. An easier way to log & debug complex requests directly from the command line. Still experimental !\n\n![investigator](https://cloud.githubusercontent.com/assets/5665322/10861471/d38bedda-7f80-11e5-9bb7-19801c14c961.gif)\n\n## Usage\n#### Nodes\n`investigator` uses a node based logging system. Log nodes (*agents*) can be nested to help organizing the different steps, synchronous or not, of the process. An `agent` is defined by its name and can be retrieved at any time in the scope of its parent agent.\n\n![investigator-nodes](https://cloud.githubusercontent.com/assets/5665322/10861540/267ff6e6-7f84-11e5-847a-5b7d395dfb34.png)\n\n```js\nimport {agent} from 'investigator';\n\nconst requestAgent = agent('request');\nconst getUserAgent = requestAgent.child('getUser')\n  .log('Retrieving user from db...');\n\n// ...\n\ngetUserAgent.success('Done !');\n// Or: requestAgent.child('getUser').success('Done !');\n```\n\n#### Asynchronous logging\n`async` agents are particular nodes which may be resolved or rejected to provide a feedback of their fulfillment.\n\n![investigator-async](https://cloud.githubusercontent.com/assets/5665322/10861606/e00908b2-7f86-11e5-862a-ab56505d3ee3.png)\n\n```js\nimport {agent} from 'investigator';\n\nconst requestAgent = agent('request');\n\n// Creates an async child agent\nconst getUserAgent = requestAgent.async('getUser')\n  .log('Retrieving user from db...');\n\nmyAsynchronousFunction().then(() => {\n  getUserAgent.resolve('Done !');\n}).catch((err) => {\n  getUserAgent.reject(err);\n});\n```\n\n#### Inspector\n`investigator` provides an `inspector` module to allow deep object logging directly in the command line interface, like a browser devtools inspector. It also displays the current stack trace of each log.\n\n![investigator-inspector](https://cloud.githubusercontent.com/assets/5665322/10861607/e00c7506-7f86-11e5-8bd8-d3ae7a072c9d.png)\n\n## Installing\nUse `npm install investigator` to install locally. See [Usage](#usage) and [API Reference](#api-reference) for more information.\n\n## Shortcuts\nIn the command line interface, the following shortcuts are available:\n- Scroll up and down with `up arrow`, `down arrow`, or mouse wheel. You may also click on a row to select it.\n- Open **Inspector** with `i` (inspect the currently selected row).\n- Scroll to bottom with `b`\n- Enable auto-scroll with `s`. Disable by pressing an arrow key.\n\n## Testing & developing\nClone the project with `git clone git@github.com:risq/investigator.git`.\n\nInstall dependencies with `npm install`.\n\nLaunch the example with `node examples/index.js`.\n\nYou can build the project (transpiling to ES5) with `npm run build`.\n\n## TODO (non-exhaustive list)\n- [ ] Log as traditional `console.log` (or use a multi-transport logging lib like [winston](https://github.com/winstonjs/winston)), then parse output stream in real time (or from a log file) with `investigator`.\n- [ ] Improve UI, navigation & controls in the CLI.\n- [ ] Add some performance monitoring.\n- [ ] Improve CLI performance for long time logging (avoid memory leaks).\n- [ ] Allow client-side logging via WebSockets.\n\n## API Reference\n### Investigator\n##### `investigator.agent(String name [, data])` -> `Agent`\nCreates a new *root* agent, with a given `name`. Data parameters of any type can also be passed to be logged into the command line interface.\n\n```js\nimport {agent} from 'investigator';\n\nonRequest(req, res) {\n  const requestAgent = agent('request', req.id, req.url);\n}\n```\n\n### Agent\n##### `agent.log(data [, data])` -> `Agent`\nLog passed data parameters under the given agent node. Returns the same agent (so it can be chained).\n\n```js\nimport {agent} from 'investigator';\n\nonRequest(req, res) {\n  const requestAgent = agent('request', req.id, req.url);\n  requestAgent.log('Hello')\n    .log('World');\n}\n```\n\n##### `agent.success(data [, data])` -> `Agent`\nLog passed data parameters under the given agent node, as a success (displayed in green). Returns the same agent (so it can be chained).\n\n##### `agent.warn(data [, data])` -> `Agent`\nLog passed data parameters under the given agent node, as a warning (displayed in yellow). Returns the same agent (so it can be chained).\n\n##### `agent.error(data [, data])` -> `Agent`\nLog passed data parameters under the given agent node, as an error (displayed in red). Returns the same agent (so it can be chained).\n\n##### `agent.child(name [, data])` -> `Agent`\nReturns a child of the current agent, defined by its name. If a child with the given name already exists on the agent, it will be returned. If not, it will be created.\n\nData objects can be passed as parameters and will be logged on the child's context.\n\n```js\nimport {agent} from 'investigator';\n\nonRequest(req, res) {\n  const requestAgent = agent('request', req.id, req.url);\n\n  if (req.url === '/user/login') {\n    requestAgent.child('login', 'Logging in...');\n\n    if (validate(req.user, req.password)) {\n      requestAgent.child('login')\n        .success('Login data validated !');\n    } else {\n      requestAgent.child('login')\n        .error('Error validating user data.')\n    }\n  }\n}\n```\n\n##### `agent.async(name [, data])` -> `Agent`\nReturns a **asynchronous** child of the current agent, defined by its name. If a child with the given name already exists on the agent, it will be returned. If not, it will be created.\n\nData objects can be passed as parameters and will be logged on the child's context.\n\nAn async agent has `.resolve()` and `.reject()` methods, to keep track of its fulfillment.\n\n```js\nimport {agent} from 'investigator';\n\nonRequest(req, res) {\n  const requestAgent = agent('request', req.id, req.url);\n\n  if (req.url === '/user/login') {\n    requestAgent.child('login', 'Logging in...');\n\n    authUser(req.user, req.password).then(() => {\n      requestAgent.child('login')\n        .success('Authentication succeeded !');\n    }).catch((err) => {\n      requestAgent.child('login')\n        .error('Error validating user data.')\n    });\n  }\n}\n```\n\n##### `agent.resolve(data [, data])` -> `Agent`\nResolves an **async** agent. Log data parameters under the given agent node, as a success. Returns the same agent (so it can be chained).\n\nAn async agent can only be resolved or rejected once.\n\n##### `agent.reject(data [, data])` -> `Agent`\nResolves an **async** agent. Log data parameters under the given agent node, as an error. Returns the same agent (so it can be chained).\n\nAn async agent can only be resolved or rejected once.\n\n## Contributing\nFeel free to contribute ! Issues and pull requests are highly welcomed and appreciated.\n\n## License\nThe MIT License (MIT)\n\nCopyright (c) 2015 Valentin Ledrapier\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "dist/investigator.js",
    "content": "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; }; })();\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }\n\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('transceiver'), require('blessed'), require('dateformat'), require('json-prune'), require('path'), require('app-root-path'), require('shortid'), require('stack-trace')) : typeof define === 'function' && define.amd ? define(['transceiver', 'blessed', 'dateformat', 'json-prune', 'path', 'app-root-path', 'shortid', 'stack-trace'], factory) : global.investigator = factory(global.transceiver, global.blessed, global.dateFormat, global.prune, global.path, global.appRoot, global.shortid, global.stack_trace);\n})(this, function (transceiver, blessed, dateFormat, _prune, path, appRoot, shortid, stack_trace) {\n  'use strict';\n\n  var LogsList = (function () {\n    function LogsList() {\n      var _this = this;\n\n      _classCallCheck(this, LogsList);\n\n      this.selectedLog = null;\n      this.logs = {};\n      this.logsCount = 0;\n      this.channel = transceiver.channel('log');\n      this.autoScroll = true;\n      this.element = blessed.list({\n        top: '0',\n        left: '0',\n        bottom: 7,\n        tags: true,\n        keys: true,\n        mouse: true,\n        scrollbar: {\n          bg: 'magenta'\n        },\n        style: {\n          selected: {\n            fg: 'black',\n            bg: 'white'\n          }\n        }\n      });\n\n      this.element.key(['up', 'down', 's', 'b'], function (ch, key) {\n        if (key.name === 's') {\n          _this.autoScroll = !_this.autoScroll;\n        } else if (key.name === 'b') {\n          _this.scrollToBottom();\n          transceiver.request('ui', 'render');\n        } else {\n          _this.autoScroll = false;\n        }\n      });\n\n      this.element.on('select item', function (element, i) {\n        _this.selectedLog = _this.getLogFromElement(element);\n        if (_this.selectedLog) {\n          _this.channel.emit('select log', _this.selectedLog);\n        }\n      });\n\n      this.channel.reply({\n        addLog: this.addLog,\n        getSelectedLog: this.getSelectedLog\n      }, this);\n    }\n\n    _createClass(LogsList, [{\n      key: 'addLog',\n      value: function addLog(log) {\n        var element = undefined;\n\n        this.logs[log.id] = log;\n        this.logsCount++;\n\n        if (log.parent) {\n          var index = this.element.getItemIndex(log.parent.element) + log.parent.getChildren().length;\n          this.element.insertItem(index, log.render());\n          element = this.element.getItem(index);\n        } else {\n          element = this.element.add(log.render());\n        }\n        element.logId = log.id;\n        if (this.autoScroll) {\n          this.scrollToBottom();\n        }\n        if (this.logsCount === 1) {\n          this.channel.emit('select log', log);\n        }\n        return element;\n      }\n    }, {\n      key: 'getSelectedLog',\n      value: function getSelectedLog() {\n        return this.selectedLog;\n      }\n    }, {\n      key: 'scrollToBottom',\n      value: function scrollToBottom() {\n        this.element.move(this.logsCount);\n      }\n    }, {\n      key: 'getLogFromElement',\n      value: function getLogFromElement(element) {\n        return this.logs[element.logId];\n      }\n    }, {\n      key: 'focus',\n      value: function focus() {\n        this.element.focus();\n      }\n    }]);\n\n    return LogsList;\n  })();\n\n  var logDetails = (function () {\n    function logDetails() {\n      _classCallCheck(this, logDetails);\n\n      this.channel = transceiver.channel('log');\n      this.element = blessed.box({\n        height: 6,\n        left: '0',\n        bottom: 0,\n        tags: true,\n        keys: true,\n        padding: {\n          left: 1,\n          right: 1\n        },\n        style: {\n          selected: {\n            fg: 'black',\n            bg: 'white',\n            border: {\n              fg: 'white'\n            },\n            hover: {\n              bg: 'green'\n            }\n          }\n        }\n      });\n\n      this.channel.on('select log', this.updateLogDetails.bind(this));\n    }\n\n    // https://github.com/yaronn/blessed-contrib/blob/master/lib/widget/tree.js\n\n    _createClass(logDetails, [{\n      key: 'updateLogDetails',\n      value: function updateLogDetails(log) {\n        this.element.setContent(this.renderType(log) + this.renderId(log) + this.renderDate(log) + this.renderDuration(log) + this.renderData(log));\n      }\n    }, {\n      key: 'renderType',\n      value: function renderType(log) {\n        if (log.type === 'root') {\n          return '{magenta-fg}{bold}ROOT NODE{/bold}{/magenta-fg}\\n';\n        }\n        if (log.type === 'success') {\n          return '{green-fg}✔ {bold}SUCCESS{/bold}{/green-fg}\\n';\n        }\n        if (log.type === 'error') {\n          return '{red-fg}✘ {bold}ERROR{/bold}{/red-fg}\\n';\n        }\n        if (log.type === 'warn') {\n          return '{yellow-fg}! {bold}WARN{/bold}{/red-fg}\\n';\n        }\n        if (log.type === 'node') {\n          return '{grey-fg}{bold}NODE{/bold}{/grey-fg}\\n';\n        }\n        if (log.type === 'async') {\n          if (log.status === 'resolved') {\n            return '{bold}{green-fg}ASYNC NODE{/bold} (RESOLVED ✔){/green-fg}\\n';\n          }\n          if (log.status === 'rejected') {\n            return '{bold}{red-fg}ASYNC NODE{/bold} (REJECTED ✘){/red-fg}\\n';\n          }\n          if (log.status === 'pending') {\n            return '{cyan-fg}{bold}ASYNC NODE{/bold} (PENDING ⌛){/cyan-fg}\\n';\n          }\n        }\n        if (log.type === 'info') {\n          return '{white-fg}{bold}INFO{/bold}{/white-fg}\\n';\n        }\n        return '';\n      }\n    }, {\n      key: 'renderId',\n      value: function renderId(log) {\n        return '{bold}ID:{/bold} {underline}' + log.id + '{/underline}\\n';\n      }\n    }, {\n      key: 'renderDate',\n      value: function renderDate(log) {\n        return '{bold}TIME:{/bold} {magenta-fg}' + dateFormat(log.date, 'dddd, mmmm dS yyyy, HH:MM:ss.L') + '{/magenta-fg}\\n';\n      }\n    }, {\n      key: 'renderDuration',\n      value: function renderDuration(log) {\n        if (log.relativeDuration && log.previousLog) {\n          return '{bold}DURATION:{/bold} {yellow-fg}' + log.relativeDuration + '{/yellow-fg} (from {underline}' + log.previousLog.id + '{/underline})\\n';\n        }\n        return '';\n      }\n    }, {\n      key: 'renderData',\n      value: function renderData(log) {\n        if (log.data) {\n          return '{bold}DATA:{/bold} ' + log.renderData() + '\\n';\n        }\n        return '';\n      }\n    }]);\n\n    return logDetails;\n  })();\n\n  var Node = blessed.Node;\n  var Box = blessed.Box;\n\n  function Tree(options) {\n\n    if (!(this instanceof Node)) {\n      return new Tree(options);\n    }\n\n    options = options || {};\n    options.bold = true;\n    var self = this;\n    this.options = options;\n    this.data = {};\n    this.nodeLines = [];\n    this.lineNbr = 0;\n    Box.call(this, options);\n\n    options.extended = options.extended || false;\n    options.keys = options.keys || ['space', 'enter'];\n\n    options.template = options.template || {};\n    options.template.extend = options.template.extend || ' [+]';\n    options.template.retract = options.template.retract || ' [-]';\n    options.template.lines = options.template.lines || false;\n\n    this.rows = blessed.list({\n      height: 0,\n      top: 1,\n      width: 0,\n      left: 1,\n      selectedFg: 'black',\n      selectedBg: 'white',\n      keys: true,\n      tags: true\n    });\n\n    this.rows.key(options.keys, function () {\n      self.nodeLines[this.getItemIndex(this.selected)].extended = !self.nodeLines[this.getItemIndex(this.selected)].extended;\n      self.setData(self.data);\n      self.screen.render();\n\n      self.emit('select', self.nodeLines[this.getItemIndex(this.selected)]);\n    });\n\n    this.append(this.rows);\n  }\n\n  Tree.prototype.walk = function (node, treeDepth) {\n    var lines = [];\n\n    if (!node.parent) {\n      node.parent = null;\n    }\n\n    if (treeDepth == '' && node.name) {\n      this.lineNbr = 0;\n      this.nodeLines[this.lineNbr++] = node;\n      lines.push(node.name);\n      treeDepth = ' ';\n    }\n\n    node.depth = treeDepth.length - 1;\n\n    if (node.children && node.extended) {\n\n      var i = 0;\n\n      if (typeof node.children == 'function') {\n        node.childrenContent = node.children(node);\n      }\n\n      if (!node.childrenContent) {\n        node.childrenContent = node.children;\n      }\n\n      for (var child in node.childrenContent) {\n\n        if (!node.childrenContent[child].name) {\n          node.childrenContent[child].name = child;\n        }\n\n        var childIndex = child;\n        child = node.childrenContent[child];\n        child.parent = node;\n        child.position = i++;\n\n        if (typeof child.extended == 'undefined') {\n          child.extended = this.options.extended;\n        }\n\n        if (typeof child.children == 'function') {\n          child.childrenContent = child.children(child);\n        } else {\n          child.childrenContent = child.children;\n        }\n\n        var isLastChild = child.position == Object.keys(child.parent.childrenContent).length - 1;\n        var tree;\n        var suffix = '';\n        if (isLastChild) {\n          tree = '└';\n        } else {\n          tree = '├';\n        }\n        if (!child.childrenContent || Object.keys(child.childrenContent).length == 0) {\n          tree += '─';\n        } else if (child.extended) {\n          tree += '┬';\n          suffix = this.options.template.retract;\n        } else {\n          tree += '─';\n          suffix = this.options.template.extend;\n        }\n\n        if (!this.options.template.lines) {\n          tree = '|-';\n        }\n\n        lines.push(treeDepth + tree + child.name + suffix);\n\n        this.nodeLines[this.lineNbr++] = child;\n\n        var parentTree;\n        if (isLastChild || !this.options.template.lines) {\n          parentTree = treeDepth + ' ';\n        } else {\n          parentTree = treeDepth + '│';\n        }\n        lines = lines.concat(this.walk(child, parentTree));\n      }\n    }\n    return lines;\n  };\n\n  Tree.prototype.focus = function () {\n    this.rows.focus();\n  };\n\n  Tree.prototype.render = function () {\n    if (this.screen.focused == this.rows) {\n      this.rows.focus();\n    }\n\n    this.rows.width = this.width - 3;\n    this.rows.height = this.height - 3;\n    Box.prototype.render.call(this);\n  };\n\n  Tree.prototype.setData = function (data) {\n\n    var formatted = [];\n    formatted = this.walk(data, '');\n\n    this.data = data;\n    this.rows.setItems(formatted);\n  };\n\n  Tree.prototype.__proto__ = Box.prototype;\n\n  Tree.prototype.type = 'tree';\n\n  var ui_tree = Tree;\n\n  var Inspector = (function () {\n    function Inspector() {\n      _classCallCheck(this, Inspector);\n\n      this.channel = transceiver.channel('log');\n\n      this.element = ui_tree({\n        top: 'center',\n        left: 'center',\n        width: '90%',\n        height: '75%',\n        hidden: true,\n        label: 'Inspector',\n        tags: true,\n        border: {\n          type: 'line'\n        },\n        style: {\n          fg: 'white',\n          border: {\n            fg: '#f0f0f0'\n          }\n        },\n        template: {\n          extend: '{bold}{green-fg} [+]{/}',\n          retract: '{bold}{yellow-fg} [-]{/}',\n          lines: true\n        }\n      });\n    }\n\n    _createClass(Inspector, [{\n      key: 'open',\n      value: function open(selectedLog) {\n        if (!selectedLog || !selectedLog.data && !selectedLog.stackTrace) {\n          return;\n        }\n        this.opened = true;\n        this.element.show();\n        this.element.focus();\n        this.element.setData(this.prepareData(selectedLog));\n      }\n    }, {\n      key: 'close',\n      value: function close() {\n        this.opened = false;\n        this.element.hide();\n      }\n    }, {\n      key: 'prepareData',\n      value: function prepareData(log) {\n        var content = {};\n        if (log.data) {\n          content.data = JSON.parse(_prune(log.data, {\n            depthDecr: 7,\n            replacer: function replacer(value, defaultValue, circular) {\n              if (typeof value === 'function') {\n                return '\"Function [pruned]\"';\n              }\n              if (Array.isArray(value)) {\n                return '\"Array (' + value.length + ') [pruned]\"';\n              }\n              if (typeof value === 'object') {\n                return '\"Object [pruned]\"';\n              }\n              return defaultValue;\n            }\n          }));\n        }\n\n        if (log.stackTrace) {\n          content['stack trace'] = log.stackTrace.map(function (callsite) {\n            var relativePath = path.relative(appRoot.toString(), callsite.file);\n            return {\n              type: callsite.type,\n              'function': callsite['function'],\n              method: callsite.method,\n              file: relativePath + ':{yellow-fg}' + callsite.line + '{/yellow-fg}:{yellow-fg}' + callsite.column + '{/yellow-fg}'\n            };\n          });\n        }\n        return this.formatData(content);\n      }\n    }, {\n      key: 'formatData',\n      value: function formatData(data, key) {\n        var _this2 = this;\n\n        var depth = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2];\n\n        depth++;\n        if (typeof data === 'object') {\n          if (data !== null) {\n            var _ret = (function () {\n              var name = undefined;\n              var extended = undefined;\n\n              if (depth === 2) {\n                name = '{yellow-fg}{bold}' + key.toUpperCase() + '{/bold}{/yellow-fg} {magenta-fg}(' + data.length + '){/magenta-fg}';\n                extended = key === 'data';\n              } else {\n                var type = Array.isArray(data) ? '[Array] {magenta-fg}(' + data.length + '){/magenta-fg}' : '[Object]';\n                name = '{blue-fg}{bold}' + (key ? key + ' ' : '') + '{/bold}' + type + '{/blue-fg}';\n                extended = depth < 4;\n              }\n              var newObj = {\n                children: {},\n                name: name,\n                extended: extended\n              };\n              Object.keys(data).forEach(function (key) {\n                var child = _this2.formatData(data[key], key, depth);\n                if (child) {\n                  newObj.children[key] = child;\n                }\n              });\n              return {\n                v: newObj\n              };\n            })();\n\n            if (typeof _ret === 'object') return _ret.v;\n          }\n        }\n        if (typeof data === 'function') {\n          return {\n            name: '{blue-fg}' + key + '{/blue-fg}: {red-fg}{bold}[Function]{/}'\n          };\n        }\n        if (typeof data === 'number') {\n          return {\n            name: '{blue-fg}' + key + '{/blue-fg}: {yellow-fg}' + data + '{/}'\n          };\n        }\n        if (data === null) {\n          return {\n            name: '{blue-fg}' + key + '{/blue-fg}: {cyan-fg}{bold}null{/}'\n          };\n        }\n        return {\n          name: '{blue-fg}' + key + '{/blue-fg}: ' + data\n        };\n      }\n    }]);\n\n    return Inspector;\n  })();\n\n  var Ui = (function () {\n    function Ui() {\n      var _this3 = this;\n\n      _classCallCheck(this, Ui);\n\n      this.channel = transceiver.channel('ui');\n      this.screen = blessed.screen({\n        smartCSR: true\n      });\n\n      this.logsList = new LogsList();\n      this.logDetails = new logDetails();\n      this.inspector = new Inspector();\n\n      this.separator = blessed.line({\n        bottom: 6,\n        orientation: 'horizontal'\n      });\n\n      this.screen.append(this.logsList.element);\n      this.screen.append(this.logDetails.element);\n      this.screen.append(this.separator);\n      this.screen.append(this.inspector.element);\n\n      this.logsList.element.focus();\n\n      this.screen.key(['q', 'C-c'], function (ch, key) {\n        return process.exit(0);\n      });\n\n      this.screen.key(['i'], this.toggleInspector.bind(this));\n\n      this.screen.render();\n\n      this.channel.reply('render', function () {\n        return _this3.screen.render();\n      });\n    }\n\n    _createClass(Ui, [{\n      key: 'toggleInspector',\n      value: function toggleInspector() {\n        if (this.inspector.opened) {\n          this.inspector.close();\n          this.logsList.focus();\n        } else {\n          this.inspector.open(this.logsList.selectedLog);\n        }\n        this.screen.render();\n      }\n    }]);\n\n    return Ui;\n  })();\n\n  var LogItem = (function () {\n    function LogItem(_ref) {\n      var name = _ref.name;\n      var type = _ref.type;\n      var status = _ref.status;\n      var parent = _ref.parent;\n      var data = _ref.data;\n      var message = _ref.message;\n      var stackTrace = _ref.stackTrace;\n      var _ref$date = _ref.date;\n      var date = _ref$date === undefined ? Date.now() : _ref$date;\n\n      _classCallCheck(this, LogItem);\n\n      this.id = shortid.generate();\n      this.name = name;\n      this.type = type;\n      this.status = status;\n      this.data = data;\n      this.message = message;\n      this.stackTrace = stackTrace;\n      this.date = date;\n      this.children = [];\n      this.channel = transceiver.channel('log');\n\n      if (parent) {\n        this.depth = parent.depth + 1;\n        this.parent = parent;\n        this.previousLog = parent.getLastChild() || parent;\n        this.relativeDuration = this.getRelativeDuration();\n        this.parent.addChild(this);\n      } else {\n        this.depth = 0;\n      }\n      this.element = this.channel.request('addLog', this);\n      this.update();\n    }\n\n    _createClass(LogItem, [{\n      key: 'update',\n      value: function update() {\n        if (this.element) {\n          this.element.content = this.render();\n          transceiver.request('ui', 'render');\n        }\n      }\n    }, {\n      key: 'render',\n      value: function render() {\n        var message = '' + this.renderState() + this.renderName() + this.renderMessage() + this.renderData() + this.renderDate() + this.renderDuration();\n        for (var i = 0; i < this.depth; i++) {\n          message = '    ' + message;\n        }\n        return message;\n      }\n    }, {\n      key: 'renderState',\n      value: function renderState() {\n        if (this.type === 'async' && this.status === 'pending') {\n          return '{cyan-fg}[⌛]{/cyan-fg} ';\n        }\n        if (this.type === 'async' && this.status === 'resolved') {\n          return '{green-fg}[✔]{/green-fg} ';\n        }\n        if (this.type === 'async' && this.status === 'rejected') {\n          return '{red-fg}[✘]{/red-fg} ';\n        }\n        if (this.type === 'success') {\n          return '{green-fg}✔{/green-fg} ';\n        }\n        if (this.type === 'error') {\n          return '{red-fg}✘{/red-fg} ';\n        }\n        if (this.type === 'warn') {\n          return '{yellow-fg}❗{/yellow-fg} ';\n        }\n        if (this.type === 'info') {\n          return '⇢ ';\n        }\n        return '';\n      }\n    }, {\n      key: 'renderName',\n      value: function renderName() {\n        if (this.depth === 0) {\n          return this.name ? '{underline}{bold}' + this.name + '{/bold}{/underline} ' : '';\n        }\n        if (this.type === 'async') {\n          if (this.status === 'resolved') {\n            return '{bold}{green-fg}' + this.name + '{/green-fg}{/bold} (async) ';\n          }\n          if (this.status === 'rejected') {\n            return '{bold}{red-fg}' + this.name + '{/red-fg}{/bold} (async) ';\n          }\n          return '{bold}' + this.name + '{/bold} (async) ';\n        }\n        if (this.type === 'success') {\n          return this.name ? '{bold}{green-fg}' + this.name + '{/green-fg}{/bold} ' : '';\n        }\n        if (this.type === 'error') {\n          return this.name ? '{bold}{red-fg}' + this.name + '{/red-fg}{/bold} ' : '';\n        }\n        if (this.type === 'warn') {\n          return this.name ? '{bold}{yellow-fg}' + this.name + '{/yellow-fg}{/bold} ' : '';\n        }\n        return this.name ? '{bold}' + this.name + '{/bold} ' : '';\n      }\n    }, {\n      key: 'renderData',\n      value: function renderData() {\n        if (this.depth === 0) {\n          // console.log(this.data);\n        }\n        if (!this.data) {\n          return '';\n        }\n        if (Array.isArray(this.data)) {\n          return this.data.map(this.renderValue.bind(this)).join(' ') + ' ';\n        }\n        return this.renderValue(this.data) + ' ';\n      }\n    }, {\n      key: 'renderValue',\n      value: function renderValue(value) {\n        if (Array.isArray(value)) {\n          return '{cyan-fg}' + this.prune(value) + '{/cyan-fg}';\n        }\n        if (typeof value === 'object') {\n          return '{blue-fg}' + this.prune(value) + '{/blue-fg}';\n        }\n        if (typeof value === 'function') {\n          return '{red-fg}{bold}[Function]{/bold}{red-fg}';\n        }\n        if (typeof value === 'number') {\n          return '{yellow-fg}' + value + '{/yellow-fg}';\n        }\n        if (typeof value === 'string') {\n          if (this.type === 'success') {\n            return '{green-fg}' + value + '{/green-fg}';\n          }\n          if (this.type === 'error') {\n            return '{red-fg}' + value + '{/red-fg}';\n          }\n          if (this.type === 'warn') {\n            return '{yellow-fg}' + value + '{/yellow-fg}';\n          }\n        }\n        return value;\n      }\n    }, {\n      key: 'renderMessage',\n      value: function renderMessage() {\n        if (this.message) {\n          if (this.type === 'success') {\n            return '{green-fg}' + this.message + '{/green-fg} ';\n          }\n          if (this.type === 'error') {\n            return '{red-fg}' + this.message + '{/red-fg} ';\n          }\n          if (this.type === 'warn') {\n            return '{yellow-fg}' + this.message + '{/yellow-fg} ';\n          }\n          return this.message + ' ';\n        }\n        return '';\n      }\n    }, {\n      key: 'renderDate',\n      value: function renderDate() {\n        if (this.depth === 0) {\n          return '{magenta-fg}(' + dateFormat(this.date, 'dd/mm/yyyy HH:MM:ss.L') + '){/magenta-fg} ';\n        }\n        return '';\n      }\n    }, {\n      key: 'renderDuration',\n      value: function renderDuration() {\n        if (this.relativeDuration) {\n          return '{grey-fg}+' + this.relativeDuration + '{/grey-fg} ';\n        }\n        return '';\n      }\n    }, {\n      key: 'getRelativeDuration',\n      value: function getRelativeDuration() {\n        return this.humanizeDuration(this.date - this.previousLog.date);\n      }\n    }, {\n      key: 'humanizeDuration',\n      value: function humanizeDuration(duration) {\n        if (duration < 1000) {\n          return duration + 'ms';\n        }\n        if (duration < 60000) {\n          var milliseconds = duration % 1000;\n          milliseconds = ('000' + milliseconds).slice(-3);\n          return Math.floor(duration / 1000) + '.' + milliseconds + 's';\n        }\n        return Math.floor(duration / 60000) + 'm ' + Math.round(duration % 60000 / 1000) + 's';\n      }\n    }, {\n      key: 'addChild',\n      value: function addChild(log) {\n        this.children.push(log);\n      }\n    }, {\n      key: 'getLastChild',\n      value: function getLastChild() {\n        return this.children[this.children.length - 1];\n      }\n    }, {\n      key: 'getChildren',\n      value: function getChildren(list) {\n        list = list || [];\n        list.push.apply(list, this.children);\n        this.children.forEach(function (child) {\n          child.getChildren(list);\n        });\n        return list;\n      }\n    }, {\n      key: 'setStatus',\n      value: function setStatus(status) {\n        this.status = status;\n        this.update();\n      }\n    }, {\n      key: 'prune',\n      value: function prune(value) {\n        return _prune(value, {\n          depthDecr: 2,\n          arrayMaxLength: 8,\n          prunedString: ' [...]'\n        });\n      }\n    }]);\n\n    return LogItem;\n  })();\n\n  var Agent = (function () {\n    function Agent(_ref2) {\n      var name = _ref2.name;\n      var type = _ref2.type;\n      var status = _ref2.status;\n      var data = _ref2.data;\n      var message = _ref2.message;\n      var _ref2$isAsync = _ref2.isAsync;\n      var isAsync = _ref2$isAsync === undefined ? false : _ref2$isAsync;\n      var ancestors = _ref2.ancestors;\n\n      _classCallCheck(this, Agent);\n\n      this.name = name;\n      this.children = {};\n      this.isAsync = isAsync;\n      this.asyncState = this.isAsync ? 'pending' : null;\n      this.type = type;\n      this.status = status;\n\n      if (!ancestors) {\n        this.ancestors = [];\n        this.isRoot = true;\n      } else {\n        this.ancestors = ancestors;\n        this.parent = this.ancestors[this.ancestors.length - 1];\n      }\n\n      this.logItem = new LogItem({\n        name: this.name,\n        type: this.type,\n        status: this.status,\n        parent: this.parent ? this.parent.logItem : null,\n        data: data,\n        message: message,\n        stackTrace: this.generateStackTrace(stack_trace.get())\n      });\n\n      return this;\n    }\n\n    _createClass(Agent, [{\n      key: 'log',\n      value: function log() {\n        for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {\n          args[_key] = arguments[_key];\n        }\n\n        new Agent({\n          type: 'info',\n          data: args,\n          ancestors: this.ancestors.concat(this)\n        });\n        return this;\n      }\n    }, {\n      key: 'warn',\n      value: function warn() {\n        for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {\n          args[_key2] = arguments[_key2];\n        }\n\n        new Agent({\n          type: 'warn',\n          data: args,\n          ancestors: this.ancestors.concat(this)\n        });\n        return this;\n      }\n    }, {\n      key: 'success',\n      value: function success() {\n        for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {\n          args[_key3] = arguments[_key3];\n        }\n\n        new Agent({\n          type: 'success',\n          data: args,\n          ancestors: this.ancestors.concat(this)\n        });\n        return this;\n      }\n    }, {\n      key: 'error',\n      value: function error() {\n        for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {\n          args[_key4] = arguments[_key4];\n        }\n\n        new Agent({\n          type: 'error',\n          data: args,\n          ancestors: this.ancestors.concat(this)\n        });\n        return this;\n      }\n    }, {\n      key: 'child',\n      value: function child(name) {\n        if (!this.children[name]) {\n          this.children[name] = new Agent({\n            name: name,\n            type: 'node',\n            ancestors: this.ancestors.concat(this)\n          });\n        }\n\n        for (var _len5 = arguments.length, args = Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) {\n          args[_key5 - 1] = arguments[_key5];\n        }\n\n        if (args.length) {\n          var _children$name;\n\n          (_children$name = this.children[name]).log.apply(_children$name, args);\n        }\n        return this.children[name];\n      }\n    }, {\n      key: 'async',\n      value: function async(name) {\n        if (!this.children[name]) {\n          this.children[name] = new Agent({\n            name: name,\n            type: 'async',\n            status: 'pending',\n            isAsync: true,\n            ancestors: this.ancestors.concat(this)\n          });\n        }\n        if (!this.children[name].isAsync) {\n          this.internalWarn('Child agent {bold}' + name + '{/bold} is defined as a non async agent');\n        }\n\n        for (var _len6 = arguments.length, args = Array(_len6 > 1 ? _len6 - 1 : 0), _key6 = 1; _key6 < _len6; _key6++) {\n          args[_key6 - 1] = arguments[_key6];\n        }\n\n        if (args.length) {\n          var _children$name2;\n\n          (_children$name2 = this.children[name]).log.apply(_children$name2, args);\n        }\n        return this.children[name];\n      }\n    }, {\n      key: 'resolve',\n      value: function resolve() {\n        if (this.isAsync) {\n          if (this.logItem.status === 'pending') {\n            this.logItem.setStatus('resolved');\n            var resolveLog = new Agent({\n              name: this.name,\n              type: 'success',\n              message: 'resolved',\n              ancestors: this.ancestors.concat(this)\n            });\n\n            for (var _len7 = arguments.length, args = Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {\n              args[_key7] = arguments[_key7];\n            }\n\n            if (args.length) {\n              resolveLog.success.apply(resolveLog, args);\n            }\n          } else {\n            this.internalWarn('Trying to resolve an already {bold}' + this.logItem.status + '{/bold} async agent');\n          }\n        } else {\n          this.internalWarn('Trying to resolve a non async agent');\n        }\n        return this;\n      }\n    }, {\n      key: 'reject',\n      value: function reject() {\n        if (this.isAsync) {\n          if (this.logItem.status === 'pending') {\n            this.logItem.setStatus('rejected');\n            var rejectLog = new Agent({\n              name: this.name,\n              type: 'error',\n              message: 'rejected',\n              ancestors: this.ancestors.concat(this)\n            });\n\n            for (var _len8 = arguments.length, args = Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {\n              args[_key8] = arguments[_key8];\n            }\n\n            if (args.length) {\n              rejectLog.error.apply(rejectLog, args);\n            }\n          } else {\n            this.internalWarn('Trying to reject an already {bold}' + this.logItem.status + '{/bold} async agent');\n          }\n        } else {\n          this.internalWarn('Trying to reject a non async agent');\n        }\n        return this;\n      }\n    }, {\n      key: 'internalWarn',\n      value: function internalWarn(message) {\n        new Agent({\n          name: this.name,\n          type: 'warn',\n          message: message,\n          ancestors: this.ancestors.concat(this)\n        });\n      }\n    }, {\n      key: 'getAncestorsNames',\n      value: function getAncestorsNames() {\n        return this.ancestors.map(function (ancestor) {\n          return ancestor.name;\n        });\n      }\n    }, {\n      key: 'generateStackTrace',\n      value: function generateStackTrace(trace) {\n        var stackTrace = [];\n        for (var i = 0; i < 5; i++) {\n          stackTrace.push({\n            type: trace[i].getTypeName(),\n            'function': trace[i].getFunctionName(),\n            method: trace[i].getMethodName(),\n            file: trace[i].getFileName(),\n            line: trace[i].getLineNumber(),\n            column: trace[i].getColumnNumber()\n          });\n        }\n        return stackTrace;\n      }\n    }]);\n\n    return Agent;\n  })();\n\n  var agent = function agent(name) {\n    for (var _len9 = arguments.length, args = Array(_len9 > 1 ? _len9 - 1 : 0), _key9 = 1; _key9 < _len9; _key9++) {\n      args[_key9 - 1] = arguments[_key9];\n    }\n\n    return new Agent({\n      name: name,\n      type: 'root',\n      data: args.length ? args : undefined\n    });\n  };\n\n  transceiver.setPromise(null);\n\n  var ui = new Ui();\n\n  var investigator = { ui: ui, agent: agent };\n\n  return investigator;\n});\n//# sourceMappingURL=investigator.js.map\n"
  },
  {
    "path": "examples/example.js",
    "content": "import investigator from '../src/investigator';\n\nlet i = 1;\nsetTimeout(runScraping, 2000);\n\nfunction runScraping() {\n  if (i < 5) {\n    i++;\n    scrapPage('http://example.com');\n    setTimeout(() => {\n      runScraping();\n    }, Math.random() * 15000);\n  }\n}\n\nfunction scrapPage(url) {\n  const agent = investigator.agent('scraping page', url)\n    .async('getData');\n\n  agent.async('downloadPage', url);\n  downloadPage(url).then((res) => {\n    agent.child('downloadPage')\n      .log('status', res.status)\n      .resolve(res.size);\n\n    const info = getPageInfo(res);\n    agent.child('getPageInfo')\n      .log('id:', info.id)\n      .log('title:', info.title)\n      .log('metas:', info.metas);\n\n    return Promise.all([\n      getVideos(res.data.videos, agent),\n      getPostsData(res.data.posts, agent),\n    ]);\n  }).then(() => {\n    agent.resolve('Well done !');\n  });\n}\n\nfunction getPostsData(posts, agent) {\n  agent.async('getPosts');\n  return Promise.all(\n    posts.map((post) => {\n      const postAgent = agent.child('getPosts')\n        .child(`post #${post.id}`)\n        .log('Retrieving post data...');\n      return Promise.all([\n        downloadPostImage(post).then((img) => {\n          postAgent.success('Image downloaded - status:', img.status);\n          return img;\n        }).catch((err) => postAgent.error(err)),\n        getPostComments(post).then((comments) => {\n          if (comments.length) {\n            postAgent.success(`Comments retrieved (${comments.length})`, comments);\n          } else {\n            postAgent.warn(`No comment found`);\n          }\n          return comments;\n        }),\n      ]);\n    }))\n    .then((posts) => {\n      agent.child('getPosts').resolve(`${posts.length} posts retrieved`);\n    });\n}\n\nfunction getVideos(videos, agent) {\n  agent.async('getVideos')\n    .log('Downloading', videos.length, 'videos');\n\n  return Promise.all(\n    videos.map((video) => {\n      return downloadVideo(video)\n        .then((data) => {\n          agent.child('getVideos')\n            .success('video', video.id, 'downloaded');\n        });\n    }))\n    .then((videos) => {\n      agent.child('getVideos').resolve(`${videos.length} videos retrieved`)\n        .log(videos);\n    })\n    .catch((err) => {\n      agent.child('getVideos').reject(err);\n    });\n}\n\nfunction downloadPage(url) {\n  return new Promise((resolve) => setTimeout(resolve, Math.random() * 10000, {\n    status: 200,\n    size: `${Math.round(Math.random() * 30)}kb`,\n    data: {\n      posts: [{\n          id: Math.round(Math.random() * 1000),\n          img: null\n        }, {\n          id: Math.round(Math.random() * 1000),\n          img: 'img2.png'\n        }, {\n          id: Math.round(Math.random() * 1000),\n          img: 'img3.png'\n        }],\n      videos: [{\n          id: Math.round(Math.random() * 1000),\n          url: 'video1.mp4'\n        }, {\n          id: Math.round(Math.random() * 1000),\n          url: 'video2.mp4'\n        }, {\n          id: Math.round(Math.random() * 1000),\n          url: null\n        },{\n          id: Math.round(Math.random() * 1000),\n          url: null\n        }],\n    },\n  }));\n}\n\nfunction getPageInfo(page) {\n  return {\n    id: Math.round(Math.random() * 100),\n    title: 'Lorem ipsum',\n    metas: fakeMetas,\n  };\n}\n\nfunction downloadPostImage(post) {\n  return new Promise((resolve, reject) => post.img ? setTimeout(resolve, Math.random() * 6000, {status: 200}) :\n    setTimeout(reject, Math.random() * 10000, 'Error downloading image'));\n}\n\nfunction getPostComments(post) {\n  return new Promise((resolve, reject) => setTimeout(resolve, Math.random() * 6000,\n    ['Lorem', 'Ipsum'].slice(0, Math.round(Math.random() * 2))\n  ));\n}\n\nfunction downloadVideo(video) {\n  return new Promise((resolve, reject) => video.url ? setTimeout(resolve, Math.random() * 6000, {status: 200, size: Math.round(Math.random() * 10, 1)}) :\n    setTimeout(reject, Math.random() * 12000, 'Error downloading video'));\n}\n\nconst fakeMetas = {\n  facebook: {\n    'og:url': 'https://github.com',\n    'og:site_name': 'GitHub',\n    'og:title': 'Build software better, together',\n    'og:description': 'GitHub is where people build software. More than 11 million people use GitHub to discover, fork, and contribute to over 28 million projects.',\n    'og:image': 'https://assets-cdn.github.com/images/modules/open_graph/github-logo.png',\n    'og:image:type': 'image/png',\n    'og:image:width': '1200',\n    'og:image:height': '1200',\n    'og:image': 'https://assets-cdn.github.com/images/modules/open_graph/github-mark.png',\n    'og:image:type': 'image/png',\n    'og:image:width': '1200',\n    'og:image:height': '620',\n    'og:image': 'https://assets-cdn.github.com/images/modules/open_graph/github-octocat.png',\n    'og:image:type': 'image/png',\n    'og:image:width': '1200',\n    'og:image:height': '620',\n  },\n  twitter: {\n    'twitter:site': 'github',\n    'twitter:site:id': '13334762',\n    'twitter:creator': 'github',\n    'twitter:creator:id': '13334762',\n    'twitter:card': 'summary_large_image',\n    'twitter:title': 'GitHub',\n    'twitter:description': 'GitHub is where people build software. More than 11 million people use GitHub to discover, fork, and contribute to over 28 million projects.',\n    'twitter:image:src': 'https://assets-cdn.github.com/images/modules/open_graph/github-logo.png',\n    'twitter:image:width': '1200',\n    'twitter:image:height': '1200'\n  },\n  length: 26,\n};\n"
  },
  {
    "path": "examples/index.js",
    "content": "require('babel-core/register');\nrequire('./example.js');\n"
  },
  {
    "path": "gulpfile.js",
    "content": "// Load Gulp and all of our Gulp plugins\nconst gulp = require('gulp');\nconst $ = require('gulp-load-plugins')();\n\n// Load other npm modules\nconst del = require('del');\nconst glob = require('glob');\nconst path = require('path');\nconst isparta = require('isparta');\nconst babelify = require('babelify');\nconst watchify = require('watchify');\nconst buffer = require('vinyl-buffer');\nconst esperanto = require('esperanto');\nconst browserify = require('browserify');\nconst runSequence = require('run-sequence');\nconst source = require('vinyl-source-stream');\n\n// Gather the library data from `package.json`\nconst manifest = require('./package.json');\nconst config = manifest.babelBoilerplateOptions;\nconst mainFile = manifest.main;\nconst destinationFolder = path.dirname(mainFile);\nconst exportFileName = path.basename(mainFile, path.extname(mainFile));\n\n// Remove the built files\ngulp.task('clean', function(cb) {\n  del([destinationFolder], cb);\n});\n\n// Remove our temporary files\ngulp.task('clean-tmp', function(cb) {\n  del(['tmp'], cb);\n});\n\n// Send a notification when JSCS fails,\n// so that you know your changes didn't build\nfunction jscsNotify(file) {\n  if (!file.jscs) { return; }\n  return file.jscs.success ? false : 'JSCS failed';\n}\n\nfunction createLintTask(taskName, files) {\n  gulp.task(taskName, function() {\n    return gulp.src(files)\n      .pipe($.plumber())\n      .pipe($.eslint())\n      .pipe($.eslint.format())\n      .pipe($.eslint.failOnError())\n      .pipe($.jscs())\n      .pipe($.notify(jscsNotify));\n  });\n}\n\n// Lint our source code\ncreateLintTask('lint-src', ['src/**/*.js']);\n\n// Lint our test code\ncreateLintTask('lint-test', ['test/**/*.js']);\n\n// Build two versions of the library\ngulp.task('build', ['lint-src', 'clean'], function(done) {\n  esperanto.bundle({\n    base: 'src',\n    entry: config.entryFileName,\n  }).then(function(bundle) {\n    var res = bundle.toUmd({\n      // Don't worry about the fact that the source map is inlined at this step.\n      // `gulp-sourcemaps`, which comes next, will externalize them.\n      sourceMap: 'inline',\n      name: config.mainVarName\n    });\n\n    $.file(exportFileName + '.js', res.code, { src: true })\n      .pipe($.plumber())\n      .pipe($.sourcemaps.init({ loadMaps: true }))\n      .pipe($.babel())\n      .pipe($.sourcemaps.write('./'))\n      .pipe(gulp.dest(destinationFolder))\n      .pipe($.filter(['*', '!**/*.js.map']))\n      .pipe($.rename(exportFileName + '.min.js'))\n      .pipe($.sourcemaps.init({ loadMaps: true }))\n      .pipe($.uglify())\n      .pipe($.sourcemaps.write('./'))\n      .pipe(gulp.dest(destinationFolder))\n      .on('end', done);\n  })\n  .catch(done);\n});\n\nfunction bundle(bundler) {\n  return bundler.bundle()\n    .on('error', function(err) {\n      console.log(err.message);\n      this.emit('end');\n    })\n    .pipe($.plumber())\n    .pipe(source('./tmp/__spec-build.js'))\n    .pipe(buffer())\n    .pipe(gulp.dest(''))\n    .pipe($.livereload());\n}\n\nfunction getBundler() {\n  // Our browserify bundle is made up of our unit tests, which\n  // should individually load up pieces of our application.\n  // We also include the browserify setup file.\n  var testFiles = glob.sync('./test/unit/**/*');\n  var allFiles = ['./test/setup/browserify.js'].concat(testFiles);\n\n  // Create our bundler, passing in the arguments required for watchify\n  var bundler = browserify(allFiles, watchify.args);\n\n  // Watch the bundler, and re-bundle it whenever files change\n  bundler = watchify(bundler);\n  bundler.on('update', function() {\n    bundle(bundler);\n  });\n\n  // Set up Babelify so that ES6 works in the tests\n  bundler.transform(babelify.configure({\n    sourceMapRelative: __dirname + '/src'\n  }));\n\n  return bundler;\n};\n\n// Build the unit test suite for running tests\n// in the browser\ngulp.task('browserify', function() {\n  return bundle(getBundler());\n});\n\nfunction test() {\n  return gulp.src(['test/setup/node.js', 'test/unit/**/*.js'], {read: false})\n    .pipe($.mocha({reporter: 'dot', globals: config.mochaGlobals}));\n}\n\ngulp.task('coverage', ['lint-src', 'lint-test'], function(done) {\n  require('babel-core/register');\n  gulp.src(['src/**/*.js'])\n    .pipe($.istanbul({instrumenter: isparta.Instrumenter}))\n    .pipe($.istanbul.hookRequire())\n    .on('finish', function() {\n      return test()\n        .pipe($.istanbul.writeReports())\n        .on('end', done);\n    });\n});\n\n// Lint and run our tests\ngulp.task('test', ['lint-src', 'lint-test'], function() {\n  require('babel-core/register');\n  return test();\n});\n\n// Ensure that linting occurs before browserify runs. This prevents\n// the build from breaking due to poorly formatted code.\ngulp.task('build-in-sequence', function(callback) {\n  runSequence(['lint-src', 'lint-test'], 'browserify', callback);\n});\n\n// These are JS files that should be watched by Gulp. When running tests in the browser,\n// watchify is used instead, so these aren't included.\nconst jsWatchFiles = ['src/**/*', 'test/**/*'];\n// These are files other than JS files which are to be watched. They are always watched.\nconst otherWatchFiles = ['package.json', '**/.eslintrc', '.jscsrc'];\n\n// Run the headless unit tests as you make changes.\ngulp.task('watch', function() {\n  const watchFiles = jsWatchFiles.concat(otherWatchFiles);\n  gulp.watch(watchFiles, ['test']);\n});\n\n// Set up a livereload environment for our spec runner\ngulp.task('test-browser', ['build-in-sequence'], function() {\n  $.livereload.listen({port: 35729, host: 'localhost', start: true});\n  return gulp.watch(otherWatchFiles, ['build-in-sequence']);\n});\n\n// An alias of test\ngulp.task('default', ['test']);\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"investigator\",\n  \"version\": \"0.1.1\",\n  \"description\": \"Interactive and asynchronous logging tool for Node.js. An easier way to log & debug complex requests directly from the command line. Still experimental !\",\n  \"main\": \"dist/investigator.js\",\n  \"scripts\": {\n    \"test\": \"gulp\",\n    \"test-browser\": \"gulp test-browser\",\n    \"build\": \"gulp build\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/risq/investigator.git\"\n  },\n  \"keywords\": [\n    \"log\",\n    \"logger\",\n    \"logging\",\n    \"debug\",\n    \"debugger\",\n    \"cli\",\n    \"inspect\",\n    \"inspecter\"\n  ],\n  \"author\": \"risq <valentin.ledrapier@gmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/risq/investigator/issues\"\n  },\n  \"homepage\": \"https://github.com/risq/investigator\",\n  \"devDependencies\": {\n    \"babel-core\": \"^5.2.17\",\n    \"babel-eslint\": \"^4.0.5\",\n    \"babelify\": \"^6.0.0\",\n    \"browserify\": \"^11.0.1\",\n    \"chai\": \"^3.2.0\",\n    \"del\": \"^1.1.1\",\n    \"esperanto\": \"^0.7.4\",\n    \"glob\": \"^5.0.14\",\n    \"gulp\": \"^3.8.10\",\n    \"gulp-babel\": \"^5.0.0\",\n    \"gulp-eslint\": \"^1.0.0\",\n    \"gulp-file\": \"^0.2.0\",\n    \"gulp-filter\": \"^3.0.0\",\n    \"gulp-istanbul\": \"^0.10.0\",\n    \"gulp-jscs\": \"^2.0.0\",\n    \"gulp-livereload\": \"^3.4.0\",\n    \"gulp-load-plugins\": \"^0.10.0\",\n    \"gulp-mocha\": \"^2.0.0\",\n    \"gulp-notify\": \"^2.1.0\",\n    \"gulp-plumber\": \"^1.0.1\",\n    \"gulp-rename\": \"^1.2.0\",\n    \"gulp-sourcemaps\": \"^1.3.0\",\n    \"gulp-uglify\": \"^1.2.0\",\n    \"isparta\": \"^3.0.3\",\n    \"mocha\": \"^2.1.0\",\n    \"run-sequence\": \"^1.0.2\",\n    \"sinon\": \"^1.12.2\",\n    \"sinon-chai\": \"^2.7.0\",\n    \"vinyl-buffer\": \"^1.0.0\",\n    \"vinyl-source-stream\": \"^1.0.0\",\n    \"watchify\": \"^3.3.1\"\n  },\n  \"babelBoilerplateOptions\": {\n    \"entryFileName\": \"investigator\",\n    \"mainVarName\": \"investigator\",\n    \"mochaGlobals\": [\n      \"stub\",\n      \"spy\",\n      \"expect\"\n    ]\n  },\n  \"dependencies\": {\n    \"app-root-path\": \"^1.0.0\",\n    \"blessed\": \"^0.1.81\",\n    \"dateformat\": \"^1.0.11\",\n    \"json-prune\": \"^1.0.1\",\n    \"shortid\": \"^2.2.4\",\n    \"stack-trace\": \"0.0.9\",\n    \"transceiver\": \"^0.3.4\"\n  }\n}\n"
  },
  {
    "path": "src/agent.js",
    "content": "import shortid from 'shortid';\nimport transceiver from 'transceiver';\nimport stackTrace from 'stack-trace';\n\nimport LogItem from './ui/logItem';\n\nclass Agent {\n  constructor({name, type, status, data, message, isAsync = false, ancestors}) {\n    this.name = name;\n    this.children = {};\n    this.isAsync = isAsync;\n    this.asyncState = this.isAsync ? 'pending' : null;\n    this.type = type;\n    this.status = status;\n\n    if (!ancestors) {\n      this.ancestors = [];\n      this.isRoot = true;\n    } else {\n      this.ancestors = ancestors;\n      this.parent = this.ancestors[this.ancestors.length - 1];\n    }\n\n    this.logItem = new LogItem({\n      name: this.name,\n      type: this.type,\n      status: this.status,\n      parent: this.parent ? this.parent.logItem : null,\n      data: data,\n      message: message,\n      stackTrace: this.generateStackTrace(stackTrace.get()),\n    });\n\n    return this;\n  }\n\n  log(...args) {\n    new Agent({\n      type: 'info',\n      data: args,\n      ancestors: this.ancestors.concat(this)\n    });\n    return this;\n  }\n\n  warn(...args) {\n    new Agent({\n      type: 'warn',\n      data: args,\n      ancestors: this.ancestors.concat(this)\n    });\n    return this;\n  }\n\n  success(...args) {\n    new Agent({\n      type: 'success',\n      data: args,\n      ancestors: this.ancestors.concat(this),\n    });\n    return this;\n  }\n\n  error(...args) {\n    new Agent({\n      type: 'error',\n      data: args,\n      ancestors: this.ancestors.concat(this),\n    });\n    return this;\n  }\n\n  child(name, ...args) {\n    if (!this.children[name]) {\n      this.children[name] = new Agent({\n        name,\n        type: 'node',\n        ancestors: this.ancestors.concat(this),\n      });\n    }\n    if (args.length) {\n      this.children[name].log(...args);\n    }\n    return this.children[name];\n  }\n\n  async(name, ...args) {\n    if (!this.children[name]) {\n      this.children[name] = new Agent({\n        name,\n        type: 'async',\n        status: 'pending',\n        isAsync: true,\n        ancestors: this.ancestors.concat(this),\n      });\n    }\n    if (!this.children[name].isAsync) {\n      this.internalWarn(`Child agent {bold}${name}{/bold} is defined as a non async agent`);\n    }\n    if (args.length) {\n      this.children[name].log(...args);\n    }\n    return this.children[name];\n  }\n\n  resolve(...args) {\n    if (this.isAsync) {\n      if (this.logItem.status === 'pending') {\n        this.logItem.setStatus('resolved');\n        const resolveLog = new Agent({\n          name: this.name,\n          type: 'success',\n          message: 'resolved',\n          ancestors: this.ancestors.concat(this),\n        });\n        if (args.length) {\n          resolveLog.success(...args);\n        }\n      } else {\n        this.internalWarn(`Trying to resolve an already {bold}${this.logItem.status}{/bold} async agent`);\n      }\n    } else {\n      this.internalWarn('Trying to resolve a non async agent');\n    }\n    return this;\n  }\n\n  reject(...args) {\n    if (this.isAsync) {\n      if (this.logItem.status === 'pending') {\n        this.logItem.setStatus('rejected');\n        const rejectLog = new Agent({\n          name: this.name,\n          type: 'error',\n          message: 'rejected',\n          ancestors: this.ancestors.concat(this),\n        });\n        if (args.length) {\n          rejectLog.error(...args);\n        }\n      } else {\n        this.internalWarn(`Trying to reject an already {bold}${this.logItem.status}{/bold} async agent`);\n      }\n    } else {\n      this.internalWarn('Trying to reject a non async agent');\n    }\n    return this;\n  }\n\n  internalWarn(message) {\n    new Agent({\n      name: this.name,\n      type: 'warn',\n      message,\n      ancestors: this.ancestors.concat(this),\n    });\n  }\n\n  getAncestorsNames() {\n    return this.ancestors.map(ancestor => ancestor.name);\n  }\n\n  generateStackTrace(trace) {\n    const stackTrace = [];\n    for (let i = 0; i < 5; i++) {\n      stackTrace.push({\n        type: trace[i].getTypeName(),\n        function: trace[i].getFunctionName(),\n        method: trace[i].getMethodName(),\n        file: trace[i].getFileName(),\n        line: trace[i].getLineNumber(),\n        column: trace[i].getColumnNumber(),\n      });\n    }\n    return stackTrace;\n  }\n}\n\nexport default function(name, ...args) {\n  return new Agent({\n    name,\n    type: 'root',\n    data: args.length ? args : undefined,\n  });\n};\n"
  },
  {
    "path": "src/investigator.js",
    "content": "import transceiver from 'transceiver';\n\nimport Ui from './ui';\nimport agent from './agent';\n\ntransceiver.setPromise(null);\n\nconst ui = new Ui();\n\nexport default {ui, agent};\n"
  },
  {
    "path": "src/ui/index.js",
    "content": "import blessed from 'blessed';\nimport transceiver from 'transceiver';\n\nimport LogsList from './logsList';\nimport LogDetails from './logDetails';\nimport Inspector from './inspector';\n\nexport default class Ui {\n  constructor() {\n    this.channel = transceiver.channel('ui');\n    this.screen = blessed.screen({\n      smartCSR: true\n    });\n\n    this.logsList = new LogsList();\n    this.logDetails = new LogDetails();\n    this.inspector = new Inspector();\n\n    this.separator = blessed.line({\n      bottom: 6,\n      orientation: 'horizontal'\n    });\n\n    this.screen.append(this.logsList.element);\n    this.screen.append(this.logDetails.element);\n    this.screen.append(this.separator);\n    this.screen.append(this.inspector.element);\n\n    this.logsList.element.focus();\n\n    this.screen.key(['q', 'C-c'], function(ch, key) {\n      return process.exit(0);\n    });\n\n    this.screen.key(['i'], this.toggleInspector.bind(this));\n\n    this.screen.render();\n\n    this.channel.reply('render', () => this.screen.render());\n  }\n\n  toggleInspector() {\n    if (this.inspector.opened) {\n      this.inspector.close();\n      this.logsList.focus();\n    } else {\n      this.inspector.open(this.logsList.selectedLog);\n    }\n    this.screen.render();\n  }\n}\n"
  },
  {
    "path": "src/ui/inspector.js",
    "content": "import blessed from 'blessed';\nimport transceiver from 'transceiver';\nimport prune from 'json-prune';\nimport path from 'path';\nimport appRoot from 'app-root-path';\n\nimport tree from './tree';\n\nexport default class Inspector {\n  constructor() {\n    this.channel = transceiver.channel('log');\n\n    this.element = tree({\n      top: 'center',\n      left: 'center',\n      width: '90%',\n      height: '75%',\n      hidden: true,\n      label: 'Inspector',\n      tags: true,\n      border: {\n        type: 'line'\n      },\n      style: {\n        fg: 'white',\n        border: {\n          fg: '#f0f0f0'\n        },\n      },\n      template: {\n        extend: '{bold}{green-fg} [+]{/}',\n        retract: '{bold}{yellow-fg} [-]{/}',\n        lines: true,\n      }\n    });\n  }\n\n  open(selectedLog) {\n    if (!selectedLog || !selectedLog.data && !selectedLog.stackTrace) {\n      return;\n    }\n    this.opened = true;\n    this.element.show();\n    this.element.focus();\n    this.element.setData(this.prepareData(selectedLog));\n  }\n\n  close() {\n    this.opened = false;\n    this.element.hide();\n  }\n\n  prepareData(log) {\n    const content = {};\n    if (log.data) {\n      content.data = JSON.parse(prune(log.data, {\n        depthDecr: 7,\n        replacer: (value, defaultValue, circular) => {\n          if (typeof value === 'function') {\n            return '\"Function [pruned]\"';\n          }\n          if (Array.isArray(value)) {\n            return `\"Array (${value.length}) [pruned]\"`;\n          }\n          if (typeof value === 'object') {\n            return '\"Object [pruned]\"';\n          }\n          return defaultValue;\n        }\n      }));\n    }\n\n    if (log.stackTrace) {\n      content['stack trace'] = log.stackTrace.map((callsite) => {\n        const relativePath = path.relative(appRoot.toString(), callsite.file);\n        return {\n          type: callsite.type,\n          function: callsite.function,\n          method: callsite.method,\n          file: `${relativePath}:{yellow-fg}${callsite.line}{/yellow-fg}:{yellow-fg}${callsite.column}{/yellow-fg}`,\n        };\n      });\n    }\n    return this.formatData(content);\n  }\n\n  formatData(data, key, depth = 0) {\n    depth++;\n    if (typeof data === 'object') {\n      if (data !== null) {\n        let name;\n        let extended;\n\n        if (depth === 2) {\n          name = `{yellow-fg}{bold}${key.toUpperCase()}{/bold}{/yellow-fg} {magenta-fg}(${data.length}){/magenta-fg}`;\n          extended = key === 'data';\n        } else {\n          const type = (Array.isArray(data) ? `[Array] {magenta-fg}(${data.length}){/magenta-fg}` : '[Object]');\n          name = `{blue-fg}{bold}${key ? key + ' ' : ''}{/bold}${type}{/blue-fg}`;\n          extended = depth < 4;\n        }\n        const newObj = {\n          children: {},\n          name,\n          extended\n        };\n        Object.keys(data).forEach((key) => {\n          const child = this.formatData(data[key], key, depth);\n          if (child) {\n            newObj.children[key] = child;\n          }\n        });\n        return newObj;\n      }\n    }\n    if (typeof data === 'function') {\n      return {\n        name: `{blue-fg}${key}{/blue-fg}: {red-fg}{bold}[Function]{/}`,\n      };\n    }\n    if (typeof data === 'number') {\n      return {\n        name: `{blue-fg}${key}{/blue-fg}: {yellow-fg}${data}{/}`,\n      };\n    }\n    if (data === null) {\n      return {\n        name: `{blue-fg}${key}{/blue-fg}: {cyan-fg}{bold}null{/}`,\n      };\n    }\n    return {\n      name: `{blue-fg}${key}{/blue-fg}: ${data}`,\n    };\n  }\n}\n"
  },
  {
    "path": "src/ui/logDetails.js",
    "content": "import blessed from 'blessed';\nimport transceiver from 'transceiver';\nimport dateFormat from 'dateformat';\n\nexport default class logDetails {\n  constructor() {\n    this.channel = transceiver.channel('log');\n    this.element = blessed.box({\n      height: 6,\n      left: '0',\n      bottom: 0,\n      tags: true,\n      keys: true,\n      padding: {\n        left: 1,\n        right: 1,\n      },\n      style: {\n        selected: {\n          fg: 'black',\n          bg: 'white',\n          border: {\n            fg: 'white'\n          },\n          hover: {\n            bg: 'green'\n          }\n        }\n      }\n    });\n\n    this.channel.on('select log', this.updateLogDetails.bind(this));\n  }\n\n  updateLogDetails(log) {\n    this.element.setContent(this.renderType(log) + this.renderId(log) + this.renderDate(log) + this.renderDuration(log) + this.renderData(log));\n  }\n\n  renderType(log) {\n    if (log.type === 'root') {\n      return '{magenta-fg}{bold}ROOT NODE{/bold}{/magenta-fg}\\n';\n    }\n    if (log.type === 'success') {\n      return '{green-fg}✔ {bold}SUCCESS{/bold}{/green-fg}\\n';\n    }\n    if (log.type === 'error') {\n      return '{red-fg}✘ {bold}ERROR{/bold}{/red-fg}\\n';\n    }\n    if (log.type === 'warn') {\n      return '{yellow-fg}! {bold}WARN{/bold}{/red-fg}\\n';\n    }\n    if (log.type === 'node') {\n      return '{grey-fg}{bold}NODE{/bold}{/grey-fg}\\n';\n    }\n    if (log.type === 'async') {\n      if (log.status === 'resolved') {\n        return '{bold}{green-fg}ASYNC NODE{/bold} (RESOLVED ✔){/green-fg}\\n';\n      }\n      if (log.status === 'rejected') {\n        return '{bold}{red-fg}ASYNC NODE{/bold} (REJECTED ✘){/red-fg}\\n';\n      }\n      if (log.status === 'pending') {\n        return '{cyan-fg}{bold}ASYNC NODE{/bold} (PENDING ⌛){/cyan-fg}\\n';\n      }\n    }\n    if (log.type === 'info') {\n      return '{white-fg}{bold}INFO{/bold}{/white-fg}\\n';\n    }\n    return '';\n  }\n\n  renderId(log) {\n    return `{bold}ID:{/bold} {underline}${log.id}{/underline}\\n`;\n  }\n\n  renderDate(log) {\n    return `{bold}TIME:{/bold} {magenta-fg}${dateFormat(log.date, 'dddd, mmmm dS yyyy, HH:MM:ss.L')}{/magenta-fg}\\n`;\n  }\n\n  renderDuration(log) {\n    if (log.relativeDuration && log.previousLog) {\n      return `{bold}DURATION:{/bold} {yellow-fg}${log.relativeDuration}{/yellow-fg} (from {underline}${log.previousLog.id}{/underline})\\n`;\n    }\n    return '';\n  }\n\n  renderData(log) {\n    if (log.data) {\n      return `{bold}DATA:{/bold} ${log.renderData()}\\n`;\n    }\n    return '';\n  }\n}\n"
  },
  {
    "path": "src/ui/logItem.js",
    "content": "import blessed from 'blessed';\nimport shortid from 'shortid';\nimport transceiver from 'transceiver';\nimport prune from 'json-prune';\nimport dateFormat from 'dateformat';\n\nexport default class LogItem {\n  constructor({name, type, status, parent, data, message, stackTrace, date = Date.now()}) {\n    this.id = shortid.generate();\n    this.name = name;\n    this.type = type;\n    this.status = status;\n    this.data = data;\n    this.message = message;\n    this.stackTrace = stackTrace;\n    this.date = date;\n    this.children = [];\n    this.channel = transceiver.channel('log');\n\n    if (parent) {\n      this.depth = parent.depth + 1;\n      this.parent = parent;\n      this.previousLog = parent.getLastChild() || parent;\n      this.relativeDuration = this.getRelativeDuration();\n      this.parent.addChild(this);\n    } else {\n      this.depth = 0;\n    }\n    this.element = this.channel.request('addLog', this);\n    this.update();\n  }\n\n  update() {\n    if (this.element) {\n      this.element.content = this.render();\n      transceiver.request('ui', 'render');\n    }\n  }\n\n  render() {\n    let message = `${this.renderState()}${this.renderName()}${this.renderMessage()}${this.renderData()}${this.renderDate()}${this.renderDuration()}`;\n    for (let i = 0; i < this.depth; i++) {\n      message = '    ' + message;\n    }\n    return message;\n  }\n\n  renderState() {\n    if (this.type === 'async' && this.status === 'pending') {\n      return `{cyan-fg}[⌛]{/cyan-fg} `;\n    }\n    if (this.type === 'async' && this.status === 'resolved') {\n      return `{green-fg}[✔]{/green-fg} `;\n    }\n    if (this.type === 'async' && this.status === 'rejected') {\n      return `{red-fg}[✘]{/red-fg} `;\n    }\n    if (this.type === 'success') {\n      return `{green-fg}✔{/green-fg} `;\n    }\n    if (this.type === 'error') {\n      return `{red-fg}✘{/red-fg} `;\n    }\n    if (this.type === 'warn') {\n      return `{yellow-fg}❗{/yellow-fg} `;\n    }\n    if (this.type === 'info') {\n      return '⇢ ';\n    }\n    return '';\n  }\n\n  renderName() {\n    if (this.depth === 0) {\n      return this.name ? `{underline}{bold}${this.name}{/bold}{/underline} ` : '';\n    }\n    if (this.type === 'async') {\n      if (this.status === 'resolved') {\n        return `{bold}{green-fg}${this.name}{/green-fg}{/bold} (async) `;\n      }\n      if (this.status === 'rejected') {\n        return `{bold}{red-fg}${this.name}{/red-fg}{/bold} (async) `;\n      }\n      return `{bold}${this.name}{/bold} (async) `;\n    }\n    if (this.type === 'success') {\n      return this.name ? `{bold}{green-fg}${this.name}{/green-fg}{/bold} ` : '';\n    }\n    if (this.type === 'error') {\n      return this.name ? `{bold}{red-fg}${this.name}{/red-fg}{/bold} ` : '';\n    }\n    if (this.type === 'warn') {\n      return this.name ? `{bold}{yellow-fg}${this.name}{/yellow-fg}{/bold} ` : '';\n    }\n    return this.name ? `{bold}${this.name}{/bold} ` : '';\n  }\n\n  renderData() {\n    if (this.depth === 0) {\n      // console.log(this.data);\n    }\n    if (!this.data) {\n      return '';\n    }\n    if (Array.isArray(this.data)) {\n      return this.data.map(this.renderValue.bind(this)).join(' ') + ' ';\n    }\n    return this.renderValue(this.data) + ' ';\n  }\n\n  renderValue(value) {\n    if (Array.isArray(value)) {\n      return `{cyan-fg}${this.prune(value)}{/cyan-fg}`;\n    }\n    if (typeof value === 'object') {\n      return `{blue-fg}${this.prune(value)}{/blue-fg}`;\n    }\n    if (typeof value === 'function') {\n      return `{red-fg}{bold}[Function]{/bold}{red-fg}`;\n    }\n    if (typeof value === 'number') {\n      return `{yellow-fg}${value}{/yellow-fg}`;\n    }\n    if (typeof value === 'string') {\n      if (this.type === 'success') {\n        return `{green-fg}${value}{/green-fg}`;\n      }\n      if (this.type === 'error') {\n        return `{red-fg}${value}{/red-fg}`;\n      }\n      if (this.type === 'warn') {\n        return `{yellow-fg}${value}{/yellow-fg}`;\n      }\n    }\n    return value;\n  }\n\n  renderMessage() {\n    if (this.message) {\n      if (this.type === 'success') {\n        return `{green-fg}${this.message}{/green-fg} `;\n      }\n      if (this.type === 'error') {\n        return `{red-fg}${this.message}{/red-fg} `;\n      }\n      if (this.type === 'warn') {\n        return `{yellow-fg}${this.message}{/yellow-fg} `;\n      }\n      return `${this.message} `;\n    }\n    return '';\n  }\n\n  renderDate() {\n    if (this.depth === 0) {\n      return `{magenta-fg}(${dateFormat(this.date, 'dd/mm/yyyy HH:MM:ss.L')}){/magenta-fg} `;\n    }\n    return '';\n  }\n\n  renderDuration() {\n    if (this.relativeDuration) {\n      return `{grey-fg}+${this.relativeDuration}{/grey-fg} `;\n    }\n    return '';\n  }\n\n  getRelativeDuration() {\n    return this.humanizeDuration(this.date - this.previousLog.date);\n  }\n\n  humanizeDuration(duration) {\n    if (duration < 1000) {\n      return `${duration}ms`;\n    }\n    if (duration < 60000) {\n      let milliseconds = duration % 1000;\n      milliseconds = ('000' + milliseconds).slice(-3);\n      return `${Math.floor(duration / 1000)}.${milliseconds}s`;\n    }\n    return `${Math.floor(duration / 60000)}m ${Math.round((duration % 60000) / 1000)}s`;\n  }\n\n  addChild(log) {\n    this.children.push(log);\n  }\n\n  getLastChild() {\n    return this.children[this.children.length - 1];\n  }\n\n  getChildren(list) {\n    list = list || [];\n    list.push.apply(list, this.children);\n    this.children.forEach(child => {\n      child.getChildren(list);\n    });\n    return list;\n  }\n\n  setStatus(status) {\n    this.status = status;\n    this.update();\n  }\n\n  prune(value) {\n    return prune(value, {\n      depthDecr: 2,\n      arrayMaxLength: 8,\n      prunedString: ' [...]'\n    });\n  }\n}\n"
  },
  {
    "path": "src/ui/logsList.js",
    "content": "import blessed from 'blessed';\nimport transceiver from 'transceiver';\n\nexport default class LogsList {\n  constructor() {\n    this.selectedLog = null;\n    this.logs = {};\n    this.logsCount = 0;\n    this.channel = transceiver.channel('log');\n    this.autoScroll = true;\n    this.element = blessed.list({\n      top: '0',\n      left: '0',\n      bottom: 7,\n      tags: true,\n      keys: true,\n      mouse: true,\n      scrollbar: {\n        bg: 'magenta',\n      },\n      style: {\n        selected: {\n          fg: 'black',\n          bg: 'white',\n        }\n      }\n    });\n\n    this.element.key(['up', 'down', 's', 'b'], (ch, key) => {\n      if (key.name === 's') {\n        this.autoScroll = !this.autoScroll;\n      } else if (key.name === 'b') {\n        this.scrollToBottom();\n        transceiver.request('ui', 'render');\n      } else {\n        this.autoScroll = false;\n      }\n    });\n\n    this.element.on('select item', (element, i) => {\n      this.selectedLog = this.getLogFromElement(element);\n      if (this.selectedLog) {\n        this.channel.emit('select log', this.selectedLog);\n      }\n    });\n\n    this.channel.reply({\n      addLog: this.addLog,\n      getSelectedLog: this.getSelectedLog,\n    }, this);\n  }\n\n  addLog(log) {\n    let element;\n\n    this.logs[log.id] = log;\n    this.logsCount++;\n\n    if (log.parent) {\n      const index = this.element.getItemIndex(log.parent.element) + log.parent.getChildren().length;\n      this.element.insertItem(index, log.render());\n      element = this.element.getItem(index);\n    } else {\n      element = this.element.add(log.render());\n    }\n    element.logId = log.id;\n    if (this.autoScroll) {\n      this.scrollToBottom();\n    }\n    if (this.logsCount === 1) {\n      this.channel.emit('select log', log);\n    }\n    return element;\n  }\n\n  getSelectedLog() {\n    return this.selectedLog;\n  }\n\n  scrollToBottom() {\n    this.element.move(this.logsCount);\n  }\n\n  getLogFromElement(element) {\n    return this.logs[element.logId];\n  }\n\n  focus() {\n    this.element.focus();\n  }\n}\n"
  },
  {
    "path": "src/ui/tree.js",
    "content": "// https://github.com/yaronn/blessed-contrib/blob/master/lib/widget/tree.js\nimport blessed from 'blessed';\n\nconst Node = blessed.Node;\nconst Box = blessed.Box;\n\nfunction Tree(options) {\n\n  if (!(this instanceof Node)) {\n    return new Tree(options);\n  }\n\n  options = options || {};\n  options.bold = true;\n  var self = this;\n  this.options = options;\n  this.data = {};\n  this.nodeLines = [];\n  this.lineNbr = 0;\n  Box.call(this, options);\n\n  options.extended = options.extended || false;\n  options.keys = options.keys || ['space','enter'];\n\n  options.template = options.template || {};\n  options.template.extend = options.template.extend || ' [+]';\n  options.template.retract = options.template.retract || ' [-]';\n  options.template.lines = options.template.lines || false;\n\n  this.rows = blessed.list({\n    height: 0,\n    top: 1,\n    width: 0,\n    left: 1,\n    selectedFg: 'black',\n    selectedBg: 'white',\n    keys: true,\n    tags: true,\n  });\n\n  this.rows.key(options.keys,function() {\n    self.nodeLines[this.getItemIndex(this.selected)].extended = !self.nodeLines[this.getItemIndex(this.selected)].extended;\n    self.setData(self.data);\n    self.screen.render();\n\n    self.emit('select',self.nodeLines[this.getItemIndex(this.selected)]);\n  });\n\n  this.append(this.rows);\n}\n\nTree.prototype.walk = function(node, treeDepth) {\n  var lines = [];\n\n  if (!node.parent) {\n    node.parent = null;\n  }\n\n  if (treeDepth == '' && node.name) {\n    this.lineNbr = 0;\n    this.nodeLines[this.lineNbr++] = node;\n    lines.push(node.name);\n    treeDepth = ' ';\n  }\n\n  node.depth = treeDepth.length - 1;\n\n  if (node.children && node.extended) {\n\n    var i = 0;\n\n    if (typeof node.children == 'function') {\n      node.childrenContent = node.children(node);\n    }\n\n    if (!node.childrenContent) {\n      node.childrenContent = node.children;\n    }\n\n    for (var child in node.childrenContent) {\n\n      if (!node.childrenContent[child].name) {\n        node.childrenContent[child].name = child;\n      }\n\n      var childIndex = child;\n      child = node.childrenContent[child];\n      child.parent = node;\n      child.position = i++;\n\n      if (typeof child.extended == 'undefined') {\n        child.extended = this.options.extended;\n      }\n\n      if (typeof child.children == 'function') {\n        child.childrenContent = child.children(child);\n      } else {\n        child.childrenContent = child.children;\n      }\n\n      var isLastChild = child.position == Object.keys(child.parent.childrenContent).length - 1;\n      var tree;\n      var suffix = '';\n      if (isLastChild) {\n        tree = '└';\n      } else {\n        tree = '├';\n      }\n      if (!child.childrenContent || Object.keys(child.childrenContent).length == 0) {\n        tree += '─';\n      } else if (child.extended) {\n        tree += '┬';\n        suffix = this.options.template.retract;\n      } else {\n        tree += '─';\n        suffix = this.options.template.extend;\n      }\n\n      if (!this.options.template.lines) {\n        tree = '|-';\n      }\n\n      lines.push(treeDepth + tree + child.name + suffix);\n\n      this.nodeLines[this.lineNbr++] = child;\n\n      var parentTree;\n      if (isLastChild || !this.options.template.lines) {\n        parentTree = treeDepth + ' ';\n      } else {\n        parentTree = treeDepth + '│';\n      }\n      lines = lines.concat(this.walk(child, parentTree));\n    }\n  }\n  return lines;\n};\n\nTree.prototype.focus = function() {\n  this.rows.focus();\n};\n\nTree.prototype.render = function() {\n  if (this.screen.focused == this.rows) {\n    this.rows.focus();\n  }\n\n  this.rows.width = this.width - 3;\n  this.rows.height = this.height - 3;\n  Box.prototype.render.call(this);\n};\n\nTree.prototype.setData = function(data) {\n\n  var formatted = [];\n  formatted = this.walk(data,'');\n\n  this.data = data;\n  this.rows.setItems(formatted);\n};\n\nTree.prototype.__proto__ = Box.prototype;\n\nTree.prototype.type = 'tree';\n\nexport default Tree;\n"
  },
  {
    "path": "test/.eslintrc",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"rules\": {\n    \"strict\": 0,\n    \"quotes\": [2, \"single\"],\n    \"no-unused-expressions\": 0\n  },\n  \"env\": {\n    \"browser\": true,\n    \"node\": true,\n    \"mocha\": true\n  },\n  \"globals\": {\n    \"spy\": true,\n    \"expect\": true\n  }\n}"
  },
  {
    "path": "test/runner.html",
    "content": "<!doctype html>\n<html lang='en'>\n<head>\n  <meta charset='utf-8'>\n  <title>Tests</title>\n  <link rel='stylesheet' href='../node_modules/mocha/mocha.css' />\n\n  <!-- Polyfill (required by Babel) -->\n  <script src='../node_modules/babel-core/browser-polyfill.js'></script>\n\n  <!-- Testing libraries -->\n  <script src='../node_modules/mocha/mocha.js'></script>\n  <script src='../node_modules/chai/chai.js'></script>\n  <script src='../node_modules/sinon/pkg/sinon.js'></script>\n  <script src='../node_modules/sinon-chai/lib/sinon-chai.js'></script>\n\n  <!-- Livereload -->\n  <script src='http://localhost:35729/livereload.js'></script>\n\n  <!-- Load the built library -->\n  <script src='../tmp/__spec-build.js'></script>\n</head>\n<body>\n  <!-- Required for browser reporter -->\n  <div id='mocha'></div>\n</body>\n</html>\n"
  },
  {
    "path": "test/setup/browserify.js",
    "content": "var config = require('../../package.json').babelBoilerplateOptions;\n\nglobal.mocha.setup('bdd');\nglobal.onload = function() {\n  global.mocha.checkLeaks();\n  global.mocha.globals(config.mochaGlobals);\n  global.mocha.run();\n  require('./setup')();\n};\n"
  },
  {
    "path": "test/setup/node.js",
    "content": "global.chai = require('chai');\nglobal.sinon = require('sinon');\nglobal.chai.use(require('sinon-chai'));\n\nrequire('babel-core/register');\nrequire('./setup')();\n"
  },
  {
    "path": "test/setup/setup.js",
    "content": "module.exports = function() {\n  global.expect = global.chai.expect;\n\n  beforeEach(function() {\n    this.sandbox = global.sinon.sandbox.create();\n    global.stub = this.sandbox.stub.bind(this.sandbox);\n    global.spy = this.sandbox.spy.bind(this.sandbox);\n  });\n\n  afterEach(function() {\n    delete global.stub;\n    delete global.spy;\n    this.sandbox.restore();\n  });\n};\n"
  },
  {
    "path": "test/unit/investigator.js",
    "content": "import investigator from '../../src/investigator';\n\ndescribe('investigator', () => {\n  // TODO\n});\n"
  }
]