diff --git a/.vscode/launch.json b/.vscode/launch.json index 149fa31..084328b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "name": "Launch", "type": "node", "request": "launch", - "program": "${workspaceRoot}/index.js", + "program": "${workspaceRoot}/dist/debug.js", "stopOnEntry": false, "args": [], "cwd": "${workspaceRoot}", @@ -17,22 +17,6 @@ "env": { "NODE_ENV": "development" }, - "externalConsole": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/dist" - }, { - "name": "AVA", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/node_modules/ava/cli.js", - "stopOnEntry": true, - "args": ["--serial"], - "cwd": "${workspaceRoot}", - "preLaunchTask": null, - "runtimeExecutable": null, - "env": { - "NODE_ENV": "development" - }, "externalConsole": true, "sourceMaps": true, "outDir": "${workspaceRoot}/dist" diff --git a/README.md b/README.md index 0e30973..4bfb175 100644 --- a/README.md +++ b/README.md @@ -8,31 +8,20 @@ A simple tool for running multiple projects at once. This module will recursively scan the current directory, looking for supported project types. After selecting which projects you'd like to run, it will execute them all simultaneously, perfect for those multi-tiered projects! -## Example: +## Installation: ```sh -# Run select .NET Core 1.0 projects... -pb - -# Run select node projects... -md node - -# Run any selection of any projects... -md +npm i -g @jimmyboh/playbook ``` -```ts -import * as fs from 'fs'; -import {Playbook} from '@jimmyboh/playbook'; +## Usage: -// tbd... -let pb = new Playbook(); -``` +_Coming Soon!_ ## Features: - - Auto-scan for projects. - - Intelligently runs each project type. - - Remembers runtime configurations for rapid usage. + - Auto-scans filesystem for projects. + - Supports multiple project types (`dotnet`, `node`, `npm` tasks, and growing). + - Interactive REPL for advanced editing, or direct commands for quick access. ## Contribute diff --git a/src/bin/pb.ts b/src/bin/pb.ts index d5f3937..a252a35 100644 --- a/src/bin/pb.ts +++ b/src/bin/pb.ts @@ -7,7 +7,7 @@ const ansiEscapes = require('ansi-escapes'); import {Playbook, Project, Play} from '../'; import {first} from '../services/utils'; -import {ProcessDisplay} from '../services/process-display'; +import {ProcessManager} from '../services/process-manager'; interface Answers { [key: string]: any; @@ -16,6 +16,7 @@ interface Answers { const app = require('vorpal')(); const pb = new Playbook(); +let procManager: ProcessManager; //#region Commands @@ -29,8 +30,8 @@ const autocompletePlays = { }; app - .command('list [playName]', 'Shows available plays') - .alias('ls').alias('show') + .command('list [playName]', 'Shows available plays currently regsitered.') + .alias('ls', 'show') .autocomplete(autocompletePlays) .action(function (args: ParsedArgs) { return showPlays.call(this, args) @@ -39,67 +40,79 @@ app }); }); +let lastCreatedPlay: Play; app - .command('new [playName]', 'Creates a new play.') + .command('new [playName]', 'Create a new play with a collection of projects.') .alias('create') .action(function (args: ParsedArgs) { let action: Promise; - + lastCreatedPlay = null; return inputPlayName.call(this, args) .then((playName: string) => { - return pb.create(playName, process.cwd()) + return pb.create(playName, process.cwd()).then(play => lastCreatedPlay = play); }) .then(editPlay.bind(this)) + .then(() => { + lastCreatedPlay = null; + }) .catch((err: Error) => { - this.log('An error occured while creating...'); + this.log(chalk.bgRed.white('An error occured while creating! Please try again.')); + return deletePlay(lastCreatedPlay).then(() => lastCreatedPlay = null); }); + }) + .cancel(() => { + if (lastCreatedPlay) return deletePlay(lastCreatedPlay).then(() => lastCreatedPlay = null); }); app - .command('edit [playName]', 'Edit an existing play.') - .alias('update').alias('change') + .command('edit [playName]', 'Edit the projects assigned to an existing play.') + .alias('update', 'change') .autocomplete(autocompletePlays) .action(function (args: ParsedArgs) { return selectPlay.call(this, args) .then(editPlay.bind(this)) .catch((err: Error) => { - this.log('An error occured while editing...'); + this.log(chalk.bgRed.white('An error occured while editing...')); }); }); app .command('delete [playName]', 'Delete an existing play.') - .alias('del').alias('rm') + .alias('del', 'remove', 'rm') .autocomplete(autocompletePlays) .action(function (args: ParsedArgs) { return selectPlay.call(this, args) .then(deletePlay.bind(this)) .catch((err: Error) => { - this.log('An error occured while deleting...'); + this.log(chalk.bgRed.white('An error occured while deleting...')); }); }); - -let procDisplay: ProcessDisplay; app - .command('run [playName]', 'Executes a play.') - .alias('exec').alias('start') + .command('run [playName]', 'Run a play!') + .alias('exec', 'start') .autocomplete(autocompletePlays) .action(function (args: ParsedArgs) { return selectPlay.call(this, args) .then(runPlay.bind(this)) .catch((err: Error) => { - this.log('An error occured while running...'); + this.log(chalk.bgRed.white('An error occured while running... %o', err)); }); }) .cancel(function () { - - procDisplay.cancel(); + procManager.cancel(); }); +// app +// .command('clear', 'Resets the console to a blank slate.') +// .alias('cls') +// .action(function(args: ParsedArgs){ +// this.log('\x1B[2J\x1B[0f'); +// }); + app .delimiter('playbook~$') .show() @@ -120,6 +133,9 @@ function showPlays(args: ParsedArgs): Promise{ play.projects.forEach(project => { this.log(` ${project.name}`); }); + }).catch(err => { + this.log(chalk.red(`Play "${playName}" not found!`)); + return showPlays.call(this, {}); }); } @@ -127,7 +143,7 @@ function showPlays(args: ParsedArgs): Promise{ .getAll() .then((plays: Play[]) => { if (plays.length === 0) { - this.log(`No plays found! (Try 'playbook new' to create one)`) + this.log(`No plays found! (Try 'new' to create one)`) } else { plays.forEach(play => { this.log(` ${play.toString()}`); @@ -149,7 +165,7 @@ function inputPlayName(args: ParsedArgs): Promise { { type: 'input', name: answerName, - message: 'What is the name of this play?', + message: 'What is the name of this play? ', default: args['name'], } ]).then((answers: Answers) => { @@ -173,8 +189,10 @@ function selectPlay(args: ParsedArgs): Promise { { type: 'list', name: answerName, - message: 'Which play would you like?', - choices: plays.map(play => play.name) + message: 'Which play would you like? ', + choices: plays.map(play => { + return {name: play.toString(), value: play.name} + }) } ]).then((answers: Answers): Promise => { playName = answers[answerName]; @@ -202,7 +220,7 @@ function editPlay(play: Play): Promise { { type: 'checkbox', name: answerName, - message: 'Which projects should be included?', + message: 'Which projects should be included? ', choices, default: defaults } @@ -227,7 +245,7 @@ function deletePlay(play: Play): Promise{ { type: 'confirm', name: answerName, - message: 'Are you sure you want to delete this play?', + message: `Are you sure you want to delete "${play.toString()}"? `, } ]).then((answers: Answers) => { let yes = answers[answerName]; @@ -240,12 +258,11 @@ function deletePlay(play: Play): Promise{ function runPlay(play: Play): Promise{ return new Promise((resolve, reject) => { - let processes = play.run(); - procDisplay = new ProcessDisplay(processes); + procManager = play.run(); - return procDisplay.render((text) => { + return procManager.render((text) => { app.ui.redraw(text); - }, 200); + }, 100); }); } diff --git a/src/debug.ts b/src/debug.ts new file mode 100644 index 0000000..bb95946 --- /dev/null +++ b/src/debug.ts @@ -0,0 +1,15 @@ + +import {Playbook} from './services/playbook'; +import {ProcessManager} from './services/process-manager'; + +let pb = new Playbook(); +let procManager: ProcessManager; + +pb.get('main-app').then(play => { + procManager = play.run(); + + return procManager.render((text) => { + process.stdout.write('\x1B[2J\x1B[0f'); + process.stdout.write(text); + }, 400); +}); \ No newline at end of file diff --git a/src/handlers/node.ts b/src/handlers/node.ts index 979ef8d..65ab196 100644 --- a/src/handlers/node.ts +++ b/src/handlers/node.ts @@ -1,5 +1,5 @@ import * as fs from 'fs'; -import {basename, dirname} from 'path'; +import {basename, dirname, join} from 'path'; import * as pify from 'pify'; const $fs = pify(fs); @@ -31,7 +31,12 @@ export const nodeHandler: ProjectHandler = { packageJson.title = packageJson.title || basename(cwd); packageJson.main = packageJson.main || 'index.js'; - let projects: Project[] = [new NodeProject(cwd, packageJson)]; + let projects: Project[] = []; + + if(fs.existsSync(join(cwd, packageJson.main))) + { + projects.push(new NodeProject(cwd, packageJson)); + } if (packageJson.scripts) { Object.keys(packageJson.scripts) diff --git a/src/models/play.ts b/src/models/play.ts index f115bad..fac6820 100644 --- a/src/models/play.ts +++ b/src/models/play.ts @@ -2,6 +2,7 @@ import {ChildProcess, exec} from 'child_process'; import {IProject, Project} from './project'; +import {ProcessManager} from '../services/process-manager'; export interface IPlay { @@ -28,16 +29,22 @@ export class Play implements IPlay this.projects = (data.projects || []).map(proj => new Project(proj)); } - public run(): Lookup + public run(): ProcessManager { - let procs: Lookup = {}; - this.projects.forEach(proj => { - procs[proj.name] = exec(`${proj.command} ${proj.args.join(' ')}`, { cwd: proj.cwd }); + let projs = this.projects.map(proj => { + proj.currentProcess = exec(`${proj.command} ${proj.args.join(' ')}`, { cwd: proj.cwd }); + return proj; }); - return procs; + return new ProcessManager(projs); } + + /** + * Prints out the name and count of projects. + * + * @returns {string} + */ public toString(): string { return `${this.name} (${this.projects.length})`; } diff --git a/src/models/project.ts b/src/models/project.ts index 1355296..41781d7 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -29,6 +29,8 @@ export interface IProject command?: string; args?: string[]; + + currentProcess?: ChildProcess; } export class Project implements IProject @@ -41,10 +43,13 @@ export class Project implements IProject public args: string[]; + public currentProcess: ChildProcess; + constructor(opts?: IProject) { this.name = opts && opts.name; this.cwd = opts && opts.cwd; this.command = opts && opts.command; this.args = (opts && opts.args) || []; + this.currentProcess = opts && opts.currentProcess; } } \ No newline at end of file diff --git a/src/services/process-display.ts b/src/services/process-display.ts deleted file mode 100644 index 8e64fe3..0000000 --- a/src/services/process-display.ts +++ /dev/null @@ -1,95 +0,0 @@ -import {ChildProcess} from 'child_process'; -import {Queue} from './utils'; -const chalk = require('chalk'); - -const GRAY_LINE = chalk.gray('_'); -const GRAY_BLOCK = chalk.gray('█'); -const GREEN_BLOCK = chalk.green('█'); -const RED_BLOCK = chalk.red('█'); -const STATUS_BAR_WIDTH = 40; - -interface ProcessTracker { - name: string; - process: ChildProcess; - step: number; - buffer: Queue; -} - -class StatusQueue extends Queue -{ - constructor() { - super(STATUS_BAR_WIDTH); - - this.length = STATUS_BAR_WIDTH; - this.fill(GRAY_LINE); - } - - public toString() - { - return this.join(''); - } -} - -export class ProcessDisplay -{ - public processNames: string[]; - - private _processes: Array; - - private _maxNameLength: number = 0; - - private _interval: NodeJS.Timer; - - constructor(processes: Lookup) { - - let processNames = Object.keys(processes); - - this._processes = processNames.map(name => { - let process = processes[name]; - this._maxNameLength = Math.max(this._maxNameLength, name.length) - let tracker: ProcessTracker = { - name, - process, - step: 0, - buffer: new StatusQueue() - }; - - tracker.process.stdout.on('data', (data: string) => { - tracker.buffer.enqueue(GREEN_BLOCK); - }); - - tracker.process.stderr.on('data', (data: string) => { - tracker.buffer.enqueue(RED_BLOCK); - }); - - tracker.process.on('close', (code:number, signal: string) => { - tracker.buffer.enqueue(GRAY_BLOCK); - }); - - return tracker; - }); - } - - public render(drawFn: (str: string) => void, delay: number): Promise - { - return new Promise((resolve, reject) => { - this._interval = setInterval(() => { - - let projectList = this._processes.map(proc => { - let paddingSpaces = (new Array(Math.max(0, this._maxNameLength - proc.name.length))).fill(' ').join(''); - return `${paddingSpaces}${proc.name}: ${proc.buffer.toString()}`; - }).join('\n'); - - drawFn(`Projects: ------------------------------------- -${projectList} -`); - }, delay); - }); - } - - public cancel(): void - { - clearInterval(this._interval); - } -} \ No newline at end of file diff --git a/src/services/process-manager.ts b/src/services/process-manager.ts new file mode 100644 index 0000000..d6d7d0e --- /dev/null +++ b/src/services/process-manager.ts @@ -0,0 +1,155 @@ +import {ChildProcess} from 'child_process'; +import {EOL} from 'os'; +const chalk = require('chalk'); + +import {Queue} from './utils'; +import {Project} from '../models/project'; + +const MAX_ERROR_LENGTH = 500; + +const COLORS = [ + chalk.blue, + chalk.magenta, + chalk.cyan, + chalk.green, + chalk.yellow, + chalk.red +]; + +function getColor(i: number): ((message: any) => string){ + return COLORS[i % COLORS.length]; +} + +const SPINNER_CHARS = '▁▃▄▅▆▇█▇▆▅▄▃';//'▉▊▋▌▍▎▏▎▍▌▋▊▉';// '|/-\\'; + +function getSpinnerChar(p: ProcessTracker): string { + return p.color(SPINNER_CHARS[p.step % SPINNER_CHARS.length]); +} + +const GRAY_LINE = chalk.gray('_'); +const GRAY_BLOCK = chalk.gray('█'); +const GREEN_BLOCK = chalk.green('█'); +const RED_BLOCK = chalk.red('█'); +const STATUS_BAR_WIDTH = 40; + +interface ProcessTracker { + name: string; + process: ChildProcess; + step: number; + buffer: Queue; + color?: (message: any) => string; + lastError?: string; +} + +class StatusQueue extends Queue +{ + constructor() { + super(STATUS_BAR_WIDTH); + + this.length = STATUS_BAR_WIDTH; + this.fill(GRAY_LINE); + } + + public toString() + { + return this.join(''); + } +} + +export class ProcessManager +{ + private _processNames: string[]; + + private _processes: Array; + + private _maxNameLength: number = 0; + + private _interval: NodeJS.Timer; + + private _lastError: Lookup = {}; + + constructor(projects: Project[]) { + this._processNames = []; + this._processes = projects.map((project, index) => { + this._processNames.push(project.name); + this._maxNameLength = Math.max(this._maxNameLength, project.name.length) + let tracker: ProcessTracker = { + name: project.name, + process: project.currentProcess, + step: 0, + buffer: new StatusQueue(), + color: getColor(index) + }; + + tracker.process.stdout.on('data', (data: string) => { + tracker.step++; + tracker.buffer.enqueue(GREEN_BLOCK); + }); + + tracker.process.stderr.on('data', (data: string) => { + tracker.step++; + tracker.buffer.enqueue(RED_BLOCK); + this._lastError[tracker.name] = tracker.color(data.substring(0, MAX_ERROR_LENGTH)); + }); + + tracker.process.on('close', (code:number, signal: string) => { + tracker.step++; + tracker.buffer.enqueue(GRAY_BLOCK); + }); + tracker.process.on('error', (code:number, signal: string) => { + tracker.step++; + tracker.buffer.enqueue(RED_BLOCK); + this._lastError[tracker.name] = tracker.color(`Process Error: ${code} (${signal})`); + }); + tracker.process.on('exit', (code:number, signal: string) => { + tracker.step++; + tracker.buffer.enqueue(GRAY_BLOCK); + }); + + return tracker; + }); + } + + public render(drawFn: (str: string) => void, delay: number): Promise + { + return new Promise((resolve, reject) => { + this._interval = setInterval(() => { + + let drawString = `Projects: +------------------------------------ +`; + + let projectNum = 0; + let projectList = this._processes.map(proc => { + let paddingSpaces = (new Array(Math.max(0, this._maxNameLength - proc.name.length))).fill(' ').join(''); + return `${paddingSpaces}${proc.color(proc.name)}: ${proc.buffer.toString()} ${getSpinnerChar(proc)}`; + }).join(EOL); + + drawString += projectList; + + + drawString += ` +Errors: +------------------------------------ +`; + + this._processNames.forEach(procName => { + if(this._lastError[procName]){ + drawString += this._lastError[procName] + EOL; + } + }); + + drawString += ` +------------------------------------ +`; + + drawFn(drawString); + }, delay); + }); + } + + public cancel(): void + { + clearInterval(this._interval); + } +} \ No newline at end of file diff --git a/src/services/utils.ts b/src/services/utils.ts index 94de0fc..d71bf03 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -135,7 +135,7 @@ export class Queue extends Array * @param {T} item */ public enqueue(item: T) { - this.unshift(item); + this.push(item); if (this._limit > 0 && this.length > this._limit) { this.dequeue(); @@ -148,6 +148,6 @@ export class Queue extends Array * @returns {T} */ public dequeue(): T { - return this.pop(); + return this.shift(); } } \ No newline at end of file