diff --git a/bun.lock b/bun.lock index 568f1260d..861a623b2 100644 --- a/bun.lock +++ b/bun.lock @@ -63,8 +63,12 @@ "peerDependencies": { "@earendil-works/pi-coding-agent": "*", "@earendil-works/pi-tui": "*", + "@ff-labs/fff-bun": "*", "@sinclair/typebox": "*", }, + "optionalPeers": [ + "@ff-labs/fff-bun", + ], }, }, "packages": { diff --git a/packages/pi-fff/package.json b/packages/pi-fff/package.json index 04a250cb7..bf40c6928 100644 --- a/packages/pi-fff/package.json +++ b/packages/pi-fff/package.json @@ -45,8 +45,14 @@ "peerDependencies": { "@earendil-works/pi-coding-agent": "*", "@earendil-works/pi-tui": "*", + "@ff-labs/fff-bun": "*", "@sinclair/typebox": "*" }, + "peerDependenciesMeta": { + "@ff-labs/fff-bun": { + "optional": true + } + }, "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.0.0" diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index 86eff2329..6d8ea80ca 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -12,16 +12,49 @@ import { Text, } from "@earendil-works/pi-tui"; import type { + FileFinderApi, GrepCursor, GrepMode, GrepResult, + InitOptions, MixedItem, + Result, SearchResult, } from "@ff-labs/fff-node"; -import { FileFinder } from "@ff-labs/fff-node"; import { Type } from "@sinclair/typebox"; import { buildQuery } from "./query"; +// Isomorphic SDK loading. pi hosts run under either bun (omp / oh-my-pi) or +// node; the two SDKs implement the same FileFinderApi. Static imports of +// @ff-labs/fff-node pull ffi-rs + optional native binaries into the module +// graph, which trips oh-my-pi's static extension validator (issue #668). We +// defeat static resolution with a variable-name dynamic import. +type FileFinderStatic = { + create(options: InitOptions): Result; +}; + +let sdkPromise: Promise<{ FileFinder: FileFinderStatic }> | null = null; + +function detectRuntime(): "bun" | "node" { + if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") return "bun"; + if ( + typeof process !== "undefined" && + (process as { versions?: { bun?: string } }).versions?.bun + ) + return "bun"; + return "node"; +} + +function loadSdk(): Promise<{ FileFinder: FileFinderStatic }> { + if (sdkPromise) return sdkPromise; + // Fail loud on wrong-runtime SDK rather than falling back — a fallback would + // re-introduce the ffi-rs cost on bun (the whole point of the bun SDK is to + // avoid it) and mask packaging bugs where the correct SDK wasn't installed. + const pkg = detectRuntime() === "bun" ? "@ff-labs/fff-bun" : "@ff-labs/fff-node"; + sdkPromise = import(pkg) as Promise<{ FileFinder: FileFinderStatic }>; + return sdkPromise; +} + // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- @@ -277,13 +310,13 @@ function createFffMentionProvider( // --------------------------------------------------------------------------- export default function fffExtension(pi: ExtensionAPI) { - let finder: FileFinder | null = null; + let finder: FileFinderApi | null = null; let finderCwd: string | null = null; // Concurrent ensureFinder() callers share the same in-flight promise so // FileFinder.create() (which takes native DB locks) runs at most once per // base path at a time — otherwise parallel tool calls would race and // deadlock at the native layer (issue #403). - let finderPromise: Promise | null = null; + let finderPromise: Promise | null = null; let activeCwd = process.cwd(); // Mode resolution: flag > env > default @@ -331,7 +364,7 @@ export default function fffExtension(pi: ExtensionAPI) { return currentMode !== "tools-only"; } - function ensureFinder(cwd: string): Promise { + function ensureFinder(cwd: string): Promise { if (finder && !finder.isDestroyed && finderCwd === cwd) return Promise.resolve(finder); if (finderPromise) return finderPromise; @@ -343,6 +376,7 @@ export default function fffExtension(pi: ExtensionAPI) { finderCwd = null; } + const { FileFinder } = await loadSdk(); const result = FileFinder.create({ basePath: cwd, frecencyDbPath, diff --git a/packages/pi-fff/test/extension.test.ts b/packages/pi-fff/test/extension.test.ts index 3695a84e0..c0d351c6e 100644 --- a/packages/pi-fff/test/extension.test.ts +++ b/packages/pi-fff/test/extension.test.ts @@ -34,7 +34,7 @@ function createMockFinder(): MockFinder { }; } -mock.module("@ff-labs/fff-node", () => ({ +const finderModule = { FileFinder: { create: mock((options: unknown) => { createCalls.push(options); @@ -43,7 +43,10 @@ mock.module("@ff-labs/fff-node", () => ({ return { ok: true, value: finder }; }), }, -})); +}; + +mock.module("@ff-labs/fff-node", () => finderModule); +mock.module("@ff-labs/fff-bun", () => finderModule); mock.module("@earendil-works/pi-tui", () => ({ Text: class Text {