UNPKG

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
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 };