import { Subprocess } from 'bun' import { Hype } from 'hype' import { readdirSync, readFileSync } from 'fs' import { join } from 'path' const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps') type RunningApp = { name: string port: number proc: Subprocess } const runningApps = new Map() const err = (app: string, ...msg: string[]) => console.error('๐Ÿพ', `${app}:`, ...msg) const info = (app: string, ...msg: string[]) => console.log('๐Ÿพ', `${app}:`, ...msg) const log = (app: string, ...msg: string[]) => console.log('๐Ÿพ', `${app}ยป`, ...msg) const appNames = () => { return readdirSync(APPS_DIR, { withFileTypes: true }) .filter(e => e.isDirectory()) .map(e => e.name) .sort() } let NEXT_PORT = 3001 const getPort = () => NEXT_PORT++ const runApps = () => { for (const dir of appNames()) { if (!isApp(dir)) continue const port = getPort() runApp(dir, port) } } const isApp = (dir: string): boolean => { try { const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8') const json = JSON.parse(file) return !!json.scripts?.toes } catch (e) { return false } } const loadApp = (dir: string) => { try { const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8') const json = JSON.parse(file) if (json.scripts?.toes) { return json } else { err(dir, 'No `bun toes` script in package.json') return {} } } catch (e) { err(dir, 'No package.json') return {} } } const runApp = (dir: string, port: number) => { const pkg = loadApp(dir) if (!pkg.scripts?.toes) return const cwd = join(APPS_DIR, dir) const cmd = ['bun', 'run', 'toes'] info(dir, `Starting on port ${port}...`) const proc = Bun.spawn(cmd, { cwd, env: { ...process.env, PORT: String(port) }, stdout: 'pipe', stderr: 'pipe', }) runningApps.set(dir, { name: dir, port, proc }) const streamOutput = async (stream: ReadableStream | null, isErr: boolean) => { if (!stream) return const reader = stream.getReader() const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break const text = decoder.decode(value).trimEnd() if (text) { //isErr ? err(dir, text) : info(dir, text) log(dir, text) } } } streamOutput(proc.stdout, false) streamOutput(proc.stderr, true) // Handle process exit proc.exited.then(code => { if (code !== 0) err(dir, `Exited with code ${code}`) else info(dir, 'Stopped') runningApps.delete(dir) }) } const getRunningApps = () => Array.from(runningApps.values()).map(({ name, port }) => ({ name, port })) const stopApp = (dir: string) => { const app = runningApps.get(dir) if (app) { info(dir, 'Stopping...') app.proc.kill() } } console.log('๐Ÿพ Toes!') runApps() const app = new Hype() app.get('/', c => { return c.html( <>

๐Ÿพ Running Apps

{getRunningApps().map(app =>

{app.port}: {app.name}

)} ) }) export { getRunningApps, stopApp } export default app.defaults