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:
7
.changeset/fn-7955-ce-skills-published.md
Normal file
7
.changeset/fn-7955-ce-skills-published.md
Normal 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.
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user