Unified · Split
| @@ -75,7 +75,7 @@ const renderSessionSummary = (kind: "web" | "mcp", baseUrl: string): string => { · 74 unmodified lines | |||
| 75 | `Executor ${displayKind} session is ready.`, | 75 | `Executor ${displayKind} session is ready.`, |
| 76 | `${primaryLabel}: ${primaryUrl}`, | 76 | `${primaryLabel}: ${primaryUrl}`, |
| 77 | `${secondaryLabel}: ${secondaryUrl}`, | 77 | `${secondaryLabel}: ${secondaryUrl}`, |
| 78 | `OpenAPI: ${appendUrlPath(baseUrl, "docs")}`, | 78 | `OpenAPI: ${appendUrlPath(baseUrl, "api/docs")}`, |
| 79 | "", | 79 | "", |
| 80 | guidance, | 80 | guidance, |
| 81 | "Press Ctrl+C to stop.", | 81 | "Press Ctrl+C to stop.", |
| @@ -88,7 +88,7 @@ const renderSessionSummary = (kind: "web" | "mcp", baseUrl: string): string => { · 6 unmodified lines | |||
| 88 | 88 | ||
| 89 | const isServerReachable = async (baseUrl: string): Promise<boolean> => { | 89 | const isServerReachable = async (baseUrl: string): Promise<boolean> => { |
| 90 | try { | 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 | return res.ok; | 92 | return res.ok; |
| 93 | } catch { | 93 | } catch { |
| 94 | return false; | 94 | return false; |
| @@ -217,11 +217,7 @@ const runForegroundSession = (input: { kind: "web" | "mcp"; port: number }) => · 122 unmodified lines | |||
| 217 | return handlers.mcp.handleRequest(request); | 217 | return handlers.mcp.handleRequest(request); |
| 218 | } | 218 | } |
| 219 | 219 | ||
| 220 | if ( | 220 | if (url.pathname.startsWith("/api/") || url.pathname === "/api") { |
| 221 | url.pathname.startsWith("/v1/") || | ||
| 222 | url.pathname.startsWith("/docs") || | ||
| 223 | url.pathname === "/openapi.json" | ||
| 224 | ) { | ||
| 225 | return handlers.api.handler(request); | 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( | 3 | export const startInstance = createStart(() => ({})); |
| 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 | })); | ||
| @@ -1,5 +1,5 @@ | |||
| 1 | import { readFileSync } from "node:fs"; | 1 | import { readFileSync } from "node:fs"; |
| 2 | import { defineConfig } from "vite"; | 2 | import { defineConfig, type Plugin } from "vite"; |
| 3 | import { tanstackStart } from "@tanstack/react-start/plugin/vite"; | 3 | import { tanstackStart } from "@tanstack/react-start/plugin/vite"; |
| 4 | import react from "@vitejs/plugin-react"; | 4 | import react from "@vitejs/plugin-react"; |
| 5 | import tailwindcss from "@tailwindcss/vite"; | 5 | import tailwindcss from "@tailwindcss/vite"; |
| @@ -22,6 +22,80 @@ const EXECUTOR_GITHUB_URL = (rootPackage.homepage ?? repositoryUrl ?? "https://g · 16 unmodified lines | |||
| 22 | .replace(/^git\+/, "") | 22 | .replace(/^git\+/, "") |
| 23 | .replace(/\.git$/, ""); | 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 | export default defineConfig({ | 99 | export default defineConfig({ |
| 26 | define: { | 100 | define: { |
| 27 | "import.meta.env.VITE_APP_VERSION": JSON.stringify(EXECUTOR_VERSION), | 101 | "import.meta.env.VITE_APP_VERSION": JSON.stringify(EXECUTOR_VERSION), |
| @@ -49,5 +123,6 @@ export default defineConfig({ · 21 unmodified lines | |||
| 49 | spa: { enabled: true }, | 123 | spa: { enabled: true }, |
| 50 | }), | 124 | }), |
| 51 | ...react(), | 125 | ...react(), |
| 126 | executorApiPlugin(), | ||
| 52 | ], | 127 | ], |
| 53 | }); | 128 | }); |
| @@ -23,9 +23,8 @@ export const CoreExecutorApi = HttpApi.make("executor") · 22 unmodified lines | |||
| 23 | /** | 23 | /** |
| 24 | * Compose the core API with a plugin group. | 24 | * Compose the core API with a plugin group. |
| 25 | */ | 25 | */ |
| 26 | export const addGroup = <G extends HttpApiGroup.HttpApiGroup.Any>( | 26 | export const addGroup = <G extends HttpApiGroup.HttpApiGroup.Any>(group: G) => |
| 27 | group: G, | 27 | CoreExecutorApi.add(group); |
| 28 | ) => CoreExecutorApi.add(group); | ||
| 29 | 28 | ||
| 30 | /** Default API with no plugin groups */ | 29 | /** Default API with no plugin groups */ |
| 31 | export const ExecutorApi = CoreExecutorApi; | 30 | export const ExecutorApi = CoreExecutorApi; |