UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

126 lines (123 loc) • 3.77 kB
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url); import { collectRuleFiles } from "./chunk-YME53X5M.js"; import { loadRuleScriptMap } from "./chunk-SS625UO5.js"; import { embedRuleMarker, mintRuleId, parseRuleMarker, ruleTextHash, stripRuleMarker } from "./chunk-ACJQW7SB.js"; import { renameWithRetrySync } from "./chunk-Z3NGWHFX.js"; import { PATHS } from "./chunk-IHMBVS6H.js"; import { init_esm_shims } from "./chunk-IDFRWBMN.js"; // src/rule-scripts/editor.ts init_esm_shims(); import { mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname, join } from "path"; function writeRuleFile(abs, text) { mkdirSync(dirname(abs), { recursive: true }); const tmp = `${abs}.${process.pid}.tmp`; writeFileSync(tmp, text, "utf8"); renameWithRetrySync(tmp, abs); } function takenIds(projectRoot) { const taken = /* @__PURE__ */ new Set(); for (const r of loadRuleScriptMap(projectRoot)?.rules ?? []) { taken.add(r.id); } for (const r of loadRuleScriptMap(projectRoot)?.archived ?? []) { taken.add(r.id); } for (const rel of collectRuleFiles(projectRoot)) { for (const line of readFileSync(join(projectRoot, rel), "utf8").split("\n")) { const id = parseRuleMarker(line); if (id) { taken.add(id); } } } return taken; } function cleanRuleText(input) { return input.trim().replace(/^[-*]\s+/, ""); } function assertRuleSource(sourceRel) { const normalized = sourceRel.split("\\").join("/"); if (!normalized.startsWith(`${PATHS.RULES_DIR}/`) || !normalized.endsWith(".md")) { throw new Error( `addRule: source must be a .md file under ${PATHS.RULES_DIR}/ (got "${sourceRel}")` ); } } function addRule(projectRoot, sourceRel, text, options = {}) { assertRuleSource(sourceRel); const abs = join(projectRoot, sourceRel); const clean = cleanRuleText(text); const id = mintRuleId(sourceRel, clean, takenIds(projectRoot)); const bullet = embedRuleMarker(`- ${clean}`, id, options.discovered); let existing = ""; try { existing = readFileSync(abs, "utf8"); } catch (err) { if (err.code !== "ENOENT") { throw err; } } const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""; writeRuleFile(abs, `${existing}${sep}${bullet} `); return { id, source: sourceRel, text: clean, text_hash: ruleTextHash(clean) }; } function locateRule(projectRoot, ruleId) { for (const rel of collectRuleFiles(projectRoot)) { const lines = readFileSync(join(projectRoot, rel), "utf8").split("\n"); const line = lines.findIndex((l) => parseRuleMarker(l) === ruleId); if (line >= 0) { return { source: rel, line, lines }; } } return null; } function editRuleText(projectRoot, ruleId, newText, options = {}) { const located = locateRule(projectRoot, ruleId); if (!located) { return null; } const original = located.lines[located.line]; const indent = /^(\s*)/.exec(original)?.[1] ?? ""; const marker = /^[-*]/.exec(stripRuleMarker(original).text.trim())?.[0] ?? "-"; const clean = cleanRuleText(newText); located.lines[located.line] = embedRuleMarker( `${indent}${marker} ${clean}`, ruleId, options.discovered ); writeRuleFile(join(projectRoot, located.source), located.lines.join("\n")); return { source: located.source }; } function removeRuleBullet(projectRoot, ruleId) { const located = locateRule(projectRoot, ruleId); if (!located) { return null; } located.lines.splice(located.line, 1); writeRuleFile(join(projectRoot, located.source), located.lines.join("\n")); return { source: located.source }; } export { cleanRuleText, addRule, editRuleText, removeRuleBullet };