Enterprise-CMCS/macpro-appian-connector

View on GitHub
src/runner.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { spawn } from "child_process";

// LabeledProcessRunner is a command runner that interleaves the output from different
// calls to run_command_and_output each with their own prefix
export default class LabeledProcessRunner {
  private prefixColors: Record<string, string> = {};
  private colors = [
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "9",
    "10",
    "11",
    "12",
    "13",
    "14",
  ];

  // formattedPrefix pads the prefix for a given process so that all prefixes are
  // right aligned in your terminal.
  private formattedPrefix(prefix: string): string {
    let color: string;

    if (prefix! in this.prefixColors) {
      color = this.prefixColors[prefix];
    } else {
      const frontColor = this.colors.shift();
      if (frontColor != undefined) {
        color = frontColor;
        this.colors.push(color);
        this.prefixColors[prefix] = color;
      } else {
        throw "dev.ts programming error";
      }
    }

    let maxLength = 0;
    for (let pre in this.prefixColors) {
      if (pre.length > maxLength) {
        maxLength = pre.length;
      }
    }

    return `\x1b[38;5;${color}m ${prefix.padStart(maxLength)}|\x1b[0m`;
  }

  // run_command_and_output runs the given shell command and interleaves its output with all
  // other commands run via this method.
  //
  // prefix: the prefix to display at the start of every line printed by this command
  // cmd: an array containing the command and all arguments to the command to be run
  // cwd: optional directory to change into before running the command
  // returns a promise that errors if the command exits error and resolves on success
  async run_command_and_output(
    prefix: string,
    cmd: string[],
    cwd: string | null,
    catchAll = false,
    silenced: {
      open?: boolean;
      stdout?: boolean;
      stderr?: boolean;
      close?: boolean;
    } = {}
  ) {
    silenced = {
      ...{ open: false, stdout: false, stderr: false, close: false },
      ...silenced,
    };
    const proc_opts = cwd ? { cwd } : {};

    const command = cmd[0];
    const args = cmd.slice(1);

    const proc = spawn(command, args, proc_opts);
    const paddedPrefix = `[${prefix}]`;
    if (!silenced.open)
      process.stdout.write(`${paddedPrefix} Running: ${cmd.join(" ")}\n`);

    const handleOutput = (data: Buffer, prefix: string, silenced: boolean) => {
      const paddedPrefix = this.formattedPrefix(prefix);
      if (!silenced)
        for (let line of data.toString().split("\n")) {
          process.stdout.write(`${paddedPrefix} ${line}\n`);
        }
    };

    proc.stdout.on("data", (data) =>
      handleOutput(data, prefix, silenced.stdout!)
    );
    proc.stderr.on("data", (data) =>
      handleOutput(data, prefix, silenced.stderr!)
    );

    return new Promise<void>((resolve, reject) => {
      proc.on("error", (error) => {
        if (!silenced.stderr)
          process.stdout.write(`${paddedPrefix} A PROCESS ERROR: ${error}\n`);
        reject(error);
      });

      proc.on("close", (code) => {
        if (!silenced.close)
          process.stdout.write(`${paddedPrefix} Exit: ${code}\n`);
        // If there's a failure and we haven't asked to catch all...
        if (code != 0 && !catchAll) {
          // This is not my area.
          // Deploy failures don't get handled and show up here with non zero exit codes
          // Here we throw an error.  Not sure what's best.
          throw `Exit ${code}`;
        }
        resolve();
      });
    });
  }
}