Files
fusion/packages/cli/src/plugins/bundled-plugin-install.ts
gsxdsm ea1e4fe062 fix: load live workspace plugin over stale staged bundle in dev
Closes the durability gap behind "grok chat returns empty replies". In a source
checkout the dashboard resolved the staged CLI tsup bundle
(packages/cli/dist/plugins/<id>/bundled.js), which resolvePluginEntryPath prefers
verbatim with no freshness check. The FN-7779 dev prebuild rebuilds each plugin's
own plugins/<id>/dist but never the staged tsup bundle, so a source-only plugin
fix ran stale until a manual `pnpm build`.

getCandidatePluginDirs now probes the live workspace source dir (<repo>/plugins/<id>)
before the staged bundle, so dev loads the freshness-checked live plugin (dist-vs-src),
self-healing even when the prebuild is skipped. The global-staged dir stays first,
so published installs (no workspace dir) are unaffected — asserted by the retained
global-install regression test plus a new source-checkout preference test.

Verified on a live dashboard: grok chat streams text and the loader now writes its
reload copies under plugins/fusion-plugin-grok-runtime/dist, not the staged bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 21:19:22 -07:00

100 lines
4.6 KiB
TypeScript

/**
* FNXC:PluginLoader 2026-07-07-00:00:
* Bundled-plugin auto-install logic was ported to @fusion/core
* (packages/core/src/plugins/bundled-plugin-install.ts) so the desktop embedded
* runtime can auto-install bundled runtime plugins without depending on this CLI
* package (FN-7637). This module is now a thin, behavior-preserving CLI adapter:
* it supplies the CLI-specific candidate bundle-directory resolution
* (`<cli>/dist/plugins/<id>` staged layout, resolved from `import.meta.url`) to
* the shared helper and re-exports the same public surface `dashboard.ts`,
* `serve.ts`, and `daemon.ts` already depend on. `resolvePluginEntryPath` is
* re-exported directly from `@fusion/core` (no local duplicate) since the
* shared helper already delegates to it.
*/
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
ensureBundledCursorRuntimePluginInstalled as coreEnsureBundledCursorRuntimePluginInstalled,
ensureBundledGrokRuntimePluginInstalled as coreEnsureBundledGrokRuntimePluginInstalled,
ensureBundledDependencyGraphPluginInstalled as coreEnsureBundledDependencyGraphPluginInstalled,
ensureBundledPluginInstalled as coreEnsureBundledPluginInstalled,
type EnsureBundledResult,
type PluginLoader,
type PluginStore,
} from "@fusion/core";
export { BUNDLED_PLUGIN_IDS, isBundledPluginId, resolvePluginEntryPath } from "@fusion/core";
export type { BundledPluginId, EnsureBundledResult } from "@fusion/core";
/*
FNXC:PluginLoader 2026-07-10-00:00:
Candidate order encodes a freshness contract, not just a search path.
Published/global install (the regression the first candidate protects): plugins
are staged next to the running bin at `<cli>/dist/plugins/<id>`, and no workspace
`plugins/` dir exists — so `join(moduleDir, "plugins", <id>)` MUST stay first and
win.
Source checkout / `pnpm dev` (the durability fix): the running dashboard would
otherwise resolve the STAGED tsup bundle at `<cli>/dist/plugins/<id>/bundled.js`,
which `resolvePluginEntryPath` prefers verbatim with NO freshness check. That
bundle is a build artifact only `tsup` regenerates — the FN-7779 dev prebuild
rebuilds each plugin's OWN `plugins/<id>/dist` but never the staged bundle, so a
source-only plugin fix (e.g. the FN-7796 Grok adapter) silently ran stale and
grok chat returned empty replies. Probe the workspace source dir
(`<repo>/plugins/<id>`) BEFORE the staged bundle so dev loads the live plugin
whose entry `resolvePluginEntryPath` freshness-checks (dist vs src) — self-healing
even when the prebuild is skipped. The workspace dir only exists in a checkout, so
published installs are unaffected.
*/
function getCandidatePluginDirs(pluginId: string): string[] {
const moduleDir = dirname(fileURLToPath(import.meta.url));
const cliPackageRoot = resolve(moduleDir, "..", "..");
return [
// Bundled/global runtime: moduleDir is typically <cli>/dist, and plugins are
// staged under <cli>/dist/plugins/<id>. Keep first for the global-install regression.
join(moduleDir, "plugins", pluginId),
// Source checkout: prefer the live workspace plugin (freshness-checked by
// resolvePluginEntryPath) over the stale staged tsup bundle below.
join(cliPackageRoot, "..", "..", "plugins", pluginId),
// Source/dev fallbacks.
join(cliPackageRoot, "dist", "plugins", pluginId),
join(cliPackageRoot, "plugins", pluginId),
];
}
export async function ensureBundledPluginInstalled(
pluginStore: PluginStore,
pluginLoader: PluginLoader,
pluginId: string,
): Promise<EnsureBundledResult> {
return coreEnsureBundledPluginInstalled(pluginStore, pluginLoader, pluginId, getCandidatePluginDirs);
}
/**
* @deprecated Use {@link ensureBundledPluginInstalled} with the explicit plugin id.
* Kept for backwards compatibility with existing call sites.
*/
export async function ensureBundledDependencyGraphPluginInstalled(
pluginStore: PluginStore,
pluginLoader: PluginLoader,
): Promise<EnsureBundledResult> {
return coreEnsureBundledDependencyGraphPluginInstalled(pluginStore, pluginLoader, getCandidatePluginDirs);
}
export async function ensureBundledCursorRuntimePluginInstalled(
pluginStore: PluginStore,
pluginLoader: PluginLoader,
): Promise<EnsureBundledResult> {
return coreEnsureBundledCursorRuntimePluginInstalled(pluginStore, pluginLoader, getCandidatePluginDirs);
}
export async function ensureBundledGrokRuntimePluginInstalled(
pluginStore: PluginStore,
pluginLoader: PluginLoader,
): Promise<EnsureBundledResult> {
return coreEnsureBundledGrokRuntimePluginInstalled(pluginStore, pluginLoader, getCandidatePluginDirs);
}