aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
91 lines • 3 kB
JavaScript
/**
* Checksum Manifest — Fast Change Detection
*
* Stores `{ path, checksum, mtime, size }` per indexed file to enable
* two-phase change detection:
* Phase 1: stat() comparison (no file read) — filter out unchanged files
* Phase 2: content read + checksum — only for candidates where stat differs
*
* This avoids reading every file on `aiwg index build` when most haven't
* changed, reducing rebuild time from O(N reads) to O(N stats + M reads)
* where M << N on typical projects.
*
* @implements #794
* @source @src/artifacts/index-builder.ts
*/
import fs from 'fs';
import path from 'path';
const MANIFEST_FILENAME = 'checksum-manifest.json';
/**
* Build a manifest entry from a file's current stat + checksum
*/
export function makeEntry(checksum, stat) {
return {
checksum,
mtime: stat.mtime.toISOString(),
size: stat.size,
};
}
/**
* Load the checksum manifest from an index directory.
* Returns an empty manifest if the file is missing or malformed.
*/
export function loadManifest(indexDir) {
const manifestPath = path.join(indexDir, MANIFEST_FILENAME);
if (!fs.existsSync(manifestPath)) {
return { version: 1, generated: '', entries: {} };
}
try {
const raw = fs.readFileSync(manifestPath, 'utf-8');
const parsed = JSON.parse(raw);
if (parsed.version !== 1 || !parsed.entries) {
return { version: 1, generated: '', entries: {} };
}
return parsed;
}
catch {
return { version: 1, generated: '', entries: {} };
}
}
/**
* Write the checksum manifest atomically (write to temp, rename).
*/
export function writeManifest(indexDir, manifest) {
fs.mkdirSync(indexDir, { recursive: true });
const manifestPath = path.join(indexDir, MANIFEST_FILENAME);
const tmpPath = `${manifestPath}.tmp`;
const payload = {
version: 1,
generated: new Date().toISOString(),
entries: manifest.entries,
};
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), 'utf-8');
fs.renameSync(tmpPath, manifestPath);
}
/**
* Phase 1 check: can we skip this file based on stat alone?
*
* Returns true if the file's current mtime and size match the manifest entry —
* in which case we presume the content is unchanged and skip the read.
*/
export function statMatches(stat, entry) {
if (!entry)
return false;
return stat.mtime.toISOString() === entry.mtime && stat.size === entry.size;
}
/**
* Remove stale entries (files that no longer exist on disk) from the manifest.
* Returns the count removed.
*/
export function pruneManifest(manifest, cwd) {
let removed = 0;
for (const relativePath of Object.keys(manifest.entries)) {
const fullPath = path.join(cwd, relativePath);
if (!fs.existsSync(fullPath)) {
delete manifest.entries[relativePath];
removed++;
}
}
return removed;
}
//# sourceMappingURL=checksum-manifest.js.map