diff --git a/bin/cli/src/commands/deploy.ts b/bin/cli/src/commands/deploy.ts index 9dc101755a..7ccbba928a 100644 --- a/bin/cli/src/commands/deploy.ts +++ b/bin/cli/src/commands/deploy.ts @@ -1,7 +1,7 @@ import { Argv } from "yargs"; import { checkIfAuthenticated, - LabeledProcessRunner, + runCommand, project, region, writeUiEnvFile, @@ -14,8 +14,6 @@ import { } from "@aws-sdk/client-cloudfront"; import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm"; -const runner = new LabeledProcessRunner(); - export const deploy = { command: "deploy", describe: "deploy the project", @@ -24,19 +22,15 @@ export const deploy = { }, handler: async (options: { stage: string; stack?: string }) => { await checkIfAuthenticated(); - await runner.run_command_and_output( - "CDK Deploy", - ["cdk", "deploy", "-c", `stage=${options.stage}`, "--all"], + await runCommand( + "cdk", + ["deploy", "-c", `stage=${options.stage}`, "--all"], ".", ); await writeUiEnvFile(options.stage); - await runner.run_command_and_output( - "Build", - ["bun", "run", "build"], - "react-app", - ); + await runCommand("bun", ["run", "build"], "react-app"); const { s3BucketName, cloudfrontDistributionId } = JSON.parse( ( @@ -63,14 +57,14 @@ export const deploy = { // There's a mime type issue when aws s3 syncing files up // Empirically, this issue never presents itself if the bucket is cleared just before. // Until we have a neat way of ensuring correct mime types, we'll remove all files from the bucket. - await runner.run_command_and_output( - "S3 Clean", - ["aws", "s3", "rm", `s3://${s3BucketName}/`, "--recursive"], + await runCommand( + "aws", + ["s3", "rm", `s3://${s3BucketName}/`, "--recursive"], ".", ); - await runner.run_command_and_output( - "S3 Sync", - ["aws", "s3", "sync", buildDir, `s3://${s3BucketName}/`], + await runCommand( + "aws", + ["s3", "sync", buildDir, `s3://${s3BucketName}/`], ".", ); diff --git a/bin/cli/src/commands/docs.ts b/bin/cli/src/commands/docs.ts index 345af28f9d..a20075197d 100644 --- a/bin/cli/src/commands/docs.ts +++ b/bin/cli/src/commands/docs.ts @@ -1,7 +1,5 @@ import { Argv } from "yargs"; -import { LabeledProcessRunner } from "../lib"; - -const runner = new LabeledProcessRunner(); +import { runCommand } from "../lib"; export const docs = { command: "docs", @@ -14,17 +12,12 @@ export const docs = { default: false, }), handler: async ({ stop }: { stop: boolean }) => { - await runner.run_command_and_output( - "Stop any existing container.", - ["docker", "rm", "-f", "jekyll"], - "docs", - ); + await runCommand("docker", ["rm", "-f", "jekyll"], "docs"); if (!stop) { - await runner.run_command_and_output( - "Run docs at http://localhost:4000", + await runCommand( + "docker", [ - "docker", "run", "--rm", "-i", diff --git a/bin/cli/src/commands/e2e.ts b/bin/cli/src/commands/e2e.ts index 0874957631..8d06be5d48 100644 --- a/bin/cli/src/commands/e2e.ts +++ b/bin/cli/src/commands/e2e.ts @@ -1,7 +1,5 @@ import { Argv } from "yargs"; -import { checkIfAuthenticated, LabeledProcessRunner } from "../lib"; - -const runner = new LabeledProcessRunner(); +import { checkIfAuthenticated, runCommand } from "../lib"; export const e2e = { command: "e2e", @@ -14,16 +12,8 @@ export const e2e = { }), handler: async ({ ui }: { ui: boolean }) => { await checkIfAuthenticated(); - await runner.run_command_and_output( - "Install playwright", - ["bun", "playwright", "install", "--with-deps"], - ".", - ); + await runCommand("bun", ["playwright", "install", "--with-deps"], "."); - await runner.run_command_and_output( - ui ? "e2e:ui tests" : "e2e tests", - ["bun", ui ? "e2e:ui" : "e2e"], - ".", - ); + await runCommand("bun", [ui ? "e2e:ui" : "e2e"], "."); }, }; diff --git a/bin/cli/src/commands/install.ts b/bin/cli/src/commands/install.ts index 9f2bc6743b..9f3ef08ff3 100644 --- a/bin/cli/src/commands/install.ts +++ b/bin/cli/src/commands/install.ts @@ -1,16 +1,9 @@ -import { LabeledProcessRunner } from "../lib"; - -const runner = new LabeledProcessRunner(); +import { runCommand } from "../lib"; export const install = { command: "install", describe: "install all project dependencies", handler: async () => { - await runner.run_command_and_output( - "Install", - ["bun", "install"], - ".", - true, - ); + await runCommand("bun", ["install"], "."); }, }; diff --git a/bin/cli/src/commands/logs.ts b/bin/cli/src/commands/logs.ts index ebba23093b..43d9093aae 100644 --- a/bin/cli/src/commands/logs.ts +++ b/bin/cli/src/commands/logs.ts @@ -1,7 +1,7 @@ import { Argv } from "yargs"; import { checkIfAuthenticated, - LabeledProcessRunner, + runCommand, project, region, setStageFromBranch, @@ -20,8 +20,6 @@ import prompts from "prompts"; const lambdaClient = new LambdaClient({ region }); -const runner = new LabeledProcessRunner(); - export const logs = { command: "logs", describe: "Stream a lambda's cloudwatch logs.", @@ -79,9 +77,9 @@ export const logs = { const lambdaLogGroup = await getLambdaLogGroup(lambda); // Stream the logs - await runner.run_command_and_output( - "stream awslogs", - ["awslogs", "get", lambdaLogGroup, "-s10m", "--watch"], + await runCommand( + "awslogs", + ["get", lambdaLogGroup, "-s10m", "--watch"], ".", ); }, diff --git a/bin/cli/src/commands/test.ts b/bin/cli/src/commands/test.ts index ee6d4b32f3..172e030a20 100644 --- a/bin/cli/src/commands/test.ts +++ b/bin/cli/src/commands/test.ts @@ -1,7 +1,5 @@ import { Argv } from "yargs"; -import { LabeledProcessRunner } from "../lib"; - -const runner = new LabeledProcessRunner(); +import { runCommand } from "../lib"; export const test = { command: "test", @@ -33,10 +31,6 @@ export const test = { if (argv.ui) { testCommand = "test:ui"; } - await runner.run_command_and_output( - "Unit Tests", - ["bun", "run", testCommand], - ".", - ); + await runCommand("bun", ["run", testCommand], "."); }, }; diff --git a/bin/cli/src/commands/ui.ts b/bin/cli/src/commands/ui.ts index 7a3ad5b813..2ea799fcc5 100644 --- a/bin/cli/src/commands/ui.ts +++ b/bin/cli/src/commands/ui.ts @@ -1,13 +1,11 @@ import { Argv } from "yargs"; import { checkIfAuthenticated, - LabeledProcessRunner, + runCommand, setStageFromBranch, writeUiEnvFile, } from "../lib"; -const runner = new LabeledProcessRunner(); - export const ui = { command: "ui", describe: "Run react-server locally against an aws backend", @@ -18,15 +16,7 @@ export const ui = { await checkIfAuthenticated(); const stage = options.stage || (await setStageFromBranch()); await writeUiEnvFile(stage, true); - await runner.run_command_and_output( - `Build`, - ["bun", "run", "build"], - "react-app", - ); - await runner.run_command_and_output( - `Run`, - ["bun", "run", "dev"], - `react-app`, - ); + await runCommand("bun", ["run", "build"], "react-app"); + await runCommand("bun", ["run", "dev"], `react-app`); }, }; diff --git a/bin/cli/src/lib/runner.ts b/bin/cli/src/lib/runner.ts index f35b6625f0..731b908fec 100644 --- a/bin/cli/src/lib/runner.ts +++ b/bin/cli/src/lib/runner.ts @@ -1,126 +1,28 @@ -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 class LabeledProcessRunner { - private prefixColors: Record = {}; - 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; +import { spawn, SpawnOptions } from "child_process"; + +export async function runCommand( + command: string, + args: string[], + cwd: string | null, +): Promise { + return new Promise((resolve, reject) => { + const options: SpawnOptions = cwd + ? { cwd, stdio: ["inherit", "inherit", "inherit"] } + : { stdio: ["inherit", "inherit", "inherit"] }; + + const proc = spawn(command, args, options); + + proc.on("error", (error) => { + console.error(`Error: ${error.message}`); + reject(error); + }); - 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; + proc.on("close", (code) => { + if (code !== 0) { + reject(new Error(`Command failed with exit code ${code}`)); } 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`; - } - - private sanitizeInput(input) { - // A basic pattern that allows letters, numbers, dashes, underscores, and periods - // Adjust the pattern to fit the expected input format - const sanitizedInput = input.replace(/[^a-zA-Z0-9-_\.]/g, ""); - return sanitizedInput; - } - - // 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 = this.sanitizeInput(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((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(); - }); + } }); - } + }); }