Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/memory/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
// - The createRequire banner lets esbuild's __require resolve real requires for bundled CommonJS
// deps (e.g. `yaml`) under ESM output — without it, their internal require() throws
// "Dynamic require not supported" at startup.
import { writeFileSync } from "node:fs";
import { build } from "esbuild";
import { srcHash } from "../../scripts/lib/src-hash.mjs";

await build({
entryPoints: ["src/index.ts"],
Expand All @@ -19,4 +21,7 @@ await build({
outfile: "dist/index.js",
});

// Stamp the source content-hash next to the bundle so dist-lockstep is verifiable without mtimes.
writeFileSync("dist/.srchash", srcHash("src"));

console.log("built dist/index.js");
1 change: 1 addition & 0 deletions packages/memory/dist/.srchash
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
b55bf962a8923ce9ec3384e6a2892ae7036058c017647979c2aa991f7044afbd
13 changes: 8 additions & 5 deletions scripts/check-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { srcHash } from "./lib/src-hash.mjs";

const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");

Expand Down Expand Up @@ -106,11 +107,13 @@ if (existsSync(memSrc)) {
if (!existsSync(memDist)) {
warn("packages/memory/dist/index.js missing — build + commit before the MCP can run (dist-lockstep)");
} else {
const newestSrc = readdirSync(memSrc, { recursive: true })
.map((f) => join(memSrc, String(f)))
.filter((p) => existsSync(p) && statSync(p).isFile())
.reduce((mx, p) => Math.max(mx, statSync(p).mtimeMs), 0);
if (newestSrc > statSync(memDist).mtimeMs) warn("packages/memory/dist is older than src — rebuild + commit (dist-lockstep)");
// Content-hash compare, not mtime: git doesn't preserve mtimes, so a checkout/squash-merge would
// otherwise trip a false "dist is stale". build.mjs stamps dist/.srchash; recompute + compare.
const stampPath = join(ROOT, "packages", "memory", "dist", ".srchash");
const stamped = existsSync(stampPath) ? readFileSync(stampPath, "utf8").trim() : "";
const current = srcHash(memSrc);
if (!stamped) warn("packages/memory/dist/.srchash missing — rebuild so dist-lockstep is verifiable");
else if (stamped !== current) warn("packages/memory/dist is stale (src changed since last build) — rebuild + commit (dist-lockstep)");
else ok("dist-lockstep", "up to date");
}
}
Expand Down
26 changes: 26 additions & 0 deletions scripts/lib/src-hash.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Deterministic content hash of a source tree — the dist-lockstep signal.
//
// mtimes are not preserved by git (a clone / checkout / squash-merge rewrites them), so comparing
// dist-vs-src mtimes yields false "dist is stale" warnings. Hashing file *contents* is checkout-proof:
// build.mjs stamps this hash into the committed dist, and check-plugin recomputes + compares it.
import { createHash } from "node:crypto";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative, sep } from "node:path";

// SHA-256 over every file's POSIX-normalized relative path + its bytes, in sorted path order.
// Path order makes it stable across filesystems; including the path catches renames/moves.
export function srcHash(srcDir) {
const files = readdirSync(srcDir, { recursive: true })
.map((f) => join(srcDir, String(f)))
.filter((p) => statSync(p).isFile())
.map((p) => relative(srcDir, p).split(sep).join("/"))
.sort();
const h = createHash("sha256");
for (const rel of files) {
h.update(rel);
h.update("\0");
h.update(readFileSync(join(srcDir, rel)));
h.update("\0");
}
return h.digest("hex");
}