import { execFileSync } from "node:child_process"; import { readdirSync, readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); function main() { const options = parseArgs(process.argv.slice(2)); const config = readConfig(); const trackedRepoNames = readTrackedRepoNames(config.archivePath, options.repoNames); const availableRepos = readAvailableRepos(config.sourceRepoRoot); const sinceIso = new Date(Date.now() - options.hours * 60 * 60 * 1000).toISOString(); const repos = trackedRepoNames.map((repoName) => inspectRepoActivity({ repoName, repoPath: availableRepos.get(repoName.toLowerCase()), sinceIso, maxCommits: options.maxCommits, }), ); repos.sort(compareRepoActivity); process.stdout.write( `${JSON.stringify( { generatedAt: new Date().toISOString(), sourceRepoRoot: config.sourceRepoRoot, archivePath: config.archivePath, since: sinceIso, hours: options.hours, maxCommits: options.maxCommits, totalTrackedRepos: trackedRepoNames.length, repos, digest: renderDigest(repos, sinceIso), }, null, 2, )}\n`, ); } function parseArgs(args) { const options = { hours: 72, maxCommits: 3, repoNames: undefined, }; for (let index = 0; index < args.length; index += 1) { const argument = args[index]; switch (argument) { case "--hours": options.hours = Number.parseInt(args[index + 1] ?? "", 10); index += 1; break; case "--max-commits": options.maxCommits = Number.parseInt(args[index + 1] ?? "", 10); index += 1; break; case "--repos": options.repoNames = parseRepoList(args[index + 1] ?? ""); index += 1; break; default: throw new Error(`Unknown argument: ${argument}`); } } if (!Number.isInteger(options.hours) || options.hours <= 0) { throw new Error("--hours must be a positive integer."); } if (!Number.isInteger(options.maxCommits) || options.maxCommits <= 0) { throw new Error("--max-commits must be a positive integer."); } return options; } function parseRepoList(value) { return value .split(",") .map((entry) => entry.trim()) .filter(Boolean); } function readConfig() { const envPath = resolve(repoRoot, ".env"); const env = parseDotEnvSafe(envPath); const sourceRepoRoot = env.SOURCE_REPO_ROOT?.trim(); const archivePath = resolve( repoRoot, env.RAG_SOURCE_ARCHIVE_PATH?.trim() || ".voidbot/rag/source-documents.json", ); if (!sourceRepoRoot) { throw new Error("SOURCE_REPO_ROOT is not configured in .env."); } return { sourceRepoRoot: resolve(sourceRepoRoot), archivePath, }; } function parseDotEnvSafe(envPath) { try { return parseDotEnv(stripLeadingBom(readFileSync(envPath, "utf8"))); } catch (error) { if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { return {}; } throw error; } } function parseDotEnv(raw) { const result = {}; for (const line of raw.split(/\r?\n/)) { const trimmed = line.trim(); if (trimmed.length === 0 || trimmed.startsWith("#")) { continue; } const separatorIndex = trimmed.indexOf("="); if (separatorIndex === -1) { continue; } const key = trimmed.slice(0, separatorIndex).trim(); let value = trimmed.slice(separatorIndex + 1).trim(); if ( value.length >= 2 && ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) ) { value = value.slice(1, -1); } result[key] = value; } return result; } function readTrackedRepoNames(archivePath, requestedRepoNames) { const raw = stripLeadingBom(readFileSync(archivePath, "utf8")); const parsed = JSON.parse(raw); const repoNames = Array.isArray(parsed?.repos) ? parsed.repos .map((entry) => (entry && typeof entry.repoName === "string" ? entry.repoName : undefined)) .filter(Boolean) : []; if (repoNames.length === 0) { throw new Error(`No tracked repos were found in ${archivePath}.`); } if (!requestedRepoNames || requestedRepoNames.length === 0) { return repoNames; } const requestedLookup = new Set(requestedRepoNames.map((value) => value.toLowerCase())); const selected = repoNames.filter((repoName) => requestedLookup.has(repoName.toLowerCase())); const missing = requestedRepoNames.filter( (repoName) => !selected.some((value) => value.toLowerCase() === repoName.toLowerCase()), ); if (missing.length > 0) { throw new Error(`Requested tracked repos are missing from the source archive manifest: ${missing.join(", ")}`); } return selected; } function readAvailableRepos(sourceRepoRoot) { const lookup = new Map(); for (const entry of readdirSync(sourceRepoRoot, { withFileTypes: true })) { if (!entry.isDirectory()) { continue; } lookup.set(entry.name.toLowerCase(), resolve(sourceRepoRoot, entry.name)); } return lookup; } function inspectRepoActivity({ repoName, repoPath, sinceIso, maxCommits }) { if (!repoPath) { return { repoName, repoPath: null, status: "missing", branch: null, recentCommitCount: 0, latestCommit: null, commits: [], }; } try { const branch = execGit(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"]).trim(); const latestCommit = parseCommitLine(execGit(repoPath, ["log", "-1", "--date=iso-strict", "--pretty=format:%H%x09%cI%x09%an%x09%s"])); const recentCommitCount = Number.parseInt( execGit(repoPath, ["rev-list", "--count", `--since=${sinceIso}`, "HEAD"]).trim(), 10, ); const recentCommitLines = execGit(repoPath, [ "log", `--since=${sinceIso}`, `--max-count=${maxCommits}`, "--date=iso-strict", "--pretty=format:%H%x09%cI%x09%an%x09%s", ]); const commits = recentCommitLines .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) .map((line) => enrichCommit(repoPath, parseCommitLine(line))) .filter(Boolean); return { repoName, repoPath, status: "ok", branch, recentCommitCount: Number.isFinite(recentCommitCount) ? recentCommitCount : commits.length, latestCommit, commits, }; } catch (error) { return { repoName, repoPath, status: "error", branch: null, recentCommitCount: 0, latestCommit: null, commits: [], error: error instanceof Error ? error.message : String(error), }; } } function execGit(repoPath, args) { return execFileSync("git", ["-C", repoPath, ...args], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); } function parseCommitLine(line) { if (!line) { return null; } const [hash, committedAt, author, ...subjectParts] = line.split("\t"); return { hash, committedAt, author, subject: subjectParts.join("\t"), }; } function enrichCommit(repoPath, commit) { if (!commit?.hash) { return commit; } try { const statLines = execGit(repoPath, [ "show", "--shortstat", "--format=", "--name-only", commit.hash, ]) .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); const summaryLine = statLines.find((line) => /files? changed|insertions?|deletions?/.test(line)); const changedPaths = statLines.filter((line) => !/files? changed|insertions?|deletions?/.test(line)).slice(0, 8); return { ...commit, diffstat: parseDiffstat(summaryLine), changedPaths, }; } catch { return { ...commit, diffstat: null, changedPaths: [], }; } } function parseDiffstat(summaryLine) { if (!summaryLine) { return null; } return { filesChanged: readFirstInt(summaryLine, /(\d+)\s+files?\s+changed/i), insertions: readFirstInt(summaryLine, /(\d+)\s+insertions?\(\+\)/i), deletions: readFirstInt(summaryLine, /(\d+)\s+deletions?\(-\)/i), }; } function readFirstInt(input, pattern) { const match = input.match(pattern); return match ? Number.parseInt(match[1], 10) : 0; } function compareRepoActivity(left, right) { const leftRecent = left.recentCommitCount ?? 0; const rightRecent = right.recentCommitCount ?? 0; if (rightRecent !== leftRecent) { return rightRecent - leftRecent; } const leftTimestamp = left.commits?.[0]?.committedAt ?? left.latestCommit?.committedAt ?? ""; const rightTimestamp = right.commits?.[0]?.committedAt ?? right.latestCommit?.committedAt ?? ""; return rightTimestamp.localeCompare(leftTimestamp); } function renderDigest(repos, sinceIso) { const lines = [`Recent tracked repo activity since ${sinceIso}:`]; for (const repo of repos) { if (repo.status === "missing") { lines.push(`- ${repo.repoName}: missing locally under SOURCE_REPO_ROOT`); continue; } if (repo.status === "error") { lines.push(`- ${repo.repoName}: failed to read git activity (${repo.error})`); continue; } const header = repo.recentCommitCount > 0 ? `- ${repo.repoName} [${repo.branch}]: ${repo.recentCommitCount} recent commit${repo.recentCommitCount === 1 ? "" : "s"}` : `- ${repo.repoName} [${repo.branch}]: no recent commits`; lines.push(header); if (repo.commits.length > 0) { for (const commit of repo.commits) { const statBits = []; if (commit.diffstat?.filesChanged) { statBits.push(`${commit.diffstat.filesChanged} files`); } if (commit.diffstat?.insertions) { statBits.push(`+${commit.diffstat.insertions}`); } if (commit.diffstat?.deletions) { statBits.push(`-${commit.diffstat.deletions}`); } lines.push( ` - ${commit.committedAt} ${commit.author}: ${commit.subject}${statBits.length > 0 ? ` (${statBits.join(", ")})` : ""}`, ); if (Array.isArray(commit.changedPaths) && commit.changedPaths.length > 0) { lines.push(` paths: ${commit.changedPaths.join(", ")}`); } } } else if (repo.latestCommit) { lines.push( ` - latest overall ${repo.latestCommit.committedAt} ${repo.latestCommit.author}: ${repo.latestCommit.subject}`, ); } } return lines.join("\n"); } function stripLeadingBom(input) { return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input; } try { main(); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }