//// // Web server that serves shell commands, websocket connections, etc import { Hono } from "hono" import { serveStatic, upgradeWebSocket, websocket } from "hono/bun" import { prettyJSON } from "hono/pretty-json" import color from "kleur" import type { Message } from "./shared/types" import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN, DEFAULT_PROJECT } from "./config" import { transpile, isFile, tilde, isDir } from "./utils" import { serveApp } from "./webapp" import { commands, commandPath, loadCommandModule } from "./commands" import { runCommandFn } from "./shell" import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket" import { initSneakers, disconnectSneakers } from "./sneaker" import { dispatchMessage } from "./dispatch" import { fatal } from "./fatal" import { getState } from "./state" import { sessionRun } from "./session" import { Layout } from "./html/layout" import { Terminal } from "./html/terminal" import { Error } from "./html/error" import { initDNS } from "./dns" import { initNoseDir } from "./nosedir" import { initCommands } from "./commands" // // Hono setup // const app = new Hono() app.use("*", prettyJSON()) app.use('/*', serveStatic({ root: './public' })) app.use('/img/*', serveStatic({ root: './public' })) app.use('/vendor/*', serveStatic({ root: './public' })) app.use('/css/*', serveStatic({ root: './src' })) app.use("*", async (c, next) => { const start = Date.now() await next() const end = Date.now() const fn = c.res.status < 300 ? color.green : c.res.status < 500 ? color.yellow : color.red console.log(fn(`${c.res.status} ${c.req.method} ${c.req.url} (${end - start}ms)`)) }) app.use("*", async (c, next) => { const error = fatal ? fatal : !isDir(NOSE_DIR) ? `NOSE_DIR doesn't exist: ${NOSE_DIR}` : undefined if (!error || (["/cmd/restart", "/cmd/reboot"].includes(c.req.path))) { await next() } else { return c.html(, 500) } }) app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => { let path = "./src/" + c.req.path.replace("..", ".") // path must end in .js or .ts if (!path.endsWith(".js") && !path.endsWith(".ts")) path += ".ts" const ts = path.replace(".js", ".ts") if (isFile(ts)) { return new Response(await transpile(ts), { headers: { "Content-Type": "text/javascript" } }) } else if (isFile(path)) { return new Response(Bun.file(path), { headers: { "Content-Type": "text/javascript" } }) } else { return c.text("File not found", 404) } }) // // app routes // // inject browser-nav.js into NOSE apps when loaded in a NOSE iframe app.use("*", async (c, next) => { await next() const url = new URL(c.req.url) const domains = url.hostname.split(".") const isSubdomain = domains.length > (url.hostname.endsWith("localhost") ? 1 : 2) if (!isSubdomain) return const contentType = c.res.headers.get('content-type') if (!contentType?.includes('text/html')) return const secFetchDest = c.req.header('Sec-Fetch-Dest') const isIframe = secFetchDest === 'iframe' if (!isIframe) return const originalBody = await c.res.text() const shimScript = `` let modifiedBody = originalBody if (originalBody.includes('')) { modifiedBody = originalBody.replace('', `${shimScript}\n`) } else if (originalBody.includes('')) { modifiedBody = originalBody.replace('', `\n${shimScript}`) } c.res = new Response(modifiedBody, { status: c.res.status, headers: c.res.headers }) }) app.use("*", async (c, next) => { const url = new URL(c.req.url) const localhost = url.hostname.endsWith("localhost") const domains = url.hostname.split(".") let subdomain = "" if (domains.length > (localhost ? 1 : 2)) subdomain = domains[0]! if (subdomain) { const app = serveApp(c, subdomain) return await app } return next() }) app.get("/source/:name", async c => { const name = c.req.param("name") const sessionId = c.req.query("session") || "0" const path = await sessionRun(sessionId, () => commandPath(name)) if (!path) return c.text("Command not found", 404) return new Response(await transpile(path), { headers: { "Content-Type": "text/javascript" } }) }) app.on(["GET", "POST"], ["/cmd/:name"], async c => { const sessionId = c.req.header("X-Session") || "0" const cmd = c.req.param("name") const method = c.req.method try { const mod = await sessionRun(sessionId, () => loadCommandModule(cmd)) if (!mod || !mod[method]) return c.json({ status: "error", output: `No ${method} export in ${cmd}` }, 500) return c.json(await runCommandFn({ sessionId }, async () => mod[method](c))) } catch (e: any) { return c.json({ status: "error", output: e.message || e.toString() }, 500) } }) app.get("/", c => c.html()) // // websocket // app.get("/ws", c => { const _sessionId = c.req.query("session") return upgradeWebSocket(c, { async onOpen(_e, ws) { addWebsocket(ws) send(ws, { type: "commands", data: await commands() }) send(ws, { type: "session:start", data: { NOSE_DIR: NOSE_DIR, project: DEFAULT_PROJECT, cwd: "/", mode: getState("ui:mode") || "tall" } }) }, async onMessage(event, ws) { let data: Message | undefined try { data = JSON.parse(event.data.toString()) } catch (e) { console.error("JSON parsing error", e) send(ws, { type: "error", data: "json parsing error" }) return } if (!data) return await dispatchMessage(ws, data) }, onClose: (event, ws) => removeWebsocket(ws) }) }) // // hot reload mode cleanup // if (process.env.BUN_HOT) { // @ts-ignore globalThis.__hot_reload_cleanup?.() // @ts-ignore globalThis.__hot_reload_cleanup = () => { closeWebsockets() disconnectSneakers() } for (const sig of ["SIGINT", "SIGTERM"] as const) { process.on(sig, () => { // @ts-ignore globalThis.__hot_reload_cleanup?.() process.exit(0) }) } } // // production mode // if (process.env.NODE_ENV === "production") { initDNS() } // // server start // console.log(color.cyan(NOSE_ICON)) console.log(color.blue(" NOSE_BIN:"), color.yellow(tilde(NOSE_BIN))) console.log(color.blue(" NOSE_DATA:"), color.yellow(tilde(NOSE_DATA))) console.log(color.blue(" NOSE_DIR:"), color.yellow(tilde(NOSE_DIR))) console.log(color.blue("NOSE_ROOT_BIN:"), color.yellow(tilde(NOSE_ROOT_BIN))) await initNoseDir() initCommands() initSneakers() export default { port: process.env.PORT || 3000, hostname: "0.0.0.0", fetch: app.fetch, idleTimeout: 0, websocket }