feat(FN-3239): auto-install dependency graph plugin in CLI daemon, dashboar

Merged the three-step FN-3239 feature: auto-installing the dependency graph plugin on first run across the `daemon`, `dashboard`, and `serve` commands, with the core logic centralized in `bundled-plugin-install.ts`, bundling configuration in `tsup.config.ts`, and a documentation file for the default

Fusion-Task-Id: FN-3239
This commit is contained in:
Fusion
2026-05-02 22:24:09 -07:00
committed by gsxdsm
parent bd8bf5d827
commit 3fbe3eeb85
8 changed files with 130 additions and 1 deletions

View File

@@ -35,6 +35,7 @@
"dist/client/**",
"dist/pi-claude-cli/**",
"dist/droid-cli/**",
"dist/plugins/**",
"skill/**",
"README.md"
],

View File

@@ -58,6 +58,7 @@ import { resolveSelfExtension } from "./self-extension.js";
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { resolveProject } from "../project-context.js";
import { ensureBundledDependencyGraphPluginInstalled } from "../plugins/bundled-plugin-install.js";
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
let daemonStartTime = 0;
@@ -374,6 +375,17 @@ export async function runDaemon(opts: DaemonOptions = {}) {
taskStore: store,
});
try {
const installStatus = await ensureBundledDependencyGraphPluginInstalled(pluginStore, pluginLoader);
if (installStatus === "installed") {
console.log("[plugins] Installed bundled Dependency Graph plugin");
} else if (installStatus === "missing-bundle") {
console.warn("[plugins] Bundled Dependency Graph plugin was not found in this build");
}
} catch (err) {
console.warn(`[plugins] Failed to auto-install bundled Dependency Graph plugin: ${err instanceof Error ? err.message : err}`);
}
// Auto-load all enabled plugins so runtime UI (NewAgentDialog, AgentDetailView)
// can discover installed runtimes like Hermes and OpenClaw.
try {

View File

@@ -54,6 +54,7 @@ import {
} from "./droid-cli-extension.js";
import { getCachedUpdateStatus, isUpdateCheckEnabled } from "../update-cache.js";
import { resolveSelfExtension } from "./self-extension.js";
import { ensureBundledDependencyGraphPluginInstalled } from "../plugins/bundled-plugin-install.js";
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
import { DashboardTUI, DashboardLogSink, isTTYAvailable, type SystemInfo, type GitStatus, type GitCommit, type GitCommitDetail, type GitBranch, type GitWorktree, type FileEntry, type FileReadResult, type TaskStep as TUITaskStep, type TaskLogEntry as TUITaskLogEntry, type TaskDetailData, type TaskEvent } from "./dashboard-tui/index.js";
@@ -1077,6 +1078,20 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
taskStore: store,
});
try {
const installStatus = await ensureBundledDependencyGraphPluginInstalled(pluginStore, pluginLoader);
if (installStatus === "installed") {
logSink.log("Installed bundled Dependency Graph plugin", "plugins");
} else if (installStatus === "missing-bundle") {
logSink.log("Bundled Dependency Graph plugin was not found in this build", "plugins");
}
} catch (err) {
logSink.log(
`Failed to auto-install bundled Dependency Graph plugin: ${err instanceof Error ? err.message : err}`,
"plugins",
);
}
// Auto-load all enabled plugins so runtime UI (NewAgentDialog, AgentDetailView)
// can discover installed runtimes like Hermes and OpenClaw.
try {

View File

@@ -59,6 +59,7 @@ import {
} from "./droid-cli-extension.js";
import { resolveSelfExtension } from "./self-extension.js";
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
import { ensureBundledDependencyGraphPluginInstalled } from "../plugins/bundled-plugin-install.js";
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
let diagnosticIntervalHandle: ReturnType<typeof setInterval> | null = null;
@@ -428,6 +429,17 @@ export async function runServe(
taskStore: store,
});
try {
const installStatus = await ensureBundledDependencyGraphPluginInstalled(pluginStore, pluginLoader);
if (installStatus === "installed") {
console.log("[plugins] Installed bundled Dependency Graph plugin");
} else if (installStatus === "missing-bundle") {
console.warn("[plugins] Bundled Dependency Graph plugin was not found in this build");
}
} catch (err) {
console.warn(`[plugins] Failed to auto-install bundled Dependency Graph plugin: ${err instanceof Error ? err.message : err}`);
}
// Auto-load all enabled plugins so runtime UI (NewAgentDialog, AgentDetailView)
// can discover installed runtimes like Hermes and OpenClaw.
try {

View File

@@ -0,0 +1,67 @@
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { validatePluginManifest, type PluginLoader, type PluginManifest, type PluginStore } from "@fusion/core";
const DEPENDENCY_GRAPH_PLUGIN_ID = "fusion-plugin-dependency-graph";
function getCandidatePluginPaths(): string[] {
const moduleDir = dirname(fileURLToPath(import.meta.url));
const cliPackageRoot = resolve(moduleDir, "..", "..");
return [
join(cliPackageRoot, "dist", "plugins", DEPENDENCY_GRAPH_PLUGIN_ID),
join(cliPackageRoot, "plugins", DEPENDENCY_GRAPH_PLUGIN_ID),
join(cliPackageRoot, "..", "..", "plugins", DEPENDENCY_GRAPH_PLUGIN_ID),
];
}
async function loadManifest(pluginDir: string): Promise<PluginManifest> {
const manifestPath = join(pluginDir, "manifest.json");
const content = await readFile(manifestPath, "utf-8");
const manifest = JSON.parse(content);
const validation = validatePluginManifest(manifest);
if (!validation.valid) {
throw new Error(`Invalid plugin manifest: ${validation.errors.join(", ")}`);
}
return manifest;
}
function resolveBundledDependencyGraphPath(): string | null {
for (const path of getCandidatePluginPaths()) {
if (existsSync(join(path, "manifest.json"))) {
return path;
}
}
return null;
}
export async function ensureBundledDependencyGraphPluginInstalled(
pluginStore: PluginStore,
pluginLoader: PluginLoader,
): Promise<"installed" | "already-installed" | "missing-bundle"> {
try {
await pluginStore.getPlugin(DEPENDENCY_GRAPH_PLUGIN_ID);
return "already-installed";
} catch {
// Continue; plugin not installed yet.
}
const bundledPath = resolveBundledDependencyGraphPath();
if (!bundledPath) {
return "missing-bundle";
}
const manifest = await loadManifest(bundledPath);
const plugin = await pluginStore.registerPlugin({
manifest,
path: bundledPath,
});
if (plugin.enabled) {
await pluginLoader.loadPlugin(plugin.id);
}
return "installed";
}

View File

@@ -10,6 +10,8 @@ const piClaudeCliSrc = join(__dirname, "..", "pi-claude-cli");
const piClaudeCliDest = join(__dirname, "dist", "pi-claude-cli");
const droidCliSrc = join(__dirname, "..", "droid-cli");
const droidCliDest = join(__dirname, "dist", "droid-cli");
const dependencyGraphPluginSrc = join(__dirname, "..", "..", "plugins", "fusion-plugin-dependency-graph");
const dependencyGraphPluginDest = join(__dirname, "dist", "plugins", "fusion-plugin-dependency-graph");
const dashboardClientStub = `<!doctype html>
<html lang="en">
<head>
@@ -88,6 +90,21 @@ export default defineConfig({
);
}
if (existsSync(dependencyGraphPluginDest)) {
rmSync(dependencyGraphPluginDest, { recursive: true, force: true });
}
if (existsSync(dependencyGraphPluginSrc)) {
mkdirSync(dependencyGraphPluginDest, { recursive: true });
cpSync(join(dependencyGraphPluginSrc, "manifest.json"), join(dependencyGraphPluginDest, "manifest.json"));
cpSync(join(dependencyGraphPluginSrc, "package.json"), join(dependencyGraphPluginDest, "package.json"));
cpSync(join(dependencyGraphPluginSrc, "src"), join(dependencyGraphPluginDest, "src"), { recursive: true });
console.log("Copied dependency graph plugin to dist/plugins/fusion-plugin-dependency-graph/");
} else {
console.warn(
`WARNING: Dependency graph plugin source not found at ${dependencyGraphPluginSrc}; bundled auto-install will be unavailable.`,
);
}
if (existsSync(dashboardClientDest)) {
rmSync(dashboardClientDest, { recursive: true, force: true });
}