Full Code of risq/investigator for AI

master 9372477a46cf cached
28 files
78.7 KB
21.1k tokens
79 symbols
1 requests
Download .txt
Repository: risq/investigator
Branch: master
Commit: 9372477a46cf
Files: 28
Total size: 78.7 KB

Directory structure:
gitextract_95_54p6y/

├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .jscsrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist/
│   └── investigator.js
├── examples/
│   ├── example.js
│   └── index.js
├── gulpfile.js
├── package.json
├── src/
│   ├── agent.js
│   ├── investigator.js
│   └── ui/
│       ├── index.js
│       ├── inspector.js
│       ├── logDetails.js
│       ├── logItem.js
│       ├── logsList.js
│       └── tree.js
└── test/
    ├── .eslintrc
    ├── runner.html
    ├── setup/
    │   ├── browserify.js
    │   ├── node.js
    │   └── setup.js
    └── unit/
        └── investigator.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  "blacklist": ["useStrict"]
}

================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: http://EditorConfig.org

root = true;

[*]
#  Ensure there's no lingering whitespace
trim_trailing_whitespace = true
# Ensure a newline at the end of each file
insert_final_newline = true

[*.js]
# Unix-style newlines
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 2

================================================
FILE: .eslintrc
================================================
{
  "parser": "babel-eslint",
  "rules": {
    "strict": 0,
    "quotes": [2, "single"]
  },
  "env": {
    "browser": false,
    "node": true
  }
}


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directory
# Commenting this out is preferred by some people, see
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
bower_components
coverage
tmp

# Users Environment Variables
.lock-wscript


================================================
FILE: .jscsrc
================================================
{
  "preset": "google",
  "maximumLineLength": null,
  "esnext": true
}


================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
  - "0.10"
  - "0.12"
  - "io.js"
sudo: false
script: "gulp coverage"
after_success:
  - npm install -g codeclimate-test-reporter
  - codeclimate-test-reporter < coverage/lcov.info


================================================
FILE: CHANGELOG.md
================================================
### [0.0.1](https://github.com/risq/investigator/releases/tag/v0.0.1)

- The first release

================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2015 risq <valentin.ledrapier@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# investigator
Interactive and asynchronous logging tool for Node.js. An easier way to log & debug complex requests directly from the command line. Still experimental !

