import { allApps, initApps, TOES_URL } from '$apps' import { buildAppUrl } from '@urls' import appsRouter from './api/apps' import eventsRouter from './api/events' import syncRouter from './api/sync' import systemRouter from './api/system' import { Hype } from '@because/hype' import { cleanupStalePublishers } from './mdns' import { extractSubdomain, proxySubdomain, proxyWebSocket, websocket } from './proxy' import type { Server } from 'bun' import type { WsData } from './proxy' const app = new Hype({ layout: false, logging: !!process.env.DEBUG }) app.route('/api/apps', appsRouter) app.route('/api/events', eventsRouter) app.route('/api/sync', syncRouter) app.route('/api/system', systemRouter) // Tool URLs: /tool/code?app=todo&file=README.md -> redirect to tool subdomain app.get('/tool/:tool', c => { const toolName = c.req.param('tool') const tool = allApps().find(a => a.tool && a.name === toolName) if (!tool || tool.state !== 'running' || !tool.port) { return c.text(`Tool "${toolName}" not found or not running`, 404) } const params = new URLSearchParams(c.req.query()).toString() const base = buildAppUrl(toolName, TOES_URL) const url = params ? `${base}?${params}` : base return c.redirect(url) }) // Tool API proxy: /api/tools/:tool/* -> proxy to tool port app.all('/api/tools/:tool/:path{.+}', async c => { const toolName = c.req.param('tool') const tool = allApps().find(a => a.tool && a.name === toolName) if (!tool || tool.state !== 'running' || !tool.port) { return c.json({ error: `Tool "${toolName}" not found or not running` }, 404) } const subPath = '/' + c.req.param('path') // Build target URL const params = new URLSearchParams(c.req.query()).toString() const targetUrl = params ? `http://localhost:${tool.port}${subPath}?${params}` : `http://localhost:${tool.port}${subPath}` // Proxy the request const response = await fetch(targetUrl, { method: c.req.method, headers: c.req.raw.headers, body: c.req.method !== 'GET' && c.req.method !== 'HEAD' ? c.req.raw.body : undefined, }) return new Response(response.body, { status: response.status, headers: response.headers, }) }) const CLI_ENTRY = import.meta.dir + '/../cli/index.ts' const DIST_DIR = import.meta.dir + '/../../dist' const INSTALL_SCRIPT = await Bun.file(import.meta.dir + '/install.sh').text() interface BuildTarget { arch: string name: string os: string } const BUILD_TARGETS: BuildTarget[] = [ { os: 'darwin', arch: 'arm64', name: 'toes-macos-arm64' }, { os: 'darwin', arch: 'x64', name: 'toes-macos-x64' }, { os: 'linux', arch: 'arm64', name: 'toes-linux-arm64' }, { os: 'linux', arch: 'x64', name: 'toes-linux-x64' }, ] const buildInFlight = new Map>() async function buildBinary(target: BuildTarget): Promise { const existing = buildInFlight.get(target.name) if (existing) return existing const promise = (async () => { const { existsSync, mkdirSync } = await import('fs') if (!existsSync(DIST_DIR)) mkdirSync(DIST_DIR, { recursive: true }) const proc = Bun.spawn([ 'bun', 'build', CLI_ENTRY, '--compile', '--target', `bun-${target.os}-${target.arch}`, '--minify', '--sourcemap=external', '--outfile', `${DIST_DIR}/${target.name}`, ], { stdout: 'inherit', stderr: 'inherit' }) const exitCode = await proc.exited return exitCode === 0 })() buildInFlight.set(target.name, promise) promise.finally(() => buildInFlight.delete(target.name)) return promise } // Install script: curl -fsSL http://toes.local/install | bash app.get('/install', c => { if (!TOES_URL) return c.text('TOES_URL is not configured', 500) const script = INSTALL_SCRIPT.replace('__TOES_URL__', TOES_URL) return c.text(script, 200, { 'content-type': 'text/plain' }) }) // Serve built CLI binaries from dist/, building on-demand if needed app.get('/dist/:file', async c => { const file = c.req.param('file') if (!file || file.includes('/') || file.includes('..')) { return c.text('Not found', 404) } const bunFile = Bun.file(`${DIST_DIR}/${file}`) if (!(await bunFile.exists())) { const target = BUILD_TARGETS.find(t => t.name === file) if (!target) return c.text('Not found', 404) const ok = await buildBinary(target) if (!ok) return c.text(`Failed to build "${file}"`, 500) } return new Response(Bun.file(`${DIST_DIR}/${file}`), { headers: { 'content-type': 'application/octet-stream' }, }) }) cleanupStalePublishers() await initApps() const defaults = app.defaults export default { ...defaults, maxRequestBodySize: 1024 * 1024 * 50, // 50MB fetch(req: Request, server: Server) { const subdomain = extractSubdomain(req.headers.get('host') ?? '') if (subdomain) { if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') { return proxyWebSocket(subdomain, req, server) } return proxySubdomain(subdomain, req) } return defaults.fetch.call(app, req, server) }, websocket, }