diff --git a/server.ts b/server.ts index 6ffe289..06cc0c9 100644 --- a/server.ts +++ b/server.ts @@ -29,7 +29,7 @@ // dependency to rebuild against bb's embedded runtime on every app update). // Writes go through `bb memory` — the public surface — so content guards, // version history and optimistic concurrency apply exactly as they do to agents. -import { execFile } from "node:child_process"; +import { execFile, execFileSync } from "node:child_process"; import os from "node:os"; import { promisify } from "node:util"; import { defineRpcContract, type BbPluginApi } from "@bb/plugin-sdk"; @@ -351,7 +351,72 @@ async function bbMemory(args: string[], projectId: string | null) { } } +/** + * 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) { + // This plugin is a UI surface — until now it registered only `bb.rpc` and had no CLI at + // all, so `bb memory-ui` was genuinely "unknown command". That also made it the one + // plugin whose RUNNING revision could not be asked for from a shell, and so the one the + // loaded-revision check in dotfiles could never cover. + // + // Registering a single introspection command rather than a whole surface: `build` is not + // a feature, it is the answer to "is what I merged what is running". + bb.cli.register({ + name: "memory-ui", + summary: "The memory browser UI (no interactive commands — this surface is introspection only)", + commands: [ + { + name: "build", + summary: "Which commit this RUNNING process was loaded from (not the checkout)", + usage: "bb memory-ui build [--json]", + }, + ], + async run(argv) { + if (argv[0] === "build" || argv.length === 0) { + 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}`, + }; + } + return { exitCode: 1, stderr: `unknown subcommand: ${argv[0]} — only 'build' exists` }; + }, + }); + const titleCache = new Map(); const viewClause = (view: string) =>