ccguard
Version:
Automated enforcement of net-negative LOC, complexity constraints, and quality standards for Claude code
195 lines • 7.12 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileScanner = void 0;
const fs_1 = __importDefault(require("fs"));
const crypto_1 = __importDefault(require("crypto"));
const child_process_1 = require("child_process");
const util_1 = require("util");
const GitIgnoreParser_1 = require("./GitIgnoreParser");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class FileScanner {
gitIgnoreParser;
rootDir;
ignoreEmptyLines;
maxFileSizeBytes;
// Default max file size: 10MB
static DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
constructor(rootDir, ignoreEmptyLines = true, maxFileSizeBytes) {
this.rootDir = rootDir;
this.gitIgnoreParser = new GitIgnoreParser_1.GitIgnoreParser(rootDir);
this.ignoreEmptyLines = ignoreEmptyLines;
this.maxFileSizeBytes = maxFileSizeBytes ?? FileScanner.DEFAULT_MAX_FILE_SIZE;
}
/**
* Scan all files in the project (excluding those in .gitignore)
*/
async scanProject() {
const files = new Map();
const allFiles = this.gitIgnoreParser.getAllFiles();
for (const filePath of allFiles) {
try {
const snapshot = await this.scanFile(filePath);
if (snapshot) {
files.set(filePath, snapshot);
}
}
catch (error) {
console.debug(`Error scanning file ${filePath}:`, error);
}
}
return files;
}
/**
* Scan specific files
*/
async scanFiles(filePaths) {
const files = new Map();
for (const filePath of filePaths) {
if (this.gitIgnoreParser.isIgnored(filePath)) {
continue;
}
try {
const snapshot = await this.scanFile(filePath);
if (snapshot) {
files.set(filePath, snapshot);
}
}
catch (error) {
console.debug(`Error scanning file ${filePath}:`, error);
}
}
return files;
}
/**
* Scan a single file
*/
async scanFile(filePath) {
try {
// Check if file exists
if (!fs_1.default.existsSync(filePath)) {
return null;
}
const stats = fs_1.default.statSync(filePath);
// Skip if not a regular file
if (!stats.isFile()) {
return null;
}
// Skip files that are too large (security and performance)
if (stats.size > this.maxFileSizeBytes) {
console.debug(`Skipping large file ${filePath}: ${stats.size} bytes (max: ${this.maxFileSizeBytes})`);
return null;
}
// Skip binary files (simple heuristic based on extension)
if (this.isBinaryFile(filePath)) {
return null;
}
const content = fs_1.default.readFileSync(filePath, 'utf-8');
const locCount = await this.countLinesWithWc(filePath);
const hash = this.calculateHash(content);
return {
path: filePath,
locCount,
hash,
lastModified: stats.mtimeMs,
content,
};
}
catch {
// Handle files that can't be read (permissions, etc.)
return null;
}
}
/**
* Count lines using wc -l command
*/
async countLinesWithWc(filePath) {
try {
// Use wc -l to count lines
const { stdout } = await execAsync(`wc -l < "${filePath}"`);
let totalLines = parseInt(stdout.trim(), 10);
// wc -l doesn't count the last line if it doesn't end with newline
// Check if file ends with newline
const content = fs_1.default.readFileSync(filePath, 'utf-8');
if (content.length > 0 && !content.endsWith('\n')) {
totalLines += 1;
}
// If ignoring empty lines, we need to count non-empty lines
if (this.ignoreEmptyLines) {
// Use grep to count non-empty lines
try {
const { stdout: grepOut } = await execAsync(`grep -c -v '^$' "${filePath}"`);
return parseInt(grepOut.trim(), 10);
}
catch (grepError) {
// grep returns exit code 1 if no lines match (all empty)
if (grepError.code === 1) {
return 0;
}
// Fall back to total lines if grep fails
return totalLines;
}
}
return totalLines;
}
catch {
// Fallback: count lines manually if command fails
const content = fs_1.default.readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
if (this.ignoreEmptyLines) {
return lines.filter(line => line.trim().length > 0).length;
}
return lines.length;
}
}
/**
* Calculate hash of file content
*/
calculateHash(content) {
return crypto_1.default.createHash('sha256').update(content).digest('hex');
}
/**
* Simple heuristic to detect binary files
*/
isBinaryFile(filePath) {
const binaryExtensions = [
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.svg',
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.zip', '.tar', '.gz', '.rar', '.7z',
'.exe', '.dll', '.so', '.dylib',
'.mp3', '.mp4', '.avi', '.mov', '.wmv',
'.ttf', '.otf', '.woff', '.woff2',
'.db', '.sqlite',
'.pyc', '.class', '.o', '.a',
'.min.js', '.min.css', // Minified files
];
const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0];
return ext ? binaryExtensions.includes(ext) : false;
}
/**
* Get files that would be affected by an operation
*/
getAffectedFiles(operation) {
// For known file operations, return specific files for efficiency
switch (operation.tool_name) {
case 'Edit':
case 'Write':
if (operation.tool_input?.file_path) {
return [operation.tool_input.file_path];
}
break;
case 'MultiEdit':
if (operation.tool_input?.file_path) {
return [operation.tool_input.file_path];
}
break;
}
// For all other tools (including Bash), return empty array
// This will trigger a full project scan in the snapshot manager
return [];
}
}
exports.FileScanner = FileScanner;
//# sourceMappingURL=FileScanner.js.map