fix(desktop): boot embedded Postgres in packaged app and ship omp dist (#2106)

## Summary

Packaged Fusion desktop Local mode failed after the SQLite→Postgres
cutover:

1. **Embedded Postgres** could not start from `app.asar` — platform
packages resolve `initdb`/`postgres` via `import.meta.url` into the asar
virtual path, and `spawn` fails with `ENOTDIR`.
2. **After Postgres was fixed**, Local mode still fell back to the mode
chooser because `@fusion-plugin-examples/omp-runtime` was never built
into `dist/` (dashboard imports it from `runtime-provider-probes.ts`).

This PR makes packaged Local mode boot embedded Postgres reliably and
keep the dashboard shell up.

### Changes

- **CJS bootstrap** (`main-bootstrap.cjs`) as Electron `main`: patches
`child_process.spawn` / `fs.promises.stat|chmod` before the ESM main
loads so asar binary paths rewrite to real files.
- **Materialize** the full native PG install (`bin` + `lib` + `share`)
under `~/.fusion/embedded-postgres/runtime-bin/<plat-arch>/`.
- **electron-builder**: full `asarUnpack` of embedded-postgres packages;
allowlist PG deps and `@fusion-plugin-examples/**/*` (+ plugin-sdk / ACP
SDK).
- **Build** `fusion-plugin-omp-runtime` with the other dashboard-static
runtime plugins; export `DASHBOARD_RUNTIME_PLUGIN_PACKAGES` for tests.
- Unit coverage for asar path rewrite, packaging allowlists, and omp
build inclusion.

## Test plan

- [x] `pnpm --filter @fusion/core test:embedded-postgres` (23/23)
- [x] Desktop packaging unit tests (`build-bundling`,
`electron-builder-config`)
- [x] Packaged macOS `Fusion.app` Local mode:
  - [x] `embedded postgres: ready on port … (database "fusion")`
  - [x] `desktopMode` stays `"local"` (no chooser fallback)
- [x] `GET /api/health` → `status: ok`, `database.healthy: true`,
`engine.available: true`
- [x] Linux embedded binary lifecycle smoke (Docker aarch64,
`@embedded-postgres/linux-arm64`) — initdb/start/persist/restart
- [ ] CI release desktop jobs (macOS/Linux) when this lands
- [ ] Windows packaged desktop Local + PG (separate agent / host)

## Verification notes

| Platform | Embedded Postgres | Packaged Local shell |
|----------|-------------------|----------------------|
| macOS | Working | Working after this PR |
| Linux | Native binary smoke pass | Full AppImage not built on this
host |
| Windows | Out of scope here | Separate verification |

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved embedded PostgreSQL reliability in Electron-packaged apps by
rewriting bundled `app.asar` binary paths to their unpacked/materialized
locations.
* Ensured embedded PostgreSQL runtime binaries resolve correctly across
platforms/architectures, with best-effort executable permissions and
macOS dylib link normalization.

* **Packaging**
* Updated the desktop Electron entry to use a bootstrap module for
embedded PostgreSQL binary resolution.
* Expanded Electron Builder inclusion and asar-unpack rules for
embedded-postgres and related packages, plus required runtime plugin/sdk
assets.

* **Tests**
* Updated and added checks to match the new packaging and plugin/runtime
expectations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-14 20:44:09 -07:00
committed by GitHub
parent 4f037679ad
commit 7568705244
9 changed files with 927 additions and 33 deletions

View File

@@ -20,12 +20,14 @@ import {
existsSync,
rmSync,
writeFileSync,
readFileSync,
readlinkSync,
mkdirSync,
symlinkSync,
} from "node:fs";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { dirname, join } from "node:path";
import postgres from "postgres";
import {
EmbeddedPostgresLifecycle,
@@ -34,9 +36,17 @@ import {
isDataDirInitialized,
normalizeMacosEmbeddedPostgresDylibSymlinks,
readPortFromPostmasterPid,
resolveElectronAsarUnpackedPath,
fingerprintEmbeddedPostgresNativeRoot,
buildEmbeddedPostgresMaterializationMarker,
materializeEmbeddedPostgresRuntimeBinaries,
installElectronAsarNativePathPatch,
uninstallElectronAsarNativePathPatchForTests,
type EmbeddedLifecycleOptions,
} from "../../postgres/embedded-lifecycle.js";
const testRequire = createRequire(import.meta.url);
const SKIP = process.env.FUSION_EMBEDDED_TEST_SKIP === "1";
const embeddedDescribe = SKIP ? describe.skip : describe;
@@ -147,6 +157,305 @@ describe("embedded-lifecycle: constructor + URL helpers (no process)", () => {
});
});
describe("embedded-lifecycle: Electron asar unpacked path rewrite", () => {
/*
* FNXC:DesktopEmbeddedPostgres 2026-07-14-18:30:
* Packaged desktop resolves platform binaries under app.asar even when
* asarUnpack places them on disk under app.asar.unpacked. Prove the rewrite
* prefers the real unpacked file and leaves ordinary paths alone.
*
* FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
* Use a non-materialized binary name (`pg_dump`) so these assertions never
* short-circuit through the user's real ~/.fusion runtime-bin cache (which
* only applies to postgres/initdb/pg_ctl basenames).
*/
it("rewrites app.asar binary paths to app.asar.unpacked when present", () => {
const root = mkdtempSync(join(tmpdir(), "fusion-asar-rewrite-"));
try {
// pg_dump is intentionally NOT in the materialization BIN_NAMES set.
const asarBin = join(root, "app.asar", "node_modules", "pkg", "bin", "pg_dump");
const unpackedBin = join(root, "app.asar.unpacked", "node_modules", "pkg", "bin", "pg_dump");
mkdirSync(dirname(unpackedBin), { recursive: true });
writeFileSync(unpackedBin, "");
expect(resolveElectronAsarUnpackedPath(asarBin)).toBe(unpackedBin);
expect(resolveElectronAsarUnpackedPath(unpackedBin)).toBe(unpackedBin);
expect(resolveElectronAsarUnpackedPath(join(root, "plain", "pg_dump"))).toBe(
join(root, "plain", "pg_dump"),
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("keeps the asar path when no unpacked twin exists", () => {
const asarOnly = join(tmpdir(), "app.asar", "missing", "pg_dump");
expect(resolveElectronAsarUnpackedPath(asarOnly)).toBe(asarOnly);
});
it("patched spawn/stat/chmod receive rewritten asar paths without real processes", async () => {
/*
* FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
* Surface enumeration: exercise the production patch entry points (CJS
* child_process.spawn + fs.promises.stat/chmod), not only the pure path
* resolver. Install a recording bottom layer, then the production patch on
* top, and assert rewritten paths without launching Postgres.
*/
const root = mkdtempSync(join(tmpdir(), "fusion-asar-patch-"));
const childProcessMod = testRequire("child_process") as {
spawn: (...args: unknown[]) => unknown;
};
const fsPromisesMod = testRequire("fs/promises") as {
stat: (...args: unknown[]) => unknown;
chmod: (...args: unknown[]) => unknown;
};
// Undo any prior production patch so we can install fakes as the bottom layer.
uninstallElectronAsarNativePathPatchForTests();
const prevSpawn = childProcessMod.spawn;
const prevStat = fsPromisesMod.stat;
const prevChmod = fsPromisesMod.chmod;
const spawnSeen: unknown[] = [];
const statSeen: unknown[] = [];
const chmodSeen: unknown[] = [];
try {
const asarBin = join(root, "app.asar", "node_modules", "pkg", "bin", "pg_dump");
const unpackedBin = join(
root,
"app.asar.unpacked",
"node_modules",
"pkg",
"bin",
"pg_dump",
);
mkdirSync(dirname(unpackedBin), { recursive: true });
writeFileSync(unpackedBin, "fake-bin");
const plainPath = join(root, "plain", "pg_dump");
// Bottom-layer fakes record the command/path the production patch forwards.
childProcessMod.spawn = (command: unknown, ..._rest: unknown[]) => {
spawnSeen.push(command);
return {
pid: 0,
on: () => undefined,
kill: () => true,
stdout: null,
stderr: null,
};
};
fsPromisesMod.stat = async (p: unknown, ..._rest: unknown[]) => {
statSeen.push(p);
return { mode: 0o755, isFile: () => true };
};
fsPromisesMod.chmod = async (p: unknown, ..._rest: unknown[]) => {
chmodSeen.push(p);
};
installElectronAsarNativePathPatch();
childProcessMod.spawn(asarBin);
childProcessMod.spawn(plainPath);
await fsPromisesMod.stat(asarBin);
await fsPromisesMod.stat(plainPath);
await fsPromisesMod.chmod(asarBin, 0o755);
await fsPromisesMod.chmod(plainPath, 0o755);
expect(spawnSeen).toEqual([unpackedBin, plainPath]);
expect(statSeen).toEqual([unpackedBin, plainPath]);
expect(chmodSeen).toEqual([unpackedBin, plainPath]);
} finally {
// Restore production patch wrapper first (back to fakes), then real builtins.
uninstallElectronAsarNativePathPatchForTests();
childProcessMod.spawn = prevSpawn;
fsPromisesMod.stat = prevStat;
fsPromisesMod.chmod = prevChmod;
rmSync(root, { recursive: true, force: true });
}
});
});
describe("embedded-lifecycle: materialize runtime binaries (update-safe marker)", () => {
/*
* FNXC:DesktopEmbeddedPostgres 2026-07-15-02:55:
* Greptile P1 security: path-only `.materialized-from` markers reuse stale
* Postgres binaries after an in-place packaged app update (nativeRoot path is
* stable). Marker must include a content fingerprint so payload changes force
* a re-copy of the host-local runtime-bin cache.
*
* FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
* Fingerprint now content-hashes lib/ + share/ (not path+size only) so
* same-size library/support-file patches invalidate the cache.
*/
const postgresBin = process.platform === "win32" ? "postgres.exe" : "postgres";
const initdbBin = process.platform === "win32" ? "initdb.exe" : "initdb";
const pgCtlBin = process.platform === "win32" ? "pg_ctl.exe" : "pg_ctl";
function seedNativeRoot(root: string, postgresBody: string): void {
mkdirSync(join(root, "bin"), { recursive: true });
mkdirSync(join(root, "lib", "postgresql"), { recursive: true });
mkdirSync(join(root, "share", "postgresql"), { recursive: true });
writeFileSync(join(root, "bin", postgresBin), postgresBody);
writeFileSync(join(root, "bin", initdbBin), "initdb-stub");
writeFileSync(join(root, "bin", pgCtlBin), "pg_ctl-stub");
writeFileSync(join(root, "lib", "postgresql", "plpgsql.so"), "ext-v1");
writeFileSync(join(root, "share", "postgresql", "postgres.bki"), "share-v1");
}
it("fingerprint changes when binary contents change at the same path", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-fp-"));
try {
seedNativeRoot(nativeRoot, "postgres-v1");
const first = fingerprintEmbeddedPostgresNativeRoot(nativeRoot);
writeFileSync(join(nativeRoot, "bin", postgresBin), "postgres-v2-updated-payload");
const second = fingerprintEmbeddedPostgresNativeRoot(nativeRoot);
expect(first).not.toBe(second);
expect(first).toMatch(/^[a-f0-9]{64}$/);
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
}
});
it("fingerprint changes when a library payload changes without bin renames", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-fp-lib-"));
try {
seedNativeRoot(nativeRoot, "postgres-stable");
const first = fingerprintEmbeddedPostgresNativeRoot(nativeRoot);
// Same name and size, different contents — must invalidate (content hash).
writeFileSync(join(nativeRoot, "lib", "postgresql", "plpgsql.so"), "ext-v2");
const second = fingerprintEmbeddedPostgresNativeRoot(nativeRoot);
expect(first).not.toBe(second);
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
}
});
it("fingerprint changes when share support files change without bin renames", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-fp-share-"));
try {
seedNativeRoot(nativeRoot, "postgres-stable");
const first = fingerprintEmbeddedPostgresNativeRoot(nativeRoot);
// share/ is copied into runtime-bin and must participate in the fingerprint.
writeFileSync(join(nativeRoot, "share", "postgresql", "postgres.bki"), "share-v2");
const second = fingerprintEmbeddedPostgresNativeRoot(nativeRoot);
expect(first).not.toBe(second);
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
}
});
it("same-size library content change forces rematerialization", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-mat-samesize-"));
const destRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-mat-samesize-dst-"));
try {
seedNativeRoot(nativeRoot, "postgres-stable");
materializeEmbeddedPostgresRuntimeBinaries(nativeRoot, { destRoot });
expect(readFileSync(join(destRoot, "lib", "postgresql", "plpgsql.so"), "utf8")).toBe(
"ext-v1",
);
// Equal-length security patch at the same path (in-place app update).
writeFileSync(join(nativeRoot, "lib", "postgresql", "plpgsql.so"), "ext-v2");
materializeEmbeddedPostgresRuntimeBinaries(nativeRoot, { destRoot });
expect(readFileSync(join(destRoot, "lib", "postgresql", "plpgsql.so"), "utf8")).toBe(
"ext-v2",
);
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
rmSync(destRoot, { recursive: true, force: true });
}
});
it("buildEmbeddedPostgresMaterializationMarker includes path + fingerprint", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-marker-build-"));
try {
seedNativeRoot(nativeRoot, "postgres-body");
const marker = buildEmbeddedPostgresMaterializationMarker(nativeRoot);
const fingerprint = fingerprintEmbeddedPostgresNativeRoot(nativeRoot);
expect(marker.startsWith("v2\n")).toBe(true);
expect(marker).toContain(nativeRoot);
expect(marker).toContain(fingerprint);
// Path alone must not equal the full marker (legacy path-only markers rematerialize).
expect(marker.trim()).not.toBe(nativeRoot);
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
}
});
it("skips re-copy when marker + fingerprint still match (idempotent)", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-mat-src-"));
const destRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-mat-dst-"));
try {
seedNativeRoot(nativeRoot, "postgres-stable");
// First materialization.
materializeEmbeddedPostgresRuntimeBinaries(nativeRoot, { destRoot });
const markerPath = join(destRoot, ".materialized-from");
const markerAfterFirst = readFileSync(markerPath, "utf8");
const destPostgres = join(destRoot, "bin", postgresBin);
expect(readFileSync(destPostgres, "utf8")).toBe("postgres-stable");
// Destination-only sentinel: an always-recopy implementation would wipe it.
const reuseSentinel = join(destRoot, ".reuse-sentinel");
writeFileSync(reuseSentinel, "preserve");
// Second call must reuse (same path + same fingerprint) without error.
const returned = materializeEmbeddedPostgresRuntimeBinaries(nativeRoot, { destRoot });
expect(returned).toBe(destRoot);
expect(readFileSync(reuseSentinel, "utf8")).toBe("preserve");
expect(readFileSync(markerPath, "utf8")).toBe(markerAfterFirst);
expect(readFileSync(destPostgres, "utf8")).toBe("postgres-stable");
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
rmSync(destRoot, { recursive: true, force: true });
}
});
it("re-copies when payload changes even though nativeRoot path is unchanged", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-mat-update-"));
const destRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-mat-update-dst-"));
try {
seedNativeRoot(nativeRoot, "postgres-release-1");
materializeEmbeddedPostgresRuntimeBinaries(nativeRoot, { destRoot });
expect(readFileSync(join(destRoot, "bin", postgresBin), "utf8")).toBe("postgres-release-1");
// Leave a stale orphan that force-copy alone would not remove.
writeFileSync(join(destRoot, "bin", "orphan-from-old-release"), "stale");
// Simulate in-place app update: same nativeRoot path, new binary payload.
writeFileSync(join(nativeRoot, "bin", postgresBin), "postgres-release-2");
materializeEmbeddedPostgresRuntimeBinaries(nativeRoot, { destRoot });
expect(readFileSync(join(destRoot, "bin", postgresBin), "utf8")).toBe("postgres-release-2");
// Rematerialization clears dest first so orphans from prior releases do not linger.
expect(existsSync(join(destRoot, "bin", "orphan-from-old-release"))).toBe(false);
expect(readFileSync(join(destRoot, ".materialized-from"), "utf8")).toBe(
buildEmbeddedPostgresMaterializationMarker(nativeRoot),
);
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
rmSync(destRoot, { recursive: true, force: true });
}
});
it("treats legacy path-only markers as stale and rematerializes", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-mat-legacy-"));
const destRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-mat-legacy-dst-"));
try {
seedNativeRoot(nativeRoot, "postgres-current");
// Seed dest as if an older build wrote path-only markers.
mkdirSync(join(destRoot, "bin"), { recursive: true });
mkdirSync(join(destRoot, "lib", "postgresql"), { recursive: true });
writeFileSync(join(destRoot, "bin", postgresBin), "postgres-stale-legacy");
writeFileSync(join(destRoot, "lib", "postgresql", "plpgsql.so"), "old");
writeFileSync(join(destRoot, ".materialized-from"), nativeRoot);
materializeEmbeddedPostgresRuntimeBinaries(nativeRoot, { destRoot });
expect(readFileSync(join(destRoot, "bin", postgresBin), "utf8")).toBe("postgres-current");
expect(readFileSync(join(destRoot, ".materialized-from"), "utf8")).toBe(
buildEmbeddedPostgresMaterializationMarker(nativeRoot),
);
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
rmSync(destRoot, { recursive: true, force: true });
}
});
});
describe("embedded-lifecycle: macOS dylib compatibility links", () => {
it("repairs missing compatibility-name symlinks from versioned dylibs", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-native-"));

View File

@@ -45,22 +45,384 @@
// the flip-embedded-pg-default change; the runtime startup factory is the
// sole caller and it dynamically imports this module only in that case).
import {
cpSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
readlinkSync,
readdirSync,
rmSync,
statSync,
symlinkSync,
unlinkSync,
chmodSync,
writeFileSync,
} from "node:fs";
import { createHash } from "node:crypto";
import { homedir } from "node:os";
import { createServer, type Server } from "node:net";
import { dirname, join, basename } from "node:path";
import { createRequire } from "node:module";
import { dirname, join, basename, sep } from "node:path";
import { createRequire, syncBuiltinESMExports } from "node:module";
import { createLogger } from "../logger.js";
import { redactConnectionString } from "./credential-redact.js";
import type { ResolvedBackend } from "./backend-resolver.js";
const require = createRequire(import.meta.url);
const EMBEDDED_PG_BIN_NAMES = new Set([
"postgres",
"initdb",
"pg_ctl",
"postgres.exe",
"initdb.exe",
"pg_ctl.exe",
]);
/*
* FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
* Bump when the marker payload shape or fingerprint algorithm changes so older
* host-local caches always rematerialize after a desktop update that ships a
* new fingerprinting strategy (e.g. content-hashing lib/share, not path+size).
*/
const MATERIALIZATION_MARKER_VERSION = 2;
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-14-18:30:
* Electron packages app code into app.asar. Platform package entrypoints resolve
* native binary paths via import.meta.url, so paths look like
* `.../app.asar/node_modules/@embedded-postgres/.../native/bin/postgres` even when
* asarUnpack places the real files under `app.asar.unpacked/...`.
* Node's spawn/chmod against the asar virtual path fail with ENOTDIR; rewrite to
* the unpacked real path when that file exists. No-op outside Electron asar trees.
*/
export function resolveElectronAsarUnpackedPath(filePath: string): string {
if (!filePath) return filePath;
// Prefer a materialized runtime-bin path when we already copied binaries out of asar.
const materialized = resolveMaterializedEmbeddedPostgresBinary(filePath);
if (materialized) return materialized;
if (filePath.includes(`${sep}app.asar.unpacked${sep}`)) {
return filePath;
}
const marker = `${sep}app.asar${sep}`;
const index = filePath.indexOf(marker);
if (index === -1) return filePath;
const unpacked =
filePath.slice(0, index) +
`${sep}app.asar.unpacked${sep}` +
filePath.slice(index + marker.length);
return existsSync(unpacked) ? unpacked : filePath;
}
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-14-18:45:
* Host-local copy of packaged embedded Postgres binaries. Electron can still treat
* paths that contain the `app.asar` substring oddly at spawn time on some hosts;
* materializing into ~/.fusion avoids asar virtual-path issues entirely.
*/
export function embeddedPostgresRuntimeBinRoot(
platform: NodeJS.Platform = process.platform,
arch: string = process.arch,
home: string = homedir(),
): string {
return join(home, ".fusion", "embedded-postgres", "runtime-bin", `${platform}-${arch}`);
}
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-15-02:55:
* Content-aware fingerprint of a packaged native root. Packaged in-place updates
* keep `nativeRoot` path stable (same app.asar.unpacked layout) while replacing
* postgres binaries and libraries; path-only markers would reuse stale payload.
* Fingerprint mixes binary content hashes + sizes so both binary and library-only
* payload changes invalidate the host-local materialization cache.
*
* FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
* Greptile P1: path+size for lib files missed same-size content patches, and
* share/ was copied into runtime-bin but omitted from the fingerprint. Hash full
* file contents under bin/ + lib/ + share/ so any payload byte change forces
* rematerialization after an in-place app update.
*/
export function fingerprintEmbeddedPostgresNativeRoot(nativeRoot: string): string {
const hash = createHash("sha256");
const binNames =
process.platform === "win32"
? (["postgres.exe", "initdb.exe", "pg_ctl.exe"] as const)
: (["postgres", "initdb", "pg_ctl"] as const);
for (const name of binNames) {
const path = join(nativeRoot, "bin", name);
hash.update(name);
hash.update("\0");
try {
const st = statSync(path);
hash.update(String(st.size));
hash.update("\0");
// Full content hash of each critical binary (sizes are small enough for startup).
hash.update(readFileSync(path));
} catch {
hash.update("missing");
}
hash.update("\0");
}
// Full content walk of every tree that materialize() copies (lib + share).
// Budget bounds pathological trees; real embedded-postgres installs are well under it.
const budget = { remaining: 4096 };
for (const tree of ["lib", "share"] as const) {
const treeDir = join(nativeRoot, tree);
hash.update(`tree:${tree}\0`);
if (existsSync(treeDir)) {
try {
hashPayloadTreeContents(treeDir, tree, hash, budget);
} catch {
hash.update(`${tree}-unreadable`);
}
} else {
hash.update(`${tree}-missing`);
}
}
return hash.digest("hex");
}
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
* Recursive content fingerprint for a payload subtree (lib/ or share/). Records
* relative path + full file bytes so same-size security patches invalidate the
* materialization marker. Directory entries are structural markers only.
*/
function hashPayloadTreeContents(
absDir: string,
relPrefix: string,
hash: ReturnType<typeof createHash>,
budget: { remaining: number },
): void {
if (budget.remaining <= 0) return;
let entries: string[];
try {
entries = readdirSync(absDir).sort();
} catch {
return;
}
for (const entry of entries) {
if (budget.remaining <= 0) return;
const abs = join(absDir, entry);
const rel = relPrefix ? `${relPrefix}/${entry}` : entry;
try {
// lstat so macOS dylib compatibility symlinks are fingerprinted by target
// name (not followed) and same-size file content patches still hash bytes.
const st = lstatSync(abs);
if (st.isSymbolicLink()) {
budget.remaining -= 1;
hash.update(`l:${rel}:`);
try {
hash.update(readlinkSync(abs));
} catch {
hash.update("?");
}
hash.update("\0");
} else if (st.isDirectory()) {
hash.update(`d:${rel}\0`);
hashPayloadTreeContents(abs, rel, hash, budget);
} else if (st.isFile()) {
budget.remaining -= 1;
hash.update(`f:${rel}\0`);
try {
hash.update(readFileSync(abs));
} catch {
hash.update("unreadable");
}
hash.update("\0");
}
} catch {
hash.update(`?:${rel}\0`);
}
}
}
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-15-02:55:
* Materialization cache marker. Must change whenever either the source path OR
* the native payload changes so in-place app updates re-copy fresh Postgres
* binaries instead of reusing the previous release's host-local cache.
* Legacy path-only markers fail equality and force rematerialization.
*/
export function buildEmbeddedPostgresMaterializationMarker(nativeRoot: string): string {
const fingerprint = fingerprintEmbeddedPostgresNativeRoot(nativeRoot);
return `v${MATERIALIZATION_MARKER_VERSION}\n${nativeRoot}\n${fingerprint}\n`;
}
function resolveMaterializedEmbeddedPostgresBinary(filePath: string): string | null {
const name = basename(filePath);
if (!EMBEDDED_PG_BIN_NAMES.has(name)) return null;
if (!filePath.includes(`${sep}app.asar`)) return null;
const candidate = join(embeddedPostgresRuntimeBinRoot(), "bin", name);
return existsSync(candidate) ? candidate : null;
}
export interface MaterializeEmbeddedPostgresOptions {
/**
* Override the host-local dest root (tests). Defaults to
* `~/.fusion/embedded-postgres/runtime-bin/<platform>-<arch>`.
*/
readonly destRoot?: string;
}
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-14-18:45:
* Copy initdb/pg_ctl/postgres (+ lib tree for dyld/@loader_path) from the packaged
* native root into ~/.fusion so spawn never has to execute out of app.asar*.
* Idempotent: skips when marker + binaries already exist.
*
* FNXC:DesktopEmbeddedPostgres 2026-07-15-02:55:
* Marker identity includes a content fingerprint of the source native root, not
* only the path. Packaged app updates leave nativeRoot paths unchanged; without
* the fingerprint this guard would keep serving the previous release's binaries
* and libraries and bundled PostgreSQL fixes would never take effect.
*/
export function materializeEmbeddedPostgresRuntimeBinaries(
nativeRoot: string,
options?: MaterializeEmbeddedPostgresOptions,
): string {
/*
* FNXC:DesktopEmbeddedPostgres 2026-07-14-18:55:
* Postgres expects a full install layout next to the binaries: bin/, lib/
* (including lib/postgresql extension modules), and share/postgresql.
* A shallow bin-only copy fails at start with "could not open directory
* .../lib/postgresql". Copy the entire native root recursively.
*/
const destRoot = options?.destRoot ?? embeddedPostgresRuntimeBinRoot();
const destBin = join(destRoot, "bin");
const marker = join(destRoot, ".materialized-from");
const sourceMarker = buildEmbeddedPostgresMaterializationMarker(nativeRoot);
if (
existsSync(marker) &&
readFileSync(marker, "utf8") === sourceMarker &&
existsSync(join(destBin, process.platform === "win32" ? "postgres.exe" : "postgres")) &&
existsSync(join(destRoot, "lib", "postgresql"))
) {
return destRoot;
}
/*
* FNXC:DesktopEmbeddedPostgres 2026-07-15-02:55:
* Clear the previous materialization before re-copy so files removed in a newer
* payload cannot linger beside the updated binaries (force-copy alone does not
* delete orphans).
*/
if (existsSync(destRoot)) {
rmSync(destRoot, { recursive: true, force: true });
}
mkdirSync(destRoot, { recursive: true });
// Recursive copy of bin/lib/share (and any other native install dirs).
for (const entry of readdirSync(nativeRoot)) {
const from = join(nativeRoot, entry);
const to = join(destRoot, entry);
cpSync(from, to, { recursive: true, force: true });
}
// Ensure executables keep +x after asar materialization.
if (existsSync(destBin)) {
for (const name of readdirSync(destBin)) {
try {
chmodSync(join(destBin, name), 0o755);
} catch {
// best-effort
}
}
}
// Re-apply macOS ABI compatibility links against the materialized lib dir.
normalizeMacosEmbeddedPostgresDylibSymlinks(destRoot);
writeFileSync(marker, sourceMarker, "utf8");
return destRoot;
}
let electronAsarNativePathPatchInstalled = false;
/** Restores the pre-patch CJS/ESM builtins; used by unit tests only. */
let electronAsarNativePathPatchRestore: (() => void) | null = null;
type MutableSpawnModule = {
spawn: (...args: unknown[]) => unknown;
};
type MutableFsPromisesModule = {
stat: (...args: unknown[]) => unknown;
chmod: (...args: unknown[]) => unknown;
};
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-14-18:30:
* Install once before constructing embedded-postgres. That library calls
* fs.promises.stat/chmod and child_process.spawn on binary paths derived from
* asar module URLs; without this patch, packaged desktop local mode cannot boot
* Postgres (ENOTDIR).
*
* Mutate the CJS exports objects (`require("child_process")` / `require("fs/promises")`)
* rather than the frozen ESM namespace (`import * as ...`).
*
* FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
* Replacing CJS export properties does NOT automatically update already-resolved
* ESM named imports of the same builtins — they keep the pre-patch function until
* `syncBuiltinESMExports()` runs. Call it after every CJS mutation so ESM importers
* of child_process/fs.promises (including embedded-postgres) observe the rewrite.
* Safe outside Electron — rewrite is a no-op without asar.
*/
export function installElectronAsarNativePathPatch(): void {
if (electronAsarNativePathPatchInstalled) return;
electronAsarNativePathPatchInstalled = true;
// Materialize only when the platform package lives under Electron's asar tree.
// Dev/CLI installs already use real filesystem paths and must not copy binaries.
try {
const nativeRoot = resolveGenericEmbeddedPostgresNativeRoot();
if (nativeRoot && nativeRoot.includes(`${sep}app.asar`)) {
const sourceRoot = resolveElectronAsarUnpackedPath(nativeRoot);
if (existsSync(join(sourceRoot, "bin"))) {
materializeEmbeddedPostgresRuntimeBinaries(sourceRoot);
}
}
} catch {
// Materialization is best-effort; path rewrite still helps when possible.
}
const childProcessMod = require("child_process") as MutableSpawnModule;
const originalSpawn = childProcessMod.spawn.bind(childProcessMod);
childProcessMod.spawn = (command: unknown, ...rest: unknown[]) => {
const fixedCommand =
typeof command === "string" ? resolveElectronAsarUnpackedPath(command) : command;
return originalSpawn(fixedCommand, ...rest);
};
const fsPromisesMod = require("fs/promises") as MutableFsPromisesModule;
const originalStat = fsPromisesMod.stat.bind(fsPromisesMod);
fsPromisesMod.stat = (path: unknown, ...rest: unknown[]) => {
const fixedPath = typeof path === "string" ? resolveElectronAsarUnpackedPath(path) : path;
return originalStat(fixedPath, ...rest);
};
const originalChmod = fsPromisesMod.chmod.bind(fsPromisesMod);
fsPromisesMod.chmod = (path: unknown, ...rest: unknown[]) => {
const fixedPath = typeof path === "string" ? resolveElectronAsarUnpackedPath(path) : path;
return originalChmod(fixedPath, ...rest);
};
// Propagate CJS mutations to ESM named exports (spawn/stat/chmod).
syncBuiltinESMExports();
electronAsarNativePathPatchRestore = () => {
childProcessMod.spawn = originalSpawn;
fsPromisesMod.stat = originalStat;
fsPromisesMod.chmod = originalChmod;
syncBuiltinESMExports();
electronAsarNativePathPatchInstalled = false;
electronAsarNativePathPatchRestore = null;
};
}
/**
* FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
* Test-only undo for {@link installElectronAsarNativePathPatch} so unit tests can
* install a recording bottom-layer spawn/stat/chmod stub, then reinstall the
* production patch on top and assert rewritten paths without real processes.
*/
export function uninstallElectronAsarNativePathPatchForTests(): void {
electronAsarNativePathPatchRestore?.();
}
/**
* Lazily resolve the `embedded-postgres` default export. Cached after the
* first call. Throws if the package is not installed (e.g. a stripped-down
@@ -82,6 +444,10 @@ type EmbeddedPostgresInstance = InstanceType<EmbeddedPostgresCtor>;
let embeddedPostgresCtorCache: EmbeddedPostgresCtor | null = null;
function getEmbeddedPostgresCtor(): EmbeddedPostgresCtor {
if (embeddedPostgresCtorCache) return embeddedPostgresCtorCache;
// FNXC:DesktopEmbeddedPostgres 2026-07-14-18:30:
// Patch asar binary paths before loading embedded-postgres so its module-level
// binary promise and later spawn/chmod use real unpacked executables.
installElectronAsarNativePathPatch();
// Use require() so the bundler leaves this as a runtime resolution (esbuild
// keeps createRequire'd specifiers out of the static import graph).
const mod = require("embedded-postgres") as { default: EmbeddedPostgresCtor };
@@ -265,23 +631,43 @@ function resolvePnpmPlatformPackageNativeRoot(packageName: string): string | nul
}
}
function resolveMacosEmbeddedPostgresNativeRoot(): string | null {
if (process.platform !== "darwin") return null;
const packageName = process.arch === "arm64"
? "@embedded-postgres/darwin-arm64"
: process.arch === "x64"
? "@embedded-postgres/darwin-x64"
: null;
if (!packageName) return null;
function embeddedPostgresPlatformPackageName(
platform: NodeJS.Platform = process.platform,
arch: string = process.arch,
): string | null {
if (platform === "darwin") {
if (arch === "arm64") return "@embedded-postgres/darwin-arm64";
if (arch === "x64") return "@embedded-postgres/darwin-x64";
return null;
}
if (platform === "linux") {
if (arch === "arm64") return "@embedded-postgres/linux-arm64";
if (arch === "x64") return "@embedded-postgres/linux-x64";
if (arch === "arm") return "@embedded-postgres/linux-arm";
if (arch === "ia32") return "@embedded-postgres/linux-ia32";
if (arch === "ppc64") return "@embedded-postgres/linux-ppc64";
return null;
}
if (platform === "win32" && arch === "x64") return "@embedded-postgres/windows-x64";
return null;
}
function resolveGenericEmbeddedPostgresNativeRoot(): string | null {
const packageName = embeddedPostgresPlatformPackageName();
if (!packageName) return null;
try {
const entrypoint = require.resolve(packageName);
return join(dirname(entrypoint), "..", "native");
return resolveElectronAsarUnpackedPath(join(dirname(entrypoint), "..", "native"));
} catch {
return resolvePnpmPlatformPackageNativeRoot(packageName);
}
}
function resolveMacosEmbeddedPostgresNativeRoot(): string | null {
if (process.platform !== "darwin") return null;
return resolveGenericEmbeddedPostgresNativeRoot();
}
function normalizeBundledMacosDylibs(onLog: (message: string) => void): void {
const nativeRoot = resolveMacosEmbeddedPostgresNativeRoot();
if (!nativeRoot) return;

View File

@@ -82,16 +82,57 @@ files:
- node_modules/protobufjs/**/*
- node_modules/tar-fs/**/*
- node_modules/uuid/**/*
# FNXC:DesktopEmbeddedPostgres 2026-07-14-18:25:
# Zero-config desktop local mode boots embedded PostgreSQL via @fusion/core.
# These packages are required runtime deps of that path and must be in the
# explicit files allowlist so electron-builder cannot drop them when walking
# the staged production closure.
- node_modules/embedded-postgres/**/*
- node_modules/@embedded-postgres/**/*
- node_modules/pg/**/*
- node_modules/pg-connection-string/**/*
- node_modules/pg-int8/**/*
- node_modules/pg-pool/**/*
- node_modules/pg-protocol/**/*
- node_modules/pg-types/**/*
- node_modules/pgpass/**/*
- node_modules/postgres-array/**/*
- node_modules/postgres-bytea/**/*
- node_modules/postgres-date/**/*
- node_modules/postgres-interval/**/*
- node_modules/split2/**/*
- node_modules/xtend/**/*
- node_modules/postgres/**/*
- node_modules/async-exit-hook/**/*
# FNXC:DesktopOmpPlugin 2026-07-14-18:55:
# Dashboard server statically imports @fusion-plugin-examples/* runtime probes
# (hermes/openclaw/cursor/grok/omp/paperclip/droid/roadmap/dependency-graph).
# Include the whole scoped tree so a new statically-imported plugin cannot be
# dropped from the packaged app.asar when electron-builder walks the allowlist.
- node_modules/@fusion-plugin-examples/**/*
- node_modules/@fusion/plugin-sdk/**/*
- node_modules/@agentclientprotocol/sdk/**/*
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:30:
# Embedded Postgres launches native initdb/pg_ctl/postgres child processes.
# Executables and their shared libraries must live on the real filesystem;
# processes cannot execute payloads directly from Electron's virtual app.asar.
# FNXC:DesktopEmbeddedPostgres 2026-07-14-18:25:
# Embedded Postgres launches native initdb/pg_ctl/postgres child processes and resolves
# those paths from platform-package dist/index.js via import.meta.url.
# If only native/** is unpacked, dist/index.js stays inside app.asar so binary paths
# resolve as .../app.asar/.../native/bin/postgres. Electron marks those files unpacked,
# but fs.promises.chmod (used by embedded-postgres ensureBinIsExecutable) still fails
# with ENOTDIR against the asar path — packaged desktop local mode cannot start Postgres.
# Unpack the full platform packages (JS entrypoints + native trees) so __dirname is a
# real filesystem path under app.asar.unpacked and chmod/spawn work without asar redirects.
# Also keep the top-level embedded-postgres package unpacked so its dynamic import of the
# platform package resolves to the same real tree.
asarUnpack:
- "node_modules/@embedded-postgres/*/native/**/*"
- "node_modules/@embedded-postgres/**/*"
- "node_modules/embedded-postgres/**/*"
extraMetadata:
main: dist/main.js
# FNXC:DesktopEmbeddedPostgres 2026-07-14-18:50:
# CJS bootstrap patches child_process.spawn before ESM main loads so packaged
# embedded Postgres binaries resolve out of app.asar (see main-bootstrap.cjs).
main: dist/main-bootstrap.cjs
extraResources:
- from: src/icons

View File

@@ -19,7 +19,7 @@
},
"private": true,
"type": "module",
"main": "dist/main.js",
"main": "dist/main-bootstrap.cjs",
"engines": {
"node": ">=22.5.0"
},

View File

@@ -58,6 +58,17 @@ async function ensureDashboardBuild(): Promise<void> {
async function buildElectronEntrypoints(): Promise<void> {
console.log("[desktop:build] Bundling Electron main/preload with esbuild...");
/*
* FNXC:DesktopEmbeddedPostgres 2026-07-14-18:50:
* Ship the CJS bootstrap that patches child_process.spawn before the ESM main
* loads. package.json main + electron-builder extraMetadata point here so
* packaged local mode can execute embedded Postgres outside app.asar.
*/
await cp(
join(packageRoot, "src", "main-bootstrap.cjs"),
join(desktopDistDir, "main-bootstrap.cjs"),
);
await Promise.all([
build({
entryPoints: [join(packageRoot, "src", "main.ts")],
@@ -127,7 +138,8 @@ async function ensureEmbeddedRuntimeBuild(): Promise<void> {
// the Fusion backend". Assert the required files in BOTH the source `dist/`
// (esbuild output) and the staged `deploy/dist/` (what electron-builder packs
// into app.asar), since only the latter reflects what ships.
const REQUIRED_PACKAGED_ASSETS = ["main.js", "preload.js", join("client", "index.html")];
// FNXC:DesktopEmbeddedPostgres 2026-07-14-18:50: main-bootstrap.cjs is the package entry; main.js is loaded from it.
const REQUIRED_PACKAGED_ASSETS = ["main-bootstrap.cjs", "main.js", "preload.js", join("client", "index.html")];
async function verifyPackagedArtifacts(): Promise<void> {
console.log("[desktop:build] Verifying required packaged assets are present...");

View File

@@ -70,20 +70,29 @@ async function buildPackage(relativePath: string): Promise<void> {
// 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, grok, droid and roadmap; dependency-graph
// pull hermes, openclaw, paperclip, cursor, grok, droid, omp and roadmap; dependency-graph
// backs a dashboard view. Keep this list in sync with dashboard's static plugin imports.
//
// FNXC:DesktopOmpPlugin 2026-07-14-18:55:
// Oh My Pi (omp) ACP runtime is imported by runtime-provider-probes.ts. Without
// building its dist, packaged Local mode boots Postgres then fails with
// ERR_MODULE_NOT_FOUND for @fusion-plugin-examples/omp-runtime/dist/index.js and
// falls back to the mode chooser.
export const DASHBOARD_RUNTIME_PLUGIN_PACKAGES = [
"plugins/fusion-plugin-dependency-graph",
"plugins/fusion-plugin-hermes-runtime",
"plugins/fusion-plugin-openclaw-runtime",
"plugins/fusion-plugin-paperclip-runtime",
"plugins/fusion-plugin-cursor-runtime",
"plugins/fusion-plugin-grok-runtime",
"plugins/fusion-plugin-omp-runtime",
"plugins/fusion-plugin-droid-runtime",
"plugins/fusion-plugin-roadmap",
] as const;
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"),
buildPackage("plugins/fusion-plugin-cursor-runtime"),
buildPackage("plugins/fusion-plugin-grok-runtime"),
buildPackage("plugins/fusion-plugin-droid-runtime"),
buildPackage("plugins/fusion-plugin-roadmap"),
]);
await Promise.all(DASHBOARD_RUNTIME_PLUGIN_PACKAGES.map((relativePath) => buildPackage(relativePath)));
}
export async function buildDashboard(): Promise<void> {

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
DASHBOARD_RUNTIME_PLUGIN_PACKAGES,
requiredEmbeddedPostgresPackages,
verifyEmbeddedPostgresPayloads,
} from "../../scripts/workspace-tools";
@@ -26,6 +27,23 @@ describe("desktop Electron main bundling", () => {
expect(buildScript).toContain("await cp(dashboardRegistryManifestSource, dashboardRegistryManifestDist)");
});
it("builds every dashboard-static runtime plugin including omp before packaging", async () => {
/*
* FNXC:DesktopOmpPlugin 2026-07-14-18:55:
* runtime-provider-probes imports omp-runtime; missing dist crashes Local mode
* after Postgres boots. Keep the build list aligned with that import surface.
*/
expect(DASHBOARD_RUNTIME_PLUGIN_PACKAGES).toContain("plugins/fusion-plugin-omp-runtime");
expect(DASHBOARD_RUNTIME_PLUGIN_PACKAGES).toContain("plugins/fusion-plugin-hermes-runtime");
expect(DASHBOARD_RUNTIME_PLUGIN_PACKAGES).toContain("plugins/fusion-plugin-grok-runtime");
expect(DASHBOARD_RUNTIME_PLUGIN_PACKAGES).toContain("plugins/fusion-plugin-cursor-runtime");
const workspaceTools = await readDesktopFile("scripts/workspace-tools.ts");
expect(workspaceTools).toContain("buildDashboardRuntimePlugins");
expect(workspaceTools).toContain("DASHBOARD_RUNTIME_PLUGIN_PACKAGES");
expect(workspaceTools).toContain("fusion-plugin-omp-runtime");
});
it("externalizes production main-process packages so updater CJS deps are not bundled into ESM", async () => {
const buildScript = await readDesktopFile("scripts/build.ts");
const mainBuildBlock = buildScript.match(/entryPoints: \[join\(packageRoot, "src", "main\.ts"\)\],[\s\S]*?logLevel: "info",/m)?.[0];

View File

@@ -132,11 +132,20 @@ describe("electron-builder desktop config", () => {
}
});
it("unpacks native embedded Postgres processes from app.asar", async () => {
it("unpacks full embedded Postgres packages from app.asar", async () => {
/*
* FNXC:DesktopEmbeddedPostgres 2026-07-14-18:25:
* Native-only asarUnpack left dist/index.js inside app.asar; binary paths then
* resolved under the archive and chmod/spawn failed on packaged desktop boots.
* Assert the full platform + parent packages are unpacked.
*/
const builderConfig = await readDesktopFile("electron-builder.yml");
expect(builderConfig).toMatch(
/asarUnpack:\s*[\s\S]*?-\s*"node_modules\/@embedded-postgres\/\*\/native\/\*\*\/\*"/m,
/asarUnpack:\s*[\s\S]*?-\s*"node_modules\/@embedded-postgres\/\*\*\/\*"/m,
);
expect(builderConfig).toMatch(
/asarUnpack:\s*[\s\S]*?-\s*"node_modules\/embedded-postgres\/\*\*\/\*"/m,
);
});
@@ -162,6 +171,31 @@ describe("electron-builder desktop config", () => {
"node_modules/minizlib/**/*",
"node_modules/yallist/**/*",
"node_modules/yaml/**/*",
// FNXC:DesktopEmbeddedPostgres 2026-07-14-18:25: local-mode Postgres boot deps
// FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11: assert the full pg transitive
// allowlist (protocol/types/pgpass/codecs/split2/xtend) so dropping any glob
// fails this regression and cannot silently break packaged startup.
"node_modules/embedded-postgres/**/*",
"node_modules/@embedded-postgres/**/*",
"node_modules/pg/**/*",
"node_modules/pg-connection-string/**/*",
"node_modules/pg-int8/**/*",
"node_modules/pg-pool/**/*",
"node_modules/pg-protocol/**/*",
"node_modules/pg-types/**/*",
"node_modules/pgpass/**/*",
"node_modules/postgres-array/**/*",
"node_modules/postgres-bytea/**/*",
"node_modules/postgres-date/**/*",
"node_modules/postgres-interval/**/*",
"node_modules/split2/**/*",
"node_modules/xtend/**/*",
"node_modules/postgres/**/*",
"node_modules/async-exit-hook/**/*",
// FNXC:DesktopOmpPlugin 2026-07-14-18:55: dashboard-static plugin packages
"node_modules/@fusion-plugin-examples/**/*",
"node_modules/@fusion/plugin-sdk/**/*",
"node_modules/@agentclientprotocol/sdk/**/*",
];
for (const dependencyGlob of requiredRuntimeDependencyGlobs) {

View File

@@ -0,0 +1,85 @@
/*
FNXC:DesktopEmbeddedPostgres 2026-07-14-18:50:
Packaged Electron loads @fusion/core + embedded-postgres as ESM after app.asar is mounted.
Platform package entrypoints resolve native binaries via import.meta.url to
`.../app.asar/.../native/bin/postgres`, and spawn against that path fails with ENOTDIR.
CJS mutation of child_process.spawn must run BEFORE any ESM importer binds spawn.
This bootstrap is the package main: patch builtins, then dynamically import the ESM main.
FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
After CJS reassignment of child_process.spawn / fs.promises.stat|chmod, already-resolved
ESM named imports keep the pre-patch functions until module.syncBuiltinESMExports() runs.
Call it after every CJS mutation so the subsequent dynamic import of main.js (and any
ESM embedded-postgres import) observes the rewritten paths.
*/
"use strict";
const fs = require("fs");
const path = require("path");
const os = require("os");
const cp = require("child_process");
const fsp = require("fs/promises");
const { syncBuiltinESMExports } = require("module");
const BIN_NAMES = new Set([
"postgres",
"initdb",
"pg_ctl",
"postgres.exe",
"initdb.exe",
"pg_ctl.exe",
]);
function runtimeBinRoot() {
return path.join(os.homedir(), ".fusion", "embedded-postgres", "runtime-bin", `${process.platform}-${process.arch}`);
}
function resolveAsarUnpacked(filePath) {
if (!filePath || typeof filePath !== "string") return filePath;
const base = path.basename(filePath);
if (BIN_NAMES.has(base) && filePath.includes(`${path.sep}app.asar`)) {
const materialized = path.join(runtimeBinRoot(), "bin", base);
if (fs.existsSync(materialized)) return materialized;
}
if (filePath.includes(`${path.sep}app.asar.unpacked${path.sep}`)) return filePath;
const marker = `${path.sep}app.asar${path.sep}`;
const index = filePath.indexOf(marker);
if (index === -1) return filePath;
const unpacked =
filePath.slice(0, index) +
`${path.sep}app.asar.unpacked${path.sep}` +
filePath.slice(index + marker.length);
return fs.existsSync(unpacked) ? unpacked : filePath;
}
function installSpawnPatch() {
const originalSpawn = cp.spawn.bind(cp);
cp.spawn = function patchedSpawn(command, ...rest) {
const fixed = typeof command === "string" ? resolveAsarUnpacked(command) : command;
return originalSpawn(fixed, ...rest);
};
const originalStat = fsp.stat.bind(fsp);
fsp.stat = function patchedStat(p, ...rest) {
const fixed = typeof p === "string" ? resolveAsarUnpacked(p) : p;
return originalStat(fixed, ...rest);
};
const originalChmod = fsp.chmod.bind(fsp);
fsp.chmod = function patchedChmod(p, ...rest) {
const fixed = typeof p === "string" ? resolveAsarUnpacked(p) : p;
return originalChmod(fixed, ...rest);
};
// Propagate CJS mutations to ESM named exports before main.js loads.
syncBuiltinESMExports();
}
installSpawnPatch();
// Load the ESM Electron main after builtins are patched.
const { pathToFileURL } = require("url");
import(pathToFileURL(path.join(__dirname, "main.js")).href).catch((err) => {
console.error("[desktop/bootstrap] failed to load main.js", err);
process.exit(1);
});