├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── README.md ├── bin └── bn.js ├── bower.json ├── gulpfile.js ├── images └── monitor.jpg ├── index.js ├── lib ├── app │ ├── commands │ │ ├── exit.js │ │ ├── images.js │ │ ├── monitor.js │ │ ├── ps.js │ │ ├── reload.js │ │ ├── restart.js │ │ ├── start-script.js │ │ ├── start.js │ │ ├── stop-script.js │ │ └── stop.js │ ├── index.js │ ├── lib │ │ ├── image.js │ │ ├── process.js │ │ └── utils.js │ └── monitor │ │ ├── index.js │ │ ├── knurly.js │ │ ├── lib │ │ ├── connection.js │ │ └── session.js │ │ ├── public │ │ ├── fonts │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── monitor.css │ │ └── monitor.js │ │ ├── src │ │ ├── app │ │ │ ├── directives │ │ │ │ ├── image-controls │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── template.pug │ │ │ │ ├── image-script-controls │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── template.pug │ │ │ │ ├── image-status │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── template.pug │ │ │ │ ├── log-monitor │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── template.jade │ │ │ │ └── script-button │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── template.pug │ │ │ ├── index.js │ │ │ ├── modules │ │ │ │ ├── ansi │ │ │ │ │ ├── index.js │ │ │ │ │ └── lib │ │ │ │ │ │ └── ansi2html.js │ │ │ │ └── rx-ext │ │ │ │ │ └── index.js │ │ │ ├── services │ │ │ │ ├── cmd.js │ │ │ │ ├── data.js │ │ │ │ └── socket.js │ │ │ ├── states │ │ │ │ ├── app.dashboard │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── template.pug │ │ │ │ ├── app.images.image │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── template.pug │ │ │ │ ├── app.images │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── template.pug │ │ │ │ ├── app │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── template.pug │ │ │ │ ├── index.js │ │ │ │ └── styles.scss │ │ │ └── styles.scss │ │ ├── boot.js │ │ └── main.scss │ │ └── view │ │ └── index.html └── vorpal-plugins │ └── commands │ ├── exit.js │ ├── images.js │ ├── monitor.js │ ├── ps.js │ ├── reload.js │ ├── restart.js │ ├── start-script.js │ ├── start.js │ ├── stop-script.js │ └── stop.js ├── package-lock.json ├── package.json ├── test ├── app.js ├── topics │ ├── detect-execute-scripts │ │ ├── node-compose.yml │ │ ├── service-a │ │ │ ├── package.json │ │ │ └── script.js │ │ └── test │ │ │ └── unit.js │ ├── detect-start-command │ │ ├── node-compose.yml │ │ ├── service-a │ │ │ └── server.js │ │ ├── service-b │ │ │ └── server.js │ │ ├── service-c │ │ │ └── index.js │ │ ├── service-d │ │ │ ├── package.json │ │ │ └── run.js │ │ └── test │ │ │ └── unit.js │ ├── environment-vars │ │ ├── node-compose.yml │ │ ├── service-a │ │ │ └── package.json │ │ ├── service-b │ │ │ └── package.json │ │ └── test │ │ │ └── unit.js │ ├── processes │ │ ├── node-compose.yml │ │ ├── service │ │ │ ├── index.js │ │ │ └── package.json │ │ └── test │ │ │ └── unit.js │ └── start-restart-stop │ │ ├── node-compose.yml │ │ ├── service-a │ │ └── index.js │ │ ├── service-b │ │ └── index.js │ │ └── test │ │ └── unit.js └── unit │ └── class-process.js └── webpack.config.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "vendor" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tabs 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.jade] 16 | insert_final_newline = false 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | temp/ 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "white": true 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #node-compose 2 | 3 | Start node-processes as with docker-compose and with a neat little ui. 4 | 5 | 6 | 7 | 8 | 9 | [![Build Status](https://travis-ci.org/platdesign/node-compose.svg?branch=master)](https://travis-ci.org/platdesign/node-compose) 10 | [![Package Quality](http://npm.packagequality.com/shield/node-compose.svg)](http://packagequality.com/#?package=node-compose) 11 | [![Gitter](https://badges.gitter.im/platdesign/node-compose.svg)](https://gitter.im/platdesign/node-compose?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 12 | 13 | [![](https://nodei.co/npm/node-compose.png?downloads=true&downloadRank=true&stars=true)](https://www.npmjs.com/package/node-compose) 14 | [Detailed npm trends and stats](http://npm-stat.com/charts.html?package=node-compose) 15 | 16 | ---- 17 | 18 | ![Monitor preview](./images/monitor.jpg) 19 | 20 | 21 | 22 | # Important 23 | 24 | **`node-compose` is for development only! Don't use it in production!** 25 | 26 | 27 | # Prerequisite 28 | 29 | `node-compose` needs [nodemon](http://nodemon.io/) to be installed globally. 30 | 31 | Install [nodemon](http://nodemon.io/): `npm install -g nodemon` 32 | 33 | # Install 34 | 35 | `npm install -g node-compose` 36 | 37 | 38 | # Example 39 | 40 | Try a [simple example repo](https://github.com/platdesign/node-compose-example) or create your own example with following steps. 41 | 42 | 1. Create multiple servers. (eg for multiple microservices) 43 | 44 | **`./process-a/index.js`** (Port 3000) 45 | 46 | ```javascript 47 | const http = require('http'); 48 | 49 | const hostname = '127.0.0.1'; 50 | const port = 3000; 51 | 52 | const server = http.createServer((req, res) => { 53 | res.statusCode = 200; 54 | res.setHeader('Content-Type', 'text/plain'); 55 | res.end('Hello World\n'); 56 | }); 57 | 58 | server.listen(port, hostname, () => { 59 | console.log(`Server running at http://${hostname}:${port}/`); 60 | }); 61 | ``` 62 | 63 | **`./process-b/index.js`** (Port 4000) 64 | 65 | ```javascript 66 | const http = require('http'); 67 | 68 | const hostname = '127.0.0.1'; 69 | const port = 4000; 70 | 71 | const server = http.createServer((req, res) => { 72 | res.statusCode = 200; 73 | res.setHeader('Content-Type', 'text/plain'); 74 | res.end('Hello World\n'); 75 | }); 76 | 77 | server.listen(port, hostname, () => { 78 | console.log(`Server running at http://${hostname}:${port}/`); 79 | }); 80 | ``` 81 | 82 | 2. Create a compose-file. **`node-compose.yml`** 83 | 84 | ```yaml 85 | process_a: 86 | build: ./process-a 87 | environment: 88 | - NODE_ENV=development 89 | 90 | process_b: 91 | build: ./process-b 92 | environment: 93 | - NODE_ENV=development 94 | ``` 95 | 96 | 3. Start node-compose shell with `node-compose`. Inside the shell start all processes with `start all` or type `help` for more information about possible commands. 97 | 98 | # Commands 99 | 100 | ```bash 101 | Commands: 102 | help [command...] Provides help for a given command. 103 | exit Closing shell and stop all images 104 | ps List running images 105 | reload Reloads config file and restarts processes if needed. 106 | start [images...] Start an image by or all 107 | stop [images...] Stop an image by or all 108 | restart [images...] Restart one or more images by given name or all 109 | images List all images found in config file. 110 | monitor [options] Start web monitor 111 | start-script Start a script of an image 112 | stop-script Stop a script of an image 113 | 114 | ``` 115 | 116 | 117 | # Todo 118 | 119 | - Use nodemon as a module to avoid global dep. 120 | - Add more information about running processes to `ps`-view. 121 | 122 | 123 | 124 | 125 | # Tests 126 | 127 | Tests are written with [mocha](https://mochajs.org/). 128 | 129 | 1. Clone repo `git clone https://github.com/platdesign/node-compose.git` 130 | 2. Move to repo directory. `cd node-compose` 131 | 3. Install all dependencies. `npm install` 132 | 4. Run tests with `mocha` 133 | 134 | 135 | # Author 136 | 137 | Christian Blaschke 138 | 139 | 140 | 141 | 142 | ### MIT License 143 | Copyright (c) 2016 node-compose 144 | 145 | Permission is hereby granted, free of charge, to any person obtaining a copy 146 | of this software and associated documentation files (the "Software"), to deal 147 | in the Software without restriction, including without limitation the rights 148 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 149 | copies of the Software, and to permit persons to whom the Software is 150 | furnished to do so, subject to the following conditions: 151 | 152 | The above copyright notice and this permission notice shall be included in all 153 | copies or substantial portions of the Software. 154 | 155 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 156 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 157 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 158 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 159 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 160 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 161 | SOFTWARE. 162 | -------------------------------------------------------------------------------- /bin/bn.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const bn = require('../'); 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootnode", 3 | "description": "A project by platdesign.de", 4 | "author": "Christian Blaschke ", 5 | "version": "0.0.0", 6 | "dependencies": {} 7 | } 8 | 9 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const gulp = require('gulp'); 5 | const path = require('path'); 6 | 7 | const knurly = require('knurly')(gulp); 8 | const transpiler = knurly.transpiler; 9 | 10 | 11 | 12 | transpiler.loadComponent( path.join(__dirname, 'lib', 'app', 'monitor') ); 13 | 14 | 15 | 16 | knurly.registerDefaultTasks(); 17 | -------------------------------------------------------------------------------- /images/monitor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platdesign/node-compose/79087833e8558a439f14bd03a45d1144f2715186/images/monitor.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Deps 4 | const path = require('path'); 5 | const App = require('./lib/app'); 6 | const Vorpal = require('vorpal'); 7 | const updateNotifier = require('update-notifier'); 8 | const pkg = require('./package.json'); 9 | 10 | 11 | // Init vorpal instance 12 | const vorpal = Vorpal(); 13 | 14 | // Notify about module updates 15 | updateNotifier({ pkg: pkg, updateCheckInterval: 0 }).notify({ defer: false }); 16 | 17 | // Init App 18 | const app = App({ 19 | CWD: process.cwd(), 20 | configFile: process.argv[2], 21 | logger: vorpal.log.bind(vorpal) 22 | }); 23 | 24 | // RegisterVorpal-Plugins 25 | 26 | // Commands 27 | [ 28 | 'ps', 29 | 'reload', 30 | 'start', 31 | 'stop', 32 | 'restart', 33 | 'images', 34 | 'exit', 35 | 'monitor', 36 | 'start-script', 37 | 'stop-script', 38 | ].forEach( registerCMD ); 39 | 40 | 41 | 42 | // Final configs 43 | vorpal 44 | .delimiter('node-compose $') 45 | .show(); 46 | 47 | 48 | 49 | 50 | function registerCMD(name) { 51 | vorpal.use( require( path.join(__dirname, 'lib', 'vorpal-plugins', 'commands', name) ), { app: app }); 52 | } 53 | -------------------------------------------------------------------------------- /lib/app/commands/exit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(args) { 5 | 6 | return this.close() 7 | .then(() => { 8 | process.exit(); 9 | }); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /lib/app/commands/images.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const colors = require('colors'); 4 | 5 | module.exports = function(args) { 6 | 7 | let headers = ['No', 'Name', 'Start', 'CWD', 'Status']; 8 | 9 | let rows = this.imagesAsArray().map((image, $index) => { 10 | 11 | let state; 12 | switch(image.state) { 13 | case 'idle': 14 | state = colors.gray('Idle'); 15 | break; 16 | case 'running': 17 | state = colors.green('Running'); 18 | break; 19 | case 'starting': 20 | state = colors.green('Starting'); 21 | break; 22 | case 'stopping': 23 | state = colors.red('Stopping'); 24 | break; 25 | default: 26 | state = colors.gray('Unknown'); 27 | break; 28 | } 29 | 30 | return [ 31 | $index+1, 32 | image._name, 33 | image.config.commands.start, 34 | image.config.cwd, 35 | state 36 | ]; 37 | }); 38 | 39 | 40 | this.log(['#stdout'], this.utils.createTableView('Images', headers, rows)); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /lib/app/commands/monitor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createServer = require('../monitor'); 4 | const open = require('open'); 5 | const ctxq = require('ctxq'); 6 | 7 | 8 | 9 | module.exports = function(args) { 10 | 11 | return ctxq() 12 | .push(() => { 13 | 14 | if (this.monitorServer) { 15 | this.log(['error', 'monitor'], `Monitor is already running at ${this.monitorServer.info.uri}`); 16 | } else { 17 | return createServer(this, { 18 | port: args.options.port, 19 | host: args.options.host 20 | }).then((server) => this.monitorServer = server); 21 | } 22 | 23 | }) 24 | .push(() => { 25 | if(args.options.open && this.monitorServer) { 26 | if(args.options.open && args.options.open !== true) { 27 | open(this.monitorServer.info.uri, args.options.open); 28 | } else { 29 | open(this.monitorServer.info.uri); 30 | } 31 | } 32 | }) 33 | .run(); 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /lib/app/commands/ps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(args) { 5 | 6 | let headers = ['No', 'Name']; 7 | 8 | let rows = this.imagesAsArray() 9 | .filter((image) => { 10 | return image.state !== 'idle'; 11 | }) 12 | .map((image, $index) => { 13 | return [$index + 1, image._name]; 14 | }); 15 | 16 | 17 | this.log(['#stdout'], this.utils.createTableView('Running images', headers, rows)); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /lib/app/commands/reload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(args) { 5 | let config = this.loadConfigFile(); 6 | return this.updateImagesConfig(config); 7 | } 8 | -------------------------------------------------------------------------------- /lib/app/commands/restart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(args) { 5 | 6 | let images; 7 | 8 | if (args.image === 'all') { 9 | images = this.imagesAsArray() 10 | .filter((image) => { 11 | return image.state === 'running'; 12 | }); 13 | 14 | if(!images.length) { 15 | this.log(['log'], 'No running image(s) to restart.'); 16 | } 17 | } else { 18 | images = this.expectImagesByNameArray( 19 | [args.image].concat((args.images || [])) 20 | ); 21 | } 22 | 23 | 24 | return Promise.all( 25 | images.map((image) => { 26 | return image.restart(); 27 | }) 28 | ); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /lib/app/commands/start-script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(args) { 5 | 6 | let image; 7 | try { 8 | image = this.getImageByName(args.image); 9 | } catch(e) { 10 | return this.log(['error'], e.message); 11 | } 12 | 13 | return image.startScript(args.script); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/commands/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(args) { 5 | 6 | let images; 7 | 8 | if (args.image === 'all') { 9 | images = this.imagesAsArray() 10 | .filter((image) => { 11 | return image.state === 'idle'; 12 | }); 13 | 14 | if(!images.length) { 15 | this.log(['log'], 'No idle images found to start.'); 16 | } 17 | } else { 18 | images = this.expectImagesByNameArray( 19 | [args.image].concat((args.images || [])) 20 | ); 21 | } 22 | 23 | 24 | return Promise.all( 25 | images.map((image) => { 26 | return image.start(); 27 | }) 28 | ); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /lib/app/commands/stop-script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(args) { 5 | 6 | let image; 7 | try { 8 | image = this.getImageByName(args.image); 9 | } catch(e) { 10 | return this.log(['error'], e.message); 11 | } 12 | 13 | return image.stopScript(args.script); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/commands/stop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(args) { 5 | 6 | let images; 7 | 8 | if (args.image === 'all') { 9 | images = this.imagesAsArray() 10 | .filter((image) => { 11 | return image.state === 'running'; 12 | }); 13 | 14 | if(!images.length) { 15 | this.log(['log'], 'No running images found to stop.'); 16 | } 17 | } else { 18 | images = this.expectImagesByNameArray( 19 | [args.image].concat((args.images || [])) 20 | ); 21 | } 22 | 23 | 24 | return Promise.all( 25 | images.map((image) => { 26 | return image.stop(); 27 | }) 28 | ); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /lib/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Third party deps 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const colors = require('colors'); 7 | const Yaml = require('js-yaml'); 8 | const rx = require('rx'); 9 | 10 | // Custom deps 11 | const utils = require('./lib/utils'); 12 | const Image = require('./lib/image'); 13 | 14 | 15 | /** 16 | * Main app-class which handles main config and image-store. 17 | * 18 | * @param {Object} config App configuration 19 | * 20 | * { 21 | * CWD: , 22 | * configFile: , 23 | * logger: 24 | * } 25 | * 26 | */ 27 | class App { 28 | 29 | constructor(config) { 30 | 31 | // add utils to instance for easy use 32 | // and to avoid loading it from different locations. 33 | this.utils = utils; 34 | 35 | // assign given config 36 | this._config = config; 37 | 38 | // image store 39 | this._images = {}; 40 | 41 | // Add on/emit methods to instance 42 | utils.extendEvents(this, '_eventsSubject'); 43 | 44 | // define observables 45 | this._defineObservables(); 46 | 47 | // define which logger-method to use 48 | // if logger is not available in given config us console.log 49 | this._logger = config.logger || console.log.bind(console); 50 | 51 | // subscribe logger to logs observable 52 | this.observables.logs.subscribe(function(msg) { 53 | this._logger(colors.green('[ ' + msg.tags.join(', ') + ' ] ') + msg.data); 54 | }.bind(this)); 55 | 56 | // define vorpal command handlers 57 | this._defineCommands(); 58 | 59 | // Load config file 60 | this.updateImagesConfig(this.loadConfigFile()); 61 | 62 | } 63 | 64 | 65 | getState() { 66 | 67 | return utils.object2array(this._images) 68 | .reduce((acc, image) => { 69 | acc[image._name] = image.getState(); 70 | return acc; 71 | }, {}); 72 | 73 | } 74 | 75 | 76 | /** 77 | * Helper to tranform current images-store to array 78 | * @return {Array} Array of images 79 | */ 80 | imagesAsArray() { 81 | return Object.keys(this._images).map(function(name) { 82 | return this._images[name]; 83 | }.bind(this)); 84 | } 85 | 86 | 87 | /** 88 | * helper to send new log entry to log observable 89 | * @param {Array} tags 90 | * @param {All} data Data which should be logged 91 | * @return {Void} 92 | */ 93 | log(tags, data) { 94 | this.observables.logs.onNext({ 95 | tags: tags, 96 | data: data, 97 | createdAt: Date.now() 98 | }); 99 | } 100 | 101 | 102 | 103 | /** 104 | * Loads config file and returns config object 105 | * 106 | * 1. Use configFile of this._config - otherwise use node-compose.yml 107 | * 2. Check if file exists at given CWD 108 | * 3. Load and parse yml to object 109 | * 110 | * @return {Object} config 111 | */ 112 | loadConfigFile() { 113 | const composeFileName = this._config.configFile || 'node-compose.yml'; 114 | const composeFilePath = path.resolve(this._config.CWD, composeFileName); 115 | const relativeComposeFilePath = path.relative(this._config.CWD, composeFilePath); 116 | 117 | if (!fs.existsSync(composeFilePath)) { 118 | return this.exit(('File not found: '.red + composeFilePath)); 119 | } 120 | 121 | let config; 122 | try { 123 | this.log(['verbose'], 'Reading file: '.cyan + relativeComposeFilePath); 124 | config = Yaml.safeLoad(fs.readFileSync(composeFilePath, 'utf8')); 125 | } catch (e) { 126 | return this.exit('Invalid yaml file.'.red); 127 | } 128 | 129 | return config || {}; 130 | } 131 | 132 | 133 | 134 | /** 135 | * Iterate trough given config and update/create image instances. 136 | * @param {Object} config parsed config file 137 | * @return {Promise} which resolves when all images are ready. 138 | */ 139 | updateImagesConfig(config) { 140 | 141 | let promises = []; 142 | 143 | Object.keys(config).forEach(function(name) { 144 | 145 | let conf = config[name]; 146 | 147 | if (this._images[name]) { 148 | promises.push(this._images[name].reload(conf)); 149 | } else { 150 | this._initImageAndAddToStore(name, conf); 151 | } 152 | 153 | }.bind(this)); 154 | 155 | return Promise.all(promises); 156 | } 157 | 158 | 159 | 160 | /** 161 | * Returns image from store 162 | * @param {String} name image name 163 | * @return {Object} instance of Image 164 | */ 165 | getImageByName(name) { 166 | if (!this._images[name]) { 167 | throw new Error(`Image '${name}' not found`); 168 | } 169 | 170 | return this._images[name]; 171 | } 172 | 173 | 174 | 175 | /** 176 | * Tries to get all requested images by given names. 177 | * If an image is not found it will log an error. 178 | * @param {Array} nameArr Strings of image names 179 | * @return {Array} Image instances 180 | */ 181 | expectImagesByNameArray(nameArr) { 182 | return (nameArr || []) 183 | .map(function(name) { 184 | try { 185 | return this.getImageByName(name); 186 | } catch (e) { 187 | this.log(['error'], e.message); 188 | } 189 | }.bind(this)) 190 | .filter(Boolean); 191 | } 192 | 193 | 194 | 195 | /** 196 | * Exit the app with given reason (process will be exited) 197 | * @param {String} reason 198 | * @return {Void} 199 | */ 200 | exit(reason) { 201 | this.close(); 202 | this.log(['exit'], reason); 203 | process.exit(1); 204 | } 205 | 206 | close() { 207 | 208 | return Promise.all( 209 | utils.object2array(this._images) 210 | .map((image) => { 211 | return image.close(); 212 | }) 213 | ); 214 | 215 | } 216 | 217 | /** 218 | * Registers some observables to this.observables 219 | * @return {Void} 220 | */ 221 | _defineObservables() { 222 | 223 | let obs = this.observables = {}; 224 | 225 | // Logs 226 | obs.logs = new rx.ReplaySubject(1000); 227 | 228 | // Events 229 | obs.events = this._eventsSubject; 230 | 231 | } 232 | 233 | 234 | 235 | /** 236 | * Loads and registers command handlers for vorpal to this.commands 237 | * @return {Void} 238 | */ 239 | _defineCommands() { 240 | 241 | this.commands = { 242 | ps: requireCommand(this, 'ps'), 243 | reload: requireCommand(this, 'reload'), 244 | start: requireCommand(this, 'start'), 245 | stop: requireCommand(this, 'stop'), 246 | restart: requireCommand(this, 'restart'), 247 | images: requireCommand(this, 'images'), 248 | monitor: requireCommand(this, 'monitor'), 249 | startScript: requireCommand(this, 'start-script'), 250 | stopScript: requireCommand(this, 'stop-script'), 251 | exit: requireCommand(this, 'exit'), 252 | }; 253 | 254 | } 255 | 256 | 257 | 258 | /** 259 | * Create Image instance and add to store. 260 | * @param {String} name Image ID 261 | * @param {Object} config Config of image from parsed config file 262 | * @return {Object} Instance of Image 263 | */ 264 | _initImageAndAddToStore(name, config) { 265 | return this._images[name] = Image(this, name, config); 266 | } 267 | 268 | 269 | } 270 | 271 | 272 | /** 273 | * Init function to create new instance of app 274 | * @param {Object} config will be passed directly to App constructor 275 | * @return {Object} instance of App 276 | */ 277 | module.exports = function Init(config) { 278 | return new App(config); 279 | }; 280 | 281 | 282 | /** 283 | * Helper to load a command-handler, 284 | * bind app to it and wrap its result into a promise 285 | * @param {Object} app instance of app 286 | * @param {String} name name of command-handler-file 287 | * @return {Function} Command function 288 | */ 289 | function requireCommand(app, name) { 290 | 291 | const cmd = require(path.join(__dirname, 'commands', name)); 292 | 293 | return function() { 294 | return Promise.resolve(cmd.apply(app, arguments)) 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /lib/app/lib/image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const which = require('which'); 5 | const spawn = require('child_process').spawn; 6 | const usage = require('pidusage'); // For cpu-monitoring 7 | const rx = require('rx'); 8 | const extend = require('extend'); 9 | const fs = require('fs'); 10 | const utils = require('./utils'); 11 | const Process = require('./process'); 12 | 13 | 14 | class Image { 15 | 16 | 17 | /** 18 | * Constructor 19 | * @param {Object} app App instance of node-compose 20 | * @param {String} name ID of image 21 | * @param {Object} config image-config from node-compose.yml 22 | * @return {Object} self 23 | */ 24 | constructor(app, name, config) { 25 | this._app = app; 26 | this._name = name; 27 | this.state = 'idle'; 28 | 29 | // Adds on&emit as methods to image instance 30 | utils.extendEvents(this, 'events'); 31 | 32 | // connect this.events to apps events 33 | this._connectToAppEvents(app); 34 | 35 | // start initial configuration based on given config object 36 | this._configure(config); 37 | } 38 | 39 | 40 | getState() { 41 | 42 | return { 43 | image: this.imageProcess.getState(), 44 | scripts: utils.object2array( this.scriptProcesses ) 45 | .map((pro) => { 46 | return function(acc) { 47 | acc[pro.name] = pro.getState(); 48 | } 49 | }) 50 | .reduce((acc, fn) => { 51 | fn(acc); 52 | return acc; 53 | }, {}) 54 | } 55 | 56 | } 57 | 58 | 59 | /** 60 | * Logger 61 | * @param {Array} tags Tags to filter logs 62 | * @param {String|Object|Array|Number|Error} data Additional data which should be logged 63 | */ 64 | log(tags, data) { 65 | this.emit('log', { 66 | tags: tags, 67 | data: data 68 | }); 69 | 70 | tags.push('#image', this._name); 71 | return this._app.log(tags, data); 72 | } 73 | 74 | 75 | 76 | /** 77 | * Set one of following states: 78 | * 79 | * - starting 80 | * - stopping 81 | * - idle 82 | * - running 83 | * 84 | * @param {String} state 85 | */ 86 | _setState(state) { 87 | this.state = state; 88 | this.emit('state', { state: state }); 89 | 90 | switch (state) { 91 | case 'starting': 92 | this.log(['log'], 'Starting'); 93 | break; 94 | case 'stopping': 95 | this.log(['log'], 'Stopping'); 96 | break; 97 | case 'running': 98 | this.log(['log'], 'Running'); 99 | break; 100 | case 'idle': 101 | this.log(['log'], 'Idle'); 102 | break; 103 | } 104 | 105 | } 106 | 107 | 108 | 109 | /** 110 | * Prefix image events with 'image' and subscribe stream to given app. 111 | * @param {Object} app Instance of App 112 | * @return {Void} 113 | */ 114 | _connectToAppEvents(app) { 115 | 116 | var sub = this.events 117 | .map(function(e) { 118 | return extend(true, {}, e, { 119 | name: 'image:' + e.name, 120 | data: { 121 | _image: this 122 | } 123 | }); 124 | }.bind(this)) 125 | .subscribe(app._eventsSubject); 126 | 127 | this.on('cleanup', () => { 128 | sub.dispose(); 129 | }); 130 | 131 | } 132 | 133 | 134 | 135 | /** 136 | * Starts the start-process of the image. If its already running, it will log an error. 137 | * @return {Object} Promise which resolves after starting is completed. 138 | */ 139 | start() { 140 | 141 | const that = this; 142 | 143 | this.log(['#verbose'], 'Starting'); 144 | 145 | return this.imageProcess 146 | .start() 147 | .catch((err) => { 148 | 149 | this.log(['#verbose'], 'Cant start'); 150 | 151 | if(err.code === 2) { 152 | return this.log(['error'], 'Already starting'); 153 | } 154 | 155 | if(err.code === 3) { 156 | return this.log(['error'], 'Already running'); 157 | } 158 | 159 | if(err.code === 4) { 160 | return this.log(['error'], 'Cant start during stopping.'); 161 | } 162 | 163 | }); 164 | 165 | } 166 | 167 | 168 | 169 | /** 170 | * Stops the image. If its not running, it will log an error. 171 | * @return {Object} Promise which resolves after stopping is completed. 172 | */ 173 | stop() { 174 | 175 | return this.imageProcess 176 | .stop() 177 | .catch((err) => { 178 | 179 | if(err.code === 1) { 180 | return this.log(['error'], 'Cant stop idle process.'); 181 | } 182 | 183 | if(err.code === 2) { 184 | return this.log(['error'], 'Cant stop during start.'); 185 | } 186 | 187 | if(err.code === 4) { 188 | return this.log(['error'], 'Cant stop. Not running.'); 189 | } 190 | 191 | }); 192 | 193 | } 194 | 195 | 196 | 197 | /** 198 | * Tries to restart the image. 199 | * @return {Object} Promise which resolves on completed. 200 | */ 201 | restart() { 202 | 203 | if (this.state !== 'running') { 204 | this.log(['error'], 'Not running or during starting/stopping'); 205 | return Promise.resolve(); 206 | } 207 | 208 | return this.stop().then(this.start.bind(this)); 209 | } 210 | 211 | 212 | 213 | startScript(name) { 214 | 215 | return this._getScriptProcessByName(name) 216 | .then((script) => { 217 | return script.start(); 218 | }); 219 | 220 | } 221 | 222 | 223 | stopScript(name) { 224 | return this._getScriptProcessByName(name) 225 | .then((script) => { 226 | return script.stop(); 227 | }); 228 | } 229 | 230 | restartScript(name) { 231 | return this._getScriptProcessByName(name) 232 | .then((script) => { 233 | return script.restart(); 234 | }); 235 | } 236 | 237 | 238 | 239 | 240 | _getScriptProcessByName(name) { 241 | if(!this.scriptProcesses[name]) { 242 | return Promise.reject( new Error(`Script ${name} not defined`)); 243 | } 244 | 245 | return Promise.resolve( this.scriptProcesses[name] ); 246 | } 247 | 248 | 249 | /** 250 | * Reloads a new config. If image is running, it will be stopped before. 251 | * @param {Object} config new image config from node-compose.yml 252 | * @return {Object} Promise which resolves on completed. 253 | */ 254 | reload(config) { 255 | 256 | this.log(['#verbose'], 'Reloading'); 257 | 258 | let state = this.getState(); 259 | 260 | // TODO: implement, that scripts will restart after configure 261 | // if they were running at this time. 262 | 263 | // Get all scripts which are currently running 264 | let restartScripts = Object.keys(state.scripts) 265 | .filter((name) => { 266 | return state.scripts[name] === 'running'; 267 | }); 268 | 269 | // Stop running scripts 270 | return Promise.all( 271 | restartScripts 272 | .map(function(name) { 273 | return this.stopScript(name); 274 | }.bind(this)) 275 | ) 276 | 277 | // Stop image if running and reconfigure - start it again if it was running before 278 | .then(function() { 279 | 280 | if (state.image === 'running' || state.image === 'starting') { 281 | return this.stop() 282 | .then(function() { 283 | this._configure(config); 284 | return this.start(); 285 | }.bind(this)); 286 | } else { 287 | this._configure(config); 288 | return Promise.resolve(); 289 | } 290 | 291 | }.bind(this)) 292 | 293 | // Start before running scripts 294 | .then(function() { 295 | return Promise.all( 296 | restartScripts 297 | // Filter before running script to check if script is still available 298 | // after reconfiguration. 299 | // Maybe the package.json has changed and script command was removed. 300 | .filter(function(name) { 301 | return this.scriptProcesses[name]; 302 | }.bind(this)) 303 | // Start all left scripts 304 | .map(function(name) { 305 | return this.startScript(name); 306 | }.bind(this)) 307 | ) 308 | }.bind(this)) 309 | 310 | } 311 | 312 | 313 | 314 | 315 | 316 | /** 317 | * Clean up image and stop all running processes. Important before exit, close, etc. 318 | * @return {Object} Promise which resolves on completed. 319 | */ 320 | clean() { 321 | this.emit('cleanup'); 322 | return this.stop(); 323 | } 324 | 325 | 326 | 327 | /** 328 | * Executed on app-exit 329 | * @return {Object} Promise which resolves on completed. 330 | */ 331 | close() { 332 | 333 | let stoppingProcesses = [this.imageProcess] 334 | .concat( utils.object2array(this.scriptProcesses) ) 335 | .filter((pro) => { 336 | return pro.getState() === 'running'; 337 | }) 338 | .map((pro) => { 339 | return pro.stop(); 340 | }); 341 | 342 | 343 | 344 | return Promise.all(stoppingProcesses) 345 | .then(function() { 346 | this.emit('cleanup'); 347 | }.bind(this)); 348 | } 349 | 350 | 351 | 352 | /** 353 | * Generates an object with information about the image. 354 | * @return {Object} 355 | */ 356 | toObject() { 357 | 358 | let obj = { 359 | id: this._name, 360 | config: this._config, 361 | absPath: this.config.cwd, 362 | relPath: this.config.relBuildPath, 363 | isRunning: this.state === 'running', 364 | isIdle: this.state === 'idle', 365 | isStarting: this.state === 'starting', 366 | isStopping: this.state === 'stopping', 367 | state: this.state, 368 | commands: this.config.commands, 369 | scripts: Object.keys(this.scriptProcesses) 370 | .map(function(key) { 371 | let p = this.scriptProcesses[key]; 372 | return function(acc) { 373 | acc[key] = { 374 | name: p.name, 375 | state: p.getState(), 376 | cmd: p.cmd 377 | } 378 | }; 379 | }.bind(this)) 380 | .reduce((acc, fn) => { 381 | fn(acc); 382 | return acc; 383 | }, {}) 384 | }; 385 | 386 | if(this.pkg) { 387 | obj.pkg = { 388 | name: this.pkg.name, 389 | description: this.pkg.description, 390 | version: this.pkg.version, 391 | } 392 | } 393 | 394 | return obj; 395 | } 396 | 397 | 398 | 399 | /** 400 | * Configures the image based on given config (from yml file) 401 | * 402 | * @param {Object} config yml-image-config 403 | * @return {VOID} 404 | */ 405 | _configure(config) { 406 | 407 | this.emit('configure'); 408 | 409 | this._config = extend({}, config); 410 | this._loadPkgJson(); 411 | 412 | this.config = { 413 | cwd: this._getCwd(), 414 | relBuildPath: this._getRelBuildPath(), 415 | 416 | environment: this._parseEnvironmentToObject(config.environment), 417 | rawEnvironment: config.environment || [], 418 | 419 | commands: { 420 | start: this._getStartCommandString(), 421 | test: null 422 | }, 423 | scripts: Object.keys(this.pkg && this.pkg.scripts || {}) 424 | }; 425 | 426 | 427 | this.imageProcess = this._createImageProcess(); 428 | this.scriptProcesses = this._getScriptProcesses(); 429 | 430 | this.emit('configured'); 431 | } 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | _createImageProcess() { 440 | 441 | let watcherCmd = this._config.nowatch ? [] : [which.sync('nodemon'), '-x']; 442 | 443 | let cmd = watcherCmd.concat( 444 | utils.parseCmdStringToArray( 445 | this.config.commands.start 446 | ) 447 | ).join(' '); 448 | 449 | let env = this.config.environment; 450 | let cwd = this.config.cwd; 451 | 452 | let p = new Process('#image', cmd, cwd, env); 453 | 454 | let state = p.state.subscribe(this._setState.bind(this)); 455 | 456 | let logsSub = p.logs.subscribe(function(e) { 457 | e.tags = e.tags.concat(['#image']); 458 | this.log(e.tags, e.data); 459 | }.bind(this)); 460 | 461 | // Dispose sub on reconfig to avoid unexpected state changes 462 | this.on('configure', () => { 463 | state.dispose(); 464 | logsSub.dispose(); 465 | }); 466 | 467 | return p; 468 | } 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | _createScriptProcess(name, cmd, cwd) { 478 | 479 | cmd = utils.parseCmdStringToArray( cmd ).join(' '); 480 | 481 | let p = new Process(name, cmd, cwd); 482 | 483 | let logsSub = p.logs.subscribe(function(e) { 484 | e.tags = e.tags.concat(['#script']); 485 | this.log(e.tags, e.data); 486 | }.bind(this)); 487 | 488 | let stateSub = p.state.subscribe(function(e) { 489 | this.emit('script-state', e); 490 | }.bind(this)); 491 | 492 | 493 | // Cleanup subs 494 | this.on(['cleanup', 'configure'], () => { 495 | p.stop().catch(e => null); 496 | logsSub.dispose(); 497 | stateSub.dispose(); 498 | }); 499 | 500 | return p; 501 | } 502 | 503 | 504 | 505 | _getScriptProcesses() { 506 | 507 | let processes = {}; 508 | 509 | if(this.config.scripts) { 510 | this.config.scripts 511 | .filter((key) => { 512 | return ['start'].indexOf(key) === -1; 513 | }) 514 | .forEach(function(key) { 515 | let cmd = which.sync('npm') + ' run -s ' + key; 516 | 517 | processes[key] = this._createScriptProcess(key, cmd, this.config.cwd); 518 | 519 | }.bind(this)) 520 | } 521 | 522 | return processes; 523 | } 524 | 525 | 526 | /** 527 | * Transforms an array of env exports to an object based on process.env 528 | * @param {Array} data Array of env-exports 529 | * @return {Object} key-value pairs 530 | * 531 | * @example 532 | * let input = ['NODE_ENV=development', 'PORT=4444'] 533 | * let output = this._parseEnvironmentToIbject(input); 534 | * 535 | * { 536 | * "NODE_ENV":"development", 537 | * "PORT":"4444" 538 | * } 539 | */ 540 | _parseEnvironmentToObject(data) { 541 | return (data || []) 542 | .map((item) => { 543 | return item.split('='); 544 | }) 545 | .reduce((res, item) => { 546 | let key = item.shift(); 547 | res[key] = item.join('='); 548 | return res; 549 | }, Object.create(process.env)); 550 | } 551 | 552 | 553 | 554 | 555 | /** 556 | * Get absolute CWD of image 557 | * @return {String} 558 | */ 559 | _getCwd() { 560 | return path.resolve(this._app._config.CWD, this._config.build); 561 | } 562 | 563 | 564 | 565 | /** 566 | * Get relative CWD of image from origin of node-compose.yml 567 | * @return {String} 568 | */ 569 | _getRelBuildPath() { 570 | return path.relative(this._app._config.CWD, this._getCwd()); 571 | } 572 | 573 | 574 | 575 | /** 576 | * Tries to load package.json from cwd 577 | * @return {Object|undefined} content of package.json or undefined 578 | */ 579 | _loadPkgJson() { 580 | let pkgPath = path.join(this._getCwd(), 'package.json'); 581 | 582 | if (fs.existsSync(pkgPath)) { 583 | this.pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); 584 | } else { 585 | this.pkg = void 0; 586 | } 587 | 588 | return this.pkg; 589 | } 590 | 591 | 592 | 593 | /** 594 | * Returns the start command as string based on 595 | * - command attribute from image-config 596 | * - start-script-command from package.json 597 | * - otherwise: node . 598 | * 599 | * @return {String} Start command string 600 | */ 601 | _getStartCommandString() { 602 | 603 | let indexFile = path.join(this._getCwd(), 'index.js'); 604 | 605 | if (this._config.command) { 606 | return this._config.command; 607 | } else if (this.pkg && this.pkg.scripts && this.pkg.scripts.start) { 608 | return this.pkg.scripts.start; 609 | } else if (fs.existsSync(indexFile)) { 610 | return 'node .'; 611 | } else { 612 | this.log(['error'], 'Cand find a start command'); 613 | return ''; 614 | } 615 | 616 | } 617 | 618 | } 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | /** 627 | * Image init method 628 | * @param {Object} app node-compose app instance 629 | * @param {String} name Name of the image 630 | * @param {Object} config image config from node-compose.yml 631 | * @return {Object} Instance of image 632 | */ 633 | module.exports = function(app, name, config) { 634 | return new Image(app, name, config); 635 | }; 636 | -------------------------------------------------------------------------------- /lib/app/lib/process.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const rx = require('rx'); 4 | const utils = require('./utils'); 5 | const spawn = require('child_process').spawn; 6 | const fs = require('fs'); 7 | const extend = require('extend'); 8 | 9 | 10 | const STATE_CODES = { 11 | 'idle': 1, 12 | 'starting': 2, 13 | 'running': 3, 14 | 'stopping': 4 15 | }; 16 | 17 | 18 | class Process { 19 | 20 | 21 | 22 | 23 | constructor(name, cmd, cwd, env) { 24 | 25 | if(!name) { 26 | throw new Error('Missing name'); 27 | } 28 | 29 | if(!cmd) { 30 | throw new Error('Missing command'); 31 | } 32 | 33 | this.name = name; 34 | this.cmd = cmd; 35 | 36 | 37 | if(cwd && !fs.existsSync(cwd)) { 38 | throw new Error('CWD not found'); 39 | } 40 | 41 | this.cwd = cwd; 42 | this.env = extend(true, Object.create(process.env), env); 43 | 44 | 45 | this.process = null; 46 | 47 | 48 | this._logs = new rx.Subject(); 49 | this._state = new rx.BehaviorSubject('idle'); 50 | 51 | this.state = this._state.filter(Boolean); 52 | 53 | this.logs = this._logs.map((e) => { 54 | e.tags.push('#process', name); 55 | return e; 56 | }); 57 | 58 | } 59 | 60 | 61 | 62 | 63 | 64 | 65 | _log(tags, data) { 66 | this._logs.onNext({ 67 | tags: tags, 68 | data: data 69 | }); 70 | } 71 | 72 | 73 | 74 | 75 | 76 | 77 | _setState(state) { 78 | this._state.onNext(state); 79 | this._log(['log', '#verbose'], `State: ${state}`); 80 | } 81 | 82 | 83 | 84 | 85 | getState() { 86 | return this._state.getValue(); 87 | } 88 | 89 | 90 | 91 | 92 | start() { 93 | 94 | if(this.getState() !== 'idle') { 95 | return Promise.reject( new Error('Not idle', STATE_CODES[this.getState()]) ); 96 | } 97 | 98 | let args = utils.parseCmdStringToArray(this.cmd); 99 | let cmd = args.shift(); 100 | 101 | this._setState('starting'); 102 | 103 | const p = spawn(cmd, args, { 104 | cwd: this.cwd, 105 | env: this.env 106 | }); 107 | 108 | this.process = p; 109 | 110 | p.stdout.on('data', function(payload) { 111 | this._log(['log'], utils.sanitizeProcessDataPayload(payload)); 112 | }.bind(this)); 113 | 114 | p.stderr.on('data', function(payload) { 115 | this._log(['error'], utils.sanitizeProcessDataPayload(payload)); 116 | }.bind(this)); 117 | 118 | p.on('exit', function(code) { 119 | this.process = null; 120 | this._setState('idle'); 121 | }.bind(this)); 122 | 123 | this._setState('running'); 124 | 125 | return Promise.resolve(); 126 | } 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | stop() { 135 | const that = this; 136 | 137 | if(this.getState() !== 'running') { 138 | return Promise.reject( new Error('Not running', STATE_CODES[this.getState()]) ); 139 | } 140 | 141 | this._setState('stopping'); 142 | 143 | return new Promise((resolve, reject) => { 144 | that.process.on('exit', () => { 145 | 146 | /** 147 | * Process-State is set to idle as soon as that.process emits an exit event. 148 | * The handler for that is registered after starting the process 149 | * to avoid state-inconsistency on unexpected process-exits. 150 | */ 151 | 152 | resolve(); 153 | }); 154 | 155 | 156 | utils.killPID(that.process.pid); 157 | }); 158 | 159 | } 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | restart() { 168 | if(this.getState() === 'running') { 169 | return this.stop() 170 | .then(function() { 171 | return this.start(); 172 | }.bind(this)); 173 | } 174 | 175 | return this.stop(); 176 | } 177 | 178 | 179 | 180 | 181 | 182 | } 183 | 184 | 185 | module.exports = Process; 186 | -------------------------------------------------------------------------------- /lib/app/lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Deps 4 | const Table = require('cli-table'); 5 | const colors = require('colors'); 6 | const psTree = require('ps-tree'); 7 | const which = require('which'); 8 | const rx = require('rx'); 9 | 10 | 11 | let utils = module.exports = {}; 12 | 13 | 14 | utils.object2array = function(object) { 15 | return Object.keys(object) 16 | .map((key) => { 17 | return object[key]; 18 | }); 19 | } 20 | 21 | 22 | 23 | /** 24 | * Transform given string|buffer to whitespace-trimmed string 25 | * @param {String|Buffer} data 26 | * @return {String} 27 | */ 28 | utils.sanitizeProcessDataPayload = function(data) { 29 | return (data || '').toString().replace(/^\s+|\s+$/g, ''); 30 | } 31 | 32 | 33 | 34 | 35 | 36 | /** 37 | * Extends given object with two methods to emit and register (on) to events 38 | * @param {Object} obj instance to register event system on 39 | * @param {String} key private attribute which holds rx.Subject 40 | * @return {Void} 41 | */ 42 | utils.extendEvents = function(obj, key) { 43 | 44 | if (obj[key]) { 45 | throw new Error(`Key ${key} already exists.`); 46 | } 47 | 48 | // register subject 49 | obj[key] = new rx.Subject(); 50 | 51 | 52 | // register method to create event listeners 53 | // if no handler is registered, the filterd observable is returned. 54 | // 55 | // If name is an array of strings, the event passes the filter if one of the items 56 | // matches the event name. 57 | obj.on = function(name, handler) { 58 | 59 | let observable = this[key].filter((e) => { 60 | 61 | if(Array.isArray(name)) { 62 | return name.some((name) => { 63 | return e.name === name; 64 | }); 65 | } 66 | 67 | return e.name === name; 68 | }); 69 | 70 | 71 | if(handler) { 72 | let sub = observable.subscribe((e) => { 73 | handler(e.data); 74 | }); 75 | 76 | return function() { 77 | sub.dispose(); 78 | }; 79 | } 80 | 81 | return observable.map((e) => { return e.data; }); 82 | }; 83 | 84 | 85 | // register method to emit events 86 | obj.emit = function(name, data) { 87 | this[key].onNext({ 88 | name: name, 89 | data: data || {} 90 | }); 91 | }; 92 | 93 | }; 94 | 95 | 96 | 97 | /** 98 | * Transforms a given cl-string to array to use it with spawn. 99 | * 100 | * @param {String} cmd cl-string 101 | * @param {Boolean} withWhich if false first command will not be replaced by "whiched" one 102 | * @return {Array} 103 | */ 104 | utils.parseCmdStringToArray = function(cmd, withWhich) { 105 | cmd = cmd.split(' '); 106 | if (withWhich !== false) { 107 | try { 108 | cmd[0] = which.sync(cmd[0]); 109 | } catch(e) { 110 | } 111 | } 112 | return cmd; 113 | }; 114 | 115 | 116 | 117 | /** 118 | * Creates a string representing a table for console output 119 | * @param {Array} headers Strings for header titles 120 | * @param {Array} rows Array of Arrays with row data 121 | * @return {String} 122 | */ 123 | utils.createTableString = function(headers, rows) { 124 | 125 | let table = new Table({ 126 | head: headers.map((item) => { 127 | return (item).cyan 128 | }) 129 | }); 130 | 131 | table.push.apply(table, rows); 132 | return table.toString(); 133 | 134 | }; 135 | 136 | 137 | 138 | /** 139 | * Creates a console view with title, table and footer 140 | * @param {String} title 141 | * @param {Array} headers 142 | * @param {Array} rows 143 | * @param {String} footer 144 | * @return {String} 145 | */ 146 | utils.createTableView = function(title, headers, rows, footer) { 147 | return utils.createDefaultView(title, utils.createTableString(headers, rows), footer); 148 | }; 149 | 150 | 151 | 152 | /** 153 | * Creates a console view with title, content, footer 154 | * @param {String} title 155 | * @param {String} content 156 | * @param {String} footer 157 | * @return {String} 158 | */ 159 | utils.createDefaultView = function(title, content, footer) { 160 | 161 | let res = '\n'; 162 | 163 | if (title) { 164 | res += colors.bold(title) + '\n'; 165 | } 166 | 167 | if (content) { 168 | res += content + '\n\n'; 169 | } 170 | 171 | if (footer) { 172 | res += footer; 173 | } 174 | 175 | return res; 176 | }; 177 | 178 | 179 | 180 | /** 181 | * Kill process by given pid and signal 182 | * @param {Number} pid 183 | * @param {String} signal 184 | * @param {Function} callback 185 | * @return {Void} 186 | */ 187 | utils.killPID = function(pid, signal, callback) { 188 | signal = signal || 'SIGKILL'; 189 | callback = callback || function() {}; 190 | var killTree = true; 191 | if (killTree) { 192 | psTree(pid, function(err, children) { 193 | [pid].concat( 194 | children.map(function(p) { 195 | return p.PID; 196 | }) 197 | ).forEach(function(tpid) { 198 | try { 199 | process.kill(tpid, signal) 200 | } catch (ex) {} 201 | }); 202 | callback(); 203 | }); 204 | } else { 205 | try { 206 | process.kill(pid, signal) 207 | } catch (ex) {} 208 | callback(); 209 | } 210 | }; 211 | -------------------------------------------------------------------------------- /lib/app/monitor/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Third party deps 4 | const Hapi = require('hapi'); 5 | const socketIo = require('socket.io'); 6 | const path = require('path'); 7 | const ctxq = require('ctxq'); 8 | 9 | 10 | // Custom deps 11 | const Session = require('./lib/session'); 12 | const connection = require('./lib/connection'); 13 | 14 | 15 | 16 | 17 | 18 | module.exports = function createServer(app, options) { 19 | 20 | // Init Hapi-Server instance 21 | const server = new Hapi.Server(); 22 | 23 | 24 | 25 | // Add connection 26 | let conn = server.connection({ 27 | host: options.host || '127.0.0.1', 28 | port: options.port || 9669 29 | }); 30 | 31 | 32 | 33 | // Bind socketIO instance to connection listener 34 | const io = socketIo(conn.listener, { 35 | serveClient: true, 36 | log: true 37 | }); 38 | 39 | 40 | 41 | 42 | io.on('connection', (socket) => { 43 | 44 | const session = new Session.SocketIoSession(); 45 | 46 | session.bindSocket(socket, 'ddp'); 47 | 48 | connection(app, session); 49 | 50 | }); 51 | 52 | 53 | 54 | 55 | // Create a promised context-q 56 | return ctxq() 57 | 58 | // Register inert as asset-server 59 | .push(() => server.register(require('inert'))) 60 | .push(() => server.register(require('h2o2'))) 61 | 62 | .push(() => { 63 | 64 | 65 | let assetsRouteHandler; 66 | let externalAssetServer = process.env.ASSET_SERVER; 67 | 68 | // webpack-dev-server 69 | if(externalAssetServer) { 70 | assetsRouteHandler = { 71 | proxy: { 72 | uri: `${externalAssetServer}/{param}`, 73 | passThrough: true, 74 | localStatePassThrough: true 75 | } 76 | }; 77 | } else { 78 | assetsRouteHandler = { 79 | directory: { 80 | path: path.join(__dirname, 'public'), 81 | redirectToSlash: false, 82 | index: false 83 | } 84 | }; 85 | } 86 | 87 | 88 | // Define assets route based on assetsRouteHandler 89 | server.route({ 90 | method: 'GET', 91 | path: '/assets/{param*}', 92 | handler: assetsRouteHandler 93 | }); 94 | 95 | 96 | // Define index route 97 | server.route({ 98 | method: 'GET', 99 | path: '/', 100 | handler: function(req, reply) { 101 | reply.file( path.join(__dirname, 'view', 'index.html'), { confine:false } ); 102 | } 103 | }); 104 | 105 | 106 | 107 | }) 108 | 109 | // Start server 110 | .push(() => server.start()) 111 | .run() 112 | .then( 113 | () => app.log(['log', 'monitor'], `Open monitor at ${server.info.uri}`), 114 | (err) => app.log(['error', 'monitor'], err) 115 | ) 116 | .then(() => server); 117 | 118 | }; 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /lib/app/monitor/knurly.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const jsExternals = [ 6 | 'jquery', 7 | 'angular', 8 | 'angular-ui-router', 9 | 'angular-moment', 10 | 'moment-duration-format', 11 | 'socket.io-client', 12 | 'rx-angular', 13 | 'ramda', 14 | 'css-element-queries/src/ElementQueries', 15 | 'ngstorage' 16 | ]; 17 | 18 | 19 | module.exports = function() { 20 | 21 | this.js([ 22 | { 23 | name: 'monitor-vendor', 24 | src: path.join(__dirname, 'src', 'vendor.js'), 25 | dest: path.join(__dirname, 'public', 'vendor.js'), 26 | require: jsExternals 27 | }, 28 | { 29 | name: 'monitor-app', 30 | src: path.join(__dirname, 'src', 'boot.js'), 31 | dest: path.join(__dirname, 'public', 'boot.js'), 32 | external: jsExternals 33 | } 34 | ]); 35 | 36 | this.scss([ 37 | { 38 | name: 'monitor-css', 39 | src: path.join(__dirname, 'src', 'main.scss'), 40 | dest: path.join(__dirname, 'public', 'main.css'), 41 | sass: { 42 | includePaths: ['node_modules', 'bower_components'], 43 | } 44 | } 45 | ]); 46 | 47 | 48 | this.html({ 49 | name: 'monitor-html', 50 | src: path.join(__dirname, 'src', 'index.pug'), 51 | dest: path.join(__dirname, 'view'), 52 | }); 53 | 54 | this.font({ 55 | name: 'monitor-fonts', 56 | src: [ 57 | './node_modules/font-awesome/fonts/**/*' 58 | ], 59 | dest: path.join(__dirname, 'public', 'fonts') 60 | }); 61 | 62 | }; 63 | -------------------------------------------------------------------------------- /lib/app/monitor/lib/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const rx = require('rx'); 4 | 5 | module.exports = function(app, session) { 6 | 7 | // Transform images array to ddp-collection-add messages 8 | const images = rx.Observable.from(app.imagesAsArray()) 9 | .map((image) => ({ 10 | type: 'collection', 11 | collection: 'images', 12 | method: 'add', 13 | id: image._name, 14 | model: image.toObject() 15 | })); 16 | 17 | // Publish images 18 | session.publish(images); 19 | 20 | 21 | 22 | // Transform image state changes to ddp-collection-update messages 23 | const imagesStateChanges = app.on(['image:state', 'image:configured', 'image:script-state']) 24 | .map(function(d) { 25 | return d._image; 26 | }) 27 | .map((image) => ({ 28 | type: 'collection', 29 | collection: 'images', 30 | method: 'update', 31 | id: image._name, 32 | model: image.toObject() 33 | })); 34 | 35 | // Publish imageStateChanges 36 | session.publish(imagesStateChanges); 37 | 38 | 39 | 40 | // Transform logs to ddp-collection-update messages 41 | const logs = app.observables.logs 42 | .map((log) => ({ 43 | type: 'collection', 44 | collection: 'logs', 45 | method: 'add', 46 | id: log.createdAt, 47 | model: log 48 | })); 49 | 50 | // Publish logs 51 | session.publish(logs); 52 | 53 | 54 | 55 | const exec = session.rx.filter((e) => { 56 | return e.type === 'exec'; 57 | }); 58 | 59 | const commands = exec.filter((e) => { 60 | return e.cmd === 'command'; 61 | }) 62 | .map((e) => { 63 | return e.params; 64 | }); 65 | 66 | 67 | commands.subscribe(function(e) { 68 | app.commands[e.cmd](e.args); 69 | }); 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /lib/app/monitor/lib/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const rx = require('rx'); 4 | 5 | 6 | 7 | class SocketSession { 8 | 9 | constructor() { 10 | this._onCloseHandlers = []; 11 | 12 | this.tx = new rx.Subject(); 13 | this.rx = new rx.Subject(); 14 | 15 | this._onCloseHandlers.push(function(){ 16 | this.tx.onCompleted(); 17 | this.rx.onCompleted(); 18 | }.bind(this)); 19 | } 20 | 21 | publish(observable) { 22 | let sub = observable.subscribe(function(e) { 23 | this.tx.onNext(e); 24 | }.bind(this)); 25 | 26 | this._onCloseHandlers.push(() => { 27 | sub.dispose(); 28 | }); 29 | } 30 | 31 | subscribe(observer) { 32 | let sub = this.rx.subscribe(observer); 33 | 34 | this._onCloseHandlers.push(() => { 35 | sub.dispose(); 36 | }); 37 | } 38 | 39 | close() { 40 | this._onCloseHandlers.forEach((fn) => { 41 | fn(); 42 | }); 43 | } 44 | 45 | } 46 | 47 | 48 | class SocketIoSession extends SocketSession { 49 | 50 | bindSocket(socket, channel) { 51 | const session = this; 52 | 53 | let txSub = session.tx.subscribe((data) => { 54 | socket.emit(channel, data); 55 | }); 56 | 57 | socket.on(channel, (data) => { 58 | session.rx.onNext(data); 59 | }); 60 | 61 | socket.on('disconnect', () => { 62 | txSub.dispose(); 63 | session.close(); 64 | }); 65 | } 66 | 67 | } 68 | 69 | 70 | 71 | module.exports = { 72 | SocketSession: SocketSession, 73 | SocketIoSession: SocketIoSession 74 | }; 75 | -------------------------------------------------------------------------------- /lib/app/monitor/public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platdesign/node-compose/79087833e8558a439f14bd03a45d1144f2715186/lib/app/monitor/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /lib/app/monitor/public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platdesign/node-compose/79087833e8558a439f14bd03a45d1144f2715186/lib/app/monitor/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /lib/app/monitor/public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platdesign/node-compose/79087833e8558a439f14bd03a45d1144f2715186/lib/app/monitor/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /lib/app/monitor/public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platdesign/node-compose/79087833e8558a439f14bd03a45d1144f2715186/lib/app/monitor/public/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /lib/app/monitor/public/monitor.css: -------------------------------------------------------------------------------- 1 | /*! normalize-scss | MIT/GPLv2 License | bit.ly/normalize-scss */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}[hidden],template{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit;font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}h1{font-size:2em;margin:.75em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}svg:not(:root){overflow:hidden}figure{margin:1.5em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}input{overflow:visible}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;display:table;max-width:100%;white-space:normal;color:inherit;padding:0}optgroup{font-weight:700}textarea{overflow:auto}body,html{height:100%}body{font-family:Source Sans Pro,Helvetica Neue,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.4;overflow-x:hidden}*{box-sizing:border-box}h1,h2,h3,h4,h5,h6{margin:0;font-weight:400;padding:0}nav li a{display:block}a{cursor:pointer;color:inherit;text-decoration:none}img{max-width:100%;display:block}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}[ng-click],[ui-sref]{cursor:pointer}[ng-click],[ui-sref],a{user-select:none}.fa,.fa+span{vertical-align:baseline}.fa+span{margin-left:.4em}.d4Ijm7XD{position:relative}.d4Ijm7XD>header{background:#1b7ca4;border-radius:3px 3px 0 0;display:flex;color:hsla(0,0%,100%,.7);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overflow:hidden;align-items:center}.d4Ijm7XD>header .tabs{display:flex}.d4Ijm7XD>header .tabs .tab{padding:6px 12px}.d4Ijm7XD>header .tabs .tab:not(:last-child){border-right:1px solid #3e90b2}.d4Ijm7XD>header .tabs .tab:hover{background:#2280a7}.d4Ijm7XD>header .tabs .tab.active{background:#3e90b2;color:hsla(0,0%,100%,.9)}.d4Ijm7XD>header .tabs .tab .title{font-weight:700}.d4Ijm7XD>header .tabs .tab>*{vertical-align:middle}.d4Ijm7XD>.screen{color:#eee;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;padding:10px;font-family:Courier;font-size:.92em;line-height:1.25;overflow:auto;min-height:400px;max-height:400px;height:400px;background:#232728}.d4Ijm7XD>.screen .item{white-space:pre-wrap}.d4Ijm7XD>.screen .item>.tags:after{content:' '}.d4Ijm7XD>.screen .item.log .tags{color:#0fdc77}.d4Ijm7XD>.screen .item.verbose .tags{color:#d2d22d}.d4Ijm7XD>.screen .item.error .tags{color:#ff4d4d}.d4Ijm7XD>footer{transition:all .3s;background:#1b7ca4;border-radius:0 0 3px 3px;display:flex;color:hsla(0,0%,100%,.7);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;user-select:none}.d4Ijm7XD>footer>.col:not(:last-child){border-right:1px solid rgba(50,55,57,.2)}.d4Ijm7XD>footer select{border:none;appearance:none;background:transparent;padding:0;outline:none;color:inherit;font-size:inherit;font-weight:inherit;vertical-align:middle;display:inline}.d4Ijm7XD>footer label{padding:7px 10px;display:block}.d4Ijm7XD>footer input[type=checkbox]{appearance:none;border:1px solid hsla(0,0%,100%,.7);width:1.05em;height:1.05em;vertical-align:middle;position:relative;outline:none}.d4Ijm7XD>footer input[type=checkbox]:checked:after{content:'';position:absolute;top:1px;left:1px;bottom:1px;right:1px;background:hsla(0,0%,100%,.7)}.d4Ijm7XD>footer span{display:inline-block;vertical-align:middle}.d4Ijm7XD>footer input+span{margin-left:.5em}.d4Ijm7XD>footer span+select{margin-left:.5em;vertical-align:middle}.G4fEHzEw{display:inline-block}.G4fEHzEw-off{color:#d19494}.G4fEHzEw-on{color:#21ca76}.Ns5DbCka{display:flex;padding:5px}.Ns5DbCka button{font-weight:100}.Ns5DbCka button span{letter-spacing:.05em}.Ns5DbCka button+button{margin-left:.5em}.Ns5DbCka[rows]{flex-direction:column}.Ns5DbCka[rows] button{margin:0}.Ns5DbCka[rows] button+button{margin-top:5px}.MOyjqils{display:flex;padding:5px}.MOyjqils[rows]{flex-direction:column}.MOyjqils[rows] .n2sXE18g{margin:0}.MOyjqils[rows] .n2sXE18g+.n2sXE18g{margin-top:5px}.n2sXE18g+.n2sXE18g{margin-left:5px}.n2sXE18g button{width:100%}.n2sXE18g button.state-idle{background:#cbd0d2;color:#6e797d}.n2sXE18g button.state-running{background:#21ca76}.RCgWH2Vt{display:flex;flex-direction:column;height:100%;color:#6e797d}.RCgWH2Vt>header{flex:0 0 auto;justify-content:space-between;align-items:center;display:flex;background:#323739;padding:5px}.RCgWH2Vt>header>nav{display:flex}.RCgWH2Vt>header>nav li{list-style:none}.RCgWH2Vt>header>nav li:not(:last-child){margin-right:.4em}.RCgWH2Vt>header>nav li button{font-size:1.1em}.RCgWH2Vt>header>nav li button>span{font-weight:100}.RCgWH2Vt>header .title{font-size:1.7em;white-space:nowrap}.RCgWH2Vt>header .title>.fat{font-weight:700;color:#51595c}.RCgWH2Vt>header .title>.thin{font-weight:100;color:#7a868a;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.RCgWH2Vt>main{flex:1 1 auto;overflow:auto;overflow-x:hidden;height:100%}.RCgWH2Vt>footer{flex:0 0 auto;background:#323739;color:#6e797d;padding:10px;font-size:.8em;text-align:center}.R4rmOwgT{padding:20px}.R4rmOwgT .table--images{width:100%;margin-bottom:20px}.R4rmOwgT .table--images td{padding:5px;background:#e6e9ea;vertical-align:top}.R4rmOwgT .table--images thead{font-weight:700}.R4rmOwgT .table--images thead td{background:#cbd0d2}.R4rmOwgT .table--images thead td:first-child{border-radius:3px 0 0 0}.R4rmOwgT .table--images thead td:last-child{border-radius:0 3px 0 0}.R4rmOwgT .table--images tbody td.controls,.R4rmOwgT .table--images tbody td.script-controls{padding:0;width:150px}.Qepcd21K{display:flex;height:100%;width:100%}.Qepcd21K>aside{flex:0 0 auto;min-width:250px;background:#cbd0d2;overflow-y:auto;box-shadow:3px 0 5px -3px #bec3c6;padding-top:14px}.Qepcd21K>aside li{background:#fff;margin:1px 0;list-style:none;padding:10px;height:50px;background:#e6e9ea;position:relative;transition:all .3s}.Qepcd21K>aside li.active{background:#f7f8f8}.Qepcd21K>aside li .title{font-weight:700}.Qepcd21K>aside li .status{position:absolute;right:10px;top:10px;text-align:right}.Qepcd21K>aside li .status>.state{font-size:.9em}.Qepcd21K>main{flex:1 1 auto;min-width:1px;padding:5px}.Qepcd21K>main .image-list{padding:20px}.Qepcd21K>main .image-list li{list-style:none;background:#e6e9ea;margin:0 0 10px;padding:10px;border-radius:3px;box-shadow:0 0 2px 0 rgba(50,55,57,.4);position:relative}.Qepcd21K>main .image-list li .title{font-size:1.5em}.Qepcd21K>main .image-list li .controls{position:absolute;right:10px;top:10px}.UyjZzIBR>header{display:flex;margin:10px}.UyjZzIBR>header .details{flex:1 1 auto;background:#e6e9ea;margin-right:10px;padding:10px;border-radius:3px;position:relative}.UyjZzIBR>header .details .version{position:absolute;top:10px;right:10px;font-size:1.4em;opacity:.7}.UyjZzIBR>header .details .title{margin-bottom:.5em}.UyjZzIBR>header .details .title .pkg-name{font-size:.6em;opacity:.5}.UyjZzIBR>header .details .table--config{width:100%}.UyjZzIBR>header .details .table--config td{vertical-align:top;padding:.4em;background:#f7f8f8}.UyjZzIBR>header .details .table--config td:first-child{width:80px;font-weight:700}.UyjZzIBR>header .details .table--config td .env{line-height:1.5}.UyjZzIBR>header .controls{flex:0 0 auto;background:#e6e9ea;border-radius:3px;min-width:180px}.UyjZzIBR>header .controls hr{margin:0;padding:0;border:none;border-bottom:1px solid #cbd0d2}.UyjZzIBR>section.logs{margin:10px}.btn--default{border:none;background:#1b7ca4;font-weight:inherit;outline:none;cursor:pointer;color:hsla(0,0%,100%,.7);padding:.4em .9em .4em 2em;border-radius:3px;position:relative}.btn--default:hover{color:hsla(0,0%,100%,.8)}.btn--default:active{color:#fff;box-shadow:inset 0 0 10px 0 rgba(0,0,0,.2)}.btn--default .fa{left:.8em;top:50%;transform:translateY(-50%);position:absolute}.btn--default:disabled{background:#cbd0d2}/*! 2 | * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot);src:url(fonts/fontawesome-webfont.eot?#iefix&v=4.6.3) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2) format("woff2"),url(fonts/fontawesome-webfont.woff) format("woff"),url(fonts/fontawesome-webfont.ttf) format("truetype"),url(fonts/fontawesome-webfont.svg#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\F000"}.fa-music:before{content:"\F001"}.fa-search:before{content:"\F002"}.fa-envelope-o:before{content:"\F003"}.fa-heart:before{content:"\F004"}.fa-star:before{content:"\F005"}.fa-star-o:before{content:"\F006"}.fa-user:before{content:"\F007"}.fa-film:before{content:"\F008"}.fa-th-large:before{content:"\F009"}.fa-th:before{content:"\F00A"}.fa-th-list:before{content:"\F00B"}.fa-check:before{content:"\F00C"}.fa-close:before,.fa-remove:before,.fa-times:before{content:"\F00D"}.fa-search-plus:before{content:"\F00E"}.fa-search-minus:before{content:"\F010"}.fa-power-off:before{content:"\F011"}.fa-signal:before{content:"\F012"}.fa-cog:before,.fa-gear:before{content:"\F013"}.fa-trash-o:before{content:"\F014"}.fa-home:before{content:"\F015"}.fa-file-o:before{content:"\F016"}.fa-clock-o:before{content:"\F017"}.fa-road:before{content:"\F018"}.fa-download:before{content:"\F019"}.fa-arrow-circle-o-down:before{content:"\F01A"}.fa-arrow-circle-o-up:before{content:"\F01B"}.fa-inbox:before{content:"\F01C"}.fa-play-circle-o:before{content:"\F01D"}.fa-repeat:before,.fa-rotate-right:before{content:"\F01E"}.fa-refresh:before{content:"\F021"}.fa-list-alt:before{content:"\F022"}.fa-lock:before{content:"\F023"}.fa-flag:before{content:"\F024"}.fa-headphones:before{content:"\F025"}.fa-volume-off:before{content:"\F026"}.fa-volume-down:before{content:"\F027"}.fa-volume-up:before{content:"\F028"}.fa-qrcode:before{content:"\F029"}.fa-barcode:before{content:"\F02A"}.fa-tag:before{content:"\F02B"}.fa-tags:before{content:"\F02C"}.fa-book:before{content:"\F02D"}.fa-bookmark:before{content:"\F02E"}.fa-print:before{content:"\F02F"}.fa-camera:before{content:"\F030"}.fa-font:before{content:"\F031"}.fa-bold:before{content:"\F032"}.fa-italic:before{content:"\F033"}.fa-text-height:before{content:"\F034"}.fa-text-width:before{content:"\F035"}.fa-align-left:before{content:"\F036"}.fa-align-center:before{content:"\F037"}.fa-align-right:before{content:"\F038"}.fa-align-justify:before{content:"\F039"}.fa-list:before{content:"\F03A"}.fa-dedent:before,.fa-outdent:before{content:"\F03B"}.fa-indent:before{content:"\F03C"}.fa-video-camera:before{content:"\F03D"}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:"\F03E"}.fa-pencil:before{content:"\F040"}.fa-map-marker:before{content:"\F041"}.fa-adjust:before{content:"\F042"}.fa-tint:before{content:"\F043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\F044"}.fa-share-square-o:before{content:"\F045"}.fa-check-square-o:before{content:"\F046"}.fa-arrows:before{content:"\F047"}.fa-step-backward:before{content:"\F048"}.fa-fast-backward:before{content:"\F049"}.fa-backward:before{content:"\F04A"}.fa-play:before{content:"\F04B"}.fa-pause:before{content:"\F04C"}.fa-stop:before{content:"\F04D"}.fa-forward:before{content:"\F04E"}.fa-fast-forward:before{content:"\F050"}.fa-step-forward:before{content:"\F051"}.fa-eject:before{content:"\F052"}.fa-chevron-left:before{content:"\F053"}.fa-chevron-right:before{content:"\F054"}.fa-plus-circle:before{content:"\F055"}.fa-minus-circle:before{content:"\F056"}.fa-times-circle:before{content:"\F057"}.fa-check-circle:before{content:"\F058"}.fa-question-circle:before{content:"\F059"}.fa-info-circle:before{content:"\F05A"}.fa-crosshairs:before{content:"\F05B"}.fa-times-circle-o:before{content:"\F05C"}.fa-check-circle-o:before{content:"\F05D"}.fa-ban:before{content:"\F05E"}.fa-arrow-left:before{content:"\F060"}.fa-arrow-right:before{content:"\F061"}.fa-arrow-up:before{content:"\F062"}.fa-arrow-down:before{content:"\F063"}.fa-mail-forward:before,.fa-share:before{content:"\F064"}.fa-expand:before{content:"\F065"}.fa-compress:before{content:"\F066"}.fa-plus:before{content:"\F067"}.fa-minus:before{content:"\F068"}.fa-asterisk:before{content:"\F069"}.fa-exclamation-circle:before{content:"\F06A"}.fa-gift:before{content:"\F06B"}.fa-leaf:before{content:"\F06C"}.fa-fire:before{content:"\F06D"}.fa-eye:before{content:"\F06E"}.fa-eye-slash:before{content:"\F070"}.fa-exclamation-triangle:before,.fa-warning:before{content:"\F071"}.fa-plane:before{content:"\F072"}.fa-calendar:before{content:"\F073"}.fa-random:before{content:"\F074"}.fa-comment:before{content:"\F075"}.fa-magnet:before{content:"\F076"}.fa-chevron-up:before{content:"\F077"}.fa-chevron-down:before{content:"\F078"}.fa-retweet:before{content:"\F079"}.fa-shopping-cart:before{content:"\F07A"}.fa-folder:before{content:"\F07B"}.fa-folder-open:before{content:"\F07C"}.fa-arrows-v:before{content:"\F07D"}.fa-arrows-h:before{content:"\F07E"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\F080"}.fa-twitter-square:before{content:"\F081"}.fa-facebook-square:before{content:"\F082"}.fa-camera-retro:before{content:"\F083"}.fa-key:before{content:"\F084"}.fa-cogs:before,.fa-gears:before{content:"\F085"}.fa-comments:before{content:"\F086"}.fa-thumbs-o-up:before{content:"\F087"}.fa-thumbs-o-down:before{content:"\F088"}.fa-star-half:before{content:"\F089"}.fa-heart-o:before{content:"\F08A"}.fa-sign-out:before{content:"\F08B"}.fa-linkedin-square:before{content:"\F08C"}.fa-thumb-tack:before{content:"\F08D"}.fa-external-link:before{content:"\F08E"}.fa-sign-in:before{content:"\F090"}.fa-trophy:before{content:"\F091"}.fa-github-square:before{content:"\F092"}.fa-upload:before{content:"\F093"}.fa-lemon-o:before{content:"\F094"}.fa-phone:before{content:"\F095"}.fa-square-o:before{content:"\F096"}.fa-bookmark-o:before{content:"\F097"}.fa-phone-square:before{content:"\F098"}.fa-twitter:before{content:"\F099"}.fa-facebook-f:before,.fa-facebook:before{content:"\F09A"}.fa-github:before{content:"\F09B"}.fa-unlock:before{content:"\F09C"}.fa-credit-card:before{content:"\F09D"}.fa-feed:before,.fa-rss:before{content:"\F09E"}.fa-hdd-o:before{content:"\F0A0"}.fa-bullhorn:before{content:"\F0A1"}.fa-bell:before{content:"\F0F3"}.fa-certificate:before{content:"\F0A3"}.fa-hand-o-right:before{content:"\F0A4"}.fa-hand-o-left:before{content:"\F0A5"}.fa-hand-o-up:before{content:"\F0A6"}.fa-hand-o-down:before{content:"\F0A7"}.fa-arrow-circle-left:before{content:"\F0A8"}.fa-arrow-circle-right:before{content:"\F0A9"}.fa-arrow-circle-up:before{content:"\F0AA"}.fa-arrow-circle-down:before{content:"\F0AB"}.fa-globe:before{content:"\F0AC"}.fa-wrench:before{content:"\F0AD"}.fa-tasks:before{content:"\F0AE"}.fa-filter:before{content:"\F0B0"}.fa-briefcase:before{content:"\F0B1"}.fa-arrows-alt:before{content:"\F0B2"}.fa-group:before,.fa-users:before{content:"\F0C0"}.fa-chain:before,.fa-link:before{content:"\F0C1"}.fa-cloud:before{content:"\F0C2"}.fa-flask:before{content:"\F0C3"}.fa-cut:before,.fa-scissors:before{content:"\F0C4"}.fa-copy:before,.fa-files-o:before{content:"\F0C5"}.fa-paperclip:before{content:"\F0C6"}.fa-floppy-o:before,.fa-save:before{content:"\F0C7"}.fa-square:before{content:"\F0C8"}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:"\F0C9"}.fa-list-ul:before{content:"\F0CA"}.fa-list-ol:before{content:"\F0CB"}.fa-strikethrough:before{content:"\F0CC"}.fa-underline:before{content:"\F0CD"}.fa-table:before{content:"\F0CE"}.fa-magic:before{content:"\F0D0"}.fa-truck:before{content:"\F0D1"}.fa-pinterest:before{content:"\F0D2"}.fa-pinterest-square:before{content:"\F0D3"}.fa-google-plus-square:before{content:"\F0D4"}.fa-google-plus:before{content:"\F0D5"}.fa-money:before{content:"\F0D6"}.fa-caret-down:before{content:"\F0D7"}.fa-caret-up:before{content:"\F0D8"}.fa-caret-left:before{content:"\F0D9"}.fa-caret-right:before{content:"\F0DA"}.fa-columns:before{content:"\F0DB"}.fa-sort:before,.fa-unsorted:before{content:"\F0DC"}.fa-sort-desc:before,.fa-sort-down:before{content:"\F0DD"}.fa-sort-asc:before,.fa-sort-up:before{content:"\F0DE"}.fa-envelope:before{content:"\F0E0"}.fa-linkedin:before{content:"\F0E1"}.fa-rotate-left:before,.fa-undo:before{content:"\F0E2"}.fa-gavel:before,.fa-legal:before{content:"\F0E3"}.fa-dashboard:before,.fa-tachometer:before{content:"\F0E4"}.fa-comment-o:before{content:"\F0E5"}.fa-comments-o:before{content:"\F0E6"}.fa-bolt:before,.fa-flash:before{content:"\F0E7"}.fa-sitemap:before{content:"\F0E8"}.fa-umbrella:before{content:"\F0E9"}.fa-clipboard:before,.fa-paste:before{content:"\F0EA"}.fa-lightbulb-o:before{content:"\F0EB"}.fa-exchange:before{content:"\F0EC"}.fa-cloud-download:before{content:"\F0ED"}.fa-cloud-upload:before{content:"\F0EE"}.fa-user-md:before{content:"\F0F0"}.fa-stethoscope:before{content:"\F0F1"}.fa-suitcase:before{content:"\F0F2"}.fa-bell-o:before{content:"\F0A2"}.fa-coffee:before{content:"\F0F4"}.fa-cutlery:before{content:"\F0F5"}.fa-file-text-o:before{content:"\F0F6"}.fa-building-o:before{content:"\F0F7"}.fa-hospital-o:before{content:"\F0F8"}.fa-ambulance:before{content:"\F0F9"}.fa-medkit:before{content:"\F0FA"}.fa-fighter-jet:before{content:"\F0FB"}.fa-beer:before{content:"\F0FC"}.fa-h-square:before{content:"\F0FD"}.fa-plus-square:before{content:"\F0FE"}.fa-angle-double-left:before{content:"\F100"}.fa-angle-double-right:before{content:"\F101"}.fa-angle-double-up:before{content:"\F102"}.fa-angle-double-down:before{content:"\F103"}.fa-angle-left:before{content:"\F104"}.fa-angle-right:before{content:"\F105"}.fa-angle-up:before{content:"\F106"}.fa-angle-down:before{content:"\F107"}.fa-desktop:before{content:"\F108"}.fa-laptop:before{content:"\F109"}.fa-tablet:before{content:"\F10A"}.fa-mobile-phone:before,.fa-mobile:before{content:"\F10B"}.fa-circle-o:before{content:"\F10C"}.fa-quote-left:before{content:"\F10D"}.fa-quote-right:before{content:"\F10E"}.fa-spinner:before{content:"\F110"}.fa-circle:before{content:"\F111"}.fa-mail-reply:before,.fa-reply:before{content:"\F112"}.fa-github-alt:before{content:"\F113"}.fa-folder-o:before{content:"\F114"}.fa-folder-open-o:before{content:"\F115"}.fa-smile-o:before{content:"\F118"}.fa-frown-o:before{content:"\F119"}.fa-meh-o:before{content:"\F11A"}.fa-gamepad:before{content:"\F11B"}.fa-keyboard-o:before{content:"\F11C"}.fa-flag-o:before{content:"\F11D"}.fa-flag-checkered:before{content:"\F11E"}.fa-terminal:before{content:"\F120"}.fa-code:before{content:"\F121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\F122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\F123"}.fa-location-arrow:before{content:"\F124"}.fa-crop:before{content:"\F125"}.fa-code-fork:before{content:"\F126"}.fa-chain-broken:before,.fa-unlink:before{content:"\F127"}.fa-question:before{content:"\F128"}.fa-info:before{content:"\F129"}.fa-exclamation:before{content:"\F12A"}.fa-superscript:before{content:"\F12B"}.fa-subscript:before{content:"\F12C"}.fa-eraser:before{content:"\F12D"}.fa-puzzle-piece:before{content:"\F12E"}.fa-microphone:before{content:"\F130"}.fa-microphone-slash:before{content:"\F131"}.fa-shield:before{content:"\F132"}.fa-calendar-o:before{content:"\F133"}.fa-fire-extinguisher:before{content:"\F134"}.fa-rocket:before{content:"\F135"}.fa-maxcdn:before{content:"\F136"}.fa-chevron-circle-left:before{content:"\F137"}.fa-chevron-circle-right:before{content:"\F138"}.fa-chevron-circle-up:before{content:"\F139"}.fa-chevron-circle-down:before{content:"\F13A"}.fa-html5:before{content:"\F13B"}.fa-css3:before{content:"\F13C"}.fa-anchor:before{content:"\F13D"}.fa-unlock-alt:before{content:"\F13E"}.fa-bullseye:before{content:"\F140"}.fa-ellipsis-h:before{content:"\F141"}.fa-ellipsis-v:before{content:"\F142"}.fa-rss-square:before{content:"\F143"}.fa-play-circle:before{content:"\F144"}.fa-ticket:before{content:"\F145"}.fa-minus-square:before{content:"\F146"}.fa-minus-square-o:before{content:"\F147"}.fa-level-up:before{content:"\F148"}.fa-level-down:before{content:"\F149"}.fa-check-square:before{content:"\F14A"}.fa-pencil-square:before{content:"\F14B"}.fa-external-link-square:before{content:"\F14C"}.fa-share-square:before{content:"\F14D"}.fa-compass:before{content:"\F14E"}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:"\F150"}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:"\F151"}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:"\F152"}.fa-eur:before,.fa-euro:before{content:"\F153"}.fa-gbp:before{content:"\F154"}.fa-dollar:before,.fa-usd:before{content:"\F155"}.fa-inr:before,.fa-rupee:before{content:"\F156"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:"\F157"}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:"\F158"}.fa-krw:before,.fa-won:before{content:"\F159"}.fa-bitcoin:before,.fa-btc:before{content:"\F15A"}.fa-file:before{content:"\F15B"}.fa-file-text:before{content:"\F15C"}.fa-sort-alpha-asc:before{content:"\F15D"}.fa-sort-alpha-desc:before{content:"\F15E"}.fa-sort-amount-asc:before{content:"\F160"}.fa-sort-amount-desc:before{content:"\F161"}.fa-sort-numeric-asc:before{content:"\F162"}.fa-sort-numeric-desc:before{content:"\F163"}.fa-thumbs-up:before{content:"\F164"}.fa-thumbs-down:before{content:"\F165"}.fa-youtube-square:before{content:"\F166"}.fa-youtube:before{content:"\F167"}.fa-xing:before{content:"\F168"}.fa-xing-square:before{content:"\F169"}.fa-youtube-play:before{content:"\F16A"}.fa-dropbox:before{content:"\F16B"}.fa-stack-overflow:before{content:"\F16C"}.fa-instagram:before{content:"\F16D"}.fa-flickr:before{content:"\F16E"}.fa-adn:before{content:"\F170"}.fa-bitbucket:before{content:"\F171"}.fa-bitbucket-square:before{content:"\F172"}.fa-tumblr:before{content:"\F173"}.fa-tumblr-square:before{content:"\F174"}.fa-long-arrow-down:before{content:"\F175"}.fa-long-arrow-up:before{content:"\F176"}.fa-long-arrow-left:before{content:"\F177"}.fa-long-arrow-right:before{content:"\F178"}.fa-apple:before{content:"\F179"}.fa-windows:before{content:"\F17A"}.fa-android:before{content:"\F17B"}.fa-linux:before{content:"\F17C"}.fa-dribbble:before{content:"\F17D"}.fa-skype:before{content:"\F17E"}.fa-foursquare:before{content:"\F180"}.fa-trello:before{content:"\F181"}.fa-female:before{content:"\F182"}.fa-male:before{content:"\F183"}.fa-gittip:before,.fa-gratipay:before{content:"\F184"}.fa-sun-o:before{content:"\F185"}.fa-moon-o:before{content:"\F186"}.fa-archive:before{content:"\F187"}.fa-bug:before{content:"\F188"}.fa-vk:before{content:"\F189"}.fa-weibo:before{content:"\F18A"}.fa-renren:before{content:"\F18B"}.fa-pagelines:before{content:"\F18C"}.fa-stack-exchange:before{content:"\F18D"}.fa-arrow-circle-o-right:before{content:"\F18E"}.fa-arrow-circle-o-left:before{content:"\F190"}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:"\F191"}.fa-dot-circle-o:before{content:"\F192"}.fa-wheelchair:before{content:"\F193"}.fa-vimeo-square:before{content:"\F194"}.fa-try:before,.fa-turkish-lira:before{content:"\F195"}.fa-plus-square-o:before{content:"\F196"}.fa-space-shuttle:before{content:"\F197"}.fa-slack:before{content:"\F198"}.fa-envelope-square:before{content:"\F199"}.fa-wordpress:before{content:"\F19A"}.fa-openid:before{content:"\F19B"}.fa-bank:before,.fa-institution:before,.fa-university:before{content:"\F19C"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\F19D"}.fa-yahoo:before{content:"\F19E"}.fa-google:before{content:"\F1A0"}.fa-reddit:before{content:"\F1A1"}.fa-reddit-square:before{content:"\F1A2"}.fa-stumbleupon-circle:before{content:"\F1A3"}.fa-stumbleupon:before{content:"\F1A4"}.fa-delicious:before{content:"\F1A5"}.fa-digg:before{content:"\F1A6"}.fa-pied-piper-pp:before{content:"\F1A7"}.fa-pied-piper-alt:before{content:"\F1A8"}.fa-drupal:before{content:"\F1A9"}.fa-joomla:before{content:"\F1AA"}.fa-language:before{content:"\F1AB"}.fa-fax:before{content:"\F1AC"}.fa-building:before{content:"\F1AD"}.fa-child:before{content:"\F1AE"}.fa-paw:before{content:"\F1B0"}.fa-spoon:before{content:"\F1B1"}.fa-cube:before{content:"\F1B2"}.fa-cubes:before{content:"\F1B3"}.fa-behance:before{content:"\F1B4"}.fa-behance-square:before{content:"\F1B5"}.fa-steam:before{content:"\F1B6"}.fa-steam-square:before{content:"\F1B7"}.fa-recycle:before{content:"\F1B8"}.fa-automobile:before,.fa-car:before{content:"\F1B9"}.fa-cab:before,.fa-taxi:before{content:"\F1BA"}.fa-tree:before{content:"\F1BB"}.fa-spotify:before{content:"\F1BC"}.fa-deviantart:before{content:"\F1BD"}.fa-soundcloud:before{content:"\F1BE"}.fa-database:before{content:"\F1C0"}.fa-file-pdf-o:before{content:"\F1C1"}.fa-file-word-o:before{content:"\F1C2"}.fa-file-excel-o:before{content:"\F1C3"}.fa-file-powerpoint-o:before{content:"\F1C4"}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:"\F1C5"}.fa-file-archive-o:before,.fa-file-zip-o:before{content:"\F1C6"}.fa-file-audio-o:before,.fa-file-sound-o:before{content:"\F1C7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\F1C8"}.fa-file-code-o:before{content:"\F1C9"}.fa-vine:before{content:"\F1CA"}.fa-codepen:before{content:"\F1CB"}.fa-jsfiddle:before{content:"\F1CC"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:"\F1CD"}.fa-circle-o-notch:before{content:"\F1CE"}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:"\F1D0"}.fa-empire:before,.fa-ge:before{content:"\F1D1"}.fa-git-square:before{content:"\F1D2"}.fa-git:before{content:"\F1D3"}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:"\F1D4"}.fa-tencent-weibo:before{content:"\F1D5"}.fa-qq:before{content:"\F1D6"}.fa-wechat:before,.fa-weixin:before{content:"\F1D7"}.fa-paper-plane:before,.fa-send:before{content:"\F1D8"}.fa-paper-plane-o:before,.fa-send-o:before{content:"\F1D9"}.fa-history:before{content:"\F1DA"}.fa-circle-thin:before{content:"\F1DB"}.fa-header:before{content:"\F1DC"}.fa-paragraph:before{content:"\F1DD"}.fa-sliders:before{content:"\F1DE"}.fa-share-alt:before{content:"\F1E0"}.fa-share-alt-square:before{content:"\F1E1"}.fa-bomb:before{content:"\F1E2"}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:"\F1E3"}.fa-tty:before{content:"\F1E4"}.fa-binoculars:before{content:"\F1E5"}.fa-plug:before{content:"\F1E6"}.fa-slideshare:before{content:"\F1E7"}.fa-twitch:before{content:"\F1E8"}.fa-yelp:before{content:"\F1E9"}.fa-newspaper-o:before{content:"\F1EA"}.fa-wifi:before{content:"\F1EB"}.fa-calculator:before{content:"\F1EC"}.fa-paypal:before{content:"\F1ED"}.fa-google-wallet:before{content:"\F1EE"}.fa-cc-visa:before{content:"\F1F0"}.fa-cc-mastercard:before{content:"\F1F1"}.fa-cc-discover:before{content:"\F1F2"}.fa-cc-amex:before{content:"\F1F3"}.fa-cc-paypal:before{content:"\F1F4"}.fa-cc-stripe:before{content:"\F1F5"}.fa-bell-slash:before{content:"\F1F6"}.fa-bell-slash-o:before{content:"\F1F7"}.fa-trash:before{content:"\F1F8"}.fa-copyright:before{content:"\F1F9"}.fa-at:before{content:"\F1FA"}.fa-eyedropper:before{content:"\F1FB"}.fa-paint-brush:before{content:"\F1FC"}.fa-birthday-cake:before{content:"\F1FD"}.fa-area-chart:before{content:"\F1FE"}.fa-pie-chart:before{content:"\F200"}.fa-line-chart:before{content:"\F201"}.fa-lastfm:before{content:"\F202"}.fa-lastfm-square:before{content:"\F203"}.fa-toggle-off:before{content:"\F204"}.fa-toggle-on:before{content:"\F205"}.fa-bicycle:before{content:"\F206"}.fa-bus:before{content:"\F207"}.fa-ioxhost:before{content:"\F208"}.fa-angellist:before{content:"\F209"}.fa-cc:before{content:"\F20A"}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:"\F20B"}.fa-meanpath:before{content:"\F20C"}.fa-buysellads:before{content:"\F20D"}.fa-connectdevelop:before{content:"\F20E"}.fa-dashcube:before{content:"\F210"}.fa-forumbee:before{content:"\F211"}.fa-leanpub:before{content:"\F212"}.fa-sellsy:before{content:"\F213"}.fa-shirtsinbulk:before{content:"\F214"}.fa-simplybuilt:before{content:"\F215"}.fa-skyatlas:before{content:"\F216"}.fa-cart-plus:before{content:"\F217"}.fa-cart-arrow-down:before{content:"\F218"}.fa-diamond:before{content:"\F219"}.fa-ship:before{content:"\F21A"}.fa-user-secret:before{content:"\F21B"}.fa-motorcycle:before{content:"\F21C"}.fa-street-view:before{content:"\F21D"}.fa-heartbeat:before{content:"\F21E"}.fa-venus:before{content:"\F221"}.fa-mars:before{content:"\F222"}.fa-mercury:before{content:"\F223"}.fa-intersex:before,.fa-transgender:before{content:"\F224"}.fa-transgender-alt:before{content:"\F225"}.fa-venus-double:before{content:"\F226"}.fa-mars-double:before{content:"\F227"}.fa-venus-mars:before{content:"\F228"}.fa-mars-stroke:before{content:"\F229"}.fa-mars-stroke-v:before{content:"\F22A"}.fa-mars-stroke-h:before{content:"\F22B"}.fa-neuter:before{content:"\F22C"}.fa-genderless:before{content:"\F22D"}.fa-facebook-official:before{content:"\F230"}.fa-pinterest-p:before{content:"\F231"}.fa-whatsapp:before{content:"\F232"}.fa-server:before{content:"\F233"}.fa-user-plus:before{content:"\F234"}.fa-user-times:before{content:"\F235"}.fa-bed:before,.fa-hotel:before{content:"\F236"}.fa-viacoin:before{content:"\F237"}.fa-train:before{content:"\F238"}.fa-subway:before{content:"\F239"}.fa-medium:before{content:"\F23A"}.fa-y-combinator:before,.fa-yc:before{content:"\F23B"}.fa-optin-monster:before{content:"\F23C"}.fa-opencart:before{content:"\F23D"}.fa-expeditedssl:before{content:"\F23E"}.fa-battery-4:before,.fa-battery-full:before{content:"\F240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\F241"}.fa-battery-2:before,.fa-battery-half:before{content:"\F242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\F243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\F244"}.fa-mouse-pointer:before{content:"\F245"}.fa-i-cursor:before{content:"\F246"}.fa-object-group:before{content:"\F247"}.fa-object-ungroup:before{content:"\F248"}.fa-sticky-note:before{content:"\F249"}.fa-sticky-note-o:before{content:"\F24A"}.fa-cc-jcb:before{content:"\F24B"}.fa-cc-diners-club:before{content:"\F24C"}.fa-clone:before{content:"\F24D"}.fa-balance-scale:before{content:"\F24E"}.fa-hourglass-o:before{content:"\F250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\F251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\F252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\F253"}.fa-hourglass:before{content:"\F254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\F255"}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:"\F256"}.fa-hand-scissors-o:before{content:"\F257"}.fa-hand-lizard-o:before{content:"\F258"}.fa-hand-spock-o:before{content:"\F259"}.fa-hand-pointer-o:before{content:"\F25A"}.fa-hand-peace-o:before{content:"\F25B"}.fa-trademark:before{content:"\F25C"}.fa-registered:before{content:"\F25D"}.fa-creative-commons:before{content:"\F25E"}.fa-gg:before{content:"\F260"}.fa-gg-circle:before{content:"\F261"}.fa-tripadvisor:before{content:"\F262"}.fa-odnoklassniki:before{content:"\F263"}.fa-odnoklassniki-square:before{content:"\F264"}.fa-get-pocket:before{content:"\F265"}.fa-wikipedia-w:before{content:"\F266"}.fa-safari:before{content:"\F267"}.fa-chrome:before{content:"\F268"}.fa-firefox:before{content:"\F269"}.fa-opera:before{content:"\F26A"}.fa-internet-explorer:before{content:"\F26B"}.fa-television:before,.fa-tv:before{content:"\F26C"}.fa-contao:before{content:"\F26D"}.fa-500px:before{content:"\F26E"}.fa-amazon:before{content:"\F270"}.fa-calendar-plus-o:before{content:"\F271"}.fa-calendar-minus-o:before{content:"\F272"}.fa-calendar-times-o:before{content:"\F273"}.fa-calendar-check-o:before{content:"\F274"}.fa-industry:before{content:"\F275"}.fa-map-pin:before{content:"\F276"}.fa-map-signs:before{content:"\F277"}.fa-map-o:before{content:"\F278"}.fa-map:before{content:"\F279"}.fa-commenting:before{content:"\F27A"}.fa-commenting-o:before{content:"\F27B"}.fa-houzz:before{content:"\F27C"}.fa-vimeo:before{content:"\F27D"}.fa-black-tie:before{content:"\F27E"}.fa-fonticons:before{content:"\F280"}.fa-reddit-alien:before{content:"\F281"}.fa-edge:before{content:"\F282"}.fa-credit-card-alt:before{content:"\F283"}.fa-codiepie:before{content:"\F284"}.fa-modx:before{content:"\F285"}.fa-fort-awesome:before{content:"\F286"}.fa-usb:before{content:"\F287"}.fa-product-hunt:before{content:"\F288"}.fa-mixcloud:before{content:"\F289"}.fa-scribd:before{content:"\F28A"}.fa-pause-circle:before{content:"\F28B"}.fa-pause-circle-o:before{content:"\F28C"}.fa-stop-circle:before{content:"\F28D"}.fa-stop-circle-o:before{content:"\F28E"}.fa-shopping-bag:before{content:"\F290"}.fa-shopping-basket:before{content:"\F291"}.fa-hashtag:before{content:"\F292"}.fa-bluetooth:before{content:"\F293"}.fa-bluetooth-b:before{content:"\F294"}.fa-percent:before{content:"\F295"}.fa-gitlab:before{content:"\F296"}.fa-wpbeginner:before{content:"\F297"}.fa-wpforms:before{content:"\F298"}.fa-envira:before{content:"\F299"}.fa-universal-access:before{content:"\F29A"}.fa-wheelchair-alt:before{content:"\F29B"}.fa-question-circle-o:before{content:"\F29C"}.fa-blind:before{content:"\F29D"}.fa-audio-description:before{content:"\F29E"}.fa-volume-control-phone:before{content:"\F2A0"}.fa-braille:before{content:"\F2A1"}.fa-assistive-listening-systems:before{content:"\F2A2"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:"\F2A3"}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:"\F2A4"}.fa-glide:before{content:"\F2A5"}.fa-glide-g:before{content:"\F2A6"}.fa-sign-language:before,.fa-signing:before{content:"\F2A7"}.fa-low-vision:before{content:"\F2A8"}.fa-viadeo:before{content:"\F2A9"}.fa-viadeo-square:before{content:"\F2AA"}.fa-snapchat:before{content:"\F2AB"}.fa-snapchat-ghost:before{content:"\F2AC"}.fa-snapchat-square:before{content:"\F2AD"}.fa-pied-piper:before{content:"\F2AE"}.fa-first-order:before{content:"\F2B0"}.fa-yoast:before{content:"\F2B1"}.fa-themeisle:before{content:"\F2B2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\F2B3"}.fa-fa:before,.fa-font-awesome:before{content:"\F2B4"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/image-controls/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var angular = require('angular'); 5 | 6 | 7 | var mod = module.exports = angular.module('app.directives.image-controls', []); 8 | 9 | 10 | 11 | mod.directive('imageControls', function() { 12 | return { 13 | restrict: 'E', 14 | template: require('./template.pug'), 15 | replace: true, 16 | scope: { 17 | image: '=' 18 | } 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/image-controls/styles.scss: -------------------------------------------------------------------------------- 1 | .Ns5DbCka { 2 | display: flex; 3 | padding: 5px; 4 | 5 | button { 6 | font-weight: 100; 7 | span { 8 | letter-spacing: .05em; 9 | } 10 | } 11 | 12 | button + button { 13 | margin-left: .5em; 14 | } 15 | 16 | &[rows] { 17 | flex-direction: column; 18 | button { 19 | margin: 0; 20 | & + button { 21 | margin-top: 5px; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/image-controls/template.pug: -------------------------------------------------------------------------------- 1 | .Ns5DbCka 2 | 3 | button.btn--default( 4 | ng-disabled="true", 5 | ng-if="image.isStarting || image.isStopping" 6 | ) 7 | .fa.fa-refresh.fa-spin 8 | span {{ image.isStopping ? 'Stopping' : 'Starting' }} 9 | 10 | button.btn--default( 11 | ng-click="$event.stopPropagation(); $root.$cmd.exec('restart', { image: image.id })", 12 | ng-if="image.isRunning" 13 | ) 14 | .fa.fa-refresh 15 | span Restart 16 | 17 | 18 | button.btn--default( 19 | ng-click="$event.stopPropagation(); $root.$cmd.exec('start', { image: image.id })", 20 | ng-if="image.isIdle" 21 | ) 22 | .fa.fa-play 23 | span Start 24 | 25 | 26 | button.btn--default( 27 | ng-click="$event.stopPropagation(); $root.$cmd.exec('stop', { image: image.id })", 28 | ng-if="image.isRunning" 29 | ) 30 | .fa.fa-stop 31 | span Stop 32 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/image-script-controls/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | 5 | var angular = require('angular'); 6 | 7 | 8 | var mod = module.exports = angular.module('app.directives.image-script-controls', []); 9 | 10 | 11 | 12 | mod.directive('imageScriptControls', function() { 13 | return { 14 | restrict: 'E', 15 | template: require('./template.pug'), 16 | replace: true, 17 | scope: { 18 | image: '=' 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/image-script-controls/styles.scss: -------------------------------------------------------------------------------- 1 | .MOyjqils { 2 | display: flex; 3 | padding: 5px; 4 | 5 | &[rows] { 6 | flex-direction: column; 7 | .n2sXE18g { 8 | margin: 0; 9 | & + .n2sXE18g { 10 | margin-top: 5px; 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/image-script-controls/template.pug: -------------------------------------------------------------------------------- 1 | .MOyjqils 2 | script-button( 3 | image="image", 4 | script="script", 5 | ng-repeat="script in image.scripts" 6 | ) 7 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/image-status/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | 5 | var angular = require('angular'); 6 | 7 | 8 | var mod = module.exports = angular.module('app.directives.image-status', []); 9 | 10 | 11 | 12 | mod.directive('imageStatus', function() { 13 | return { 14 | restrict: 'E', 15 | template: require('./template.pug'), 16 | replace: true, 17 | scope: { 18 | image: '=' 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/image-status/styles.scss: -------------------------------------------------------------------------------- 1 | .G4fEHzEw { 2 | display: inline-block; 3 | 4 | &-off { 5 | color: $color-off; 6 | } 7 | &-on { 8 | color: $color-on; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/image-status/template.pug: -------------------------------------------------------------------------------- 1 | .fa.G4fEHzEw 2 | .G4fEHzEw-on.fa.fa-heart(ng-if="image.isRunning") 3 | .G4fEHzEw-off.fa.fa-heart-o(ng-if="!image.isRunning") 4 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/log-monitor/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var angular = require('angular'); 5 | require('rx-angular'); 6 | 7 | var mod = module.exports = angular.module('app.directives.log-monitor', [ 8 | require('ngstorage').name, 9 | 'rx' 10 | ]); 11 | 12 | 13 | 14 | 15 | 16 | 17 | mod.directive('logMonitor', function($localStorage) { 18 | return { 19 | restrict: 'E', 20 | template: require('./template.jade'), 21 | replace: true, 22 | scope: { 23 | config: '=' 24 | }, 25 | controller: function($scope, $data, $element, rx, observeOnScope, $sce, ansi2html) { 26 | 27 | $scope.$storage = $localStorage; 28 | 29 | if(!$scope.$storage.monitorConfig) { 30 | $scope.$storage.monitorConfig = { 31 | limit: 500, 32 | verbose: false 33 | }; 34 | } 35 | 36 | $scope.monitorConfig = $scope.$storage.monitorConfig; 37 | 38 | 39 | 40 | // Filterobject (every, some, etc) 41 | const filter$ = rx.Observable.merge( 42 | $scope.$createObservableFunction('selectTab').pluck('filter'), 43 | rx.Observable.of({}) 44 | ); 45 | 46 | 47 | 48 | // Monitor config object (limit, verbose) 49 | const monitorConfig$ = rx.Observable.combineLatest( 50 | observeOnScope($scope, 'monitorConfig.verbose').pluck('newValue'), 51 | observeOnScope($scope, 'monitorConfig.limit').pluck('newValue'), 52 | (verbose, limit) => ({ 53 | verbose, 54 | limit 55 | }) 56 | ); 57 | 58 | 59 | // Screen element 60 | const $screen = $element.find('.screen'); 61 | 62 | 63 | // Scroll-Status 64 | // Important to detect if user has scrolled upwards. (see scrollBottom$) 65 | const scolledDown$ = rx.Observable.merge( 66 | rx.Observable 67 | .fromEvent($screen, 'scroll') 68 | .pluck('target') 69 | .map((e) => e.scrollTop + e.offsetHeight === e.scrollHeight), 70 | rx.Observable.of(true) 71 | ); 72 | 73 | 74 | 75 | const scrollBottomSubject$ = new rx.Subject(); 76 | 77 | 78 | // If user has NOT scrolled upwards in screen every new item should scroll to "new" bottom 79 | const scrollBottom$ = scrollBottomSubject$ 80 | .pausable(scolledDown$) 81 | .debounce(10) 82 | .do(() => ($screen[0].scrollTop = $screen[0].scrollHeight)); 83 | 84 | 85 | // Log items array filtered by filter and monitorConfig 86 | const items$ = rx.Observable.combineLatest( 87 | $data.collection('logs').changes, 88 | filter$, 89 | monitorConfig$, 90 | (items, filter, monitorConfig) => items 91 | 92 | // Filter items based on filter object 93 | .filter( createLogsFilter( filter ) ) 94 | 95 | // remove items which have #verbose if monitorConfig.verbose === false 96 | .filter((log) => monitorConfig.verbose ? true : log.tags.indexOf('#verbose') === -1) 97 | 98 | // limit items to monitorConfig.limit 99 | .slice(0 - monitorConfig.limit) 100 | 101 | .map((item) => { 102 | item.displayTags = getDisplayTags(item.tags, filter); 103 | item.html = item.html || $sce.trustAsHtml(ansi2html(item.data)); 104 | return item; 105 | }) 106 | 107 | ) 108 | .distinctUntilChanged() 109 | .do(() => scrollBottomSubject$.onNext()); 110 | 111 | 112 | 113 | 114 | 115 | $scope.attach({ 116 | logs: items$, 117 | _scrollBottom: scrollBottom$ 118 | }); 119 | 120 | 121 | 122 | 123 | 124 | $scope.getItemClassDef = (tags) => ({ 125 | verbose: tags.indexOf('#verbose') !== -1, 126 | log: tags.indexOf('log') !== -1, 127 | error: tags.indexOf('error') !== -1, 128 | }); 129 | 130 | 131 | 132 | } 133 | }; 134 | }); 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | function getDisplayTags(tags, filter) { 151 | 152 | if(filter.removeDisplayTags) { 153 | tags = tags.filter(function(tag) { 154 | return !$some(tag, filter.removeDisplayTags); 155 | }); 156 | } 157 | 158 | return tags.filter(function(tag) { 159 | return !$some(tag, ['#verbose', '#process', '#script', '#image', 'log', 'error']); 160 | }); 161 | 162 | } 163 | 164 | 165 | 166 | 167 | 168 | 169 | function createLogsFilter(options) { 170 | 171 | return function(item) { 172 | var all = []; 173 | 174 | if(options.every) { 175 | all.push( 176 | options.every.every(function(tag) { 177 | return item.tags.indexOf(tag) !== -1; 178 | }) 179 | ); 180 | } 181 | 182 | if(options.none) { 183 | all.push( 184 | options.none.every(function(tag) { 185 | return item.tags.indexOf(tag) === -1; 186 | }) 187 | ); 188 | } 189 | 190 | return all.every(Boolean); 191 | }; 192 | 193 | } 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | function $every(needle, haystack ) { 204 | if (Array.isArray(needle)) { 205 | return needle.every(function(val) { 206 | return haystack.indexOf(val) !== -1; 207 | }); 208 | } else { 209 | return haystack.indexOf(needle) !== -1 210 | } 211 | } 212 | 213 | 214 | function $some(needle, haystack ) { 215 | if (Array.isArray(needle)) { 216 | return needle.every(function(val) { 217 | return haystack.indexOf(val) !== -1; 218 | }); 219 | } else { 220 | return haystack.indexOf(needle) !== -1 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/log-monitor/styles.scss: -------------------------------------------------------------------------------- 1 | .d4Ijm7XD { 2 | position: relative; 3 | 4 | > header { 5 | background: $color-ci; 6 | //height: 30px; 7 | border-radius: $border-radius $border-radius 0 0; 8 | display: flex; 9 | color: rgba(white, .7); 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | overflow: hidden; 13 | align-items: center; 14 | 15 | 16 | .tabs { 17 | display: flex; 18 | .tab { 19 | padding: 6px 12px; 20 | &:not(:last-child) { 21 | border-right: 1px solid $color-ci-light; 22 | } 23 | &:hover { 24 | background: mix($color-ci, $color-ci-light, 80%); 25 | } 26 | &.active { 27 | background: $color-ci-light; 28 | color: rgba(white, .9); 29 | } 30 | 31 | .title { 32 | font-weight: 700; 33 | } 34 | 35 | > * { 36 | vertical-align: middle; 37 | } 38 | } 39 | } 40 | } 41 | 42 | > .screen { 43 | 44 | color: #eee; 45 | -webkit-font-smoothing: antialiased; 46 | -moz-osx-font-smoothing: grayscale; 47 | padding: 10px; 48 | font-family: Courier; 49 | font-size: .92em; 50 | line-height: 1.25; 51 | overflow: auto; 52 | min-height: 400px; 53 | max-height: 400px; 54 | height: 400px; 55 | background: mix(black, $color-ci-dark, 30%); 56 | 57 | .item { 58 | white-space: pre-wrap; 59 | > .tags { 60 | &:after { content: ' '; } 61 | } 62 | 63 | > .msg { 64 | 65 | } 66 | 67 | 68 | 69 | &.log { 70 | .tags { 71 | color: saturate($color-on, 15%); 72 | } 73 | } 74 | 75 | &.verbose { 76 | .tags { 77 | color: desaturate(yellow, 35%); 78 | } 79 | } 80 | 81 | &.error { 82 | .tags { 83 | color: darken(saturate($color-off, 100%), 5%); 84 | } 85 | } 86 | 87 | 88 | } 89 | 90 | 91 | } 92 | 93 | 94 | > footer { 95 | transition: all .3s; 96 | background:$color-ci; 97 | border-radius: 0 0 $border-radius $border-radius; 98 | display: flex; 99 | color: rgba(white, .7); 100 | 101 | -webkit-font-smoothing: antialiased; 102 | -moz-osx-font-smoothing: grayscale; 103 | 104 | user-select: none; 105 | 106 | > .col { 107 | &:not(:last-child) { 108 | border-right: 1px solid rgba($color-ci-dark, .2); 109 | } 110 | } 111 | 112 | select { 113 | border: none; 114 | appearance: none; 115 | background:transparent; 116 | padding: 0; 117 | outline: none; 118 | color:inherit; 119 | font-size: inherit; 120 | font-weight: inherit; 121 | vertical-align: middle; 122 | display: inline; 123 | } 124 | 125 | label { 126 | padding: 7px 10px; 127 | display: block; 128 | } 129 | 130 | input[type="checkbox"] { 131 | $color: rgba(white, .7); 132 | 133 | appearance: none; 134 | border: 1px solid $color; 135 | width: 1.05em; 136 | height: 1.05em; 137 | vertical-align: middle; 138 | position: relative; 139 | outline: none; 140 | 141 | &:checked:after { 142 | content: ''; 143 | position: absolute; 144 | top: 1px; 145 | left: 1px; 146 | bottom: 1px; 147 | right: 1px; 148 | background: $color; 149 | } 150 | } 151 | 152 | span { 153 | display: inline-block; 154 | vertical-align: middle; 155 | } 156 | 157 | input + span { 158 | margin-left: .5em; 159 | } 160 | 161 | span + select { 162 | margin-left: .5em; 163 | vertical-align: middle; 164 | } 165 | 166 | } 167 | 168 | 169 | } 170 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/log-monitor/template.jade: -------------------------------------------------------------------------------- 1 | .d4Ijm7XD 2 | 3 | header 4 | 5 | .tabs 6 | .tab( 7 | ng-class="{active: !currentTab }", 8 | ng-click="currentTab = null; selectTab({ filter: {} })" 9 | ) 10 | span.title {{ config.title || 'Logs' }} 11 | 12 | .tab( 13 | ng-repeat="tab in config.tabs", 14 | ng-click="$parent.currentTab = tab.title; selectTab(tab)", 15 | ng-class="{active: currentTab === tab.title }" 16 | ) 17 | .fa(class="{{ tab.icon }}", ng-if="tab.icon") 18 | span {{ tab.title }} 19 | 20 | .screen 21 | .item( 22 | ng-repeat="log in logs track by log.createdAt", 23 | ng-class="::getItemClassDef(log.tags)" 24 | ) 25 | span.tags [{{ ::log.displayTags.join(', ') }}] 26 | span.msg(ng-bind-html="::log.html") 27 | 28 | 29 | footer 30 | .col 31 | label 32 | span Limit 33 | select(ng-model="monitorConfig.limit", ng-options="item as item for item in [10, 50, 100, 500, 1000]") 34 | .col 35 | label 36 | input( 37 | ng-model="monitorConfig.verbose", 38 | type="checkbox" 39 | ) 40 | span Verbose -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/script-button/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var angular = require('angular'); 5 | 6 | 7 | var mod = module.exports = angular.module('app.directives.script-button', []); 8 | 9 | mod.directive('scriptButton', function() { 10 | return { 11 | restrict: 'E', 12 | template: require('./template.pug'), 13 | replace: true, 14 | scope: { 15 | script: '=', 16 | image: '=' 17 | }, 18 | controller: function($scope, $cmd) { 19 | 20 | $scope.start = function() { 21 | $cmd.exec('startScript', { image: $scope.image.id, script: $scope.script.name }); 22 | }; 23 | 24 | $scope.stop = function() { 25 | $cmd.exec('stopScript', { image: $scope.image.id, script: $scope.script.name }); 26 | }; 27 | 28 | 29 | } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/script-button/styles.scss: -------------------------------------------------------------------------------- 1 | .n2sXE18g { 2 | & + .n2sXE18g { 3 | margin-left: 5px; 4 | } 5 | 6 | button { 7 | width: 100%; 8 | 9 | &.state-idle { 10 | background:$color-ci-haze-dark; 11 | color: $color-ci-font; 12 | } 13 | 14 | &.state-running { 15 | background:$color-on; 16 | } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/directives/script-button/template.pug: -------------------------------------------------------------------------------- 1 | .n2sXE18g 2 | button.btn--default( 3 | type="button", 4 | ng-disabled="script.state !== 'idle' && script.state !== 'running'", 5 | ng-click="$event.stopPropagation(); script.state === 'idle' ? start() : stop()", 6 | class="state-{{script.state}}" 7 | ) 8 | .fa.fa-play(ng-if="script.state === 'idle'") 9 | .fa.fa-stop(ng-if="script.state === 'running'") 10 | .fa.fa-refresh.fa-spin(ng-if="script.state !== 'idle' && script.state !== 'running'") 11 | span {{ script.name }} 12 | 13 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const angular = require('angular'); 5 | let t = require('rx-angular'); 6 | 7 | 8 | 9 | const mod = module.exports = angular.module('app', [ 10 | 11 | require('angular-ui-router'), 12 | require('angular-moment').name, 13 | 'rx', 14 | 15 | 16 | require('./modules/ansi').name, 17 | require('./modules/rx-ext').name, 18 | 19 | 20 | require('./services/socket').name, 21 | require('./services/data').name, 22 | require('./services/cmd').name, 23 | 24 | require('./directives/log-monitor').name, 25 | require('./directives/image-status').name, 26 | require('./directives/image-controls').name, 27 | require('./directives/image-script-controls').name, 28 | 29 | require('./directives/script-button').name, 30 | 31 | require('./states').name, 32 | ]); 33 | 34 | mod.value('config', { 35 | 36 | }); 37 | 38 | 39 | 40 | mod.run(function(socket, $data, rx) { 41 | 42 | const ddpRx = socket.channelObservable('ddp'); 43 | //const ddpTx = socket.channelObserver('ddp'); 44 | 45 | ddpRx 46 | .subscribe(function(e) { 47 | 48 | if(e.type === 'collection') { 49 | if(e.method === 'add' || e.method === 'update') { 50 | $data.collection(e.collection).push(e.id, e.model); 51 | } 52 | } 53 | 54 | }); 55 | 56 | }); 57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/modules/ansi/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var angular = require('angular'); 5 | var ansi2html = require('./lib/ansi2html'); 6 | 7 | var mod = module.exports = angular.module('ansi2html', []); 8 | 9 | mod.value('ansi2html', ansi2html); 10 | 11 | 12 | mod.filter('ansi2html', function($sce) { 13 | return function(input) { 14 | return $sce.trustAsHtml(ansi2html(input)); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/modules/ansi/lib/ansi2html.js: -------------------------------------------------------------------------------- 1 | function trimWhitespace(str) { 2 | return (str || '').toString().replace(/^\s+|\s+$/g, ''); 3 | } 4 | 5 | 6 | function ansi2html(str) { 7 | str = trimWhitespace(str); 8 | 9 | var props = {}, 10 | open = false 11 | 12 | var stylemap = { 13 | bold: "font-weight", 14 | underline: "text-decoration", 15 | color: "color", 16 | background: "background" 17 | } 18 | 19 | function style() { 20 | var key, val, style = [] 21 | for (var key in props) { 22 | val = props[key] 23 | if (!val) continue 24 | if (val == true) { 25 | style.push(stylemap[key] + ':' + key) 26 | } else { 27 | style.push(stylemap[key] + ':' + val) 28 | } 29 | } 30 | return style.join(';') 31 | } 32 | 33 | 34 | function tag(code) { 35 | var i, tag = '', 36 | n = ansi2html.table[code] 37 | 38 | if (open) tag += '' 39 | open = false 40 | 41 | if (n) { 42 | for (i in n) props[i] = n[i] 43 | tag += '' 44 | open = true 45 | } else { 46 | props = {} 47 | } 48 | 49 | return tag 50 | } 51 | 52 | return str.replace(/\[(\d+;)?(\d+)*m/g, function(match, b1, b2) { 53 | var i, code, res = '' 54 | if (b2 == '' || b2 == null) b2 = '0' 55 | for (i = 1; i < arguments.length - 2; i++) { 56 | if (!arguments[i]) continue 57 | code = parseInt(arguments[i]) 58 | res += tag(code) 59 | } 60 | return res 61 | }) + tag() 62 | } 63 | 64 | /* not implemented: 65 | * italic 66 | * blink 67 | * invert 68 | * strikethrough 69 | */ 70 | ansi2html.table = { 71 | 0: null, 72 | 1: { 73 | bold: true 74 | }, 75 | 3: { 76 | italic: true 77 | }, 78 | 4: { 79 | underline: true 80 | }, 81 | 5: { 82 | blink: true 83 | }, 84 | 6: { 85 | blink: true 86 | }, 87 | 7: { 88 | invert: true 89 | }, 90 | 9: { 91 | strikethrough: true 92 | }, 93 | 23: { 94 | italic: false 95 | }, 96 | 24: { 97 | underline: false 98 | }, 99 | 25: { 100 | blink: false 101 | }, 102 | 27: { 103 | invert: false 104 | }, 105 | 29: { 106 | strikethrough: false 107 | }, 108 | 30: { 109 | color: 'black' 110 | }, 111 | 31: { 112 | color: 'red' 113 | }, 114 | 32: { 115 | color: 'green' 116 | }, 117 | 33: { 118 | color: 'yellow' 119 | }, 120 | 34: { 121 | color: 'blue' 122 | }, 123 | 35: { 124 | color: 'magenta' 125 | }, 126 | 36: { 127 | color: 'cyan' 128 | }, 129 | 37: { 130 | color: 'white' 131 | }, 132 | 39: { 133 | color: null 134 | }, 135 | 40: { 136 | background: 'black' 137 | }, 138 | 41: { 139 | background: 'red' 140 | }, 141 | 42: { 142 | background: 'green' 143 | }, 144 | 43: { 145 | background: 'yellow' 146 | }, 147 | 44: { 148 | background: 'blue' 149 | }, 150 | 45: { 151 | background: 'magenta' 152 | }, 153 | 46: { 154 | background: 'cyan' 155 | }, 156 | 47: { 157 | background: 'white' 158 | }, 159 | 49: { 160 | background: null 161 | } 162 | } 163 | 164 | 165 | 166 | module.exports = ansi2html; 167 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/modules/rx-ext/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var angular = require('angular'); 5 | require('rx-angular'); 6 | 7 | 8 | var mod = module.exports = angular.module('rx-angular-extensions', ['rx']); 9 | 10 | 11 | mod.run(function($rootScope, $parse) { 12 | 13 | $rootScope.__proto__.autoDispose = function(sub) { 14 | this.$on('$destroy', function() { 15 | sub.dispose(); 16 | }); 17 | return this; 18 | }; 19 | 20 | $rootScope.__proto__.attach = function(obj) { 21 | var $scope = this; 22 | 23 | angular.forEach(obj, function(val, key) { 24 | 25 | var setter = $parse(key).assign; 26 | 27 | var sub = val.safeApply($scope, function(result) { 28 | setter($scope, result); 29 | }) 30 | .subscribe(); 31 | 32 | $scope.autoDispose( sub ); 33 | 34 | }); 35 | }; 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/services/cmd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var angular = require('angular'); 5 | 6 | var mod = module.exports = angular.module('app.cmd', []); 7 | 8 | 9 | mod.service('$cmd', function(socket) { 10 | 11 | this.exec = function(cmd, args) { 12 | socket.tx.onNext({ 13 | channel: 'ddp', 14 | data: { 15 | type: 'exec', 16 | cmd: 'command', 17 | params: { 18 | cmd: cmd, 19 | args: args 20 | } 21 | } 22 | }); 23 | }; 24 | 25 | }); 26 | 27 | mod.run(function($cmd, $rootScope) { 28 | $rootScope.$cmd = $cmd; 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/services/data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var angular = require('angular'); 5 | require('rx-angular'); 6 | 7 | var mod = module.exports = angular.module('app.data', [ 8 | 'rx' 9 | ]); 10 | 11 | 12 | mod.factory('$data', function(rx) { 13 | 14 | 15 | 16 | function Collection() { 17 | 18 | var store = {}; 19 | 20 | var observable = new rx.BehaviorSubject([]); 21 | 22 | let updateTimer; 23 | 24 | 25 | function update() { 26 | 27 | clearTimeout(updateTimer); 28 | 29 | updateTimer = setTimeout(() => { 30 | var models = Object.keys(store).map(function(id) { 31 | return store[id]; 32 | }); 33 | observable.onNext(models); 34 | }); 35 | } 36 | 37 | 38 | this.push = function(id, data) { 39 | if(store[id]) { 40 | angular.extend(store[id], data); 41 | } else { 42 | store[id] = data; 43 | } 44 | update(); 45 | }; 46 | 47 | this.changes = observable; 48 | 49 | } 50 | 51 | 52 | 53 | var service = {}; 54 | var collections = {}; 55 | 56 | service.collection = function(name) { 57 | if(collections[name]) { 58 | return collections[name]; 59 | } else { 60 | return (collections[name] = new Collection()); 61 | } 62 | }; 63 | 64 | return service; 65 | 66 | 67 | 68 | 69 | 70 | }); 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/services/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var angular = require('angular'); 4 | var SocketIo = require('socket.io-client'); 5 | 6 | 7 | var mod = module.exports = angular.module('socket', [ 8 | 'rx' 9 | ]); 10 | 11 | 12 | 13 | mod.service('socket', function(rx) { 14 | 15 | var socket = SocketIo(); 16 | 17 | var onevent = socket.onevent; 18 | socket.onevent = function (packet) { 19 | var args = packet.data || []; 20 | onevent.call (this, packet); // original call 21 | packet.data = ["*"].concat(args); 22 | onevent.call(this, packet); // additional call to catch-all 23 | }; 24 | 25 | var _rx = this.rx = new rx.Subject(); 26 | var _tx = this.tx = new rx.Subject(); 27 | 28 | socket.on('*',function(channel, data) { 29 | _rx.onNext({ 30 | channel: channel, 31 | data: data 32 | }) 33 | }); 34 | 35 | 36 | _tx.subscribe(function(msg) { 37 | socket.emit(msg.channel, msg.data); 38 | }); 39 | 40 | 41 | this.channelObservable = function(name) { 42 | return _rx.filter(function(e) { 43 | return e.channel = name; 44 | }) 45 | .map(function(e) { 46 | return e.data; 47 | }); 48 | }; 49 | 50 | this.channelObserver = function(name) { 51 | return rx.Observer.create(function(data) { 52 | _tx.onNext({ 53 | channel: name, 54 | data: data 55 | }) 56 | }); 57 | }; 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app.dashboard/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | url: '/', 5 | template: require('./template.pug'), 6 | controller: ['$scope', '$data', function($scope, $data) { 7 | $scope.attach({ 8 | images: $data.collection('images').changes 9 | }) 10 | }] 11 | }; 12 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app.dashboard/styles.scss: -------------------------------------------------------------------------------- 1 | .R4rmOwgT { 2 | padding: 20px; 3 | 4 | .table--images { 5 | width: 100%; 6 | margin-bottom: 20px; 7 | 8 | td { 9 | //padding: .5em .7em; 10 | padding: 5px; 11 | background: $color-ci-haze; 12 | vertical-align: top; 13 | } 14 | 15 | thead { 16 | td { 17 | background: $color-ci-haze-dark; 18 | 19 | &:first-child { 20 | border-radius: $border-radius 0 0 0; 21 | } 22 | &:last-child { 23 | border-radius: 0 $border-radius 0 0; 24 | } 25 | 26 | } 27 | font-weight: 700; 28 | } 29 | 30 | 31 | tbody { 32 | td.script-controls, 33 | td.controls { 34 | padding:0; 35 | width: 150px; 36 | } 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app.dashboard/template.pug: -------------------------------------------------------------------------------- 1 | .R4rmOwgT 2 | h1 Images 3 | 4 | table.table--images 5 | thead 6 | tr 7 | td.no No 8 | td.name Name 9 | td.state Status 10 | td.controls Controls 11 | td.script-controls Scripts 12 | 13 | tbody 14 | tr(ng-repeat="image in images") 15 | td(ui-sref="^.images.image({ imageId: image.id })").no {{ $index + 1 }} 16 | td(ui-sref="^.images.image({ imageId: image.id })").name {{ image.id }} 17 | td(ui-sref="^.images.image({ imageId: image.id })").state: image-status(image="image") 18 | td.controls: image-controls(image="image", rows) 19 | td.script-controls: image-script-controls(image="image", rows) 20 | 21 | h1 Logs 22 | log-monitor 23 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app.images.image/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | url: '/:imageId', 5 | template: require('./template.pug'), 6 | controller: ['$scope', '$state', '$data', 'rx', function($scope, $state, $data, rx) { 7 | 8 | var id = $state.params.imageId; 9 | 10 | var image = $data.collection('images') 11 | .changes.flatMap(function(items) { 12 | return rx.Observable.from(items); 13 | }) 14 | .filter(function(model) { 15 | return model.id === id; 16 | }); 17 | 18 | 19 | var monitorConfig = image.map(function(image) { 20 | 21 | var imageTab = { 22 | title: image.id, 23 | filter: { 24 | every: ['#image', image.id], 25 | none: ['#script'] 26 | }, 27 | icon: image.isRunning ? 'fa-heart' : 'fa-heart-o' 28 | }; 29 | 30 | var scriptTabs = Object.keys(image.scripts) 31 | .map(function(key) { 32 | return image.scripts[key]; 33 | }) 34 | .map(function(script) { 35 | return { 36 | title: script.name, 37 | filter: { 38 | every: ['#script', image.id, script.name], 39 | removeDisplayTags: [image.id] 40 | }, 41 | icon: 'fa-code' 42 | } 43 | }); 44 | 45 | 46 | 47 | return { 48 | tabs: scriptTabs.length ? [imageTab].concat(scriptTabs) : [] 49 | } 50 | }); 51 | 52 | 53 | $scope.attach({ 54 | image: image, 55 | monitorConfig: monitorConfig 56 | }); 57 | 58 | 59 | }] 60 | }; 61 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app.images.image/styles.scss: -------------------------------------------------------------------------------- 1 | .UyjZzIBR { 2 | 3 | > header { 4 | display: flex; 5 | margin: 10px; 6 | 7 | .details { 8 | flex: 1 1 auto; 9 | background: $color-ci-haze; 10 | margin-right: 10px; 11 | padding: 10px; 12 | border-radius: $border-radius; 13 | position: relative; 14 | 15 | .version { 16 | position: absolute; 17 | top: 10px; 18 | right: 10px; 19 | font-size: 1.4em; 20 | opacity: .7; 21 | } 22 | .title { 23 | margin-bottom: .5em; 24 | 25 | .pkg-name { 26 | font-size: .6em; 27 | opacity: .5; 28 | } 29 | } 30 | 31 | .table--config { 32 | width: 100%; 33 | 34 | td { 35 | vertical-align: top; 36 | padding: .4em; 37 | background: $color-ci-haze-light; 38 | 39 | &:first-child { 40 | width: 80px; 41 | font-weight: 700; 42 | } 43 | 44 | .env { 45 | line-height: 1.5; 46 | } 47 | 48 | } 49 | } 50 | 51 | } 52 | 53 | .controls { 54 | flex: 0 0 auto; 55 | background: $color-ci-haze; 56 | border-radius: $border-radius; 57 | min-width: 180px; 58 | 59 | hr { 60 | margin: 0; 61 | padding: 0; 62 | border: none; 63 | border-bottom: 1px solid $color-ci-haze-dark; 64 | } 65 | } 66 | } 67 | 68 | > section.logs { 69 | margin: 10px; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app.images.image/template.pug: -------------------------------------------------------------------------------- 1 | .UyjZzIBR(ng-if="image") 2 | header 3 | .details 4 | h1.title 5 | image-status(image="image") 6 | span.id {{ image.id }} 7 | span.pkg-name(ng-if="image.pkg.name") /{{ image.pkg.name }} 8 | 9 | p.description {{ image.pkg.description }} 10 | 11 | .version {{ image.pkg.version }} 12 | 13 | table.table--config 14 | tbody 15 | tr 16 | td CWD 17 | td {{ image.absPath }} 18 | tr 19 | td Start 20 | td {{ image.commands.start }} 21 | tr 22 | td Export 23 | td 24 | .env(ng-repeat="env in image.config.environment") 25 | span {{ env }} 26 | 27 | 28 | 29 | .controls 30 | image-controls(image="image", rows) 31 | hr 32 | image-script-controls(image="image", rows) 33 | 34 | 35 | section.logs 36 | log-monitor( 37 | filter="{ every: [image.id] }", 38 | config="monitorConfig" 39 | ) 40 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app.images/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | url: '/images', 5 | template: require('./template.pug'), 6 | controller: ['$scope', '$data', '$state', function($scope, $data, $state) { 7 | 8 | $scope.$watch(function() { 9 | $scope.showAside = $state.params.imageId || false; 10 | }); 11 | 12 | $scope.showAside = $state.params.imageId || false; 13 | 14 | $scope.attach({ 15 | images: $data.collection('images').changes 16 | }); 17 | 18 | }] 19 | }; 20 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app.images/styles.scss: -------------------------------------------------------------------------------- 1 | .Qepcd21K { 2 | display: flex; 3 | height: 100%; 4 | width: 100%; 5 | 6 | > aside { 7 | flex: 0 0 auto; 8 | min-width: 250px; 9 | background: $color-ci-haze-dark; 10 | overflow-y: auto; 11 | box-shadow: 3px 0 5px -3px darken($color-ci-haze-dark, 5%); 12 | padding-top: 14px; 13 | 14 | li { 15 | background:white; 16 | margin: 1px 0; 17 | list-style: none; 18 | padding: 10px; 19 | height: 50px; 20 | background: $color-ci-haze; 21 | position: relative; 22 | transition: all .3s; 23 | 24 | &.active { 25 | background: $color-ci-haze-light; 26 | } 27 | 28 | .title { 29 | font-weight: 700; 30 | } 31 | 32 | .status { 33 | position: absolute; 34 | right: 10px; 35 | top: 10px; 36 | text-align: right; 37 | > .state { 38 | font-size: .9em; 39 | } 40 | } 41 | } 42 | } 43 | 44 | > main { 45 | flex: 1 1 auto; 46 | min-width: 1px; 47 | padding: 5px; 48 | 49 | .image-list { 50 | padding: 20px; 51 | 52 | li { 53 | list-style: none; 54 | background: $color-ci-haze; 55 | margin: 0 0 10px 0; 56 | padding: 10px; 57 | border-radius: $border-radius; 58 | box-shadow: 0 0 2px 0 rgba($color-ci-dark, .4); 59 | position: relative; 60 | 61 | .title { 62 | font-size: 1.5em; 63 | } 64 | 65 | .controls { 66 | position: absolute; 67 | right: 10px; 68 | top: 10px; 69 | } 70 | } 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app.images/template.pug: -------------------------------------------------------------------------------- 1 | .Qepcd21K 2 | aside(ng-if="showAside") 3 | li(ng-repeat="image in images", ui-sref=".image({ imageId: image.id })", ui-sref-active="active") 4 | .title {{ image.id }} 5 | .status 6 | image-status(image="image") 7 | .state {{ image.state }} 8 | .version {{ image.pkg.version }} 9 | 10 | main.ui-view 11 | .image-list 12 | li(ng-repeat="image in images", ui-sref=".image({ imageId: image.id })", ui-sref-active="active") 13 | .title 14 | image-status(image="image") 15 | span {{ image.id }} 16 | .details {{ image.absPath }} 17 | .controls 18 | image-controls(image="image") 19 | 20 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | abstract: true, 5 | url: '^', 6 | template: require('./template.pug') 7 | }; 8 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app/styles.scss: -------------------------------------------------------------------------------- 1 | .RCgWH2Vt { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | color: $color-ci-font; 6 | 7 | > header { 8 | flex: 0 0 auto; 9 | justify-content: space-between; 10 | align-items: center; 11 | 12 | display: flex; 13 | background: $color-ci-dark; 14 | padding: 5px; 15 | 16 | 17 | 18 | > nav { 19 | display: flex; 20 | 21 | li { 22 | list-style: none; 23 | 24 | &:not(:last-child) { 25 | margin-right: .4em; 26 | } 27 | 28 | button { 29 | font-size: 1.1em; 30 | > span { 31 | font-weight: 100; 32 | } 33 | } 34 | 35 | } 36 | } 37 | 38 | .title { 39 | font-size: 1.7em; 40 | white-space: nowrap; 41 | > .fat { 42 | font-weight: 700; 43 | color: darken($color-ci-font, 12%); 44 | } 45 | > .thin { 46 | font-weight: 100; 47 | color: lighten($color-ci-font, 5%); 48 | -webkit-font-smoothing: antialiased; 49 | -moz-osx-font-smoothing: grayscale; 50 | } 51 | } 52 | 53 | } 54 | 55 | > main { 56 | flex: 1 1 auto; 57 | overflow: auto; 58 | overflow-x: hidden; 59 | height: 100%; 60 | } 61 | 62 | > footer { 63 | flex: 0 0 auto; 64 | background: $color-ci-dark; 65 | color: lighten($color-ci-dark, 25%); 66 | padding: 10px; 67 | font-size: .8em; 68 | text-align: center; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/app/template.pug: -------------------------------------------------------------------------------- 1 | .RCgWH2Vt 2 | header 3 | nav.left 4 | li 5 | button.btn--default(ui-sref=".dashboard") 6 | .fa.fa-tachometer 7 | span Dashboard 8 | li 9 | button.btn--default(ui-sref=".images") 10 | .fa.fa-sitemap 11 | span Images 12 | 13 | nav.center 14 | .title 15 | span.fat node-compose 16 | span.thin monitor 17 | nav.right 18 | li 19 | button.btn--default(ng-click="$cmd.exec('reload')") 20 | .fa.fa-refresh 21 | span Reload config 22 | li 23 | button.btn--default(ng-click="$cmd.exec('stop', { image: 'all'})") 24 | .fa.fa-stop 25 | span Stop all 26 | li 27 | button.btn--default(ng-click="$cmd.exec('start', { image: 'all'})") 28 | .fa.fa-play 29 | span Start all 30 | 31 | 32 | main.ui-view 33 | footer 34 | span Build with   35 | span.fa.fa-heart 36 | span by platdesign 37 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var angular = require('angular'); 4 | 5 | var mod = module.exports = angular.module('app.states', []); 6 | 7 | 8 | mod.config(function($urlRouterProvider, $stateProvider) { 9 | 10 | $urlRouterProvider.otherwise('/'); 11 | 12 | 13 | $stateProvider 14 | .state('app', require('./app')) 15 | .state('app.dashboard', require('./app.dashboard')) 16 | .state('app.images', require('./app.images')) 17 | .state('app.images.image', require('./app.images.image')) 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/states/styles.scss: -------------------------------------------------------------------------------- 1 | @import './app/styles'; 2 | @import './app.dashboard/styles'; 3 | @import './app.images/styles'; 4 | @import './app.images.image/styles'; 5 | -------------------------------------------------------------------------------- /lib/app/monitor/src/app/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | $color-ci: darken(skyblue, 35%); 4 | //$color-ci: darken(mix(green, skyblue, 65%), 10%); 5 | 6 | $color-ci-dark: mix($color-ci, #333, 5%); 7 | $color-ci-light: mix($color-ci, #fff, 85%); 8 | $color-ci-font: lighten($color-ci-dark, 25%); 9 | $color-ci-haze: lighten($color-ci-dark, 70%); 10 | $color-ci-haze-light: lighten($color-ci-haze, 6%); 11 | $color-ci-haze-dark: darken($color-ci-haze, 10%); 12 | $color-on: saturate(mix(green, skyblue), 30%); 13 | $color-off: lighten(desaturate(darken(red, 10%), 60%), 30%); 14 | 15 | 16 | $border-radius: 3px; 17 | 18 | @import './directives/log-monitor/styles'; 19 | @import './directives/image-status/styles'; 20 | @import './directives/image-controls/styles'; 21 | @import './directives/image-script-controls/styles'; 22 | 23 | @import './directives/script-button/styles'; 24 | 25 | @import './states/styles'; 26 | 27 | 28 | .btn--default { 29 | border: none; 30 | background: $color-ci; 31 | font-weight: inherit; 32 | outline: none; 33 | cursor: pointer; 34 | color: rgba(white, .7); 35 | padding: .4em .9em .4em 2em; 36 | border-radius: $border-radius; 37 | position: relative; 38 | 39 | &:hover { 40 | color: rgba(white, .8); 41 | } 42 | &:active { 43 | color: white; 44 | box-shadow: inset 0 0 10px 0 rgba(black, .2); 45 | } 46 | 47 | .fa { 48 | left:.8em; 49 | top: 50%; 50 | transform: translateY(-50%); 51 | position: absolute; 52 | } 53 | 54 | &:disabled { 55 | background: $color-ci-haze-dark; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/app/monitor/src/boot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./main.scss'); 4 | require('font-awesome/scss/font-awesome.scss'); 5 | 6 | 7 | 8 | const $ = global.$ = global.jQuery = require('jquery'); 9 | const angular = require('angular'); 10 | const app = require('./app'); 11 | const EQ = require('css-element-queries/src/ElementQueries'); 12 | 13 | 14 | 15 | // Really the best approach? 16 | app.run(($rootScope) => $rootScope.$watch(() => EQ.init())); 17 | 18 | 19 | 20 | $(global.document).ready(() => { 21 | try { 22 | angular.bootstrap(global.document, [app.name]); 23 | } catch(e) { 24 | console.error(e.message); 25 | } 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /lib/app/monitor/src/main.scss: -------------------------------------------------------------------------------- 1 | 2 | @import 'normalize-scss/sass/_normalize'; 3 | @include normalize(); 4 | 5 | 6 | html { 7 | height: 100%; 8 | } 9 | body { 10 | font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; 11 | font-size: 14px; 12 | line-height: 1.4; 13 | overflow-x: hidden; 14 | height: 100%; 15 | } 16 | 17 | * { 18 | box-sizing: border-box; 19 | } 20 | 21 | h1, h2, h3, h4, h5, h6 { 22 | margin: 0; 23 | font-weight: 400; 24 | padding:0; 25 | } 26 | 27 | nav li a { 28 | display: block; 29 | } 30 | 31 | a { 32 | cursor: pointer; 33 | color: inherit; 34 | text-decoration: none; 35 | } 36 | 37 | // limit images to max-width: 100% of parent container 38 | img { 39 | max-width: 100%; 40 | display: block; 41 | } 42 | 43 | 44 | /* Angular reset */ 45 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], 46 | .ng-cloak, .x-ng-cloak, 47 | .ng-hide:not(.ng-hide-animate) { 48 | display: none !important; 49 | } 50 | 51 | ng\:form { 52 | display: block; 53 | } 54 | 55 | [ng-click], [ui-sref] { 56 | cursor: pointer; 57 | } 58 | 59 | a, [ng-click], [ui-sref] { 60 | user-select: none; 61 | } 62 | 63 | 64 | 65 | /* Font-Awesome reset */ 66 | .fa { 67 | vertical-align: baseline; 68 | } 69 | .fa + span { 70 | margin-left: .4em; 71 | vertical-align: baseline; 72 | } 73 | 74 | 75 | @import './app/styles'; 76 | -------------------------------------------------------------------------------- /lib/app/monitor/view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | node-compose monitor 5 | 6 | 7 | 8 | 9 |
Loading ...
10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/exit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | 9 | vorpal.find('exit') 10 | .description('Closing shell and stop all images') 11 | .action(function(args) { 12 | return app.commands.exit(args); 13 | }); 14 | 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/images.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | vorpal 9 | .command('images', 'List all images found in config file.') 10 | .action(function(args) { 11 | return app.commands.images(args); 12 | }); 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/monitor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | vorpal 9 | .command('monitor', 'Start web monitor') 10 | .option('-o, --open [browser]', 'Open monitor with default or favored browser.') 11 | .option('-p, --port ', 'Bind to custom port. (Default: 9669)') 12 | .option('-h, --host ', 'Bind to custom host. (Default: 127.0.0.1)') 13 | .action(function(args) { 14 | return app.commands.monitor(args); 15 | }); 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/ps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | vorpal 9 | .command('ps', 'List running images') 10 | .action(function(args) { 11 | return app.commands.ps(args); 12 | }); 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/reload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | vorpal 9 | .command('reload', 'Reloads config file and restarts processes if needed.') 10 | .action(function(args) { 11 | return app.commands.reload(args); 12 | }); 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/restart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | vorpal 9 | .command('restart [images...]', 'Restart one or more images by given name or all') 10 | .autocomplete({ 11 | data: function() { 12 | return app.imagesAsArray() 13 | .filter((image) => { 14 | return image.state === 'running'; 15 | }) 16 | .map((image) => { 17 | return image._name; 18 | }) 19 | } 20 | }) 21 | .action(function(args) { 22 | return app.commands.restart(args); 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/start-script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | vorpal 9 | .command('start-script ', 'Start a script of an image') 10 | .autocomplete({ 11 | data: function() { 12 | 13 | return app.imagesAsArray() 14 | .map((image) => { 15 | 16 | let scriptStates = image.getState().scripts; 17 | 18 | return Object.keys(scriptStates) 19 | .filter((name) => { 20 | return scriptStates[name] === 'idle'; 21 | }) 22 | .map((name) => { 23 | return image._name + ':' + name; 24 | }); 25 | }) 26 | .reduce((acc, val) => { 27 | return acc.concat(val); 28 | }, []); 29 | 30 | } 31 | }) 32 | .action(function(args) { 33 | 34 | let splitted = args['image:script'].split(':'); 35 | args.image = splitted[0]; 36 | args.script = splitted[1]; 37 | delete args['image:script']; 38 | 39 | return app.commands.startScript(args); 40 | }); 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | vorpal 9 | .command('start [images...]', 'Start an image by or all') 10 | .autocomplete({ 11 | data: function() { 12 | return app.imagesAsArray() 13 | .filter((image) => { 14 | return image.state === 'idle'; 15 | }) 16 | .map((image) => { 17 | return image._name; 18 | }) 19 | } 20 | }) 21 | .action(function(args) { 22 | return app.commands.start(args); 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/stop-script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | vorpal 9 | .command('stop-script ', 'Stop a script of an image') 10 | .autocomplete({ 11 | data: function() { 12 | 13 | return app.imagesAsArray() 14 | .map((image) => { 15 | 16 | let scriptStates = image.getState().scripts; 17 | 18 | return Object.keys(scriptStates) 19 | .filter((name) => { 20 | return scriptStates[name] === 'running'; 21 | }) 22 | .map((name) => { 23 | return image._name + ':' + name; 24 | }); 25 | }) 26 | .reduce((acc, val) => { 27 | return acc.concat(val); 28 | }, []); 29 | 30 | } 31 | }) 32 | .action(function(args) { 33 | 34 | let splitted = args['image:script'].split(':'); 35 | args.image = splitted[0]; 36 | args.script = splitted[1]; 37 | delete args['image:script']; 38 | 39 | return app.commands.stopScript(args); 40 | }); 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /lib/vorpal-plugins/commands/stop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function(vorpal, options) { 5 | 6 | const app = options.app; 7 | 8 | vorpal 9 | .command('stop [images...]', 'Stop an image by or all') 10 | .autocomplete({ 11 | data: function() { 12 | return app.imagesAsArray() 13 | .filter((image) => { 14 | return image.state === 'running'; 15 | }) 16 | .map((image) => { 17 | return image._name; 18 | }) 19 | } 20 | }) 21 | .action(function(args) { 22 | return app.commands.stop(args); 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-compose", 3 | "description": "start node-processes as with docker-compose", 4 | "author": { 5 | "name": "Christian Blaschke", 6 | "email": "mail@platdesign.de", 7 | "url": "https://github.com/platdesign" 8 | }, 9 | "version": "0.2.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "cli-table": "^0.3.1", 13 | "code": "^4.0.0", 14 | "colors": "^1.1.2", 15 | "ctxq": "^0.1.5", 16 | "extend": "^3.0.0", 17 | "h2o2": "^5.4.0", 18 | "hapi": "^16.0.0", 19 | "inert": "^4.0.1", 20 | "is": "^3.1.0", 21 | "js-yaml": "^3.6.1", 22 | "nodemon": "^1.18.2", 23 | "open": "0.0.5", 24 | "pidusage": "^1.0.4", 25 | "ps-tree": "^1.1.0", 26 | "rx": "^4.1.0", 27 | "socket.io": "^1.4.8", 28 | "update-notifier": "^1.0.2", 29 | "vorpal": "^1.11.2", 30 | "which": "^1.2.10" 31 | }, 32 | "repository": "git@github.com:platdesign/node-compose.git", 33 | "homepage": "https://github.com/platdesign/node-compose", 34 | "main": "./index.js", 35 | "bin": { 36 | "node-compose": "bin/bn.js" 37 | }, 38 | "scripts": { 39 | "test": "NODE_ENV=test mocha", 40 | "test-watch": "NODE_ENV=test mocha -w", 41 | "build": "rm -rf ./lib/app/monitor/public && NODE_ENV=production webpack -p", 42 | "assets-dev": "webpack-dev-server --port 50505 --devtool eval --progress --colors --content-base build", 43 | "run-dev": "ASSET_SERVER=http://localhost:50505 nodemon ." 44 | }, 45 | "keywords": [ 46 | "microservices", 47 | "multiprocess", 48 | "docker", 49 | "docker-compose", 50 | "dev", 51 | "devtools", 52 | "devhelper", 53 | "manage", 54 | "nodejs", 55 | "javascript", 56 | "ilovejs" 57 | ], 58 | "devDependencies": { 59 | "angular": "^1.5.7", 60 | "angular-moment": "^1.0.0-beta.6", 61 | "angular-ui-router": "^0.3.1", 62 | "babel-core": "^6.18.2", 63 | "babel-loader": "^6.2.7", 64 | "babel-preset-es2015": "^6.18.0", 65 | "css-element-queries": "^0.3.2", 66 | "css-loader": "^0.25.0", 67 | "extract-text-webpack-plugin": "^1.0.1", 68 | "file-loader": "^0.9.0", 69 | "font-awesome": "^4.6.3", 70 | "gulp": "^3.9.1", 71 | "jquery": "^3.0.0", 72 | "json-loader": "^0.5.4", 73 | "mocha": "^3.0.2", 74 | "moment-duration-format": "^1.3.0", 75 | "ng-annotate-webpack-plugin": "^0.1.3", 76 | "ngstorage": "^0.3.10", 77 | "node-sass": "^3.11.2", 78 | "normalize-scss": "^5.0.3", 79 | "pug-html-loader": "^1.0.9", 80 | "ramda": "^0.22.0", 81 | "rx-angular": "^1.1.3", 82 | "sass-loader": "^4.0.2", 83 | "socket.io-client": "^1.4.8", 84 | "style-loader": "^0.13.1", 85 | "url-loader": "^0.5.7", 86 | "webpack": "^1.13.3" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const code = require('code'); 4 | const expect = code.expect; 5 | const path = require('path'); 6 | 7 | const App = require('../lib/app'); 8 | 9 | 10 | describe('Unit', () => { 11 | describe('App', () => { 12 | 13 | require('./topics/detect-start-command/test/unit')(App); 14 | require('./topics/environment-vars/test/unit')(App); 15 | require('./topics/start-restart-stop/test/unit')(App); 16 | require('./topics/detect-execute-scripts/test/unit')(App); 17 | require('./topics/processes/test/unit')(App); 18 | 19 | }); 20 | 21 | require('./unit/class-process')(); 22 | 23 | }); 24 | 25 | 26 | 27 | 28 | /** 29 | * TODO: 30 | * - test for valid yml format on reload 31 | */ 32 | -------------------------------------------------------------------------------- /test/topics/detect-execute-scripts/node-compose.yml: -------------------------------------------------------------------------------- 1 | serviceA: 2 | build: ./service-a 3 | -------------------------------------------------------------------------------- /test/topics/detect-execute-scripts/service-a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts":{ 3 | "test":"node script.js" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/topics/detect-execute-scripts/service-a/script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | setInterval(() => { 4 | console.log('123') 5 | }); 6 | -------------------------------------------------------------------------------- /test/topics/detect-execute-scripts/test/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const code = require('code'); 4 | const expect = code.expect; 5 | const path = require('path'); 6 | 7 | 8 | module.exports = function(App) { 9 | 10 | describe('Testing defined image-scripts', () => { 11 | 12 | let app; 13 | 14 | beforeEach(() => { 15 | app = new App({ 16 | CWD: path.join(__dirname, '..'), 17 | logger: function() {} 18 | }); 19 | }); 20 | 21 | afterEach(() => { 22 | app.close(); 23 | }); 24 | 25 | describe('service-a', () => { 26 | 27 | it('should have method startScript', () => { 28 | let image = app._images['serviceA']; 29 | 30 | expect(image.startScript) 31 | .to.be.a.function(); 32 | }); 33 | 34 | it('should have method stopScript', () => { 35 | let image = app._images['serviceA']; 36 | 37 | expect(image.stopScript) 38 | .to.be.a.function(); 39 | }); 40 | 41 | it('should have method restartScript', () => { 42 | let image = app._images['serviceA']; 43 | 44 | expect(image.restartScript) 45 | .to.be.a.function(); 46 | }); 47 | 48 | 49 | 50 | it('should have one script defined', () => { 51 | let image = app._images['serviceA']; 52 | 53 | expect(image.config.scripts) 54 | .to.be.an.array() 55 | .and.have.length(1); 56 | 57 | }); 58 | 59 | 60 | 61 | it('should start/restart/stop script', () => { 62 | 63 | let image = app._images['serviceA']; 64 | 65 | return image.startScript('test') 66 | .then(() => { 67 | 68 | expect( image.scriptProcesses.test.getState() ) 69 | .to.be.a.string() 70 | .and.equal('running'); 71 | 72 | return image.restartScript('test'); 73 | }) 74 | .then(() => { 75 | 76 | expect( image.scriptProcesses.test.getState() ) 77 | .to.be.a.string() 78 | .and.equal('running'); 79 | 80 | return image.stopScript('test'); 81 | }) 82 | .then(() => { 83 | 84 | expect( image.scriptProcesses.test.getState() ) 85 | .to.be.a.string() 86 | .and.equal('idle'); 87 | 88 | }); 89 | 90 | }); 91 | 92 | 93 | 94 | 95 | 96 | it('should log with expected tags on startScript()', () => { 97 | 98 | let image = app._images['serviceA']; 99 | 100 | let res = image.on('log') 101 | .take(2) 102 | .do((e) => { 103 | 104 | expect(e.tags) 105 | .to.be.an.array() 106 | .contain(['#verbose', 'log', '#process', '#script', 'test']); 107 | 108 | }) 109 | .toPromise(); 110 | 111 | image.startScript('test'); 112 | 113 | return res; 114 | 115 | }); 116 | 117 | 118 | it('should log with expected tags on stopScript()', () => { 119 | 120 | let image = app._images['serviceA']; 121 | 122 | 123 | return image.startScript('test') 124 | .then(() => { 125 | 126 | let res = image.on('log') 127 | .take(2) 128 | .do((e) => { 129 | 130 | expect(e.tags) 131 | .to.be.an.array() 132 | .contain(['#verbose', 'log', '#process', '#script', 'test']); 133 | 134 | }) 135 | .toPromise(); 136 | 137 | image.stopScript('test'); 138 | 139 | return res; 140 | 141 | }) 142 | 143 | 144 | 145 | }); 146 | 147 | 148 | 149 | it('script process should have same cwd as image', () => { 150 | 151 | let image = app._images['serviceA']; 152 | let proc = image.scriptProcesses.test; 153 | 154 | expect(image.config.cwd) 155 | .to.equal(proc.cwd); 156 | 157 | }); 158 | 159 | it('image-process should have same cwd as image', () => { 160 | 161 | let image = app._images['serviceA']; 162 | let proc = image.imageProcess; 163 | 164 | expect(image.config.cwd) 165 | .to.equal(proc.cwd); 166 | 167 | }); 168 | 169 | 170 | 171 | }); 172 | 173 | 174 | 175 | 176 | }); 177 | 178 | 179 | }; 180 | -------------------------------------------------------------------------------- /test/topics/detect-start-command/node-compose.yml: -------------------------------------------------------------------------------- 1 | serviceA: 2 | build: ./service-a 3 | command: node server.js 4 | environment: 5 | - NODE_ENV=production 6 | 7 | serviceB: 8 | build: ./service-b 9 | 10 | 11 | serviceC: 12 | build: ./service-c 13 | 14 | 15 | serviceD: 16 | build: ./service-d 17 | -------------------------------------------------------------------------------- /test/topics/detect-start-command/service-a/server.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platdesign/node-compose/79087833e8558a439f14bd03a45d1144f2715186/test/topics/detect-start-command/service-a/server.js -------------------------------------------------------------------------------- /test/topics/detect-start-command/service-b/server.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platdesign/node-compose/79087833e8558a439f14bd03a45d1144f2715186/test/topics/detect-start-command/service-b/server.js -------------------------------------------------------------------------------- /test/topics/detect-start-command/service-c/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platdesign/node-compose/79087833e8558a439f14bd03a45d1144f2715186/test/topics/detect-start-command/service-c/index.js -------------------------------------------------------------------------------- /test/topics/detect-start-command/service-d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts":{ 3 | "start":"node run.js" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/topics/detect-start-command/service-d/run.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platdesign/node-compose/79087833e8558a439f14bd03a45d1144f2715186/test/topics/detect-start-command/service-d/run.js -------------------------------------------------------------------------------- /test/topics/detect-start-command/test/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const code = require('code'); 4 | const expect = code.expect; 5 | const path = require('path'); 6 | 7 | 8 | module.exports = function(App) { 9 | 10 | describe('Testing start command configuration', function() { 11 | this.timeout(0); 12 | 13 | 14 | let app; 15 | 16 | beforeEach((done) => { 17 | app = new App({ 18 | CWD: path.join(__dirname, '..'), 19 | logger: function() {} 20 | }); 21 | done(); 22 | }); 23 | 24 | afterEach((done) => { 25 | app.close(); 26 | done(); 27 | }); 28 | 29 | describe('service-a (with command attr)', () => { 30 | 31 | it('should have start command: node server.js', () => { 32 | let image = app._images['serviceA']; 33 | 34 | expect(image.config.commands.start) 35 | .to.be.a.string() 36 | .and.equal('node server.js'); 37 | }); 38 | 39 | }); 40 | 41 | 42 | 43 | describe('service-b (no command)', () => { 44 | 45 | it('should have empty start command', () => { 46 | let image = app._images['serviceB']; 47 | 48 | expect(image.config.commands.start) 49 | .to.be.a.string() 50 | .and.to.be.empty() 51 | }); 52 | 53 | }); 54 | 55 | 56 | 57 | describe('service-c (found index.js)', () => { 58 | 59 | it('should have start command: node .', () => { 60 | let image = app._images['serviceC']; 61 | 62 | expect(image.config.commands.start) 63 | .to.be.a.string() 64 | .and.equal('node .'); 65 | }); 66 | 67 | }); 68 | 69 | 70 | describe('service-d (start script command from package.json)', () => { 71 | 72 | it('should have start command: node run.js', () => { 73 | let image = app._images['serviceD']; 74 | 75 | expect(image.config.commands.start) 76 | .to.be.a.string() 77 | .and.equal('node run.js'); 78 | }); 79 | 80 | }); 81 | 82 | 83 | }); 84 | 85 | 86 | }; 87 | -------------------------------------------------------------------------------- /test/topics/environment-vars/node-compose.yml: -------------------------------------------------------------------------------- 1 | serviceA: 2 | build: ./service-a 3 | environment: 4 | - NODE_ENV=production 5 | - PORT=9001 6 | 7 | serviceB: 8 | build: ./service-b 9 | -------------------------------------------------------------------------------- /test/topics/environment-vars/service-a/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/topics/environment-vars/service-b/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/topics/environment-vars/test/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const code = require('code'); 4 | const expect = code.expect; 5 | const path = require('path'); 6 | 7 | 8 | module.exports = function(App) { 9 | 10 | describe('Testing correct parsing and setting of environment variables', () => { 11 | 12 | let app; 13 | 14 | beforeEach(() => { 15 | app = new App({ 16 | CWD: path.join(__dirname, '..'), 17 | logger: function() {} 18 | }); 19 | }); 20 | 21 | afterEach(() => { 22 | app.close(); 23 | }); 24 | 25 | describe('service-a', () => { 26 | 27 | it('should have 2 raw-environment vars', () => { 28 | 29 | let image = app._images['serviceA']; 30 | 31 | let arr = image.config.rawEnvironment; 32 | 33 | expect(arr) 34 | .to.be.an.array() 35 | .and.have.length(2); 36 | 37 | expect(arr[0]) 38 | .to.be.a.string() 39 | .and.equal('NODE_ENV=production'); 40 | 41 | expect(arr[1]) 42 | .to.be.a.string() 43 | .and.equal('PORT=9001'); 44 | 45 | }); 46 | 47 | it('should have 2 parsed environment vars', () => { 48 | 49 | let image = app._images['serviceA']; 50 | 51 | let obj = image.config.environment; 52 | 53 | expect(obj) 54 | .to.be.an.object() 55 | .and.have.length(2); 56 | 57 | expect(obj['NODE_ENV']) 58 | .to.be.a.string() 59 | .and.equal('production'); 60 | 61 | expect(obj['PORT']) 62 | .to.be.a.string() 63 | .and.equal('9001'); 64 | 65 | }); 66 | 67 | }); 68 | 69 | 70 | describe('service-b', () => { 71 | 72 | it('should have an empry array of raw environment vars', () => { 73 | 74 | let image = app._images['serviceB']; 75 | 76 | expect(image.config.rawEnvironment) 77 | .to.be.an.array() 78 | .and.have.length(0); 79 | 80 | }); 81 | 82 | it('should have empty object of parsed environment vars', () => { 83 | 84 | let image = app._images['serviceB']; 85 | 86 | expect(image.config.environment) 87 | .to.be.an.object() 88 | .and.have.length(0); 89 | 90 | }); 91 | 92 | }); 93 | 94 | 95 | }); 96 | 97 | 98 | }; 99 | -------------------------------------------------------------------------------- /test/topics/processes/node-compose.yml: -------------------------------------------------------------------------------- 1 | service: 2 | build: ./service 3 | -------------------------------------------------------------------------------- /test/topics/processes/service/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | setInterval(() => { 5 | console.log('123'); 6 | }, 500) 7 | -------------------------------------------------------------------------------- /test/topics/processes/service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts":{ 3 | "long":"tail -f index.js", 4 | "short":"ls -als" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/topics/processes/test/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const code = require('code'); 4 | const expect = code.expect; 5 | const path = require('path'); 6 | 7 | 8 | module.exports = function(App) { 9 | 10 | describe('expected process handling', () => { 11 | 12 | let app; 13 | let image; 14 | 15 | beforeEach(() => { 16 | app = new App({ 17 | CWD: path.join(__dirname, '..'), 18 | logger: function() {} 19 | }); 20 | 21 | image = app._images['service']; 22 | }); 23 | 24 | afterEach(() => { 25 | app.close(); 26 | }); 27 | 28 | 29 | 30 | describe('service', () => { 31 | 32 | describe('starting image and long-script', () => { 33 | 34 | 35 | beforeEach(() => { 36 | return Promise.all([ 37 | app.commands.start({ image: 'service'}), 38 | app.commands.startScript({ image: 'service', script: 'long' }) 39 | ]); 40 | }); 41 | 42 | it('image should have state: running', () => { 43 | 44 | expect(image.imageProcess.getState()) 45 | .to.equal('running'); 46 | 47 | }); 48 | 49 | it('long-script should have state: running', () => { 50 | 51 | expect(image.scriptProcesses['long'].getState()) 52 | .to.equal('running'); 53 | 54 | }); 55 | 56 | it('short-script should have state: idle', () => { 57 | 58 | expect(image.scriptProcesses['short'].getState()) 59 | .to.equal('idle'); 60 | 61 | }); 62 | 63 | 64 | describe('after reloading', () => { 65 | 66 | beforeEach(() => { 67 | return app.commands.reload(); 68 | }); 69 | 70 | it('image should have state: running', () => { 71 | 72 | expect(image.imageProcess.getState()) 73 | .to.equal('running'); 74 | 75 | }); 76 | 77 | it('long-script should have state: running', () => { 78 | 79 | expect(image.scriptProcesses['long'].getState()) 80 | .to.equal('running'); 81 | 82 | }); 83 | 84 | it('short-script should have state: idle', () => { 85 | 86 | expect(image.scriptProcesses['short'].getState()) 87 | .to.equal('idle'); 88 | 89 | }); 90 | 91 | }); 92 | 93 | describe('after closing the app', () => { 94 | 95 | beforeEach(() => { 96 | return app.close(); 97 | }); 98 | 99 | it('image should have state: idle', () => { 100 | 101 | expect(image.imageProcess.getState()) 102 | .to.equal('idle'); 103 | 104 | }); 105 | 106 | it('long-script should have state: idle', () => { 107 | 108 | expect(image.scriptProcesses['long'].getState()) 109 | .to.equal('idle'); 110 | 111 | }); 112 | 113 | it('short-script should have state: idle', () => { 114 | 115 | expect(image.scriptProcesses['short'].getState()) 116 | .to.equal('idle'); 117 | 118 | }); 119 | 120 | }); 121 | 122 | 123 | }); 124 | 125 | 126 | }); 127 | 128 | 129 | 130 | 131 | }); 132 | 133 | 134 | }; 135 | -------------------------------------------------------------------------------- /test/topics/start-restart-stop/node-compose.yml: -------------------------------------------------------------------------------- 1 | serviceA: 2 | build: ./service-a 3 | 4 | serviceB: 5 | build: ./service-b 6 | -------------------------------------------------------------------------------- /test/topics/start-restart-stop/service-a/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | setInterval(function() { 4 | console.log('service-a'); 5 | }, 1000) 6 | -------------------------------------------------------------------------------- /test/topics/start-restart-stop/service-b/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | setInterval(function() { 4 | console.log('service-b'); 5 | }, 1000) 6 | -------------------------------------------------------------------------------- /test/topics/start-restart-stop/test/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const code = require('code'); 4 | const expect = code.expect; 5 | const path = require('path'); 6 | 7 | 8 | module.exports = function(App) { 9 | 10 | describe('Start - Restart - Stop with all', () => { 11 | 12 | let app; 13 | 14 | beforeEach(() => { 15 | app = new App({ 16 | CWD: path.join(__dirname, '..'), 17 | logger: function() {} 18 | }); 19 | }); 20 | 21 | afterEach(() => { 22 | app.close(); 23 | }); 24 | 25 | it('should start all', () => { 26 | 27 | return app.commands.start({ 28 | image: 'all' 29 | }) 30 | .then(() => { 31 | 32 | let allRunning = app.imagesAsArray() 33 | .every((image) => { 34 | return image.state === 'running'; 35 | }); 36 | 37 | expect(allRunning).to.equal(true); 38 | 39 | }); 40 | 41 | }); 42 | 43 | 44 | // it('should restart all', () => { 45 | 46 | // return app.commands.restart({ 47 | // image: 'all' 48 | // }) 49 | // .then(() => { 50 | 51 | // let allRunning = app.imagesAsArray() 52 | // .every((image) => { 53 | // return image.state === 'running'; 54 | // }); 55 | 56 | // expect(allRunning).to.equal(true); 57 | 58 | // }); 59 | 60 | // }); 61 | 62 | 63 | it('should stop all', () => { 64 | 65 | return app.commands.stop({ 66 | image: 'all' 67 | }) 68 | .then(() => { 69 | 70 | let allIdle = app.imagesAsArray() 71 | .every((image) => { 72 | return image.state === 'idle'; 73 | }); 74 | 75 | expect(allIdle).to.equal(true); 76 | 77 | }); 78 | 79 | }); 80 | 81 | }); 82 | 83 | 84 | 85 | 86 | describe('Start - Restart - Stop with single image', () => { 87 | 88 | let app; 89 | 90 | before(() => { 91 | app = new App({ 92 | CWD: path.join(__dirname, '..'), 93 | logger: function() {} 94 | }); 95 | }); 96 | 97 | it('should start serviceA', () => { 98 | 99 | let logs = []; 100 | 101 | let stopStateListener = app.on('image:state', (e) => { 102 | logs.push(e._image._name + ':' + e._image.state); 103 | }); 104 | 105 | return app.commands.start({ 106 | image: 'serviceA' 107 | }) 108 | .then(() => { 109 | 110 | expect( app.getImageByName('serviceA').state ).to.equal('running'); 111 | 112 | expect(logs).to.equal([ 113 | 'serviceA:starting', 114 | 'serviceA:running' 115 | ]); 116 | 117 | stopStateListener(); 118 | }); 119 | 120 | }); 121 | 122 | 123 | it('should restart serviceA', () => { 124 | 125 | let logs = []; 126 | 127 | let stopStateListener = app.on('image:state', (e) => { 128 | logs.push(e._image._name + ':' + e._image.state); 129 | }); 130 | 131 | return app.commands.restart({ 132 | image: 'serviceA' 133 | }) 134 | .then(() => { 135 | 136 | expect( app.getImageByName('serviceA').state ).to.equal('running'); 137 | 138 | expect(logs).to.equal([ 139 | 'serviceA:stopping', 140 | 'serviceA:idle', 141 | 'serviceA:starting', 142 | 'serviceA:running' 143 | ]); 144 | 145 | stopStateListener(); 146 | }); 147 | 148 | }); 149 | 150 | 151 | it('should stop serviceA', () => { 152 | 153 | let logs = []; 154 | 155 | let stopStateListener = app.on('image:state', (e) => { 156 | logs.push(e._image._name + ':' + e._image.state); 157 | }); 158 | 159 | return app.commands.stop({ 160 | image: 'serviceA' 161 | }) 162 | .then(() => { 163 | 164 | expect( app.getImageByName('serviceA').state ).to.equal('idle'); 165 | 166 | expect(logs).to.equal([ 167 | 'serviceA:stopping', 168 | 'serviceA:idle', 169 | ]); 170 | 171 | stopStateListener(); 172 | }); 173 | 174 | }); 175 | 176 | }); 177 | }; 178 | -------------------------------------------------------------------------------- /test/unit/class-process.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const code = require('code'); 4 | const expect = code.expect; 5 | const path = require('path'); 6 | const rx = require('rx'); 7 | 8 | 9 | const Process = require('../../lib/app/lib/process'); 10 | 11 | 12 | module.exports = function() { 13 | 14 | describe('Class: Process', () => { 15 | 16 | 17 | 18 | it('should throw error on missing name', () => { 19 | 20 | let error; 21 | try { 22 | new Process(); 23 | } catch(e) { 24 | error = e; 25 | } 26 | 27 | expect(error) 28 | .to.be.an.error(); 29 | 30 | expect(error.message) 31 | .to.be.a.string() 32 | .and.equal('Missing name'); 33 | 34 | }); 35 | 36 | 37 | 38 | 39 | 40 | 41 | it('should throw error on missing command', () => { 42 | 43 | let error; 44 | try { 45 | new Process('test'); 46 | } catch(e) { 47 | error = e; 48 | } 49 | 50 | expect(error) 51 | .to.be.an.error(); 52 | 53 | expect(error.message) 54 | .to.be.a.string() 55 | .and.equal('Missing command'); 56 | 57 | }); 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | it('should throw error on invalid cwd', () => { 67 | 68 | let error; 69 | try { 70 | new Process('test', 'ls -als', '/qweqweqweqwe'); 71 | } catch(e) { 72 | error = e; 73 | } 74 | 75 | expect(error) 76 | .to.be.an.error(); 77 | 78 | expect(error.message) 79 | .to.be.a.string() 80 | .and.equal('CWD not found'); 81 | 82 | }); 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | it('should have env attribute which inherits from process.env', () => { 91 | 92 | let p = new Process('ls', 'ls -als'); 93 | 94 | 95 | expect(p.env.__proto__) 96 | .to.be.an.object() 97 | .and.equal(process.env); 98 | 99 | }); 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | it('should have env attribute without given env-vars', () => { 108 | 109 | let p = new Process('ls', 'ls -als'); 110 | 111 | expect(p.env) 112 | .to.be.an.object(); 113 | 114 | }); 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | it('should have env attribute with given env-vars', () => { 123 | 124 | let p = new Process('ls', 'ls -als', __dirname, { 125 | NODE_ENV: 'production' 126 | }); 127 | 128 | expect(p.env.NODE_ENV) 129 | .to.be.a.string() 130 | .and.equal('production'); 131 | 132 | }); 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | it('should set name attribute', () => { 141 | let p = new Process('ls', 'ls -als'); 142 | 143 | expect(p.name) 144 | .to.be.a.string() 145 | .and.equal('ls'); 146 | 147 | }); 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | it('should set cmd attribute', () => { 156 | let p = new Process('ls', 'ls -als'); 157 | 158 | expect(p.cmd) 159 | .to.be.a.string() 160 | .and.equal('ls -als'); 161 | 162 | }); 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | it('should set cwd attribute', () => { 171 | let p = new Process('ls', 'ls -als', __dirname); 172 | 173 | expect(p.cwd) 174 | .to.be.a.string() 175 | .and.equal(__dirname); 176 | 177 | }); 178 | 179 | 180 | 181 | 182 | 183 | 184 | it('should have method start()', () => { 185 | let p = new Process('ls', 'ls -als'); 186 | 187 | expect(p.start) 188 | .to.be.a.function(); 189 | }); 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | it('should have method stop()', () => { 199 | let p = new Process('ls', 'ls -als'); 200 | 201 | expect(p.stop) 202 | .to.be.a.function(); 203 | }); 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | it('should have method restart()', () => { 212 | let p = new Process('ls', 'ls -als'); 213 | 214 | expect(p.restart) 215 | .to.be.a.function(); 216 | }); 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | it('should have method getState()', () => { 225 | let p = new Process('ls', 'ls -als'); 226 | 227 | expect(p.getState) 228 | .to.be.a.function(); 229 | }); 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | it('should have logs observable', () => { 238 | 239 | let p = new Process('ls', 'ls -als'); 240 | 241 | expect(p._logs) 242 | .to.be.an.instanceOf(rx.Subject); 243 | 244 | expect(p.logs) 245 | .to.be.an.object(); 246 | 247 | expect(p.logs.subscribe) 248 | .to.be.a.function(); 249 | 250 | }); 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | it('should have state observable', () => { 259 | 260 | let p = new Process('ls', 'ls -als'); 261 | 262 | expect(p._state) 263 | .to.be.an.instanceOf(rx.BehaviorSubject); 264 | 265 | expect(p.state) 266 | .to.be.an.object(); 267 | 268 | expect(p.state.subscribe) 269 | .to.be.a.function(); 270 | 271 | }); 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | it('start() on running should reject', () => { 291 | 292 | let p = new Process('ls', 'ls -als'); 293 | 294 | return p.start() 295 | .then(() => { 296 | return p.start(); 297 | }) 298 | .catch((e) => { 299 | 300 | expect(e) 301 | .to.be.an.error(); 302 | 303 | expect(e.message) 304 | .to.be.a.string() 305 | .and.equal('Not idle'); 306 | 307 | }); 308 | 309 | }); 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | it('start() on starting should reject', () => { 319 | 320 | let p = new Process('ls', 'ls -als'); 321 | 322 | p.start(); 323 | 324 | return p.start().catch((e) => { 325 | 326 | expect(e) 327 | .to.be.an.error(); 328 | 329 | expect(e.message) 330 | .to.be.a.string() 331 | .and.equal('Not idle'); 332 | 333 | }); 334 | 335 | }); 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | it('start() on stopping should reject', () => { 344 | 345 | let p = new Process('ls', 'ls -als'); 346 | 347 | return p.start() 348 | .then(() => { 349 | 350 | p.stop(); 351 | 352 | return p.start(); 353 | }) 354 | .catch((e) => { 355 | 356 | expect(e) 357 | .to.be.an.error(); 358 | 359 | expect(e.message) 360 | .to.be.a.string() 361 | .and.equal('Not idle'); 362 | 363 | }); 364 | 365 | }); 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | it('stop() on idle should reject', () => { 376 | 377 | let p = new Process('ls', 'ls -als'); 378 | 379 | return p.stop() 380 | .catch((e) => { 381 | 382 | expect(e) 383 | .to.be.an.error(); 384 | 385 | expect(e.message) 386 | .to.be.a.string() 387 | .and.equal('Not running'); 388 | 389 | }); 390 | 391 | }); 392 | 393 | 394 | 395 | 396 | 397 | 398 | it('stop() on starting should reject', () => { 399 | 400 | let p = new Process('ls', 'ls -als'); 401 | 402 | p.start(); 403 | 404 | return p.stop() 405 | .catch((e) => { 406 | 407 | expect(e) 408 | .to.be.an.error(); 409 | 410 | expect(e.message) 411 | .to.be.a.string() 412 | .and.equal('Not running'); 413 | 414 | }); 415 | 416 | }); 417 | 418 | 419 | 420 | 421 | 422 | 423 | it('stop() on stopping should reject', () => { 424 | 425 | let p = new Process('ls', 'ls -als'); 426 | 427 | return p.start() 428 | .then(() => { 429 | 430 | p.stop(); 431 | 432 | return p.stop(); 433 | }) 434 | .catch((e) => { 435 | 436 | expect(e) 437 | .to.be.an.error(); 438 | 439 | expect(e.message) 440 | .to.be.a.string() 441 | .and.equal('Not running'); 442 | 443 | }); 444 | 445 | }); 446 | 447 | 448 | 449 | 450 | 451 | it('restart() on running should restart', () => { 452 | 453 | let p = new Process('ls', 'ls -als'); 454 | 455 | let states = []; 456 | 457 | p.state.subscribe((state) => { 458 | states.push(state); 459 | }); 460 | 461 | return p.start() 462 | .then(() => { 463 | 464 | expect(p.getState()) 465 | .to.be.a.string() 466 | .and.equal('running'); 467 | 468 | let res = p.restart(); 469 | 470 | expect(p.getState()) 471 | .to.be.a.string() 472 | .and.equal('stopping'); 473 | 474 | return res; 475 | }) 476 | .then(() => { 477 | 478 | expect(p.getState()) 479 | .to.be.a.string() 480 | .and.equal('running'); 481 | 482 | 483 | expect(states) 484 | .to.equal([ 485 | 'idle', 486 | 'starting', 487 | 'running', 488 | 'stopping', 489 | 'idle', 490 | 'starting', 491 | 'running' 492 | ]); 493 | 494 | 495 | }); 496 | 497 | }); 498 | 499 | 500 | 501 | 502 | 503 | it('restart() on idle should reject', () => { 504 | 505 | let p = new Process('ls', 'ls -als'); 506 | 507 | return p.restart() 508 | .catch((e) => { 509 | 510 | expect(e) 511 | .to.be.an.error(); 512 | 513 | expect(e.message) 514 | .to.be.a.string() 515 | .and.equal('Not running'); 516 | 517 | }); 518 | 519 | }); 520 | 521 | 522 | 523 | 524 | it('should run through expected states during whole life cycle', () => { 525 | 526 | let p = new Process('ls', 'ls -als'); 527 | 528 | let states = []; 529 | 530 | p.state 531 | .subscribe((state) => { 532 | states.push(state); 533 | }); 534 | 535 | 536 | expect(p.getState()) 537 | .to.be.a.string() 538 | .and.equal('idle'); 539 | 540 | return p.start() 541 | .then(() => { 542 | 543 | expect(p.getState()) 544 | .to.be.a.string() 545 | .and.equal('running'); 546 | 547 | let res = p.stop(); 548 | 549 | expect(p.getState()) 550 | .to.be.a.string() 551 | .and.equal('stopping'); 552 | 553 | return res; 554 | }) 555 | .then(() => { 556 | 557 | expect(p.getState()) 558 | .to.be.a.string() 559 | .and.equal('idle'); 560 | 561 | expect(states) 562 | .to.have.length(5) 563 | .and.equal([ 564 | 'idle', 565 | 'starting', 566 | 'running', 567 | 'stopping', 568 | 'idle' 569 | ]); 570 | 571 | }); 572 | 573 | }); 574 | 575 | 576 | 577 | 578 | it('should log with expected tags on start()', () => { 579 | 580 | let p = new Process('qwe', 'ls -als'); 581 | 582 | let res = p.logs 583 | .take(2) 584 | .do((e) => { 585 | 586 | expect(e.tags) 587 | .to.be.an.array() 588 | .and.have.length(4) 589 | .and.contain(['#verbose', 'log', 'qwe', '#process']); 590 | 591 | }) 592 | .toPromise() 593 | 594 | p.start(); 595 | 596 | return res; 597 | 598 | }); 599 | 600 | 601 | 602 | }); 603 | 604 | } 605 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const ngAnnotatePlugin = require('ng-annotate-webpack-plugin'); 7 | 8 | const extractScss = new ExtractTextPlugin('[name].css'); 9 | 10 | 11 | const config = module.exports = { 12 | 13 | entry: { 14 | monitor: './lib/app/monitor/src/boot' 15 | }, 16 | 17 | output: { 18 | path: './lib/app/monitor/public', 19 | filename: '[name].js', 20 | sourceMapFilename: '[name].map', 21 | }, 22 | 23 | resolve: { 24 | alias: {}, 25 | modules: [], 26 | extensions: ['', '.js', '.jsx', '.css', '.scss'] 27 | }, 28 | 29 | sassLoader: { 30 | includePaths: [ 31 | path.resolve(__dirname, 'node_modules') 32 | ] 33 | }, 34 | 35 | module: { 36 | loaders: [ 37 | // Extract css files 38 | { 39 | test: /\.scss$/, 40 | //exclude: /node_modules/, 41 | loader: extractScss.extract('style-loader', ['css-loader', 'sass-loader']) 42 | }, 43 | 44 | { 45 | test: /\.json$/, 46 | loaders: ['json-loader'], 47 | }, 48 | 49 | { 50 | test: /\.(pug|jade)$/, 51 | exclude: /node_modules/, 52 | loader: 'pug-html-loader', 53 | query: { 54 | root: __dirname 55 | } 56 | }, 57 | 58 | { 59 | test: /\.(jpe?g|png|gif|svg)$/i, 60 | loaders: [ 61 | 'file?hash=sha512&digest=hex&name=images/[hash].[ext]', 62 | 'image-webpack' 63 | ] 64 | }, 65 | 66 | { 67 | test: /\.(eot|svg|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, 68 | loader: 'file?name=fonts/[name].[ext]' 69 | }, 70 | 71 | { 72 | test: /\.(js|jsx)$/, 73 | exclude: /(node_modules|bower_components)/, 74 | loader: 'babel-loader', 75 | query: { 76 | presets: ['babel-preset-es2015'] 77 | } 78 | } 79 | ] 80 | }, 81 | 82 | plugins: [ 83 | extractScss 84 | ], 85 | 86 | }; 87 | 88 | 89 | 90 | if (process.env.NODE_ENV === 'production') { 91 | config.plugins.push(new ngAnnotatePlugin()); 92 | } else { 93 | config.devtool = 'source-map'; 94 | config.devServer = { 95 | inline: true 96 | }; 97 | } 98 | --------------------------------------------------------------------------------