├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .jscsrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── investigator.js ├── investigator.js.map ├── investigator.min.js └── investigator.min.js.map ├── 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 /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "blacklist": ["useStrict"] 3 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true; 4 | 5 | [*] 6 | # Ensure there's no lingering whitespace 7 | trim_trailing_whitespace = true 8 | # Ensure a newline at the end of each file 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | # Unix-style newlines 13 | end_of_line = lf 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 0, 5 | "quotes": [2, "single"] 6 | }, 7 | "env": { 8 | "browser": false, 9 | "node": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | bower_components 27 | coverage 28 | tmp 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "maximumLineLength": null, 4 | "esnext": true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "io.js" 6 | sudo: false 7 | script: "gulp coverage" 8 | after_success: 9 | - npm install -g codeclimate-test-reporter 10 | - codeclimate-test-reporter < coverage/lcov.info 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### [0.0.1](https://github.com/risq/investigator/releases/tag/v0.0.1) 2 | 3 | - The first release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 risq 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # investigator 2 | Interactive and asynchronous logging tool for Node.js. An easier way to log & debug complex requests directly from the command line. Still experimental ! 3 | 4 | ![investigator](https://cloud.githubusercontent.com/assets/5665322/10861471/d38bedda-7f80-11e5-9bb7-19801c14c961.gif) 5 | 6 | ## Usage 7 | #### Nodes 8 | `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. 9 | 10 | ![investigator-nodes](https://cloud.githubusercontent.com/assets/5665322/10861540/267ff6e6-7f84-11e5-847a-5b7d395dfb34.png) 11 | 12 | ```js 13 | import {agent} from 'investigator'; 14 | 15 | const requestAgent = agent('request'); 16 | const getUserAgent = requestAgent.child('getUser') 17 | .log('Retrieving user from db...'); 18 | 19 | // ... 20 | 21 | getUserAgent.success('Done !'); 22 | // Or: requestAgent.child('getUser').success('Done !'); 23 | ``` 24 | 25 | #### Asynchronous logging 26 | `async` agents are particular nodes which may be resolved or rejected to provide a feedback of their fulfillment. 27 | 28 | ![investigator-async](https://cloud.githubusercontent.com/assets/5665322/10861606/e00908b2-7f86-11e5-862a-ab56505d3ee3.png) 29 | 30 | ```js 31 | import {agent} from 'investigator'; 32 | 33 | const requestAgent = agent('request'); 34 | 35 | // Creates an async child agent 36 | const getUserAgent = requestAgent.async('getUser') 37 | .log('Retrieving user from db...'); 38 | 39 | myAsynchronousFunction().then(() => { 40 | getUserAgent.resolve('Done !'); 41 | }).catch((err) => { 42 | getUserAgent.reject(err); 43 | }); 44 | ``` 45 | 46 | #### Inspector 47 | `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. 48 | 49 | ![investigator-inspector](https://cloud.githubusercontent.com/assets/5665322/10861607/e00c7506-7f86-11e5-8bd8-d3ae7a072c9d.png) 50 | 51 | ## Installing 52 | Use `npm install investigator` to install locally. See [Usage](#usage) and [API Reference](#api-reference) for more information. 53 | 54 | ## Shortcuts 55 | In the command line interface, the following shortcuts are available: 56 | - Scroll up and down with `up arrow`, `down arrow`, or mouse wheel. You may also click on a row to select it. 57 | - Open **Inspector** with `i` (inspect the currently selected row). 58 | - Scroll to bottom with `b` 59 | - Enable auto-scroll with `s`. Disable by pressing an arrow key. 60 | 61 | ## Testing & developing 62 | Clone the project with `git clone git@github.com:risq/investigator.git`. 63 | 64 | Install dependencies with `npm install`. 65 | 66 | Launch the example with `node examples/index.js`. 67 | 68 | You can build the project (transpiling to ES5) with `npm run build`. 69 | 70 | ## TODO (non-exhaustive list) 71 | - [ ] 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`. 72 | - [ ] Improve UI, navigation & controls in the CLI. 73 | - [ ] Add some performance monitoring. 74 | - [ ] Improve CLI performance for long time logging (avoid memory leaks). 75 | - [ ] Allow client-side logging via WebSockets. 76 | 77 | ## API Reference 78 | ### Investigator 79 | ##### `investigator.agent(String name [, data])` -> `Agent` 80 | 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. 81 | 82 | ```js 83 | import {agent} from 'investigator'; 84 | 85 | onRequest(req, res) { 86 | const requestAgent = agent('request', req.id, req.url); 87 | } 88 | ``` 89 | 90 | ### Agent 91 | ##### `agent.log(data [, data])` -> `Agent` 92 | Log passed data parameters under the given agent node. Returns the same agent (so it can be chained). 93 | 94 | ```js 95 | import {agent} from 'investigator'; 96 | 97 | onRequest(req, res) { 98 | const requestAgent = agent('request', req.id, req.url); 99 | requestAgent.log('Hello') 100 | .log('World'); 101 | } 102 | ``` 103 | 104 | ##### `agent.success(data [, data])` -> `Agent` 105 | Log passed data parameters under the given agent node, as a success (displayed in green). Returns the same agent (so it can be chained). 106 | 107 | ##### `agent.warn(data [, data])` -> `Agent` 108 | Log passed data parameters under the given agent node, as a warning (displayed in yellow). Returns the same agent (so it can be chained). 109 | 110 | ##### `agent.error(data [, data])` -> `Agent` 111 | Log passed data parameters under the given agent node, as an error (displayed in red). Returns the same agent (so it can be chained). 112 | 113 | ##### `agent.child(name [, data])` -> `Agent` 114 | 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. 115 | 116 | Data objects can be passed as parameters and will be logged on the child's context. 117 | 118 | ```js 119 | import {agent} from 'investigator'; 120 | 121 | onRequest(req, res) { 122 | const requestAgent = agent('request', req.id, req.url); 123 | 124 | if (req.url === '/user/login') { 125 | requestAgent.child('login', 'Logging in...'); 126 | 127 | if (validate(req.user, req.password)) { 128 | requestAgent.child('login') 129 | .success('Login data validated !'); 130 | } else { 131 | requestAgent.child('login') 132 | .error('Error validating user data.') 133 | } 134 | } 135 | } 136 | ``` 137 | 138 | ##### `agent.async(name [, data])` -> `Agent` 139 | 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. 140 | 141 | Data objects can be passed as parameters and will be logged on the child's context. 142 | 143 | An async agent has `.resolve()` and `.reject()` methods, to keep track of its fulfillment. 144 | 145 | ```js 146 | import {agent} from 'investigator'; 147 | 148 | onRequest(req, res) { 149 | const requestAgent = agent('request', req.id, req.url); 150 | 151 | if (req.url === '/user/login') { 152 | requestAgent.child('login', 'Logging in...'); 153 | 154 | authUser(req.user, req.password).then(() => { 155 | requestAgent.child('login') 156 | .success('Authentication succeeded !'); 157 | }).catch((err) => { 158 | requestAgent.child('login') 159 | .error('Error validating user data.') 160 | }); 161 | } 162 | } 163 | ``` 164 | 165 | ##### `agent.resolve(data [, data])` -> `Agent` 166 | Resolves an **async** agent. Log data parameters under the given agent node, as a success. Returns the same agent (so it can be chained). 167 | 168 | An async agent can only be resolved or rejected once. 169 | 170 | ##### `agent.reject(data [, data])` -> `Agent` 171 | Resolves an **async** agent. Log data parameters under the given agent node, as an error. Returns the same agent (so it can be chained). 172 | 173 | An async agent can only be resolved or rejected once. 174 | 175 | ## Contributing 176 | Feel free to contribute ! Issues and pull requests are highly welcomed and appreciated. 177 | 178 | ## License 179 | The MIT License (MIT) 180 | 181 | Copyright (c) 2015 Valentin Ledrapier 182 | 183 | Permission is hereby granted, free of charge, to any person obtaining a copy 184 | of this software and associated documentation files (the "Software"), to deal 185 | in the Software without restriction, including without limitation the rights 186 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 187 | copies of the Software, and to permit persons to whom the Software is 188 | furnished to do so, subject to the following conditions: 189 | 190 | The above copyright notice and this permission notice shall be included in 191 | all copies or substantial portions of the Software. 192 | 193 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 194 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 195 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 196 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 197 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 198 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 199 | THE SOFTWARE. 200 | -------------------------------------------------------------------------------- /dist/investigator.js: -------------------------------------------------------------------------------- 1 | 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; }; })(); 2 | 3 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 4 | 5 | (function (global, factory) { 6 | 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); 7 | })(this, function (transceiver, blessed, dateFormat, _prune, path, appRoot, shortid, stack_trace) { 8 | 'use strict'; 9 | 10 | var LogsList = (function () { 11 | function LogsList() { 12 | var _this = this; 13 | 14 | _classCallCheck(this, LogsList); 15 | 16 | this.selectedLog = null; 17 | this.logs = {}; 18 | this.logsCount = 0; 19 | this.channel = transceiver.channel('log'); 20 | this.autoScroll = true; 21 | this.element = blessed.list({ 22 | top: '0', 23 | left: '0', 24 | bottom: 7, 25 | tags: true, 26 | keys: true, 27 | mouse: true, 28 | scrollbar: { 29 | bg: 'magenta' 30 | }, 31 | style: { 32 | selected: { 33 | fg: 'black', 34 | bg: 'white' 35 | } 36 | } 37 | }); 38 | 39 | this.element.key(['up', 'down', 's', 'b'], function (ch, key) { 40 | if (key.name === 's') { 41 | _this.autoScroll = !_this.autoScroll; 42 | } else if (key.name === 'b') { 43 | _this.scrollToBottom(); 44 | transceiver.request('ui', 'render'); 45 | } else { 46 | _this.autoScroll = false; 47 | } 48 | }); 49 | 50 | this.element.on('select item', function (element, i) { 51 | _this.selectedLog = _this.getLogFromElement(element); 52 | if (_this.selectedLog) { 53 | _this.channel.emit('select log', _this.selectedLog); 54 | } 55 | }); 56 | 57 | this.channel.reply({ 58 | addLog: this.addLog, 59 | getSelectedLog: this.getSelectedLog 60 | }, this); 61 | } 62 | 63 | _createClass(LogsList, [{ 64 | key: 'addLog', 65 | value: function addLog(log) { 66 | var element = undefined; 67 | 68 | this.logs[log.id] = log; 69 | this.logsCount++; 70 | 71 | if (log.parent) { 72 | var index = this.element.getItemIndex(log.parent.element) + log.parent.getChildren().length; 73 | this.element.insertItem(index, log.render()); 74 | element = this.element.getItem(index); 75 | } else { 76 | element = this.element.add(log.render()); 77 | } 78 | element.logId = log.id; 79 | if (this.autoScroll) { 80 | this.scrollToBottom(); 81 | } 82 | if (this.logsCount === 1) { 83 | this.channel.emit('select log', log); 84 | } 85 | return element; 86 | } 87 | }, { 88 | key: 'getSelectedLog', 89 | value: function getSelectedLog() { 90 | return this.selectedLog; 91 | } 92 | }, { 93 | key: 'scrollToBottom', 94 | value: function scrollToBottom() { 95 | this.element.move(this.logsCount); 96 | } 97 | }, { 98 | key: 'getLogFromElement', 99 | value: function getLogFromElement(element) { 100 | return this.logs[element.logId]; 101 | } 102 | }, { 103 | key: 'focus', 104 | value: function focus() { 105 | this.element.focus(); 106 | } 107 | }]); 108 | 109 | return LogsList; 110 | })(); 111 | 112 | var logDetails = (function () { 113 | function logDetails() { 114 | _classCallCheck(this, logDetails); 115 | 116 | this.channel = transceiver.channel('log'); 117 | this.element = blessed.box({ 118 | height: 6, 119 | left: '0', 120 | bottom: 0, 121 | tags: true, 122 | keys: true, 123 | padding: { 124 | left: 1, 125 | right: 1 126 | }, 127 | style: { 128 | selected: { 129 | fg: 'black', 130 | bg: 'white', 131 | border: { 132 | fg: 'white' 133 | }, 134 | hover: { 135 | bg: 'green' 136 | } 137 | } 138 | } 139 | }); 140 | 141 | this.channel.on('select log', this.updateLogDetails.bind(this)); 142 | } 143 | 144 | // https://github.com/yaronn/blessed-contrib/blob/master/lib/widget/tree.js 145 | 146 | _createClass(logDetails, [{ 147 | key: 'updateLogDetails', 148 | value: function updateLogDetails(log) { 149 | this.element.setContent(this.renderType(log) + this.renderId(log) + this.renderDate(log) + this.renderDuration(log) + this.renderData(log)); 150 | } 151 | }, { 152 | key: 'renderType', 153 | value: function renderType(log) { 154 | if (log.type === 'root') { 155 | return '{magenta-fg}{bold}ROOT NODE{/bold}{/magenta-fg}\n'; 156 | } 157 | if (log.type === 'success') { 158 | return '{green-fg}✔ {bold}SUCCESS{/bold}{/green-fg}\n'; 159 | } 160 | if (log.type === 'error') { 161 | return '{red-fg}✘ {bold}ERROR{/bold}{/red-fg}\n'; 162 | } 163 | if (log.type === 'warn') { 164 | return '{yellow-fg}! {bold}WARN{/bold}{/red-fg}\n'; 165 | } 166 | if (log.type === 'node') { 167 | return '{grey-fg}{bold}NODE{/bold}{/grey-fg}\n'; 168 | } 169 | if (log.type === 'async') { 170 | if (log.status === 'resolved') { 171 | return '{bold}{green-fg}ASYNC NODE{/bold} (RESOLVED ✔){/green-fg}\n'; 172 | } 173 | if (log.status === 'rejected') { 174 | return '{bold}{red-fg}ASYNC NODE{/bold} (REJECTED ✘){/red-fg}\n'; 175 | } 176 | if (log.status === 'pending') { 177 | return '{cyan-fg}{bold}ASYNC NODE{/bold} (PENDING ⌛){/cyan-fg}\n'; 178 | } 179 | } 180 | if (log.type === 'info') { 181 | return '{white-fg}{bold}INFO{/bold}{/white-fg}\n'; 182 | } 183 | return ''; 184 | } 185 | }, { 186 | key: 'renderId', 187 | value: function renderId(log) { 188 | return '{bold}ID:{/bold} {underline}' + log.id + '{/underline}\n'; 189 | } 190 | }, { 191 | key: 'renderDate', 192 | value: function renderDate(log) { 193 | return '{bold}TIME:{/bold} {magenta-fg}' + dateFormat(log.date, 'dddd, mmmm dS yyyy, HH:MM:ss.L') + '{/magenta-fg}\n'; 194 | } 195 | }, { 196 | key: 'renderDuration', 197 | value: function renderDuration(log) { 198 | if (log.relativeDuration && log.previousLog) { 199 | return '{bold}DURATION:{/bold} {yellow-fg}' + log.relativeDuration + '{/yellow-fg} (from {underline}' + log.previousLog.id + '{/underline})\n'; 200 | } 201 | return ''; 202 | } 203 | }, { 204 | key: 'renderData', 205 | value: function renderData(log) { 206 | if (log.data) { 207 | return '{bold}DATA:{/bold} ' + log.renderData() + '\n'; 208 | } 209 | return ''; 210 | } 211 | }]); 212 | 213 | return logDetails; 214 | })(); 215 | 216 | var Node = blessed.Node; 217 | var Box = blessed.Box; 218 | 219 | function Tree(options) { 220 | 221 | if (!(this instanceof Node)) { 222 | return new Tree(options); 223 | } 224 | 225 | options = options || {}; 226 | options.bold = true; 227 | var self = this; 228 | this.options = options; 229 | this.data = {}; 230 | this.nodeLines = []; 231 | this.lineNbr = 0; 232 | Box.call(this, options); 233 | 234 | options.extended = options.extended || false; 235 | options.keys = options.keys || ['space', 'enter']; 236 | 237 | options.template = options.template || {}; 238 | options.template.extend = options.template.extend || ' [+]'; 239 | options.template.retract = options.template.retract || ' [-]'; 240 | options.template.lines = options.template.lines || false; 241 | 242 | this.rows = blessed.list({ 243 | height: 0, 244 | top: 1, 245 | width: 0, 246 | left: 1, 247 | selectedFg: 'black', 248 | selectedBg: 'white', 249 | keys: true, 250 | tags: true 251 | }); 252 | 253 | this.rows.key(options.keys, function () { 254 | self.nodeLines[this.getItemIndex(this.selected)].extended = !self.nodeLines[this.getItemIndex(this.selected)].extended; 255 | self.setData(self.data); 256 | self.screen.render(); 257 | 258 | self.emit('select', self.nodeLines[this.getItemIndex(this.selected)]); 259 | }); 260 | 261 | this.append(this.rows); 262 | } 263 | 264 | Tree.prototype.walk = function (node, treeDepth) { 265 | var lines = []; 266 | 267 | if (!node.parent) { 268 | node.parent = null; 269 | } 270 | 271 | if (treeDepth == '' && node.name) { 272 | this.lineNbr = 0; 273 | this.nodeLines[this.lineNbr++] = node; 274 | lines.push(node.name); 275 | treeDepth = ' '; 276 | } 277 | 278 | node.depth = treeDepth.length - 1; 279 | 280 | if (node.children && node.extended) { 281 | 282 | var i = 0; 283 | 284 | if (typeof node.children == 'function') { 285 | node.childrenContent = node.children(node); 286 | } 287 | 288 | if (!node.childrenContent) { 289 | node.childrenContent = node.children; 290 | } 291 | 292 | for (var child in node.childrenContent) { 293 | 294 | if (!node.childrenContent[child].name) { 295 | node.childrenContent[child].name = child; 296 | } 297 | 298 | var childIndex = child; 299 | child = node.childrenContent[child]; 300 | child.parent = node; 301 | child.position = i++; 302 | 303 | if (typeof child.extended == 'undefined') { 304 | child.extended = this.options.extended; 305 | } 306 | 307 | if (typeof child.children == 'function') { 308 | child.childrenContent = child.children(child); 309 | } else { 310 | child.childrenContent = child.children; 311 | } 312 | 313 | var isLastChild = child.position == Object.keys(child.parent.childrenContent).length - 1; 314 | var tree; 315 | var suffix = ''; 316 | if (isLastChild) { 317 | tree = '└'; 318 | } else { 319 | tree = '├'; 320 | } 321 | if (!child.childrenContent || Object.keys(child.childrenContent).length == 0) { 322 | tree += '─'; 323 | } else if (child.extended) { 324 | tree += '┬'; 325 | suffix = this.options.template.retract; 326 | } else { 327 | tree += '─'; 328 | suffix = this.options.template.extend; 329 | } 330 | 331 | if (!this.options.template.lines) { 332 | tree = '|-'; 333 | } 334 | 335 | lines.push(treeDepth + tree + child.name + suffix); 336 | 337 | this.nodeLines[this.lineNbr++] = child; 338 | 339 | var parentTree; 340 | if (isLastChild || !this.options.template.lines) { 341 | parentTree = treeDepth + ' '; 342 | } else { 343 | parentTree = treeDepth + '│'; 344 | } 345 | lines = lines.concat(this.walk(child, parentTree)); 346 | } 347 | } 348 | return lines; 349 | }; 350 | 351 | Tree.prototype.focus = function () { 352 | this.rows.focus(); 353 | }; 354 | 355 | Tree.prototype.render = function () { 356 | if (this.screen.focused == this.rows) { 357 | this.rows.focus(); 358 | } 359 | 360 | this.rows.width = this.width - 3; 361 | this.rows.height = this.height - 3; 362 | Box.prototype.render.call(this); 363 | }; 364 | 365 | Tree.prototype.setData = function (data) { 366 | 367 | var formatted = []; 368 | formatted = this.walk(data, ''); 369 | 370 | this.data = data; 371 | this.rows.setItems(formatted); 372 | }; 373 | 374 | Tree.prototype.__proto__ = Box.prototype; 375 | 376 | Tree.prototype.type = 'tree'; 377 | 378 | var ui_tree = Tree; 379 | 380 | var Inspector = (function () { 381 | function Inspector() { 382 | _classCallCheck(this, Inspector); 383 | 384 | this.channel = transceiver.channel('log'); 385 | 386 | this.element = ui_tree({ 387 | top: 'center', 388 | left: 'center', 389 | width: '90%', 390 | height: '75%', 391 | hidden: true, 392 | label: 'Inspector', 393 | tags: true, 394 | border: { 395 | type: 'line' 396 | }, 397 | style: { 398 | fg: 'white', 399 | border: { 400 | fg: '#f0f0f0' 401 | } 402 | }, 403 | template: { 404 | extend: '{bold}{green-fg} [+]{/}', 405 | retract: '{bold}{yellow-fg} [-]{/}', 406 | lines: true 407 | } 408 | }); 409 | } 410 | 411 | _createClass(Inspector, [{ 412 | key: 'open', 413 | value: function open(selectedLog) { 414 | if (!selectedLog || !selectedLog.data && !selectedLog.stackTrace) { 415 | return; 416 | } 417 | this.opened = true; 418 | this.element.show(); 419 | this.element.focus(); 420 | this.element.setData(this.prepareData(selectedLog)); 421 | } 422 | }, { 423 | key: 'close', 424 | value: function close() { 425 | this.opened = false; 426 | this.element.hide(); 427 | } 428 | }, { 429 | key: 'prepareData', 430 | value: function prepareData(log) { 431 | var content = {}; 432 | if (log.data) { 433 | content.data = JSON.parse(_prune(log.data, { 434 | depthDecr: 7, 435 | replacer: function replacer(value, defaultValue, circular) { 436 | if (typeof value === 'function') { 437 | return '"Function [pruned]"'; 438 | } 439 | if (Array.isArray(value)) { 440 | return '"Array (' + value.length + ') [pruned]"'; 441 | } 442 | if (typeof value === 'object') { 443 | return '"Object [pruned]"'; 444 | } 445 | return defaultValue; 446 | } 447 | })); 448 | } 449 | 450 | if (log.stackTrace) { 451 | content['stack trace'] = log.stackTrace.map(function (callsite) { 452 | var relativePath = path.relative(appRoot.toString(), callsite.file); 453 | return { 454 | type: callsite.type, 455 | 'function': callsite['function'], 456 | method: callsite.method, 457 | file: relativePath + ':{yellow-fg}' + callsite.line + '{/yellow-fg}:{yellow-fg}' + callsite.column + '{/yellow-fg}' 458 | }; 459 | }); 460 | } 461 | return this.formatData(content); 462 | } 463 | }, { 464 | key: 'formatData', 465 | value: function formatData(data, key) { 466 | var _this2 = this; 467 | 468 | var depth = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2]; 469 | 470 | depth++; 471 | if (typeof data === 'object') { 472 | if (data !== null) { 473 | var _ret = (function () { 474 | var name = undefined; 475 | var extended = undefined; 476 | 477 | if (depth === 2) { 478 | name = '{yellow-fg}{bold}' + key.toUpperCase() + '{/bold}{/yellow-fg} {magenta-fg}(' + data.length + '){/magenta-fg}'; 479 | extended = key === 'data'; 480 | } else { 481 | var type = Array.isArray(data) ? '[Array] {magenta-fg}(' + data.length + '){/magenta-fg}' : '[Object]'; 482 | name = '{blue-fg}{bold}' + (key ? key + ' ' : '') + '{/bold}' + type + '{/blue-fg}'; 483 | extended = depth < 4; 484 | } 485 | var newObj = { 486 | children: {}, 487 | name: name, 488 | extended: extended 489 | }; 490 | Object.keys(data).forEach(function (key) { 491 | var child = _this2.formatData(data[key], key, depth); 492 | if (child) { 493 | newObj.children[key] = child; 494 | } 495 | }); 496 | return { 497 | v: newObj 498 | }; 499 | })(); 500 | 501 | if (typeof _ret === 'object') return _ret.v; 502 | } 503 | } 504 | if (typeof data === 'function') { 505 | return { 506 | name: '{blue-fg}' + key + '{/blue-fg}: {red-fg}{bold}[Function]{/}' 507 | }; 508 | } 509 | if (typeof data === 'number') { 510 | return { 511 | name: '{blue-fg}' + key + '{/blue-fg}: {yellow-fg}' + data + '{/}' 512 | }; 513 | } 514 | if (data === null) { 515 | return { 516 | name: '{blue-fg}' + key + '{/blue-fg}: {cyan-fg}{bold}null{/}' 517 | }; 518 | } 519 | return { 520 | name: '{blue-fg}' + key + '{/blue-fg}: ' + data 521 | }; 522 | } 523 | }]); 524 | 525 | return Inspector; 526 | })(); 527 | 528 | var Ui = (function () { 529 | function Ui() { 530 | var _this3 = this; 531 | 532 | _classCallCheck(this, Ui); 533 | 534 | this.channel = transceiver.channel('ui'); 535 | this.screen = blessed.screen({ 536 | smartCSR: true 537 | }); 538 | 539 | this.logsList = new LogsList(); 540 | this.logDetails = new logDetails(); 541 | this.inspector = new Inspector(); 542 | 543 | this.separator = blessed.line({ 544 | bottom: 6, 545 | orientation: 'horizontal' 546 | }); 547 | 548 | this.screen.append(this.logsList.element); 549 | this.screen.append(this.logDetails.element); 550 | this.screen.append(this.separator); 551 | this.screen.append(this.inspector.element); 552 | 553 | this.logsList.element.focus(); 554 | 555 | this.screen.key(['q', 'C-c'], function (ch, key) { 556 | return process.exit(0); 557 | }); 558 | 559 | this.screen.key(['i'], this.toggleInspector.bind(this)); 560 | 561 | this.screen.render(); 562 | 563 | this.channel.reply('render', function () { 564 | return _this3.screen.render(); 565 | }); 566 | } 567 | 568 | _createClass(Ui, [{ 569 | key: 'toggleInspector', 570 | value: function toggleInspector() { 571 | if (this.inspector.opened) { 572 | this.inspector.close(); 573 | this.logsList.focus(); 574 | } else { 575 | this.inspector.open(this.logsList.selectedLog); 576 | } 577 | this.screen.render(); 578 | } 579 | }]); 580 | 581 | return Ui; 582 | })(); 583 | 584 | var LogItem = (function () { 585 | function LogItem(_ref) { 586 | var name = _ref.name; 587 | var type = _ref.type; 588 | var status = _ref.status; 589 | var parent = _ref.parent; 590 | var data = _ref.data; 591 | var message = _ref.message; 592 | var stackTrace = _ref.stackTrace; 593 | var _ref$date = _ref.date; 594 | var date = _ref$date === undefined ? Date.now() : _ref$date; 595 | 596 | _classCallCheck(this, LogItem); 597 | 598 | this.id = shortid.generate(); 599 | this.name = name; 600 | this.type = type; 601 | this.status = status; 602 | this.data = data; 603 | this.message = message; 604 | this.stackTrace = stackTrace; 605 | this.date = date; 606 | this.children = []; 607 | this.channel = transceiver.channel('log'); 608 | 609 | if (parent) { 610 | this.depth = parent.depth + 1; 611 | this.parent = parent; 612 | this.previousLog = parent.getLastChild() || parent; 613 | this.relativeDuration = this.getRelativeDuration(); 614 | this.parent.addChild(this); 615 | } else { 616 | this.depth = 0; 617 | } 618 | this.element = this.channel.request('addLog', this); 619 | this.update(); 620 | } 621 | 622 | _createClass(LogItem, [{ 623 | key: 'update', 624 | value: function update() { 625 | if (this.element) { 626 | this.element.content = this.render(); 627 | transceiver.request('ui', 'render'); 628 | } 629 | } 630 | }, { 631 | key: 'render', 632 | value: function render() { 633 | var message = '' + this.renderState() + this.renderName() + this.renderMessage() + this.renderData() + this.renderDate() + this.renderDuration(); 634 | for (var i = 0; i < this.depth; i++) { 635 | message = ' ' + message; 636 | } 637 | return message; 638 | } 639 | }, { 640 | key: 'renderState', 641 | value: function renderState() { 642 | if (this.type === 'async' && this.status === 'pending') { 643 | return '{cyan-fg}[⌛]{/cyan-fg} '; 644 | } 645 | if (this.type === 'async' && this.status === 'resolved') { 646 | return '{green-fg}[✔]{/green-fg} '; 647 | } 648 | if (this.type === 'async' && this.status === 'rejected') { 649 | return '{red-fg}[✘]{/red-fg} '; 650 | } 651 | if (this.type === 'success') { 652 | return '{green-fg}✔{/green-fg} '; 653 | } 654 | if (this.type === 'error') { 655 | return '{red-fg}✘{/red-fg} '; 656 | } 657 | if (this.type === 'warn') { 658 | return '{yellow-fg}❗{/yellow-fg} '; 659 | } 660 | if (this.type === 'info') { 661 | return '⇢ '; 662 | } 663 | return ''; 664 | } 665 | }, { 666 | key: 'renderName', 667 | value: function renderName() { 668 | if (this.depth === 0) { 669 | return this.name ? '{underline}{bold}' + this.name + '{/bold}{/underline} ' : ''; 670 | } 671 | if (this.type === 'async') { 672 | if (this.status === 'resolved') { 673 | return '{bold}{green-fg}' + this.name + '{/green-fg}{/bold} (async) '; 674 | } 675 | if (this.status === 'rejected') { 676 | return '{bold}{red-fg}' + this.name + '{/red-fg}{/bold} (async) '; 677 | } 678 | return '{bold}' + this.name + '{/bold} (async) '; 679 | } 680 | if (this.type === 'success') { 681 | return this.name ? '{bold}{green-fg}' + this.name + '{/green-fg}{/bold} ' : ''; 682 | } 683 | if (this.type === 'error') { 684 | return this.name ? '{bold}{red-fg}' + this.name + '{/red-fg}{/bold} ' : ''; 685 | } 686 | if (this.type === 'warn') { 687 | return this.name ? '{bold}{yellow-fg}' + this.name + '{/yellow-fg}{/bold} ' : ''; 688 | } 689 | return this.name ? '{bold}' + this.name + '{/bold} ' : ''; 690 | } 691 | }, { 692 | key: 'renderData', 693 | value: function renderData() { 694 | if (this.depth === 0) { 695 | // console.log(this.data); 696 | } 697 | if (!this.data) { 698 | return ''; 699 | } 700 | if (Array.isArray(this.data)) { 701 | return this.data.map(this.renderValue.bind(this)).join(' ') + ' '; 702 | } 703 | return this.renderValue(this.data) + ' '; 704 | } 705 | }, { 706 | key: 'renderValue', 707 | value: function renderValue(value) { 708 | if (Array.isArray(value)) { 709 | return '{cyan-fg}' + this.prune(value) + '{/cyan-fg}'; 710 | } 711 | if (typeof value === 'object') { 712 | return '{blue-fg}' + this.prune(value) + '{/blue-fg}'; 713 | } 714 | if (typeof value === 'function') { 715 | return '{red-fg}{bold}[Function]{/bold}{red-fg}'; 716 | } 717 | if (typeof value === 'number') { 718 | return '{yellow-fg}' + value + '{/yellow-fg}'; 719 | } 720 | if (typeof value === 'string') { 721 | if (this.type === 'success') { 722 | return '{green-fg}' + value + '{/green-fg}'; 723 | } 724 | if (this.type === 'error') { 725 | return '{red-fg}' + value + '{/red-fg}'; 726 | } 727 | if (this.type === 'warn') { 728 | return '{yellow-fg}' + value + '{/yellow-fg}'; 729 | } 730 | } 731 | return value; 732 | } 733 | }, { 734 | key: 'renderMessage', 735 | value: function renderMessage() { 736 | if (this.message) { 737 | if (this.type === 'success') { 738 | return '{green-fg}' + this.message + '{/green-fg} '; 739 | } 740 | if (this.type === 'error') { 741 | return '{red-fg}' + this.message + '{/red-fg} '; 742 | } 743 | if (this.type === 'warn') { 744 | return '{yellow-fg}' + this.message + '{/yellow-fg} '; 745 | } 746 | return this.message + ' '; 747 | } 748 | return ''; 749 | } 750 | }, { 751 | key: 'renderDate', 752 | value: function renderDate() { 753 | if (this.depth === 0) { 754 | return '{magenta-fg}(' + dateFormat(this.date, 'dd/mm/yyyy HH:MM:ss.L') + '){/magenta-fg} '; 755 | } 756 | return ''; 757 | } 758 | }, { 759 | key: 'renderDuration', 760 | value: function renderDuration() { 761 | if (this.relativeDuration) { 762 | return '{grey-fg}+' + this.relativeDuration + '{/grey-fg} '; 763 | } 764 | return ''; 765 | } 766 | }, { 767 | key: 'getRelativeDuration', 768 | value: function getRelativeDuration() { 769 | return this.humanizeDuration(this.date - this.previousLog.date); 770 | } 771 | }, { 772 | key: 'humanizeDuration', 773 | value: function humanizeDuration(duration) { 774 | if (duration < 1000) { 775 | return duration + 'ms'; 776 | } 777 | if (duration < 60000) { 778 | var milliseconds = duration % 1000; 779 | milliseconds = ('000' + milliseconds).slice(-3); 780 | return Math.floor(duration / 1000) + '.' + milliseconds + 's'; 781 | } 782 | return Math.floor(duration / 60000) + 'm ' + Math.round(duration % 60000 / 1000) + 's'; 783 | } 784 | }, { 785 | key: 'addChild', 786 | value: function addChild(log) { 787 | this.children.push(log); 788 | } 789 | }, { 790 | key: 'getLastChild', 791 | value: function getLastChild() { 792 | return this.children[this.children.length - 1]; 793 | } 794 | }, { 795 | key: 'getChildren', 796 | value: function getChildren(list) { 797 | list = list || []; 798 | list.push.apply(list, this.children); 799 | this.children.forEach(function (child) { 800 | child.getChildren(list); 801 | }); 802 | return list; 803 | } 804 | }, { 805 | key: 'setStatus', 806 | value: function setStatus(status) { 807 | this.status = status; 808 | this.update(); 809 | } 810 | }, { 811 | key: 'prune', 812 | value: function prune(value) { 813 | return _prune(value, { 814 | depthDecr: 2, 815 | arrayMaxLength: 8, 816 | prunedString: ' [...]' 817 | }); 818 | } 819 | }]); 820 | 821 | return LogItem; 822 | })(); 823 | 824 | var Agent = (function () { 825 | function Agent(_ref2) { 826 | var name = _ref2.name; 827 | var type = _ref2.type; 828 | var status = _ref2.status; 829 | var data = _ref2.data; 830 | var message = _ref2.message; 831 | var _ref2$isAsync = _ref2.isAsync; 832 | var isAsync = _ref2$isAsync === undefined ? false : _ref2$isAsync; 833 | var ancestors = _ref2.ancestors; 834 | 835 | _classCallCheck(this, Agent); 836 | 837 | this.name = name; 838 | this.children = {}; 839 | this.isAsync = isAsync; 840 | this.asyncState = this.isAsync ? 'pending' : null; 841 | this.type = type; 842 | this.status = status; 843 | 844 | if (!ancestors) { 845 | this.ancestors = []; 846 | this.isRoot = true; 847 | } else { 848 | this.ancestors = ancestors; 849 | this.parent = this.ancestors[this.ancestors.length - 1]; 850 | } 851 | 852 | this.logItem = new LogItem({ 853 | name: this.name, 854 | type: this.type, 855 | status: this.status, 856 | parent: this.parent ? this.parent.logItem : null, 857 | data: data, 858 | message: message, 859 | stackTrace: this.generateStackTrace(stack_trace.get()) 860 | }); 861 | 862 | return this; 863 | } 864 | 865 | _createClass(Agent, [{ 866 | key: 'log', 867 | value: function log() { 868 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 869 | args[_key] = arguments[_key]; 870 | } 871 | 872 | new Agent({ 873 | type: 'info', 874 | data: args, 875 | ancestors: this.ancestors.concat(this) 876 | }); 877 | return this; 878 | } 879 | }, { 880 | key: 'warn', 881 | value: function warn() { 882 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 883 | args[_key2] = arguments[_key2]; 884 | } 885 | 886 | new Agent({ 887 | type: 'warn', 888 | data: args, 889 | ancestors: this.ancestors.concat(this) 890 | }); 891 | return this; 892 | } 893 | }, { 894 | key: 'success', 895 | value: function success() { 896 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 897 | args[_key3] = arguments[_key3]; 898 | } 899 | 900 | new Agent({ 901 | type: 'success', 902 | data: args, 903 | ancestors: this.ancestors.concat(this) 904 | }); 905 | return this; 906 | } 907 | }, { 908 | key: 'error', 909 | value: function error() { 910 | for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 911 | args[_key4] = arguments[_key4]; 912 | } 913 | 914 | new Agent({ 915 | type: 'error', 916 | data: args, 917 | ancestors: this.ancestors.concat(this) 918 | }); 919 | return this; 920 | } 921 | }, { 922 | key: 'child', 923 | value: function child(name) { 924 | if (!this.children[name]) { 925 | this.children[name] = new Agent({ 926 | name: name, 927 | type: 'node', 928 | ancestors: this.ancestors.concat(this) 929 | }); 930 | } 931 | 932 | for (var _len5 = arguments.length, args = Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) { 933 | args[_key5 - 1] = arguments[_key5]; 934 | } 935 | 936 | if (args.length) { 937 | var _children$name; 938 | 939 | (_children$name = this.children[name]).log.apply(_children$name, args); 940 | } 941 | return this.children[name]; 942 | } 943 | }, { 944 | key: 'async', 945 | value: function async(name) { 946 | if (!this.children[name]) { 947 | this.children[name] = new Agent({ 948 | name: name, 949 | type: 'async', 950 | status: 'pending', 951 | isAsync: true, 952 | ancestors: this.ancestors.concat(this) 953 | }); 954 | } 955 | if (!this.children[name].isAsync) { 956 | this.internalWarn('Child agent {bold}' + name + '{/bold} is defined as a non async agent'); 957 | } 958 | 959 | for (var _len6 = arguments.length, args = Array(_len6 > 1 ? _len6 - 1 : 0), _key6 = 1; _key6 < _len6; _key6++) { 960 | args[_key6 - 1] = arguments[_key6]; 961 | } 962 | 963 | if (args.length) { 964 | var _children$name2; 965 | 966 | (_children$name2 = this.children[name]).log.apply(_children$name2, args); 967 | } 968 | return this.children[name]; 969 | } 970 | }, { 971 | key: 'resolve', 972 | value: function resolve() { 973 | if (this.isAsync) { 974 | if (this.logItem.status === 'pending') { 975 | this.logItem.setStatus('resolved'); 976 | var resolveLog = new Agent({ 977 | name: this.name, 978 | type: 'success', 979 | message: 'resolved', 980 | ancestors: this.ancestors.concat(this) 981 | }); 982 | 983 | for (var _len7 = arguments.length, args = Array(_len7), _key7 = 0; _key7 < _len7; _key7++) { 984 | args[_key7] = arguments[_key7]; 985 | } 986 | 987 | if (args.length) { 988 | resolveLog.success.apply(resolveLog, args); 989 | } 990 | } else { 991 | this.internalWarn('Trying to resolve an already {bold}' + this.logItem.status + '{/bold} async agent'); 992 | } 993 | } else { 994 | this.internalWarn('Trying to resolve a non async agent'); 995 | } 996 | return this; 997 | } 998 | }, { 999 | key: 'reject', 1000 | value: function reject() { 1001 | if (this.isAsync) { 1002 | if (this.logItem.status === 'pending') { 1003 | this.logItem.setStatus('rejected'); 1004 | var rejectLog = new Agent({ 1005 | name: this.name, 1006 | type: 'error', 1007 | message: 'rejected', 1008 | ancestors: this.ancestors.concat(this) 1009 | }); 1010 | 1011 | for (var _len8 = arguments.length, args = Array(_len8), _key8 = 0; _key8 < _len8; _key8++) { 1012 | args[_key8] = arguments[_key8]; 1013 | } 1014 | 1015 | if (args.length) { 1016 | rejectLog.error.apply(rejectLog, args); 1017 | } 1018 | } else { 1019 | this.internalWarn('Trying to reject an already {bold}' + this.logItem.status + '{/bold} async agent'); 1020 | } 1021 | } else { 1022 | this.internalWarn('Trying to reject a non async agent'); 1023 | } 1024 | return this; 1025 | } 1026 | }, { 1027 | key: 'internalWarn', 1028 | value: function internalWarn(message) { 1029 | new Agent({ 1030 | name: this.name, 1031 | type: 'warn', 1032 | message: message, 1033 | ancestors: this.ancestors.concat(this) 1034 | }); 1035 | } 1036 | }, { 1037 | key: 'getAncestorsNames', 1038 | value: function getAncestorsNames() { 1039 | return this.ancestors.map(function (ancestor) { 1040 | return ancestor.name; 1041 | }); 1042 | } 1043 | }, { 1044 | key: 'generateStackTrace', 1045 | value: function generateStackTrace(trace) { 1046 | var stackTrace = []; 1047 | for (var i = 0; i < 5; i++) { 1048 | stackTrace.push({ 1049 | type: trace[i].getTypeName(), 1050 | 'function': trace[i].getFunctionName(), 1051 | method: trace[i].getMethodName(), 1052 | file: trace[i].getFileName(), 1053 | line: trace[i].getLineNumber(), 1054 | column: trace[i].getColumnNumber() 1055 | }); 1056 | } 1057 | return stackTrace; 1058 | } 1059 | }]); 1060 | 1061 | return Agent; 1062 | })(); 1063 | 1064 | var agent = function agent(name) { 1065 | for (var _len9 = arguments.length, args = Array(_len9 > 1 ? _len9 - 1 : 0), _key9 = 1; _key9 < _len9; _key9++) { 1066 | args[_key9 - 1] = arguments[_key9]; 1067 | } 1068 | 1069 | return new Agent({ 1070 | name: name, 1071 | type: 'root', 1072 | data: args.length ? args : undefined 1073 | }); 1074 | }; 1075 | 1076 | transceiver.setPromise(null); 1077 | 1078 | var ui = new Ui(); 1079 | 1080 | var investigator = { ui: ui, agent: agent }; 1081 | 1082 | return investigator; 1083 | }); 1084 | //# sourceMappingURL=investigator.js.map 1085 | -------------------------------------------------------------------------------- /dist/investigator.min.js: -------------------------------------------------------------------------------- 1 | function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var n=0;nr}var o={children:{},name:s,extended:i};return Object.keys(e).forEach(function(t){var s=n.formatData(e[t],t,r);s&&(o.children[t]=s)}),{v:o}}();if("object"==typeof s)return s.v}return"function"==typeof e?{name:"{blue-fg}"+t+"{/blue-fg}: {red-fg}{bold}[Function]{/}"}:"number"==typeof e?{name:"{blue-fg}"+t+"{/blue-fg}: {yellow-fg}"+e+"{/}"}:null===e?{name:"{blue-fg}"+t+"{/blue-fg}: {cyan-fg}{bold}null{/}"}:{name:"{blue-fg}"+t+"{/blue-fg}: "+e}}}]),t}(),y=function(){function n(){var r=this;_classCallCheck(this,n),this.channel=e.channel("ui"),this.screen=t.screen({smartCSR:!0}),this.logsList=new h,this.logDetails=new c,this.inspector=new f,this.separator=t.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(e,t){return process.exit(0)}),this.screen.key(["i"],this.toggleInspector.bind(this)),this.screen.render(),this.channel.reply("render",function(){return r.screen.render()})}return _createClass(n,[{key:"toggleInspector",value:function(){this.inspector.opened?(this.inspector.close(),this.logsList.focus()):this.inspector.open(this.logsList.selectedLog),this.screen.render()}}]),n}(),p=function(){function t(n){var r=n.name,s=n.type,i=n.status,o=n.parent,l=n.data,h=n.message,c=n.stackTrace,u=n.date,d=void 0===u?Date.now():u;_classCallCheck(this,t),this.id=a.generate(),this.name=r,this.type=s,this.status=i,this.data=l,this.message=h,this.stackTrace=c,this.date=d,this.children=[],this.channel=e.channel("log"),o?(this.depth=o.depth+1,this.parent=o,this.previousLog=o.getLastChild()||o,this.relativeDuration=this.getRelativeDuration(),this.parent.addChild(this)):this.depth=0,this.element=this.channel.request("addLog",this),this.update()}return _createClass(t,[{key:"update",value:function(){this.element&&(this.element.content=this.render(),e.request("ui","render"))}},{key:"render",value:function(){for(var e=""+this.renderState()+this.renderName()+this.renderMessage()+this.renderData()+this.renderDate()+this.renderDuration(),t=0;te)return e+"ms";if(6e4>e){var t=e%1e3;return t=("000"+t).slice(-3),Math.floor(e/1e3)+"."+t+"s"}return Math.floor(e/6e4)+"m "+Math.round(e%6e4/1e3)+"s"}},{key:"addChild",value:function(e){this.children.push(e)}},{key:"getLastChild",value:function(){return this.children[this.children.length-1]}},{key:"getChildren",value:function(e){return e=e||[],e.push.apply(e,this.children),this.children.forEach(function(t){t.getChildren(e)}),e}},{key:"setStatus",value:function(e){this.status=e,this.update()}},{key:"prune",value:function(e){return r(e,{depthDecr:2,arrayMaxLength:8,prunedString:" [...]"})}}]),t}(),m=function(){function e(t){var n=t.name,r=t.type,s=t.status,i=t.data,a=t.message,l=t.isAsync,h=void 0===l?!1:l,c=t.ancestors;return _classCallCheck(this,e),this.name=n,this.children={},this.isAsync=h,this.asyncState=this.isAsync?"pending":null,this.type=r,this.status=s,c?(this.ancestors=c,this.parent=this.ancestors[this.ancestors.length-1]):(this.ancestors=[],this.isRoot=!0),this.logItem=new p({name:this.name,type:this.type,status:this.status,parent:this.parent?this.parent.logItem:null,data:i,message:a,stackTrace:this.generateStackTrace(o.get())}),this}return _createClass(e,[{key:"log",value:function(){for(var t=arguments.length,n=Array(t),r=0;t>r;r++)n[r]=arguments[r];return new e({type:"info",data:n,ancestors:this.ancestors.concat(this)}),this}},{key:"warn",value:function(){for(var t=arguments.length,n=Array(t),r=0;t>r;r++)n[r]=arguments[r];return new e({type:"warn",data:n,ancestors:this.ancestors.concat(this)}),this}},{key:"success",value:function(){for(var t=arguments.length,n=Array(t),r=0;t>r;r++)n[r]=arguments[r];return new e({type:"success",data:n,ancestors:this.ancestors.concat(this)}),this}},{key:"error",value:function(){for(var t=arguments.length,n=Array(t),r=0;t>r;r++)n[r]=arguments[r];return new e({type:"error",data:n,ancestors:this.ancestors.concat(this)}),this}},{key:"child",value:function(t){this.children[t]||(this.children[t]=new e({name:t,type:"node",ancestors:this.ancestors.concat(this)}));for(var n=arguments.length,r=Array(n>1?n-1:0),s=1;n>s;s++)r[s-1]=arguments[s];if(r.length){var i;(i=this.children[t]).log.apply(i,r)}return this.children[t]}},{key:"async",value:function(t){this.children[t]||(this.children[t]=new e({name:t,type:"async",status:"pending",isAsync:!0,ancestors:this.ancestors.concat(this)})),this.children[t].isAsync||this.internalWarn("Child agent {bold}"+t+"{/bold} is defined as a non async agent");for(var n=arguments.length,r=Array(n>1?n-1:0),s=1;n>s;s++)r[s-1]=arguments[s];if(r.length){var i;(i=this.children[t]).log.apply(i,r)}return this.children[t]}},{key:"resolve",value:function(){if(this.isAsync)if("pending"===this.logItem.status){this.logItem.setStatus("resolved");for(var t=new e({name:this.name,type:"success",message:"resolved",ancestors:this.ancestors.concat(this)}),n=arguments.length,r=Array(n),s=0;n>s;s++)r[s]=arguments[s];r.length&&t.success.apply(t,r)}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(){if(this.isAsync)if("pending"===this.logItem.status){this.logItem.setStatus("rejected");for(var t=new e({name:this.name,type:"error",message:"rejected",ancestors:this.ancestors.concat(this)}),n=arguments.length,r=Array(n),s=0;n>s;s++)r[s]=arguments[s];r.length&&t.error.apply(t,r)}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(t){new e({name:this.name,type:"warn",message:t,ancestors:this.ancestors.concat(this)})}},{key:"getAncestorsNames",value:function(){return this.ancestors.map(function(e){return e.name})}},{key:"generateStackTrace",value:function(e){for(var t=[],n=0;5>n;n++)t.push({type:e[n].getTypeName(),"function":e[n].getFunctionName(),method:e[n].getMethodName(),file:e[n].getFileName(),line:e[n].getLineNumber(),column:e[n].getColumnNumber()});return t}}]),e}(),v=function(e){for(var t=arguments.length,n=Array(t>1?t-1:0),r=1;t>r;r++)n[r-1]=arguments[r];return new m({name:e,type:"root",data:n.length?n:void 0})};e.setPromise(null);var b=new y,k={ui:b,agent:v};return k}); 2 | //# sourceMappingURL=investigator.min.js.map 3 | -------------------------------------------------------------------------------- /dist/investigator.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["investigator.min.js","/source/investigator.js","/source/src/ui/tree.js","/source/src/ui/logsList.js","/source/src/ui/logDetails.js","/source/src/ui/inspector.js","/source/src/ui/index.js","/source/src/ui/logItem.js","/source/src/agent.js","/source/src/investigator.js"],"names":["_classCallCheck","instance","Constructor","TypeError","_createClass","defineProperties","target","props","i","length","descriptor","enumerable","configurable","writable","Object","defineProperty","key","protoProps","staticProps","prototype","global","factory","exports","module","require","define","amd","investigator","transceiver","blessed","dateFormat","prune","path","appRoot","shortid","stack_trace","this","_prune","Tree","options","Node","bold","self","data","nodeLines","lineNbr","Box","call","extended","keys","template","extend","retract","lines","rows","list","height","top","width","left","selectedFg","selectedBg","tags","getItemIndex","selected","setData","screen","render","emit","append","LogsList","_this","selectedLog","logs","logsCount","channel","autoScroll","element","bottom","mouse","scrollbar","bg","style","fg","ch","name","scrollToBottom","request","on","getLogFromElement","reply","addLog","getSelectedLog","value","log","undefined","id","parent","index","getChildren","insertItem","getItem","add","logId","move","focus","logDetails","box","padding","right","border","hover","updateLogDetails","bind","setContent","renderType","renderId","renderDate","renderDuration","renderData","type","status","date","relativeDuration","previousLog","walk","node","treeDepth","push","depth","children","childrenContent","child","position","tree","isLastChild","suffix","parentTree","concat","focused","formatted","setItems","__proto__","ui_tree","Inspector","hidden","label","stackTrace","opened","show","prepareData","hide","content","JSON","parse","depthDecr","replacer","defaultValue","circular","Array","isArray","map","callsite","relativePath","relative","toString","file","function","method","line","column","formatData","_this2","arguments","_ret","toUpperCase","newObj","forEach","v","Ui","_this3","smartCSR","logsList","inspector","separator","orientation","process","exit","toggleInspector","close","open","LogItem","_ref","message","_ref$date","Date","now","generate","getLastChild","getRelativeDuration","addChild","update","renderState","renderName","renderMessage","renderValue","join","humanizeDuration","duration","milliseconds","slice","Math","floor","round","apply","arrayMaxLength","prunedString","Agent","_ref2","_ref2$isAsync","isAsync","ancestors","asyncState","isRoot","logItem","generateStackTrace","get","_len","args","_key","_len2","_key2","_len3","_key3","_len4","_key4","_len5","_key5","_children$name","internalWarn","_len6","_key6","_children$name2","setStatus","resolveLog","_len7","_key7","success","rejectLog","_len8","_key8","error","ancestor","trace","getTypeName","getFunctionName","getMethodName","getFileName","getLineNumber","getColumnNumber","agent","_len9","_key9","setPromise","ui"],"mappings":"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","file":"investigator.min.js","sourcesContent":[null,"(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('transceiver'), require('blessed'), require('dateformat'), require('json-prune'), require('path'), require('app-root-path'), require('shortid'), require('stack-trace')) :\n typeof define === 'function' && define.amd ? define(['transceiver', 'blessed', 'dateformat', 'json-prune', 'path', 'app-root-path', 'shortid', 'stack-trace'], factory) :\n global.investigator = factory(global.transceiver, global.blessed, global.dateFormat, global.prune, global.path, global.appRoot, global.shortid, global.stack_trace)\n}(this, function (transceiver, blessed, dateFormat, prune, path, appRoot, shortid, stack_trace) { 'use strict';\n\n class LogsList {\n constructor() {\n this.selectedLog = null;\n this.logs = {};\n this.logsCount = 0;\n this.channel = transceiver.channel('log');\n this.autoScroll = true;\n this.element = blessed.list({\n top: '0',\n left: '0',\n bottom: 7,\n tags: true,\n keys: true,\n mouse: true,\n scrollbar: {\n bg: 'magenta',\n },\n style: {\n selected: {\n fg: 'black',\n bg: 'white',\n }\n }\n });\n\n this.element.key(['up', 'down', 's', 'b'], (ch, key) => {\n if (key.name === 's') {\n this.autoScroll = !this.autoScroll;\n } else if (key.name === 'b') {\n this.scrollToBottom();\n transceiver.request('ui', 'render');\n } else {\n this.autoScroll = false;\n }\n });\n\n this.element.on('select item', (element, i) => {\n this.selectedLog = this.getLogFromElement(element);\n if (this.selectedLog) {\n this.channel.emit('select log', this.selectedLog);\n }\n });\n\n this.channel.reply({\n addLog: this.addLog,\n getSelectedLog: this.getSelectedLog,\n }, this);\n }\n\n addLog(log) {\n let element;\n\n this.logs[log.id] = log;\n this.logsCount++;\n\n if (log.parent) {\n const index = this.element.getItemIndex(log.parent.element) + log.parent.getChildren().length;\n this.element.insertItem(index, log.render());\n element = this.element.getItem(index);\n } else {\n element = this.element.add(log.render());\n }\n element.logId = log.id;\n if (this.autoScroll) {\n this.scrollToBottom();\n }\n if (this.logsCount === 1) {\n this.channel.emit('select log', log);\n }\n return element;\n }\n\n getSelectedLog() {\n return this.selectedLog;\n }\n\n scrollToBottom() {\n this.element.move(this.logsCount);\n }\n\n getLogFromElement(element) {\n return this.logs[element.logId];\n }\n\n focus() {\n this.element.focus();\n }\n }\n\n class logDetails {\n constructor() {\n this.channel = transceiver.channel('log');\n this.element = blessed.box({\n height: 6,\n left: '0',\n bottom: 0,\n tags: true,\n keys: true,\n padding: {\n left: 1,\n right: 1,\n },\n style: {\n selected: {\n fg: 'black',\n bg: 'white',\n border: {\n fg: 'white'\n },\n hover: {\n bg: 'green'\n }\n }\n }\n });\n\n this.channel.on('select log', this.updateLogDetails.bind(this));\n }\n\n updateLogDetails(log) {\n this.element.setContent(this.renderType(log) + this.renderId(log) + this.renderDate(log) + this.renderDuration(log) + this.renderData(log));\n }\n\n renderType(log) {\n if (log.type === 'root') {\n return '{magenta-fg}{bold}ROOT NODE{/bold}{/magenta-fg}\\n';\n }\n if (log.type === 'success') {\n return '{green-fg}✔ {bold}SUCCESS{/bold}{/green-fg}\\n';\n }\n if (log.type === 'error') {\n return '{red-fg}✘ {bold}ERROR{/bold}{/red-fg}\\n';\n }\n if (log.type === 'warn') {\n return '{yellow-fg}! {bold}WARN{/bold}{/red-fg}\\n';\n }\n if (log.type === 'node') {\n return '{grey-fg}{bold}NODE{/bold}{/grey-fg}\\n';\n }\n if (log.type === 'async') {\n if (log.status === 'resolved') {\n return '{bold}{green-fg}ASYNC NODE{/bold} (RESOLVED ✔){/green-fg}\\n';\n }\n if (log.status === 'rejected') {\n return '{bold}{red-fg}ASYNC NODE{/bold} (REJECTED ✘){/red-fg}\\n';\n }\n if (log.status === 'pending') {\n return '{cyan-fg}{bold}ASYNC NODE{/bold} (PENDING ⌛){/cyan-fg}\\n';\n }\n }\n if (log.type === 'info') {\n return '{white-fg}{bold}INFO{/bold}{/white-fg}\\n';\n }\n return '';\n }\n\n renderId(log) {\n return `{bold}ID:{/bold} {underline}${log.id}{/underline}\\n`;\n }\n\n renderDate(log) {\n return `{bold}TIME:{/bold} {magenta-fg}${dateFormat(log.date, 'dddd, mmmm dS yyyy, HH:MM:ss.L')}{/magenta-fg}\\n`;\n }\n\n renderDuration(log) {\n if (log.relativeDuration && log.previousLog) {\n return `{bold}DURATION:{/bold} {yellow-fg}${log.relativeDuration}{/yellow-fg} (from {underline}${log.previousLog.id}{/underline})\\n`;\n }\n return '';\n }\n\n renderData(log) {\n if (log.data) {\n return `{bold}DATA:{/bold} ${log.renderData()}\\n`;\n }\n return '';\n }\n }\n\n // https://github.com/yaronn/blessed-contrib/blob/master/lib/widget/tree.js\n const Node = blessed.Node;\n const Box = blessed.Box;\n\n function Tree(options) {\n\n if (!(this instanceof Node)) {\n return new Tree(options);\n }\n\n options = options || {};\n options.bold = true;\n var self = this;\n this.options = options;\n this.data = {};\n this.nodeLines = [];\n this.lineNbr = 0;\n Box.call(this, options);\n\n options.extended = options.extended || false;\n options.keys = options.keys || ['space','enter'];\n\n options.template = options.template || {};\n options.template.extend = options.template.extend || ' [+]';\n options.template.retract = options.template.retract || ' [-]';\n options.template.lines = options.template.lines || false;\n\n this.rows = blessed.list({\n height: 0,\n top: 1,\n width: 0,\n left: 1,\n selectedFg: 'black',\n selectedBg: 'white',\n keys: true,\n tags: true,\n });\n\n this.rows.key(options.keys,function() {\n self.nodeLines[this.getItemIndex(this.selected)].extended = !self.nodeLines[this.getItemIndex(this.selected)].extended;\n self.setData(self.data);\n self.screen.render();\n\n self.emit('select',self.nodeLines[this.getItemIndex(this.selected)]);\n });\n\n this.append(this.rows);\n }\n\n Tree.prototype.walk = function(node, treeDepth) {\n var lines = [];\n\n if (!node.parent) {\n node.parent = null;\n }\n\n if (treeDepth == '' && node.name) {\n this.lineNbr = 0;\n this.nodeLines[this.lineNbr++] = node;\n lines.push(node.name);\n treeDepth = ' ';\n }\n\n node.depth = treeDepth.length - 1;\n\n if (node.children && node.extended) {\n\n var i = 0;\n\n if (typeof node.children == 'function') {\n node.childrenContent = node.children(node);\n }\n\n if (!node.childrenContent) {\n node.childrenContent = node.children;\n }\n\n for (var child in node.childrenContent) {\n\n if (!node.childrenContent[child].name) {\n node.childrenContent[child].name = child;\n }\n\n var childIndex = child;\n child = node.childrenContent[child];\n child.parent = node;\n child.position = i++;\n\n if (typeof child.extended == 'undefined') {\n child.extended = this.options.extended;\n }\n\n if (typeof child.children == 'function') {\n child.childrenContent = child.children(child);\n } else {\n child.childrenContent = child.children;\n }\n\n var isLastChild = child.position == Object.keys(child.parent.childrenContent).length - 1;\n var tree;\n var suffix = '';\n if (isLastChild) {\n tree = '└';\n } else {\n tree = '├';\n }\n if (!child.childrenContent || Object.keys(child.childrenContent).length == 0) {\n tree += '─';\n } else if (child.extended) {\n tree += '┬';\n suffix = this.options.template.retract;\n } else {\n tree += '─';\n suffix = this.options.template.extend;\n }\n\n if (!this.options.template.lines) {\n tree = '|-';\n }\n\n lines.push(treeDepth + tree + child.name + suffix);\n\n this.nodeLines[this.lineNbr++] = child;\n\n var parentTree;\n if (isLastChild || !this.options.template.lines) {\n parentTree = treeDepth + ' ';\n } else {\n parentTree = treeDepth + '│';\n }\n lines = lines.concat(this.walk(child, parentTree));\n }\n }\n return lines;\n };\n\n Tree.prototype.focus = function() {\n this.rows.focus();\n };\n\n Tree.prototype.render = function() {\n if (this.screen.focused == this.rows) {\n this.rows.focus();\n }\n\n this.rows.width = this.width - 3;\n this.rows.height = this.height - 3;\n Box.prototype.render.call(this);\n };\n\n Tree.prototype.setData = function(data) {\n\n var formatted = [];\n formatted = this.walk(data,'');\n\n this.data = data;\n this.rows.setItems(formatted);\n };\n\n Tree.prototype.__proto__ = Box.prototype;\n\n Tree.prototype.type = 'tree';\n\n var ui_tree = Tree;\n\n class Inspector {\n constructor() {\n this.channel = transceiver.channel('log');\n\n this.element = ui_tree({\n top: 'center',\n left: 'center',\n width: '90%',\n height: '75%',\n hidden: true,\n label: 'Inspector',\n tags: true,\n border: {\n type: 'line'\n },\n style: {\n fg: 'white',\n border: {\n fg: '#f0f0f0'\n },\n },\n template: {\n extend: '{bold}{green-fg} [+]{/}',\n retract: '{bold}{yellow-fg} [-]{/}',\n lines: true,\n }\n });\n }\n\n open(selectedLog) {\n if (!selectedLog || !selectedLog.data && !selectedLog.stackTrace) {\n return;\n }\n this.opened = true;\n this.element.show();\n this.element.focus();\n this.element.setData(this.prepareData(selectedLog));\n }\n\n close() {\n this.opened = false;\n this.element.hide();\n }\n\n prepareData(log) {\n const content = {};\n if (log.data) {\n content.data = JSON.parse(prune(log.data, {\n depthDecr: 7,\n replacer: (value, defaultValue, circular) => {\n if (typeof value === 'function') {\n return '\"Function [pruned]\"';\n }\n if (Array.isArray(value)) {\n return `\"Array (${value.length}) [pruned]\"`;\n }\n if (typeof value === 'object') {\n return '\"Object [pruned]\"';\n }\n return defaultValue;\n }\n }));\n }\n\n if (log.stackTrace) {\n content['stack trace'] = log.stackTrace.map((callsite) => {\n const relativePath = path.relative(appRoot.toString(), callsite.file);\n return {\n type: callsite.type,\n function: callsite.function,\n method: callsite.method,\n file: `${relativePath}:{yellow-fg}${callsite.line}{/yellow-fg}:{yellow-fg}${callsite.column}{/yellow-fg}`,\n };\n });\n }\n return this.formatData(content);\n }\n\n formatData(data, key, depth = 0) {\n depth++;\n if (typeof data === 'object') {\n if (data !== null) {\n let name;\n let extended;\n\n if (depth === 2) {\n name = `{yellow-fg}{bold}${key.toUpperCase()}{/bold}{/yellow-fg} {magenta-fg}(${data.length}){/magenta-fg}`;\n extended = key === 'data';\n } else {\n const type = (Array.isArray(data) ? `[Array] {magenta-fg}(${data.length}){/magenta-fg}` : '[Object]');\n name = `{blue-fg}{bold}${key ? key + ' ' : ''}{/bold}${type}{/blue-fg}`;\n extended = depth < 4;\n }\n const newObj = {\n children: {},\n name,\n extended\n };\n Object.keys(data).forEach((key) => {\n const child = this.formatData(data[key], key, depth);\n if (child) {\n newObj.children[key] = child;\n }\n });\n return newObj;\n }\n }\n if (typeof data === 'function') {\n return {\n name: `{blue-fg}${key}{/blue-fg}: {red-fg}{bold}[Function]{/}`,\n };\n }\n if (typeof data === 'number') {\n return {\n name: `{blue-fg}${key}{/blue-fg}: {yellow-fg}${data}{/}`,\n };\n }\n if (data === null) {\n return {\n name: `{blue-fg}${key}{/blue-fg}: {cyan-fg}{bold}null{/}`,\n };\n }\n return {\n name: `{blue-fg}${key}{/blue-fg}: ${data}`,\n };\n }\n }\n\n class Ui {\n constructor() {\n this.channel = transceiver.channel('ui');\n this.screen = blessed.screen({\n smartCSR: true\n });\n\n this.logsList = new LogsList();\n this.logDetails = new logDetails();\n this.inspector = new Inspector();\n\n this.separator = blessed.line({\n bottom: 6,\n orientation: 'horizontal'\n });\n\n this.screen.append(this.logsList.element);\n this.screen.append(this.logDetails.element);\n this.screen.append(this.separator);\n this.screen.append(this.inspector.element);\n\n this.logsList.element.focus();\n\n this.screen.key(['q', 'C-c'], function(ch, key) {\n return process.exit(0);\n });\n\n this.screen.key(['i'], this.toggleInspector.bind(this));\n\n this.screen.render();\n\n this.channel.reply('render', () => this.screen.render());\n }\n\n toggleInspector() {\n if (this.inspector.opened) {\n this.inspector.close();\n this.logsList.focus();\n } else {\n this.inspector.open(this.logsList.selectedLog);\n }\n this.screen.render();\n }\n }\n\n class LogItem {\n constructor({name, type, status, parent, data, message, stackTrace, date = Date.now()}) {\n this.id = shortid.generate();\n this.name = name;\n this.type = type;\n this.status = status;\n this.data = data;\n this.message = message;\n this.stackTrace = stackTrace;\n this.date = date;\n this.children = [];\n this.channel = transceiver.channel('log');\n\n if (parent) {\n this.depth = parent.depth + 1;\n this.parent = parent;\n this.previousLog = parent.getLastChild() || parent;\n this.relativeDuration = this.getRelativeDuration();\n this.parent.addChild(this);\n } else {\n this.depth = 0;\n }\n this.element = this.channel.request('addLog', this);\n this.update();\n }\n\n update() {\n if (this.element) {\n this.element.content = this.render();\n transceiver.request('ui', 'render');\n }\n }\n\n render() {\n let message = `${this.renderState()}${this.renderName()}${this.renderMessage()}${this.renderData()}${this.renderDate()}${this.renderDuration()}`;\n for (let i = 0; i < this.depth; i++) {\n message = ' ' + message;\n }\n return message;\n }\n\n renderState() {\n if (this.type === 'async' && this.status === 'pending') {\n return `{cyan-fg}[⌛]{/cyan-fg} `;\n }\n if (this.type === 'async' && this.status === 'resolved') {\n return `{green-fg}[✔]{/green-fg} `;\n }\n if (this.type === 'async' && this.status === 'rejected') {\n return `{red-fg}[✘]{/red-fg} `;\n }\n if (this.type === 'success') {\n return `{green-fg}✔{/green-fg} `;\n }\n if (this.type === 'error') {\n return `{red-fg}✘{/red-fg} `;\n }\n if (this.type === 'warn') {\n return `{yellow-fg}❗{/yellow-fg} `;\n }\n if (this.type === 'info') {\n return '⇢ ';\n }\n return '';\n }\n\n renderName() {\n if (this.depth === 0) {\n return this.name ? `{underline}{bold}${this.name}{/bold}{/underline} ` : '';\n }\n if (this.type === 'async') {\n if (this.status === 'resolved') {\n return `{bold}{green-fg}${this.name}{/green-fg}{/bold} (async) `;\n }\n if (this.status === 'rejected') {\n return `{bold}{red-fg}${this.name}{/red-fg}{/bold} (async) `;\n }\n return `{bold}${this.name}{/bold} (async) `;\n }\n if (this.type === 'success') {\n return this.name ? `{bold}{green-fg}${this.name}{/green-fg}{/bold} ` : '';\n }\n if (this.type === 'error') {\n return this.name ? `{bold}{red-fg}${this.name}{/red-fg}{/bold} ` : '';\n }\n if (this.type === 'warn') {\n return this.name ? `{bold}{yellow-fg}${this.name}{/yellow-fg}{/bold} ` : '';\n }\n return this.name ? `{bold}${this.name}{/bold} ` : '';\n }\n\n renderData() {\n if (this.depth === 0) {\n // console.log(this.data);\n }\n if (!this.data) {\n return '';\n }\n if (Array.isArray(this.data)) {\n return this.data.map(this.renderValue.bind(this)).join(' ') + ' ';\n }\n return this.renderValue(this.data) + ' ';\n }\n\n renderValue(value) {\n if (Array.isArray(value)) {\n return `{cyan-fg}${this.prune(value)}{/cyan-fg}`;\n }\n if (typeof value === 'object') {\n return `{blue-fg}${this.prune(value)}{/blue-fg}`;\n }\n if (typeof value === 'function') {\n return `{red-fg}{bold}[Function]{/bold}{red-fg}`;\n }\n if (typeof value === 'number') {\n return `{yellow-fg}${value}{/yellow-fg}`;\n }\n if (typeof value === 'string') {\n if (this.type === 'success') {\n return `{green-fg}${value}{/green-fg}`;\n }\n if (this.type === 'error') {\n return `{red-fg}${value}{/red-fg}`;\n }\n if (this.type === 'warn') {\n return `{yellow-fg}${value}{/yellow-fg}`;\n }\n }\n return value;\n }\n\n renderMessage() {\n if (this.message) {\n if (this.type === 'success') {\n return `{green-fg}${this.message}{/green-fg} `;\n }\n if (this.type === 'error') {\n return `{red-fg}${this.message}{/red-fg} `;\n }\n if (this.type === 'warn') {\n return `{yellow-fg}${this.message}{/yellow-fg} `;\n }\n return `${this.message} `;\n }\n return '';\n }\n\n renderDate() {\n if (this.depth === 0) {\n return `{magenta-fg}(${dateFormat(this.date, 'dd/mm/yyyy HH:MM:ss.L')}){/magenta-fg} `;\n }\n return '';\n }\n\n renderDuration() {\n if (this.relativeDuration) {\n return `{grey-fg}+${this.relativeDuration}{/grey-fg} `;\n }\n return '';\n }\n\n getRelativeDuration() {\n return this.humanizeDuration(this.date - this.previousLog.date);\n }\n\n humanizeDuration(duration) {\n if (duration < 1000) {\n return `${duration}ms`;\n }\n if (duration < 60000) {\n let milliseconds = duration % 1000;\n milliseconds = ('000' + milliseconds).slice(-3);\n return `${Math.floor(duration / 1000)}.${milliseconds}s`;\n }\n return `${Math.floor(duration / 60000)}m ${Math.round((duration % 60000) / 1000)}s`;\n }\n\n addChild(log) {\n this.children.push(log);\n }\n\n getLastChild() {\n return this.children[this.children.length - 1];\n }\n\n getChildren(list) {\n list = list || [];\n list.push.apply(list, this.children);\n this.children.forEach(child => {\n child.getChildren(list);\n });\n return list;\n }\n\n setStatus(status) {\n this.status = status;\n this.update();\n }\n\n prune(value) {\n return prune(value, {\n depthDecr: 2,\n arrayMaxLength: 8,\n prunedString: ' [...]'\n });\n }\n }\n\n class Agent {\n constructor({name, type, status, data, message, isAsync = false, ancestors}) {\n this.name = name;\n this.children = {};\n this.isAsync = isAsync;\n this.asyncState = this.isAsync ? 'pending' : null;\n this.type = type;\n this.status = status;\n\n if (!ancestors) {\n this.ancestors = [];\n this.isRoot = true;\n } else {\n this.ancestors = ancestors;\n this.parent = this.ancestors[this.ancestors.length - 1];\n }\n\n this.logItem = new LogItem({\n name: this.name,\n type: this.type,\n status: this.status,\n parent: this.parent ? this.parent.logItem : null,\n data: data,\n message: message,\n stackTrace: this.generateStackTrace(stack_trace.get()),\n });\n\n return this;\n }\n\n log(...args) {\n new Agent({\n type: 'info',\n data: args,\n ancestors: this.ancestors.concat(this)\n });\n return this;\n }\n\n warn(...args) {\n new Agent({\n type: 'warn',\n data: args,\n ancestors: this.ancestors.concat(this)\n });\n return this;\n }\n\n success(...args) {\n new Agent({\n type: 'success',\n data: args,\n ancestors: this.ancestors.concat(this),\n });\n return this;\n }\n\n error(...args) {\n new Agent({\n type: 'error',\n data: args,\n ancestors: this.ancestors.concat(this),\n });\n return this;\n }\n\n child(name, ...args) {\n if (!this.children[name]) {\n this.children[name] = new Agent({\n name,\n type: 'node',\n ancestors: this.ancestors.concat(this),\n });\n }\n if (args.length) {\n this.children[name].log(...args);\n }\n return this.children[name];\n }\n\n async(name, ...args) {\n if (!this.children[name]) {\n this.children[name] = new Agent({\n name,\n type: 'async',\n status: 'pending',\n isAsync: true,\n ancestors: this.ancestors.concat(this),\n });\n }\n if (!this.children[name].isAsync) {\n this.internalWarn(`Child agent {bold}${name}{/bold} is defined as a non async agent`);\n }\n if (args.length) {\n this.children[name].log(...args);\n }\n return this.children[name];\n }\n\n resolve(...args) {\n if (this.isAsync) {\n if (this.logItem.status === 'pending') {\n this.logItem.setStatus('resolved');\n const resolveLog = new Agent({\n name: this.name,\n type: 'success',\n message: 'resolved',\n ancestors: this.ancestors.concat(this),\n });\n if (args.length) {\n resolveLog.success(...args);\n }\n } else {\n this.internalWarn(`Trying to resolve an already {bold}${this.logItem.status}{/bold} async agent`);\n }\n } else {\n this.internalWarn('Trying to resolve a non async agent');\n }\n return this;\n }\n\n reject(...args) {\n if (this.isAsync) {\n if (this.logItem.status === 'pending') {\n this.logItem.setStatus('rejected');\n const rejectLog = new Agent({\n name: this.name,\n type: 'error',\n message: 'rejected',\n ancestors: this.ancestors.concat(this),\n });\n if (args.length) {\n rejectLog.error(...args);\n }\n } else {\n this.internalWarn(`Trying to reject an already {bold}${this.logItem.status}{/bold} async agent`);\n }\n } else {\n this.internalWarn('Trying to reject a non async agent');\n }\n return this;\n }\n\n internalWarn(message) {\n new Agent({\n name: this.name,\n type: 'warn',\n message,\n ancestors: this.ancestors.concat(this),\n });\n }\n\n getAncestorsNames() {\n return this.ancestors.map(ancestor => ancestor.name);\n }\n\n generateStackTrace(trace) {\n const stackTrace = [];\n for (let i = 0; i < 5; i++) {\n stackTrace.push({\n type: trace[i].getTypeName(),\n function: trace[i].getFunctionName(),\n method: trace[i].getMethodName(),\n file: trace[i].getFileName(),\n line: trace[i].getLineNumber(),\n column: trace[i].getColumnNumber(),\n });\n }\n return stackTrace;\n }\n }\n\n var agent = function(name, ...args) {\n return new Agent({\n name,\n type: 'root',\n data: args.length ? args : undefined,\n });\n };\n\n transceiver.setPromise(null);\n\n const ui = new Ui();\n\n var investigator = {ui, agent};\n\n return investigator;\n\n}));\n","// https://github.com/yaronn/blessed-contrib/blob/master/lib/widget/tree.js\nimport blessed from 'blessed';\n\nconst Node = blessed.Node;\nconst Box = blessed.Box;\n\nfunction Tree(options) {\n\n if (!(this instanceof Node)) {\n return new Tree(options);\n }\n\n options = options || {};\n options.bold = true;\n var self = this;\n this.options = options;\n this.data = {};\n this.nodeLines = [];\n this.lineNbr = 0;\n Box.call(this, options);\n\n options.extended = options.extended || false;\n options.keys = options.keys || ['space','enter'];\n\n options.template = options.template || {};\n options.template.extend = options.template.extend || ' [+]';\n options.template.retract = options.template.retract || ' [-]';\n options.template.lines = options.template.lines || false;\n\n this.rows = blessed.list({\n height: 0,\n top: 1,\n width: 0,\n left: 1,\n selectedFg: 'black',\n selectedBg: 'white',\n keys: true,\n tags: true,\n });\n\n this.rows.key(options.keys,function() {\n self.nodeLines[this.getItemIndex(this.selected)].extended = !self.nodeLines[this.getItemIndex(this.selected)].extended;\n self.setData(self.data);\n self.screen.render();\n\n self.emit('select',self.nodeLines[this.getItemIndex(this.selected)]);\n });\n\n this.append(this.rows);\n}\n\nTree.prototype.walk = function(node, treeDepth) {\n var lines = [];\n\n if (!node.parent) {\n node.parent = null;\n }\n\n if (treeDepth == '' && node.name) {\n this.lineNbr = 0;\n this.nodeLines[this.lineNbr++] = node;\n lines.push(node.name);\n treeDepth = ' ';\n }\n\n node.depth = treeDepth.length - 1;\n\n if (node.children && node.extended) {\n\n var i = 0;\n\n if (typeof node.children == 'function') {\n node.childrenContent = node.children(node);\n }\n\n if (!node.childrenContent) {\n node.childrenContent = node.children;\n }\n\n for (var child in node.childrenContent) {\n\n if (!node.childrenContent[child].name) {\n node.childrenContent[child].name = child;\n }\n\n var childIndex = child;\n child = node.childrenContent[child];\n child.parent = node;\n child.position = i++;\n\n if (typeof child.extended == 'undefined') {\n child.extended = this.options.extended;\n }\n\n if (typeof child.children == 'function') {\n child.childrenContent = child.children(child);\n } else {\n child.childrenContent = child.children;\n }\n\n var isLastChild = child.position == Object.keys(child.parent.childrenContent).length - 1;\n var tree;\n var suffix = '';\n if (isLastChild) {\n tree = '└';\n } else {\n tree = '├';\n }\n if (!child.childrenContent || Object.keys(child.childrenContent).length == 0) {\n tree += '─';\n } else if (child.extended) {\n tree += '┬';\n suffix = this.options.template.retract;\n } else {\n tree += '─';\n suffix = this.options.template.extend;\n }\n\n if (!this.options.template.lines) {\n tree = '|-';\n }\n\n lines.push(treeDepth + tree + child.name + suffix);\n\n this.nodeLines[this.lineNbr++] = child;\n\n var parentTree;\n if (isLastChild || !this.options.template.lines) {\n parentTree = treeDepth + ' ';\n } else {\n parentTree = treeDepth + '│';\n }\n lines = lines.concat(this.walk(child, parentTree));\n }\n }\n return lines;\n};\n\nTree.prototype.focus = function() {\n this.rows.focus();\n};\n\nTree.prototype.render = function() {\n if (this.screen.focused == this.rows) {\n this.rows.focus();\n }\n\n this.rows.width = this.width - 3;\n this.rows.height = this.height - 3;\n Box.prototype.render.call(this);\n};\n\nTree.prototype.setData = function(data) {\n\n var formatted = [];\n formatted = this.walk(data,'');\n\n this.data = data;\n this.rows.setItems(formatted);\n};\n\nTree.prototype.__proto__ = Box.prototype;\n\nTree.prototype.type = 'tree';\n\nexport default Tree;\n","import blessed from 'blessed';\nimport transceiver from 'transceiver';\n\nexport default class LogsList {\n constructor() {\n this.selectedLog = null;\n this.logs = {};\n this.logsCount = 0;\n this.channel = transceiver.channel('log');\n this.autoScroll = true;\n this.element = blessed.list({\n top: '0',\n left: '0',\n bottom: 7,\n tags: true,\n keys: true,\n mouse: true,\n scrollbar: {\n bg: 'magenta',\n },\n style: {\n selected: {\n fg: 'black',\n bg: 'white',\n }\n }\n });\n\n this.element.key(['up', 'down', 's', 'b'], (ch, key) => {\n if (key.name === 's') {\n this.autoScroll = !this.autoScroll;\n } else if (key.name === 'b') {\n this.scrollToBottom();\n transceiver.request('ui', 'render');\n } else {\n this.autoScroll = false;\n }\n });\n\n this.element.on('select item', (element, i) => {\n this.selectedLog = this.getLogFromElement(element);\n if (this.selectedLog) {\n this.channel.emit('select log', this.selectedLog);\n }\n });\n\n this.channel.reply({\n addLog: this.addLog,\n getSelectedLog: this.getSelectedLog,\n }, this);\n }\n\n addLog(log) {\n let element;\n\n this.logs[log.id] = log;\n this.logsCount++;\n\n if (log.parent) {\n const index = this.element.getItemIndex(log.parent.element) + log.parent.getChildren().length;\n this.element.insertItem(index, log.render());\n element = this.element.getItem(index);\n } else {\n element = this.element.add(log.render());\n }\n element.logId = log.id;\n if (this.autoScroll) {\n this.scrollToBottom();\n }\n if (this.logsCount === 1) {\n this.channel.emit('select log', log);\n }\n return element;\n }\n\n getSelectedLog() {\n return this.selectedLog;\n }\n\n scrollToBottom() {\n this.element.move(this.logsCount);\n }\n\n getLogFromElement(element) {\n return this.logs[element.logId];\n }\n\n focus() {\n this.element.focus();\n }\n}\n","import blessed from 'blessed';\nimport transceiver from 'transceiver';\nimport dateFormat from 'dateformat';\n\nexport default class logDetails {\n constructor() {\n this.channel = transceiver.channel('log');\n this.element = blessed.box({\n height: 6,\n left: '0',\n bottom: 0,\n tags: true,\n keys: true,\n padding: {\n left: 1,\n right: 1,\n },\n style: {\n selected: {\n fg: 'black',\n bg: 'white',\n border: {\n fg: 'white'\n },\n hover: {\n bg: 'green'\n }\n }\n }\n });\n\n this.channel.on('select log', this.updateLogDetails.bind(this));\n }\n\n updateLogDetails(log) {\n this.element.setContent(this.renderType(log) + this.renderId(log) + this.renderDate(log) + this.renderDuration(log) + this.renderData(log));\n }\n\n renderType(log) {\n if (log.type === 'root') {\n return '{magenta-fg}{bold}ROOT NODE{/bold}{/magenta-fg}\\n';\n }\n if (log.type === 'success') {\n return '{green-fg}✔ {bold}SUCCESS{/bold}{/green-fg}\\n';\n }\n if (log.type === 'error') {\n return '{red-fg}✘ {bold}ERROR{/bold}{/red-fg}\\n';\n }\n if (log.type === 'warn') {\n return '{yellow-fg}! {bold}WARN{/bold}{/red-fg}\\n';\n }\n if (log.type === 'node') {\n return '{grey-fg}{bold}NODE{/bold}{/grey-fg}\\n';\n }\n if (log.type === 'async') {\n if (log.status === 'resolved') {\n return '{bold}{green-fg}ASYNC NODE{/bold} (RESOLVED ✔){/green-fg}\\n';\n }\n if (log.status === 'rejected') {\n return '{bold}{red-fg}ASYNC NODE{/bold} (REJECTED ✘){/red-fg}\\n';\n }\n if (log.status === 'pending') {\n return '{cyan-fg}{bold}ASYNC NODE{/bold} (PENDING ⌛){/cyan-fg}\\n';\n }\n }\n if (log.type === 'info') {\n return '{white-fg}{bold}INFO{/bold}{/white-fg}\\n';\n }\n return '';\n }\n\n renderId(log) {\n return `{bold}ID:{/bold} {underline}${log.id}{/underline}\\n`;\n }\n\n renderDate(log) {\n return `{bold}TIME:{/bold} {magenta-fg}${dateFormat(log.date, 'dddd, mmmm dS yyyy, HH:MM:ss.L')}{/magenta-fg}\\n`;\n }\n\n renderDuration(log) {\n if (log.relativeDuration && log.previousLog) {\n return `{bold}DURATION:{/bold} {yellow-fg}${log.relativeDuration}{/yellow-fg} (from {underline}${log.previousLog.id}{/underline})\\n`;\n }\n return '';\n }\n\n renderData(log) {\n if (log.data) {\n return `{bold}DATA:{/bold} ${log.renderData()}\\n`;\n }\n return '';\n }\n}\n","import blessed from 'blessed';\nimport transceiver from 'transceiver';\nimport prune from 'json-prune';\nimport path from 'path';\nimport appRoot from 'app-root-path';\n\nimport tree from './tree';\n\nexport default class Inspector {\n constructor() {\n this.channel = transceiver.channel('log');\n\n this.element = tree({\n top: 'center',\n left: 'center',\n width: '90%',\n height: '75%',\n hidden: true,\n label: 'Inspector',\n tags: true,\n border: {\n type: 'line'\n },\n style: {\n fg: 'white',\n border: {\n fg: '#f0f0f0'\n },\n },\n template: {\n extend: '{bold}{green-fg} [+]{/}',\n retract: '{bold}{yellow-fg} [-]{/}',\n lines: true,\n }\n });\n }\n\n open(selectedLog) {\n if (!selectedLog || !selectedLog.data && !selectedLog.stackTrace) {\n return;\n }\n this.opened = true;\n this.element.show();\n this.element.focus();\n this.element.setData(this.prepareData(selectedLog));\n }\n\n close() {\n this.opened = false;\n this.element.hide();\n }\n\n prepareData(log) {\n const content = {};\n if (log.data) {\n content.data = JSON.parse(prune(log.data, {\n depthDecr: 7,\n replacer: (value, defaultValue, circular) => {\n if (typeof value === 'function') {\n return '\"Function [pruned]\"';\n }\n if (Array.isArray(value)) {\n return `\"Array (${value.length}) [pruned]\"`;\n }\n if (typeof value === 'object') {\n return '\"Object [pruned]\"';\n }\n return defaultValue;\n }\n }));\n }\n\n if (log.stackTrace) {\n content['stack trace'] = log.stackTrace.map((callsite) => {\n const relativePath = path.relative(appRoot.toString(), callsite.file);\n return {\n type: callsite.type,\n function: callsite.function,\n method: callsite.method,\n file: `${relativePath}:{yellow-fg}${callsite.line}{/yellow-fg}:{yellow-fg}${callsite.column}{/yellow-fg}`,\n };\n });\n }\n return this.formatData(content);\n }\n\n formatData(data, key, depth = 0) {\n depth++;\n if (typeof data === 'object') {\n if (data !== null) {\n let name;\n let extended;\n\n if (depth === 2) {\n name = `{yellow-fg}{bold}${key.toUpperCase()}{/bold}{/yellow-fg} {magenta-fg}(${data.length}){/magenta-fg}`;\n extended = key === 'data';\n } else {\n const type = (Array.isArray(data) ? `[Array] {magenta-fg}(${data.length}){/magenta-fg}` : '[Object]');\n name = `{blue-fg}{bold}${key ? key + ' ' : ''}{/bold}${type}{/blue-fg}`;\n extended = depth < 4;\n }\n const newObj = {\n children: {},\n name,\n extended\n };\n Object.keys(data).forEach((key) => {\n const child = this.formatData(data[key], key, depth);\n if (child) {\n newObj.children[key] = child;\n }\n });\n return newObj;\n }\n }\n if (typeof data === 'function') {\n return {\n name: `{blue-fg}${key}{/blue-fg}: {red-fg}{bold}[Function]{/}`,\n };\n }\n if (typeof data === 'number') {\n return {\n name: `{blue-fg}${key}{/blue-fg}: {yellow-fg}${data}{/}`,\n };\n }\n if (data === null) {\n return {\n name: `{blue-fg}${key}{/blue-fg}: {cyan-fg}{bold}null{/}`,\n };\n }\n return {\n name: `{blue-fg}${key}{/blue-fg}: ${data}`,\n };\n }\n}\n","import blessed from 'blessed';\nimport transceiver from 'transceiver';\n\nimport LogsList from './logsList';\nimport LogDetails from './logDetails';\nimport Inspector from './inspector';\n\nexport default class Ui {\n constructor() {\n this.channel = transceiver.channel('ui');\n this.screen = blessed.screen({\n smartCSR: true\n });\n\n this.logsList = new LogsList();\n this.logDetails = new LogDetails();\n this.inspector = new Inspector();\n\n this.separator = blessed.line({\n bottom: 6,\n orientation: 'horizontal'\n });\n\n this.screen.append(this.logsList.element);\n this.screen.append(this.logDetails.element);\n this.screen.append(this.separator);\n this.screen.append(this.inspector.element);\n\n this.logsList.element.focus();\n\n this.screen.key(['q', 'C-c'], function(ch, key) {\n return process.exit(0);\n });\n\n this.screen.key(['i'], this.toggleInspector.bind(this));\n\n this.screen.render();\n\n this.channel.reply('render', () => this.screen.render());\n }\n\n toggleInspector() {\n if (this.inspector.opened) {\n this.inspector.close();\n this.logsList.focus();\n } else {\n this.inspector.open(this.logsList.selectedLog);\n }\n this.screen.render();\n }\n}\n","import blessed from 'blessed';\nimport shortid from 'shortid';\nimport transceiver from 'transceiver';\nimport prune from 'json-prune';\nimport dateFormat from 'dateformat';\n\nexport default class LogItem {\n constructor({name, type, status, parent, data, message, stackTrace, date = Date.now()}) {\n this.id = shortid.generate();\n this.name = name;\n this.type = type;\n this.status = status;\n this.data = data;\n this.message = message;\n this.stackTrace = stackTrace;\n this.date = date;\n this.children = [];\n this.channel = transceiver.channel('log');\n\n if (parent) {\n this.depth = parent.depth + 1;\n this.parent = parent;\n this.previousLog = parent.getLastChild() || parent;\n this.relativeDuration = this.getRelativeDuration();\n this.parent.addChild(this);\n } else {\n this.depth = 0;\n }\n this.element = this.channel.request('addLog', this);\n this.update();\n }\n\n update() {\n if (this.element) {\n this.element.content = this.render();\n transceiver.request('ui', 'render');\n }\n }\n\n render() {\n let message = `${this.renderState()}${this.renderName()}${this.renderMessage()}${this.renderData()}${this.renderDate()}${this.renderDuration()}`;\n for (let i = 0; i < this.depth; i++) {\n message = ' ' + message;\n }\n return message;\n }\n\n renderState() {\n if (this.type === 'async' && this.status === 'pending') {\n return `{cyan-fg}[⌛]{/cyan-fg} `;\n }\n if (this.type === 'async' && this.status === 'resolved') {\n return `{green-fg}[✔]{/green-fg} `;\n }\n if (this.type === 'async' && this.status === 'rejected') {\n return `{red-fg}[✘]{/red-fg} `;\n }\n if (this.type === 'success') {\n return `{green-fg}✔{/green-fg} `;\n }\n if (this.type === 'error') {\n return `{red-fg}✘{/red-fg} `;\n }\n if (this.type === 'warn') {\n return `{yellow-fg}❗{/yellow-fg} `;\n }\n if (this.type === 'info') {\n return '⇢ ';\n }\n return '';\n }\n\n renderName() {\n if (this.depth === 0) {\n return this.name ? `{underline}{bold}${this.name}{/bold}{/underline} ` : '';\n }\n if (this.type === 'async') {\n if (this.status === 'resolved') {\n return `{bold}{green-fg}${this.name}{/green-fg}{/bold} (async) `;\n }\n if (this.status === 'rejected') {\n return `{bold}{red-fg}${this.name}{/red-fg}{/bold} (async) `;\n }\n return `{bold}${this.name}{/bold} (async) `;\n }\n if (this.type === 'success') {\n return this.name ? `{bold}{green-fg}${this.name}{/green-fg}{/bold} ` : '';\n }\n if (this.type === 'error') {\n return this.name ? `{bold}{red-fg}${this.name}{/red-fg}{/bold} ` : '';\n }\n if (this.type === 'warn') {\n return this.name ? `{bold}{yellow-fg}${this.name}{/yellow-fg}{/bold} ` : '';\n }\n return this.name ? `{bold}${this.name}{/bold} ` : '';\n }\n\n renderData() {\n if (this.depth === 0) {\n // console.log(this.data);\n }\n if (!this.data) {\n return '';\n }\n if (Array.isArray(this.data)) {\n return this.data.map(this.renderValue.bind(this)).join(' ') + ' ';\n }\n return this.renderValue(this.data) + ' ';\n }\n\n renderValue(value) {\n if (Array.isArray(value)) {\n return `{cyan-fg}${this.prune(value)}{/cyan-fg}`;\n }\n if (typeof value === 'object') {\n return `{blue-fg}${this.prune(value)}{/blue-fg}`;\n }\n if (typeof value === 'function') {\n return `{red-fg}{bold}[Function]{/bold}{red-fg}`;\n }\n if (typeof value === 'number') {\n return `{yellow-fg}${value}{/yellow-fg}`;\n }\n if (typeof value === 'string') {\n if (this.type === 'success') {\n return `{green-fg}${value}{/green-fg}`;\n }\n if (this.type === 'error') {\n return `{red-fg}${value}{/red-fg}`;\n }\n if (this.type === 'warn') {\n return `{yellow-fg}${value}{/yellow-fg}`;\n }\n }\n return value;\n }\n\n renderMessage() {\n if (this.message) {\n if (this.type === 'success') {\n return `{green-fg}${this.message}{/green-fg} `;\n }\n if (this.type === 'error') {\n return `{red-fg}${this.message}{/red-fg} `;\n }\n if (this.type === 'warn') {\n return `{yellow-fg}${this.message}{/yellow-fg} `;\n }\n return `${this.message} `;\n }\n return '';\n }\n\n renderDate() {\n if (this.depth === 0) {\n return `{magenta-fg}(${dateFormat(this.date, 'dd/mm/yyyy HH:MM:ss.L')}){/magenta-fg} `;\n }\n return '';\n }\n\n renderDuration() {\n if (this.relativeDuration) {\n return `{grey-fg}+${this.relativeDuration}{/grey-fg} `;\n }\n return '';\n }\n\n getRelativeDuration() {\n return this.humanizeDuration(this.date - this.previousLog.date);\n }\n\n humanizeDuration(duration) {\n if (duration < 1000) {\n return `${duration}ms`;\n }\n if (duration < 60000) {\n let milliseconds = duration % 1000;\n milliseconds = ('000' + milliseconds).slice(-3);\n return `${Math.floor(duration / 1000)}.${milliseconds}s`;\n }\n return `${Math.floor(duration / 60000)}m ${Math.round((duration % 60000) / 1000)}s`;\n }\n\n addChild(log) {\n this.children.push(log);\n }\n\n getLastChild() {\n return this.children[this.children.length - 1];\n }\n\n getChildren(list) {\n list = list || [];\n list.push.apply(list, this.children);\n this.children.forEach(child => {\n child.getChildren(list);\n });\n return list;\n }\n\n setStatus(status) {\n this.status = status;\n this.update();\n }\n\n prune(value) {\n return prune(value, {\n depthDecr: 2,\n arrayMaxLength: 8,\n prunedString: ' [...]'\n });\n }\n}\n","import shortid from 'shortid';\nimport transceiver from 'transceiver';\nimport stackTrace from 'stack-trace';\n\nimport LogItem from './ui/logItem';\n\nclass Agent {\n constructor({name, type, status, data, message, isAsync = false, ancestors}) {\n this.name = name;\n this.children = {};\n this.isAsync = isAsync;\n this.asyncState = this.isAsync ? 'pending' : null;\n this.type = type;\n this.status = status;\n\n if (!ancestors) {\n this.ancestors = [];\n this.isRoot = true;\n } else {\n this.ancestors = ancestors;\n this.parent = this.ancestors[this.ancestors.length - 1];\n }\n\n this.logItem = new LogItem({\n name: this.name,\n type: this.type,\n status: this.status,\n parent: this.parent ? this.parent.logItem : null,\n data: data,\n message: message,\n stackTrace: this.generateStackTrace(stackTrace.get()),\n });\n\n return this;\n }\n\n log(...args) {\n new Agent({\n type: 'info',\n data: args,\n ancestors: this.ancestors.concat(this)\n });\n return this;\n }\n\n warn(...args) {\n new Agent({\n type: 'warn',\n data: args,\n ancestors: this.ancestors.concat(this)\n });\n return this;\n }\n\n success(...args) {\n new Agent({\n type: 'success',\n data: args,\n ancestors: this.ancestors.concat(this),\n });\n return this;\n }\n\n error(...args) {\n new Agent({\n type: 'error',\n data: args,\n ancestors: this.ancestors.concat(this),\n });\n return this;\n }\n\n child(name, ...args) {\n if (!this.children[name]) {\n this.children[name] = new Agent({\n name,\n type: 'node',\n ancestors: this.ancestors.concat(this),\n });\n }\n if (args.length) {\n this.children[name].log(...args);\n }\n return this.children[name];\n }\n\n async(name, ...args) {\n if (!this.children[name]) {\n this.children[name] = new Agent({\n name,\n type: 'async',\n status: 'pending',\n isAsync: true,\n ancestors: this.ancestors.concat(this),\n });\n }\n if (!this.children[name].isAsync) {\n this.internalWarn(`Child agent {bold}${name}{/bold} is defined as a non async agent`);\n }\n if (args.length) {\n this.children[name].log(...args);\n }\n return this.children[name];\n }\n\n resolve(...args) {\n if (this.isAsync) {\n if (this.logItem.status === 'pending') {\n this.logItem.setStatus('resolved');\n const resolveLog = new Agent({\n name: this.name,\n type: 'success',\n message: 'resolved',\n ancestors: this.ancestors.concat(this),\n });\n if (args.length) {\n resolveLog.success(...args);\n }\n } else {\n this.internalWarn(`Trying to resolve an already {bold}${this.logItem.status}{/bold} async agent`);\n }\n } else {\n this.internalWarn('Trying to resolve a non async agent');\n }\n return this;\n }\n\n reject(...args) {\n if (this.isAsync) {\n if (this.logItem.status === 'pending') {\n this.logItem.setStatus('rejected');\n const rejectLog = new Agent({\n name: this.name,\n type: 'error',\n message: 'rejected',\n ancestors: this.ancestors.concat(this),\n });\n if (args.length) {\n rejectLog.error(...args);\n }\n } else {\n this.internalWarn(`Trying to reject an already {bold}${this.logItem.status}{/bold} async agent`);\n }\n } else {\n this.internalWarn('Trying to reject a non async agent');\n }\n return this;\n }\n\n internalWarn(message) {\n new Agent({\n name: this.name,\n type: 'warn',\n message,\n ancestors: this.ancestors.concat(this),\n });\n }\n\n getAncestorsNames() {\n return this.ancestors.map(ancestor => ancestor.name);\n }\n\n generateStackTrace(trace) {\n const stackTrace = [];\n for (let i = 0; i < 5; i++) {\n stackTrace.push({\n type: trace[i].getTypeName(),\n function: trace[i].getFunctionName(),\n method: trace[i].getMethodName(),\n file: trace[i].getFileName(),\n line: trace[i].getLineNumber(),\n column: trace[i].getColumnNumber(),\n });\n }\n return stackTrace;\n }\n}\n\nexport default function(name, ...args) {\n return new Agent({\n name,\n type: 'root',\n data: args.length ? args : undefined,\n });\n};\n","import transceiver from 'transceiver';\n\nimport Ui from './ui';\nimport agent from './agent';\n\ntransceiver.setPromise(null);\n\nconst ui = new Ui();\n\nexport default {ui, agent};\n"],"sourceRoot":"/source/"} -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | import investigator from '../src/investigator'; 2 | 3 | let i = 1; 4 | setTimeout(runScraping, 2000); 5 | 6 | function runScraping() { 7 | if (i < 5) { 8 | i++; 9 | scrapPage('http://example.com'); 10 | setTimeout(() => { 11 | runScraping(); 12 | }, Math.random() * 15000); 13 | } 14 | } 15 | 16 | function scrapPage(url) { 17 | const agent = investigator.agent('scraping page', url) 18 | .async('getData'); 19 | 20 | agent.async('downloadPage', url); 21 | downloadPage(url).then((res) => { 22 | agent.child('downloadPage') 23 | .log('status', res.status) 24 | .resolve(res.size); 25 | 26 | const info = getPageInfo(res); 27 | agent.child('getPageInfo') 28 | .log('id:', info.id) 29 | .log('title:', info.title) 30 | .log('metas:', info.metas); 31 | 32 | return Promise.all([ 33 | getVideos(res.data.videos, agent), 34 | getPostsData(res.data.posts, agent), 35 | ]); 36 | }).then(() => { 37 | agent.resolve('Well done !'); 38 | }); 39 | } 40 | 41 | function getPostsData(posts, agent) { 42 | agent.async('getPosts'); 43 | return Promise.all( 44 | posts.map((post) => { 45 | const postAgent = agent.child('getPosts') 46 | .child(`post #${post.id}`) 47 | .log('Retrieving post data...'); 48 | return Promise.all([ 49 | downloadPostImage(post).then((img) => { 50 | postAgent.success('Image downloaded - status:', img.status); 51 | return img; 52 | }).catch((err) => postAgent.error(err)), 53 | getPostComments(post).then((comments) => { 54 | if (comments.length) { 55 | postAgent.success(`Comments retrieved (${comments.length})`, comments); 56 | } else { 57 | postAgent.warn(`No comment found`); 58 | } 59 | return comments; 60 | }), 61 | ]); 62 | })) 63 | .then((posts) => { 64 | agent.child('getPosts').resolve(`${posts.length} posts retrieved`); 65 | }); 66 | } 67 | 68 | function getVideos(videos, agent) { 69 | agent.async('getVideos') 70 | .log('Downloading', videos.length, 'videos'); 71 | 72 | return Promise.all( 73 | videos.map((video) => { 74 | return downloadVideo(video) 75 | .then((data) => { 76 | agent.child('getVideos') 77 | .success('video', video.id, 'downloaded'); 78 | }); 79 | })) 80 | .then((videos) => { 81 | agent.child('getVideos').resolve(`${videos.length} videos retrieved`) 82 | .log(videos); 83 | }) 84 | .catch((err) => { 85 | agent.child('getVideos').reject(err); 86 | }); 87 | } 88 | 89 | function downloadPage(url) { 90 | return new Promise((resolve) => setTimeout(resolve, Math.random() * 10000, { 91 | status: 200, 92 | size: `${Math.round(Math.random() * 30)}kb`, 93 | data: { 94 | posts: [{ 95 | id: Math.round(Math.random() * 1000), 96 | img: null 97 | }, { 98 | id: Math.round(Math.random() * 1000), 99 | img: 'img2.png' 100 | }, { 101 | id: Math.round(Math.random() * 1000), 102 | img: 'img3.png' 103 | }], 104 | videos: [{ 105 | id: Math.round(Math.random() * 1000), 106 | url: 'video1.mp4' 107 | }, { 108 | id: Math.round(Math.random() * 1000), 109 | url: 'video2.mp4' 110 | }, { 111 | id: Math.round(Math.random() * 1000), 112 | url: null 113 | },{ 114 | id: Math.round(Math.random() * 1000), 115 | url: null 116 | }], 117 | }, 118 | })); 119 | } 120 | 121 | function getPageInfo(page) { 122 | return { 123 | id: Math.round(Math.random() * 100), 124 | title: 'Lorem ipsum', 125 | metas: fakeMetas, 126 | }; 127 | } 128 | 129 | function downloadPostImage(post) { 130 | return new Promise((resolve, reject) => post.img ? setTimeout(resolve, Math.random() * 6000, {status: 200}) : 131 | setTimeout(reject, Math.random() * 10000, 'Error downloading image')); 132 | } 133 | 134 | function getPostComments(post) { 135 | return new Promise((resolve, reject) => setTimeout(resolve, Math.random() * 6000, 136 | ['Lorem', 'Ipsum'].slice(0, Math.round(Math.random() * 2)) 137 | )); 138 | } 139 | 140 | function downloadVideo(video) { 141 | return new Promise((resolve, reject) => video.url ? setTimeout(resolve, Math.random() * 6000, {status: 200, size: Math.round(Math.random() * 10, 1)}) : 142 | setTimeout(reject, Math.random() * 12000, 'Error downloading video')); 143 | } 144 | 145 | const fakeMetas = { 146 | facebook: { 147 | 'og:url': 'https://github.com', 148 | 'og:site_name': 'GitHub', 149 | 'og:title': 'Build software better, together', 150 | '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.', 151 | 'og:image': 'https://assets-cdn.github.com/images/modules/open_graph/github-logo.png', 152 | 'og:image:type': 'image/png', 153 | 'og:image:width': '1200', 154 | 'og:image:height': '1200', 155 | 'og:image': 'https://assets-cdn.github.com/images/modules/open_graph/github-mark.png', 156 | 'og:image:type': 'image/png', 157 | 'og:image:width': '1200', 158 | 'og:image:height': '620', 159 | 'og:image': 'https://assets-cdn.github.com/images/modules/open_graph/github-octocat.png', 160 | 'og:image:type': 'image/png', 161 | 'og:image:width': '1200', 162 | 'og:image:height': '620', 163 | }, 164 | twitter: { 165 | 'twitter:site': 'github', 166 | 'twitter:site:id': '13334762', 167 | 'twitter:creator': 'github', 168 | 'twitter:creator:id': '13334762', 169 | 'twitter:card': 'summary_large_image', 170 | 'twitter:title': 'GitHub', 171 | '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.', 172 | 'twitter:image:src': 'https://assets-cdn.github.com/images/modules/open_graph/github-logo.png', 173 | 'twitter:image:width': '1200', 174 | 'twitter:image:height': '1200' 175 | }, 176 | length: 26, 177 | }; 178 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('./example.js'); 3 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Load Gulp and all of our Gulp plugins 2 | const gulp = require('gulp'); 3 | const $ = require('gulp-load-plugins')(); 4 | 5 | // Load other npm modules 6 | const del = require('del'); 7 | const glob = require('glob'); 8 | const path = require('path'); 9 | const isparta = require('isparta'); 10 | const babelify = require('babelify'); 11 | const watchify = require('watchify'); 12 | const buffer = require('vinyl-buffer'); 13 | const esperanto = require('esperanto'); 14 | const browserify = require('browserify'); 15 | const runSequence = require('run-sequence'); 16 | const source = require('vinyl-source-stream'); 17 | 18 | // Gather the library data from `package.json` 19 | const manifest = require('./package.json'); 20 | const config = manifest.babelBoilerplateOptions; 21 | const mainFile = manifest.main; 22 | const destinationFolder = path.dirname(mainFile); 23 | const exportFileName = path.basename(mainFile, path.extname(mainFile)); 24 | 25 | // Remove the built files 26 | gulp.task('clean', function(cb) { 27 | del([destinationFolder], cb); 28 | }); 29 | 30 | // Remove our temporary files 31 | gulp.task('clean-tmp', function(cb) { 32 | del(['tmp'], cb); 33 | }); 34 | 35 | // Send a notification when JSCS fails, 36 | // so that you know your changes didn't build 37 | function jscsNotify(file) { 38 | if (!file.jscs) { return; } 39 | return file.jscs.success ? false : 'JSCS failed'; 40 | } 41 | 42 | function createLintTask(taskName, files) { 43 | gulp.task(taskName, function() { 44 | return gulp.src(files) 45 | .pipe($.plumber()) 46 | .pipe($.eslint()) 47 | .pipe($.eslint.format()) 48 | .pipe($.eslint.failOnError()) 49 | .pipe($.jscs()) 50 | .pipe($.notify(jscsNotify)); 51 | }); 52 | } 53 | 54 | // Lint our source code 55 | createLintTask('lint-src', ['src/**/*.js']); 56 | 57 | // Lint our test code 58 | createLintTask('lint-test', ['test/**/*.js']); 59 | 60 | // Build two versions of the library 61 | gulp.task('build', ['lint-src', 'clean'], function(done) { 62 | esperanto.bundle({ 63 | base: 'src', 64 | entry: config.entryFileName, 65 | }).then(function(bundle) { 66 | var res = bundle.toUmd({ 67 | // Don't worry about the fact that the source map is inlined at this step. 68 | // `gulp-sourcemaps`, which comes next, will externalize them. 69 | sourceMap: 'inline', 70 | name: config.mainVarName 71 | }); 72 | 73 | $.file(exportFileName + '.js', res.code, { src: true }) 74 | .pipe($.plumber()) 75 | .pipe($.sourcemaps.init({ loadMaps: true })) 76 | .pipe($.babel()) 77 | .pipe($.sourcemaps.write('./')) 78 | .pipe(gulp.dest(destinationFolder)) 79 | .pipe($.filter(['*', '!**/*.js.map'])) 80 | .pipe($.rename(exportFileName + '.min.js')) 81 | .pipe($.sourcemaps.init({ loadMaps: true })) 82 | .pipe($.uglify()) 83 | .pipe($.sourcemaps.write('./')) 84 | .pipe(gulp.dest(destinationFolder)) 85 | .on('end', done); 86 | }) 87 | .catch(done); 88 | }); 89 | 90 | function bundle(bundler) { 91 | return bundler.bundle() 92 | .on('error', function(err) { 93 | console.log(err.message); 94 | this.emit('end'); 95 | }) 96 | .pipe($.plumber()) 97 | .pipe(source('./tmp/__spec-build.js')) 98 | .pipe(buffer()) 99 | .pipe(gulp.dest('')) 100 | .pipe($.livereload()); 101 | } 102 | 103 | function getBundler() { 104 | // Our browserify bundle is made up of our unit tests, which 105 | // should individually load up pieces of our application. 106 | // We also include the browserify setup file. 107 | var testFiles = glob.sync('./test/unit/**/*'); 108 | var allFiles = ['./test/setup/browserify.js'].concat(testFiles); 109 | 110 | // Create our bundler, passing in the arguments required for watchify 111 | var bundler = browserify(allFiles, watchify.args); 112 | 113 | // Watch the bundler, and re-bundle it whenever files change 114 | bundler = watchify(bundler); 115 | bundler.on('update', function() { 116 | bundle(bundler); 117 | }); 118 | 119 | // Set up Babelify so that ES6 works in the tests 120 | bundler.transform(babelify.configure({ 121 | sourceMapRelative: __dirname + '/src' 122 | })); 123 | 124 | return bundler; 125 | }; 126 | 127 | // Build the unit test suite for running tests 128 | // in the browser 129 | gulp.task('browserify', function() { 130 | return bundle(getBundler()); 131 | }); 132 | 133 | function test() { 134 | return gulp.src(['test/setup/node.js', 'test/unit/**/*.js'], {read: false}) 135 | .pipe($.mocha({reporter: 'dot', globals: config.mochaGlobals})); 136 | } 137 | 138 | gulp.task('coverage', ['lint-src', 'lint-test'], function(done) { 139 | require('babel-core/register'); 140 | gulp.src(['src/**/*.js']) 141 | .pipe($.istanbul({instrumenter: isparta.Instrumenter})) 142 | .pipe($.istanbul.hookRequire()) 143 | .on('finish', function() { 144 | return test() 145 | .pipe($.istanbul.writeReports()) 146 | .on('end', done); 147 | }); 148 | }); 149 | 150 | // Lint and run our tests 151 | gulp.task('test', ['lint-src', 'lint-test'], function() { 152 | require('babel-core/register'); 153 | return test(); 154 | }); 155 | 156 | // Ensure that linting occurs before browserify runs. This prevents 157 | // the build from breaking due to poorly formatted code. 158 | gulp.task('build-in-sequence', function(callback) { 159 | runSequence(['lint-src', 'lint-test'], 'browserify', callback); 160 | }); 161 | 162 | // These are JS files that should be watched by Gulp. When running tests in the browser, 163 | // watchify is used instead, so these aren't included. 164 | const jsWatchFiles = ['src/**/*', 'test/**/*']; 165 | // These are files other than JS files which are to be watched. They are always watched. 166 | const otherWatchFiles = ['package.json', '**/.eslintrc', '.jscsrc']; 167 | 168 | // Run the headless unit tests as you make changes. 169 | gulp.task('watch', function() { 170 | const watchFiles = jsWatchFiles.concat(otherWatchFiles); 171 | gulp.watch(watchFiles, ['test']); 172 | }); 173 | 174 | // Set up a livereload environment for our spec runner 175 | gulp.task('test-browser', ['build-in-sequence'], function() { 176 | $.livereload.listen({port: 35729, host: 'localhost', start: true}); 177 | return gulp.watch(otherWatchFiles, ['build-in-sequence']); 178 | }); 179 | 180 | // An alias of test 181 | gulp.task('default', ['test']); 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "investigator", 3 | "version": "0.1.1", 4 | "description": "Interactive and asynchronous logging tool for Node.js. An easier way to log & debug complex requests directly from the command line. Still experimental !", 5 | "main": "dist/investigator.js", 6 | "scripts": { 7 | "test": "gulp", 8 | "test-browser": "gulp test-browser", 9 | "build": "gulp build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/risq/investigator.git" 14 | }, 15 | "keywords": [ 16 | "log", 17 | "logger", 18 | "logging", 19 | "debug", 20 | "debugger", 21 | "cli", 22 | "inspect", 23 | "inspecter" 24 | ], 25 | "author": "risq ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/risq/investigator/issues" 29 | }, 30 | "homepage": "https://github.com/risq/investigator", 31 | "devDependencies": { 32 | "babel-core": "^5.2.17", 33 | "babel-eslint": "^4.0.5", 34 | "babelify": "^6.0.0", 35 | "browserify": "^11.0.1", 36 | "chai": "^3.2.0", 37 | "del": "^1.1.1", 38 | "esperanto": "^0.7.4", 39 | "glob": "^5.0.14", 40 | "gulp": "^3.8.10", 41 | "gulp-babel": "^5.0.0", 42 | "gulp-eslint": "^1.0.0", 43 | "gulp-file": "^0.2.0", 44 | "gulp-filter": "^3.0.0", 45 | "gulp-istanbul": "^0.10.0", 46 | "gulp-jscs": "^2.0.0", 47 | "gulp-livereload": "^3.4.0", 48 | "gulp-load-plugins": "^0.10.0", 49 | "gulp-mocha": "^2.0.0", 50 | "gulp-notify": "^2.1.0", 51 | "gulp-plumber": "^1.0.1", 52 | "gulp-rename": "^1.2.0", 53 | "gulp-sourcemaps": "^1.3.0", 54 | "gulp-uglify": "^1.2.0", 55 | "isparta": "^3.0.3", 56 | "mocha": "^2.1.0", 57 | "run-sequence": "^1.0.2", 58 | "sinon": "^1.12.2", 59 | "sinon-chai": "^2.7.0", 60 | "vinyl-buffer": "^1.0.0", 61 | "vinyl-source-stream": "^1.0.0", 62 | "watchify": "^3.3.1" 63 | }, 64 | "babelBoilerplateOptions": { 65 | "entryFileName": "investigator", 66 | "mainVarName": "investigator", 67 | "mochaGlobals": [ 68 | "stub", 69 | "spy", 70 | "expect" 71 | ] 72 | }, 73 | "dependencies": { 74 | "app-root-path": "^1.0.0", 75 | "blessed": "^0.1.81", 76 | "dateformat": "^1.0.11", 77 | "json-prune": "^1.0.1", 78 | "shortid": "^2.2.4", 79 | "stack-trace": "0.0.9", 80 | "transceiver": "^0.3.4" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/agent.js: -------------------------------------------------------------------------------- 1 | import shortid from 'shortid'; 2 | import transceiver from 'transceiver'; 3 | import stackTrace from 'stack-trace'; 4 | 5 | import LogItem from './ui/logItem'; 6 | 7 | class Agent { 8 | constructor({name, type, status, data, message, isAsync = false, ancestors}) { 9 | this.name = name; 10 | this.children = {}; 11 | this.isAsync = isAsync; 12 | this.asyncState = this.isAsync ? 'pending' : null; 13 | this.type = type; 14 | this.status = status; 15 | 16 | if (!ancestors) { 17 | this.ancestors = []; 18 | this.isRoot = true; 19 | } else { 20 | this.ancestors = ancestors; 21 | this.parent = this.ancestors[this.ancestors.length - 1]; 22 | } 23 | 24 | this.logItem = new LogItem({ 25 | name: this.name, 26 | type: this.type, 27 | status: this.status, 28 | parent: this.parent ? this.parent.logItem : null, 29 | data: data, 30 | message: message, 31 | stackTrace: this.generateStackTrace(stackTrace.get()), 32 | }); 33 | 34 | return this; 35 | } 36 | 37 | log(...args) { 38 | new Agent({ 39 | type: 'info', 40 | data: args, 41 | ancestors: this.ancestors.concat(this) 42 | }); 43 | return this; 44 | } 45 | 46 | warn(...args) { 47 | new Agent({ 48 | type: 'warn', 49 | data: args, 50 | ancestors: this.ancestors.concat(this) 51 | }); 52 | return this; 53 | } 54 | 55 | success(...args) { 56 | new Agent({ 57 | type: 'success', 58 | data: args, 59 | ancestors: this.ancestors.concat(this), 60 | }); 61 | return this; 62 | } 63 | 64 | error(...args) { 65 | new Agent({ 66 | type: 'error', 67 | data: args, 68 | ancestors: this.ancestors.concat(this), 69 | }); 70 | return this; 71 | } 72 | 73 | child(name, ...args) { 74 | if (!this.children[name]) { 75 | this.children[name] = new Agent({ 76 | name, 77 | type: 'node', 78 | ancestors: this.ancestors.concat(this), 79 | }); 80 | } 81 | if (args.length) { 82 | this.children[name].log(...args); 83 | } 84 | return this.children[name]; 85 | } 86 | 87 | async(name, ...args) { 88 | if (!this.children[name]) { 89 | this.children[name] = new Agent({ 90 | name, 91 | type: 'async', 92 | status: 'pending', 93 | isAsync: true, 94 | ancestors: this.ancestors.concat(this), 95 | }); 96 | } 97 | if (!this.children[name].isAsync) { 98 | this.internalWarn(`Child agent {bold}${name}{/bold} is defined as a non async agent`); 99 | } 100 | if (args.length) { 101 | this.children[name].log(...args); 102 | } 103 | return this.children[name]; 104 | } 105 | 106 | resolve(...args) { 107 | if (this.isAsync) { 108 | if (this.logItem.status === 'pending') { 109 | this.logItem.setStatus('resolved'); 110 | const resolveLog = new Agent({ 111 | name: this.name, 112 | type: 'success', 113 | message: 'resolved', 114 | ancestors: this.ancestors.concat(this), 115 | }); 116 | if (args.length) { 117 | resolveLog.success(...args); 118 | } 119 | } else { 120 | this.internalWarn(`Trying to resolve an already {bold}${this.logItem.status}{/bold} async agent`); 121 | } 122 | } else { 123 | this.internalWarn('Trying to resolve a non async agent'); 124 | } 125 | return this; 126 | } 127 | 128 | reject(...args) { 129 | if (this.isAsync) { 130 | if (this.logItem.status === 'pending') { 131 | this.logItem.setStatus('rejected'); 132 | const rejectLog = new Agent({ 133 | name: this.name, 134 | type: 'error', 135 | message: 'rejected', 136 | ancestors: this.ancestors.concat(this), 137 | }); 138 | if (args.length) { 139 | rejectLog.error(...args); 140 | } 141 | } else { 142 | this.internalWarn(`Trying to reject an already {bold}${this.logItem.status}{/bold} async agent`); 143 | } 144 | } else { 145 | this.internalWarn('Trying to reject a non async agent'); 146 | } 147 | return this; 148 | } 149 | 150 | internalWarn(message) { 151 | new Agent({ 152 | name: this.name, 153 | type: 'warn', 154 | message, 155 | ancestors: this.ancestors.concat(this), 156 | }); 157 | } 158 | 159 | getAncestorsNames() { 160 | return this.ancestors.map(ancestor => ancestor.name); 161 | } 162 | 163 | generateStackTrace(trace) { 164 | const stackTrace = []; 165 | for (let i = 0; i < 5; i++) { 166 | stackTrace.push({ 167 | type: trace[i].getTypeName(), 168 | function: trace[i].getFunctionName(), 169 | method: trace[i].getMethodName(), 170 | file: trace[i].getFileName(), 171 | line: trace[i].getLineNumber(), 172 | column: trace[i].getColumnNumber(), 173 | }); 174 | } 175 | return stackTrace; 176 | } 177 | } 178 | 179 | export default function(name, ...args) { 180 | return new Agent({ 181 | name, 182 | type: 'root', 183 | data: args.length ? args : undefined, 184 | }); 185 | }; 186 | -------------------------------------------------------------------------------- /src/investigator.js: -------------------------------------------------------------------------------- 1 | import transceiver from 'transceiver'; 2 | 3 | import Ui from './ui'; 4 | import agent from './agent'; 5 | 6 | transceiver.setPromise(null); 7 | 8 | const ui = new Ui(); 9 | 10 | export default {ui, agent}; 11 | -------------------------------------------------------------------------------- /src/ui/index.js: -------------------------------------------------------------------------------- 1 | import blessed from 'blessed'; 2 | import transceiver from 'transceiver'; 3 | 4 | import LogsList from './logsList'; 5 | import LogDetails from './logDetails'; 6 | import Inspector from './inspector'; 7 | 8 | export default class Ui { 9 | constructor() { 10 | this.channel = transceiver.channel('ui'); 11 | this.screen = blessed.screen({ 12 | smartCSR: true 13 | }); 14 | 15 | this.logsList = new LogsList(); 16 | this.logDetails = new LogDetails(); 17 | this.inspector = new Inspector(); 18 | 19 | this.separator = blessed.line({ 20 | bottom: 6, 21 | orientation: 'horizontal' 22 | }); 23 | 24 | this.screen.append(this.logsList.element); 25 | this.screen.append(this.logDetails.element); 26 | this.screen.append(this.separator); 27 | this.screen.append(this.inspector.element); 28 | 29 | this.logsList.element.focus(); 30 | 31 | this.screen.key(['q', 'C-c'], function(ch, key) { 32 | return process.exit(0); 33 | }); 34 | 35 | this.screen.key(['i'], this.toggleInspector.bind(this)); 36 | 37 | this.screen.render(); 38 | 39 | this.channel.reply('render', () => this.screen.render()); 40 | } 41 | 42 | toggleInspector() { 43 | if (this.inspector.opened) { 44 | this.inspector.close(); 45 | this.logsList.focus(); 46 | } else { 47 | this.inspector.open(this.logsList.selectedLog); 48 | } 49 | this.screen.render(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ui/inspector.js: -------------------------------------------------------------------------------- 1 | import blessed from 'blessed'; 2 | import transceiver from 'transceiver'; 3 | import prune from 'json-prune'; 4 | import path from 'path'; 5 | import appRoot from 'app-root-path'; 6 | 7 | import tree from './tree'; 8 | 9 | export default class Inspector { 10 | constructor() { 11 | this.channel = transceiver.channel('log'); 12 | 13 | this.element = tree({ 14 | top: 'center', 15 | left: 'center', 16 | width: '90%', 17 | height: '75%', 18 | hidden: true, 19 | label: 'Inspector', 20 | tags: true, 21 | border: { 22 | type: 'line' 23 | }, 24 | style: { 25 | fg: 'white', 26 | border: { 27 | fg: '#f0f0f0' 28 | }, 29 | }, 30 | template: { 31 | extend: '{bold}{green-fg} [+]{/}', 32 | retract: '{bold}{yellow-fg} [-]{/}', 33 | lines: true, 34 | } 35 | }); 36 | } 37 | 38 | open(selectedLog) { 39 | if (!selectedLog || !selectedLog.data && !selectedLog.stackTrace) { 40 | return; 41 | } 42 | this.opened = true; 43 | this.element.show(); 44 | this.element.focus(); 45 | this.element.setData(this.prepareData(selectedLog)); 46 | } 47 | 48 | close() { 49 | this.opened = false; 50 | this.element.hide(); 51 | } 52 | 53 | prepareData(log) { 54 | const content = {}; 55 | if (log.data) { 56 | content.data = JSON.parse(prune(log.data, { 57 | depthDecr: 7, 58 | replacer: (value, defaultValue, circular) => { 59 | if (typeof value === 'function') { 60 | return '"Function [pruned]"'; 61 | } 62 | if (Array.isArray(value)) { 63 | return `"Array (${value.length}) [pruned]"`; 64 | } 65 | if (typeof value === 'object') { 66 | return '"Object [pruned]"'; 67 | } 68 | return defaultValue; 69 | } 70 | })); 71 | } 72 | 73 | if (log.stackTrace) { 74 | content['stack trace'] = log.stackTrace.map((callsite) => { 75 | const relativePath = path.relative(appRoot.toString(), callsite.file); 76 | return { 77 | type: callsite.type, 78 | function: callsite.function, 79 | method: callsite.method, 80 | file: `${relativePath}:{yellow-fg}${callsite.line}{/yellow-fg}:{yellow-fg}${callsite.column}{/yellow-fg}`, 81 | }; 82 | }); 83 | } 84 | return this.formatData(content); 85 | } 86 | 87 | formatData(data, key, depth = 0) { 88 | depth++; 89 | if (typeof data === 'object') { 90 | if (data !== null) { 91 | let name; 92 | let extended; 93 | 94 | if (depth === 2) { 95 | name = `{yellow-fg}{bold}${key.toUpperCase()}{/bold}{/yellow-fg} {magenta-fg}(${data.length}){/magenta-fg}`; 96 | extended = key === 'data'; 97 | } else { 98 | const type = (Array.isArray(data) ? `[Array] {magenta-fg}(${data.length}){/magenta-fg}` : '[Object]'); 99 | name = `{blue-fg}{bold}${key ? key + ' ' : ''}{/bold}${type}{/blue-fg}`; 100 | extended = depth < 4; 101 | } 102 | const newObj = { 103 | children: {}, 104 | name, 105 | extended 106 | }; 107 | Object.keys(data).forEach((key) => { 108 | const child = this.formatData(data[key], key, depth); 109 | if (child) { 110 | newObj.children[key] = child; 111 | } 112 | }); 113 | return newObj; 114 | } 115 | } 116 | if (typeof data === 'function') { 117 | return { 118 | name: `{blue-fg}${key}{/blue-fg}: {red-fg}{bold}[Function]{/}`, 119 | }; 120 | } 121 | if (typeof data === 'number') { 122 | return { 123 | name: `{blue-fg}${key}{/blue-fg}: {yellow-fg}${data}{/}`, 124 | }; 125 | } 126 | if (data === null) { 127 | return { 128 | name: `{blue-fg}${key}{/blue-fg}: {cyan-fg}{bold}null{/}`, 129 | }; 130 | } 131 | return { 132 | name: `{blue-fg}${key}{/blue-fg}: ${data}`, 133 | }; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/ui/logDetails.js: -------------------------------------------------------------------------------- 1 | import blessed from 'blessed'; 2 | import transceiver from 'transceiver'; 3 | import dateFormat from 'dateformat'; 4 | 5 | export default class logDetails { 6 | constructor() { 7 | this.channel = transceiver.channel('log'); 8 | this.element = blessed.box({ 9 | height: 6, 10 | left: '0', 11 | bottom: 0, 12 | tags: true, 13 | keys: true, 14 | padding: { 15 | left: 1, 16 | right: 1, 17 | }, 18 | style: { 19 | selected: { 20 | fg: 'black', 21 | bg: 'white', 22 | border: { 23 | fg: 'white' 24 | }, 25 | hover: { 26 | bg: 'green' 27 | } 28 | } 29 | } 30 | }); 31 | 32 | this.channel.on('select log', this.updateLogDetails.bind(this)); 33 | } 34 | 35 | updateLogDetails(log) { 36 | this.element.setContent(this.renderType(log) + this.renderId(log) + this.renderDate(log) + this.renderDuration(log) + this.renderData(log)); 37 | } 38 | 39 | renderType(log) { 40 | if (log.type === 'root') { 41 | return '{magenta-fg}{bold}ROOT NODE{/bold}{/magenta-fg}\n'; 42 | } 43 | if (log.type === 'success') { 44 | return '{green-fg}✔ {bold}SUCCESS{/bold}{/green-fg}\n'; 45 | } 46 | if (log.type === 'error') { 47 | return '{red-fg}✘ {bold}ERROR{/bold}{/red-fg}\n'; 48 | } 49 | if (log.type === 'warn') { 50 | return '{yellow-fg}! {bold}WARN{/bold}{/red-fg}\n'; 51 | } 52 | if (log.type === 'node') { 53 | return '{grey-fg}{bold}NODE{/bold}{/grey-fg}\n'; 54 | } 55 | if (log.type === 'async') { 56 | if (log.status === 'resolved') { 57 | return '{bold}{green-fg}ASYNC NODE{/bold} (RESOLVED ✔){/green-fg}\n'; 58 | } 59 | if (log.status === 'rejected') { 60 | return '{bold}{red-fg}ASYNC NODE{/bold} (REJECTED ✘){/red-fg}\n'; 61 | } 62 | if (log.status === 'pending') { 63 | return '{cyan-fg}{bold}ASYNC NODE{/bold} (PENDING ⌛){/cyan-fg}\n'; 64 | } 65 | } 66 | if (log.type === 'info') { 67 | return '{white-fg}{bold}INFO{/bold}{/white-fg}\n'; 68 | } 69 | return ''; 70 | } 71 | 72 | renderId(log) { 73 | return `{bold}ID:{/bold} {underline}${log.id}{/underline}\n`; 74 | } 75 | 76 | renderDate(log) { 77 | return `{bold}TIME:{/bold} {magenta-fg}${dateFormat(log.date, 'dddd, mmmm dS yyyy, HH:MM:ss.L')}{/magenta-fg}\n`; 78 | } 79 | 80 | renderDuration(log) { 81 | if (log.relativeDuration && log.previousLog) { 82 | return `{bold}DURATION:{/bold} {yellow-fg}${log.relativeDuration}{/yellow-fg} (from {underline}${log.previousLog.id}{/underline})\n`; 83 | } 84 | return ''; 85 | } 86 | 87 | renderData(log) { 88 | if (log.data) { 89 | return `{bold}DATA:{/bold} ${log.renderData()}\n`; 90 | } 91 | return ''; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ui/logItem.js: -------------------------------------------------------------------------------- 1 | import blessed from 'blessed'; 2 | import shortid from 'shortid'; 3 | import transceiver from 'transceiver'; 4 | import prune from 'json-prune'; 5 | import dateFormat from 'dateformat'; 6 | 7 | export default class LogItem { 8 | constructor({name, type, status, parent, data, message, stackTrace, date = Date.now()}) { 9 | this.id = shortid.generate(); 10 | this.name = name; 11 | this.type = type; 12 | this.status = status; 13 | this.data = data; 14 | this.message = message; 15 | this.stackTrace = stackTrace; 16 | this.date = date; 17 | this.children = []; 18 | this.channel = transceiver.channel('log'); 19 | 20 | if (parent) { 21 | this.depth = parent.depth + 1; 22 | this.parent = parent; 23 | this.previousLog = parent.getLastChild() || parent; 24 | this.relativeDuration = this.getRelativeDuration(); 25 | this.parent.addChild(this); 26 | } else { 27 | this.depth = 0; 28 | } 29 | this.element = this.channel.request('addLog', this); 30 | this.update(); 31 | } 32 | 33 | update() { 34 | if (this.element) { 35 | this.element.content = this.render(); 36 | transceiver.request('ui', 'render'); 37 | } 38 | } 39 | 40 | render() { 41 | let message = `${this.renderState()}${this.renderName()}${this.renderMessage()}${this.renderData()}${this.renderDate()}${this.renderDuration()}`; 42 | for (let i = 0; i < this.depth; i++) { 43 | message = ' ' + message; 44 | } 45 | return message; 46 | } 47 | 48 | renderState() { 49 | if (this.type === 'async' && this.status === 'pending') { 50 | return `{cyan-fg}[⌛]{/cyan-fg} `; 51 | } 52 | if (this.type === 'async' && this.status === 'resolved') { 53 | return `{green-fg}[✔]{/green-fg} `; 54 | } 55 | if (this.type === 'async' && this.status === 'rejected') { 56 | return `{red-fg}[✘]{/red-fg} `; 57 | } 58 | if (this.type === 'success') { 59 | return `{green-fg}✔{/green-fg} `; 60 | } 61 | if (this.type === 'error') { 62 | return `{red-fg}✘{/red-fg} `; 63 | } 64 | if (this.type === 'warn') { 65 | return `{yellow-fg}❗{/yellow-fg} `; 66 | } 67 | if (this.type === 'info') { 68 | return '⇢ '; 69 | } 70 | return ''; 71 | } 72 | 73 | renderName() { 74 | if (this.depth === 0) { 75 | return this.name ? `{underline}{bold}${this.name}{/bold}{/underline} ` : ''; 76 | } 77 | if (this.type === 'async') { 78 | if (this.status === 'resolved') { 79 | return `{bold}{green-fg}${this.name}{/green-fg}{/bold} (async) `; 80 | } 81 | if (this.status === 'rejected') { 82 | return `{bold}{red-fg}${this.name}{/red-fg}{/bold} (async) `; 83 | } 84 | return `{bold}${this.name}{/bold} (async) `; 85 | } 86 | if (this.type === 'success') { 87 | return this.name ? `{bold}{green-fg}${this.name}{/green-fg}{/bold} ` : ''; 88 | } 89 | if (this.type === 'error') { 90 | return this.name ? `{bold}{red-fg}${this.name}{/red-fg}{/bold} ` : ''; 91 | } 92 | if (this.type === 'warn') { 93 | return this.name ? `{bold}{yellow-fg}${this.name}{/yellow-fg}{/bold} ` : ''; 94 | } 95 | return this.name ? `{bold}${this.name}{/bold} ` : ''; 96 | } 97 | 98 | renderData() { 99 | if (this.depth === 0) { 100 | // console.log(this.data); 101 | } 102 | if (!this.data) { 103 | return ''; 104 | } 105 | if (Array.isArray(this.data)) { 106 | return this.data.map(this.renderValue.bind(this)).join(' ') + ' '; 107 | } 108 | return this.renderValue(this.data) + ' '; 109 | } 110 | 111 | renderValue(value) { 112 | if (Array.isArray(value)) { 113 | return `{cyan-fg}${this.prune(value)}{/cyan-fg}`; 114 | } 115 | if (typeof value === 'object') { 116 | return `{blue-fg}${this.prune(value)}{/blue-fg}`; 117 | } 118 | if (typeof value === 'function') { 119 | return `{red-fg}{bold}[Function]{/bold}{red-fg}`; 120 | } 121 | if (typeof value === 'number') { 122 | return `{yellow-fg}${value}{/yellow-fg}`; 123 | } 124 | if (typeof value === 'string') { 125 | if (this.type === 'success') { 126 | return `{green-fg}${value}{/green-fg}`; 127 | } 128 | if (this.type === 'error') { 129 | return `{red-fg}${value}{/red-fg}`; 130 | } 131 | if (this.type === 'warn') { 132 | return `{yellow-fg}${value}{/yellow-fg}`; 133 | } 134 | } 135 | return value; 136 | } 137 | 138 | renderMessage() { 139 | if (this.message) { 140 | if (this.type === 'success') { 141 | return `{green-fg}${this.message}{/green-fg} `; 142 | } 143 | if (this.type === 'error') { 144 | return `{red-fg}${this.message}{/red-fg} `; 145 | } 146 | if (this.type === 'warn') { 147 | return `{yellow-fg}${this.message}{/yellow-fg} `; 148 | } 149 | return `${this.message} `; 150 | } 151 | return ''; 152 | } 153 | 154 | renderDate() { 155 | if (this.depth === 0) { 156 | return `{magenta-fg}(${dateFormat(this.date, 'dd/mm/yyyy HH:MM:ss.L')}){/magenta-fg} `; 157 | } 158 | return ''; 159 | } 160 | 161 | renderDuration() { 162 | if (this.relativeDuration) { 163 | return `{grey-fg}+${this.relativeDuration}{/grey-fg} `; 164 | } 165 | return ''; 166 | } 167 | 168 | getRelativeDuration() { 169 | return this.humanizeDuration(this.date - this.previousLog.date); 170 | } 171 | 172 | humanizeDuration(duration) { 173 | if (duration < 1000) { 174 | return `${duration}ms`; 175 | } 176 | if (duration < 60000) { 177 | let milliseconds = duration % 1000; 178 | milliseconds = ('000' + milliseconds).slice(-3); 179 | return `${Math.floor(duration / 1000)}.${milliseconds}s`; 180 | } 181 | return `${Math.floor(duration / 60000)}m ${Math.round((duration % 60000) / 1000)}s`; 182 | } 183 | 184 | addChild(log) { 185 | this.children.push(log); 186 | } 187 | 188 | getLastChild() { 189 | return this.children[this.children.length - 1]; 190 | } 191 | 192 | getChildren(list) { 193 | list = list || []; 194 | list.push.apply(list, this.children); 195 | this.children.forEach(child => { 196 | child.getChildren(list); 197 | }); 198 | return list; 199 | } 200 | 201 | setStatus(status) { 202 | this.status = status; 203 | this.update(); 204 | } 205 | 206 | prune(value) { 207 | return prune(value, { 208 | depthDecr: 2, 209 | arrayMaxLength: 8, 210 | prunedString: ' [...]' 211 | }); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/ui/logsList.js: -------------------------------------------------------------------------------- 1 | import blessed from 'blessed'; 2 | import transceiver from 'transceiver'; 3 | 4 | export default class LogsList { 5 | constructor() { 6 | this.selectedLog = null; 7 | this.logs = {}; 8 | this.logsCount = 0; 9 | this.channel = transceiver.channel('log'); 10 | this.autoScroll = true; 11 | this.element = blessed.list({ 12 | top: '0', 13 | left: '0', 14 | bottom: 7, 15 | tags: true, 16 | keys: true, 17 | mouse: true, 18 | scrollbar: { 19 | bg: 'magenta', 20 | }, 21 | style: { 22 | selected: { 23 | fg: 'black', 24 | bg: 'white', 25 | } 26 | } 27 | }); 28 | 29 | this.element.key(['up', 'down', 's', 'b'], (ch, key) => { 30 | if (key.name === 's') { 31 | this.autoScroll = !this.autoScroll; 32 | } else if (key.name === 'b') { 33 | this.scrollToBottom(); 34 | transceiver.request('ui', 'render'); 35 | } else { 36 | this.autoScroll = false; 37 | } 38 | }); 39 | 40 | this.element.on('select item', (element, i) => { 41 | this.selectedLog = this.getLogFromElement(element); 42 | if (this.selectedLog) { 43 | this.channel.emit('select log', this.selectedLog); 44 | } 45 | }); 46 | 47 | this.channel.reply({ 48 | addLog: this.addLog, 49 | getSelectedLog: this.getSelectedLog, 50 | }, this); 51 | } 52 | 53 | addLog(log) { 54 | let element; 55 | 56 | this.logs[log.id] = log; 57 | this.logsCount++; 58 | 59 | if (log.parent) { 60 | const index = this.element.getItemIndex(log.parent.element) + log.parent.getChildren().length; 61 | this.element.insertItem(index, log.render()); 62 | element = this.element.getItem(index); 63 | } else { 64 | element = this.element.add(log.render()); 65 | } 66 | element.logId = log.id; 67 | if (this.autoScroll) { 68 | this.scrollToBottom(); 69 | } 70 | if (this.logsCount === 1) { 71 | this.channel.emit('select log', log); 72 | } 73 | return element; 74 | } 75 | 76 | getSelectedLog() { 77 | return this.selectedLog; 78 | } 79 | 80 | scrollToBottom() { 81 | this.element.move(this.logsCount); 82 | } 83 | 84 | getLogFromElement(element) { 85 | return this.logs[element.logId]; 86 | } 87 | 88 | focus() { 89 | this.element.focus(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/ui/tree.js: -------------------------------------------------------------------------------- 1 | // https://github.com/yaronn/blessed-contrib/blob/master/lib/widget/tree.js 2 | import blessed from 'blessed'; 3 | 4 | const Node = blessed.Node; 5 | const Box = blessed.Box; 6 | 7 | function Tree(options) { 8 | 9 | if (!(this instanceof Node)) { 10 | return new Tree(options); 11 | } 12 | 13 | options = options || {}; 14 | options.bold = true; 15 | var self = this; 16 | this.options = options; 17 | this.data = {}; 18 | this.nodeLines = []; 19 | this.lineNbr = 0; 20 | Box.call(this, options); 21 | 22 | options.extended = options.extended || false; 23 | options.keys = options.keys || ['space','enter']; 24 | 25 | options.template = options.template || {}; 26 | options.template.extend = options.template.extend || ' [+]'; 27 | options.template.retract = options.template.retract || ' [-]'; 28 | options.template.lines = options.template.lines || false; 29 | 30 | this.rows = blessed.list({ 31 | height: 0, 32 | top: 1, 33 | width: 0, 34 | left: 1, 35 | selectedFg: 'black', 36 | selectedBg: 'white', 37 | keys: true, 38 | tags: true, 39 | }); 40 | 41 | this.rows.key(options.keys,function() { 42 | self.nodeLines[this.getItemIndex(this.selected)].extended = !self.nodeLines[this.getItemIndex(this.selected)].extended; 43 | self.setData(self.data); 44 | self.screen.render(); 45 | 46 | self.emit('select',self.nodeLines[this.getItemIndex(this.selected)]); 47 | }); 48 | 49 | this.append(this.rows); 50 | } 51 | 52 | Tree.prototype.walk = function(node, treeDepth) { 53 | var lines = []; 54 | 55 | if (!node.parent) { 56 | node.parent = null; 57 | } 58 | 59 | if (treeDepth == '' && node.name) { 60 | this.lineNbr = 0; 61 | this.nodeLines[this.lineNbr++] = node; 62 | lines.push(node.name); 63 | treeDepth = ' '; 64 | } 65 | 66 | node.depth = treeDepth.length - 1; 67 | 68 | if (node.children && node.extended) { 69 | 70 | var i = 0; 71 | 72 | if (typeof node.children == 'function') { 73 | node.childrenContent = node.children(node); 74 | } 75 | 76 | if (!node.childrenContent) { 77 | node.childrenContent = node.children; 78 | } 79 | 80 | for (var child in node.childrenContent) { 81 | 82 | if (!node.childrenContent[child].name) { 83 | node.childrenContent[child].name = child; 84 | } 85 | 86 | var childIndex = child; 87 | child = node.childrenContent[child]; 88 | child.parent = node; 89 | child.position = i++; 90 | 91 | if (typeof child.extended == 'undefined') { 92 | child.extended = this.options.extended; 93 | } 94 | 95 | if (typeof child.children == 'function') { 96 | child.childrenContent = child.children(child); 97 | } else { 98 | child.childrenContent = child.children; 99 | } 100 | 101 | var isLastChild = child.position == Object.keys(child.parent.childrenContent).length - 1; 102 | var tree; 103 | var suffix = ''; 104 | if (isLastChild) { 105 | tree = '└'; 106 | } else { 107 | tree = '├'; 108 | } 109 | if (!child.childrenContent || Object.keys(child.childrenContent).length == 0) { 110 | tree += '─'; 111 | } else if (child.extended) { 112 | tree += '┬'; 113 | suffix = this.options.template.retract; 114 | } else { 115 | tree += '─'; 116 | suffix = this.options.template.extend; 117 | } 118 | 119 | if (!this.options.template.lines) { 120 | tree = '|-'; 121 | } 122 | 123 | lines.push(treeDepth + tree + child.name + suffix); 124 | 125 | this.nodeLines[this.lineNbr++] = child; 126 | 127 | var parentTree; 128 | if (isLastChild || !this.options.template.lines) { 129 | parentTree = treeDepth + ' '; 130 | } else { 131 | parentTree = treeDepth + '│'; 132 | } 133 | lines = lines.concat(this.walk(child, parentTree)); 134 | } 135 | } 136 | return lines; 137 | }; 138 | 139 | Tree.prototype.focus = function() { 140 | this.rows.focus(); 141 | }; 142 | 143 | Tree.prototype.render = function() { 144 | if (this.screen.focused == this.rows) { 145 | this.rows.focus(); 146 | } 147 | 148 | this.rows.width = this.width - 3; 149 | this.rows.height = this.height - 3; 150 | Box.prototype.render.call(this); 151 | }; 152 | 153 | Tree.prototype.setData = function(data) { 154 | 155 | var formatted = []; 156 | formatted = this.walk(data,''); 157 | 158 | this.data = data; 159 | this.rows.setItems(formatted); 160 | }; 161 | 162 | Tree.prototype.__proto__ = Box.prototype; 163 | 164 | Tree.prototype.type = 'tree'; 165 | 166 | export default Tree; 167 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 0, 5 | "quotes": [2, "single"], 6 | "no-unused-expressions": 0 7 | }, 8 | "env": { 9 | "browser": true, 10 | "node": true, 11 | "mocha": true 12 | }, 13 | "globals": { 14 | "spy": true, 15 | "expect": true 16 | } 17 | } -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /test/setup/browserify.js: -------------------------------------------------------------------------------- 1 | var config = require('../../package.json').babelBoilerplateOptions; 2 | 3 | global.mocha.setup('bdd'); 4 | global.onload = function() { 5 | global.mocha.checkLeaks(); 6 | global.mocha.globals(config.mochaGlobals); 7 | global.mocha.run(); 8 | require('./setup')(); 9 | }; 10 | -------------------------------------------------------------------------------- /test/setup/node.js: -------------------------------------------------------------------------------- 1 | global.chai = require('chai'); 2 | global.sinon = require('sinon'); 3 | global.chai.use(require('sinon-chai')); 4 | 5 | require('babel-core/register'); 6 | require('./setup')(); 7 | -------------------------------------------------------------------------------- /test/setup/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | global.expect = global.chai.expect; 3 | 4 | beforeEach(function() { 5 | this.sandbox = global.sinon.sandbox.create(); 6 | global.stub = this.sandbox.stub.bind(this.sandbox); 7 | global.spy = this.sandbox.spy.bind(this.sandbox); 8 | }); 9 | 10 | afterEach(function() { 11 | delete global.stub; 12 | delete global.spy; 13 | this.sandbox.restore(); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /test/unit/investigator.js: -------------------------------------------------------------------------------- 1 | import investigator from '../../src/investigator'; 2 | 3 | describe('investigator', () => { 4 | // TODO 5 | }); 6 | --------------------------------------------------------------------------------