creatrip-agent-rules-builder
Version:
Unified converter for AI coding agent rules across Cursor, Windsurf, and Claude
219 lines (180 loc) • 6.88 kB
text/typescript
import chalk from "chalk";
import * as path from "path";
import * as fs from "fs";
import { parseAgentRulesFile, getDefaultRulesFilePath } from "./parser";
import { generateCursorRules } from "./generators/cursor";
import { generateWindsurfRules } from "./generators/windsurf";
import { generateClaudeRules } from "./generators/claude";
import { BuildOptions } from "./types";
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) => {
// /sub-app/ 같은 절대 경로 패턴 (슬래시로 끝남)
if (pattern.startsWith("/") && pattern.endsWith("/")) {
const cleanPattern = pattern.slice(1, -1);
return (
relativePath === cleanPattern ||
relativePath.startsWith(cleanPattern + "/")
);
}
// /sub-app 같은 절대 경로 패턴 (슬래시로 끝나지 않음)
if (pattern.startsWith("/")) {
const cleanPattern = pattern.slice(1);
return (
relativePath === cleanPattern ||
relativePath.startsWith(cleanPattern + "/")
);
}
// sub-app/ 같은 상대 경로 패턴
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(currentDir);
}
}
} catch (error) {
// 권한 문제 등으로 읽을 수 없는 디렉토리는 무시
}
return results;
}
return searchRecursively(dir);
}
function processRulesFile(filePath: string, showDetails: boolean = true): void {
const baseDir = path.dirname(filePath);
if (showDetails) {
console.log(chalk.gray(`처리 중: ${filePath}`));
}
const parsedContent = parseAgentRulesFile(filePath);
generateCursorRules(parsedContent, baseDir);
generateWindsurfRules(parsedContent, baseDir);
generateClaudeRules(parsedContent, baseDir);
}
export async function buildRules(options: BuildOptions): Promise<void> {
console.log(chalk.blue("🚀 Agent Rules Builder"));
// -f와 -r 옵션 동시 사용 체크
if (options.file && options.recursive) {
console.error(
chalk.red.bold(
"❌ 오류: -f(--file)와 -r(--recursive) 옵션은 동시에 사용할 수 없습니다",
),
);
console.log(chalk.yellow("\n💡 사용법:"));
console.log(
chalk.gray("특정 파일 처리: agent-rules build -f YOUR_FILE.md"),
);
console.log(chalk.gray("재귀 처리: agent-rules build -r"));
process.exit(1);
}
try {
if (options.recursive) {
const startDir = process.cwd();
console.log(chalk.gray(`재귀 탐색 시작: ${startDir}`));
const agentRulesFiles = findAgentRulesFiles(startDir);
if (agentRulesFiles.length === 0) {
console.log(chalk.yellow("⚠️ AGENTS.md 파일을 찾을 수 없습니다"));
return;
}
console.log(
chalk.yellow(
`📝 ${agentRulesFiles.length}개 위치에서 AGENTS.md 발견\n`,
),
);
for (const filePath of agentRulesFiles) {
const relativePath = path.relative(startDir, filePath) || ".";
console.log(chalk.cyan(`📁 /${relativePath}`));
processRulesFile(path.join(filePath, "AGENTS.md"), false);
console.log(chalk.green(` ✅ Cursor 규칙 생성 완료`));
console.log(chalk.green(` ✅ Windsurf 규칙 생성 완료`));
console.log(chalk.green(` ✅ Claude 규칙 생성 완료`));
console.log(""); // 위치 간 구분
}
console.log("========================================");
console.log(
chalk.green.bold(
`🎉 ${agentRulesFiles.length}개 위치에서 모든 규칙 파일이 생성되었습니다!`,
),
);
} else {
const filePath = options.file || getDefaultRulesFilePath();
const baseDir = path.dirname(filePath);
console.log(chalk.gray(`입력 파일: ${filePath}`));
console.log(chalk.gray(`출력 디렉토리: ${baseDir}`));
console.log(chalk.yellow("📝 파일 파싱 중..."));
const parsedContent = parseAgentRulesFile(filePath);
console.log(chalk.yellow("🔄 변환 중..."));
generateCursorRules(parsedContent, baseDir);
console.log(chalk.green(`✅ Cursor 규칙 생성 완료`));
generateWindsurfRules(parsedContent, baseDir);
console.log(chalk.green(`✅ Windsurf 규칙 생성 완료`));
generateClaudeRules(parsedContent, baseDir);
console.log(chalk.green(`✅ Claude 규칙 생성 완료`));
console.log(
chalk.green.bold("\n🎉 모든 규칙 파일이 성공적으로 생성되었습니다!"),
);
}
} catch (error) {
console.error(chalk.red.bold("❌ 오류 발생:"));
console.error(
chalk.red(error instanceof Error ? error.message : String(error)),
);
if (
error instanceof Error &&
error.message.includes("파일을 찾을 수 없습니다")
) {
console.log(chalk.yellow("\n💡 사용법:"));
console.log(chalk.gray("1. AGENTS.md 파일을 생성하세요"));
console.log(chalk.gray("2. agent-rules build 명령어를 실행하세요"));
console.log(
chalk.gray(
"3. 또는 agent-rules build --file YOUR_FILE.md로 다른 파일을 지정하세요",
),
);
}
process.exit(1);
}
}