diff --git a/server.ts b/server.ts index 6bc83e5..2de596a 100644 --- a/server.ts +++ b/server.ts @@ -6,6 +6,7 @@ // stores no per-user UI preferences, so this plugin supplies the missing // shared place: plugin KV holds the canonical snapshot, and every connected // frontend pulls it, pushes its own edits, and gets told when it changes. +import { execFileSync } from "node:child_process"; import { defineRpcContract, type BbPluginApi } from "@bb/plugin-sdk"; import { z } from "zod"; import { parseSyncedKeys, type Snapshot, type StoredValue } from "./lib/sync.ts"; @@ -44,6 +45,40 @@ export const rpcContract = defineRpcContract({ }, }); +/** + * Which commit is this PROCESS running? (MX-139/MX-141) + * + * bb bundles a `path:` plugin FROM SOURCE at reload, so a revision read here — at module + * load, the same moment — is by construction the code now executing. Nothing else can say: + * `bb plugin list` prints `running` and the source path but no revision, `bb plugin source` + * has none to record for a path: source, and dist/ is NOT the loaded artifact (its mtime was + * measured lying by 15 minutes). So a checkout can sit clean on main, every drift check + * green, while the process runs something older. + * + * Synchronous on purpose: the value must be fixed before anything can observe it, and it is + * one git call per load. Failure yields rev: null rather than a guess — a tarball install has + * no git dir, and that must stay distinguishable from a real mismatch so a checker reports + * UNKNOWN rather than OK. `dirty` rides along because a bundle built from an edited tree + * matches NO commit, and comparing revisions alone would call that a match. + */ +const BUILD_STAMP: { rev: string | null; dirty: boolean | null; sourceDir: string; loadedAt: string; why: string | null } = (() => { + const here = import.meta.dirname; + const loadedAt = new Date().toISOString(); + try { + const git = (args: string[]): string => + execFileSync("git", ["-C", here, ...args], { encoding: "utf8", timeout: 5000 }).trim(); + return { + rev: git(["rev-parse", "HEAD"]), + dirty: git(["status", "--porcelain"]).length > 0, + sourceDir: git(["rev-parse", "--show-toplevel"]), + loadedAt, + why: null, + }; + } catch (e) { + return { rev: null, dirty: null, sourceDir: here, loadedAt, why: e instanceof Error ? e.message : String(e) }; + } +})(); + export default async function plugin(bb: BbPluginApi) { const settings = bb.settings.define({ syncedKeys: { @@ -108,8 +143,26 @@ export default async function plugin(bb: BbPluginApi) { commands: [ { name: "status", summary: "Show the shared snapshot", usage: "bb sidebar-sync status" }, { name: "reset", summary: "Forget it and let the next UI reseed", usage: "bb sidebar-sync reset" }, + { + name: "build", + summary: "Which commit this RUNNING process was loaded from (not the checkout)", + usage: "bb sidebar-sync build [--json]", + }, ], async run(argv) { + // Answered FIRST, ahead of `reset` (which DELETES the shared snapshot) and ahead of + // the default path that loads it: "what is running" must stay answerable when the + // thing running is broken, and must never have a side effect of its own. + if (argv[0] === "build") { + if (argv.includes("--json")) return { exitCode: 0, stdout: JSON.stringify(BUILD_STAMP) }; + const dirty = BUILD_STAMP.dirty === null ? "" : BUILD_STAMP.dirty ? " +dirty" : ""; + const why = BUILD_STAMP.why ? ` (${BUILD_STAMP.why})` : ""; + return { + exitCode: 0, + stdout: `loaded ${BUILD_STAMP.rev ?? "unknown"}${dirty} from ${BUILD_STAMP.sourceDir} at ${BUILD_STAMP.loadedAt}${why}`, + }; + } + if (argv[0] === "reset") { await bb.storage.kv.delete(SNAPSHOT_KEY); return { exitCode: 0, stdout: "snapshot cleared" };