creatrip-agent-rules-builder
Version:
Unified converter for AI coding agent rules across Cursor, Windsurf, and Claude
267 lines (239 loc) • 8.07 kB
text/typescript
import { Command } from "commander";
import chalk from "chalk";
import { verifyAgents } from "../verifiers";
import { buildRules } from "../build";
import { LocationVerificationResult } from "../verifiers/types";
interface CIResult {
status: "pass" | "fail";
summary: {
total: number;
synced: number;
autoBuild: number;
needsReview: number;
};
needsReview?: Array<{
path: string;
issue: string;
files?: string[];
}>;
message: string;
}
export function createCICommand(): Command {
const command = new Command("ci");
command
.description("스마트 동기화: 자동으로 검증하고 필요시 빌드 실행")
.option("-r, --recursive", "하위 디렉토리를 재귀적으로 처리")
.option("--json", "JSON 형식으로 결과 출력")
.action(async (options) => {
const isRecursive = options.recursive || false;
const isJson = options.json || false;
try {
const result = await runCI(isRecursive, isJson);
if (isJson) {
console.log(JSON.stringify(result, null, 2));
}
process.exit(result.status === "pass" ? 0 : 1);
} catch (error) {
if (!isJson) {
console.error(
chalk.red(
`오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
),
);
}
process.exit(1);
}
});
return command;
}
async function runCI(isRecursive: boolean, isJson: boolean): Promise<CIResult> {
const rootPath = process.cwd();
if (!isJson) {
console.log(chalk.blue("🔍 일관성 검증 중..."));
}
// 1단계: 검증
const verifyResult = await verifyAgents(rootPath, "json", isRecursive);
const ciResult: CIResult = {
status: "pass",
summary: {
total: 0,
synced: 0,
autoBuild: 0,
needsReview: 0,
},
message: "",
};
const needsReviewList: Array<{
path: string;
issue: string;
files?: string[];
}> = [];
// 각 위치별로 처리
for (const location of verifyResult.locations) {
ciResult.summary.total++;
if (location.status === "pass") {
// 이미 동기화됨
ciResult.summary.synced++;
if (!isJson) {
const relativePath = location.path.replace(rootPath, "") || "/";
if (isRecursive) {
console.log(chalk.green(`📁 ${relativePath}`));
console.log(chalk.green(" ✅ 동기화됨"));
}
}
} else {
// 패턴에 따라 처리
switch (location.diagnosis?.pattern) {
case "all_outdated":
// 자동 빌드 실행
if (!isJson) {
const relativePath = location.path.replace(rootPath, "") || "/";
if (isRecursive) {
console.log(chalk.cyan(`📁 ${relativePath}`));
console.log(chalk.yellow(" ⚠️ 오래됨 → 자동 빌드"));
} else {
console.log(
chalk.yellow("⚠️ 모든 파일이 오래됨 (AGENTS.md가 더 최신)"),
);
console.log(chalk.blue("🔨 자동으로 빌드를 실행합니다..."));
}
}
// 빌드 실행
await buildRulesForLocation(location.path, isJson);
ciResult.summary.autoBuild++;
if (!isJson) {
if (isRecursive) {
console.log(chalk.green(" ✅ 빌드 완료"));
} else {
console.log(
chalk.green("✅ 빌드 완료 - 모든 파일이 동기화되었습니다"),
);
}
}
break;
case "single_diverged":
case "multiple_diverged":
// 수동 검토 필요
ciResult.status = "fail";
ciResult.summary.needsReview++;
const relativePath = location.path.replace(rootPath, "") || "/";
needsReviewList.push({
path: relativePath,
issue: location.diagnosis.pattern,
files: location.diagnosis.diverged,
});
if (!isJson) {
if (isRecursive) {
console.log(chalk.red(`📁 ${relativePath}`));
if (location.diagnosis.pattern === "single_diverged") {
console.log(
chalk.red(
` ❌ ${location.diagnosis.diverged?.[0]} 파일 수동 편집 감지`,
),
);
} else {
console.log(chalk.red(` ❌ 여러 파일 수동 편집 감지`));
}
} else {
if (location.diagnosis.pattern === "single_diverged") {
console.log(
chalk.red(
`❌ ${location.diagnosis.diverged?.[0]} 파일에 수동 편집 감지`,
),
);
console.log("");
console.log(chalk.yellow("수동 검토가 필요합니다:"));
console.log(
chalk.yellow(
`- ${location.diagnosis.diverged?.[0]} 파일이 직접 수정되었습니다`,
),
);
console.log(
chalk.yellow(
"- AGENTS.md를 수정하려던 것이 아닌지 확인하세요",
),
);
} else {
console.log(chalk.red("❌ 여러 파일에서 수동 편집 감지"));
console.log("");
console.log(chalk.yellow("수동 검토가 필요합니다:"));
location.diagnosis.diverged?.forEach((file) => {
console.log(chalk.yellow(`- ${file} 파일이 직접 수정됨`));
});
}
}
}
break;
}
}
if (!isJson && isRecursive) {
console.log(""); // 위치 간 구분
}
}
// 결과 메시지 설정
if (ciResult.status === "pass") {
if (ciResult.summary.autoBuild > 0) {
ciResult.message = `Auto-build completed for ${ciResult.summary.autoBuild} location(s)`;
} else {
ciResult.message = "All files are already synchronized";
}
if (!isJson) {
if (isRecursive) {
console.log("========================================");
console.log(
chalk.green(
`📊 요약: ${ciResult.summary.total}개 위치 모두 동기화됨`,
),
);
if (ciResult.summary.autoBuild > 0) {
console.log(
chalk.green(` (${ciResult.summary.autoBuild}개 자동 빌드됨)`),
);
}
} else if (ciResult.summary.autoBuild === 0) {
console.log(chalk.green("✅ 이미 모든 파일이 동기화되어 있습니다"));
}
}
} else {
ciResult.needsReview = needsReviewList;
ciResult.message = `Manual review required for ${ciResult.summary.needsReview} location(s)`;
if (!isJson && isRecursive) {
console.log("========================================");
console.log(
chalk.yellow(
`📊 요약: ${ciResult.summary.total}개 위치 중 ${ciResult.summary.needsReview}개 수동 검토 필요`,
),
);
console.log(chalk.red("❌ CI 실패: 수동 검토가 필요한 파일이 있습니다"));
console.log("");
console.log(chalk.yellow("문제 위치:"));
needsReviewList.forEach((item) => {
console.log(
chalk.yellow(
`- ${item.path}: ${item.files?.join(", ")} 파일 수동 편집`,
),
);
});
}
}
return ciResult;
}
async function buildRulesForLocation(
locationPath: string,
isJson: boolean,
): Promise<void> {
// build.ts의 로직 재사용
const options = {
file: `${locationPath}/AGENTS.md`,
};
// 빌드 실행 (출력 억제)
const originalLog = console.log;
if (isJson) {
console.log = () => {}; // JSON 모드에서는 출력 억제
}
try {
await buildRules(options);
} finally {
console.log = originalLog;
}
}