| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { readdir } from "node:fs/promises"; |
| import { extname, join, normalize, resolve } from "node:path"; |
| import { SkillCore } from "./lib/skill.ts"; |
| import { planSkill } from "./lib/plan.ts"; |
| import { exportSkill } from "./lib/exporter.ts"; |
|
|
| const PORT = Number(process.env.PORT ?? 7860); |
| const ROOT = resolve(import.meta.dir, ".."); |
| const STATIC_DIR = join(ROOT, "src/static"); |
| const SAMPLES_DIR = join(ROOT, "samples"); |
| const RECEIPTS_DIR = join(ROOT, "receipts"); |
|
|
| const MIME: Record<string, string> = { |
| ".html": "text/html; charset=utf-8", |
| ".css": "text/css; charset=utf-8", |
| ".js": "application/javascript; charset=utf-8", |
| ".json": "application/json; charset=utf-8", |
| ".png": "image/png", |
| ".svg": "image/svg+xml", |
| ".webp": "image/webp", |
| ".ico": "image/x-icon", |
| }; |
|
|
| function mimeFor(path: string): string { |
| return MIME[extname(path).toLowerCase()] ?? "application/octet-stream"; |
| } |
|
|
| function safeJoin(base: string, rel: string): string | null { |
| const target = normalize(join(base, rel)); |
| if (!target.startsWith(base)) return null; |
| return target; |
| } |
|
|
| async function serveFile(absPath: string): Promise<Response> { |
| const file = Bun.file(absPath); |
| if (!(await file.exists())) { |
| return new Response("not found", { status: 404 }); |
| } |
| return new Response(file, { headers: { "Content-Type": mimeFor(absPath) } }); |
| } |
|
|
| async function readJsonBody(req: Request): Promise<any> { |
| const text = await req.text(); |
| if (!text) return {}; |
| try { |
| return JSON.parse(text); |
| } catch (e) { |
| throw new Response(JSON.stringify({ error: "invalid JSON body", detail: (e as Error).message }), { |
| status: 400, |
| headers: { "Content-Type": "application/json" }, |
| }); |
| } |
| } |
|
|
| function jsonResponse(data: unknown, init: ResponseInit = {}): Response { |
| return new Response(JSON.stringify(data, null, 2), { |
| ...init, |
| headers: { "Content-Type": "application/json", ...(init.headers ?? {}) }, |
| }); |
| } |
|
|
| async function listSamples(): Promise<{ name: string; bytes: number }[]> { |
| try { |
| const entries = await readdir(SAMPLES_DIR); |
| const out: { name: string; bytes: number }[] = []; |
| for (const e of entries.filter((x) => x.endsWith(".json")).sort()) { |
| const file = Bun.file(join(SAMPLES_DIR, e)); |
| out.push({ name: e.replace(/\.json$/, ""), bytes: file.size }); |
| } |
| return out; |
| } catch { |
| return []; |
| } |
| } |
|
|
| async function listReceiptFiles(demo: string): Promise<string[]> { |
| const dir = safeJoin(RECEIPTS_DIR, demo); |
| if (!dir) return []; |
| try { |
| const entries = await readdir(dir); |
| return entries.sort(); |
| } catch { |
| return []; |
| } |
| } |
|
|
| const server = Bun.serve({ |
| port: PORT, |
| hostname: "0.0.0.0", |
| async fetch(req) { |
| const url = new URL(req.url); |
| const path = url.pathname; |
| const method = req.method; |
|
|
| try { |
| if (method === "GET" && (path === "/" || path === "/index.html")) { |
| return serveFile(join(STATIC_DIR, "index.html")); |
| } |
|
|
| if (method === "GET" && path.startsWith("/static/")) { |
| const rel = path.slice("/static/".length); |
| const abs = safeJoin(STATIC_DIR, rel); |
| if (!abs) return new Response("forbidden", { status: 403 }); |
| return serveFile(abs); |
| } |
|
|
| if (method === "GET" && path === "/samples") { |
| return jsonResponse(await listSamples()); |
| } |
| if (method === "GET" && path.startsWith("/samples/")) { |
| const rel = path.slice("/samples/".length); |
| const abs = safeJoin(SAMPLES_DIR, rel); |
| if (!abs) return new Response("forbidden", { status: 403 }); |
| return serveFile(abs); |
| } |
|
|
| if (method === "GET" && path === "/receipts") { |
| try { |
| const dirs = (await readdir(RECEIPTS_DIR)).sort(); |
| const out: { name: string; files: string[] }[] = []; |
| for (const d of dirs) out.push({ name: d, files: await listReceiptFiles(d) }); |
| return jsonResponse(out); |
| } catch { |
| return jsonResponse([]); |
| } |
| } |
| if (method === "GET" && path.startsWith("/receipts/")) { |
| const rel = path.slice("/receipts/".length); |
| const abs = safeJoin(RECEIPTS_DIR, rel); |
| if (!abs) return new Response("forbidden", { status: 403 }); |
| return serveFile(abs); |
| } |
|
|
| if (method === "POST" && path === "/api/validate") { |
| const body = await readJsonBody(req); |
| const result = SkillCore.safeParse(body.skill ?? body); |
| if (result.success) { |
| return jsonResponse({ |
| ok: true, |
| name: result.data.name, |
| steps: result.data.steps.length, |
| placeholders: result.data.meta.placeholders, |
| }); |
| } |
| return jsonResponse( |
| { ok: false, issues: result.error.issues.slice(0, 20) }, |
| { status: 400 }, |
| ); |
| } |
|
|
| if (method === "POST" && path === "/api/plan") { |
| const body = await readJsonBody(req); |
| const skill = SkillCore.parse(body.skill ?? body); |
| const inputs = (body.inputs ?? {}) as Record<string, string>; |
| return jsonResponse(planSkill(skill, inputs)); |
| } |
|
|
| if (method === "POST" && path === "/api/export") { |
| const body = await readJsonBody(req); |
| const skill = SkillCore.parse(body.skill ?? body); |
| return jsonResponse(exportSkill(skill)); |
| } |
|
|
| if (method === "GET" && path === "/api/health") { |
| return jsonResponse({ ok: true, port: PORT }); |
| } |
|
|
| return new Response("not found", { status: 404 }); |
| } catch (e) { |
| if (e instanceof Response) return e; |
| const msg = (e as Error).message ?? "unknown"; |
| return jsonResponse({ error: msg.slice(0, 600) }, { status: 500 }); |
| } |
| }, |
| }); |
|
|
| console.log(`meeseeks hf-space listening at http://${server.hostname}:${server.port}/`); |
|
|