- e2e ports are now derived from a hash of the checkout root (42000-45999 blocks) so concurrent worktrees never collide; E2E_*_PORT/URL still override. - globalsetups fail fast with the squatting PID when a port is taken instead of silently attaching to a foreign checkout's dev server (the cause of 31-scenario auth-error cascades). - scripts/bootstrap.ts: one idempotent command for submodules + install + playwright; scripts/reap-dev-servers.ts kills orphaned dev stacks. - e2e: bun run ports prints this checkout's derived ports. - sources-api scenario: align with payload-first OpenAPI results (PR #956 changed the product, the cloud-only scenario was not updated). - AGENTS.md: fresh-worktree setup + environment gotchas (fish shell, /tmp scripts, bun.lock conflicts, per-checkout ports).
Showing the changes to scripts (2 of 15 files). Show all
Unified · Split
| @@ -0,0 +1,32 @@ | ||
| 1 | +// One-command setup for a fresh checkout or agent worktree: submodules, | |
| 2 | +// dependencies (whose prepare hook builds the internal packages dev servers | |
| 3 | +// need), and the Playwright browser the e2e suite drives. Idempotent and | |
| 4 | +// safe to re-run; each step prints what it is doing. | |
| 5 | +import { execFileSync } from "node:child_process"; | |
| 6 | +import { existsSync } from "node:fs"; | |
| 7 | +import { resolve } from "node:path"; | |
| 8 | +import { fileURLToPath } from "node:url"; | |
| 9 | + | |
| 10 | +const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); | |
| 11 | + | |
| 12 | +const run = (label: string, cmd: string, args: ReadonlyArray<string>) => { | |
| 13 | + console.log(`\n[bootstrap] ${label}: ${cmd} ${args.join(" ")}`); | |
| 14 | + execFileSync(cmd, [...args], { cwd: repoRoot, stdio: "inherit" }); | |
| 15 | +}; | |
| 16 | + | |
| 17 | +run("vendor submodules", "git", ["submodule", "update", "--init", "--recursive"]); | |
| 18 | + | |
| 19 | +// `bun install` runs the workspace prepare hook, which builds | |
| 20 | +// @executor-js/vite-plugin and @executor-js/react — the two artifacts the | |
| 21 | +// apps' vite dev servers fail without in a fresh worktree. | |
| 22 | +run("dependencies (+ prepare builds)", "bun", ["install"]); | |
| 23 | + | |
| 24 | +// e2e browser scenarios need Playwright's chromium; the cache is shared | |
| 25 | +// per-machine so this is a fast no-op when already present. | |
| 26 | +run("playwright chromium", "bunx", ["playwright", "install", "chromium"]); | |
| 27 | + | |
| 28 | +if (!existsSync(resolve(repoRoot, "node_modules/.bin/vitest"))) { | |
| 29 | + throw new Error("bootstrap: vitest missing after install — bun install likely failed"); | |
| 30 | +} | |
| 31 | + | |
| 32 | +console.log("\n[bootstrap] done — `cd e2e && bun run test` runs the full suite."); | |
| @@ -0,0 +1,65 @@ | ||
| 1 | +// Find (and optionally kill) leaked dev-stack processes — vite dev servers, | |
| 2 | +// PGlite dev-dbs, e2e emulators — left behind when a session dies without | |
| 3 | +// running its teardown. Default: kill only ORPHANS (processes whose checkout | |
| 4 | +// path no longer exists, e.g. a removed worktree) and list the rest. | |
| 5 | +// `--all` also kills live-checkout servers (do this only when you know no | |
| 6 | +// other agent session is using them). `--dry-run` lists without killing. | |
| 7 | +import { execFileSync } from "node:child_process"; | |
| 8 | +import { existsSync } from "node:fs"; | |
| 9 | + | |
| 10 | +const args = new Set(process.argv.slice(2)); | |
| 11 | +const dryRun = args.has("--dry-run"); | |
| 12 | +const killAll = args.has("--all"); | |
| 13 | + | |
| 14 | +const DEV_PATTERN = /vite dev|dev-db\.ts|scripts\/dev\.ts/; | |
| 15 | + | |
| 16 | +const ps = execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" }); | |
| 17 | + | |
| 18 | +interface Candidate { | |
| 19 | + readonly pid: number; | |
| 20 | + readonly command: string; | |
| 21 | + readonly checkout: string | undefined; | |
| 22 | + readonly orphan: boolean; | |
| 23 | +} | |
| 24 | + | |
| 25 | +const candidates: Candidate[] = []; | |
| 26 | +for (const line of ps.split("\n")) { | |
| 27 | + const match = line.match(/^\s*(\d+)\s+(.*)$/); | |
| 28 | + if (!match) continue; | |
| 29 | + const [, pidText, command] = match; | |
| 30 | + if (!DEV_PATTERN.test(command!)) continue; | |
| 31 | + if (Number(pidText) === process.pid) continue; | |
| 32 | + // The checkout root is whatever absolute path prefixes node_modules/ or a | |
| 33 | + // workspace dir in the command line. | |
| 34 | + const pathMatch = command!.match(/(\/[^ ]*?)\/(?:node_modules|apps|packages|e2e)\//); | |
| 35 | + const checkout = pathMatch?.[1]; | |
| 36 | + const orphan = checkout !== undefined && !existsSync(checkout); | |
| 37 | + candidates.push({ pid: Number(pidText), command: command!.slice(0, 160), checkout, orphan }); | |
| 38 | +} | |
| 39 | + | |
| 40 | +if (candidates.length === 0) { | |
| 41 | + console.log("reap: no dev-stack processes found."); | |
| 42 | + process.exit(0); | |
| 43 | +} | |
| 44 | + | |
| 45 | +for (const c of candidates) { | |
| 46 | + const shouldKill = !dryRun && (c.orphan || killAll); | |
| 47 | + const tag = c.orphan ? "ORPHAN" : "live "; | |
| 48 | + console.log(`${shouldKill ? "KILL " : "keep "} ${tag} pid=${c.pid} ${c.command}`); | |
| 49 | + if (shouldKill) { | |
| 50 | + try { | |
| 51 | + // Group kill (detached boots) with a direct-pid fallback. | |
| 52 | + try { | |
| 53 | + process.kill(-c.pid, "SIGTERM"); | |
| 54 | + } catch { | |
| 55 | + process.kill(c.pid, "SIGTERM"); | |
| 56 | + } | |
| 57 | + } catch (error) { | |
| 58 | + console.error(` failed to kill ${c.pid}: ${String(error)}`); | |
| 59 | + } | |
| 60 | + } | |
| 61 | +} | |
| 62 | + | |
| 63 | +if (!killAll && candidates.some((c) => !c.orphan)) { | |
| 64 | + console.log("\nLive-checkout servers were kept (another session may own them); --all kills those too."); | |
| 65 | +} | |