![investigator](https://cloud.githubusercontent.com/assets/5665322/10861471/d38bedda-7f80-11e5-9bb7-19801c14c961.gif)

## Usage
#### Nodes
`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.

![investigator-nodes](https://cloud.githubusercontent.com/assets/5665322/10861540/267ff6e6-7f84-11e5-847a-5b7d395dfb34.png)

```js
import {agent} from 'investigator';

const requestAgent = agent('request');
const getUserAgent = requestAgent.child('getUser')
  .log('Retrieving user from db...');

// ...

getUserAgent.success('Done !');
// Or: requestAgent.child('getUser').success('Done !');
```

#### Asynchronous logging
`async` agents are particular nodes which may be resolved or rejected to provide a feedback of their fulfillment.

![investigator-async](https://cloud.githubusercontent.com/assets/5665322/10861606/e00908b2-7f86-11e5-862a-ab56505d3ee3.png)

```js
import {agent} from 'investigator';

const requestAgent = agent('request');

// Creates an async child agent
const getUserAgent = requestAgent.async('getUser')
  .log('Retrieving user from db...');

myAsynchronousFunction().then(() => {
  getUserAgent.resolve('Done !');
}).catch((err) => {
  getUserAgent.reject(err);
});
```

#### Inspector
`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.

![investigator-inspector](https://cloud.githubusercontent.com/assets/5665322/10861607/e00c7506-7f86-11e5-8bd8-d3ae7a072c9d.png)

## Installing
Use `npm install investigator` to install locally. See [Usage](#usage) and [API Reference](#api-reference) for more information.

## Shortcuts
In the command line interface, the following shortcuts are available:
- Scroll up and down with `up arrow`, `down arrow`, or mouse wheel. You may also click on a row to select it.
- Open **Inspector** with `i` (inspect the currently selected row).
- Scroll to bottom with `b`
- Enable auto-scroll with `s`. Disable by pressing an arrow key.

## Testing & developing
Clone the project with `git clone git@github.com:risq/investigator.git`.

Install dependencies with `npm install`.

Launch the example with `node examples/index.js`.

You can build the project (transpiling to ES5) with `npm run build`.

## TODO (non-exhaustive list)
- [ ] 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`.
- [ ] Improve UI, navigation & controls in the CLI.
- [ ] Add some performance monitoring.
- [ ] Improve CLI performance for long time logging (avoid memory leaks).
- [ ] Allow client-side logging via WebSockets.

## API Reference
### Investigator
##### `investigator.agent(String name [, data])` -> `Agent`
Creates 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.

```js
import {agent} from 'investigator';

onRequest(req, res) {
  const requestAgent = agent('request', req.id, req.url);
}
```

### Agent
##### `agent.log(data [, data])` -> `Agent`
Log passed data parameters under the given agent node. Returns the same agent (so it can be chained).

```js
import {agent} from 'investigator';

onRequest(req, res) {
  const requestAgent = agent('request', req.id, req.url);
  requestAgent.log('Hello')
    .log('World');
}
```

##### `agent.success(data [, data])` -> `Agent`
Log passed data parameters under the given agent node, as a success (displayed in green). Returns the same agent (so it can be chained).

##### `agent.warn(data [, data])` -> `Agent`
Log passed data parameters under the given agent node, as a warning (displayed in yellow). Returns the same agent (so it can be chained).

##### `agent.error(data [, data])` -> `Agent`
Log passed data parameters under the given agent node, as an error (displayed in red). Returns the same agent (so it can be chained).

##### `agent.child(name [, data])` -> `Agent`
Returns 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.

Data objects can be passed as parameters and will be logged on the child's context.

```js
import {agent} from 'investigator';

onRequest(req, res) {
  const requestAgent = agent('request', req.id, req.url);

  if (req.url === '/user/login') {
    requestAgent.child('login', 'Logging in...');

    if (validate(req.user, req.password)) {
      requestAgent.child('login')
        .success('Login data validated !');
    } else {
      requestAgent.child('login')
        .error('Error validating user data.')
    }
  }
}
```

##### `agent.async(name [, data])` -> `Agent`
Returns 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.

Data objects can be passed as parameters and will be logged on the child's context.

An async agent has `.resolve()` and `.reject()` methods, to keep track of its fulfillment.

```js
import {agent} from 'investigator';

onRequest(req, res) {
  const requestAgent = agent('request', req.id, req.url);

  if (req.url === '/user/login') {
    requestAgent.child('login', 'Logging in...');

    authUser(req.user, req.password).then(() => {
      requestAgent.child('login')
        .success('Authentication succeeded !');
    }).catch((err) => {
      requestAgent.child('login')
        .error('Error validating user data.')
    });
  }
}
```

##### `agent.resolve(data [, data])` -> `Agent`
Resolves an **async** agent. Log data parameters under the given agent node, as a success. Returns the same agent (so it can be chained).

An async agent can only be resolved or rejected once.

##### `agent.reject(data [, data])` -> `Agent`
Resolves an **async** agent. Log data parameters under the given agent node, as an error. Returns the same agent (so it can be chained).

An async agent can only be resolved or rejected once.

## Contributing
Feel free to contribute ! Issues and pull requests are highly welcomed and appreciated.

## License
The MIT License (MIT)

Copyright (c) 2015 Valentin Ledrapier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: dist/investigator.js
================================================
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 _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

(function (global, factory) {
  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);
})(this, function (transceiver, blessed, dateFormat, _prune, path, appRoot, shortid, stack_trace) {
  'use strict';

  var LogsList = (function () {
    function LogsList() {
      var _this = this;

      _classCallCheck(this, LogsList);

      this.selectedLog = null;
      this.logs = {};
      this.logsCount = 0;
      this.channel = transceiver.channel('log');
      this.autoScroll = true;
      this.element = blessed.list({
        top: '0',
        left: '0',
        bottom: 7,
        tags: true,
        keys: true,
        mouse: true,
        scrollbar: {
          bg: 'magenta'
        },
        style: {
          selected: {
            fg: 'black',
            bg: 'white'
          }
        }
      });

      this.element.key(['up', 'down', 's', 'b'], function (ch, key) {
        if (key.name === 's') {
          _this.autoScroll = !_this.autoScroll;
        } else if (key.name === 'b') {
          _this.scrollToBottom();
          transceiver.request('ui', 'render');
        } else {
          _this.autoScroll = false;
        }
      });

      this.element.on('select item', function (element, i) {
        _this.selectedLog = _this.getLogFromElement(element);
        if (_this.selectedLog) {
          _this.channel.emit('select log', _this.selectedLog);
        }
      });

      this.channel.reply({
        addLog: this.addLog,
        getSelectedLog: this.getSelectedLog
      }, this);
    }

    _createClass(LogsList, [{
      key: 'addLog',
      value: function addLog(log) {
        var element = undefined;

        this.logs[log.id] = log;
        this.logsCount++;

        if (log.parent) {
          var index = this.element.getItemIndex(log.parent.element) + log.parent.getChildren().length;
          this.element.insertItem(index, log.render());
          element = this.element.getItem(index);
        } else {
          element = this.element.add(log.render());
        }
        element.logId = log.id;
        if (this.autoScroll) {
          this.scrollToBottom();
        }
        if (this.logsCount === 1) {
          this.channel.emit('select log', log);
        }
        return element;
      }
    }, {
      key: 'getSelectedLog',
      value: function getSelectedLog() {
        return this.selectedLog;
      }
    }, {
      key: 'scrollToBottom',
      value: function scrollToBottom() {
        this.element.move(this.logsCount);
      }
    }, {
      key: 'getLogFromElement',
      value: function getLogFromElement(element) {
        return this.logs[element.logId];
      }
    }, {
      key: 'focus',
      value: function focus() {
        this.element.focus();
      }
    }]);

    return LogsList;
  })();

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

      this.channel = transceiver.channel('log');
      this.element = blessed.box({
        height: 6,
        left: '0',
        bottom: 0,
        tags: true,
        keys: true,
        padding: {
          left: 1,
          right: 1
        },
        style: {
          selected: {
            fg: 'black',
            bg: 'white',
            border: {
              fg: 'white'
            },
            hover: {
              bg: 'green'
            }
          }
        }
      });

      this.channel.on('select log', this.updateLogDetails.bind(this));
    }

    // https://github.com/yaronn/blessed-contrib/blob/master/lib/widget/tree.js

    _createClass(logDetails, [{
      key: 'updateLogDetails',
      value: function updateLogDetails(log) {
        this.element.setContent(this.renderType(log) + this.renderId(log) + this.renderDate(log) + this.renderDuration(log) + this.renderData(log));
      }
    }, {
      key: 'renderType',
      value: function renderType(log) {
        if (log.type === 'root') {
          return '{magenta-fg}{bold}ROOT NODE{/bold}{/magenta-fg}\n';
        }
        if (log.type === 'success') {
          return '{green-fg}✔ {bold}SUCCESS{/bold}{/green-fg}\n';
        }
        if (log.type === 'error') {
          return '{red-fg}✘ {bold}ERROR{/bold}{/red-fg}\n';
        }
        if (log.type === 'warn') {
          return '{yellow-fg}! {bold}WARN{/bold}{/red-fg}\n';
        }
        if (log.type === 'node') {
          return '{grey-fg}{bold}NODE{/bold}{/grey-fg}\n';
        }
        if (log.type === 'async') {
          if (log.status === 'resolved') {
            return '{bold}{green-fg}ASYNC NODE{/bold} (RESOLVED ✔){/green-fg}\n';
          }
          if (log.status === 'rejected') {
            return '{bold}{red-fg}ASYNC NODE{/bold} (REJECTED ✘){/red-fg}\n';
          }
          if (log.status === 'pending') {
            return '{cyan-fg}{bold}ASYNC NODE{/bold} (PENDING ⌛){/cyan-fg}\n';
          }
        }
        if (log.type === 'info') {
          return '{white-fg}{bold}INFO{/bold}{/white-fg}\n';
        }
        return '';
      }
    }, {
      key: 'renderId',
      value: function renderId(log) {
        return '{bold}ID:{/bold} {underline}' + log.id + '{/underline}\n';
      }
    }, {
      key: 'renderDate',
      value: function renderDate(log) {
        return '{bold}TIME:{/bold} {magenta-fg}' + dateFormat(log.date, 'dddd, mmmm dS yyyy, HH:MM:ss.L') + '{/magenta-fg}\n';
      }
    }, {
      key: 'renderDuration',
      value: function renderDuration(log) {
        if (log.relativeDuration && log.previousLog) {
          return '{bold}DURATION:{/bold} {yellow-fg}' + log.relativeDuration + '{/yellow-fg} (from {underline}' + log.previousLog.id + '{/underline})\n';
        }
        return '';
      }
    }, {
      key: 'renderData',
      value: function renderData(log) {
        if (log.data) {
          return '{bold}DATA:{/bold} ' + log.renderData() + '\n';
        }
        return '';
      }
    }]);

    return logDetails;
  })();

  var Node = blessed.Node;
  var Box = blessed.Box;

  function Tree(options) {

    if (!(this instanceof Node)) {
      return new Tree(options);
    }

    options = options || {};
    options.bold = true;
    var self = this;
    this.options = options;
    this.data = {};
    this.nodeLines = [];
    this.lineNbr = 0;
    Box.call(this, options);

    options.extended = options.extended || false;
    options.keys = options.keys || ['space', 'enter'];

    options.template = options.template || {};
    options.template.extend = options.template.extend || ' [+]';
    options.template.retract = options.template.retract || ' [-]';
    options.template.lines = options.template.lines || false;

    this.rows = blessed.list({
      height: 0,
      top: 1,
      width: 0,
      left: 1,
      selectedFg: 'black',
      selectedBg: 'white',
      keys: true,
      tags: true
    });

    this.rows.key(options.keys, function () {
      self.nodeLines[this.getItemIndex(this.selected)].extended = !self.nodeLines[this.getItemIndex(this.selected)].extended;
      self.setData(self.data);
      self.screen.render();

      self.emit('select', self.nodeLines[this.getItemIndex(this.selected)]);
    });

    this.append(this.rows);
  }

  Tree.prototype.walk = function (node, treeDepth) {
    var lines = [];

    if (!node.parent) {
      node.parent = null;
    }

    if (treeDepth == '' && node.name) {
      this.lineNbr = 0;
      this.nodeLines[this.lineNbr++] = node;
      lines.push(node.name);
      treeDepth = ' ';
    }

    node.depth = treeDepth.length - 1;

    if (node.children && node.extended) {

      var i = 0;

      if (typeof node.children == 'function') {
        node.childrenContent = node.children(node);
      }

      if (!node.childrenContent) {
        node.childrenContent = node.children;
      }

      for (var child in node.childrenContent) {

        if (!node.childrenContent[child].name) {
          node.childrenContent[child].name = child;
        }

        var childIndex = child;
        child = node.childrenContent[child];
        child.parent = node;
        child.position = i++;

        if (typeof child.extended == 'undefined') {
          child.extended = this.options.extended;
        }

        if (typeof child.children == 'function') {
          child.childrenContent = child.children(child);
        } else {
          child.childrenContent = child.children;
        }

        var isLastChild = child.position == Object.keys(child.parent.childrenContent).length - 1;
        var tree;
        var suffix = '';
        if (isLastChild) {
          tree = '└';
        } else {
          tree = '├';
        }
        if (!child.childrenContent || Object.keys(child.childrenContent).length == 0) {
          tree += '─';
        } else if (child.extended) {
          tree += '┬';
          suffix = this.options.template.retract;
        } else {
          tree += '─';
          suffix = this.options.template.extend;
        }

        if (!this.options.template.lines) {
          tree = '|-';
        }

        lines.push(treeDepth + tree + child.name + suffix);

        this.nodeLines[this.lineNbr++] = child;

        var parentTree;
        if (isLastChild || !this.options.template.lines) {
          parentTree = treeDepth + ' ';
        } else {
          parentTree = treeDepth + '│';
        }
        lines = lines.concat(this.walk(child, parentTree));
      }
    }
    return lines;
  };

  Tree.prototype.focus = function () {
    this.rows.focus();
  };

  Tree.prototype.render = function () {
    if (this.screen.focused == this.rows) {
      this.rows.focus();
    }

    this.rows.width = this.width - 3;
    this.rows.height = this.height - 3;
    Box.prototype.render.call(this);
  };

  Tree.prototype.setData = function (data) {

    var formatted = [];
    formatted = this.walk(data, '');

    this.data = data;
    this.rows.setItems(formatted);
  };

  Tree.prototype.__proto__ = Box.prototype;

  Tree.prototype.type = 'tree';

  var ui_tree = Tree;

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

      this.channel = transceiver.channel('log');

      this.element = ui_tree({
        top: 'center',
        left: 'center',
        width: '90%',
        height: '75%',
        hidden: true,
        label: 'Inspector',
        tags: true,
        border: {
          type: 'line'
        },
        style: {
          fg: 'white',
          border: {
            fg: '#f0f0f0'
          }
        },
        template: {
          extend: '{bold}{green-fg} [+]{/}',
          retract: '{bold}{yellow-fg} [-]{/}',
          lines: true
        }
      });
    }

    _createClass(Inspector, [{
      key: 'open',
      value: function open(selectedLog) {
        if (!selectedLog || !selectedLog.data && !selectedLog.stackTrace) {
          return;
        }
        this.opened = true;
        this.element.show();
        this.element.focus();
        this.element.setData(this.prepareData(selectedLog));
      }
    }, {
      key: 'close',
      value: function close() {
        this.opened = false;
        this.element.hide();
      }
    }, {
      key: 'prepareData',
      value: function prepareData(log) {
        var content = {};
        if (log.data) {
          content.data = JSON.parse(_prune(log.data, {
            depthDecr: 7,
            replacer: function replacer(value, defaultValue, circular) {
              if (typeof value === 'function') {
                return '"Function [pruned]"';
              }
              if (Array.isArray(value)) {
                return '"Array (' + value.length + ') [pruned]"';
              }
              if (typeof value === 'object') {
                return '"Object [pruned]"';
              }
              return defaultValue;
            }
          }));
        }

        if (log.stackTrace) {
          content['stack trace'] = log.stackTrace.map(function (callsite) {
            var relativePath = path.relative(appRoot.toString(), callsite.file);
            return {
              type: callsite.type,
              'function': callsite['function'],
              method: callsite.method,
              file: relativePath + ':{yellow-fg}' + callsite.line + '{/yellow-fg}:{yellow-fg}' + callsite.column + '{/yellow-fg}'
            };
          });
        }
        return this.formatData(content);
      }
    }, {
      key: 'formatData',
      value: function formatData(data, key) {
        var _this2 = this;

        var depth = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2];

        depth++;
        if (typeof data === 'object') {
          if (data !== null) {
            var _ret = (function () {
              var name = undefined;
              var extended = undefined;

              if (depth === 2) {
                name = '{yellow-fg}{bold}' + key.toUpperCase() + '{/bold}{/yellow-fg} {magenta-fg}(' + data.length + '){/magenta-fg}';
                extended = key === 'data';
              } else {
                var type = Array.isArray(data) ? '[Array] {magenta-fg}(' + data.length + '){/magenta-fg}' : '[Object]';
                name = '{blue-fg}{bold}' + (key ? key + ' ' : '') + '{/bold}' + type + '{/blue-fg}';
                extended = depth < 4;
              }
              var newObj = {
                children: {},
                name: name,
                extended: extended
              };
              Object.keys(data).forEach(function (key) {
                var child = _this2.formatData(data[key], key, depth);
                if (child) {
                  newObj.children[key] = child;
                }
              });
              return {
                v: newObj
              };
            })();

            if (typeof _ret === 'object') return _ret.v;
          }
        }
        if (typeof data === 'function') {
          return {
            name: '{blue-fg}' + key + '{/blue-fg}: {red-fg}{bold}[Function]{/}'
          };
        }
        if (typeof data === 'number') {
          return {
            name: '{blue-fg}' + key + '{/blue-fg}: {yellow-fg}' + data + '{/}'
          };
        }
        if (data === null) {
          return {
            name: '{blue-fg}' + key + '{/blue-fg}: {cyan-fg}{bold}null{/}'
          };
        }
        return {
          name: '{blue-fg}' + key + '{/blue-fg}: ' + data
        };
      }
    }]);

    return Inspector;
  })();

  var Ui = (function () {
    function Ui() {
      var _this3 = this;

      _classCallCheck(this, Ui);

      this.channel = transceiver.channel('ui');
      this.screen = blessed.screen({
        smartCSR: true
      });

      this.logsList = new LogsList();
      this.logDetails = new logDetails();
      this.inspector = new Inspector();

      this.separator = blessed.line({
        bottom: 6,
        orientation: 'horizontal'
      });

      this.screen.append(this.logsList.element);
      this.screen.append(this.logDetails.element);
      this.screen.append(this.separator);
      this.screen.append(this.inspector.element);

      this.logsList.element.focus();

      this.screen.key(['q', 'C-c'], function (ch, key) {
        return process.exit(0);
      });

      this.screen.key(['i'], this.toggleInspector.bind(this));

      this.screen.render();

      this.channel.reply('render', function () {
        return _this3.screen.render();
      });
    }

    _createClass(Ui, [{
      key: 'toggleInspector',
      value: function toggleInspector() {
        if (this.inspector.opened) {
          this.inspector.close();
          this.logsList.focus();
        } else {
          this.inspector.open(this.logsList.selectedLog);
        }
        this.screen.render();
      }
    }]);

    return Ui;
  })();

  var LogItem = (function () {
    function LogItem(_ref) {
      var name = _ref.name;
      var type = _ref.type;
      var status = _ref.status;
      var parent = _ref.parent;
      var data = _ref.data;
      var message = _ref.message;
      var stackTrace = _ref.stackTrace;
      var _ref$date = _ref.date;
      var date = _ref$date === undefined ? Date.now() : _ref$date;

      _classCallCheck(this, LogItem);

      this.id = shortid.generate();
      this.name = name;
      this.type = type;
      this.status = status;
      this.data = data;
      this.message = message;
      this.stackTrace = stackTrace;
      this.date = date;
      this.children = [];
      this.channel = transceiver.channel('log');

      if (parent) {
        this.depth = parent.depth + 1;
        this.parent = parent;
        this.previousLog = parent.getLastChild() || parent;
        this.relativeDuration = this.getRelativeDuration();
        this.parent.addChild(this);
      } else {
        this.depth = 0;
      }
      this.element = this.channel.request('addLog', this);
      this.update();
    }

    _createClass(LogItem, [{
      key: 'update',
      value: function update() {
        if (this.element) {
          this.element.content = this.render();
          transceiver.request('ui', 'render');
        }
      }
    }, {
      key: 'render',
      value: function render() {
        var message = '' + this.renderState() + this.renderName() + this.renderMessage() + this.renderData() + this.renderDate() + this.renderDuration();
        for (var i = 0; i < this.depth; i++) {
          message = '    ' + message;
        }
        return message;
      }
    }, {
      key: 'renderState',
      value: function renderState() {
        if (this.type === 'async' && this.status === 'pending') {
          return '{cyan-fg}[⌛]{/cyan-fg} ';
        }
        if (this.type === 'async' && this.status === 'resolved') {
          return '{green-fg}[✔]{/green-fg} ';
        }
        if (this.type === 'async' && this.status === 'rejected') {
          return '{red-fg}[✘]{/red-fg} ';
        }
        if (this.type === 'success') {
          return '{green-fg}✔{/green-fg} ';
        }
        if (this.type === 'error') {
          return '{red-fg}✘{/red-fg} ';
        }
        if (this.type === 'warn') {
          return '{yellow-fg}❗{/yellow-fg} ';
        }
        if (this.type === 'info') {
          return '⇢ ';
        }
        return '';
      }
    }, {
      key: 'renderName',
      value: function renderName() {
        if (this.depth === 0) {
          return this.name ? '{underline}{bold}' + this.name + '{/bold}{/underline} ' : '';
        }
        if (this.type === 'async') {
          if (this.status === 'resolved') {
            return '{bold}{green-fg}' + this.name + '{/green-fg}{/bold} (async) ';
          }
          if (this.status === 'rejected') {
            return '{bold}{red-fg}' + this.name + '{/red-fg}{/bold} (async) ';
          }
          return '{bold}' + this.name + '{/bold} (async) ';
        }
        if (this.type === 'success') {
          return this.name ? '{bold}{green-fg}' + this.name + '{/green-fg}{/bold} ' : '';
        }
        if (this.type === 'error') {
          return this.name ? '{bold}{red-fg}' + this.name + '{/red-fg}{/bold} ' : '';
        }
        if (this.type === 'warn') {
          return this.name ? '{bold}{yellow-fg}' + this.name + '{/yellow-fg}{/bold} ' : '';
        }
        return this.name ? '{bold}' + this.name + '{/bold} ' : '';
      }
    }, {
      key: 'renderData',
      value: function renderData() {
        if (this.depth === 0) {
          // console.log(this.data);
        }
        if (!this.data) {
          return '';
        }
        if (Array.isArray(this.data)) {
          return this.data.map(this.renderValue.bind(this)).join(' ') + ' ';
        }
        return this.renderValue(this.data) + ' ';
      }
    }, {
      key: 'renderValue',
      value: function renderValue(value) {
        if (Array.isArray(value)) {
          return '{cyan-fg}' + this.prune(value) + '{/cyan-fg}';
        }
        if (typeof value === 'object') {
          return '{blue-fg}' + this.prune(value) + '{/blue-fg}';
        }
        if (typeof value === 'function') {
          return '{red-fg}{bold}[Function]{/bold}{red-fg}';
        }
        if (typeof value === 'number') {
          return '{yellow-fg}' + value + '{/yellow-fg}';
        }
        if (typeof value === 'string') {
          if (this.type === 'success') {
            return '{green-fg}' + value + '{/green-fg}';
          }
          if (this.type === 'error') {
            return '{red-fg}' + value + '{/red-fg}';
          }
          if (this.type === 'warn') {
            return '{yellow-fg}' + value + '{/yellow-fg}';
          }
        }
        return value;
      }
    }, {
      key: 'renderMessage',
      value: function renderMessage() {
        if (this.message) {
          if (this.type === 'success') {
            return '{green-fg}' + this.message + '{/green-fg} ';
          }
          if (this.type === 'error') {
            return '{red-fg}' + this.message + '{/red-fg} ';
          }
          if (this.type === 'warn') {
            return '{yellow-fg}' + this.message + '{/yellow-fg} ';
          }
          return this.message + ' ';
        }
        return '';
      }
    }, {
      key: 'renderDate',
      value: function renderDate() {
        if (this.depth === 0) {
          return '{magenta-fg}(' + dateFormat(this.date, 'dd/mm/yyyy HH:MM:ss.L') + '){/magenta-fg} ';
        }
        return '';
      }
    }, {
      key: 'renderDuration',
      value: function renderDuration() {
        if (this.relativeDuration) {
          return '{grey-fg}+' + this.relativeDuration + '{/grey-fg} ';
        }
        return '';
      }
    }, {
      key: 'getRelativeDuration',
      value: function getRelativeDuration() {
        return this.humanizeDuration(this.date - this.previousLog.date);
      }
    }, {
      key: 'humanizeDuration',
      value: function humanizeDuration(duration) {
        if (duration < 1000) {
          return duration + 'ms';
        }
        if (duration < 60000) {
          var milliseconds = duration % 1000;
          milliseconds = ('000' + milliseconds).slice(-3);
          return Math.floor(duration / 1000) + '.' + milliseconds + 's';
        }
        return Math.floor(duration / 60000) + 'm ' + Math.round(duration % 60000 / 1000) + 's';
      }
    }, {
      key: 'addChild',
      value: function addChild(log) {
        this.children.push(log);
      }
    }, {
      key: 'getLastChild',
      value: function getLastChild() {
        return this.children[this.children.length - 1];
      }
    }, {
      key: 'getChildren',
      value: function getChildren(list) {
        list = list || [];
        list.push.apply(list, this.children);
        this.children.forEach(function (child) {
          child.getChildren(list);
        });
        return list;
      }
    }, {
      key: 'setStatus',
      value: function setStatus(status) {
        this.status = status;
        this.update();
      }
    }, {
      key: 'prune',
      value: function prune(value) {
        return _prune(value, {
          depthDecr: 2,
          arrayMaxLength: 8,
          prunedString: ' [...]'
        });
      }
    }]);

    return LogItem;
  })();

  var Agent = (function () {
    function Agent(_ref2) {
      var name = _ref2.name;
      var type = _ref2.type;
      var status = _ref2.status;
      var data = _ref2.data;
      var message = _ref2.message;
      var _ref2$isAsync = _ref2.isAsync;
      var isAsync = _ref2$isAsync === undefined ? false : _ref2$isAsync;
      var ancestors = _ref2.ancestors;

      _classCallCheck(this, Agent);

      this.name = name;
      this.children = {};
      this.isAsync = isAsync;
      this.asyncState = this.isAsync ? 'pending' : null;
      this.type = type;
      this.status = status;

      if (!ancestors) {
        this.ancestors = [];
        this.isRoot = true;
      } else {
        this.ancestors = ancestors;
        this.parent = this.ancestors[this.ancestors.length - 1];
      }

      this.logItem = new LogItem({
        name: this.name,
        type: this.type,
        status: this.status,
        parent: this.parent ? this.parent.logItem : null,
        data: data,
        message: message,
        stackTrace: this.generateStackTrace(stack_trace.get())
      });

      return this;
    }

    _createClass(Agent, [{
      key: 'log',
      value: function log() {
        for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
          args[_key] = arguments[_key];
        }

        new Agent({
          type: 'info',
          data: args,
          ancestors: this.ancestors.concat(this)
        });
        return this;
      }
    }, {
      key: 'warn',
      value: function warn() {
        for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
          args[_key2] = arguments[_key2];
        }

        new Agent({
          type: 'warn',
          data: args,
          ancestors: this.ancestors.concat(this)
        });
        return this;
      }
    }, {
      key: 'success',
      value: function success() {
        for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
          args[_key3] = arguments[_key3];
        }

        new Agent({
          type: 'success',
          data: args,
          ancestors: this.ancestors.concat(this)
        });
        return this;
      }
    }, {
      key: 'error',
      value: function error() {
        for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
          args[_key4] = arguments[_key4];
        }

        new Agent({
          type: 'error',
          data: args,
          ancestors: this.ancestors.concat(this)
        });
        return this;
      }
    }, {
      key: 'child',
      value: function child(name) {
        if (!this.children[name]) {
          this.children[name] = new Agent({
            name: name,
            type: 'node',
            ancestors: this.ancestors.concat(this)
          });
        }

        for (var _len5 = arguments.length, args = Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) {
          args[_key5 - 1] = arguments[_key5];
        }

        if (args.length) {
          var _children$name;

          (_children$name = this.children[name]).log.apply(_children$name, args);
        }
        return this.children[name];
      }
    }, {
      key: 'async',
      value: function async(name) {
        if (!this.children[name]) {
          this.children[name] = new Agent({
            name: name,
            type: 'async',
            status: 'pending',
            isAsync: true,
            ancestors: this.ancestors.concat(this)
          });
        }
        if (!this.children[name].isAsync) {
          this.internalWarn('Child agent {bold}' + name + '{/bold} is defined as a non async agent');
        }

        for (var _len6 = arguments.length, args = Array(_len6 > 1 ? _len6 - 1 : 0), _key6 = 1; _key6 < _len6; _key6++) {
          args[_key6 - 1] = arguments[_key6];
        }

        if (args.length) {
          var _children$name2;

          (_children$name2 = this.children[name]).log.apply(_children$name2, args);
        }
        return this.children[name];
      }
    }, {
      key: 'resolve',
      value: function resolve() {
        if (this.isAsync) {
          if (this.logItem.status === 'pending') {
            this.logItem.setStatus('resolved');
            var resolveLog = new Agent({
              name: this.name,
              type: 'success',
              message: 'resolved',
              ancestors: this.ancestors.concat(this)
            });

            for (var _len7 = arguments.length, args = Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
              args[_key7] = arguments[_key7];
            }

            if (args.length) {
              resolveLog.success.apply(resolveLog, args);
            }
          } else {
            this.internalWarn('Trying to resolve an already {bold}' + this.logItem.status + '{/bold} async agent');
          }
        } else {
          this.internalWarn('Trying to resolve a non async agent');
        }
        return this;
      }
    }, {
      key: 'reject',
      value: function reject() {
        if (this.isAsync) {
          if (this.logItem.status === 'pending') {
            this.logItem.setStatus('rejected');
            var rejectLog = new Agent({
              name: this.name,
              type: 'error',
              message: 'rejected',
              ancestors: this.ancestors.concat(this)
            });

            for (var _len8 = arguments.length, args = Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
              args[_key8] = arguments[_key8];
            }

            if (args.length) {
              rejectLog.error.apply(rejectLog, args);
            }
          } else {
            this.internalWarn('Trying to reject an already {bold}' + this.logItem.status + '{/bold} async agent');
          }
        } else {
          this.internalWarn('Trying to reject a non async agent');
        }
        return this;
      }
    }, {
      key: 'internalWarn',
      value: function internalWarn(message) {
        new Agent({
          name: this.name,
          type: 'warn',
          message: message,
          ancestors: this.ancestors.concat(this)
        });
      }
    }, {
      key: 'getAncestorsNames',
      value: function getAncestorsNames() {
        return this.ancestors.map(function (ancestor) {
          return ancestor.name;
        });
      }
    }, {
      key: 'generateStackTrace',
      value: function generateStackTrace(trace) {
        var stackTrace = [];
        for (var i = 0; i < 5; i++) {
          stackTrace.push({
            type: trace[i].getTypeName(),
            'function': trace[i].getFunctionName(),
            method: trace[i].getMethodName(),
            file: trace[i].getFileName(),
            line: trace[i].getLineNumber(),
            column: trace[i].getColumnNumber()
          });
        }
        return stackTrace;
      }
    }]);

    return Agent;
  })();

  var agent = function agent(name) {
    for (var _len9 = arguments.length, args = Array(_len9 > 1 ? _len9 - 1 : 0), _key9 = 1; _key9 < _len9; _key9++) {
      args[_key9 - 1] = arguments[_key9];
    }

    return new Agent({
      name: name,
      type: 'root',
      data: args.length ? args : undefined
    });
  };

  transceiver.setPromise(null);

  var ui = new Ui();

  var investigator = { ui: ui, agent: agent };

  return investigator;
});
//# sourceMappingURL=investigator.js.map


================================================
FILE: examples/example.js
================================================
import investigator from '../src/investigator';

let i = 1;
setTimeout(runScraping, 2000);

function runScraping() {
  if (i < 5) {
    i++;
    scrapPage('http://example.com');
    setTimeout(() => {
      runScraping();
    }, Math.random() * 15000);
  }
}

function scrapPage(url) {
  const agent = investigator.agent('scraping page', url)
    .async('getData');

  agent.async('downloadPage', url);
  downloadPage(url).then((res) => {
    agent.child('downloadPage')
      .log('status', res.status)
      .resolve(res.size);

    const info = getPageInfo(res);
    agent.child('getPageInfo')
      .log('id:', info.id)
      .log('title:', info.title)
      .log('metas:', info.metas);

    return Promise.all([
      getVideos(res.data.videos, agent),
      getPostsData(res.data.posts, agent),
    ]);
  }).then(() => {
    agent.resolve('Well done !');
  });
}

function getPostsData(posts, agent) {
  agent.async('getPosts');
  return Promise.all(
    posts.map((post) => {
      const postAgent = agent.child('getPosts')
        .child(`post #${post.id}`)
        .log('Retrieving post data...');
      return Promise.all([
        downloadPostImage(post).then((img) => {
          postAgent.success('Image downloaded - status:', img.status);
          return img;
        }).catch((err) => postAgent.error(err)),
        getPostComments(post).then((comments) => {
          if (comments.length) {
            postAgent.success(`Comments retrieved (${comments.length})`, comments);
          } else {
            postAgent.warn(`No comment found`);
          }
          return comments;
        }),
      ]);
    }))
    .then((posts) => {
      agent.child('getPosts').resolve(`${posts.length} posts retrieved`);
    });
}

