chore: cursor agents, rules, hooks and workspace docs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-05-11 22:20:28 +08:00
parent 01cde7476c
commit a58732f78a
25 changed files with 1016 additions and 261 deletions
+167 -58
View File
@@ -4,15 +4,32 @@ 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 {
implementation: "pending",
review: "pending",
tests: "pending",
};
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 };
}
return JSON.parse(fs.readFileSync(statePath, "utf8"));
}
function fail(msg) {
@@ -20,75 +37,167 @@ function fail(msg) {
process.exit(0);
}
function getDndPlayerRoot() {
if (process.env.DND_PLAYER_ROOT) {
/** Родитель каталога `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 cwd = process.cwd();
const candidates = [
path.join(cwd, "..", "dnd_player"),
path.join(cwd, "dnd_player"),
cwd,
];
for (const root of candidates) {
const pkgPath = path.join(root, "package.json");
if (!fs.existsSync(pkgPath)) continue;
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
if (
pkg.scripts?.lint &&
pkg.scripts?.typecheck &&
pkg.scripts?.test
) {
return root;
}
} catch {
continue;
}
}
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();
if (state.implementation !== "done") {
fail("Run frontend-senior stage");
}
const envRepo = process.env.VERIFY_REPO;
const verifyRepo =
envRepo && REPO_IDS.has(envRepo) ? envRepo : state.verify_repo || "dnd_player";
if (state.review !== "done") {
fail("Run reviewer stage");
}
if (state.tests !== "done") {
fail("Run unit-tests stage");
}
const dndPlayerRoot = getDndPlayerRoot();
if (!dndPlayerRoot) {
if (!REPO_IDS.has(verifyRepo)) {
fail(
"Cannot find dnd_player (expected sibling ../dnd_player or env DND_PLAYER_ROOT)",
`Invalid verify_repo "${verifyRepo}". Use: dnd_player | project-converter | DndGamePlayerLicenseServer | none (override: env VERIFY_REPO)`,
);
}
const opts = { stdio: "pipe", cwd: dndPlayerRoot };
if (verifyRepo === "none") {
process.exit(0);
}
try {
execSync("npm run lint", opts);
} catch {
fail("Lint failed (run from dnd_player root)");
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 {
execSync("npm run typecheck", opts);
} catch {
fail("Typecheck failed (run from dnd_player root)");
}
try {
execSync("npm run test", opts);
} catch {
fail("Tests failed (run from dnd_player root)");
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);