aicm
Version:
A TypeScript CLI tool for managing AI IDE rules across different projects and teams
183 lines (180 loc) • 6.71 kB
JavaScript
;
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();
}