import * as fs from "node:fs"; import * as path from "node:path"; export interface ResolvedRule { name: string; // path relative to its source dir, forward-slashed absPath: string; source: "base" | "local"; } function scan(dir: string, base = ""): { name: string; absPath: string }[] { const out: { name: string; absPath: string }[] = []; let entries: fs.Dirent[]; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { const code = (e as NodeJS.ErrnoException).code; if (code === "ENOENT" || code === "ENOTDIR") return out; throw e; } for (const e of entries) { const rel = base ? `${base}/${e.name}` : e.name; if (e.isDirectory()) out.push(...scan(path.join(dir, e.name), rel)); else if (e.isFile() && e.name.endsWith(".md")) out.push({ name: rel, absPath: path.join(dir, e.name) }); } return out; } /** Base rules ⊕ local rules; local appended last; a same-named local rule * replaces the base entry in place. Names are sorted within each source. */ export function resolveRuleFiles(baseDir: string, localDir: string): ResolvedRule[] { const byName = new Map(); for (const f of scan(baseDir).sort((a, b) => a.name.localeCompare(b.name))) byName.set(f.name, { ...f, source: "base" }); for (const f of scan(localDir).sort((a, b) => a.name.localeCompare(b.name))) byName.set(f.name, { ...f, source: "local" }); // local wins, keeps insertion slot return [...byName.values()]; }