FN-7955: stage bundled plugin skills

Ensure bundled Compound Engineering skills are present in published CLI packages.

- Copy plugin src/skills directories into dist/plugins/<id>/skills during CLI packaging.
- Add bundle-output coverage that verifies Compound Engineering SKILL.md files stage and resolve from the plugin root.
- Document runtime-read bundled plugin asset staging and add a patch changeset for @runfusion/fusion.

Files changed:
 .changeset/fn-7955-ce-skills-published.md        |  7 ++++
 docs/PLUGIN_AUTHORING.md                         |  3 ++
 packages/cli/src/__tests__/bundle-output.test.ts | 51 ++++++++++++++++++++++++
 packages/cli/tsup.config.ts                      | 14 +++++++
 4 files changed, 75 insertions(+)

Fusion-Task-Id: FN-7955

Fusion-Task-Lineage: 32c4ad31-4f3a-478b-996f-ce6bcafd1e27

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-14 17:29:16 -07:00
parent d0ce7829c0
commit 9bdbdc5f16
4 changed files with 75 additions and 0 deletions

View File

@@ -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/<id>/skills during bundlePluginEntry() for #2094 / FN-7955.

View File

@@ -770,6 +770,9 @@ Bundled workspace plugin pattern:
<!-- FNXC:BundledPlugins 2026-07-13-00:00: FN-7936 requires `@runfusion/fusion` bundled plugin backend outputs to be install-self-contained. The CLI bundler resolves plugin-sdk runtime re-exports from `@fusion/core` through `plugin-sdk-core-runtime-shim.ts`, so `packages/cli/dist/plugins/<id>/bundled.js` must not ship private `@fusion/*` runtime specifiers that npm installs cannot resolve. -->
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.
<!-- FNXC:BundledPlugins 2026-07-14-12:00: FN-7955 requires runtime-read bundled plugin assets to be self-contained too. esbuild only inlines statically imported code, so package-local files read from disk, including Compound Engineering `src/skills/<id>/SKILL.md` bodies, must be copied by `bundlePluginEntry()` into the matching `packages/cli/dist/plugins/<id>/skills/` tree before publishing. -->
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/<id>/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
<!-- FNXC:BundledPlugins 2026-06-17-22:31: Bundled plugins can load gitignored compiled artifacts before source during workspace/dev resolution, so plugin authors need a documented recovery path when the generic freshness guard detects stale dist output. -->

View File

@@ -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[] = [];

View File

@@ -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/<id>/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)) {