From 14a756fe486c75e2c77ff162dd77edca9843aab7 Mon Sep 17 00:00:00 2001 From: gustav-fff <286169375+gustav-fff@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:10:52 -0700 Subject: [PATCH 1/2] fix(pi-fff): isomorphic runtime detection with lazy SDK load Detect bun vs node at runtime and dynamically import the matching SDK (@ff-labs/fff-bun on bun, @ff-labs/fff-node on node) via a variable package name so oh-my-pi's static extension validator does not chase ffi-rs' optional native binaries through the module graph. Closes #668 --- bun.lock | 4 ++ packages/pi-fff/package.json | 6 +++ packages/pi-fff/src/index.ts | 57 ++++++++++++++++++++++++-- packages/pi-fff/test/extension.test.ts | 7 +++- 4 files changed, 68 insertions(+), 6 deletions(-) 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..dd48ff925 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -12,16 +12,64 @@ 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; + const bunPkg = "@ff-labs/fff-bun"; + const nodePkg = "@ff-labs/fff-node"; + const preferred = detectRuntime() === "bun" ? bunPkg : nodePkg; + const fallback = preferred === bunPkg ? nodePkg : bunPkg; + + sdkPromise = (async () => { + try { + return (await import(preferred)) as { FileFinder: FileFinderStatic }; + } catch (e) { + try { + return (await import(fallback)) as { FileFinder: FileFinderStatic }; + } catch { + throw new Error( + `pi-fff: neither ${preferred} nor ${fallback} could be loaded (${ + e instanceof Error ? e.message : String(e) + })`, + ); + } + } + })(); + return sdkPromise; +} + // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- @@ -277,13 +325,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 +379,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 +391,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 { From be23eb57fc4d7057037f164b1cbaeb60ec8967b6 Mon Sep 17 00:00:00 2001 From: gustav-fff <286169375+gustav-fff@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:46:01 -0700 Subject: [PATCH 2/2] fix(pi-fff): fail loud on wrong-runtime SDK Drop the cross-runtime SDK fallback in loadSdk. Falling back to the node SDK on a bun host re-introduces the ffi-rs cost this PR is meant to avoid, and hides packaging bugs where the correct SDK is missing. --- packages/pi-fff/src/index.ts | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index dd48ff925..6d8ea80ca 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -47,26 +47,11 @@ function detectRuntime(): "bun" | "node" { function loadSdk(): Promise<{ FileFinder: FileFinderStatic }> { if (sdkPromise) return sdkPromise; - const bunPkg = "@ff-labs/fff-bun"; - const nodePkg = "@ff-labs/fff-node"; - const preferred = detectRuntime() === "bun" ? bunPkg : nodePkg; - const fallback = preferred === bunPkg ? nodePkg : bunPkg; - - sdkPromise = (async () => { - try { - return (await import(preferred)) as { FileFinder: FileFinderStatic }; - } catch (e) { - try { - return (await import(fallback)) as { FileFinder: FileFinderStatic }; - } catch { - throw new Error( - `pi-fff: neither ${preferred} nor ${fallback} could be loaded (${ - e instanceof Error ? e.message : String(e) - })`, - ); - } - } - })(); + // 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; }