diff --git a/.changeset/fn-6410-standalone-pi-dependency.md b/.changeset/fn-6410-standalone-pi-dependency.md new file mode 100644 index 0000000000..b60ad4bda9 --- /dev/null +++ b/.changeset/fn-6410-standalone-pi-dependency.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix standalone installs of the published CLI crashing with `ERR_MODULE_NOT_FOUND` for `@earendil-works/pi-coding-agent`. `@earendil-works/pi-coding-agent` and `@earendil-works/pi-ai` are now plain required dependencies instead of also being optional peers, so clean npm and pnpm installs resolve the pi runtime packages. diff --git a/packages/cli/package.json b/packages/cli/package.json index 560b4de631..de1c572cee 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -74,17 +74,9 @@ "ws": "^8.18.0" }, "peerDependencies": { - "@earendil-works/pi-ai": "*", - "@earendil-works/pi-coding-agent": "*", "typebox": "*" }, "peerDependenciesMeta": { - "@earendil-works/pi-ai": { - "optional": true - }, - "@earendil-works/pi-coding-agent": { - "optional": true - }, "typebox": { "optional": true } diff --git a/packages/cli/src/__tests__/package-config.test.ts b/packages/cli/src/__tests__/package-config.test.ts index 8e318a504b..21ce456497 100644 --- a/packages/cli/src/__tests__/package-config.test.ts +++ b/packages/cli/src/__tests__/package-config.test.ts @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { builtinModules } from "node:module"; import { parse } from "yaml"; +import { applyPrepackTransform } from "../../scripts/prepare-publish-manifest.mjs"; const workspaceRoot = join(__dirname, "..", "..", "..", ".."); @@ -32,6 +33,43 @@ function hasProjectArg(script: string | undefined, project: string): boolean { return parts.some((part, index) => part === "--project" && parts[index + 1] === project); } +function assertRuntimeDepsAreNotOptionalPeers(pkg: any, label: string): void { + const dependencies = pkg.dependencies ?? {}; + const peerDependencies = pkg.peerDependencies ?? {}; + const peerDependenciesMeta = pkg.peerDependenciesMeta ?? {}; + + for (const dependencyName of Object.keys(dependencies)) { + expect( + peerDependenciesMeta[dependencyName]?.optional, + `${label}: runtime dependency "${dependencyName}" must not also be an optional peer; npm/pnpm may omit it from clean standalone installs.`, + ).not.toBe(true); + } + + for (const dependencyName of ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai"]) { + expect(dependencies, `${label}: ${dependencyName} must remain a required runtime dependency`).toHaveProperty( + dependencyName, + "^0.79.1", + ); + expect(peerDependencies, `${label}: ${dependencyName} must not be a peer dependency`).not.toHaveProperty( + dependencyName, + ); + expect(peerDependenciesMeta, `${label}: ${dependencyName} must not have peer metadata`).not.toHaveProperty( + dependencyName, + ); + } + + expect(dependencies, `${label}: typebox must not be promoted into runtime dependencies`).not.toHaveProperty( + "typebox", + ); + expect(peerDependencies, `${label}: typebox remains the optional peer control`).toHaveProperty( + "typebox", + "*", + ); + expect(peerDependenciesMeta.typebox, `${label}: typebox remains optional peer metadata`).toEqual({ + optional: true, + }); +} + describe("CLI package.json publishing config", () => { const pkg = loadPackageJson("cli"); const prepackScript = loadCliPrepackScript(); @@ -93,6 +131,15 @@ describe("CLI package.json publishing config", () => { expect(deps).toContain("ioredis"); }); + /** + * FNXC:Packaging 2026-06-13-16:36: + * Standalone npm/pnpm installs may omit a package when the published manifest declares it as both a runtime dependency and an optional peer. Keep the pi runtime packages as plain dependencies so dist/bin.js and dist/extension.js can resolve their static imports outside the monorepo, while leaving typebox as the optional-peer control because Fusion does not import it at runtime. + */ + it("does not declare runtime dependencies as optional peers in source or published manifests", () => { + assertRuntimeDepsAreNotOptionalPeers(pkg, "source manifest"); + assertRuntimeDepsAreNotOptionalPeers(applyPrepackTransform(pkg), "published manifest"); + }); + it("defines test:docs-index as a single-file docs README index lane", () => { const script = pkg.scripts?.["test:docs-index"]; const parts = script?.trim().split(/\s+/) ?? [];