UNPKG

scai

Version:

> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.

196 lines (195 loc) 8.38 kB
import chalk from "chalk"; import { getCommentSyntax } from "../../utils/commentMap.js"; import { detectFileType } from "../../fileRules/detectFileType.js"; 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`."); // Determine language from filepath extension const ext = "." + (filepath?.split(".").pop() || "ts"); const language = detectFileType(filepath ?? ext); const syntax = getCommentSyntax(language); // --- 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 for comment preservation --- let inBlockComment = false; let blockLines = []; const classifyLine = (line) => { const trimmed = line.trimStart(); // Single-line comments for (const s of syntax.singleLine) { if (trimmed.startsWith(s)) return line; } const multiLineComments = syntax.multiLine ?? []; if (!inBlockComment) { for (const { start, end } of multiLineComments) { if (trimmed.startsWith(start) || trimmed.startsWith("/**")) { blockLines = [line]; if (trimmed.includes(end) && trimmed.indexOf(end) > trimmed.indexOf(start)) { return blockLines.join("\n"); } else { inBlockComment = true; return null; } } } // ✅ New: Handle stray lines that look like part of a comment if (trimmed.startsWith("*")) { return line; } } else { blockLines.push(line); if (trimmed.includes("*/")) { inBlockComment = false; const fullBlock = blockLines.join("\n"); blockLines = []; return fullBlock; } return null; // still inside block } return "code"; }; // --- Collect comments into map --- function collectCommentsMap(lines) { const map = new Map(); let commentBuffer = []; for (const line of lines) { const result = classifyLine(line); if (result === "code") { if (commentBuffer.length > 0) { const key = line.trim().toLowerCase(); const block = commentBuffer.join("\n").trim().toLowerCase(); if (!map.has(key)) map.set(key, new Set()); map.get(key).add(block); commentBuffer = []; } continue; } if (typeof result === "string") commentBuffer.push(result); } if (commentBuffer.length > 0) { const key = ""; const block = commentBuffer.join("\n").trim().toLowerCase(); if (!map.has(key)) map.set(key, new Set()); map.get(key).add(block); } if (blockLines.length > 0) { const key = ""; const block = blockLines.join("\n").trim().toLowerCase(); if (!map.has(key)) map.set(key, new Set()); map.get(key).add(block); blockLines = []; } return map; } // --- Step 1: Collect comments --- const modelComments = collectCommentsMap(newLines); const origComments = collectCommentsMap(origLines); // --- Step 2: Remove duplicates --- for (const [key, modelSet] of modelComments.entries()) { const origSet = origComments.get(key); if (!origSet) continue; for (const c of Array.from(modelSet)) { if (origSet.has(c)) modelSet.delete(c); } if (modelSet.size === 0) modelComments.delete(key); } // --- Step 3: Build fixed lines --- let fixedLines = []; for (const origLine of origLines) { const key = origLine.trim().toLowerCase(); if (modelComments.has(key)) { for (const block of modelComments.get(key)) { for (const line of block.split("\n")) { const norm = line.trim().toLowerCase(); if (!fixedLines.some(l => l.trim().toLowerCase() === norm)) { fixedLines.push(line); } } } } fixedLines.push(origLine); } // --- Step 4: Remove opening/closing fences at top/bottom --- while (fixedLines.length && /^```(?:\w+)?$/.test(fixedLines[0].trim())) { fixedLines.shift(); } while (fixedLines.length && /^```(?:\w+)?$/.test(fixedLines[fixedLines.length - 1].trim())) { fixedLines.pop(); } // --- Step 5: Remove opening/closing fences at top/bottom --- while (fixedLines.length && /^```(?:\w+)?$/.test(fixedLines[0].trim())) { console.log(chalk.red(`[preserveCodeModule] Removing top fence: "${fixedLines[0].trim()}"`)); fixedLines.shift(); } while (fixedLines.length && /^```(?:\w+)?$/.test(fixedLines[fixedLines.length - 1].trim())) { console.log(chalk.red(`[preserveCodeModule] Removing bottom fence: "${fixedLines[fixedLines.length - 1].trim()}"`)); fixedLines.pop(); } // --- Step 6: Remove any lingering triple ticks inside content --- fixedLines = fixedLines.filter(line => { if (/^```(?:\w+)?$/.test(line.trim())) { console.log(chalk.red(`[preserveCodeModule] Removing lingering fence: "${line.trim()}"`)); return false; } return true; }); // --- Step 7 (final): Clean stray '*' and '*/' lines --- let insideBlock = false; fixedLines = fixedLines.map(line => { const trimmed = line.trim(); // Enter block comment if (trimmed.startsWith("/*") || trimmed.startsWith("/**")) { insideBlock = true; return line; } // Exit block comment if (trimmed.startsWith("*/")) { if (insideBlock) { insideBlock = false; return line; // keep valid closer } else { return ""; // remove stray closer } } // If inside a block, keep '*' lines as-is if (insideBlock && trimmed.startsWith("*")) { return line; } // If not inside a block but line starts with '*' if (!insideBlock && trimmed.startsWith("*")) { const afterStar = trimmed.slice(1).trim(); // ✅ If it's a JSDoc tag (starts with @), keep as-is if (afterStar.startsWith("@")) { return line; } // Otherwise, treat as stray and convert to // const indent = line.slice(0, line.indexOf("*")); return indent + "//" + line.slice(line.indexOf("*") + 1); } return line; }).filter(line => line !== ""); // remove empty stray lines // --- Return PromptOutput --- return { content: fixedLines.join("\n"), filepath, mode: "overwrite", }; } };