stackrun
Version:
stackrun is a wrapper around [concurrently](https://www.npmjs.com/package/concurrently) and [cf-tunnel](https://www.npmjs.com/package/cf-tunnel) that simplifies running multiple services with optional integrated Cloudflare tunneling.
143 lines (140 loc) • 5.02 kB
JavaScript
import { execSync } from 'node:child_process';
import chalk from 'chalk';
import concurrently from 'concurrently';
import consola from 'consola';
function defineStackrunConfig(config) {
return config;
}
async function stackrun(config) {
const {
concurrentlyOptions = {},
tunnelEnabled = false,
cfTunnelConfig = {
cloudflaredConfigDir: void 0,
cfToken: process.env.CLOUDFLARE_TOKEN,
tunnelName: process.env.CLOUDFLARE_TUNNEL_NAME || "stackrun",
removeExistingTunnel: false,
removeExistingDns: false
},
beforeCommands = [],
afterCommands = [],
commands = []
} = config;
concurrentlyOptions.killOthers = concurrentlyOptions?.killOthers || "failure";
concurrentlyOptions.handleInput = concurrentlyOptions?.handleInput || true;
concurrentlyOptions.prefixColors = concurrentlyOptions?.prefixColors || "auto";
concurrentlyOptions.prefixLength = concurrentlyOptions?.prefixLength || 10;
const concurrentlyCommands = commands.filter((command) => typeof command.command === "string").map((command) => {
const env = command.env || {};
const tunnelEnv = { ...env, ...command.tunnelEnv };
return {
...command,
// Include everything
name: (() => {
if (!command.name) return void 0;
if (concurrentlyOptions.prefixLength) {
return command.name.slice(0, concurrentlyOptions.prefixLength);
}
return command.name;
})(),
command: command.command,
env: tunnelEnabled ? tunnelEnv : env
};
});
if (tunnelEnabled) {
consola.info("Tunneling is enabled");
const cfToken = cfTunnelConfig.cfToken || process.env.CF_TOKEN || process.env.CLOUDFLARE_TOKEN;
if (!cfToken) {
consola.error("Cloudflare token is required for tunneling");
return;
}
const tunnelName = cfTunnelConfig.tunnelName || process.env.CF_TUNNEL_NAME || process.env.CLOUDFLARE_TUNNEL_NAME || "stackrun";
const ingress = commands.filter((cmd) => cmd.tunnelUrl && cmd.url).map((command) => ({
hostname: command.tunnelUrl.replace("https://", "").replace("http://", ""),
service: command.url
}));
if (ingress.length === 0) {
consola.warn("No valid tunnel configurations found");
return;
}
const removeExistingDns = cfTunnelConfig.removeExistingDns || false;
const removeExistingTunnel = cfTunnelConfig.removeExistingTunnel || false;
const tunnelConfig = {
cfToken,
tunnelName,
ingress,
removeExistingDns,
removeExistingTunnel
};
const tunnelCommand = {
// command: `node --no-warnings -e "require('cf-tunnel').cfTunnel(${JSON.stringify(JSON.stringify(tunnelConfig))})"`,
command: `node --no-warnings -e 'require("cf-tunnel").cfTunnel(${JSON.stringify(tunnelConfig)})'`,
// { name, prefixColor, env, cwd, ipc }
name: (() => {
let displayName = cfTunnelConfig?.commandOptions?.name || "Tunnel";
if (concurrentlyOptions.prefixLength) {
displayName = displayName.slice(0, concurrentlyOptions.prefixLength);
}
if (cfTunnelConfig?.commandOptions?.prefixColor) {
return displayName;
} else {
const rainbowColors = [
"red",
"yellowBright",
"yellow",
"green",
"blue",
"magenta",
"cyan"
];
return [...displayName].map((char, i) => {
const colorIndex = i % rainbowColors.length;
const color = rainbowColors[colorIndex];
return chalk[color](char);
}).join("");
}
})(),
prefixColor: cfTunnelConfig?.commandOptions?.prefixColor,
env: cfTunnelConfig?.commandOptions?.env || {},
cwd: cfTunnelConfig?.commandOptions?.cwd || void 0,
ipc: cfTunnelConfig?.commandOptions?.ipc || void 0
};
concurrentlyCommands.push(tunnelCommand);
} else {
consola.info("Tunneling is disabled");
}
const execOptions = {
env: { ...process.env, PATH: process.env.PATH },
stdio: "inherit"
};
if (beforeCommands.length > 0) {
consola.info("Running beforeCommands");
for (const command of beforeCommands) {
consola.info(`Running beforeCommand: ${command}`);
execSync(command, execOptions);
}
} else {
consola.info("No beforeCommands to run");
}
const { result, commands: cmds } = concurrently(
concurrentlyCommands,
concurrentlyOptions
);
await result;
if (afterCommands.length > 0) {
consola.info("Running afterCommands");
for (const command of afterCommands) {
consola.info(`Running afterCommand: ${command}`);
execSync(command, execOptions);
}
} else {
consola.info("No afterCommands to run");
}
if (cmds && typeof cmds[Symbol.iterator] === "function") {
for (const cmd of cmds) {
consola.info(`Command ${cmd.name} ${cmd.state}`);
}
}
consola.info("Stackrun completed");
}
export { defineStackrunConfig, stackrun };