File size: 4,400 Bytes
ec675f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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<Browser> {
  console.log("Launching Chromium in-process...");
  return chromium.launch({
    headless: true,
    args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
  });
}

async function ensureBrowser(): Promise<Browser> {
  if (!browser || !browser.isConnected()) {
    browser = await launchBrowser();
  }
  return browser;
}

function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
  return Promise.race([
    promise,
    new Promise<never>((_, 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();