const fs = require("fs"); const path = require("path"); const { execSync } = require("child_process"); const statePath = path.join(process.cwd(), ".cursor", "pipeline-state.json"); const REPO_IDS = new Set([ "dnd_player", "project-converter", "DndGamePlayerLicenseServer", "none", ]); function readState() { const defaults = { frontend_development: "pending", ui_autotests: "pending", code_review: "pending", ui_browser_verification: "pending", verify_repo: "dnd_player", }; if (!fs.existsSync(statePath)) { return { ...defaults }; } try { const parsed = JSON.parse(fs.readFileSync(statePath, "utf8")); const merged = { ...defaults, ...parsed }; if (!REPO_IDS.has(merged.verify_repo)) merged.verify_repo = defaults.verify_repo; return merged; } catch { return { ...defaults }; } } function fail(msg) { process.stdout.write(JSON.stringify({ followup_message: msg })); process.exit(0); } /** Родитель каталога `cursorAi` (воркспейс с `.cursor/`) — обычно `dnd_project`. */ function getSiblingRoot(cwd) { const fromEnv = process.env.DND_PROJECT_ROOT; if (fromEnv) { const r = path.resolve(fromEnv); if (fs.existsSync(r)) return r; } return path.resolve(cwd, ".."); } function resolveRepoRoot(repoId, cwd) { const sibling = getSiblingRoot(cwd); const map = { dnd_player: path.join(sibling, "dnd_player"), "project-converter": path.join(sibling, "project-converter"), DndGamePlayerLicenseServer: path.join(sibling, "DndGamePlayerLicenseServer"), }; if (repoId === "dnd_player" && process.env.DND_PLAYER_ROOT) { const r = path.resolve(process.env.DND_PLAYER_ROOT); if (fs.existsSync(path.join(r, "package.json"))) return r; } const root = map[repoId]; if (root && fs.existsSync(path.join(root, "package.json"))) return root; if (root && fs.existsSync(root)) return root; return null; } function run(cmd, opts) { execSync(cmd, { stdio: "pipe", ...opts }); } function verifyDndPlayer(root) { const opts = { cwd: root }; run("npm run lint", opts); run("npm run typecheck", opts); run("npm run test", opts); } function verifyProjectConverter(root) { const opts = { cwd: root }; const pkgPath = path.join(root, "package.json"); let pkg; try { pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); } catch { fail("project-converter: could not read package.json"); } if (!pkg.scripts?.test) { fail( 'project-converter: add a "test" script and tests in the same PR as behavior changes (policy A — see project.mdc)', ); } try { run("npm run lint", opts); } catch { fail("project-converter: npm run lint failed"); } const files = ["src/main.js", "src/renderer.js", "src/run-electron.mjs"]; for (const f of files) { const p = path.join(root, f); if (fs.existsSync(p)) { try { run(`node --check "${p}"`, { cwd: root }); } catch { fail(`project-converter: node --check failed for ${f}`); } } } try { run("npm run test", opts); } catch { fail("project-converter: npm run test failed"); } } function verifyLicenseServer(root) { const opts = { cwd: root }; const checks = [ path.join(root, "src", "server.mjs"), path.join(root, "lib", "canonicalJson.mjs"), ]; for (const p of checks) { if (!fs.existsSync(p)) continue; try { run(`node --check "${p}"`, opts); } catch { fail(`DndGamePlayerLicenseServer: node --check failed for ${path.relative(root, p)}`); } } const pkgPath = path.join(root, "package.json"); let pkg; try { pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); } catch { fail("DndGamePlayerLicenseServer: could not read package.json"); } if (!pkg.scripts?.test) { fail( 'DndGamePlayerLicenseServer: add a "test" script and tests in the same PR as behavior changes (policy A — see project.mdc)', ); } try { run("npm run test", opts); } catch { fail("DndGamePlayerLicenseServer: npm run test failed"); } } const cwd = process.cwd(); const state = readState(); const envRepo = process.env.VERIFY_REPO; const verifyRepo = envRepo && REPO_IDS.has(envRepo) ? envRepo : state.verify_repo || "dnd_player"; if (!REPO_IDS.has(verifyRepo)) { fail( `Invalid verify_repo "${verifyRepo}". Use: dnd_player | project-converter | DndGamePlayerLicenseServer | none (override: env VERIFY_REPO)`, ); } if (verifyRepo === "none") { process.exit(0); } if (state.frontend_development !== "done") { fail("Run frontend-developer stage (see .cursor/agents/frontend-developer.md)"); } if (state.ui_autotests !== "done") { fail("Run ui-test-developer stage (see .cursor/agents/ui-test-developer.md)"); } if (state.code_review !== "done") { fail("Run code-reviewer stage (see .cursor/agents/code-reviewer.md)"); } if (state.ui_browser_verification !== "done") { fail( "Run ui-tester stage — UI or API verification (see .cursor/agents/ui-tester.md)", ); } const root = resolveRepoRoot(verifyRepo, cwd); if (!root) { fail( `Cannot resolve repo root for verify_repo="${verifyRepo}" (sibling of workspace parent, see WORKSPACE.md; override: DND_PROJECT_ROOT, DND_PLAYER_ROOT for dnd_player)`, ); } try { if (verifyRepo === "dnd_player") { verifyDndPlayer(root); } else if (verifyRepo === "project-converter") { verifyProjectConverter(root); } else if (verifyRepo === "DndGamePlayerLicenseServer") { verifyLicenseServer(root); } } catch (e) { const msg = e && typeof e === "object" && "message" in e ? e.message : String(e); fail(`Verify failed (${verifyRepo}): ${msg}`); } process.exit(0);