function getVideos(videos, agent) {
  agent.async('getVideos')
    .log('Downloading', videos.length, 'videos');

  return Promise.all(
    videos.map((video) => {
      return downloadVideo(video)
        .then((data) => {
          agent.child('getVideos')
            .success('video', video.id, 'downloaded');
        });
    }))
    .then((videos) => {
      agent.child('getVideos').resolve(`${videos.length} videos retrieved`)
        .log(videos);
    })
    .catch((err) => {
      agent.child('getVideos').reject(err);
    });
}

function downloadPage(url) {
  return new Promise((resolve) => setTimeout(resolve, Math.random() * 10000, {
    status: 200,
    size: `${Math.round(Math.random() * 30)}kb`,
    data: {
      posts: [{
          id: Math.round(Math.random() * 1000),
          img: null
        }, {
          id: Math.round(Math.random() * 1000),
          img: 'img2.png'
        }, {
          id: Math.round(Math.random() * 1000),
          img: 'img3.png'
        }],
      videos: [{
          id: Math.round(Math.random() * 1000),
          url: 'video1.mp4'
        }, {
          id: Math.round(Math.random() * 1000),
          url: 'video2.mp4'
        }, {
          id: Math.round(Math.random() * 1000),
          url: null
        },{
          id: Math.round(Math.random() * 1000),
          url: null
        }],
    },
  }));
}

