import "dotenv/config"; import express from "express"; import { chromium, Browser } from "playwright"; import { crawlPage } from "./lib/crawler"; import { chunkData } from "./lib/chunker"; import { formatAnalysisForPrompt, buildWebsitePrompt } from "./lib/ai-prompt"; import { generateWebsite } from "./lib/llm"; import { crawlQueue } from "./lib/queue"; import { parseGeneratedFiles } from "./lib/file-parser"; import { writeProjectFiles } from "./lib/project-writer"; import { validateAndSave } from "./lib/validator"; const API_KEY = process.env.CRAWL_API_KEY || "d3v-crawl-server-key-2026"; const PORT = Number(process.env.PORT) || 7860; const HOST = "0.0.0.0"; let browser: Browser | undefined; async function launchBrowser(): Promise { console.log("Launching Chromium in-process..."); return chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"], }); } async function ensureBrowser(): Promise { if (!browser || !browser.isConnected()) { browser = await launchBrowser(); } return browser; } function withTimeout(promise: Promise, ms: number, label: string): Promise { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms) ), ]); } const app = express(); app.use(express.json()); app.use((req, res, next) => { if (req.path === "/health") return next(); const auth = req.headers.authorization; if (auth !== `Bearer ${API_KEY}`) return res.status(401).json({ error: "unauthorized" }); next(); }); app.get("/crawl", async (req, res) => { const url = req.query.url as string; if (!url) return res.status(400).json({ error: "url query param required" }); try { const result = await crawlQueue.run(async () => { const b = await ensureBrowser(); return crawlPage(b, url); }); const out = req.query.chunked === "true" ? chunkData(result) : result; res.json(out); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.post("/build", async (req, res) => { const { prompt, reference, validate } = req.body || {}; if (!prompt) return res.status(400).json({ error: "prompt is required" }); try { let analysisText: string | undefined; if (reference) { console.log("[build] starting crawl of", reference); try { const crawled = await withTimeout( crawlQueue.run(async () => { const b = await ensureBrowser(); return crawlPage(b, reference); }), 70000, "crawl" ); console.log("[build] crawl finished"); analysisText = formatAnalysisForPrompt(crawled); } catch (err: any) { console.log("[build] crawl failed/timed out, continuing without reference:", err.message); } } const fullPrompt = buildWebsitePrompt(prompt, analysisText); console.log("[build] calling LLM..."); const rawResult = await generateWebsite(fullPrompt); console.log("[build] LLM call finished, length:", rawResult.length); const files = parseGeneratedFiles(rawResult); if (files.length === 0) { res.json({ prompt, reference: reference || null, result: rawResult, files: [] }); return; } const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-"); const { projectDir, written } = writeProjectFiles(files, slug); let validation = null; if (validate) { console.log("[build] validating (npm install + npm run build)..."); validation = await validateAndSave(projectDir); console.log("[build] validation finished, success:", validation.success); } res.json({ prompt, reference: reference || null, projectDir, files: written, validation }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.get("/health", (_, res) => { res.json({ status: "ok", browserConnected: browser ? browser.isConnected() : false }); }); async function start() { await ensureBrowser(); app.listen(PORT, HOST, () => console.log(`API server alive on http://${HOST}:${PORT}`)); } async function shutdown() { console.log("Shutting down API server and closing browser..."); if (browser) await browser.close(); process.exit(0); } process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); start();