Files
fusion/packages/desktop/scripts/workspace-tools.ts
gsxdsm 1f3a15ec1c FN-7360: fix Windows desktop runtime packaging
Fix packaged Windows desktop startup by preserving Node-safe runtime dependencies and dashboard registry assets.

- Externalize Electron main/preload npm packages so CommonJS updater dependencies load natively.
- Load the dashboard plugin registry manifest through file IO and copy it into dashboard server dist from all build paths.
- Split Windows NSIS and portable artifact names and verify required Electron runtime resources in CI.
- Cover the packaging/runtime invariants with desktop and dashboard regression tests.

Files changed:
 .changeset/fn-7360-desktop-windows.md              |  7 +++
 .github/workflows/desktop-windows.yml              | 21 ++++++++
 packages/dashboard/package.json                    |  2 +-
 .../src/__tests__/plugin-registry-dist.test.ts     | 32 ++++++++++++
 .../src/__tests__/routes-plugin-registry.test.ts   | 10 ++--
 packages/dashboard/src/plugin-routes.ts            | 27 +++++++++-
 packages/desktop/README.md                         |  5 +-
 packages/desktop/electron-builder.yml              |  9 ++++
 packages/desktop/scripts/build.ts                  | 29 ++++++++--
 packages/desktop/scripts/workspace-tools.ts        |  4 ++
 .../desktop/src/__tests__/build-bundling.test.ts   | 61 ++++++++++++++++++++++
 .../src/__tests__/electron-builder-config.test.ts  | 15 ++++++
 packages/desktop/src/__tests__/ipc.test.ts         | 15 ++++++
 .../desktop/src/__tests__/local-runtime.test.ts    | 20 +++++++
 14 files changed, 247 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-7360

Fusion-Task-Lineage: a2d88130-c24f-43e2-932a-1e174b6afa35

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-01 08:01:59 -07:00

79 lines
3.2 KiB
TypeScript

import { spawn } from "node:child_process";
import { dirname, resolve } from "node:path";
import { existsSync } from "node:fs";
import { cp } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
export const packageRoot = resolve(__dirname, "..");
export const workspaceRoot = resolve(packageRoot, "..", "..");
function resolveBin(command: string, cwd: string): string {
const suffix = process.platform === "win32" ? ".cmd" : "";
const localBin = resolve(cwd, "node_modules", ".bin", `${command}${suffix}`);
if (existsSync(localBin)) {
return localBin;
}
return resolve(workspaceRoot, "node_modules", ".bin", `${command}${suffix}`);
}
export function runWorkspaceBin(command: string, args: string[], cwd: string): Promise<void> {
return new Promise((resolvePromise, rejectPromise) => {
const child = spawn(resolveBin(command, cwd), args, {
cwd,
stdio: "inherit",
env: process.env,
// On Windows the resolved bin is a .cmd shim; Node refuses to spawn
// .cmd/.bat without a shell (EINVAL) since CVE-2024-27980. resolveBin
// produces an absolute, space-free path, so shell quoting is safe here.
shell: process.platform === "win32",
});
child.on("error", rejectPromise);
child.on("exit", (code) => {
if (code === 0) {
resolvePromise();
return;
}
rejectPromise(new Error(`${command} ${args.join(" ")} exited with code ${code ?? "unknown"}`));
});
});
}
export async function buildCore(): Promise<void> {
await runWorkspaceBin("tsc", [], resolve(workspaceRoot, "packages", "core"));
}
async function buildPackage(relativePath: string): Promise<void> {
await runWorkspaceBin("tsc", [], resolve(workspaceRoot, relativePath));
}
export async function buildDashboardRuntimePlugins(): Promise<void> {
await buildPackage("packages/plugin-sdk");
await Promise.all([
buildPackage("plugins/fusion-plugin-dependency-graph"),
buildPackage("plugins/fusion-plugin-hermes-runtime"),
buildPackage("plugins/fusion-plugin-openclaw-runtime"),
buildPackage("plugins/fusion-plugin-paperclip-runtime"),
]);
}
export async function buildDashboard(): Promise<void> {
const dashboardRoot = resolve(workspaceRoot, "packages", "dashboard");
await buildDashboardRuntimePlugins();
await runWorkspaceBin("vite", ["build"], dashboardRoot);
await runWorkspaceBin("tsc", [], dashboardRoot);
// FNXC:DesktopBuild 2026-07-01-11:45:
// Desktop release and test paths call this helper directly instead of the dashboard package script, so copy the Node-read registry manifest beside server dist here as the shared build invariant.
await cp(resolve(dashboardRoot, "src", "registry-manifest.json"), resolve(dashboardRoot, "dist", "registry-manifest.json"));
}
export async function buildDashboardClient(): Promise<void> {
// Desktop loads index.html via file:// from inside the asar, so absolute
// asset paths (/assets/...) resolve to the filesystem root and fail. Build
// with a relative base so the bundled HTML references ./assets/... instead.
await runWorkspaceBin("vite", ["build", "--base", "./"], resolve(workspaceRoot, "packages", "dashboard"));
}