function getPageInfo(page) {
  return {
    id: Math.round(Math.random() * 100),
    title: 'Lorem ipsum',
    metas: fakeMetas,
  };
}

function downloadPostImage(post) {
  return new Promise((resolve, reject) => post.img ? setTimeout(resolve, Math.random() * 6000, {status: 200}) :
    setTimeout(reject, Math.random() * 10000, 'Error downloading image'));
}

function getPostComments(post) {
  return new Promise((resolve, reject) => setTimeout(resolve, Math.random() * 6000,
    ['Lorem', 'Ipsum'].slice(0, Math.round(Math.random() * 2))
  ));
}

function downloadVideo(video) {
  return new Promise((resolve, reject) => video.url ? setTimeout(resolve, Math.random() * 6000, {status: 200, size: Math.round(Math.random() * 10, 1)}) :
    setTimeout(reject, Math.random() * 12000, 'Error downloading video'));
}

const fakeMetas = {
  facebook: {
    'og:url': 'https://github.com',
    'og:site_name': 'GitHub',
    'og:title': 'Build software better, together',
    '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.',
    'og:image': 'https://assets-cdn.github.com/images/modules/open_graph/github-logo.png',
    'og:image:type': 'image/png',
    'og:image:width': '1200',
    'og:image:height': '1200',
    'og:image': 'https://assets-cdn.github.com/images/modules/open_graph/github-mark.png',
    'og:image:type': 'image/png',
    'og:image:width': '1200',
    'og:image:height': '620',
    'og:image': 'https://assets-cdn.github.com/images/modules/open_graph/github-octocat.png',
    'og:image:type': 'image/png',
    'og:image:width': '1200',
    'og:image:height': '620',
  },
  twitter: {
    'twitter:site': 'github',
    'twitter:site:id': '13334762',
    'twitter:creator': 'github',
    'twitter:creator:id': '13334762',
    'twitter:card': 'summary_large_image',
    'twitter:title': 'GitHub',
    '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.',
    'twitter:image:src': 'https://assets-cdn.github.com/images/modules/open_graph/github-logo.png',
    'twitter:image:width': '1200',
    'twitter:image:height': '1200'
  },
  length: 26,
};


================================================
FILE: examples/index.js
================================================
require('babel-core/register');
require('./example.js');


================================================
FILE: gulpfile.js
================================================
// Load Gulp and all of our Gulp plugins
const gulp = require('gulp');
const $ = require('gulp-load-plugins')();

// Load other npm modules
const del = require('del');
const glob = require('glob');
const path = require('path');
const isparta = require('isparta');
const babelify = require('babelify');
const watchify = require('watchify');
const buffer = require('vinyl-buffer');
const esperanto = require('esperanto');
const browserify = require('browserify');
const runSequence = require('run-sequence');
const source = require('vinyl-source-stream');

// Gather the library data from `package.json`
const manifest = require('./package.json');
const config = manifest.babelBoilerplateOptions;
const mainFile = manifest.main;
const destinationFolder = path.dirname(mainFile);
const exportFileName = path.basename(mainFile, path.extname(mainFile));

// Remove the built files
gulp.task('clean', function(cb) {
  del([destinationFolder], cb);
});

// Remove our temporary files
gulp.task('clean-tmp', function(cb) {
  del(['tmp'], cb);
});

// Send a notification when JSCS fails,
// so that you know your changes didn't build
function jscsNotify(file) {
  if (!file.jscs) { return; }
  return file.jscs.success ? false : 'JSCS failed';
}

function createLintTask(taskName, files) {
  gulp.task(taskName, function() {
    return gulp.src(files)
      .pipe($.plumber())
      .pipe($.eslint())
      .pipe($.eslint.format())
      .pipe($.eslint.failOnError())
      .pipe($.jscs())
      .pipe($.notify(jscsNotify));
  });
}

// Lint our source code
createLintTask('lint-src', ['src/**/*.js']);

// Lint our test code
createLintTask('lint-test', ['test/**/*.js']);

// Build two versions of the library
gulp.task('build', ['lint-src', 'clean'], function(done) {
  esperanto.bundle({
    base: 'src',
    entry: config.entryFileName,
  }).then(function(bundle) {
    var res = bundle.toUmd({
      // Don't worry about the fact that the source map is inlined at this step.
      // `gulp-sourcemaps`, which comes next, will externalize them.
      sourceMap: 'inline',
      name: config.mainVarName
    });

    $.file(exportFileName + '.js', res.code, { src: true })
      .pipe($.plumber())
      .pipe($.sourcemaps.init({ loadMaps: true }))
      .pipe($.babel())
      .pipe($.sourcemaps.write('./'))
      .pipe(gulp.dest(destinationFolder))
      .pipe($.filter(['*', '!**/*.js.map']))
      .pipe($.rename(exportFileName + '.min.js'))
      .pipe($.sourcemaps.init({ loadMaps: true }))
      .pipe($.uglify())
      .pipe($.sourcemaps.write('./'))
      .pipe(gulp.dest(destinationFolder))
      .on('end', done);
  })
  .catch(done);
});

