UNPKG

aicm

Version:

A TypeScript CLI tool for managing AI IDE rules across different projects and teams

183 lines (180 loc) 6.71 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseRuleFrontmatter = parseRuleFrontmatter; exports.writeRulesFile = writeRulesFile; exports.generateRulesFileContent = generateRulesFileContent; const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); /** * Parse YAML frontmatter blocks from a rule file and return a flat metadata object */ function parseRuleFrontmatter(content) { const metadata = {}; // Support both LF and CRLF line endings const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/gm; let match; while ((match = frontmatterRegex.exec(content)) !== null) { const lines = match[1].split("\n"); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; const [key, ...rest] = trimmed.split(":"); if (!key) continue; const raw = rest.join(":").trim(); if (raw === "") { metadata[key] = ""; } else if (raw === "true" || raw === "false") { metadata[key] = raw === "true"; } else if (raw.startsWith("[") && raw.endsWith("]")) { try { const parsed = JSON.parse(raw.replace(/'/g, '"')); metadata[key] = parsed; } catch (_a) { metadata[key] = raw; } } else if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) { metadata[key] = raw.slice(1, -1); } else { metadata[key] = raw; } } } return metadata; } const RULES_BEGIN = "<!-- AICM:BEGIN -->"; const RULES_END = "<!-- AICM:END -->"; const WARNING = "<!-- WARNING: Everything between these markers will be overwritten during installation -->"; /** * Create a formatted block of content with rules markers */ function createRulesBlock(rulesContent) { return `${RULES_BEGIN} ${WARNING} ${rulesContent} ${RULES_END}`; } /** * Write rules to the .windsurfrules file * This will update the content between the RULES_BEGIN and RULES_END markers * If the file doesn't exist, it will create it * If the markers don't exist, it will append them to the existing content */ function writeRulesFile(rulesContent, rulesFilePath = path_1.default.join(process.cwd(), ".windsurfrules")) { let fileContent; const formattedRulesBlock = createRulesBlock(rulesContent); // Check if file exists if (fs_extra_1.default.existsSync(rulesFilePath)) { const existingContent = fs_extra_1.default.readFileSync(rulesFilePath, "utf8"); // Check if our markers exist if (existingContent.includes(RULES_BEGIN) && existingContent.includes(RULES_END)) { // Replace content between markers const beforeMarker = existingContent.split(RULES_BEGIN)[0]; const afterMarker = existingContent.split(RULES_END)[1]; fileContent = beforeMarker + formattedRulesBlock + afterMarker; } else { // Preserve the existing content and append markers // Ensure there's proper spacing between existing content and markers let separator = ""; if (!existingContent.endsWith("\n")) { separator += "\n"; } // Add an extra line if the file doesn't already end with multiple newlines if (!existingContent.endsWith("\n\n")) { separator += "\n"; } // Create the new file content with preserved original content fileContent = existingContent + separator + formattedRulesBlock; } } else { // Create new file with markers and content fileContent = formattedRulesBlock; } fs_extra_1.default.writeFileSync(rulesFilePath, fileContent); } /** * Generate the rules file content based on rule files */ function generateRulesFileContent(ruleFiles) { const alwaysRules = []; const autoAttachedRules = []; const agentRequestedRules = []; const manualRules = []; ruleFiles.forEach(({ path, metadata }) => { // Determine rule type based on metadata if (metadata.type === "always" || metadata.alwaysApply === true || metadata.alwaysApply === "true") { alwaysRules.push(path); } else if (metadata.type === "auto-attached" || metadata.globs) { const globPattern = typeof metadata.globs === "string" || Array.isArray(metadata.globs) ? Array.isArray(metadata.globs) ? metadata.globs.join(", ") : metadata.globs : undefined; if (globPattern !== undefined) { autoAttachedRules.push({ path, glob: globPattern }); } } else if (metadata.type === "agent-requested" || metadata.description) { agentRequestedRules.push(path); } else { // Default to manual inclusion manualRules.push(path); } }); // Generate the content let content = ""; // Always rules if (alwaysRules.length > 0) { content += "The following rules always apply to all files in the project:\n"; alwaysRules.forEach((rule) => { content += `- ${rule}\n`; }); content += "\n"; } // Auto Attached rules if (autoAttachedRules.length > 0) { content += "The following rules are automatically attached to matching glob patterns:\n"; autoAttachedRules.forEach((rule) => { content += `- [${rule.glob}] ${rule.path}\n`; }); content += "\n"; } // Agent Requested rules if (agentRequestedRules.length > 0) { content += "The following rules can be loaded when relevant. Check each file's description:\n"; agentRequestedRules.forEach((rule) => { content += `- ${rule}\n`; }); content += "\n"; } // Manual rules if (manualRules.length > 0) { content += "The following rules are only included when explicitly referenced:\n"; manualRules.forEach((rule) => { content += `- ${rule}\n`; }); content += "\n"; } return content.trim(); }