From abd596b700a047f9d68f85d650798ad7e9bbf996 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 1 Jul 2026 20:40:02 -0700 Subject: [PATCH] fix: pack full desktop dependency closure + resolve bun/desktop plugin loading on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Desktop "Local" mode crashed with ERR_MODULE_NOT_FOUND because electron-builder's pnpm collector drops `deduped` subtrees, so engine's transitive closure (@modelcontextprotocol/sdk, pi-ai provider SDKs, etc.) was never packed. Stage the complete flat prod closure with `pnpm deploy --legacy --config.node-linker=hoisted` and package it via `electron-builder --projectDir deploy`, bypassing the lossy collector entirely. Also make the dashboard-imported example plugins loadable under plain Node (the Electron main runtime): cursor/droid/roadmap now expose compiled `dist` on the `import` condition (keeping `source`→src for the bun CLI) and are built during the desktop build. Add `source` conditions to paperclip/agent-browser/even-cards/ even-realities-glasses/whatsapp-chat so the bun `--conditions=source` Windows CLI compile resolves them from source. Validated on macOS: @fusion/core|engine|dashboard import cleanly from the staged deploy; packing yields a complete 705-package asar; bun-windows-x64 cross-compiles with all plugin dist removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../fix-bun-windows-remaining-plugins.md | 7 ++ ...x-desktop-local-full-dependency-closure.md | 7 ++ .github/workflows/desktop-windows.yml | 4 +- .github/workflows/release.yml | 8 +- .gitignore | 3 + packages/desktop/package.json | 10 +-- packages/desktop/scripts/build.ts | 8 +- packages/desktop/scripts/workspace-tools.ts | 76 ++++++++++++++++++- .../fusion-plugin-agent-browser/package.json | 1 + .../fusion-plugin-cursor-runtime/package.json | 6 +- .../fusion-plugin-droid-runtime/package.json | 6 +- plugins/fusion-plugin-even-cards/package.json | 1 + .../package.json | 1 + .../package.json | 1 + plugins/fusion-plugin-roadmap/package.json | 9 ++- .../fusion-plugin-whatsapp-chat/package.json | 1 + 16 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 .changeset/fix-bun-windows-remaining-plugins.md create mode 100644 .changeset/fix-desktop-local-full-dependency-closure.md diff --git a/.changeset/fix-bun-windows-remaining-plugins.md b/.changeset/fix-bun-windows-remaining-plugins.md new file mode 100644 index 0000000000..b443f25f24 --- /dev/null +++ b/.changeset/fix-bun-windows-remaining-plugins.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix the Windows CLI binary failing to build in release. +category: fix +dev: The bun `--compile --conditions=source` build could not resolve @fusion-plugin-examples/paperclip-runtime (statically imported by dashboard runtime-provider-probes.ts) because it lacked a `source` export condition and fell through to `import`→`dist/index.js`, absent on the Windows runner. Added `"source": "./src/index.ts"` to paperclip and the remaining example plugins that export `import`→dist (agent-browser, even-cards, even-realities-glasses, whatsapp-chat), matching hermes/openclaw. Verified by cross-compiling bun-windows-x64 with all plugin dist removed. diff --git a/.changeset/fix-desktop-local-full-dependency-closure.md b/.changeset/fix-desktop-local-full-dependency-closure.md new file mode 100644 index 0000000000..709708d10d --- /dev/null +++ b/.changeset/fix-desktop-local-full-dependency-closure.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix the desktop app crashing on "Local" mode with missing-module errors. +category: fix +dev: electron-builder's pnpm support runs `pnpm list --prod` and drops `deduped` subtrees, so the embedded runtime's `import("@fusion/engine")` closure (@modelcontextprotocol/sdk, the pi-ai provider SDKs, etc.) was never packed into app.asar. The desktop build now stages the complete flat production closure with `pnpm deploy --legacy --config.node-linker=hoisted` and points electron-builder at it via `--projectDir deploy`, so packaging no longer depends on the lossy collector. Also fixes cursor/droid/roadmap plugin exports to expose compiled `dist` on the `import` condition (with `source`→src kept for the bun CLI) so the dashboard server loads them under plain Node, and builds those plugins in the desktop build. Validated by importing @fusion/core|engine|dashboard from the staged deploy and packing a complete 705-package asar. diff --git a/.github/workflows/desktop-windows.yml b/.github/workflows/desktop-windows.yml index 759497c6b3..251dd6f9d7 100644 --- a/.github/workflows/desktop-windows.yml +++ b/.github/workflows/desktop-windows.yml @@ -38,7 +38,7 @@ jobs: # Code-signing hardening is intentionally deferred to FN-5592. - name: Package signed Windows EXE if: ${{ env.WINDOWS_CERTIFICATE_BASE64 != '' }} - run: pnpm --filter @fusion/desktop exec electron-builder --win --x64 --arm64 --publish never + run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --x64 --arm64 --publish never env: WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }} CSC_LINK: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }} @@ -48,7 +48,7 @@ jobs: - name: Package unsigned Windows EXE if: ${{ env.WINDOWS_CERTIFICATE_BASE64 == '' }} - run: pnpm --filter @fusion/desktop exec electron-builder --win --x64 --arm64 --publish never + run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --x64 --arm64 --publish never env: WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }} CSC_IDENTITY_AUTO_DISCOVERY: "false" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3343e945c..2451ce510d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,7 +143,7 @@ jobs: # Use `exec electron-builder` rather than the `dist:win` script: pnpm leaks # the `--` separator into script args (electron-builder then stops parsing # at `--` and ignores `--publish never`, auto-publishing to the wrong repo). - run: pnpm --filter @fusion/desktop exec electron-builder --win --publish never + run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --publish never env: CSC_IDENTITY_AUTO_DISCOVERY: "false" GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -196,7 +196,7 @@ jobs: - name: Package signed macOS desktop DMG/ZIP if: ${{ env.APPLE_CERTIFICATE_BASE64 != '' }} - run: pnpm --filter @fusion/desktop exec electron-builder --mac --publish never + run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --mac --publish never env: APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} @@ -209,7 +209,7 @@ jobs: - name: Package unsigned macOS desktop DMG/ZIP if: ${{ env.APPLE_CERTIFICATE_BASE64 == '' }} - run: pnpm --filter @fusion/desktop exec electron-builder --mac --publish never -c.mac.notarize=false + run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --mac --publish never -c.mac.notarize=false env: APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} CSC_IDENTITY_AUTO_DISCOVERY: "false" @@ -295,7 +295,7 @@ jobs: - name: Package Linux desktop artifacts # Linux desktop code-signing is deferred to FN-5605; Linux ARM64 CLI binaries are tracked in FN-5606. - run: pnpm --filter @fusion/desktop exec electron-builder --linux --x64 --arm64 --publish never + run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --linux --x64 --arm64 --publish never env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 72fcbcdf01..7402c23b8e 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,6 @@ packages/mobile/android/ # MCP server config (contains API keys) .mcp.json + +# Desktop packaging: staged flat prod closure produced by `@fusion/desktop build` (pnpm deploy) +packages/desktop/deploy/ diff --git a/packages/desktop/package.json b/packages/desktop/package.json index eb180d5e4c..597b155bf1 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -29,11 +29,11 @@ "test": "tsx scripts/test.ts", "typecheck": "tsc --noEmit", "generate:icons": "tsx scripts/generate-icons.ts", - "pack": "electron-builder --dir", - "dist": "electron-builder", - "dist:win": "electron-builder --win", - "dist:mac": "electron-builder --mac", - "dist:linux": "electron-builder --linux" + "pack": "electron-builder --projectDir deploy --dir", + "dist": "electron-builder --projectDir deploy", + "dist:win": "electron-builder --projectDir deploy --win", + "dist:mac": "electron-builder --projectDir deploy --mac", + "dist:linux": "electron-builder --projectDir deploy --linux" }, "dependencies": { "@fusion/core": "workspace:*", diff --git a/packages/desktop/scripts/build.ts b/packages/desktop/scripts/build.ts index fc68c903f0..92bee721b3 100644 --- a/packages/desktop/scripts/build.ts +++ b/packages/desktop/scripts/build.ts @@ -1,7 +1,7 @@ import { build } from "esbuild"; import { cp, mkdir, rm, stat } from "node:fs/promises"; import { join } from "node:path"; -import { buildCore, buildDashboard, buildDashboardClient, buildEngine, packageRoot, workspaceRoot } from "./workspace-tools"; +import { buildCore, buildDashboard, buildDashboardClient, buildEngine, packageRoot, stageDesktopDeploy, workspaceRoot } from "./workspace-tools"; const dashboardRoot = join(workspaceRoot, "packages", "dashboard"); const dashboardClientDir = join(dashboardRoot, "dist", "client"); const dashboardRegistryManifestSource = join(dashboardRoot, "src", "registry-manifest.json"); @@ -122,6 +122,12 @@ async function main(): Promise { await buildElectronEntrypoints(); await copyDashboardClient(); + // FNXC:DesktopPackaging 2026-07-01-21:15: + // Stage the complete flat production closure last (after all dist exists) so + // electron-builder can package it via --projectDir instead of its pnpm collector, + // which drops `deduped` subtrees and left the embedded runtime missing deps. + await stageDesktopDeploy(); + console.log("[desktop:build] Desktop build complete"); } diff --git a/packages/desktop/scripts/workspace-tools.ts b/packages/desktop/scripts/workspace-tools.ts index 1b39145c42..8bb33ac1f8 100644 --- a/packages/desktop/scripts/workspace-tools.ts +++ b/packages/desktop/scripts/workspace-tools.ts @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; import { dirname, resolve } from "node:path"; import { existsSync } from "node:fs"; -import { cp } from "node:fs/promises"; +import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -62,6 +62,16 @@ async function buildPackage(relativePath: string): Promise { await runWorkspaceBin("tsc", [], resolve(workspaceRoot, relativePath)); } +// FNXC:DesktopBuild 2026-07-01-21:10: +// Every example plugin whose compiled entry the dashboard SERVER statically +// imports must have dist built, because the packaged desktop runs the dashboard +// under plain Node (Electron main) which cannot load a plugin's `.ts` source. +// The plugins' `import` export condition points at ./dist/*.js (with a `source` +// condition kept for the bun-compiled CLI), so a missing dist => the packaged +// app crashes on Local mode when @fusion/dashboard imports the plugin. +// routes.ts / runtime-provider-probes.ts / droid-cli-probe.ts / roadmap-routes.ts +// pull hermes, openclaw, paperclip, cursor, droid and roadmap; dependency-graph +// backs a dashboard view. Keep this list in sync with dashboard's static plugin imports. export async function buildDashboardRuntimePlugins(): Promise { await buildPackage("packages/plugin-sdk"); await Promise.all([ @@ -69,6 +79,9 @@ export async function buildDashboardRuntimePlugins(): Promise { buildPackage("plugins/fusion-plugin-hermes-runtime"), buildPackage("plugins/fusion-plugin-openclaw-runtime"), buildPackage("plugins/fusion-plugin-paperclip-runtime"), + buildPackage("plugins/fusion-plugin-cursor-runtime"), + buildPackage("plugins/fusion-plugin-droid-runtime"), + buildPackage("plugins/fusion-plugin-roadmap"), ]); } @@ -82,6 +95,67 @@ export async function buildDashboard(): Promise { await cp(resolve(dashboardRoot, "src", "registry-manifest.json"), resolve(dashboardRoot, "dist", "registry-manifest.json")); } +function runPnpm(args: string[], cwd: string): Promise { + return new Promise((resolvePromise, rejectPromise) => { + const child = spawn("pnpm", args, { + cwd, + stdio: "inherit", + env: process.env, + // pnpm resolves to a .cmd shim on Windows; Node refuses to spawn it without a shell. + shell: process.platform === "win32", + }); + child.on("error", rejectPromise); + child.on("exit", (code) => + code === 0 ? resolvePromise() : rejectPromise(new Error(`pnpm ${args.join(" ")} exited with code ${code ?? "unknown"}`)), + ); + }); +} + +/* +FNXC:DesktopPackaging 2026-07-01-21:15: +Durable fix for the packaged desktop app missing runtime dependencies. +electron-builder's pnpm support runs `pnpm list --prod` and silently drops +`deduped` subtrees, so the embedded Local runtime's `import("@fusion/engine")` +closure (@modelcontextprotocol/sdk, the pi-ai provider SDKs, etc.) was never +packed and the app crashed with ERR_MODULE_NOT_FOUND. Instead of relying on that +collector, materialize the complete production closure with `pnpm deploy` +(legacy + hoisted => a real flat node_modules that pnpm resolves correctly) and +point electron-builder at that staged directory via --projectDir. + +- --prod drops devDependencies (electron itself), so copy electron's package.json + into the stage so electron-builder can still compute the version (it downloads + the runtime separately). +- Rewrite the staged electron-builder.yml output to ../dist-electron so artifacts + still land in packages/desktop/dist-electron for the existing workflow globs. +- The stage has no pnpm-lock, so electron-builder treats it as a plain flat + project and packs the whole node_modules — no dedup gap. +*/ +export const desktopDeployDir = resolve(packageRoot, "deploy"); + +export async function stageDesktopDeploy(): Promise { + console.log("[desktop:build] Staging complete production closure via pnpm deploy..."); + await rm(desktopDeployDir, { recursive: true, force: true }); + await runPnpm( + ["--filter", "@fusion/desktop", "deploy", "--prod", "--legacy", "--config.node-linker=hoisted", desktopDeployDir], + workspaceRoot, + ); + + // electron-builder needs electron's version; --prod pruned the devDependency. + const electronPkg = resolve(packageRoot, "node_modules", "electron", "package.json"); + const stagedElectronDir = resolve(desktopDeployDir, "node_modules", "electron"); + await mkdir(stagedElectronDir, { recursive: true }); + await cp(electronPkg, resolve(stagedElectronDir, "package.json")); + + // Keep artifacts in packages/desktop/dist-electron for the existing workflow globs. + const stagedConfig = resolve(desktopDeployDir, "electron-builder.yml"); + const config = await readFile(stagedConfig, "utf8"); + const patched = /output:\s*dist-electron/.test(config) + ? config.replace(/output:\s*dist-electron/, "output: ../dist-electron") + : `directories:\n output: ../dist-electron\n${config}`; + await writeFile(stagedConfig, patched); + console.log(`[desktop:build] Deploy staged at ${desktopDeployDir}`); +} + export async function buildDashboardClient(): Promise { // Desktop loads index.html via file:// from inside the asar, so absolute // asset paths (/assets/...) resolve to the filesystem root and fail. Build diff --git a/plugins/fusion-plugin-agent-browser/package.json b/plugins/fusion-plugin-agent-browser/package.json index ade6bc888d..7585e463e2 100644 --- a/plugins/fusion-plugin-agent-browser/package.json +++ b/plugins/fusion-plugin-agent-browser/package.json @@ -7,6 +7,7 @@ "exports": { ".": { "types": "./src/index.ts", + "source": "./src/index.ts", "import": "./dist/index.js" } }, diff --git a/plugins/fusion-plugin-cursor-runtime/package.json b/plugins/fusion-plugin-cursor-runtime/package.json index 0d0b84de81..99c0e8da15 100644 --- a/plugins/fusion-plugin-cursor-runtime/package.json +++ b/plugins/fusion-plugin-cursor-runtime/package.json @@ -11,11 +11,13 @@ "exports": { ".": { "types": "./src/index.ts", - "import": "./src/index.ts" + "source": "./src/index.ts", + "import": "./dist/index.js" }, "./probe": { "types": "./src/probe.ts", - "import": "./src/probe.ts" + "source": "./src/probe.ts", + "import": "./dist/probe.js" } }, "private": true, diff --git a/plugins/fusion-plugin-droid-runtime/package.json b/plugins/fusion-plugin-droid-runtime/package.json index 0cfc8a67c4..14300ea11d 100644 --- a/plugins/fusion-plugin-droid-runtime/package.json +++ b/plugins/fusion-plugin-droid-runtime/package.json @@ -11,11 +11,13 @@ "exports": { ".": { "types": "./src/index.ts", - "import": "./src/index.ts" + "source": "./src/index.ts", + "import": "./dist/index.js" }, "./probe": { "types": "./src/probe.ts", - "import": "./src/probe.ts" + "source": "./src/probe.ts", + "import": "./dist/probe.js" } }, "private": true, diff --git a/plugins/fusion-plugin-even-cards/package.json b/plugins/fusion-plugin-even-cards/package.json index 615bdbc5ef..4f2325a55f 100644 --- a/plugins/fusion-plugin-even-cards/package.json +++ b/plugins/fusion-plugin-even-cards/package.json @@ -7,6 +7,7 @@ "exports": { ".": { "types": "./src/index.ts", + "source": "./src/index.ts", "import": "./dist/index.js" } }, diff --git a/plugins/fusion-plugin-even-realities-glasses/package.json b/plugins/fusion-plugin-even-realities-glasses/package.json index c61c1b6361..3753b22e59 100644 --- a/plugins/fusion-plugin-even-realities-glasses/package.json +++ b/plugins/fusion-plugin-even-realities-glasses/package.json @@ -12,6 +12,7 @@ "exports": { ".": { "types": "./src/index.ts", + "source": "./src/index.ts", "import": "./dist/index.js" } }, diff --git a/plugins/fusion-plugin-paperclip-runtime/package.json b/plugins/fusion-plugin-paperclip-runtime/package.json index c12ad17fbe..64fafcd5fc 100644 --- a/plugins/fusion-plugin-paperclip-runtime/package.json +++ b/plugins/fusion-plugin-paperclip-runtime/package.json @@ -9,6 +9,7 @@ "exports": { ".": { "types": "./src/index.ts", + "source": "./src/index.ts", "import": "./dist/index.js" } }, diff --git a/plugins/fusion-plugin-roadmap/package.json b/plugins/fusion-plugin-roadmap/package.json index fe909c14d2..5891c7543b 100644 --- a/plugins/fusion-plugin-roadmap/package.json +++ b/plugins/fusion-plugin-roadmap/package.json @@ -7,15 +7,18 @@ "exports": { ".": { "types": "./src/index.ts", - "import": "./src/index.ts" + "source": "./src/index.ts", + "import": "./dist/index.js" }, "./server": { "types": "./src/server/index.d.ts", - "import": "./src/server/index.ts" + "source": "./src/server/index.ts", + "import": "./dist/server/index.js" }, "./roadmap-suggestions": { "types": "./src/roadmap-suggestions.d.ts", - "import": "./src/roadmap-suggestions.ts" + "source": "./src/roadmap-suggestions.ts", + "import": "./dist/roadmap-suggestions.js" } }, "scripts": { diff --git a/plugins/fusion-plugin-whatsapp-chat/package.json b/plugins/fusion-plugin-whatsapp-chat/package.json index 14a64bc3d1..1b946d9f39 100644 --- a/plugins/fusion-plugin-whatsapp-chat/package.json +++ b/plugins/fusion-plugin-whatsapp-chat/package.json @@ -12,6 +12,7 @@ "exports": { ".": { "types": "./src/index.ts", + "source": "./src/index.ts", "import": "./dist/index.js" } },