FN-7623: wire pluginStore/pluginLoader into desktop embedded dashboard server
Fixes the desktop app's plugin subsystem, which was never wired into createServer(), breaking Settings > Plugins Browse registry and plugin install. - local-runtime.ts: construct a PluginStore + PluginLoader (mirroring the CLI dashboard command), load enabled plugins, run plugin schema-init hooks, and pass pluginStore/pluginLoader/pluginRunner into createServer() - local-server.ts: apply the same wiring to the legacy desktop local server path for consistency - Both paths fail soft: a broken plugin subsystem (e.g. corrupt manifest) is logged/traced but no longer blocks embedded dashboard startup - Extend local-runtime.test.ts and local-server.test.ts to cover plugin wiring and the fail-soft path - Add changeset (patch) documenting the fix Files changed: .changeset/fn-7623-desktop-plugin-wiring.md | 7 ++ .../desktop/src/__tests__/local-runtime.test.ts | 123 ++++++++++++++++++++- .../desktop/src/__tests__/local-server.test.ts | 69 +++++++++++- packages/desktop/src/local-runtime.ts | 50 ++++++++- packages/desktop/src/local-server.ts | 41 ++++++- 5 files changed, 286 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-7623 Fusion-Task-Lineage: c6f291fb-e6aa-4ac1-a3f3-4189fc831c60 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7623-desktop-plugin-wiring.md
Normal file
7
.changeset/fn-7623-desktop-plugin-wiring.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Fix desktop app plugin install and Browse registry (plugin subsystem now wired into the embedded server).
|
||||||
|
category: fix
|
||||||
|
dev: local-runtime.ts / local-server.ts now build a PluginStore + PluginLoader and pass pluginStore/pluginLoader/pluginRunner into createServer, mirroring the CLI dashboard command (FN-7623, issue #1937). Plugin subsystem init is fail-soft — a broken plugin (e.g. corrupt manifest) logs/traces via strace(...) but no longer blocks embedded dashboard startup.
|
||||||
@@ -47,6 +47,19 @@ class FakeServer {
|
|||||||
* `createDashboardServer` in LocalRuntimeManagerOptions). Mirrors local-server.test.ts's pattern.
|
* `createDashboardServer` in LocalRuntimeManagerOptions). Mirrors local-server.test.ts's pattern.
|
||||||
*/
|
*/
|
||||||
const engineMocks = vi.hoisted(() => {
|
const engineMocks = vi.hoisted(() => {
|
||||||
|
// FN-7623: pluginStore/pluginLoader mocks proving createDashboardServerDefault wires the plugin
|
||||||
|
// subsystem into createServer (fixes desktop's "Plugin install mode is not supported" and Browse
|
||||||
|
// registry "Plugin \"registry\" not found" symptoms).
|
||||||
|
const pluginStoreInstance = { init: vi.fn(async () => undefined) };
|
||||||
|
const pluginLoaderInstance = {
|
||||||
|
loadAllPlugins: vi.fn(async () => ({ loaded: 2, errors: 0 })),
|
||||||
|
getPluginSchemaInitHooks: vi.fn(() => []),
|
||||||
|
};
|
||||||
|
const runPluginSchemaInits = vi.fn(async () => undefined);
|
||||||
|
const PluginLoader = vi.fn(function () {
|
||||||
|
return pluginLoaderInstance;
|
||||||
|
});
|
||||||
|
|
||||||
const centralCore = {
|
const centralCore = {
|
||||||
init: vi.fn(async () => undefined),
|
init: vi.fn(async () => undefined),
|
||||||
close: vi.fn(async () => undefined),
|
close: vi.fn(async () => undefined),
|
||||||
@@ -76,14 +89,18 @@ const engineMocks = vi.hoisted(() => {
|
|||||||
centralCore,
|
centralCore,
|
||||||
engineManager,
|
engineManager,
|
||||||
CentralCore,
|
CentralCore,
|
||||||
|
PluginLoader,
|
||||||
ProjectEngineManager,
|
ProjectEngineManager,
|
||||||
seedDashboardProviders,
|
seedDashboardProviders,
|
||||||
seedDashboardProvidersDispose,
|
seedDashboardProvidersDispose,
|
||||||
createServer,
|
createServer,
|
||||||
|
pluginStoreInstance,
|
||||||
|
pluginLoaderInstance,
|
||||||
|
runPluginSchemaInits,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("@fusion/core", () => ({ CentralCore: engineMocks.CentralCore }));
|
vi.mock("@fusion/core", () => ({ CentralCore: engineMocks.CentralCore, PluginLoader: engineMocks.PluginLoader }));
|
||||||
vi.mock("@fusion/dashboard", () => ({ createServer: engineMocks.createServer }));
|
vi.mock("@fusion/dashboard", () => ({ createServer: engineMocks.createServer }));
|
||||||
vi.mock("@fusion/engine", () => ({
|
vi.mock("@fusion/engine", () => ({
|
||||||
ProjectEngineManager: engineMocks.ProjectEngineManager,
|
ProjectEngineManager: engineMocks.ProjectEngineManager,
|
||||||
@@ -97,6 +114,8 @@ describe("LocalRuntimeManager", () => {
|
|||||||
init: vi.fn(async () => undefined),
|
init: vi.fn(async () => undefined),
|
||||||
watch: vi.fn(async () => undefined),
|
watch: vi.fn(async () => undefined),
|
||||||
close: vi.fn(),
|
close: vi.fn(),
|
||||||
|
getPluginStore: vi.fn(() => engineMocks.pluginStoreInstance),
|
||||||
|
getDatabase: vi.fn(() => ({ runPluginSchemaInits: engineMocks.runPluginSchemaInits })),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -442,4 +461,106 @@ describe("LocalRuntimeManager", () => {
|
|||||||
await manager.stopLocal();
|
await manager.stopLocal();
|
||||||
expect(engineMocks.seedDashboardProvidersDispose).toHaveBeenCalledTimes(1);
|
expect(engineMocks.seedDashboardProvidersDispose).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FN-7623 symptom verification: before this fix, createDashboardServerDefault called createServer
|
||||||
|
* WITHOUT pluginStore/pluginLoader, so desktop's Browse-registry sub-router never mounted ("Plugin
|
||||||
|
* \"registry\" not found") and plugin install threw "Plugin install mode is not supported: plugin
|
||||||
|
* loader not available". Assert the fix in the engine-less (zero-projects) startup state — the
|
||||||
|
* plugin subsystem must wire in regardless of whether a primary engine resolved.
|
||||||
|
*/
|
||||||
|
it("wires PluginStore + PluginLoader into createServer when engine-less (zero projects) (FN-7623)", async () => {
|
||||||
|
const { LocalRuntimeManager } = await import("../local-runtime.ts");
|
||||||
|
const server = new FakeServer(4545);
|
||||||
|
engineMocks.createServer.mockReturnValueOnce({
|
||||||
|
listen: vi.fn(() => {
|
||||||
|
setTimeout(() => server.emit("listening"), 0);
|
||||||
|
return server as unknown as Server;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const manager = new LocalRuntimeManager({
|
||||||
|
rootDir: "/repo",
|
||||||
|
createStore: async () => store,
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.startLocal();
|
||||||
|
|
||||||
|
expect(store.getPluginStore).toHaveBeenCalledTimes(1);
|
||||||
|
expect(engineMocks.pluginStoreInstance.init).toHaveBeenCalledTimes(1);
|
||||||
|
expect(engineMocks.PluginLoader).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ pluginStore: engineMocks.pluginStoreInstance, taskStore: expect.anything() }),
|
||||||
|
);
|
||||||
|
expect(engineMocks.pluginLoaderInstance.loadAllPlugins).toHaveBeenCalledTimes(1);
|
||||||
|
expect(engineMocks.createServer).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({
|
||||||
|
pluginStore: engineMocks.pluginStoreInstance,
|
||||||
|
pluginLoader: engineMocks.pluginLoaderInstance,
|
||||||
|
pluginRunner: engineMocks.pluginLoaderInstance,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.stopLocal();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wires PluginStore + PluginLoader into createServer when a project engine resolved (projects-present) (FN-7623)", async () => {
|
||||||
|
const { LocalRuntimeManager } = await import("../local-runtime.ts");
|
||||||
|
engineMocks.centralCore.listProjects.mockResolvedValueOnce([
|
||||||
|
{ id: "project-1", name: "Repo", path: "/repo", status: "active" },
|
||||||
|
]);
|
||||||
|
const server = new FakeServer(4545);
|
||||||
|
engineMocks.createServer.mockReturnValueOnce({
|
||||||
|
listen: vi.fn(() => {
|
||||||
|
setTimeout(() => server.emit("listening"), 0);
|
||||||
|
return server as unknown as Server;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const manager = new LocalRuntimeManager({
|
||||||
|
rootDir: "/repo",
|
||||||
|
createStore: async () => store,
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.startLocal();
|
||||||
|
|
||||||
|
expect(engineMocks.createServer).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({
|
||||||
|
engine: expect.anything(),
|
||||||
|
pluginStore: engineMocks.pluginStoreInstance,
|
||||||
|
pluginLoader: engineMocks.pluginLoaderInstance,
|
||||||
|
pluginRunner: engineMocks.pluginLoaderInstance,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.stopLocal();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("boots the dashboard without plugin wiring when the plugin subsystem fails to init (fail-soft) (FN-7623)", async () => {
|
||||||
|
const { LocalRuntimeManager } = await import("../local-runtime.ts");
|
||||||
|
engineMocks.pluginStoreInstance.init.mockRejectedValueOnce(new Error("plugin db locked"));
|
||||||
|
const server = new FakeServer(4545);
|
||||||
|
engineMocks.createServer.mockReturnValueOnce({
|
||||||
|
listen: vi.fn(() => {
|
||||||
|
setTimeout(() => server.emit("listening"), 0);
|
||||||
|
return server as unknown as Server;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const manager = new LocalRuntimeManager({
|
||||||
|
rootDir: "/repo",
|
||||||
|
createStore: async () => store,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await manager.startLocal();
|
||||||
|
|
||||||
|
expect(status).toMatchObject({ source: "embedded-local", state: "running", port: 4545 });
|
||||||
|
expect(engineMocks.createServer).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.not.objectContaining({ pluginStore: expect.anything() }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.stopLocal();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,10 +30,28 @@ const mocks = vi.hoisted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FN-7623: pluginStore/pluginLoader mocks proving local-server.ts wires the plugin subsystem
|
||||||
|
// into createServer (the fix for "Plugin install mode is not supported" and the Browse-registry
|
||||||
|
// "Plugin \"registry\" not found" symptoms).
|
||||||
|
const pluginStoreInstance = {
|
||||||
|
init: vi.fn(async () => undefined),
|
||||||
|
};
|
||||||
|
const pluginLoaderInstance = {
|
||||||
|
loadAllPlugins: vi.fn(async () => ({ loaded: 2, errors: 0 })),
|
||||||
|
getPluginSchemaInitHooks: vi.fn(() => []),
|
||||||
|
};
|
||||||
|
const runPluginSchemaInits = vi.fn(async () => undefined);
|
||||||
|
const database = { runPluginSchemaInits };
|
||||||
|
const PluginLoader = vi.fn(function () {
|
||||||
|
return pluginLoaderInstance;
|
||||||
|
});
|
||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
init: vi.fn(async () => undefined),
|
init: vi.fn(async () => undefined),
|
||||||
watch: vi.fn(async () => undefined),
|
watch: vi.fn(async () => undefined),
|
||||||
close: vi.fn(),
|
close: vi.fn(),
|
||||||
|
getPluginStore: vi.fn(() => pluginStoreInstance),
|
||||||
|
getDatabase: vi.fn(() => database),
|
||||||
};
|
};
|
||||||
const centralCore = {
|
const centralCore = {
|
||||||
init: vi.fn(async () => undefined),
|
init: vi.fn(async () => undefined),
|
||||||
@@ -61,6 +79,8 @@ const mocks = vi.hoisted(() => {
|
|||||||
init = store.init;
|
init = store.init;
|
||||||
watch = store.watch;
|
watch = store.watch;
|
||||||
close = store.close;
|
close = store.close;
|
||||||
|
getPluginStore = store.getPluginStore;
|
||||||
|
getDatabase = store.getDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = Object.assign(new SimpleEmitter(), {
|
const server = Object.assign(new SimpleEmitter(), {
|
||||||
@@ -94,6 +114,7 @@ const mocks = vi.hoisted(() => {
|
|||||||
return {
|
return {
|
||||||
TaskStore,
|
TaskStore,
|
||||||
CentralCore,
|
CentralCore,
|
||||||
|
PluginLoader,
|
||||||
ProjectEngineManager,
|
ProjectEngineManager,
|
||||||
createServer,
|
createServer,
|
||||||
store,
|
store,
|
||||||
@@ -101,12 +122,15 @@ const mocks = vi.hoisted(() => {
|
|||||||
centralCore,
|
centralCore,
|
||||||
engineManager,
|
engineManager,
|
||||||
engine,
|
engine,
|
||||||
|
pluginStoreInstance,
|
||||||
|
pluginLoaderInstance,
|
||||||
|
runPluginSchemaInits,
|
||||||
seedDashboardProviders,
|
seedDashboardProviders,
|
||||||
seedDashboardProvidersDispose,
|
seedDashboardProvidersDispose,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("@fusion/core", () => ({ TaskStore: mocks.TaskStore, CentralCore: mocks.CentralCore }));
|
vi.mock("@fusion/core", () => ({ TaskStore: mocks.TaskStore, CentralCore: mocks.CentralCore, PluginLoader: mocks.PluginLoader }));
|
||||||
vi.mock("@fusion/dashboard", () => ({ createServer: mocks.createServer }));
|
vi.mock("@fusion/dashboard", () => ({ createServer: mocks.createServer }));
|
||||||
vi.mock("@fusion/engine", () => ({
|
vi.mock("@fusion/engine", () => ({
|
||||||
ProjectEngineManager: mocks.ProjectEngineManager,
|
ProjectEngineManager: mocks.ProjectEngineManager,
|
||||||
@@ -245,4 +269,47 @@ describe("DesktopLocalServerManager", () => {
|
|||||||
await manager.stop();
|
await manager.stop();
|
||||||
expect(mocks.seedDashboardProvidersDispose).toHaveBeenCalledTimes(1);
|
expect(mocks.seedDashboardProvidersDispose).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FN-7623 symptom verification: before this fix, DesktopLocalServerManager.start() called
|
||||||
|
* createServer WITHOUT pluginStore/pluginLoader, so the registry sub-router never mounted
|
||||||
|
* ("Plugin \"registry\" not found" on Browse registry) and install threw "Plugin install mode
|
||||||
|
* is not supported: plugin loader not available". Assert the fix: createServer now receives
|
||||||
|
* pluginStore, pluginLoader, and pluginRunner (aliased to the same PluginLoader instance).
|
||||||
|
*/
|
||||||
|
it("wires PluginStore + PluginLoader into createServer (FN-7623)", async () => {
|
||||||
|
const { DesktopLocalServerManager } = await import("../local-server.ts");
|
||||||
|
const manager = new DesktopLocalServerManager("/repo");
|
||||||
|
|
||||||
|
await manager.start();
|
||||||
|
|
||||||
|
expect(mocks.store.getPluginStore).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.pluginStoreInstance.init).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.PluginLoader).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ pluginStore: mocks.pluginStoreInstance, taskStore: expect.anything() }),
|
||||||
|
);
|
||||||
|
expect(mocks.pluginLoaderInstance.loadAllPlugins).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.createServer).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({
|
||||||
|
pluginStore: mocks.pluginStoreInstance,
|
||||||
|
pluginLoader: mocks.pluginLoaderInstance,
|
||||||
|
pluginRunner: mocks.pluginLoaderInstance,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("boots the dashboard without plugin wiring when the plugin subsystem fails to init (fail-soft)", async () => {
|
||||||
|
mocks.pluginStoreInstance.init.mockRejectedValueOnce(new Error("plugin db locked"));
|
||||||
|
const { DesktopLocalServerManager } = await import("../local-server.ts");
|
||||||
|
const manager = new DesktopLocalServerManager("/repo");
|
||||||
|
|
||||||
|
const runtime = await manager.start();
|
||||||
|
|
||||||
|
expect(runtime.port).toBe(4545);
|
||||||
|
expect(mocks.createServer).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.not.objectContaining({ pluginStore: expect.anything() }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,10 +36,23 @@ export interface DesktopRuntimeStatus {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:DesktopRuntime 2026-07-07-12:00:
|
||||||
|
* FN-7623: the embedded desktop server must wire a PluginStore + PluginLoader into createServer
|
||||||
|
* (as the CLI dashboard command does) or the Settings -> Plugins Browse-registry sub-router never
|
||||||
|
* mounts ("Plugin \"registry\" not found") and plugin install throws "Plugin install mode is not
|
||||||
|
* supported: plugin loader not available". getPluginStore()/getDatabase() are the two TaskStore
|
||||||
|
* members this wiring needs beyond the pre-existing init/watch/close surface.
|
||||||
|
*/
|
||||||
|
type PluginStoreLike = { init(): Promise<void> };
|
||||||
|
type PluginDatabaseLike = { runPluginSchemaInits(hooks: Array<{ pluginId: string; hook: unknown }>): Promise<void> };
|
||||||
|
|
||||||
type TaskStoreLike = {
|
type TaskStoreLike = {
|
||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
watch(): Promise<void>;
|
watch(): Promise<void>;
|
||||||
close(): void;
|
close(): void;
|
||||||
|
getPluginStore(): PluginStoreLike;
|
||||||
|
getDatabase(): PluginDatabaseLike;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RuntimeCleanup = () => Promise<void> | void;
|
type RuntimeCleanup = () => Promise<void> | void;
|
||||||
@@ -87,7 +100,7 @@ async function createStoreDefault(rootDir: string): Promise<TaskStoreLike> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createDashboardServerDefault(store: TaskStoreLike, rootDir: string): Promise<{ server: Server; cleanup: RuntimeCleanup }> {
|
async function createDashboardServerDefault(store: TaskStoreLike, rootDir: string): Promise<{ server: Server; cleanup: RuntimeCleanup }> {
|
||||||
const { CentralCore } = await import("@fusion/core");
|
const { CentralCore, PluginLoader } = await import("@fusion/core");
|
||||||
const { createServer } = await import("@fusion/dashboard");
|
const { createServer } = await import("@fusion/dashboard");
|
||||||
const { ProjectEngineManager, createFusionAuthStorage, createFusionModelRegistry, seedDashboardProviders } = await import("@fusion/engine");
|
const { ProjectEngineManager, createFusionAuthStorage, createFusionModelRegistry, seedDashboardProviders } = await import("@fusion/engine");
|
||||||
|
|
||||||
@@ -149,6 +162,40 @@ async function createDashboardServerDefault(store: TaskStoreLike, rootDir: strin
|
|||||||
log: (scope, message) => strace(`[${scope}] ${message}`),
|
log: (scope, message) => strace(`[${scope}] ${message}`),
|
||||||
});
|
});
|
||||||
providerSeeding.dispose = dispose;
|
providerSeeding.dispose = dispose;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:DesktopRuntime 2026-07-07-12:00:
|
||||||
|
* FN-7623: mirror the CLI dashboard command's plugin wiring (packages/cli/src/commands/dashboard.ts)
|
||||||
|
* — construct the store's PluginStore, build a PluginLoader, load enabled plugins, and run schema-init
|
||||||
|
* hooks — so the desktop embedded server's registry sub-router mounts (GET /api/plugins/registry) and
|
||||||
|
* POST /api/plugins install mode works. Bundled-plugin auto-install (Hermes/OpenClaw/Paperclip/Dependency
|
||||||
|
* Graph) depends on packages/cli/src/plugins/bundled-plugin-install.ts, which is CLI-only and out of
|
||||||
|
* scope for desktop (desktop must not depend on the CLI package) — see FN-7623 scope note. Failures here
|
||||||
|
* must not crash embedded startup: the dashboard still needs to boot even if the plugin subsystem can't
|
||||||
|
* come up (e.g. a corrupt plugin manifest), so this is wrapped and traced rather than left to throw.
|
||||||
|
*/
|
||||||
|
let pluginStore: PluginStoreLike | undefined;
|
||||||
|
let pluginLoader: InstanceType<typeof PluginLoader> | undefined;
|
||||||
|
try {
|
||||||
|
strace("createDashboardServer: pluginStore.init");
|
||||||
|
pluginStore = store.getPluginStore();
|
||||||
|
await pluginStore.init();
|
||||||
|
pluginLoader = new PluginLoader({ pluginStore: pluginStore as never, taskStore: store as never });
|
||||||
|
strace("createDashboardServer: pluginLoader.loadAllPlugins");
|
||||||
|
const { loaded, errors } = await pluginLoader.loadAllPlugins();
|
||||||
|
strace(`createDashboardServer: plugins loaded=${loaded} errors=${errors}`);
|
||||||
|
const schemaHooks = pluginLoader.getPluginSchemaInitHooks();
|
||||||
|
if (schemaHooks.length > 0) {
|
||||||
|
await store.getDatabase().runPluginSchemaInits(schemaHooks);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
strace(
|
||||||
|
`createDashboardServer: plugin subsystem init FAILED (non-fatal, dashboard still boots) — ${error instanceof Error ? error.stack : String(error)}`,
|
||||||
|
);
|
||||||
|
pluginStore = undefined;
|
||||||
|
pluginLoader = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
strace("createDashboardServer: createServer");
|
strace("createDashboardServer: createServer");
|
||||||
const app = createServer(store as never, {
|
const app = createServer(store as never, {
|
||||||
...(primaryEngine ? { engine: primaryEngine } : {}),
|
...(primaryEngine ? { engine: primaryEngine } : {}),
|
||||||
@@ -156,6 +203,7 @@ async function createDashboardServerDefault(store: TaskStoreLike, rootDir: strin
|
|||||||
centralCore,
|
centralCore,
|
||||||
authStorage: wrappedAuthStorage,
|
authStorage: wrappedAuthStorage,
|
||||||
modelRegistry,
|
modelRegistry,
|
||||||
|
...(pluginStore && pluginLoader ? { pluginStore: pluginStore as never, pluginLoader, pluginRunner: pluginLoader } : {}),
|
||||||
onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),
|
onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,23 @@ import type { Server } from "node:http";
|
|||||||
|
|
||||||
import { resolveDesktopRuntimePrimaryProject } from "./engine-runtime.js";
|
import { resolveDesktopRuntimePrimaryProject } from "./engine-runtime.js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:DesktopRuntime 2026-07-07-12:00:
|
||||||
|
* FN-7623: this legacy desktop local server path had the same missing plugin-subsystem wiring as
|
||||||
|
* local-runtime.ts — createServer() never received pluginStore/pluginLoader, so Settings -> Plugins
|
||||||
|
* Browse registry ("Plugin \"registry\" not found") and plugin install ("Plugin install mode is not
|
||||||
|
* supported: plugin loader not available") were both dead in this path too. Keep both desktop server
|
||||||
|
* paths consistent (see local-runtime.ts's matching comment).
|
||||||
|
*/
|
||||||
|
type PluginStoreLike = { init(): Promise<void> };
|
||||||
|
type PluginDatabaseLike = { runPluginSchemaInits(hooks: Array<{ pluginId: string; hook: unknown }>): Promise<void> };
|
||||||
|
|
||||||
type TaskStoreLike = {
|
type TaskStoreLike = {
|
||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
watch(): Promise<void>;
|
watch(): Promise<void>;
|
||||||
close(): void;
|
close(): void;
|
||||||
|
getPluginStore(): PluginStoreLike;
|
||||||
|
getDatabase(): PluginDatabaseLike;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RuntimeCleanup = () => Promise<void> | void;
|
type RuntimeCleanup = () => Promise<void> | void;
|
||||||
@@ -53,7 +66,7 @@ export class DesktopLocalServerManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { TaskStore } = await import("@fusion/core");
|
const { TaskStore } = await import("@fusion/core");
|
||||||
const { CentralCore } = await import("@fusion/core");
|
const { CentralCore, PluginLoader } = await import("@fusion/core");
|
||||||
const { createServer } = await import("@fusion/dashboard");
|
const { createServer } = await import("@fusion/dashboard");
|
||||||
const { ProjectEngineManager, createFusionAuthStorage, createFusionModelRegistry, seedDashboardProviders } = await import("@fusion/engine");
|
const { ProjectEngineManager, createFusionAuthStorage, createFusionModelRegistry, seedDashboardProviders } = await import("@fusion/engine");
|
||||||
store = new TaskStore(this.rootDir) as TaskStoreLike;
|
store = new TaskStore(this.rootDir) as TaskStoreLike;
|
||||||
@@ -93,12 +106,38 @@ export class DesktopLocalServerManager {
|
|||||||
modelRegistry,
|
modelRegistry,
|
||||||
});
|
});
|
||||||
providerSeeding.dispose = dispose;
|
providerSeeding.dispose = dispose;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:DesktopRuntime 2026-07-07-12:00:
|
||||||
|
* FN-7623: mirror the CLI dashboard command's plugin wiring — construct the store's PluginStore,
|
||||||
|
* build a PluginLoader, load enabled plugins, and run schema-init hooks — so this legacy path's
|
||||||
|
* registry sub-router mounts and install works too. Fail soft: a broken plugin subsystem must not
|
||||||
|
* prevent the embedded dashboard from booting.
|
||||||
|
*/
|
||||||
|
let pluginStore: PluginStoreLike | undefined;
|
||||||
|
let pluginLoader: InstanceType<typeof PluginLoader> | undefined;
|
||||||
|
try {
|
||||||
|
pluginStore = store.getPluginStore();
|
||||||
|
await pluginStore.init();
|
||||||
|
pluginLoader = new PluginLoader({ pluginStore: pluginStore as never, taskStore: store as never });
|
||||||
|
await pluginLoader.loadAllPlugins();
|
||||||
|
const schemaHooks = pluginLoader.getPluginSchemaInitHooks();
|
||||||
|
if (schemaHooks.length > 0) {
|
||||||
|
await store.getDatabase().runPluginSchemaInits(schemaHooks);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Plugin subsystem failures must not block embedded dashboard startup (FN-7623).
|
||||||
|
pluginStore = undefined;
|
||||||
|
pluginLoader = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const app = createServer(store as never, {
|
const app = createServer(store as never, {
|
||||||
...(primaryEngine ? { engine: primaryEngine } : {}),
|
...(primaryEngine ? { engine: primaryEngine } : {}),
|
||||||
engineManager,
|
engineManager,
|
||||||
centralCore,
|
centralCore,
|
||||||
authStorage: wrappedAuthStorage,
|
authStorage: wrappedAuthStorage,
|
||||||
modelRegistry,
|
modelRegistry,
|
||||||
|
...(pluginStore && pluginLoader ? { pluginStore: pluginStore as never, pluginLoader, pluginRunner: pluginLoader } : {}),
|
||||||
onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),
|
onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),
|
||||||
});
|
});
|
||||||
server = app.listen(0);
|
server = app.listen(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user