// Shared helper for the `local` e2e project: boot a real temporary server with // `executor web --foreground` in a recorded terminal, parse its printed one-time // `?_token=` URL, and run a body against it. Each scenario boots its OWN server // (own throwaway data dir, `--port 0`) so files can run in parallel without // colliding. The terminal stays up until the body settles, then Ctrl-C gives a // graceful shutdown (so the vite child dies and the PTY closes). import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { Effect } from "effect"; import type { CliSurface } from "../src/surfaces/cli"; import { markFocus, markRecordingStart } from "../src/timeline"; const repoRoot = fileURLToPath(new URL("../../", import.meta.url)); /** The `Open: …/?_token=` URL the CLI prints once the server is up. */ export const TOKEN_URL = /http:\/\/127\.0\.0\.1:\d+\/\?_token=[A-Za-z0-9_-]+/; export interface ServerHandle { /** The full `?_token=` bootstrap URL (origin + token). */ readonly url: string; /** The server origin, e.g. `http://127.0.0.1:54321`. */ readonly origin: string; /** The bearer token (the `_token` query param), in plaintext. */ readonly token: string; } /** * Boot `executor web --foreground` and run `body` against the resulting * {@link ServerHandle}. Keeps the server up until the body settles, then Ctrl-C * for a graceful shutdown. Cleans up the throwaway data dir. The body may drive * the browser, a typed API client, an MCP client — anything that needs the live * server. */ export const withLocalServer = ( cli: CliSurface, runDir: string, body: (server: ServerHandle) => Effect.Effect, ): Effect.Effect => Effect.gen(function* () { const dataDir = mkdtempSync(join(tmpdir(), "executor-local-e2e-")); let publishUrl!: (url: string) => void; const urlReady = new Promise((res) => { publishUrl = res; }); let signalBodyDone!: () => void; const bodyDone = new Promise((res) => { signalBodyDone = res; }); yield* Effect.all( [ cli.session( ["bun", "run", "dev:cli", "web", "--foreground", "--port", "0"], async (term) => { markRecordingStart(runDir, "terminal"); markFocus(runDir, "terminal"); const snapshot = await term.screen.waitUntil( (current) => TOKEN_URL.test(current.text), { timeoutMs: 120_000 }, ); const url = TOKEN_URL.exec(snapshot.text)?.[0]; if (!url) { throw new Error( `executor web --foreground printed no ?_token URL:\n${snapshot.text.slice(-600)}`, ); } publishUrl(url); await bodyDone; // Graceful shutdown so the vite child is killed and the PTY closes; // otherwise the orphaned child wedges the terminal teardown. markFocus(runDir, "terminal"); await term.keyboard.press("Control+C"); }, { cwd: repoRoot, env: { EXECUTOR_DATA_DIR: dataDir, EXECUTOR_SCOPE_DIR: dataDir }, record: join(runDir, "terminal.cast"), viewport: { cols: 120, rows: 40 }, }, ), Effect.gen(function* () { const url = yield* Effect.promise(() => urlReady); const parsed = new URL(url); yield* body({ url, origin: parsed.origin, token: parsed.searchParams.get("_token")!, }).pipe(Effect.ensuring(Effect.sync(() => signalBodyDone()))); }), ], { concurrency: "unbounded" }, ).pipe(Effect.ensuring(Effect.sync(() => rmSync(dataDir, { recursive: true, force: true })))); });