a58732f78a
Co-authored-by: Cursor <cursoragent@cursor.com>
204 lines
5.6 KiB
JavaScript
204 lines
5.6 KiB
JavaScript
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);
|