diff --git a/.changeset/fn-7955-ce-skills-published.md b/.changeset/fn-7955-ce-skills-published.md new file mode 100644 index 0000000000..b05d8257b7 --- /dev/null +++ b/.changeset/fn-7955-ce-skills-published.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Compound Engineering plugin skills missing from the published package. +category: fix +dev: Stages bundled plugin src/skills into dist/plugins//skills during bundlePluginEntry() for #2094 / FN-7955. diff --git a/docs/PLUGIN_AUTHORING.md b/docs/PLUGIN_AUTHORING.md index 060bb4dece..ee1022de9f 100644 --- a/docs/PLUGIN_AUTHORING.md +++ b/docs/PLUGIN_AUTHORING.md @@ -770,6 +770,9 @@ Bundled workspace plugin pattern: Bundled `bundled.js` outputs must be self-contained at runtime. Do not leave private workspace package imports such as `@fusion/core` in emitted bundled plugin code; `@fusion/plugin-sdk` core runtime re-exports are resolved through the CLI runtime shim during packaging. + +If a bundled plugin reads package-local files at runtime, stage those assets explicitly during CLI packaging. `bundlePluginEntry()` copies any committed `src/skills/` directory into `dist/plugins//skills/`; use the same pattern for similar runtime-read assets instead of assuming esbuild will include files that are never imported. + ### Bundled plugin build-freshness guard diff --git a/packages/cli/src/__tests__/bundle-output.test.ts b/packages/cli/src/__tests__/bundle-output.test.ts index 58ad6d3957..ee5f094b2a 100644 --- a/packages/cli/src/__tests__/bundle-output.test.ts +++ b/packages/cli/src/__tests__/bundle-output.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, beforeAll } from "vitest"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; +import { resolvePluginSkillBodyPath } from "@fusion/core"; import { buildCliWithRealDashboardAssets, bundlePath, @@ -9,6 +10,7 @@ import { clientIndexPath, dashboardClientStubMarker, readClientIndexHtml, + workspaceRoot, } from "./bundle-output-helpers"; import { resolveClaudeCliExtensionFromModuleUrl } from "../commands/claude-cli-extension"; import { resolveDroidCliExtensionFromModuleUrl } from "../commands/droid-cli-extension"; @@ -22,6 +24,20 @@ const bundlePluginEntryPluginIds = [ "fusion-plugin-compound-engineering", "fusion-plugin-linear-import", ] as const; +const knownCompoundEngineeringSkillIds = [ + "ce-brainstorm", + "ce-code-review", + "ce-commit", + "ce-commit-push-pr", + "ce-compound", + "ce-debug", + "ce-doc-review", + "ce-ideate", + "ce-plan", + "ce-resolve-pr-feedback", + "ce-strategy", + "ce-work", +] as const; describe("CLI bundle output", () => { beforeAll(() => { @@ -230,6 +246,41 @@ describe("CLI bundle output", () => { expect(stagedPkg.dependencies?.["@fusion/core"]).toBeUndefined(); }); + it("dist/plugins/fusion-plugin-compound-engineering/ ships skill bodies that resolve from plugin root", () => { + const sourceSkillsRoot = join(workspaceRoot, "plugins", "fusion-plugin-compound-engineering", "src", "skills"); + const stagedPluginRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-compound-engineering"); + const skillIds = readdirSync(sourceSkillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + for (const knownSkillId of knownCompoundEngineeringSkillIds) { + expect(skillIds).toContain(knownSkillId); + } + + for (const skillId of skillIds) { + const stagedSkillPath = join(stagedPluginRoot, "skills", skillId, "SKILL.md"); + expect(existsSync(stagedSkillPath), `${skillId} SKILL.md should be staged`).toBe(true); + expect( + readFileSync(stagedSkillPath, "utf-8").trim().length, + `${skillId} SKILL.md should be non-empty`, + ).toBeGreaterThan(0); + + const resolvedSkillBody = resolvePluginSkillBodyPath( + { name: skillId, skillFiles: [`skills/${skillId}/SKILL.md`] }, + stagedPluginRoot, + ); + expect(existsSync(resolvedSkillBody.absolutePath), `${skillId} should resolve via plugin skillFiles`).toBe(true); + } + }); + + it("does not create skills directories for bundled plugins without skill sources", () => { + const pluginId = "fusion-plugin-roadmap"; + + expect(existsSync(join(workspaceRoot, "plugins", pluginId, "src", "skills"))).toBe(false); + expect(existsSync(join(cliRoot, "dist", "plugins", pluginId, "skills"))).toBe(false); + }); + it("bundled plugin outputs do not import private @fusion/core at runtime", () => { const inspectedPluginIds: string[] = []; diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index c54919781c..0f41b46d8c 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -196,6 +196,20 @@ async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = fal logLevel: "warning", }); + const skillsSourceDir = join(srcDir, "src", "skills"); + if (existsSync(skillsSourceDir)) { + const skillsDestDir = join(destDir, "skills"); + /* + * FNXC:BundledPlugins 2026-07-14-12:00: + * FN-7955 / issue #2094 requires plugin-local runtime-read assets to ship with @runfusion/fusion. esbuild bundle:true only inlines statically imported JS/TS, so files read from disk through resolveBundledSkillsRoot() or PluginSkillContribution.skillFiles, such as nested SKILL.md files under src/skills, must be explicitly staged into dist/plugins//skills/ or the published npm tarball silently contains zero skill bodies. + */ + cpSync(skillsSourceDir, skillsDestDir, { recursive: true }); + if (!existsSync(skillsDestDir)) { + throw new Error(`[tsup] Missing staged skills for ${pluginId}: expected ${skillsDestDir}`); + } + console.log(`Staged plugin skills for ${pluginId} to dist/plugins/${pluginId}/skills`); + } + if (withMcpAsset) { const mcpServerAsset = join(srcDir, "src", "mcp-schema-server.cjs"); if (!existsSync(mcpServerAsset)) {