import express from "express"; import http from "node:http"; import path from "node:path"; import { WebSocketServer } from "ws"; import httpProxy from "http-proxy"; import { config } from "./config.js"; import { ProjectsStore } from "./lib/projects-store.js"; import { ProxyStore } from "./lib/proxy-store.js"; import { ProcessManager } from "./lib/process-manager.js"; import { createShellSession } from "./lib/shell.js"; import { createProjectsRouter } from "./routes/projects.js"; import { createRuntimeRouter } from "./routes/runtime.js"; import { createProxyRouter } from "./routes/proxy.js"; const projectsStore = new ProjectsStore(); const proxyStore = new ProxyStore(); const processManager = new ProcessManager(config.maxLogLines); await projectsStore.init(); await proxyStore.init(); const app = express(); const server = http.createServer(app); const wsShell = new WebSocketServer({ noServer: true }); const wsLogs = new WebSocketServer({ noServer: true }); const proxy = httpProxy.createProxyServer({ changeOrigin: true, ws: true, xfwd: true, secure: false }); app.use(express.json({ limit: "2mb" })); app.use("/api/projects", createProjectsRouter({ projectsStore })); app.use("/api/runtime", createRuntimeRouter({ projectsStore, processManager })); app.use("/api/proxy-routes", createProxyRouter({ proxyStore })); app.use("/vendor", express.static(path.join(config.rootDir, "public", "vendor"))); app.use(express.static(path.join(config.rootDir, "public"))); function rewriteProxyPath(requestPath, pathPrefix) { if (requestPath === pathPrefix) { return "/"; } if (requestPath.startsWith(pathPrefix + "/")) { return requestPath.slice(pathPrefix.length) || "/"; } return requestPath; } app.use((req, res, next) => { const route = proxyStore.match(req.path); if (!route) { next(); return; } const originalUrl = req.url; req.url = rewriteProxyPath(req.url, route.pathPrefix); proxy.web(req, res, { target: route.target, ignorePath: false }, () => { req.url = originalUrl; }); }); app.use((_req, res) => { res.status(404).json({ error: "Not found" }); }); proxy.on("error", (error, _req, res) => { if (!res.headersSent) { res.writeHead(502, { "Content-Type": "application/json" }); } res.end(JSON.stringify({ error: "Proxy error", details: error.message })); }); server.on("upgrade", (request, socket, head) => { const url = new URL(request.url, `http://${request.headers.host}`); if (url.pathname === "/ws/shell") { wsShell.handleUpgrade(request, socket, head, (ws) => { wsShell.emit("connection", ws, request, url); }); return; } if (url.pathname === "/ws/logs") { wsLogs.handleUpgrade(request, socket, head, (ws) => { wsLogs.emit("connection", ws, request, url); }); return; } const route = proxyStore.match(url.pathname); if (route) { request.url = rewriteProxyPath(url.pathname + url.search, route.pathPrefix); proxy.ws(request, socket, head, { target: route.target }); return; } socket.destroy(); }); wsShell.on("connection", (ws, _request, url) => { const projectId = url.searchParams.get("projectId"); const project = projectId ? projectsStore.getById(projectId) : null; if (!project) { ws.send(JSON.stringify({ type: "error", message: "Project not found" })); ws.close(); return; } const session = createShellSession(project.root); ws.send(JSON.stringify({ type: "ready", shell: session.shell, cwd: project.root, os: session.os })); session.process.stdout.on("data", (chunk) => { ws.send(JSON.stringify({ type: "stdout", data: String(chunk) })); }); session.process.stderr.on("data", (chunk) => { ws.send(JSON.stringify({ type: "stderr", data: String(chunk) })); }); session.process.on("close", (code) => { if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ type: "exit", code })); ws.close(); } }); ws.on("message", (payload) => { try { const message = JSON.parse(String(payload)); if (message.type === "input") { session.write(String(message.data || "")); } if (message.type === "resize") { // reserved for future node-pty upgrade } } catch { session.write(String(payload)); } }); ws.on("close", () => { session.close(); }); }); wsLogs.on("connection", (ws, _request, url) => { const projectId = url.searchParams.get("projectId"); const project = projectId ? projectsStore.getById(projectId) : null; if (!project) { ws.send(JSON.stringify({ type: "error", message: "Project not found" })); ws.close(); return; } const initialLogs = processManager.getLogs(project.id); ws.send(JSON.stringify({ type: "init", logs: initialLogs })); const onLog = (payload) => { if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify(payload)); } }; const onStatus = (payload) => { if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify(payload)); } }; processManager.events.on(`log:${project.id}`, onLog); processManager.events.on(`status:${project.id}`, onStatus); ws.on("close", () => { processManager.events.off(`log:${project.id}`, onLog); processManager.events.off(`status:${project.id}`, onStatus); }); }); server.listen(config.port, config.host, () => { console.log(`Mini Control Panel running on http://${config.host}:${config.port}`); });