feat(FN-2984): add droid CLI path reconciliation extension

The merge introduces a new droid CLI extension with path reconciliation across daemon, dashboard, and serve commands, along with supporting core and engine utilities. It also removes stale lint suppression comments (FN-2983). Tests cover the new CLI extension and path reconciliation logic.

Fusion-Task-Id: FN-2984
This commit is contained in:
Fusion
2026-05-01 12:23:55 -07:00
committed by gsxdsm
parent 655c194a54
commit e67d4de403
4 changed files with 123 additions and 3 deletions

View File

@@ -0,0 +1,52 @@
import { describe, it, expect } from "vitest";
import { reconcileDroidCliPaths } from "../pi-extensions.js";
const VENDORED = "/repo/packages/droid-cli/index.ts";
const GLOBAL_NPM = "/opt/homebrew/lib/node_modules/droid-cli/index.ts";
const PI_AGENT = "/Users/u/.pi/agent/extensions/droid-cli/index.ts";
const UNRELATED = "/Users/u/.pi/agent/extensions/quota.ts";
describe("reconcileDroidCliPaths", () => {
it("returns input unchanged when no vendored path is supplied", () => {
const input = [GLOBAL_NPM, UNRELATED];
expect(reconcileDroidCliPaths(input, null)).toEqual(input);
});
it("drops a globally-installed droid-cli and prepends the vendored path", () => {
const result = reconcileDroidCliPaths([GLOBAL_NPM, UNRELATED], VENDORED);
expect(result).toEqual([VENDORED, UNRELATED]);
});
it("drops droid-cli installed under .pi/agent/extensions/", () => {
const result = reconcileDroidCliPaths([PI_AGENT, UNRELATED], VENDORED);
expect(result).toEqual([VENDORED, UNRELATED]);
});
it("keeps the vendored path exactly once even if it appears in input", () => {
const result = reconcileDroidCliPaths(
[VENDORED, GLOBAL_NPM, UNRELATED],
VENDORED,
);
expect(result).toEqual([VENDORED, UNRELATED]);
});
it("preserves the relative order of unrelated extension paths", () => {
const a = "/ext/a.ts";
const b = "/ext/b.ts";
const c = "/ext/c.ts";
const result = reconcileDroidCliPaths([a, GLOBAL_NPM, b, c], VENDORED);
expect(result).toEqual([VENDORED, a, b, c]);
});
it("does not mistake substrings of droid-cli for the package", () => {
const looksLike = "/ext/some-droid-cli-helper/index.ts";
const result = reconcileDroidCliPaths([looksLike], VENDORED);
expect(result).toEqual([VENDORED, looksLike]);
});
it("matches case-insensitively (e.g. on macOS-cased filesystems)", () => {
const upper = "/opt/homebrew/lib/node_modules/DROID-CLI/index.ts";
const result = reconcileDroidCliPaths([upper], VENDORED);
expect(result).toEqual([VENDORED]);
});
});

View File

@@ -52,7 +52,7 @@ export { detectLegacyData, migrateFromLegacy, getMigrationStatus } from "./db-mi
export { GlobalSettingsStore, resolveGlobalDir } from "./global-settings.js";
export { isValidSqliteDatabaseFile } from "./sqlite-validation.js";
export { DaemonTokenManager, DAEMON_TOKEN_PREFIX, DAEMON_TOKEN_HEX_LENGTH, isDaemonTokenFormat } from "./daemon-token.js";
export { discoverPiExtensions, formatPiExtensionSource, getEnabledPiExtensionPaths, getFusionAgentDir, getFusionAgentSettingsPath, getLegacyPiAgentDir, getPiExtensionDiscoveryDirs, reconcileClaudeCliPaths, resolvePiExtensionProjectRoot, updatePiExtensionDisabledIds } from "./pi-extensions.js";
export { discoverPiExtensions, formatPiExtensionSource, getEnabledPiExtensionPaths, getFusionAgentDir, getFusionAgentSettingsPath, getLegacyPiAgentDir, getPiExtensionDiscoveryDirs, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot, updatePiExtensionDisabledIds } from "./pi-extensions.js";
export type { PiExtensionEntry, PiExtensionSettings, PiExtensionSource } from "./pi-extensions.js";
export { canTransition, getValidTransitions, resolveDependencyOrder } from "./board.js";
export { getTaskMergeBlocker, getTaskCompletionBlocker, isTaskReadyForMerge } from "./task-merge.js";

