Files
CursorAi/.cursor/hooks/final-verify.cjs
T
2026-05-11 22:20:28 +08:00

204 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);