From 0eaa3c7b9a1f7f2caa693914e11b7611e18a4c05 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 11 Aug 2026 03:40:21 -0700 Subject: [PATCH] FN-8990: clean up plugin reload scratch files Keep plugin hot-reload cache-busting copies ephemeral and remove legacy tracked artifacts. - Delete tracked plugin reload scratch files. - Remove temporary import copies after successful and failed reloads. - Add cleanup and tracked-artifact regression coverage. Files changed: .changeset/fn-8990-plugin-reload-scratch-cleanup.md | 7 ++ packages/core/src/__tests__/plugin-hot-reload.test.ts | 25 ++++ - packages/core/src/plugins/plugin-loader.ts | 16 ++- plugins/fusion-plugin-droid-runtime/src/.index.reload-1.ts | 84 ---------------- plugins/fusion-plugin-droid-runtime/src/.index.reload-2.ts | 84 ---------------- plugins/fusion-plugin-hermes-runtime/src/.index.reload-1.ts | 108 -------------------- plugins/fusion-plugin-hermes-runtime/src/.index.reload-2.ts | 108 -------------------- plugins/fusion-plugin-hermes-runtime/src/.index.reload-3.ts | 108 -------------------- plugins/fusion-plugin-openclaw-runtime/src/.index.reload-1.ts | 95 ----------------- plugins/fusion-plugin-paperclip-runtime/src/.index.reload-1.ts | 112 --------------------- scripts/__tests__/no-tracked-plugin-reload-artifacts.test.mjs | 27 +++++ 11 files changed, 72 insertions(+), 702 deletions(-) Fusion-Task-Id: FN-8990 Fusion-Task-Lineage: 2221ebaf-b8ab-4a3c-8934-addfa348c98c Co-authored-by: Fusion (runfusion.ai) --- .../fn-8990-plugin-reload-scratch-cleanup.md | 7 ++ .../src/__tests__/plugin-hot-reload.test.ts | 25 +++- packages/core/src/plugins/plugin-loader.ts | 16 ++- .../src/.index.reload-1.ts | 84 ------------- .../src/.index.reload-2.ts | 84 ------------- .../src/.index.reload-1.ts | 108 ----------------- .../src/.index.reload-2.ts | 108 ----------------- .../src/.index.reload-3.ts | 108 ----------------- .../src/.index.reload-1.ts | 95 --------------- .../src/.index.reload-1.ts | 112 ------------------ ...o-tracked-plugin-reload-artifacts.test.mjs | 27 +++++ 11 files changed, 72 insertions(+), 702 deletions(-) create mode 100644 .changeset/fn-8990-plugin-reload-scratch-cleanup.md delete mode 100644 plugins/fusion-plugin-droid-runtime/src/.index.reload-1.ts delete mode 100644 plugins/fusion-plugin-droid-runtime/src/.index.reload-2.ts delete mode 100644 plugins/fusion-plugin-hermes-runtime/src/.index.reload-1.ts delete mode 100644 plugins/fusion-plugin-hermes-runtime/src/.index.reload-2.ts delete mode 100644 plugins/fusion-plugin-hermes-runtime/src/.index.reload-3.ts delete mode 100644 plugins/fusion-plugin-openclaw-runtime/src/.index.reload-1.ts delete mode 100644 plugins/fusion-plugin-paperclip-runtime/src/.index.reload-1.ts create mode 100644 scripts/__tests__/no-tracked-plugin-reload-artifacts.test.mjs diff --git a/.changeset/fn-8990-plugin-reload-scratch-cleanup.md b/.changeset/fn-8990-plugin-reload-scratch-cleanup.md new file mode 100644 index 0000000000..c4772eac0b --- /dev/null +++ b/.changeset/fn-8990-plugin-reload-scratch-cleanup.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Plugin hot reload no longer leaves scratch reload files in plugin folders. +category: internal +dev: Removes seven tracked artifacts and cleans importPluginModule cache-busting copies after import. diff --git a/packages/core/src/__tests__/plugin-hot-reload.test.ts b/packages/core/src/__tests__/plugin-hot-reload.test.ts index 5903d1a3d7..4d36e3d9e6 100644 --- a/packages/core/src/__tests__/plugin-hot-reload.test.ts +++ b/packages/core/src/__tests__/plugin-hot-reload.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { writeFile, mkdir, rm } from "node:fs/promises"; import { join } from "node:path"; -import { mkdtempSync } from "node:fs"; +import { mkdtempSync, readdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { EventEmitter } from "node:events"; import { PluginLoader } from "../plugins/plugin-loader.js"; @@ -390,6 +390,29 @@ describe("PluginLoader Hot-Reload", () => { expect(pluginLoader.getPluginTools()[0].name).toBe("reloaded_tool"); }); + it("removes the cache-busting copy after a successful reload", async () => { + const entryPath = await writePluginModule(tmpDir, "index.js", baseManifest); + (mockPluginStore as any)._installation.path = entryPath; + await pluginLoader.loadPlugin("hot-reload-test"); + + await writePluginModule(tmpDir, "index.js", makeManifest({ version: "2.0.0" })); + await pluginLoader.reloadPlugin("hot-reload-test"); + + expect(pluginLoader.getPlugin("hot-reload-test")?.manifest.version).toBe("2.0.0"); + expect(readdirSync(tmpDir).filter((entry) => /^\.index\.reload-/.test(entry))).toEqual([]); + }); + + it("removes the cache-busting copy when the reload import fails", async () => { + const entryPath = await writePluginModule(tmpDir, "index.js", baseManifest); + (mockPluginStore as any)._installation.path = entryPath; + await pluginLoader.loadPlugin("hot-reload-test"); + + await writeFile(entryPath, 'throw new Error("reload import failure");'); + + await expect(pluginLoader.reloadPlugin("hot-reload-test")).rejects.toThrow("reload import failure"); + expect(readdirSync(tmpDir).filter((entry) => /^\.index\.reload-/.test(entry))).toEqual([]); + }); + it("should emit plugin:reloaded event on successful reload", async () => { await pluginLoader.loadPlugin("hot-reload-test"); diff --git a/packages/core/src/plugins/plugin-loader.ts b/packages/core/src/plugins/plugin-loader.ts index 4c9b81f03d..fa2249f4a4 100644 --- a/packages/core/src/plugins/plugin-loader.ts +++ b/packages/core/src/plugins/plugin-loader.ts @@ -11,7 +11,7 @@ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path"; import { existsSync, readdirSync, statSync } from "node:fs"; -import { copyFile, readFile, stat } from "node:fs/promises"; +import { copyFile, readFile, rm, stat } from "node:fs/promises"; import { pathToFileURL } from "node:url"; import { EventEmitter } from "node:events"; import type { TaskStore } from "../store.js"; @@ -647,7 +647,19 @@ export class PluginLoader extends EventEmitter<{ const baseName = basename(path, ext); const reloadedPath = resolve(dirname(path), `.${baseName}.reload-${moduleImportVersion}${ext}`); await copyFile(path, reloadedPath); - mod = await import(pathToFileURL(reloadedPath).href); + /* + FNXC:PluginLoader 2026-08-11-09:58: + FN-8990 requires the cache-busting copy to exist only for the dynamic ESM import. + The module remains resident after import settles, so remove the copy to prevent + plugin working trees accumulating and accidentally committing reload scratch files. + */ + try { + mod = await import(pathToFileURL(reloadedPath).href); + } finally { + try { + await rm(reloadedPath, { force: true }); + } catch {} + } } else { mod = await import(moduleUrl); } diff --git a/plugins/fusion-plugin-droid-runtime/src/.index.reload-1.ts b/plugins/fusion-plugin-droid-runtime/src/.index.reload-1.ts deleted file mode 100644 index bbc307083b..0000000000 --- a/plugins/fusion-plugin-droid-runtime/src/.index.reload-1.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { definePlugin } from "@fusion/plugin-sdk"; -import type { FusionPlugin, PluginRuntimeFactory, PluginRuntimeManifestMetadata } from "@fusion/plugin-sdk"; -import { resolveCliSettings } from "./cli-spawn.js"; -import { DroidRuntimeAdapter } from "./runtime-adapter.js"; - -export const DROID_RUNTIME_ID = "droid"; -const DROID_RUNTIME_VERSION = "0.1.0"; - -export const droidRuntimeMetadata: PluginRuntimeManifestMetadata = { - runtimeId: DROID_RUNTIME_ID, - name: "Droid Runtime", - description: "Drives the Droid CLI for Fusion agents", - version: DROID_RUNTIME_VERSION, -}; - -export const droidRuntimeFactory: PluginRuntimeFactory = async (ctx) => - new DroidRuntimeAdapter(ctx.settings as Record | undefined); - -const plugin: FusionPlugin = definePlugin({ - manifest: { - id: "fusion-plugin-droid-runtime", - name: "Droid Runtime Plugin", - version: DROID_RUNTIME_VERSION, - description: "Drives the Droid CLI for Fusion agents", - runtime: droidRuntimeMetadata, - }, - state: "installed", - hooks: { - onLoad: (ctx) => { - const settings = resolveCliSettings(ctx.settings as Record); - ctx.logger.info(`Droid Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"}`); - }, - }, - uiSlots: [ - { - slotId: "settings-provider-card", - label: "Droid CLI Provider", - componentPath: "./components/settings-provider-card.js", - order: 10, - }, - { - slotId: "settings-integration-card", - label: "Droid CLI Integration", - componentPath: "./components/settings-integration-card.js", - order: 20, - }, - { - slotId: "onboarding-provider-card", - label: "Droid CLI Provider", - componentPath: "./components/onboarding-provider-card.js", - order: 10, - }, - { - slotId: "onboarding-setup-help", - label: "Droid CLI Setup Help", - componentPath: "./components/onboarding-setup-help.js", - order: 20, - }, - { - slotId: "post-onboarding-recommendation", - label: "Droid CLI Recommendation", - componentPath: "./components/post-onboarding-recommendation.js", - order: 10, - }, - ], - runtime: { - metadata: droidRuntimeMetadata, - factory: droidRuntimeFactory, - }, -}); - -export default plugin; -export { DroidRuntimeAdapter }; -export { probeDroidBinary } from "./probe.js"; -export type { DroidBinaryStatus } from "./probe.js"; -export { streamViaCli } from "./provider.js"; -export { - discoverDroidModels, - validateCliPresenceAsync, - validateCliAuthAsync, - killAllProcesses, -} from "./process-manager.js"; -export { getCustomToolDefs, toolsFromContext, writeMcpConfig } from "./mcp-config.js"; -export type { McpToolDef } from "./mcp-config.js"; diff --git a/plugins/fusion-plugin-droid-runtime/src/.index.reload-2.ts b/plugins/fusion-plugin-droid-runtime/src/.index.reload-2.ts deleted file mode 100644 index bbc307083b..0000000000 --- a/plugins/fusion-plugin-droid-runtime/src/.index.reload-2.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { definePlugin } from "@fusion/plugin-sdk"; -import type { FusionPlugin, PluginRuntimeFactory, PluginRuntimeManifestMetadata } from "@fusion/plugin-sdk"; -import { resolveCliSettings } from "./cli-spawn.js"; -import { DroidRuntimeAdapter } from "./runtime-adapter.js"; - -export const DROID_RUNTIME_ID = "droid"; -const DROID_RUNTIME_VERSION = "0.1.0"; - -export const droidRuntimeMetadata: PluginRuntimeManifestMetadata = { - runtimeId: DROID_RUNTIME_ID, - name: "Droid Runtime", - description: "Drives the Droid CLI for Fusion agents", - version: DROID_RUNTIME_VERSION, -}; - -export const droidRuntimeFactory: PluginRuntimeFactory = async (ctx) => - new DroidRuntimeAdapter(ctx.settings as Record | undefined); - -const plugin: FusionPlugin = definePlugin({ - manifest: { - id: "fusion-plugin-droid-runtime", - name: "Droid Runtime Plugin", - version: DROID_RUNTIME_VERSION, - description: "Drives the Droid CLI for Fusion agents", - runtime: droidRuntimeMetadata, - }, - state: "installed", - hooks: { - onLoad: (ctx) => { - const settings = resolveCliSettings(ctx.settings as Record); - ctx.logger.info(`Droid Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"}`); - }, - }, - uiSlots: [ - { - slotId: "settings-provider-card", - label: "Droid CLI Provider", - componentPath: "./components/settings-provider-card.js", - order: 10, - }, - { - slotId: "settings-integration-card", - label: "Droid CLI Integration", - componentPath: "./components/settings-integration-card.js", - order: 20, - }, - { - slotId: "onboarding-provider-card", - label: "Droid CLI Provider", - componentPath: "./components/onboarding-provider-card.js", - order: 10, - }, - { - slotId: "onboarding-setup-help", - label: "Droid CLI Setup Help", - componentPath: "./components/onboarding-setup-help.js", - order: 20, - }, - { - slotId: "post-onboarding-recommendation", - label: "Droid CLI Recommendation", - componentPath: "./components/post-onboarding-recommendation.js", - order: 10, - }, - ], - runtime: { - metadata: droidRuntimeMetadata, - factory: droidRuntimeFactory, - }, -}); - -export default plugin; -export { DroidRuntimeAdapter }; -export { probeDroidBinary } from "./probe.js"; -export type { DroidBinaryStatus } from "./probe.js"; -export { streamViaCli } from "./provider.js"; -export { - discoverDroidModels, - validateCliPresenceAsync, - validateCliAuthAsync, - killAllProcesses, -} from "./process-manager.js"; -export { getCustomToolDefs, toolsFromContext, writeMcpConfig } from "./mcp-config.js"; -export type { McpToolDef } from "./mcp-config.js"; diff --git a/plugins/fusion-plugin-hermes-runtime/src/.index.reload-1.ts b/plugins/fusion-plugin-hermes-runtime/src/.index.reload-1.ts deleted file mode 100644 index 9018f93712..0000000000 --- a/plugins/fusion-plugin-hermes-runtime/src/.index.reload-1.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Hermes Runtime Plugin - * - * Provides an executable Hermes runtime adapter that drives the local `hermes` - * CLI as a subprocess. Discovered by Fusion's plugin runtime registry; the - * settings configured in the dashboard's "Runtimes → Hermes" page flow through - * `ctx.settings` into the CLI invocation. - */ - -import { definePlugin } from "@fusion/plugin-sdk"; -import { resolveCliSettings } from "./cli-spawn.js"; -import { installFusionSkillIntoHermesHome } from "./fusion-skill-install.js"; -import { HermesRuntimeAdapter } from "./runtime-adapter.js"; -import type { - FusionPlugin, - PluginContext, - PluginRuntimeFactory, - PluginRuntimeManifestMetadata, -} from "@fusion/plugin-sdk"; - -// ── Hermes Runtime Metadata ─────────────────────────────────────────────────── - -const HERMES_RUNTIME_ID = "hermes"; -const HERMES_RUNTIME_VERSION = "0.2.0"; - -const hermesRuntimeMetadata: PluginRuntimeManifestMetadata = { - runtimeId: HERMES_RUNTIME_ID, - name: "Hermes Runtime", - description: "Drives the local `hermes` CLI (NousResearch/hermes-agent)", - version: HERMES_RUNTIME_VERSION, -}; - -// ── Hermes Runtime Factory ──────────────────────────────────────────────────── - -const hermesRuntimeFactory: PluginRuntimeFactory = async (ctx) => { - return new HermesRuntimeAdapter(ctx.settings as Record | undefined); -}; - -// ── Plugin Definition ───────────────────────────────────────────────────────── - -const plugin: FusionPlugin = definePlugin({ - manifest: { - id: "fusion-plugin-hermes-runtime", - name: "Hermes Runtime Plugin", - version: HERMES_RUNTIME_VERSION, - description: - "Drives the local `hermes` CLI for Fusion agents — captures session ids and resumes via --resume.", - author: "Fusion Team", - homepage: "https://github.com/NousResearch/hermes-agent", - runtime: hermesRuntimeMetadata, - }, - state: "installed", - hooks: { - onLoad: (ctx: PluginContext) => { - const settings = resolveCliSettings(ctx.settings); - const skillInstall = installFusionSkillIntoHermesHome({ profile: settings.profile }); - - if (skillInstall.outcome === "warning") { - ctx.logger.warn( - `Hermes Runtime Plugin: Fusion skill auto-install warning: ${skillInstall.reason ?? "unknown"}`, - ); - } else if (skillInstall.outcome === "skipped") { - ctx.logger.warn( - `Hermes Runtime Plugin: Fusion skill auto-install skipped: ${skillInstall.reason ?? "unknown"}`, - ); - } - - ctx.logger.info( - `Hermes Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"} fusionSkill=${skillInstall.outcome}`, - ); - ctx.emitEvent("hermes-runtime:loaded", { - runtimeId: HERMES_RUNTIME_ID, - version: HERMES_RUNTIME_VERSION, - }); - }, - onUnload: () => { - // No persistent state to clean up — each prompt spawns a fresh subprocess. - }, - }, - runtime: { - metadata: hermesRuntimeMetadata, - factory: hermesRuntimeFactory, - }, -}); - -export default plugin; - -// ── Public exports ──────────────────────────────────────────────────────────── - -export { hermesRuntimeMetadata, hermesRuntimeFactory, HERMES_RUNTIME_ID }; -export { HermesRuntimeAdapter } from "./runtime-adapter.js"; -export { - resolveCliSettings, - invokeHermesCli, - buildHermesArgs, - parseHermesOutput, - listHermesProfiles, -} from "./cli-spawn.js"; -export { - installFusionSkillIntoHermesHome, - resolveBundledFusionSkillSource, - resolveHermesHome, -} from "./fusion-skill-install.js"; -export type { HermesCliSettings, HermesCliResult, HermesProfileSummary } from "./cli-spawn.js"; - -// Probe re-export for the dashboard's runtime-provider-probes façade. -export { probeHermesBinary } from "./probe.js"; -export type { HermesBinaryStatus } from "./probe.js"; diff --git a/plugins/fusion-plugin-hermes-runtime/src/.index.reload-2.ts b/plugins/fusion-plugin-hermes-runtime/src/.index.reload-2.ts deleted file mode 100644 index 9018f93712..0000000000 --- a/plugins/fusion-plugin-hermes-runtime/src/.index.reload-2.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Hermes Runtime Plugin - * - * Provides an executable Hermes runtime adapter that drives the local `hermes` - * CLI as a subprocess. Discovered by Fusion's plugin runtime registry; the - * settings configured in the dashboard's "Runtimes → Hermes" page flow through - * `ctx.settings` into the CLI invocation. - */ - -import { definePlugin } from "@fusion/plugin-sdk"; -import { resolveCliSettings } from "./cli-spawn.js"; -import { installFusionSkillIntoHermesHome } from "./fusion-skill-install.js"; -import { HermesRuntimeAdapter } from "./runtime-adapter.js"; -import type { - FusionPlugin, - PluginContext, - PluginRuntimeFactory, - PluginRuntimeManifestMetadata, -} from "@fusion/plugin-sdk"; - -// ── Hermes Runtime Metadata ─────────────────────────────────────────────────── - -const HERMES_RUNTIME_ID = "hermes"; -const HERMES_RUNTIME_VERSION = "0.2.0"; - -const hermesRuntimeMetadata: PluginRuntimeManifestMetadata = { - runtimeId: HERMES_RUNTIME_ID, - name: "Hermes Runtime", - description: "Drives the local `hermes` CLI (NousResearch/hermes-agent)", - version: HERMES_RUNTIME_VERSION, -}; - -// ── Hermes Runtime Factory ──────────────────────────────────────────────────── - -const hermesRuntimeFactory: PluginRuntimeFactory = async (ctx) => { - return new HermesRuntimeAdapter(ctx.settings as Record | undefined); -}; - -// ── Plugin Definition ───────────────────────────────────────────────────────── - -const plugin: FusionPlugin = definePlugin({ - manifest: { - id: "fusion-plugin-hermes-runtime", - name: "Hermes Runtime Plugin", - version: HERMES_RUNTIME_VERSION, - description: - "Drives the local `hermes` CLI for Fusion agents — captures session ids and resumes via --resume.", - author: "Fusion Team", - homepage: "https://github.com/NousResearch/hermes-agent", - runtime: hermesRuntimeMetadata, - }, - state: "installed", - hooks: { - onLoad: (ctx: PluginContext) => { - const settings = resolveCliSettings(ctx.settings); - const skillInstall = installFusionSkillIntoHermesHome({ profile: settings.profile }); - - if (skillInstall.outcome === "warning") { - ctx.logger.warn( - `Hermes Runtime Plugin: Fusion skill auto-install warning: ${skillInstall.reason ?? "unknown"}`, - ); - } else if (skillInstall.outcome === "skipped") { - ctx.logger.warn( - `Hermes Runtime Plugin: Fusion skill auto-install skipped: ${skillInstall.reason ?? "unknown"}`, - ); - } - - ctx.logger.info( - `Hermes Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"} fusionSkill=${skillInstall.outcome}`, - ); - ctx.emitEvent("hermes-runtime:loaded", { - runtimeId: HERMES_RUNTIME_ID, - version: HERMES_RUNTIME_VERSION, - }); - }, - onUnload: () => { - // No persistent state to clean up — each prompt spawns a fresh subprocess. - }, - }, - runtime: { - metadata: hermesRuntimeMetadata, - factory: hermesRuntimeFactory, - }, -}); - -export default plugin; - -// ── Public exports ──────────────────────────────────────────────────────────── - -export { hermesRuntimeMetadata, hermesRuntimeFactory, HERMES_RUNTIME_ID }; -export { HermesRuntimeAdapter } from "./runtime-adapter.js"; -export { - resolveCliSettings, - invokeHermesCli, - buildHermesArgs, - parseHermesOutput, - listHermesProfiles, -} from "./cli-spawn.js"; -export { - installFusionSkillIntoHermesHome, - resolveBundledFusionSkillSource, - resolveHermesHome, -} from "./fusion-skill-install.js"; -export type { HermesCliSettings, HermesCliResult, HermesProfileSummary } from "./cli-spawn.js"; - -// Probe re-export for the dashboard's runtime-provider-probes façade. -export { probeHermesBinary } from "./probe.js"; -export type { HermesBinaryStatus } from "./probe.js"; diff --git a/plugins/fusion-plugin-hermes-runtime/src/.index.reload-3.ts b/plugins/fusion-plugin-hermes-runtime/src/.index.reload-3.ts deleted file mode 100644 index 9018f93712..0000000000 --- a/plugins/fusion-plugin-hermes-runtime/src/.index.reload-3.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Hermes Runtime Plugin - * - * Provides an executable Hermes runtime adapter that drives the local `hermes` - * CLI as a subprocess. Discovered by Fusion's plugin runtime registry; the - * settings configured in the dashboard's "Runtimes → Hermes" page flow through - * `ctx.settings` into the CLI invocation. - */ - -import { definePlugin } from "@fusion/plugin-sdk"; -import { resolveCliSettings } from "./cli-spawn.js"; -import { installFusionSkillIntoHermesHome } from "./fusion-skill-install.js"; -import { HermesRuntimeAdapter } from "./runtime-adapter.js"; -import type { - FusionPlugin, - PluginContext, - PluginRuntimeFactory, - PluginRuntimeManifestMetadata, -} from "@fusion/plugin-sdk"; - -// ── Hermes Runtime Metadata ─────────────────────────────────────────────────── - -const HERMES_RUNTIME_ID = "hermes"; -const HERMES_RUNTIME_VERSION = "0.2.0"; - -const hermesRuntimeMetadata: PluginRuntimeManifestMetadata = { - runtimeId: HERMES_RUNTIME_ID, - name: "Hermes Runtime", - description: "Drives the local `hermes` CLI (NousResearch/hermes-agent)", - version: HERMES_RUNTIME_VERSION, -}; - -// ── Hermes Runtime Factory ──────────────────────────────────────────────────── - -const hermesRuntimeFactory: PluginRuntimeFactory = async (ctx) => { - return new HermesRuntimeAdapter(ctx.settings as Record | undefined); -}; - -// ── Plugin Definition ───────────────────────────────────────────────────────── - -const plugin: FusionPlugin = definePlugin({ - manifest: { - id: "fusion-plugin-hermes-runtime", - name: "Hermes Runtime Plugin", - version: HERMES_RUNTIME_VERSION, - description: - "Drives the local `hermes` CLI for Fusion agents — captures session ids and resumes via --resume.", - author: "Fusion Team", - homepage: "https://github.com/NousResearch/hermes-agent", - runtime: hermesRuntimeMetadata, - }, - state: "installed", - hooks: { - onLoad: (ctx: PluginContext) => { - const settings = resolveCliSettings(ctx.settings); - const skillInstall = installFusionSkillIntoHermesHome({ profile: settings.profile }); - - if (skillInstall.outcome === "warning") { - ctx.logger.warn( - `Hermes Runtime Plugin: Fusion skill auto-install warning: ${skillInstall.reason ?? "unknown"}`, - ); - } else if (skillInstall.outcome === "skipped") { - ctx.logger.warn( - `Hermes Runtime Plugin: Fusion skill auto-install skipped: ${skillInstall.reason ?? "unknown"}`, - ); - } - - ctx.logger.info( - `Hermes Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"} fusionSkill=${skillInstall.outcome}`, - ); - ctx.emitEvent("hermes-runtime:loaded", { - runtimeId: HERMES_RUNTIME_ID, - version: HERMES_RUNTIME_VERSION, - }); - }, - onUnload: () => { - // No persistent state to clean up — each prompt spawns a fresh subprocess. - }, - }, - runtime: { - metadata: hermesRuntimeMetadata, - factory: hermesRuntimeFactory, - }, -}); - -export default plugin; - -// ── Public exports ──────────────────────────────────────────────────────────── - -export { hermesRuntimeMetadata, hermesRuntimeFactory, HERMES_RUNTIME_ID }; -export { HermesRuntimeAdapter } from "./runtime-adapter.js"; -export { - resolveCliSettings, - invokeHermesCli, - buildHermesArgs, - parseHermesOutput, - listHermesProfiles, -} from "./cli-spawn.js"; -export { - installFusionSkillIntoHermesHome, - resolveBundledFusionSkillSource, - resolveHermesHome, -} from "./fusion-skill-install.js"; -export type { HermesCliSettings, HermesCliResult, HermesProfileSummary } from "./cli-spawn.js"; - -// Probe re-export for the dashboard's runtime-provider-probes façade. -export { probeHermesBinary } from "./probe.js"; -export type { HermesBinaryStatus } from "./probe.js"; diff --git a/plugins/fusion-plugin-openclaw-runtime/src/.index.reload-1.ts b/plugins/fusion-plugin-openclaw-runtime/src/.index.reload-1.ts deleted file mode 100644 index 09745e7b06..0000000000 --- a/plugins/fusion-plugin-openclaw-runtime/src/.index.reload-1.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * OpenClaw Runtime Plugin - * - * Drives the local `openclaw` CLI as a subprocess (via - * `openclaw --no-color agent --local --json`). No daemon required. - */ - -import { definePlugin } from "@fusion/plugin-sdk"; -import { OpenClawRuntimeAdapter } from "./runtime-adapter.js"; -import { resolveCliConfig } from "./pi-module.js"; -import { probeOpenClawBinary } from "./probe.js"; -import type { - FusionPlugin, - PluginContext, - PluginRuntimeFactory, - PluginRuntimeManifestMetadata, -} from "@fusion/plugin-sdk"; - -const OPENCLAW_RUNTIME_ID = "openclaw"; -const OPENCLAW_RUNTIME_VERSION = "0.2.0"; - -const openclawRuntimeMetadata: PluginRuntimeManifestMetadata = { - runtimeId: OPENCLAW_RUNTIME_ID, - name: "OpenClaw Runtime", - description: "Drives the local `openclaw` CLI (openclaw/openclaw)", - version: OPENCLAW_RUNTIME_VERSION, -}; - -const openclawRuntimeFactory: PluginRuntimeFactory = async (ctx?: PluginContext) => { - return new OpenClawRuntimeAdapter(ctx?.settings as Record | undefined); -}; - -const plugin: FusionPlugin = definePlugin({ - manifest: { - id: "fusion-plugin-openclaw-runtime", - name: "OpenClaw Runtime Plugin", - version: OPENCLAW_RUNTIME_VERSION, - description: - "Drives the local `openclaw` CLI for Fusion agents — embedded `--local` mode by default; gateway optional.", - author: "Fusion Team", - homepage: "https://docs.openclaw.ai/", - runtime: openclawRuntimeMetadata, - }, - state: "installed", - hooks: { - onLoad: async (ctx: PluginContext) => { - const config = resolveCliConfig(ctx.settings); - const probe = await probeOpenClawBinary({ binaryPath: config.binaryPath }); - - ctx.logger.info( - probe.available - ? `OpenClaw Runtime Plugin loaded — binary=${config.binaryPath}${probe.version ? ` (${probe.version})` : ""}` - : `OpenClaw Runtime Plugin loaded but binary not detected: ${probe.reason ?? "unknown"}`, - ); - ctx.emitEvent("openclaw-runtime:loaded", { - runtimeId: OPENCLAW_RUNTIME_ID, - version: OPENCLAW_RUNTIME_VERSION, - binaryAvailable: probe.available, - binaryPath: probe.binaryPath ?? config.binaryPath, - }); - }, - onUnload: () => { - // No persistent state to clean up — each prompt spawns a fresh subprocess. - }, - }, - runtime: { - metadata: openclawRuntimeMetadata, - factory: openclawRuntimeFactory, - }, -}); - -export default plugin; - -// ── Public exports ──────────────────────────────────────────────────────────── - -export { openclawRuntimeMetadata, openclawRuntimeFactory, OPENCLAW_RUNTIME_ID }; -export { OpenClawRuntimeAdapter } from "./runtime-adapter.js"; -export { - resolveCliConfig, - buildOpenClawArgs, - createCliSession, - promptCli, - describeCliModel, - extractStderrError, - configureOpenClawMcpServer, -} from "./pi-module.js"; -export type { CliConfig, GatewaySession, OpenClawAgentJson } from "./types.js"; -export { - toolsToMcpToolDefs, - writeOpenClawMcpBridgeFiles, -} from "./mcp-config.js"; - -// Probe re-export for the dashboard's runtime-provider-probes façade. -export { probeOpenClawBinary } from "./probe.js"; -export type { OpenClawBinaryStatus } from "./probe.js"; diff --git a/plugins/fusion-plugin-paperclip-runtime/src/.index.reload-1.ts b/plugins/fusion-plugin-paperclip-runtime/src/.index.reload-1.ts deleted file mode 100644 index 895c9a46b7..0000000000 --- a/plugins/fusion-plugin-paperclip-runtime/src/.index.reload-1.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { definePlugin } from "@fusion/plugin-sdk"; -import type { PluginContext } from "@fusion/plugin-sdk"; -import { - probePaperclipConnection, - resolvePaperclipConfig, -} from "./paperclip-client.js"; -import { PaperclipRuntimeAdapter } from "./runtime-adapter.js"; -import type { - FusionPlugin, - PluginRuntimeRegistration, - RuntimeLogger, -} from "./types.js"; - -// Public exports — consumed by the dashboard probe façade and tests. -export type { - PaperclipAgentSummary, - PaperclipCliDiscovery, - PaperclipCliDiscoveryResult, - PaperclipCompanySummary, - PaperclipConnectionStatus, -} from "./paperclip-client.js"; -export { - agentsMe, - agentsMeViaCli, - createIssueViaCli, - discoverPaperclipCliConfig, - getIssueViaCli, - listCompanies, - listCompaniesViaCli, - listCompanyAgents, - listCompanyAgentsViaCli, - mintAgentApiKeyViaCli, - probePaperclipConnection, - probePaperclipViaCli, -} from "./paperclip-client.js"; -export type { MintCliKeyOptions, MintedApiKey } from "./paperclip-client.js"; -export { PaperclipRuntimeAdapter } from "./runtime-adapter.js"; - -function getSettingsConfig(settings: unknown) { - return resolvePaperclipConfig((settings ?? {}) as Record); -} - -async function paperclipRuntimeFactory(ctx: { - settings?: unknown; - logger?: RuntimeLogger; -}): Promise { - const config = getSettingsConfig(ctx.settings); - // resolvePaperclipConfig returns `mode: string`; the adapter narrows it. - return new PaperclipRuntimeAdapter( - config as unknown as Record, - ctx.logger, - ); -} - -const paperclipRuntime: PluginRuntimeRegistration = { - metadata: { - runtimeId: "paperclip", - name: "Paperclip Runtime", - description: "Drives a Paperclip agent via the wakeup + heartbeat-run REST API", - version: "1.0.0", - }, - factory: paperclipRuntimeFactory, -}; - -const plugin: FusionPlugin = definePlugin({ - manifest: { - id: "fusion-plugin-paperclip-runtime", - name: "Paperclip Runtime Plugin", - version: "1.0.0", - description: "Drives a Paperclip agent via the wakeup + heartbeat-run REST API", - author: "Fusion Team", - homepage: "https://paperclip.ing/", - fusionVersion: ">=0.1.0", - runtime: { - runtimeId: "paperclip", - name: "Paperclip Runtime", - description: "Drives a Paperclip agent via the wakeup + heartbeat-run REST API", - version: "1.0.0", - }, - }, - state: "installed", - runtime: paperclipRuntime, - hooks: { - onLoad: async (ctx: PluginContext) => { - const config = getSettingsConfig(ctx.settings); - ctx.logger.info(`Paperclip Runtime Plugin loaded (apiUrl=${config.apiUrl})`); - - // Best-effort connectivity probe; failures are warnings, not errors. - try { - const status = await probePaperclipConnection({ - apiUrl: config.apiUrl, - apiKey: config.apiKey, - }); - if (status.available) { - const ident = status.identity; - ctx.logger.info( - ident - ? `Paperclip reachable as ${ident.agentName} (${ident.role ?? "agent"}) at ${ident.companyName ?? ident.companyId}` - : `Paperclip reachable at ${config.apiUrl}`, - ); - } else { - ctx.logger.warn(`Paperclip probe failed: ${status.reason ?? "unknown"}`); - } - } catch (error) { - const reason = error instanceof Error ? error.message : String(error); - ctx.logger.warn(`Paperclip probe threw: ${reason}`); - } - }, - }, -}); - -export default plugin; diff --git a/scripts/__tests__/no-tracked-plugin-reload-artifacts.test.mjs b/scripts/__tests__/no-tracked-plugin-reload-artifacts.test.mjs new file mode 100644 index 0000000000..4ef0689a8a --- /dev/null +++ b/scripts/__tests__/no-tracked-plugin-reload-artifacts.test.mjs @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); + +/* +FNXC:PluginLoader 2026-08-11-09:58: +FN-8990 requires reload scratch copies to remain untracked. Gitignore cannot +remove a path already in git's index, so inspect tracked files directly. +*/ +function getTrackedReloadArtifacts() { + return execFileSync("git", ["ls-files"], { cwd: rootDir, encoding: "utf8" }) + .split("\n") + .filter((path) => /\.index\.reload-\d+\.ts$/.test(path)); +} + +test("plugin hot-reload scratch copies are never tracked", () => { + const artifacts = getTrackedReloadArtifacts(); + assert.deepEqual( + artifacts, + [], + `Tracked plugin hot-reload scratch copies:\n${artifacts.map((path) => `- ${path}`).join("\n")}`, + ); +});