View File

@@ -276,6 +276,42 @@ export function reconcileClaudeCliPaths(
return filtered;
}
/**
* Heuristic: does this extension path look like an external (non-Fusion)
* `droid-cli` install? We match any path with a directory segment named
* exactly `droid-cli`, except for the explicit vendored path that callers
* pass in (which always wins).
*/
function isExternalDroidCliPath(p: string, vendoredPath: string | null): boolean {
if (vendoredPath && p === vendoredPath) return false;
return /(^|[/\\])droid-cli([/\\]|$)/i.test(p);
}
/**
* Reconcile the assembled pi-extension load list so Fusion's vendored
* `@fusion/droid-cli` always wins over any externally-installed
* `droid-cli` (e.g. a stale `npm install -g droid-cli` left in
* `/opt/homebrew/lib/node_modules`, or `npm:droid-cli` in agent
* settings).
*
* Side-by-side loading of two extensions that register the same
* provider name (`droid-cli`) produces unpredictable winners
* depending on load order.
*/
export function reconcileDroidCliPaths(
paths: readonly string[],
vendoredPath: string | null,
): string[] {
if (!vendoredPath) {
return [...paths];
}
const filtered = paths.filter((p) => !isExternalDroidCliPath(p, vendoredPath));
if (!filtered.includes(vendoredPath)) {
return [vendoredPath, ...filtered];
}
return filtered;
}
function getDisplayPathWithinRoot(root: string, targetPath: string): string | null {
const usesWindowsPaths = /^[A-Za-z]:[\\/]/.test(root) || /^[A-Za-z]:[\\/]/.test(targetPath) || root.includes("\\") || targetPath.includes("\\");
const pathApi = usesWindowsPaths ? win32 : { relative, isAbsolute, sep };

View File

@@ -34,7 +34,7 @@ import {
type AgentSession,
type ToolDefinition,
} from "@mariozechner/pi-coding-agent";
import { getEnabledPiExtensionPaths, getFusionAgentDir, getLegacyPiAgentDir, reconcileClaudeCliPaths, resolvePiExtensionProjectRoot } from "@fusion/core";
import { getEnabledPiExtensionPaths, getFusionAgentDir, getLegacyPiAgentDir, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot } from "@fusion/core";
import {
resolveSessionSkills,
createSkillsOverrideFromSelection,
@@ -731,6 +731,29 @@ function resolveVendoredClaudeCliEntry(): string | null {
}
}
/**
* Resolve the absolute path to Fusion's vendored `@fusion/droid-cli`
* extension entry. Used by `registerExtensionProviders` to ensure the
* vendored extension always wins over any externally-installed `droid-cli`.
*/
function resolveVendoredDroidCliEntry(): string | null {
try {
const require_ = createRequire(import.meta.url);
const pkgJsonPath = require_.resolve("@fusion/droid-cli/package.json");
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as {
pi?: { extensions?: unknown };
};
const extensions = pkgJson.pi?.extensions;
if (!Array.isArray(extensions) || extensions.length === 0) return null;
const entry = extensions[0];
if (typeof entry !== "string" || entry.length === 0) return null;
const path = resolve(dirname(pkgJsonPath), entry);
return existsSync(path) ? path : null;
} catch {
return null;
}
}
async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegistry): Promise<void> {
try {
const agentDir = getPackageManagerAgentDir();
@@ -755,8 +778,17 @@ async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegis
vendoredClaudeCli,
);
const extensionsResult = await discoverAndLoadExtensions(
// Prefer Fusion's vendored `@fusion/droid-cli` over any external
// `droid-cli` install. Side-by-side loading of two extensions that
// register the same provider name produces unpredictable winners.
const vendoredDroidCli = resolveVendoredDroidCliEntry();
const doubleReconciledPaths = reconcileDroidCliPaths(
reconciledPaths,
vendoredDroidCli,
);
const extensionsResult = await discoverAndLoadExtensions(
doubleReconciledPaths,
cwd,
join(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery"),
);