From aaf6ce83613192fec022d0adab1a528279098d73 Mon Sep 17 00:00:00 2001
From: Chris Wanstrath <2+defunkt@users.noreply.github.com>
Date: Tue, 27 Jan 2026 16:05:58 -0800
Subject: [PATCH] apps.ts
---
src/pages/index.tsx | 8 +++
src/server/apps.ts | 150 +++++++++++++++++++++++++++++++++++++++
src/server/index.tsx | 164 +------------------------------------------
3 files changed, 161 insertions(+), 161 deletions(-)
create mode 100644 src/pages/index.tsx
create mode 100644 src/server/apps.ts
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
new file mode 100644
index 0000000..fcd0541
--- /dev/null
+++ b/src/pages/index.tsx
@@ -0,0 +1,8 @@
+import { runningApps } from '../server/apps'
+
+export default () => (
+ <>
+
🐾 Running Apps
+ {runningApps().map(app => )}
+ >
+)
\ No newline at end of file
diff --git a/src/server/apps.ts b/src/server/apps.ts
new file mode 100644
index 0000000..eb8afc7
--- /dev/null
+++ b/src/server/apps.ts
@@ -0,0 +1,150 @@
+import type { Subprocess } from 'bun'
+import { existsSync, readdirSync, readFileSync, watch } 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)
+
+export 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++
+
+export 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 = async (dir: string, port: number) => {
+ const pkg = loadApp(dir)
+ if (!pkg.scripts?.toes) return
+
+ const cwd = join(APPS_DIR, dir)
+
+ const needsInstall = !existsSync(join(cwd, 'node_modules'))
+ if (needsInstall) info(dir, 'Installing dependencies...')
+ const install = Bun.spawn(['bun', 'install'], { cwd, stdout: 'pipe', stderr: 'pipe' })
+ await install.exited
+
+ info(dir, `Starting on port ${port}...`)
+
+ const proc = Bun.spawn(['bun', 'run', 'toes'], {
+ 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)
+ })
+}
+
+export const runningApps = () =>
+ Array.from(_runningApps.values())
+ .map(({ name, port }) => ({ name, port }))
+ .sort((a, b) => a.port - b.port)
+
+const stopApp = (dir: string) => {
+ const app = _runningApps.get(dir)
+ if (app) {
+ info(dir, 'Stopping...')
+ app.proc.kill()
+ }
+}
+
+const watchAppsDir = () => {
+ watch(APPS_DIR, (event, filename) => {
+ console.log('apps dir')
+ if (!filename) return
+
+ if (isApp(filename) && !_runningApps.has(filename)) {
+ const port = getPort()
+ runApp(filename, port)
+ }
+
+ if (_runningApps.has(filename) && !isApp(filename))
+ stopApp(filename)
+ })
+}
+
+export const initApps = () => {
+ runApps()
+ watchAppsDir()
+}
\ No newline at end of file
diff --git a/src/server/index.tsx b/src/server/index.tsx
index 2dba9cf..ecabf38 100644
--- a/src/server/index.tsx
+++ b/src/server/index.tsx
@@ -1,167 +1,9 @@
-import type { Subprocess } from 'bun'
import { Hype } from 'hype'
-import { existsSync, readdirSync, readFileSync, watch } 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 = async (dir: string, port: number) => {
- const pkg = loadApp(dir)
- if (!pkg.scripts?.toes) return
-
- const cwd = join(APPS_DIR, dir)
-
- const needsInstall = !existsSync(join(cwd, 'node_modules'))
- if (needsInstall) info(dir, 'Installing dependencies...')
- const install = Bun.spawn(['bun', 'install'], { cwd, stdout: 'pipe', stderr: 'pipe' })
- await install.exited
-
- info(dir, `Starting on port ${port}...`)
-
- const proc = Bun.spawn(['bun', 'run', 'toes'], {
- 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()
- }
-}
-
-const watchAppsDir = () => {
- watch(APPS_DIR, (event, filename) => {
- console.log('apps dir')
- if (!filename) return
-
- if (isApp(filename) && !runningApps.has(filename)) {
- const port = getPort()
- runApp(filename, port)
- }
-
- if (runningApps.has(filename) && !isApp(filename))
- stopApp(filename)
- })
-}
-
-const startup = () => {
- console.log('🐾 Toes!')
-
- runApps()
- watchAppsDir()
-}
+import { initApps } from './apps'
const app = new Hype()
-app.get('/', c => {
- return c.html(
- <>
- 🐾 Running Apps
- {getRunningApps().map(app => )}
- >
- )
-})
+console.log('🐾 Toes!')
+initApps()
-startup()
-
-export { getRunningApps, stopApp }
export default app.defaults