feat: AI Codebase Doc Drift Detector CLI v0.1.0
This commit is contained in:
+126
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Core library — orchestrates scan / check / update operations.
|
||||
*/
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { resolve, relative, extname } from 'node:path';
|
||||
import type { Config, ScannedFile } from './types.js';
|
||||
import { loadConfig, hasConfig } from './config.js';
|
||||
import { scanFile } from './scanner.js';
|
||||
import { generateDocs, readExistingDocs } from './doc-generator.js';
|
||||
import { detectDrift, renderDriftReport } from './drift-detector.js';
|
||||
import { getGitRoot, getAllTrackedFiles } from './git-tracker.js';
|
||||
|
||||
function shouldInclude(filePath: string, config: Config, cwd: string): boolean {
|
||||
const rel = relative(cwd, filePath);
|
||||
const ext = extname(filePath);
|
||||
if (!config.extensions.includes(ext)) return false;
|
||||
|
||||
for (const exc of config.exclude) {
|
||||
const pattern = exc.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
|
||||
const re = new RegExp(pattern);
|
||||
if (re.test(rel)) return false;
|
||||
}
|
||||
|
||||
for (const inc of config.include) {
|
||||
const incPath = resolve(cwd, inc);
|
||||
const incStat = statSync(incPath);
|
||||
if (incStat.isDirectory()) {
|
||||
if (rel.startsWith(relative(cwd, incPath))) return true;
|
||||
} else if (rel === relative(cwd, incPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function scanProject(cwd: string): { scanned: ScannedFile[]; config: Config } {
|
||||
if (!hasConfig(cwd)) {
|
||||
throw new Error(`No ai-doc-drift.json found in ${cwd}. Run \`ai-doc-drift init\` first.`);
|
||||
}
|
||||
const config = loadConfig(cwd);
|
||||
const gitRoot = getGitRoot(cwd);
|
||||
let files: string[] = [];
|
||||
|
||||
if (gitRoot) {
|
||||
files = getAllTrackedFiles(cwd)
|
||||
.map((f) => resolve(cwd, f))
|
||||
.filter((f) => shouldInclude(f, config, cwd));
|
||||
} else {
|
||||
// Fallback: recursive walk of include dirs
|
||||
const walk = (dir: string): string[] => {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
const result: string[] = [];
|
||||
for (const e of entries) {
|
||||
const p = resolve(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
const rel = relative(cwd, p);
|
||||
let excluded = false;
|
||||
for (const exc of config.exclude) {
|
||||
const pattern = exc.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
|
||||
const re = new RegExp(pattern);
|
||||
if (re.test(rel)) { excluded = true; break; }
|
||||
}
|
||||
if (!excluded) result.push(...walk(p));
|
||||
} else if (e.isFile() && shouldInclude(p, config, cwd)) {
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const inc of config.include) {
|
||||
const incPath = resolve(cwd, inc);
|
||||
if (statSync(incPath).isDirectory()) {
|
||||
files.push(...walk(incPath));
|
||||
} else {
|
||||
files.push(incPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scanned: ScannedFile[] = [];
|
||||
for (const f of files) {
|
||||
try {
|
||||
const source = readFileSync(f, 'utf-8');
|
||||
scanned.push(scanFile(relative(cwd, f), source));
|
||||
} catch {
|
||||
// skip unreadable
|
||||
}
|
||||
}
|
||||
|
||||
return { scanned, config };
|
||||
}
|
||||
|
||||
export function runCheck(cwd: string): { report: string; exitCode: number } {
|
||||
const { scanned, config } = scanProject(cwd);
|
||||
const existing = readExistingDocs(resolve(cwd, config.docsDir));
|
||||
|
||||
if (existing.size === 0) {
|
||||
return {
|
||||
report: `No existing docs found in ${config.docsDir}. Run \`ai-doc-drift scan\` first.`,
|
||||
exitCode: 1,
|
||||
};
|
||||
}
|
||||
|
||||
const drift = detectDrift(scanned, existing);
|
||||
drift.projectName = config.projectName;
|
||||
const report = renderDriftReport(drift);
|
||||
return { report, exitCode: drift.items.length > 0 ? 2 : 0 };
|
||||
}
|
||||
|
||||
export function runScan(cwd: string): { report: string; exitCode: number } {
|
||||
const { scanned, config } = scanProject(cwd);
|
||||
const written = generateDocs(scanned, config, cwd);
|
||||
return {
|
||||
report: `Generated docs:\n${written.map((w) => ` - ${w}`).join('\n')}`,
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function runUpdate(cwd: string): { report: string; exitCode: number } {
|
||||
const { scanned, config } = scanProject(cwd);
|
||||
const written = generateDocs(scanned, config, cwd);
|
||||
return {
|
||||
report: `Updated docs:\n${written.map((w) => ` - ${w}`).join('\n')}`,
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user