function bundle(bundler) {
  return bundler.bundle()
    .on('error', function(err) {
      console.log(err.message);
      this.emit('end');
    })
    .pipe($.plumber())
    .pipe(source('./tmp/__spec-build.js'))
    .pipe(buffer())
    .pipe(gulp.dest(''))
    .pipe($.livereload());
}

function getBundler() {
  // Our browserify bundle is made up of our unit tests, which
  // should individually load up pieces of our application.
  // We also include the browserify setup file.
  var testFiles = glob.sync('./test/unit/**/*');
  var allFiles = ['./test/setup/browserify.js'].concat(testFiles);

  // Create our bundler, passing in the arguments required for watchify
  var bundler = browserify(allFiles, watchify.args);

  // Watch the bundler, and re-bundle it whenever files change
  bundler = watchify(bundler);
  bundler.on('update', function() {
    bundle(bundler);
  });

  // Set up Babelify so that ES6 works in the tests
  bundler.transform(babelify.configure({
    sourceMapRelative: __dirname + '/src'
  }));

  return bundler;
};

// Build the unit test suite for running tests
// in the browser
gulp.task('browserify', function() {
  return bundle(getBundler());
});

function test() {
  return gulp.src(['test/setup/node.js', 'test/unit/**/*.js'], {read: false})
    .pipe($.mocha({reporter: 'dot', globals: config.mochaGlobals}));
}

gulp.task('coverage', ['lint-src', 'lint-test'], function(done) {
  require('babel-core/register');
  gulp.src(['src/**/*.js'])
    .pipe($.istanbul({instrumenter: isparta.Instrumenter}))
    .pipe($.istanbul.hookRequire())
    .on('finish', function() {
      return test()
        .pipe($.istanbul.writeReports())
        .on('end', done);
    });
});

// Lint and run our tests
gulp.task('test', ['lint-src', 'lint-test'], function() {
  require('babel-core/register');
  return test();
});

// Ensure that linting occurs before browserify runs. This prevents
// the build from breaking due to poorly formatted code.
gulp.task('build-in-sequence', function(callback) {
  runSequence(['lint-src', 'lint-test'], 'browserify', callback);
});

// These are JS files that should be watched by Gulp. When running tests in the browser,
// watchify is used instead, so these aren't included.
const jsWatchFiles = ['src/**/*', 'test/**/*'];
// These are files other than JS files which are to be watched. They are always watched.
const otherWatchFiles = ['package.json', '**/.eslintrc', '.jscsrc'];

// Run the headless unit tests as you make changes.
gulp.task('watch', function() {
  const watchFiles = jsWatchFiles.concat(otherWatchFiles);
  gulp.watch(watchFiles, ['test']);
});

// Set up a livereload environment for our spec runner
gulp.task('test-browser', ['build-in-sequence'], function() {
  $.livereload.listen({port: 35729, host: 'localhost', start: true});
  return gulp.watch(otherWatchFiles, ['build-in-sequence']);
});

// An alias of test
gulp.task('default', ['test']);


================================================
FILE: package.json
================================================
{
  "name": "investigator",
  "version": "0.1.1",
  "description": "Interactive and asynchronous logging tool for Node.js. An easier way to log & debug complex requests directly from the command line. Still experimental !",
  "main": "dist/investigator.js",
  "scripts": {
    "test": "gulp",
    "test-browser": "gulp test-browser",
    "build": "gulp build"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/risq/investigator.git"
  },
  "keywords": [
    "log",
    "logger",
    "logging",
    "debug",
    "debugger",
    "cli",
    "inspect",
    "inspecter"
  ],
  "author": "risq <valentin.ledrapier@gmail.com>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/risq/investigator/issues"
  },
  "homepage": "https://github.com/risq/investigator",
  "devDependencies": {
    "babel-core": "^5.2.17",
    "babel-eslint": "^4.0.5",
    "babelify": "^6.0.0",
    "browserify": "^11.0.1",
    "chai": "^3.2.0",
    "del": "^1.1.1",
    "esperanto": "^0.7.4",
    "glob": "^5.0.14",
    "gulp": "^3.8.10",
    "gulp-babel": "^5.0.0",
    "gulp-eslint": "^1.0.0",
    "gulp-file": "^0.2.0",
    "gulp-filter": "^3.0.0",
    "gulp-istanbul": "^0.10.0",
    "gulp-jscs": "^2.0.0",
    "gulp-livereload": "^3.4.0",
    "gulp-load-plugins": "^0.10.0",
    "gulp-mocha": "^2.0.0",
    "gulp-notify": "^2.1.0",
    "gulp-plumber": "^1.0.1",
    "gulp-rename": "^1.2.0",
    "gulp-sourcemaps": "^1.3.0",
    "gulp-uglify": "^1.2.0",
    "isparta": "^3.0.3",
    "mocha": "^2.1.0",
    "run-sequence": "^1.0.2",
    "sinon": "^1.12.2",
    "sinon-chai": "^2.7.0",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^1.0.0",
    "watchify": "^3.3.1"
  },
  "babelBoilerplateOptions": {
    "entryFileName": "investigator",
    "mainVarName": "investigator",
    "mochaGlobals": [
      "stub",
      "spy",
      "expect"
    ]
  },
  "dependencies": {
    "app-root-path": "^1.0.0",
    "blessed": "^0.1.81",
    "dateformat": "^1.0.11",
    "json-prune": "^1.0.1",
    "shortid": "^2.2.4",
    "stack-trace": "0.0.9",
    "transceiver": "^0.3.4"
  }
}


================================================
FILE: src/agent.js
================================================
import shortid from 'shortid';
import transceiver from 'transceiver';
import stackTrace from 'stack-trace';

import LogItem from './ui/logItem';

class Agent {
  constructor({name, type, status, data, message, isAsync = false, ancestors}) {
    this.name = name;
    this.children = {};
    this.isAsync = isAsync;
    this.asyncState = this.isAsync ? 'pending' : null;
    this.type = type;
    this.status = status;

    if (!ancestors) {
      this.ancestors = [];
      this.isRoot = true;
    } else {
      this.ancestors = ancestors;
      this.parent = this.ancestors[this.ancestors.length - 1];
    }

    this.logItem = new LogItem({
      name: this.name,
      type: this.type,
      status: this.status,
      parent: this.parent ? this.parent.logItem : null,
      data: data,
      message: message,
      stackTrace: this.generateStackTrace(stackTrace.get()),
    });

    return this;
  }

  log(...args) {
    new Agent({
      type: 'info',
      data: args,
      ancestors: this.ancestors.concat(this)
    });
    return this;
  }

  warn(...args) {
    new Agent({
      type: 'warn',
      data: args,
      ancestors: this.ancestors.concat(this)
    });
    return this;
  }

  success(...args) {
    new Agent({
      type: 'success',
      data: args,
      ancestors: this.ancestors.concat(this),
    });
    return this;
  }

  error(...args) {
    new Agent({
      type: 'error',
      data: args,
      ancestors: this.ancestors.concat(this),
    });
    return this;
  }

  child(name, ...args) {
    if (!this.children[name]) {
      this.children[name] = new Agent({
        name,
        type: 'node',
        ancestors: this.ancestors.concat(this),
      });
    }
    if (args.length) {
      this.children[name].log(...args);
    }
    return this.children[name];
  }

  async(name, ...args) {
    if (!this.children[name]) {
      this.children[name] = new Agent({
        name,
        type: 'async',
        status: 'pending',
        isAsync: true,
        ancestors: this.ancestors.concat(this),
      });
    }
    if (!this.children[name].isAsync) {
      this.internalWarn(`Child agent {bold}${name}{/bold} is defined as a non async agent`);
    }
    if (args.length) {
      this.children[name].log(...args);
    }
    return this.children[name];
  }

  resolve(...args) {
    if (this.isAsync) {
      if (this.logItem.status === 'pending') {
        this.logItem.setStatus('resolved');
        const resolveLog = new Agent({
          name: this.name,
          type: 'success',
          message: 'resolved',
          ancestors: this.ancestors.concat(this),
        });
        if (args.length) {
          resolveLog.success(...args);
        }
      } else {
        this.internalWarn(`Trying to resolve an already {bold}${this.logItem.status}{/bold} async agent`);
      }
    } else {
      this.internalWarn('Trying to resolve a non async agent');
    }
    return this;
  }

  reject(...args) {
    if (this.isAsync) {
      if (this.logItem.status === 'pending') {
        this.logItem.setStatus('rejected');
        const rejectLog = new Agent({
          name: this.name,
          type: 'error',
          message: 'rejected',
          ancestors: this.ancestors.concat(this),
        });
        if (args.length) {
          rejectLog.error(...args);
        }
      } else {
        this.internalWarn(`Trying to reject an already {bold}${this.logItem.status}{/bold} async agent`);
      }
    } else {
      this.internalWarn('Trying to reject a non async agent');
    }
    return this;
  }

  internalWarn(message) {
    new Agent({
      name: this.name,
      type: 'warn',
      message,
      ancestors: this.ancestors.concat(this),
    });
  }

  getAncestorsNames() {
    return this.ancestors.map(ancestor => ancestor.name);
  }

  generateStackTrace(trace) {
    const stackTrace = [];
    for (let i = 0; i < 5; i++) {
      stackTrace.push({
        type: trace[i].getTypeName(),
        function: trace[i].getFunctionName(),
        method: trace[i].getMethodName(),
        file: trace[i].getFileName(),
        line: trace[i].getLineNumber(),
        column: trace[i].getColumnNumber(),
      });
    }
    return stackTrace;
  }
}

export default function(name, ...args) {
  return new Agent({
    name,
    type: 'root',
    data: args.length ? args : undefined,
  });
};


================================================
FILE: src/investigator.js
================================================
import transceiver from 'transceiver';

import Ui from './ui';
import agent from './agent';

transceiver.setPromise(null);

const ui = new Ui();

export default {ui, agent};


================================================
FILE: src/ui/index.js
================================================
import blessed from 'blessed';
import transceiver from 'transceiver';

import LogsList from './logsList';
import LogDetails from './logDetails';
import Inspector from './inspector';

export default class Ui {
  constructor() {
    this.channel = transceiver.channel('ui');
    this.screen = blessed.screen({
      smartCSR: true
    });

    this.logsList = new LogsList();
    this.logDetails = new LogDetails();
    this.inspector = new Inspector();

    this.separator = blessed.line({
      bottom: 6,
      orientation: 'horizontal'
    });

    this.screen.append(this.logsList.element);
    this.screen.append(this.logDetails.element);
    this.screen.append(this.separator);
    this.screen.append(this.inspector.element);

    this.logsList.element.focus();

    this.screen.key(['q', 'C-c'], function(ch, key) {
      return process.exit(0);
    });

    this.screen.key(['i'], this.toggleInspector.bind(this));

    this.screen.render();

    this.channel.reply('render', () => this.screen.render());
  }

