127 lines
4.0 KiB
TypeScript
127 lines
4.0 KiB
TypeScript
/**
|
|
* 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,
|
|
};
|
|
}
|