meeseeks / src /server.ts
AryaYT's picture
init: meeseeks bun docker space (server, ui, samples, receipts)
71c7323
Raw
History Blame Contribute Delete
6.49 kB
// Meeseeks HF Space — Bun.serve HTTP server.
// Routes:
// GET / → static index.html
// GET /static/<file> → static assets (css/js/images)
// GET /samples → list sample skill names
// GET /samples/<name>.json → sample skill JSON
// POST /api/plan → {skill, inputs} → ExecutionPlan
// POST /api/export → {skill} → SkillBundle
// POST /api/validate → {skill} → {ok, issues}
// GET /receipts/<demo>/ → list receipt files
// GET /receipts/<demo>/<file> → receipt assets
//
// HF Spaces convention: bind to port 7860.
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}/`);