chore: cursor agents, rules, hooks and workspace docs
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+167
-58
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user