/** * The lifecycle seam for the Pi adapter. * * This module owns only phase orchestration and outcome translation. The * runtime supplied by cbm-native.ts delegates every binary, bridge, and index * operation to the official codebase-memory-mcp installer/CLI. */ export const MIN_SUPPORTED_CBM_VERSION = "0.10.4"; export const LATEST_TESTED_CBM_VERSION = "0.10.4"; export const OFFICIAL_INSTALLER_URL = `https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/v${LATEST_TESTED_CBM_VERSION}/install.sh`; export type BinarySource = "bridge" | "env" | "managed"; export interface BinaryIdentity { path: string; realPath: string; source: BinarySource; } export interface ManagedBinary { path: string; realPath: string; } export type BridgeState = "ready" | "missing" | "incompatible"; export interface BridgeStatus { path: string; state: BridgeState; reason: string; version?: string; toolCount?: number; } export interface InstallSnapshot { installDirectory: string; managed: ManagedBinary | null; active: BinaryIdentity | null; version: string | null; bridge: BridgeStatus; externalOwner: string | null; } export interface NativeResult { code: number; stdout: string; stderr: string; } export interface LifecycleRuntime { inspect(signal?: AbortSignal, managedDirectory?: string): Promise; installFresh(directory: string, signal?: AbortSignal): Promise; updateManaged(directory: string, signal?: AbortSignal): Promise; refreshBridge(binary: string, signal?: AbortSignal): Promise; index(binary: string, cwd: string, signal?: AbortSignal): Promise; projectStatus(binary: string, cwd: string, signal?: AbortSignal): Promise; } export interface InstallRequest { cwd: string; interactive: boolean; yes: boolean; noIndex: boolean; force: boolean; index?: boolean; dryRun?: boolean; directory?: string; signal?: AbortSignal; } export interface InstallIO { confirm(message: string): Promise; progress(message: string): void; } export type InstallPhaseStatus = | "not-run" | "installed" | "updated" | "unchanged" | "external-preserved" | "refreshed" | "indexed" | "degraded" | "skipped" | "error" | "cancelled"; export interface InstallPhase { status: InstallPhaseStatus; reason?: string; } export interface InstallOutcome { status: "ready" | "partial" | "failed" | "cancelled" | "cancelled-by-user" | "consent-required" | "dry-run"; binary: InstallPhase; bridge: InstallPhase; indexing: InstallPhase; snapshot: InstallSnapshot; message: string; } export interface ParsedInstallArgs { yes: boolean; noIndex: boolean; force: boolean; index: boolean; dryRun: boolean; directory?: string; error: string | null; } export type ProjectStatus = "ready" | "degraded" | "error" | "unknown"; export function parseInstallArgs(raw: string): ParsedInstallArgs { const tokens = tokenize(raw); let yes = false; let noIndex = false; let force = false; let index = false; let dryRun = false; let directory: string | undefined; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token === "--yes" || token === "-y") { yes = true; continue; } if (token === "--no-index") { if (index) return { yes, noIndex, force, index, dryRun, directory, error: "--index and --no-index cannot be combined" }; noIndex = true; continue; } if (token === "--index") { if (noIndex) return { yes, noIndex, force, index, dryRun, directory, error: "--index and --no-index cannot be combined" }; index = true; continue; } if (token === "--dry-run") { dryRun = true; continue; } if (token === "--force" || token === "-f") { force = true; continue; } if (token === "--dir") { const value = tokens[++i]; if (!value) return { yes, noIndex, force, index, dryRun, directory, error: "--dir requires a path" }; directory = value; continue; } if (token.startsWith("--dir=")) { const value = token.slice("--dir=".length); if (!value) return { yes, noIndex, force, index, dryRun, directory, error: "--dir requires a path" }; directory = value; continue; } return { yes, noIndex, force, index, dryRun, directory, error: `unknown option: ${token}` }; } return { yes, noIndex, force, index, dryRun, directory, error: null }; } function tokenize(raw: string): string[] { const tokens: string[] = []; const pattern = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^']*)'|(\S+)/g; let match: RegExpExecArray | null; while ((match = pattern.exec(raw)) !== null) { tokens.push(match[1] ?? match[2] ?? match[3]); } return tokens; } export function assessVersion(version: string | null): "missing" | "supported" | "forward-compatible" | "unsupported" | "invalid" { if (!version) return "missing"; const parsed = semver(version); if (!parsed) return "invalid"; const minimum = semver(MIN_SUPPORTED_CBM_VERSION); const latest = semver(LATEST_TESTED_CBM_VERSION); if (!minimum || !latest) return "invalid"; if (compareSemver(parsed, minimum) < 0) return "unsupported"; return compareSemver(parsed, latest) > 0 ? "forward-compatible" : "supported"; } export function isUsableRelease(version: string | null): boolean { const state = assessVersion(version); return state === "supported" || state === "forward-compatible"; } function semver(value: string): [number, number, number] | null { const match = /^v?(\d+)\.(\d+)\.(\d+)(?:\+.*)?$/.exec(value.trim()); return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null; } function compareSemver(a: [number, number, number], b: [number, number, number]): number { for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1; } return 0; } function canReuseManagedBinary(snapshot: InstallSnapshot): boolean { return Boolean( snapshot.managed && snapshot.active && !snapshot.externalOwner && snapshot.active.realPath === snapshot.managed.realPath && isUsableRelease(snapshot.version), ); } function shouldIndex(request: InstallRequest): boolean { return request.index === true && !request.noIndex; } export async function executeInstall( request: InstallRequest, runtime: LifecycleRuntime, io: InstallIO, ): Promise { let snapshot: InstallSnapshot; try { snapshot = await runtime.inspect(request.signal, request.directory); } catch (error) { if (isAbortError(error)) return cancelledOutcome(emptySnapshot(), "cancelled while inspecting the official CBM state"); return failedOutcome(emptySnapshot(), `could not inspect the official CBM state: ${describeError(error)}`); } // The runtime resolves `~` and platform defaults while inspecting the // requested directory; use that canonical value for the installer too. const directory = snapshot.installDirectory; const plan = installPlan(snapshot, request, directory); io.progress(plan); if (request.signal?.aborted) { return cancelledOutcome(snapshot, "cancelled before consent"); } if (request.dryRun) { const reason = "--dry-run: no installer, bridge, or indexing command was executed"; return { status: "dry-run", binary: { status: "not-run", reason }, bridge: { status: "not-run", reason }, indexing: { status: "not-run", reason }, snapshot, message: `dry run:\n${plan}`, }; } if (!request.yes) { if (!request.interactive) { return { status: "consent-required", binary: { status: "not-run" }, bridge: { status: "not-run" }, indexing: { status: "not-run" }, snapshot, message: "explicit consent is required; rerun with --yes in a non-interactive context", }; } if (!(await io.confirm(plan))) { return { status: "cancelled-by-user", binary: { status: "cancelled", reason: "user declined" }, bridge: { status: "not-run" }, indexing: { status: "not-run" }, snapshot, message: "cancelled", }; } } if (request.signal?.aborted) { return cancelledOutcome(snapshot, "cancelled before lifecycle work"); } let binary: InstallPhase; let verifiedSnapshot = snapshot; if (verifiedSnapshot.active && verifiedSnapshot.externalOwner) { binary = { status: "external-preserved", reason: `${verifiedSnapshot.externalOwner} owns ${verifiedSnapshot.active.path}` }; } else if (!request.force && canReuseManagedBinary(verifiedSnapshot)) { binary = { status: "unchanged", reason: `managed binary ${verifiedSnapshot.managed!.path} is already usable; use --force to run the official installer`, }; } else { const result = verifiedSnapshot.managed ? await runPhase(() => runtime.updateManaged(directory, request.signal), "updated") : await runPhase(() => runtime.installFresh(directory, request.signal), "installed"); binary = result.phase; if (result.aborted) return cancelledOutcome(verifiedSnapshot, "cancelled during binary lifecycle"); } try { verifiedSnapshot = await runtime.inspect(request.signal, directory); } catch (error) { if (isAbortError(error)) return cancelledOutcome(verifiedSnapshot, "cancelled while verifying the official CBM binary", binary); return failedOutcome(verifiedSnapshot, `could not verify the official CBM binary: ${describeError(error)}`, binary); } if (binary.status === "installed" && verifiedSnapshot.externalOwner && !verifiedSnapshot.managed) { binary = { status: "external-preserved", reason: `${verifiedSnapshot.externalOwner} owns ${verifiedSnapshot.active?.path ?? "the active binary"}` }; } const versionState = assessVersion(verifiedSnapshot.version); if (!verifiedSnapshot.active) { return failedOutcome(verifiedSnapshot, "the official CBM binary is not available after lifecycle work", binary); } if (versionState === "unsupported" || versionState === "invalid" || versionState === "missing") { return failedOutcome( verifiedSnapshot, `CBM release ${verifiedSnapshot.version ?? "unknown"} is ${versionState}; minimum supported release is ${MIN_SUPPORTED_CBM_VERSION}`, binary, ); } const forwardNotice = versionState === "forward-compatible" ? `; release ${verifiedSnapshot.version} is newer than the latest tested ${LATEST_TESTED_CBM_VERSION}` : ""; io.progress("Generating the native CBM Pi Bridge (wrapping official CLI tools)…"); const bridgeResult = await runPhase(() => runtime.refreshBridge(verifiedSnapshot.active!.path, request.signal), "refreshed"); if (bridgeResult.aborted) return cancelledOutcome(verifiedSnapshot, "cancelled during bridge refresh"); let bridge: InstallPhase = bridgeResult.phase.status === "error" ? bridgeResult.phase : { status: "refreshed" }; try { verifiedSnapshot = await runtime.inspect(request.signal, directory); } catch (error) { if (isAbortError(error)) return cancelledOutcome(verifiedSnapshot, "cancelled while verifying the native CBM bridge", binary, bridge); bridge = { status: "error", reason: describeError(error) }; } if (bridge.status !== "error" && verifiedSnapshot.bridge.state !== "ready") { bridge = { status: "error", reason: verifiedSnapshot.bridge.reason || "native CBM bridge is not compatible" }; } if (bridge.status === "error") { return { status: "failed", binary, bridge, indexing: { status: request.noIndex ? "skipped" : "not-run", reason: request.noIndex ? "--no-index" : "bridge verification failed" }, snapshot: verifiedSnapshot, message: `binary ${binary.status}; bridge failed: ${bridge.reason ?? "unknown error"}`, }; } if (!shouldIndex(request)) { const indexingReason = request.noIndex ? "--no-index" : "--index not requested"; return { status: binary.status === "error" ? "partial" : "ready", binary, bridge, indexing: { status: "skipped", reason: indexingReason }, snapshot: verifiedSnapshot, message: binary.status === "error" ? `managed lifecycle failed, but the existing binary and Pi bridge are ready: ${binary.reason ?? "unknown error"}` : `official binary and Pi bridge are ready${forwardNotice}; indexing skipped (${indexingReason})`, }; } io.progress("Indexing the current project through the native indexer…"); let nativeIndex: NativeResult; try { nativeIndex = await runtime.index(verifiedSnapshot.active!.path, request.cwd, request.signal); } catch (error) { if (isAbortError(error)) return cancelledOutcome(verifiedSnapshot, "cancelled during indexing", binary, bridge); nativeIndex = { code: 1, stdout: "", stderr: describeError(error) }; } const indexing = parseIndexPhase(nativeIndex); const status = binary.status === "error" || indexing.status === "degraded" || indexing.status === "skipped" ? "partial" : indexing.status === "indexed" ? "ready" : "failed"; return { status, binary, bridge, indexing, snapshot: verifiedSnapshot, message: status === "ready" ? `official binary, Pi bridge, and project index are ready${forwardNotice}` : `official binary ${binary.status}; Pi bridge is ready; indexing ${indexing.status}${indexing.reason ? `: ${indexing.reason}` : ""}`, }; } function installPlan(snapshot: InstallSnapshot, request: InstallRequest, directory: string): string { const binaryAction = snapshot.active && snapshot.externalOwner ? `preserve externally managed binary ${snapshot.active.path}` : !request.force && canReuseManagedBinary(snapshot) ? `reuse managed binary ${snapshot.managed!.path} (use --force to run the official installer)` : snapshot.managed ? `run the adjacent official installer for ${snapshot.managed.path}` : `download the documented official installer and install to ${directory}`; return [ request.force && !(snapshot.active && snapshot.externalOwner) ? `${binaryAction} (force requested; native installer owns update semantics)` : binaryAction, `official installer source: ${OFFICIAL_INSTALLER_URL}`, "the official installer performs its own release download, checksum verification, and activation; this adapter does not recreate those decisions", "generate the native CBM Pi Bridge that wraps official CLI tools", shouldIndex(request) ? `index the current project ${request.cwd} (--index)` : request.noIndex ? "skip project indexing (--no-index)" : "skip project indexing (use --index to opt in)", "no Pi mcp.json file will be written", ].join("\n"); } async function runPhase( run: () => Promise, successStatus: "installed" | "updated" | "refreshed", ): Promise<{ phase: InstallPhase; aborted: boolean }> { try { const result = await run(); if (result.code !== 0) { return { phase: { status: "error", reason: result.stderr.trim() || `native command exited with ${result.code}` }, aborted: false }; } return { phase: { status: successStatus }, aborted: false }; } catch (error) { if (isAbortError(error)) return { phase: { status: "cancelled", reason: "aborted" }, aborted: true }; return { phase: { status: "error", reason: describeError(error) }, aborted: false }; } } export function parseIndexPhase(result: NativeResult): InstallPhase { if (result.code !== 0) return { status: "error", reason: result.stderr.trim() || `native command exited with ${result.code}` }; const payload = parsePayload(result.stdout); const status = payload && typeof payload === "object" && "status" in payload ? payload.status : undefined; if (status === "indexed") return { status: "indexed" }; if (status === "degraded") return { status: "degraded", reason: describePayload(payload) }; if (status === "skipped") return { status: "skipped", reason: describePayload(payload) }; if (status === "error") return { status: "error", reason: describePayload(payload) }; return { status: "error", reason: "native indexer returned exit 0 without a recognized status" }; } function parsePayload(raw: string): unknown { const candidates = raw .trim() .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) .reverse(); for (const candidate of candidates) { try { const value: unknown = JSON.parse(candidate); if (isRecord(value) && Array.isArray(value.content)) { const structured = value.structuredContent; if (structured !== undefined) return structured; const text = value.content.find((item) => isRecord(item) && typeof item.text === "string"); if (isRecord(text) && typeof text.text === "string") { try { return JSON.parse(text.text); } catch { return text.text; } } } return value; } catch { // Native diagnostics may share stdout; keep scanning for the JSON envelope. } } return null; } function describePayload(payload: unknown): string { if (isRecord(payload)) { if (typeof payload.message === "string") return payload.message; if (typeof payload.error === "string") return payload.error; } return "native indexer reported an incomplete outcome"; } export function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } export function isAbortError(error: unknown): boolean { return error instanceof Error && (error.name === "AbortError" || error.message.toLowerCase().includes("aborted")); } export function describeError(error: unknown): string { return error instanceof Error ? error.message : String(error); } function emptySnapshot(): InstallSnapshot { return { installDirectory: "", managed: null, active: null, version: null, bridge: { path: "", state: "missing", reason: "state unavailable" }, externalOwner: null, }; } function failedOutcome(snapshot: InstallSnapshot, message: string, binary: InstallPhase = { status: "error", reason: message }): InstallOutcome { return { status: "failed", binary, bridge: { status: "not-run" }, indexing: { status: "not-run" }, snapshot, message, }; } function cancelledOutcome( snapshot: InstallSnapshot, message: string, binary: InstallPhase = { status: "cancelled", reason: message }, bridge: InstallPhase = { status: "not-run" }, ): InstallOutcome { return { status: "cancelled", binary, bridge, indexing: { status: "cancelled", reason: message }, snapshot, message, }; } export function formatStatusReport(snapshot: InstallSnapshot, project: { cwd: string; projectStatus: ProjectStatus; reason?: string }): string { const active = snapshot.active?.path ?? "not found"; const managed = snapshot.managed?.path ?? `not found in ${snapshot.installDirectory}`; const drift = Boolean(snapshot.active && (!snapshot.managed || snapshot.active.realPath !== snapshot.managed.realPath)); const versionState = assessVersion(snapshot.version); const lines = [ `Active Official CBM Binary: ${active}`, `Managed Official CBM Binary: ${managed}`, `Active/managed drift: ${drift ? "yes" : "no"}`, `Version: ${snapshot.version ?? "unknown"} (${versionState})`, `Native CBM Bridge: ${snapshot.bridge.state}${snapshot.bridge.reason ? `: ${snapshot.bridge.reason}` : ""}`, `Bridge generator: ${snapshot.bridge.version ? `v${snapshot.bridge.version}` : "unknown"}`, `Bridge tools: ${snapshot.bridge.toolCount ?? "unknown"}`, `Registration state: ${snapshot.bridge.state === "ready" ? "native CBM bridge present" : "not ready"}`, `Project index: ${project.projectStatus}${project.reason ? `: ${project.reason}` : ""}`, ]; if (snapshot.externalOwner) lines.push(`External owner: ${snapshot.externalOwner}`); lines.push(`Project path: ${project.cwd}`); return lines.join("\n"); } export function projectStatusFromResult(result: NativeResult): { status: ProjectStatus; reason?: string } { if (result.code !== 0) return { status: "error", reason: result.stderr.trim() || `native command exited with ${result.code}` }; const payload = parsePayload(result.stdout); if (!isRecord(payload)) return { status: "unknown", reason: "native project resolver returned no structured result" }; if (payload.status === "ready" || payload.status === "indexed") return { status: "ready" }; if (payload.status === "degraded") return { status: "degraded", reason: describePayload(payload) }; if (payload.status === "error") return { status: "error", reason: describePayload(payload) }; return { status: "unknown", reason: "native project resolver returned an unrecognized status" }; }