  toggleInspector() {
    if (this.inspector.opened) {
      this.inspector.close();
      this.logsList.focus();
    } else {
      this.inspector.open(this.logsList.selectedLog);
    }
    this.screen.render();
  }
}


================================================
FILE: src/ui/inspector.js
================================================
import blessed from 'blessed';
import transceiver from 'transceiver';
import prune from 'json-prune';
import path from 'path';
import appRoot from 'app-root-path';

import tree from './tree';

export default class Inspector {
  constructor() {
    this.channel = transceiver.channel('log');

    this.element = tree({
      top: 'center',
      left: 'center',
      width: '90%',
      height: '75%',
      hidden: true,
      label: 'Inspector',
      tags: true,
      border: {
        type: 'line'
      },
      style: {
        fg: 'white',
        border: {
          fg: '#f0f0f0'
        },
      },
      template: {
        extend: '{bold}{green-fg} [+]{/}',
        retract: '{bold}{yellow-fg} [-]{/}',
        lines: true,
      }
    });
  }

  open(selectedLog) {
    if (!selectedLog || !selectedLog.data && !selectedLog.stackTrace) {
      return;
    }
    this.opened = true;
    this.element.show();
    this.element.focus();
    this.element.setData(this.prepareData(selectedLog));
  }

  close() {
    this.opened = false;
    this.element.hide();
  }

  prepareData(log) {
    const content = {};
    if (log.data) {
      content.data = JSON.parse(prune(log.data, {
        depthDecr: 7,
        replacer: (value, defaultValue, circular) => {
          if (typeof value === 'function') {
            return '"Function [pruned]"';
          }
          if (Array.isArray(value)) {
            return `"Array (${value.length}) [pruned]"`;
          }
          if (typeof value === 'object') {
            return '"Object [pruned]"';
          }
          return defaultValue;
        }
      }));
    }

    if (log.stackTrace) {
      content['stack trace'] = log.stackTrace.map((callsite) => {
        const relativePath = path.relative(appRoot.toString(), callsite.file);
        return {
          type: callsite.type,
          function: callsite.function,
          method: callsite.method,
          file: `${relativePath}:{yellow-fg}${callsite.line}{/yellow-fg}:{yellow-fg}${callsite.column}{/yellow-fg}`,
        };
      });
    }
    return this.formatData(content);
  }

  formatData(data, key, depth = 0) {
    depth++;
    if (typeof data === 'object') {
      if (data !== null) {
        let name;
        let extended;

        if (depth === 2) {
          name = `{yellow-fg}{bold}${key.toUpperCase()}{/bold}{/yellow-fg} {magenta-fg}(${data.length}){/magenta-fg}`;
          extended = key === 'data';
        } else {
          const type = (Array.isArray(data) ? `[Array] {magenta-fg}(${data.length}){/magenta-fg}` : '[Object]');
          name = `{blue-fg}{bold}${key ? key + ' ' : ''}{/bold}${type}{/blue-fg}`;
          extended = depth < 4;
        }
        const newObj = {
          children: {},
          name,
          extended
        };
        Object.keys(data).forEach((key) => {
          const child = this.formatData(data[key], key, depth);
          if (child) {
            newObj.children[key] = child;
          }
        });
        return newObj;
      }
    }
    if (typeof data === 'function') {
      return {
        name: `{blue-fg}${key}{/blue-fg}: {red-fg}{bold}[Function]{/}`,
      };
    }
    if (typeof data === 'number') {
      return {
        name: `{blue-fg}${key}{/blue-fg}: {yellow-fg}${data}{/}`,
      };
    }
    if (data === null) {
      return {
        name: `{blue-fg}${key}{/blue-fg}: {cyan-fg}{bold}null{/}`,
      };
    }
    return {
      name: `{blue-fg}${key}{/blue-fg}: ${data}`,
    };
  }
}


================================================
FILE: src/ui/logDetails.js
================================================
import blessed from 'blessed';
import transceiver from 'transceiver';
import dateFormat from 'dateformat';

export default class logDetails {
  constructor() {
    this.channel = transceiver.channel('log');
    this.element = blessed.box({
      height: 6,
      left: '0',
      bottom: 0,
      tags: true,
      keys: true,
      padding: {
        left: 1,
        right: 1,
      },
      style: {
        selected: {
          fg: 'black',
          bg: 'white',
          border: {
            fg: 'white'
          },
          hover: {
            bg: 'green'
          }
        }
      }
    });

    this.channel.on('select log', this.updateLogDetails.bind(this));
  }

  updateLogDetails(log) {
    this.element.setContent(this.renderType(log) + this.renderId(log) + this.renderDate(log) + this.renderDuration(log) + this.renderData(log));
  }

  renderType(log) {
    if (log.type === 'root') {
      return '{magenta-fg}{bold}ROOT NODE{/bold}{/magenta-fg}\n';
    }
    if (log.type === 'success') {
      return '{green-fg}✔ {bold}SUCCESS{/bold}{/green-fg}\n';
    }
    if (log.type === 'error') {
      return '{red-fg}✘ {bold}ERROR{/bold}{/red-fg}\n';
    }
    if (log.type === 'warn') {
      return '{yellow-fg}! {bold}WARN{/bold}{/red-fg}\n';
    }
    if (log.type === 'node') {
      return '{grey-fg}{bold}NODE{/bold}{/grey-fg}\n';
    }
    if (log.type === 'async') {
      if (log.status === 'resolved') {
        return '{bold}{green-fg}ASYNC NODE{/bold} (RESOLVED ✔){/green-fg}\n';
      }
      if (log.status === 'rejected') {
        return '{bold}{red-fg}ASYNC NODE{/bold} (REJECTED ✘){/red-fg}\n';
      }
      if (log.status === 'pending') {
        return '{cyan-fg}{bold}ASYNC NODE{/bold} (PENDING ⌛){/cyan-fg}\n';
      }
    }
    if (log.type === 'info') {
      return '{white-fg}{bold}INFO{/bold}{/white-fg}\n';
    }
    return '';
  }

  renderId(log) {
    return `{bold}ID:{/bold} {underline}${log.id}{/underline}\n`;
  }

  renderDate(log) {
    return `{bold}TIME:{/bold} {magenta-fg}${dateFormat(log.date, 'dddd, mmmm dS yyyy, HH:MM:ss.L')}{/magenta-fg}\n`;
  }

  renderDuration(log) {
    if (log.relativeDuration && log.previousLog) {
      return `{bold}DURATION:{/bold} {yellow-fg}${log.relativeDuration}{/yellow-fg} (from {underline}${log.previousLog.id}{/underline})\n`;
    }
    return '';
  }

  renderData(log) {
    if (log.data) {
      return `{bold}DATA:{/bold} ${log.renderData()}\n`;
    }
    return '';
  }
}


================================================
FILE: src/ui/logItem.js
================================================
import blessed from 'blessed';
import shortid from 'shortid';
import transceiver from 'transceiver';
import prune from 'json-prune';
import dateFormat from 'dateformat';

export default class LogItem {
  constructor({name, type, status, parent, data, message, stackTrace, date = Date.now()}) {
    this.id = shortid.generate();
    this.name = name;
    this.type = type;
    this.status = status;
    this.data = data;
    this.message = message;
    this.stackTrace = stackTrace;
    this.date = date;
    this.children = [];
    this.channel = transceiver.channel('log');

    if (parent) {
      this.depth = parent.depth + 1;
      this.parent = parent;
      this.previousLog = parent.getLastChild() || parent;
      this.relativeDuration = this.getRelativeDuration();
      this.parent.addChild(this);
    } else {
      this.depth = 0;
    }
    this.element = this.channel.request('addLog', this);
    this.update();
  }

  update() {
    if (this.element) {
      this.element.content = this.render();
      transceiver.request('ui', 'render');
    }
  }

  render() {
    let message = `${this.renderState()}${this.renderName()}${this.renderMessage()}${this.renderData()}${this.renderDate()}${this.renderDuration()}`;
    for (let i = 0; i < this.depth; i++) {
      message = '    ' + message;
    }
    return message;
  }

  renderState() {
    if (this.type === 'async' && this.status === 'pending') {
      return `{cyan-fg}[⌛]{/cyan-fg} `;
    }
    if (this.type === 'async' && this.status === 'resolved') {
      return `{green-fg}[✔]{/green-fg} `;
    }
    if (this.type === 'async' && this.status === 'rejected') {
      return `{red-fg}[✘]{/red-fg} `;
    }
    if (this.type === 'success') {
      return `{green-fg}✔{/green-fg} `;
    }
    if (this.type === 'error') {
      return `{red-fg}✘{/red-fg} `;
    }
    if (this.type === 'warn') {
      return `{yellow-fg}❗{/yellow-fg} `;
    }
    if (this.type === 'info') {
      return '⇢ ';
    }
    return '';
  }

  renderName() {
    if (this.depth === 0) {
      return this.name ? `{underline}{bold}${this.name}{/bold}{/underline} ` : '';
    }
    if (this.type === 'async') {
      if (this.status === 'resolved') {
        return `{bold}{green-fg}${this.name}{/green-fg}{/bold} (async) `;
      }
      if (this.status === 'rejected') {
        return `{bold}{red-fg}${this.name}{/red-fg}{/bold} (async) `;
      }
      return `{bold}${this.name}{/bold} (async) `;
    }
    if (this.type === 'success') {
      return this.name ? `{bold}{green-fg}${this.name}{/green-fg}{/bold} ` : '';
    }
    if (this.type === 'error') {
      return this.name ? `{bold}{red-fg}${this.name}{/red-fg}{/bold} ` : '';
    }
    if (this.type === 'warn') {
      return this.name ? `{bold}{yellow-fg}${this.name}{/yellow-fg}{/bold} ` : '';
    }
    return this.name ? `{bold}${this.name}{/bold} ` : '';
  }

  renderData() {
    if (this.depth === 0) {
      // console.log(this.data);
    }
    if (!this.data) {
      return '';
    }
    if (Array.isArray(this.data)) {
      return this.data.map(this.renderValue.bind(this)).join(' ') + ' ';
    }
    return this.renderValue(this.data) + ' ';
  }

  renderValue(value) {
    if (Array.isArray(value)) {
      return `{cyan-fg}${this.prune(value)}{/cyan-fg}`;
    }
    if (typeof value === 'object') {
      return `{blue-fg}${this.prune(value)}{/blue-fg}`;
    }
    if (typeof value === 'function') {
      return `{red-fg}{bold}[Function]{/bold}{red-fg}`;
    }
    if (typeof value === 'number') {
      return `{yellow-fg}${value}{/yellow-fg}`;
    }
    if (typeof value === 'string') {
      if (this.type === 'success') {
        return `{green-fg}${value}{/green-fg}`;
      }
      if (this.type === 'error') {
        return `{red-fg}${value}{/red-fg}`;
      }
      if (this.type === 'warn') {
        return `{yellow-fg}${value}{/yellow-fg}`;
      }
    }
    return value;
  }

