feat(FN-3396): bundle Cursor CLI as a plugin provider with dashboard auth w

Merges FN-3396's full Cursor CLI provider integration (Steps 1–4): defines a CLI-backed provider contract, adds the `fusion-plugin-cursor-runtime` plugin package with process management and runtime probes, wires dashboard auth flows and UI (ProviderCard, onboarding modal, settings), and bundles the

Fusion-Task-Id: FN-3396
This commit is contained in:
Fusion
2026-05-07 04:10:15 -07:00
committed by gsxdsm
parent f3c99eb4fa
commit 3735e0565a
50 changed files with 1292 additions and 3 deletions

View File

@@ -6,6 +6,7 @@ export const cliRoot = join(__dirname, "..", "..");
export const workspaceRoot = join(cliRoot, "..", "..");
export const bundlePath = join(cliRoot, "dist", "bin.js");
export const clientIndexPath = join(cliRoot, "dist", "client", "index.html");
const cursorPluginManifestPath = join(cliRoot, "dist", "plugins", "fusion-plugin-cursor-runtime", "manifest.json");
export const dashboardClientStubMarker = "Dashboard assets not built";
@@ -28,7 +29,7 @@ function runBuildCommand(command: string, cwd: string) {
}
function hasBuiltDashboardAssets(): boolean {
if (!existsSync(bundlePath) || !existsSync(clientIndexPath)) {
if (!existsSync(bundlePath) || !existsSync(clientIndexPath) || !existsSync(cursorPluginManifestPath)) {
return false;
}

View File

@@ -169,6 +169,17 @@ describe("CLI bundle output", () => {
expect(manifest.name?.length).toBeGreaterThan(0);
});
it("dist/plugins/fusion-plugin-cursor-runtime/ is staged with a valid manifest", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-cursor-runtime");
const manifestPath = join(stagedRoot, "manifest.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe("fusion-plugin-cursor-runtime");
expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0);
});
it("pi-claude-cli source imports child process helpers from node:child_process", () => {
const processManagerSource = readFileSync(join(cliRoot, "dist", "pi-claude-cli", "src", "process-manager.ts"), "utf-8");

View File

@@ -24,6 +24,7 @@ vi.mock("@fusion/core", () => ({
// Import SUT after mocks are in place
import {
ensureBundledDependencyGraphPluginInstalled,
ensureBundledCursorRuntimePluginInstalled,
ensureBundledPluginInstalled,
resolvePluginEntryPath,
} from "../bundled-plugin-install.js";
@@ -32,6 +33,7 @@ import {
const BUNDLED_PLUGIN_ID = "fusion-plugin-dependency-graph";
const HERMES_PLUGIN_ID = "fusion-plugin-hermes-runtime";
const CURSOR_PLUGIN_ID = "fusion-plugin-cursor-runtime";
function makeManifest(overrides?: Partial<{ id: string; version: string; name: string }>) {
return {
@@ -356,6 +358,30 @@ describe("ensureBundledDependencyGraphPluginInstalled", () => {
).rejects.toThrow("Invalid plugin manifest");
});
it("registers Cursor runtime through the dedicated helper", async () => {
const manifest = makeManifest({ id: CURSOR_PLUGIN_ID, name: "Cursor Runtime" });
mockExistsSync.mockImplementation((p: string) => {
if (p.endsWith("manifest.json") && p.includes(CURSOR_PLUGIN_ID)) return true;
if (p.endsWith("/src/index.ts") && p.includes(CURSOR_PLUGIN_ID)) return true;
return false;
});
mockReadFile.mockResolvedValue(JSON.stringify(manifest));
mockValidatePluginManifest.mockReturnValue({ valid: true, errors: [] });
const store = makePluginStore();
const loader = makePluginLoader();
const result = await ensureBundledCursorRuntimePluginInstalled(
store as unknown as import("@fusion/core").PluginStore,
loader as unknown as import("@fusion/core").PluginLoader,
);
expect(result).toBe("installed");
expect(store.registerPlugin).toHaveBeenCalledWith(
expect.objectContaining({ manifest: expect.objectContaining({ id: CURSOR_PLUGIN_ID }) }),
);
});
it("registers Hermes from source entry when both src and dist entries exist", async () => {
const manifest = makeManifest({ id: HERMES_PLUGIN_ID, name: "Hermes Runtime" });
mockExistsSync.mockImplementation((p: string) => {

View File

@@ -5,12 +5,14 @@ import { fileURLToPath } from "node:url";
import { validatePluginManifest, type PluginInstallation, type PluginLoader, type PluginManifest, type PluginStore } from "@fusion/core";
const DEPENDENCY_GRAPH_PLUGIN_ID = "fusion-plugin-dependency-graph";
const CURSOR_RUNTIME_PLUGIN_ID = "fusion-plugin-cursor-runtime";
export const BUNDLED_PLUGIN_IDS = [
"fusion-plugin-dependency-graph",
"fusion-plugin-hermes-runtime",
"fusion-plugin-openclaw-runtime",
"fusion-plugin-paperclip-runtime",
"fusion-plugin-cursor-runtime",
] as const;
export type BundledPluginId = (typeof BUNDLED_PLUGIN_IDS)[number];
@@ -156,3 +158,10 @@ export async function ensureBundledDependencyGraphPluginInstalled(
): Promise<EnsureBundledResult> {
return ensureBundledPluginInstalled(pluginStore, pluginLoader, DEPENDENCY_GRAPH_PLUGIN_ID);
}
export async function ensureBundledCursorRuntimePluginInstalled(
pluginStore: PluginStore,
pluginLoader: PluginLoader,
): Promise<EnsureBundledResult> {
return ensureBundledPluginInstalled(pluginStore, pluginLoader, CURSOR_RUNTIME_PLUGIN_ID);
}

View File

@@ -12,6 +12,7 @@ const RUNTIME_PLUGIN_IDS = [
"fusion-plugin-hermes-runtime",
"fusion-plugin-openclaw-runtime",
"fusion-plugin-paperclip-runtime",
"fusion-plugin-cursor-runtime",
] as const;
const __dirname = dirname(fileURLToPath(import.meta.url));