UNPKG

too

Version:

Combine multiple processes' Stdout/Stderr/SIGINT to keep them all foreground

91 lines (90 loc) 3.32 kB
import * as child_process from 'child_process'; import * as path from 'path'; import { lookpath } from 'lookpath'; import { CYAN } from './colors.js'; import { DefaultLogger } from './logger.js'; export class Command { oneliner; env; color; logger; stdout = process.stdout; stderr = process.stderr; label; command; constructor(oneliner, env = {}, color = CYAN, logger = new DefaultLogger(), label) { this.oneliner = oneliner; this.env = env; this.color = color; this.logger = logger; // The binary is the first token that is not a leading `KEY=value` env // assignment (e.g. `FOO=bar node app.js` resolves to `node`, not `FOO=bar`). (#532) const tokens = this.oneliner.trim().split(/\s+/); let i = 0; while (i < tokens.length - 1 && /^[A-Za-z_]\w*=/.test(tokens[i])) i++; this.command = tokens[i] || ""; this.label = label || this.command; } async start() { const bin = await lookpath(this.command, { include: this.path() }); if (!bin) { return Promise.reject({ msg: `command not found: ${this.command}`, code: 127 }); } this.logger.accept(this.label, this.color, this.oneliner); const stream = child_process.spawn(this.oneliner, { // detached makes each child a process-group leader, so cleanup() can // signal the whole tree (shell + grandchildren) via process.kill(-pid). detached: true, env: { ...process.env, ...this.env, PATH: this.path().join(path.delimiter), }, shell: true, stdio: ["pipe", "pipe", "pipe"], }); stream.stdout.on("data", (chunk) => { this.logger.output(this.label, this.color, chunk, ""); }); stream.stderr.on("data", (chunk) => { this.logger.output(this.label, this.color, "", chunk); }); stream.on("close", (code, signal) => { if (code !== 0) { this.logger.output(this.label, this.color, "", `exit code ${code !== null ? code : signal}`); } }); return stream; } path() { const additionals = []; const cwd = process.cwd(); if ((path.join(cwd, "package.json"))) { additionals.push(path.join(cwd, "node_modules", ".bin")); } return [process.env.PATH || "", ...additionals]; } /** * Execute the command synchronously. */ async exec(env = this.env) { const bin = await lookpath(this.command, { include: this.path() }); if (!bin) { return Promise.reject({ msg: `command not found: ${this.command}`, code: 127 }); } this.logger.accept(this.label, this.color, this.oneliner); return new Promise((resolve, reject) => { child_process.exec(this.oneliner, { env: { ...process.env, ...env, }, }, (err, stdout, stderr) => { if (err) return reject(err); this.logger.output(this.label, this.color, stdout, stderr); return resolve(0); }); }); } }