  renderMessage() {
    if (this.message) {
      if (this.type === 'success') {
        return `{green-fg}${this.message}{/green-fg} `;
      }
      if (this.type === 'error') {
        return `{red-fg}${this.message}{/red-fg} `;
      }
      if (this.type === 'warn') {
        return `{yellow-fg}${this.message}{/yellow-fg} `;
      }
      return `${this.message} `;
    }
    return '';
  }

  renderDate() {
    if (this.depth === 0) {
      return `{magenta-fg}(${dateFormat(this.date, 'dd/mm/yyyy HH:MM:ss.L')}){/magenta-fg} `;
    }
    return '';
  }

  renderDuration() {
    if (this.relativeDuration) {
      return `{grey-fg}+${this.relativeDuration}{/grey-fg} `;
    }
    return '';
  }

  getRelativeDuration() {
    return this.humanizeDuration(this.date - this.previousLog.date);
  }

  humanizeDuration(duration) {
    if (duration < 1000) {
      return `${duration}ms`;
    }
    if (duration < 60000) {
      let milliseconds = duration % 1000;
      milliseconds = ('000' + milliseconds).slice(-3);
      return `${Math.floor(duration / 1000)}.${milliseconds}s`;
    }
    return `${Math.floor(duration / 60000)}m ${Math.round((duration % 60000) / 1000)}s`;
  }

  addChild(log) {
    this.children.push(log);
  }

  getLastChild() {
    return this.children[this.children.length - 1];
  }

  getChildren(list) {
    list = list || [];
    list.push.apply(list, this.children);
    this.children.forEach(child => {
      child.getChildren(list);
    });
    return list;
  }

  setStatus(status) {
    this.status = status;
    this.update();
  }

  prune(value) {
    return prune(value, {
      depthDecr: 2,
      arrayMaxLength: 8,
      prunedString: ' [...]'
    });
  }
}


================================================
FILE: src/ui/logsList.js
================================================
import blessed from 'blessed';
import transceiver from 'transceiver';

export default class LogsList {
  constructor() {
    this.selectedLog = null;
    this.logs = {};
    this.logsCount = 0;
    this.channel = transceiver.channel('log');
    this.autoScroll = true;
    this.element = blessed.list({
      top: '0',
      left: '0',
      bottom: 7,
      tags: true,
      keys: true,
      mouse: true,
      scrollbar: {
        bg: 'magenta',
      },
      style: {
        selected: {
          fg: 'black',
          bg: 'white',
        }
      }
    });

    this.element.key(['up', 'down', 's', 'b'], (ch, key) => {
      if (key.name === 's') {
        this.autoScroll = !this.autoScroll;
      } else if (key.name === 'b') {
        this.scrollToBottom();
        transceiver.request('ui', 'render');
      } else {
        this.autoScroll = false;
      }
    });

    this.element.on('select item', (element, i) => {
      this.selectedLog = this.getLogFromElement(element);
      if (this.selectedLog) {
        this.channel.emit('select log', this.selectedLog);
      }
    });

    this.channel.reply({
      addLog: this.addLog,
      getSelectedLog: this.getSelectedLog,
    }, this);
  }

  addLog(log) {
    let element;

    this.logs[log.id] = log;
    this.logsCount++;

    if (log.parent) {
      const index = this.element.getItemIndex(log.parent.element) + log.parent.getChildren().length;
      this.element.insertItem(index, log.render());
      element = this.element.getItem(index);
    } else {
      element = this.element.add(log.render());
    }
    element.logId = log.id;
    if (this.autoScroll) {
      this.scrollToBottom();
    }
    if (this.logsCount === 1) {
      this.channel.emit('select log', log);
    }
    return element;
  }

  getSelectedLog() {
    return this.selectedLog;
  }

  scrollToBottom() {
    this.element.move(this.logsCount);
  }

  getLogFromElement(element) {
    return this.logs[element.logId];
  }

  focus() {
    this.element.focus();
  }
}


================================================
FILE: src/ui/tree.js
================================================
// https://github.com/yaronn/blessed-contrib/blob/master/lib/widget/tree.js
import blessed from 'blessed';

const Node = blessed.Node;
const Box = blessed.Box;

function Tree(options) {

  if (!(this instanceof Node)) {
    return new Tree(options);
  }

  options = options || {};
  options.bold = true;
  var self = this;
  this.options = options;
  this.data = {};
  this.nodeLines = [];
  this.lineNbr = 0;
  Box.call(this, options);

  options.extended = options.extended || false;
  options.keys = options.keys || ['space','enter'];

  options.template = options.template || {};
  options.template.extend = options.template.extend || ' [+]';
  options.template.retract = options.template.retract || ' [-]';
  options.template.lines = options.template.lines || false;

  this.rows = blessed.list({
    height: 0,
    top: 1,
    width: 0,
    left: 1,
    selectedFg: 'black',
    selectedBg: 'white',
    keys: true,
    tags: true,
  });

  this.rows.key(options.keys,function() {
    self.nodeLines[this.getItemIndex(this.selected)].extended = !self.nodeLines[this.getItemIndex(this.selected)].extended;
    self.setData(self.data);
    self.screen.render();

    self.emit('select',self.nodeLines[this.getItemIndex(this.selected)]);
  });

  this.append(this.rows);
}

Tree.prototype.walk = function(node, treeDepth) {
  var lines = [];

  if (!node.parent) {
    node.parent = null;
  }

  if (treeDepth == '' && node.name) {
    this.lineNbr = 0;
    this.nodeLines[this.lineNbr++] = node;
    lines.push(node.name);
    treeDepth = ' ';
  }

  node.depth = treeDepth.length - 1;

  if (node.children && node.extended) {

    var i = 0;

    if (typeof node.children == 'function') {
      node.childrenContent = node.children(node);
    }

    if (!node.childrenContent) {
      node.childrenContent = node.children;
    }

    for (var child in node.childrenContent) {

      if (!node.childrenContent[child].name) {
        node.childrenContent[child].name = child;
      }

      var childIndex = child;
      child = node.childrenContent[child];
      child.parent = node;
      child.position = i++;

      if (typeof child.extended == 'undefined') {
        child.extended = this.options.extended;
      }

      if (typeof child.children == 'function') {
        child.childrenContent = child.children(child);
      } else {
        child.childrenContent = child.children;
      }

      var isLastChild = child.position == Object.keys(child.parent.childrenContent).length - 1;
      var tree;
      var suffix = '';
      if (isLastChild) {
        tree = '└';
      } else {
        tree = '├';
      }
      if (!child.childrenContent || Object.keys(child.childrenContent).length == 0) {
        tree += '─';
      } else if (child.extended) {
        tree += '┬';
        suffix = this.options.template.retract;
      } else {
        tree += '─';
        suffix = this.options.template.extend;
      }

      if (!this.options.template.lines) {
        tree = '|-';
      }

      lines.push(treeDepth + tree + child.name + suffix);

      this.nodeLines[this.lineNbr++] = child;

      var parentTree;
      if (isLastChild || !this.options.template.lines) {
        parentTree = treeDepth + ' ';
      } else {
        parentTree = treeDepth + '│';
      }
      lines = lines.concat(this.walk(child, parentTree));
    }
  }
  return lines;
};

Tree.prototype.focus = function() {
  this.rows.focus();
};

Tree.prototype.render = function() {
  if (this.screen.focused == this.rows) {
    this.rows.focus();
  }

  this.rows.width = this.width - 3;
  this.rows.height = this.height - 3;
  Box.prototype.render.call(this);
};

Tree.prototype.setData = function(data) {

  var formatted = [];
  formatted = this.walk(data,'');

  this.data = data;
  this.rows.setItems(formatted);
};

Tree.prototype.__proto__ = Box.prototype;

Tree.prototype.type = 'tree';

export default Tree;


================================================
FILE: test/.eslintrc
================================================
{
  "parser": "babel-eslint",
  "rules": {
    "strict": 0,
    "quotes": [2, "single"],
    "no-unused-expressions": 0
  },
  "env": {
    "browser": true,
    "node": true,
    "mocha": true
  },
  "globals": {
    "spy": true,
    "expect": true
  }
}

================================================
FILE: test/runner.html
================================================
<!doctype html>
<html lang='en'>
<head>
  <meta charset='utf-8'>
  <title>Tests</title>
  <link rel='stylesheet' href='../node_modules/mocha/mocha.css' />

  <!-- Polyfill (required by Babel) -->
  <script src='../node_modules/babel-core/browser-polyfill.js'></script>

  <!-- Testing libraries -->
  <script src='../node_modules/mocha/mocha.js'></script>
  <script src='../node_modules/chai/chai.js'></script>
  <script src='../node_modules/sinon/pkg/sinon.js'></script>
  <script src='../node_modules/sinon-chai/lib/sinon-chai.js'></script>

  <!-- Livereload -->
  <script src='http://localhost:35729/livereload.js'></script>

  <!-- Load the built library -->
  <script src='../tmp/__spec-build.js'></script>
</head>
<body>
  <!-- Required for browser reporter -->
  <div id='mocha'></div>
</body>
</html>


================================================
FILE: test/setup/browserify.js
================================================
var config = require('../../package.json').babelBoilerplateOptions;

global.mocha.setup('bdd');
global.onload = function() {
  global.mocha.checkLeaks();
  global.mocha.globals(config.mochaGlobals);
  global.mocha.run();
  require('./setup')();
};


================================================
FILE: test/setup/node.js
================================================
global.chai = require('chai');
global.sinon = require('sinon');
global.chai.use(require('sinon-chai'));

require('babel-core/register');
require('./setup')();


================================================
FILE: test/setup/setup.js
================================================
module.exports = function() {
  global.expect = global.chai.expect;

  beforeEach(function() {
    this.sandbox = global.sinon.sandbox.create();
    global.stub = this.sandbox.stub.bind(this.sandbox);
    global.spy = this.sandbox.spy.bind(this.sandbox);
  });

  afterEach(function() {
    delete global.stub;
    delete global.spy;
    this.sandbox.restore();
  });
};


================================================
FILE: test/unit/investigator.js
================================================
import investigator from '../../src/investigator';

describe('investigator', () => {
  // TODO
});
Download .txt
gitextract_95_54p6y/

├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .jscsrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist/
│   └── investigator.js
├── examples/
│   ├── example.js
│   └── index.js
├── gulpfile.js
├── package.json
├── src/
│   ├── agent.js
│   ├── investigator.js
│   └── ui/
│       ├── index.js
│       ├── inspector.js
│       ├── logDetails.js
│       ├── logItem.js
│       ├── logsList.js
│       └── tree.js
└── test/
    ├── .eslintrc
    ├── runner.html
    ├── setup/
    │   ├── browserify.js
    │   ├── node.js
    │   └── setup.js
    └── unit/
        └── investigator.js
