Unified · Split
| @@ -75,7 +75,7 @@ const renderSessionSummary = (kind: "web" | "mcp", baseUrl: string): string => { · 74 unmodified lines | ||
| 75 | 75 | `Executor ${displayKind} session is ready.`, |
| 76 | 76 | `${primaryLabel}: ${primaryUrl}`, |
| 77 | 77 | `${secondaryLabel}: ${secondaryUrl}`, |
| 78 | − `OpenAPI: ${appendUrlPath(baseUrl, "docs")}`, | |
| 78 | + `OpenAPI: ${appendUrlPath(baseUrl, "api/docs")}`, | |
| 79 | 79 | "", |
| 80 | 80 | guidance, |
| 81 | 81 | "Press Ctrl+C to stop.", |
| @@ -88,7 +88,7 @@ const renderSessionSummary = (kind: "web" | "mcp", baseUrl: string): string => { · 6 unmodified lines | ||
| 88 | 88 | |
| 89 | 89 | const isServerReachable = async (baseUrl: string): Promise<boolean> => { |
| 90 | 90 | try { |
| 91 | − const res = await fetch(`${baseUrl}/docs`, { signal: AbortSignal.timeout(2000) }); | |
| 91 | + const res = await fetch(`${baseUrl}/api/docs`, { signal: AbortSignal.timeout(2000) }); | |
| 92 | 92 | return res.ok; |
| 93 | 93 | } catch { |
| 94 | 94 | return false; |
| @@ -217,11 +217,7 @@ const runForegroundSession = (input: { kind: "web" | "mcp"; port: number }) => · 122 unmodified lines | ||
| 217 | 217 | return handlers.mcp.handleRequest(request); |
| 218 | 218 | } |
| 219 | 219 | |
| 220 | − if ( | |
| 221 | − url.pathname.startsWith("/v1/") || | |
| 222 | − url.pathname.startsWith("/docs") || | |
| 223 | − url.pathname === "/openapi.json" | |
| 224 | − ) { | |
| 220 | + if (url.pathname.startsWith("/api/") || url.pathname === "/api") { | |
| 225 | 221 | return handlers.api.handler(request); |
| 226 | 222 | } |
| 227 | 223 | |
| @@ -1,15 +0,0 @@ | ||
| 1 | −import { getServerHandlers } from "./main"; | |
| 2 | − | |
| 3 | −export const handleApiRequest = async (request: Request): Promise<Response> => { | |
| 4 | − const handlers = await getServerHandlers(); | |
| 5 | − // Strip /api prefix — Start request middleware forwards /api/* here, | |
| 6 | − // but the Effect handler endpoints are defined without the prefix. | |
| 7 | − const url = new URL(request.url); | |
| 8 | − url.pathname = url.pathname.replace(/^\/api/, ""); | |
| 9 | − return handlers.api.handler(new Request(url, request)); | |
| 10 | −}; | |
| 11 | − | |
| 12 | −export const handleMcpRequest = async (request: Request): Promise<Response> => { | |
| 13 | − const handlers = await getServerHandlers(); | |
| 14 | − return handlers.mcp.handleRequest(request); | |
| 15 | −}; | |
| @@ -1,19 +1,3 @@ | ||
| 1 | −import { createMiddleware, createStart } from "@tanstack/react-start"; | |
| 1 | +import { createStart } from "@tanstack/react-start"; | |
| 2 | 2 | |
| 3 | −const serverRequestMiddleware = createMiddleware({ type: "request" }).server( | |
| 4 | − async ({ pathname, request, next }) => { | |
| 5 | − if (pathname === "/api" || pathname.startsWith("/api/")) { | |
| 6 | − const { handleApiRequest } = await import("./server/api-handler"); | |
| 7 | − return handleApiRequest(request); | |
| 8 | − } | |
| 9 | − if (pathname === "/mcp" || pathname.startsWith("/mcp/")) { | |
| 10 | − const { handleMcpRequest } = await import("./server/api-handler"); | |
| 11 | − return handleMcpRequest(request); | |
| 12 | − } | |
| 13 | − return next(); | |
| 14 | − }, | |
| 15 | −); | |
| 16 | − | |
| 17 | −export const startInstance = createStart(() => ({ | |
| 18 | − requestMiddleware: [serverRequestMiddleware], | |
| 19 | −})); | |
| 3 | +export const startInstance = createStart(() => ({})); | |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { readFileSync } from "node:fs"; |
| 2 | −import { defineConfig } from "vite"; | |
| 2 | +import { defineConfig, type Plugin } from "vite"; | |
| 3 | 3 | import { tanstackStart } from "@tanstack/react-start/plugin/vite"; |
| 4 | 4 | import react from "@vitejs/plugin-react"; |
| 5 | 5 | import tailwindcss from "@tailwindcss/vite"; |
| @@ -22,6 +22,80 @@ const EXECUTOR_GITHUB_URL = (rootPackage.homepage ?? repositoryUrl ?? "https://g · 16 unmodified lines | ||
| 22 | 22 | .replace(/^git\+/, "") |
| 23 | 23 | .replace(/\.git$/, ""); |
| 24 | 24 | |
| 25 | +/** | |
| 26 | + * Vite plugin that forwards API and MCP requests to the Effect handlers | |
| 27 | + * during development — replaces the TanStack Start request middleware. | |
| 28 | + */ | |
| 29 | +function executorApiPlugin(): Plugin { | |
| 30 | + let handlers: import("./src/server/main").ServerHandlers | null = null; | |
| 31 | + | |
| 32 | + return { | |
| 33 | + name: "executor-api", | |
| 34 | + configureServer(server) { | |
| 35 | + server.middlewares.use(async (req, res, next) => { | |
| 36 | + const rawUrl = req.url ?? "/"; | |
| 37 | + const isApi = rawUrl.startsWith("/api/") || rawUrl === "/api"; | |
| 38 | + const isMcp = rawUrl.startsWith("/mcp"); | |
| 39 | + | |
| 40 | + if (!isApi && !isMcp) return next(); | |
| 41 | + | |
| 42 | + // Strip /api prefix — Effect handlers define routes without it | |
| 43 | + const url = isApi ? (rawUrl.slice("/api".length) || "/") : rawUrl; | |
| 44 | + | |
| 45 | + try { | |
| 46 | + if (!handlers) { | |
| 47 | + const { getServerHandlers } = await import("./src/server/main"); | |
| 48 | + handlers = await getServerHandlers(); | |
| 49 | + } | |
| 50 | + | |
| 51 | + // Convert Node request → Web Request | |
| 52 | + const origin = `http://${req.headers.host ?? "localhost"}`; | |
| 53 | + const headers = new Headers(); | |
| 54 | + for (const [key, value] of Object.entries(req.headers)) { | |
| 55 | + if (value) headers.set(key, Array.isArray(value) ? value.join(", ") : value); | |
| 56 | + } | |
| 57 | + | |
| 58 | + const hasBody = req.method !== "GET" && req.method !== "HEAD"; | |
| 59 | + const webRequest = new Request(new URL(url, origin), { | |
| 60 | + method: req.method, | |
| 61 | + headers, | |
| 62 | + body: hasBody ? (req as unknown as BodyInit) : undefined, | |
| 63 | + duplex: hasBody ? "half" : undefined, | |
| 64 | + } as RequestInit); | |
| 65 | + | |
| 66 | + const response = isMcp | |
| 67 | + ? await handlers.mcp.handleRequest(webRequest) | |
| 68 | + : await handlers.api.handler(webRequest); | |
| 69 | + | |
| 70 | + res.statusCode = response.status; | |
| 71 | + response.headers.forEach((v, k) => res.setHeader(k, v)); | |
| 72 | + | |
| 73 | + if (response.body) { | |
| 74 | + const reader = response.body.getReader(); | |
| 75 | + const pump = async () => { | |
| 76 | + while (true) { | |
| 77 | + const { done, value } = await reader.read(); | |
| 78 | + if (done) break; | |
| 79 | + res.write(value); | |
| 80 | + } | |
| 81 | + res.end(); | |
| 82 | + }; | |
| 83 | + await pump(); | |
| 84 | + } else { | |
| 85 | + res.end(); | |
| 86 | + } | |
| 87 | + } catch (err) { | |
| 88 | + console.error("[executor-api]", err); | |
| 89 | + if (!res.headersSent) { | |
| 90 | + res.statusCode = 500; | |
| 91 | + res.end("Internal Server Error"); | |
| 92 | + } | |
| 93 | + } | |
| 94 | + }); | |
| 95 | + }, | |
| 96 | + }; | |
| 97 | +} | |
| 98 | + | |
| 25 | 99 | export default defineConfig({ |
| 26 | 100 | define: { |
| 27 | 101 | "import.meta.env.VITE_APP_VERSION": JSON.stringify(EXECUTOR_VERSION), |
| @@ -49,5 +123,6 @@ export default defineConfig({ · 21 unmodified lines | ||
| 49 | 123 | spa: { enabled: true }, |
| 50 | 124 | }), |
| 51 | 125 | ...react(), |
| 126 | + executorApiPlugin(), | |
| 52 | 127 | ], |
| 53 | 128 | }); |
| @@ -23,9 +23,8 @@ export const CoreExecutorApi = HttpApi.make("executor") · 22 unmodified lines | ||
| 23 | 23 | /** |
| 24 | 24 | * Compose the core API with a plugin group. |
| 25 | 25 | */ |
| 26 | −export const addGroup = <G extends HttpApiGroup.HttpApiGroup.Any>( | |
| 27 | − group: G, | |
| 28 | −) => CoreExecutorApi.add(group); | |
| 26 | +export const addGroup = <G extends HttpApiGroup.HttpApiGroup.Any>(group: G) => | |
| 27 | + CoreExecutorApi.add(group); | |
| 29 | 28 | |
| 30 | 29 | /** Default API with no plugin groups */ |
| 31 | 30 | export const ExecutorApi = CoreExecutorApi; |