Skip to content

Instantly share code, notes, and snippets.

@samwightt
Last active September 4, 2025 16:00
Show Gist options
  • Select an option

  • Save samwightt/f78b0bffddca4f74b06519cae285da1c to your computer and use it in GitHub Desktop.

Select an option

Save samwightt/f78b0bffddca4f74b06519cae285da1c to your computer and use it in GitHub Desktop.
Runs a list of commands concurrently using bun. npm-run-all but in less than 100 lines of code.
interface Runnable {
name: string;
command: string[];
}
function createPrefixedWriter(prefix: string, output: Bun.BunFile) {
let buffer = "";
// Tried implementing this using a TransformStream but Bun didn't like it for some reason. So we use a
// WritableStream and Bun.write instead :(
const textWriter = new WritableStream({
write(chunk) {
buffer += chunk;
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // Keep the last incomplete line
for (const line of lines) {
Bun.write(output, `[${prefix}] ${line}\n`);
}
},
close() {
// Handle any remaining buffer
if (buffer) {
Bun.write(output, `[${prefix}] ${buffer}\n`);
}
},
});
const decoderStream = new TextDecoderStream();
decoderStream.readable.pipeTo(textWriter);
return decoderStream.writable;
}
// Takes list of commands and runs them.
export async function runConcurrently(commands: Runnable[]) {
const controller = new AbortController();
const { signal } = controller;
const processes = commands.map((cmd) => {
const proc = Bun.spawn(cmd.command, {
stdout: "pipe",
stderr: "pipe",
signal,
});
proc.stdout.pipeTo(createPrefixedWriter(cmd.name, Bun.stdout));
proc.stderr.pipeTo(createPrefixedWriter(cmd.name, Bun.stderr));
return proc;
});
const waitForAllProcessesExited = () =>
Promise.all(processes.map((p) => p.exited));
let exiting = false;
// Ask processes nicely to quit, wait 5 seconds, then nuke them if they haven't yet.
async function killAllProcesses() {
// Can receive sigint more than once for some reason. Also don't want to kill when we're already killing
// and one of the other processes has exited.
if (exiting) return;
exiting = true;
console.log("Gracefully shutting down...");
for (const item of processes) {
item.kill("SIGTERM");
}
const res = await Promise.race([
Bun.sleep(5000),
waitForAllProcessesExited(),
]);
if (res === undefined) {
console.log("Processes not quitting, sending SIGKILL...");
// controller.abort sends a SIGKILL
controller.abort();
}
process.exit(0);
}
for (const item of processes) {
item.exited.then(() => {
if (!exiting) {
console.log("Shutting down because process failed...");
killAllProcesses();
}
});
}
process.on("SIGINT", killAllProcesses);
await waitForAllProcessesExited();
console.log("Finished killing processes.");
}
// Run with bun run ./example.ts
import { runConcurrently } from './bun-concurrent'
// Silence TS.
export {};
await runConcurrently([
{
name: "dragonflydb",
command: [
"docker",
"run",
"--name",
"dragonfly",
"--rm",
"-p",
"6379:6379",
"--ulimit",
"memlock=-1",
"docker.dragonflydb.io/dragonflydb/dragonfly",
],
},
{
name: "server",
command: ["bun", "run", "dev:server"],
},
]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment