scai
Version:
> AI-powered CLI tool for commit messages **and** pull request reviews — using local models.
152 lines (151 loc) • 6.75 kB
JavaScript
import chalk from "chalk";
export const preserveCodeModule = {
name: "preserveCodeModule",
description: "Ensure code matches original exactly, preserving comments with clear before/after output",
async run(input) {
const { originalContent, content, filepath } = input;
if (!originalContent)
throw new Error("Requires `originalContent`.");
const syntax = {
singleLine: ["//"],
multiLine: [{ start: "/*", end: "*/" }, { start: "/**", end: "*/" }]
};
// --- Normalize line endings ---
const normalize = (txt) => txt.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const origLines = normalize(originalContent).split("\n");
const newLines = normalize(content).split("\n");
// --- Classify line ---
let inBlockComment = false;
let blockLines = [];
const classifyLine = (line) => {
const trimmed = line.trimStart();
// --- Single-line comment ---
for (const s of syntax.singleLine) {
if (trimmed.startsWith(s))
return "comment";
}
// --- Multi-line comment ---
for (const { start, end } of syntax.multiLine) {
if (!inBlockComment) {
if (trimmed.startsWith(start) && trimmed.includes(end)) {
return "comment"; // entire block on one line
}
if (trimmed.startsWith(start)) {
inBlockComment = true;
blockLines = [line];
return ""; // wait until block ends
}
}
else {
blockLines.push(line);
if (trimmed.includes(end)) {
inBlockComment = false;
return "comment"; // end of block
}
return ""; // inside block
}
}
return "code";
};
// --- Helper: collect comment blocks into map ---
function collectCommentsMap(lines) {
const map = new Map();
let commentBuffer = [];
for (const line of lines) {
const type = classifyLine(line);
if (type === "comment") {
// Collect full comment line
commentBuffer.push(line);
}
else if (type === "code") {
// Flush buffer when hitting code
if (commentBuffer.length > 0) {
const key = line.trim().toLowerCase();
if (!map.has(key))
map.set(key, new Set());
const commentBlock = commentBuffer.map(l => l.trimEnd()).join("\n").toLowerCase();
map.get(key).add(commentBlock);
commentBuffer = [];
}
}
}
// Flush remaining comments at EOF
if (commentBuffer.length > 0) {
const key = "";
if (!map.has(key))
map.set(key, new Set());
const commentBlock = commentBuffer.map(l => l.trimEnd()).join("\n").toLowerCase();
map.get(key).add(commentBlock);
}
return map;
}
// --- Step 1: Collect comments ---
const modelComments = collectCommentsMap(newLines); // model first
const origComments = collectCommentsMap(origLines); // original
// --- Step 2: Remove duplicates ---
for (const [key, commentSet] of modelComments.entries()) {
if (origComments.has(key)) {
commentSet.forEach(c => {
origComments.get(key).delete(c.trim().toLowerCase());
});
if (origComments.get(key).size === 0)
origComments.delete(key);
}
}
// --- Step 3: Build fixed lines with model comments inserted above original ---
const fixedLines = [];
for (const origLine of origLines) {
const key = origLine.trim().toLowerCase();
// Insert model comment blocks if any
if (modelComments.has(key)) {
modelComments.get(key).forEach(block => {
const lines = block.split("\n");
for (const line of lines) {
if (!fixedLines.includes(line)) {
fixedLines.push(line);
console.log(chalk.blue("Inserted comment:"), line.trim());
}
else {
console.log(chalk.gray("Skipped duplicate comment:"), line.trim());
}
}
});
}
fixedLines.push(origLine);
}
// --- Logging for debugging ---
console.log(chalk.bold.blue("\n=== LINE CLASSIFICATION (original) ==="));
origLines.forEach((line, i) => {
const type = classifyLine(line);
const colored = type === "code"
? chalk.green(line)
: type === "comment"
? chalk.yellow(line)
: chalk.gray(line); // "" means middle of block
console.log(`${i + 1}: ${colored} ${chalk.gray(`[${type}]`)}`);
});
console.log(chalk.bold.blue("\n=== LINE CLASSIFICATION (model) ==="));
newLines.forEach((line, i) => {
const type = classifyLine(line);
const colored = type === "code"
? chalk.green(line)
: type === "comment"
? chalk.yellow(line)
: chalk.gray(line);
console.log(`${i + 1}: ${colored} ${chalk.gray(`[${type}]`)}`);
});
console.log(chalk.bold.blue("\n=== FIXED CONTENT ==="));
fixedLines.forEach((line, i) => {
// classifyLine might not be ideal here since fixedLines are final
// so we treat anything starting with a comment marker as "comment"
const trimmed = line.trimStart();
const type = syntax.singleLine.some(s => trimmed.startsWith(s)) ||
syntax.multiLine.some(({ start }) => trimmed.startsWith(start))
? "comment"
: "code";
const colored = type === "code" ? chalk.green(line) : chalk.yellow(line);
console.log(`${i + 1}: ${colored} ${chalk.gray(`[${type}]`)}`);
});
return { content: fixedLines.join("\n"), filepath };
}
};