Download .txt
SYMBOL INDEX (79 symbols across 10 files)

FILE: dist/investigator.js
  function defineProperties (line 1) | function defineProperties(target, props) { for (var i = 0; i < props.len...
  function _classCallCheck (line 3) | function _classCallCheck(instance, Constructor) { if (!(instance instanc...
  function LogsList (line 11) | function LogsList() {
  function logDetails (line 113) | function logDetails() {
  function Tree (line 219) | function Tree(options) {
  function Inspector (line 381) | function Inspector() {
  function Ui (line 529) | function Ui() {
  function LogItem (line 585) | function LogItem(_ref) {
  function Agent (line 825) | function Agent(_ref2) {

FILE: examples/example.js
  function runScraping (line 6) | function runScraping() {
  function scrapPage (line 16) | function scrapPage(url) {
  function getPostsData (line 41) | function getPostsData(posts, agent) {
  function getVideos (line 68) | function getVideos(videos, agent) {
  function downloadPage (line 89) | function downloadPage(url) {
  function getPageInfo (line 121) | function getPageInfo(page) {
  function downloadPostImage (line 129) | function downloadPostImage(post) {
  function getPostComments (line 134) | function getPostComments(post) {
  function downloadVideo (line 140) | function downloadVideo(video) {

FILE: gulpfile.js
  function jscsNotify (line 37) | function jscsNotify(file) {
  function createLintTask (line 42) | function createLintTask(taskName, files) {
  function bundle (line 90) | function bundle(bundler) {
  function getBundler (line 103) | function getBundler() {
  function test (line 133) | function test() {

FILE: src/agent.js
  class Agent (line 7) | class Agent {
    method constructor (line 8) | constructor({name, type, status, data, message, isAsync = false, ances...
    method log (line 37) | log(...args) {
    method warn (line 46) | warn(...args) {
    method success (line 55) | success(...args) {
    method error (line 64) | error(...args) {
    method child (line 73) | child(name, ...args) {
    method async (line 87) | async(name, ...args) {
    method resolve (line 106) | resolve(...args) {
    method reject (line 128) | reject(...args) {
    method internalWarn (line 150) | internalWarn(message) {
    method getAncestorsNames (line 159) | getAncestorsNames() {
    method generateStackTrace (line 163) | generateStackTrace(trace) {

FILE: src/ui/index.js
  class Ui (line 8) | class Ui {
    method constructor (line 9) | constructor() {
    method toggleInspector (line 42) | toggleInspector() {

FILE: src/ui/inspector.js
  class Inspector (line 9) | class Inspector {
    method constructor (line 10) | constructor() {
    method open (line 38) | open(selectedLog) {
    method close (line 48) | close() {
    method prepareData (line 53) | prepareData(log) {
    method formatData (line 87) | formatData(data, key, depth = 0) {

FILE: src/ui/logDetails.js
  class logDetails (line 5) | class logDetails {
    method constructor (line 6) | constructor() {
    method updateLogDetails (line 35) | updateLogDetails(log) {
    method renderType (line 39) | renderType(log) {
    method renderId (line 72) | renderId(log) {
    method renderDate (line 76) | renderDate(log) {
    method renderDuration (line 80) | renderDuration(log) {
    method renderData (line 87) | renderData(log) {

FILE: src/ui/logItem.js
  class LogItem (line 7) | class LogItem {
    method constructor (line 8) | constructor({name, type, status, parent, data, message, stackTrace, da...
    method update (line 33) | update() {
    method render (line 40) | render() {
    method renderState (line 48) | renderState() {
    method renderName (line 73) | renderName() {
    method renderData (line 98) | renderData() {
    method renderValue (line 111) | renderValue(value) {
    method renderMessage (line 138) | renderMessage() {
    method renderDate (line 154) | renderDate() {
    method renderDuration (line 161) | renderDuration() {
    method getRelativeDuration (line 168) | getRelativeDuration() {
    method humanizeDuration (line 172) | humanizeDuration(duration) {
    method addChild (line 184) | addChild(log) {
    method getLastChild (line 188) | getLastChild() {
    method getChildren (line 192) | getChildren(list) {
    method setStatus (line 201) | setStatus(status) {
    method prune (line 206) | prune(value) {

FILE: src/ui/logsList.js
  class LogsList (line 4) | class LogsList {
    method constructor (line 5) | constructor() {
    method addLog (line 53) | addLog(log) {
    method getSelectedLog (line 76) | getSelectedLog() {
    method scrollToBottom (line 80) | scrollToBottom() {
    method getLogFromElement (line 84) | getLogFromElement(element) {
    method focus (line 88) | focus() {

FILE: src/ui/tree.js
  function Tree (line 7) | function Tree(options) {
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (85K chars).
[
  {
    "path": ".babelrc",
    "chars": 32,
    "preview": "{\n  \"blacklist\": [\"useStrict\"]\n}"
  },
  {
    "path": ".editorconfig",
    "chars": 314,
    "preview": "# EditorConfig is awesome: http://EditorConfig.org\n\nroot = true;\n\n[*]\n#  Ensure there's no lingering whitespace\ntrim_tra"
  },
  {
    "path": ".eslintrc",
    "chars": 149,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"rules\": {\n    \"strict\": 0,\n    \"quotes\": [2, \"single\"]\n  },\n  \"env\": {\n    \"browser\": f"
  },
  {
    "path": ".gitignore",
    "chars": 617,
    "preview": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nl"
  },
  {
    "path": ".jscsrc",
    "chars": 72,
    "preview": "{\n  \"preset\": \"google\",\n  \"maximumLineLength\": null,\n  \"esnext\": true\n}\n"
  },
  {
    "path": ".travis.yml",
    "chars": 208,
    "preview": "language: node_js\nnode_js:\n  - \"0.10\"\n  - \"0.12\"\n  - \"io.js\"\nsudo: false\nscript: \"gulp coverage\"\nafter_success:\n  - npm "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 90,
    "preview": "### [0.0.1](https://github.com/risq/investigator/releases/tag/v0.0.1)\n\n- The first release"
  },
  {
    "path": "LICENSE",
    "chars": 1102,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015 risq <valentin.ledrapier@gmail.com>\n\nPermission is hereby granted, free of cha"
  },
  {
    "path": "README.md",
    "chars": 7685,
    "preview": "# investigator\nInteractive and asynchronous logging tool for Node.js. An easier way to log & debug complex requests dire"
  },
  {
    "path": "dist/investigator.js",
    "chars": 31947,
    "preview": "var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { va"
  },
  {
    "path": "examples/example.js",
    "chars": 5386,
    "preview": "import investigator from '../src/investigator';\n\nlet i = 1;\nsetTimeout(runScraping, 2000);\n\nfunction runScraping() {\n  i"
  },
  {
    "path": "examples/index.js",
    "chars": 57,
    "preview": "require('babel-core/register');\nrequire('./example.js');\n"
  },
  {
    "path": "gulpfile.js",
    "chars": 5587,
    "preview": "// Load Gulp and all of our Gulp plugins\nconst gulp = require('gulp');\nconst $ = require('gulp-load-plugins')();\n\n// Loa"
  },
  {
    "path": "package.json",
    "chars": 2090,
    "preview": "{\n  \"name\": \"investigator\",\n  \"version\": \"0.1.1\",\n  \"description\": \"Interactive and asynchronous logging tool for Node.j"
  },
  {
    "path": "src/agent.js",
    "chars": 4353,
    "preview": "import shortid from 'shortid';\nimport transceiver from 'transceiver';\nimport stackTrace from 'stack-trace';\n\nimport LogI"
  },
  {
    "path": "src/investigator.js",
    "chars": 174,
    "preview": "import transceiver from 'transceiver';\n\nimport Ui from './ui';\nimport agent from './agent';\n\ntransceiver.setPromise(null"
  },
  {
    "path": "src/ui/index.js",
    "chars": 1236,
    "preview": "import blessed from 'blessed';\nimport transceiver from 'transceiver';\n\nimport LogsList from './logsList';\nimport LogDeta"
  },
  {
    "path": "src/ui/inspector.js",
    "chars": 3499,
    "preview": "import blessed from 'blessed';\nimport transceiver from 'transceiver';\nimport prune from 'json-prune';\nimport path from '"
  },
  {
    "path": "src/ui/logDetails.js",
    "chars": 2480,
    "preview": "import blessed from 'blessed';\nimport transceiver from 'transceiver';\nimport dateFormat from 'dateformat';\n\nexport defau"
  },
  {
    "path": "src/ui/logItem.js",
    "chars": 5626,
    "preview": "import blessed from 'blessed';\nimport shortid from 'shortid';\nimport transceiver from 'transceiver';\nimport prune from '"
  },
  {
    "path": "src/ui/logsList.js",
    "chars": 2019,
    "preview": "import blessed from 'blessed';\nimport transceiver from 'transceiver';\n\nexport default class LogsList {\n  constructor() {"
  },
  {
    "path": "src/ui/tree.js",
    "chars": 3904,
    "preview": "// https://github.com/yaronn/blessed-contrib/blob/master/lib/widget/tree.js\nimport blessed from 'blessed';\n\nconst Node ="
  },
  {
    "path": "test/.eslintrc",
    "chars": 254,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"rules\": {\n    \"strict\": 0,\n    \"quotes\": [2, \"single\"],\n    \"no-unused-expressions\": 0\n"
  },
  {
    "path": "test/runner.html",
    "chars": 810,
    "preview": "<!doctype html>\n<html lang='en'>\n<head>\n  <meta charset='utf-8'>\n  <title>Tests</title>\n  <link rel='stylesheet' href='."
  },
  {
    "path": "test/setup/browserify.js",
    "chars": 248,
    "preview": "var config = require('../../package.json').babelBoilerplateOptions;\n\nglobal.mocha.setup('bdd');\nglobal.onload = function"
  },
  {
    "path": "test/setup/node.js",
    "chars": 159,
    "preview": "global.chai = require('chai');\nglobal.sinon = require('sinon');\nglobal.chai.use(require('sinon-chai'));\n\nrequire('babel-"
  },
  {
    "path": "test/setup/setup.js",
    "chars": 371,
    "preview": "module.exports = function() {\n  global.expect = global.chai.expect;\n\n  beforeEach(function() {\n    this.sandbox = global"
  },
  {
    "path": "test/unit/investigator.js",
    "chars": 99,
    "preview": "import investigator from '../../src/investigator';\n\ndescribe('investigator', () => {\n  // TODO\n});\n"
  }
]

About this extraction

This page contains the full source code of the risq/investigator GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (78.7 KB), approximately 21.1k tokens, and a symbol index with 79 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!