creatrip-agent-rules-builder
Version:
Unified converter for AI coding agent rules across Cursor, Windsurf, and Claude
281 lines (241 loc) • 7.09 kB
text/typescript
import * as fs from "fs";
import * as path from "path";
import {
VerificationSummary,
VerificationResult,
LocationVerificationResult,
OutputFormat,
} from "./types";
import { compareContent, extractRulesFromMdc } from "./comparator";
import { parseAgentRulesFile } from "../parser";
interface AgentInfo {
name: string;
type: "standard" | "cursor-mdc";
getPath: (baseDir: string, config: any) => string;
}
const AGENTS: AgentInfo[] = [
{
name: "cursor",
type: "cursor-mdc",
getPath: (baseDir: string, config: any) => {
const filename = config.cursor?.filename || "rules";
return path.join(baseDir, ".cursor", "rules", `${filename}.mdc`);
},
},
{
name: "windsurf",
type: "standard",
getPath: (baseDir: string) => path.join(baseDir, ".windsurfrules"),
},
{
name: "claude",
type: "standard",
getPath: (baseDir: string) => path.join(baseDir, "CLAUDE.md"),
},
];
async function verifyAgent(
agentInfo: AgentInfo,
sourcePath: string,
baseDir: string,
): Promise<VerificationResult> {
try {
const parsedContent = parseAgentRulesFile(sourcePath);
const targetPath = agentInfo.getPath(baseDir, parsedContent.config);
if (!fs.existsSync(targetPath)) {
return {
agent: agentInfo.name,
passed: false,
error: "file not found",
};
}
const targetContent = fs.readFileSync(targetPath, "utf8");
let processedTarget = targetContent;
if (agentInfo.type === "cursor-mdc") {
processedTarget = extractRulesFromMdc(targetContent);
}
const isEqual = compareContent(parsedContent.rules, processedTarget);
return {
agent: agentInfo.name,
passed: isEqual,
error: isEqual ? undefined : "content mismatch",
};
} catch (error) {
return {
agent: agentInfo.name,
passed: false,
error: error instanceof Error ? error.message : "unknown error",
};
}
}
async function verifyLocation(
locationPath: string,
): Promise<LocationVerificationResult> {
const sourcePath = path.join(locationPath, "AGENTS.md");
if (!fs.existsSync(sourcePath)) {
throw new Error(`Source file not found: ${sourcePath}`);
}
const results: VerificationResult[] = await Promise.all(
AGENTS.map((agent) => verifyAgent(agent, sourcePath, locationPath)),
);
const location: LocationVerificationResult = {
path: locationPath,
status: "pass",
agents: {},
summary: {
total: results.length,
passed: 0,
failed: 0,
},
};
const failedAgents: string[] = [];
for (const result of results) {
location.agents[result.agent] = {
passed: result.passed,
error: result.error,
};
if (result.passed) {
location.summary.passed++;
} else {
location.summary.failed++;
location.status = "fail";
failedAgents.push(result.agent);
}
}
// 패턴 분석 및 diagnosis 추가
const failedCount = failedAgents.length;
if (failedCount === 0) {
location.diagnosis = {
pattern: "all_synced",
action: "none",
};
} else if (failedCount === results.length) {
location.diagnosis = {
pattern: "all_outdated",
action: "build",
};
} else if (failedCount === 1) {
location.diagnosis = {
pattern: "single_diverged",
action: "review",
diverged: failedAgents,
};
} else {
location.diagnosis = {
pattern: "multiple_diverged",
action: "manual",
diverged: failedAgents,
};
}
return location;
}
function findAgentRulesFiles(dir: string): string[] {
const gitignorePath = path.join(dir, ".gitignore");
let gitignorePatterns: string[] = [];
if (fs.existsSync(gitignorePath)) {
const gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
gitignorePatterns = gitignoreContent
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"));
}
const defaultIgnorePatterns = [
"node_modules",
".git",
"dist",
"build",
"out",
".next",
".nuxt",
"coverage",
".nyc_output",
];
const allIgnorePatterns = [
...new Set([...defaultIgnorePatterns, ...gitignorePatterns]),
];
function shouldIgnoreDir(dirPath: string): boolean {
const dirName = path.basename(dirPath);
const relativePath = path.relative(dir, dirPath);
return allIgnorePatterns.some((pattern) => {
if (pattern.startsWith("/") && pattern.endsWith("/")) {
const cleanPattern = pattern.slice(1, -1);
return (
relativePath === cleanPattern ||
relativePath.startsWith(cleanPattern + "/")
);
}
if (pattern.startsWith("/")) {
const cleanPattern = pattern.slice(1);
return (
relativePath === cleanPattern ||
relativePath.startsWith(cleanPattern + "/")
);
}
if (pattern.endsWith("/")) {
const cleanPattern = pattern.slice(0, -1);
return (
dirName === cleanPattern ||
relativePath === cleanPattern ||
relativePath.startsWith(cleanPattern + "/")
);
}
return dirName === pattern || relativePath === pattern;
});
}
function searchRecursively(currentDir: string): string[] {
const results: string[] = [];
try {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (!shouldIgnoreDir(fullPath)) {
results.push(...searchRecursively(fullPath));
}
} else if (entry.name === "AGENTS.md") {
results.push(path.dirname(fullPath));
}
}
} catch (error) {
// 권한 문제 등으로 읽을 수 없는 디렉토리는 무시
}
return results;
}
return searchRecursively(dir);
}
export async function verifyAgents(
rootPath: string,
outputFormat: OutputFormat,
recursive: boolean = false,
): Promise<VerificationSummary> {
let locations: LocationVerificationResult[] = [];
if (recursive) {
// 재귀 모드: 여러 AGENTS.md 파일 찾기
const agentRulesPaths = findAgentRulesFiles(rootPath);
if (agentRulesPaths.length === 0) {
throw new Error(`No AGENTS.md files found in ${rootPath}`);
}
for (const locationPath of agentRulesPaths) {
try {
const result = await verifyLocation(locationPath);
locations.push(result);
} catch (error) {
// 개별 위치 오류는 스킵하고 계속
console.error(`Error verifying ${locationPath}: ${error}`);
}
}
} else {
// 단일 디렉토리 모드
const result = await verifyLocation(rootPath);
locations = [result];
}
// 전체 상태 계산
const overallStatus = locations.some((loc) => loc.status === "fail")
? "fail"
: "pass";
return {
status: overallStatus,
locations,
totalLocations: locations.length,
timestamp: new Date().toISOString(),
};
}