fix(cli): make the standalone fn binary boot PostgreSQL in both modes
The bun-compiled exe has been unbootable since the PG cutover: bun
standalone binaries do no node_modules resolution, so the deliberately
out-of-graph require("embedded-postgres") failed from /$bunfs, and
readFile'd migration .sql files were never embedded, so even external
DATABASE_URL mode died at schema init.
- schema-applier: resolveMigrationsDir() — FUSION_MIGRATIONS_DIR env >
module-relative dist/migrations (npm/desktop, unchanged) >
execPath-relative migrations/ (standalone exe), probe-based.
- embedded-lifecycle: require("embedded-postgres") first (npm/desktop
untouched), falling back to a self-contained staged bundle at
<execDir>/runtime/<platform>/embedded-postgres/dist/index.cjs
(FUSION_EMBEDDED_PG_RUNTIME_DIR override) with the native
initdb/pg_ctl/postgres payload beside it.
- build.ts: stage dist/migrations plus the per-target embedded-postgres
bundle + native payload (warn when a cross-target payload is absent on
the host, mirroring desktop's verifyEmbeddedPostgresPayloads).
- release.yml: package fn-cli-<os>-<arch>.tar.gz (binary + migrations +
runtime + client) with sha256 per leg; prune staged payload files from
the release-collection globs; bare fn-cli-* binaries still uploaded.
E2E-verified on the compiled binary: embedded mode initdb→/api/health
200 database healthy; DATABASE_URL mode applied migrations 0000–0019
(109 tables). Core typecheck clean; schema-applier 58/58 and
embedded-lifecycle 44/44 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/standalone-exe-postgres-boot.md
Normal file
7
.changeset/standalone-exe-postgres-boot.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Fix the standalone `fn` binary failing to boot in both embedded-Postgres and DATABASE_URL modes.
|
||||||
|
category: fix
|
||||||
|
dev: Migrations now resolve via FUSION_MIGRATIONS_DIR > module-relative > execPath-relative; embedded-postgres ships as a self-contained bundle + native payload under runtime/<platform>/embedded-postgres (override root with FUSION_EMBEDDED_PG_RUNTIME_DIR); releases add self-contained fn-cli-<platform>.tar.gz assets.
|
||||||
53
.github/workflows/release.yml
vendored
53
.github/workflows/release.yml
vendored
@@ -110,6 +110,47 @@ jobs:
|
|||||||
$hash = (Get-FileHash ${{ matrix.binary }} -Algorithm SHA256).Hash.ToLower()
|
$hash = (Get-FileHash ${{ matrix.binary }} -Algorithm SHA256).Hash.ToLower()
|
||||||
"$hash ${{ matrix.binary }}" | Out-File -Encoding ascii ${{ matrix.binary }}.sha256
|
"$hash ${{ matrix.binary }}" | Out-File -Encoding ascii ${{ matrix.binary }}.sha256
|
||||||
|
|
||||||
|
# FNXC:Release 2026-07-17-13:45:
|
||||||
|
# The bare binary alone cannot boot: the compiled exe resolves PostgreSQL
|
||||||
|
# migrations and the embedded-postgres runtime payload execPath-relative
|
||||||
|
# (see packages/core/src/postgres/schema-applier.ts and
|
||||||
|
# embedded-lifecycle.ts). Ship a self-contained tarball (binary +
|
||||||
|
# migrations/ + runtime/<platform>/) per target so a downloaded release
|
||||||
|
# asset works out of the box. The bare binary + .sha256 continue to be
|
||||||
|
# uploaded unchanged so existing download links stay valid.
|
||||||
|
- name: Package release tarball
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd packages/cli/dist
|
||||||
|
BASE="${{ matrix.binary }}"
|
||||||
|
BASE="${BASE%.exe}"
|
||||||
|
PLAT="${{ matrix.target }}"
|
||||||
|
PLAT="${PLAT#bun-}"
|
||||||
|
STAGE="tarball-stage"
|
||||||
|
rm -rf "$STAGE"
|
||||||
|
mkdir -p "$STAGE/runtime"
|
||||||
|
cp "${{ matrix.binary }}" "$STAGE/"
|
||||||
|
if [ -d migrations ]; then
|
||||||
|
cp -R migrations "$STAGE/migrations"
|
||||||
|
else
|
||||||
|
echo "::warning::packages/cli/dist/migrations missing; tarball will lack schema migrations"
|
||||||
|
fi
|
||||||
|
if [ -d "runtime/$PLAT" ]; then
|
||||||
|
cp -R "runtime/$PLAT" "$STAGE/runtime/$PLAT"
|
||||||
|
else
|
||||||
|
echo "::warning::packages/cli/dist/runtime/$PLAT missing; tarball will lack native runtime assets"
|
||||||
|
fi
|
||||||
|
if [ -d client ]; then
|
||||||
|
cp -R client "$STAGE/client"
|
||||||
|
fi
|
||||||
|
tar -czf "$BASE.tar.gz" -C "$STAGE" .
|
||||||
|
rm -rf "$STAGE"
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum "$BASE.tar.gz" > "$BASE.tar.gz.sha256"
|
||||||
|
else
|
||||||
|
shasum -a 256 "$BASE.tar.gz" > "$BASE.tar.gz.sha256"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -117,6 +158,9 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
packages/cli/dist/${{ matrix.binary }}
|
packages/cli/dist/${{ matrix.binary }}
|
||||||
packages/cli/dist/${{ matrix.binary }}.sha256
|
packages/cli/dist/${{ matrix.binary }}.sha256
|
||||||
|
packages/cli/dist/*.tar.gz
|
||||||
|
packages/cli/dist/*.tar.gz.sha256
|
||||||
|
packages/cli/dist/migrations/**/*
|
||||||
packages/cli/dist/runtime/**/*
|
packages/cli/dist/runtime/**/*
|
||||||
|
|
||||||
# ── Build Windows desktop EXE artifacts ──────────────────────────────
|
# ── Build Windows desktop EXE artifacts ──────────────────────────────
|
||||||
@@ -568,7 +612,14 @@ jobs:
|
|||||||
id: collect
|
id: collect
|
||||||
run: |
|
run: |
|
||||||
mkdir release-files
|
mkdir release-files
|
||||||
find artifacts -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.apk" -o -name "*.aab" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" -o -name "latest*.yml" \) -exec cp {} release-files/ \;
|
# FNXC:Release 2026-07-17-13:45:
|
||||||
|
# Prune the CLI runtime/ and migrations/ staging trees: they exist in the
|
||||||
|
# artifact only as tarball inputs and contain files that would otherwise
|
||||||
|
# match the flat collection globs (e.g. embedded-postgres postgres.exe).
|
||||||
|
# The self-contained fn-cli-<platform>.tar.gz (+ .sha256) matches the
|
||||||
|
# existing *.tar.gz globs and reaches the release alongside the bare
|
||||||
|
# fn-cli-* binaries.
|
||||||
|
find artifacts \( -path "*/runtime/*" -o -path "*/migrations/*" \) -prune -o -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.apk" -o -name "*.aab" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" -o -name "latest*.yml" \) -print -exec cp {} release-files/ \;
|
||||||
ls -la release-files/
|
ls -la release-files/
|
||||||
count=$(find release-files -type f | wc -l | tr -d ' ')
|
count=$(find release-files -type f | wc -l | tr -d ' ')
|
||||||
echo "count=$count" >> "$GITHUB_OUTPUT"
|
echo "count=$count" >> "$GITHUB_OUTPUT"
|
||||||
|
|||||||
@@ -212,6 +212,165 @@ function ensureClientAssets(): ClientAssetMode {
|
|||||||
return "stub";
|
return "stub";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:StandaloneExeMigrations 2026-07-17-13:40:
|
||||||
|
The compiled binary cannot read module-relative assets out of /$bunfs, so the
|
||||||
|
PostgreSQL migrations must ship as real files next to the binary. Stage
|
||||||
|
packages/core/src/postgres/migrations (same source tsup.config.ts stages into
|
||||||
|
dist/migrations for the npm package) into the exe output dir; core's
|
||||||
|
schema-applier resolves them execPath-relative at runtime.
|
||||||
|
*/
|
||||||
|
const pgMigrationsSrc = join(workspaceRoot, "packages", "core", "src", "postgres", "migrations");
|
||||||
|
const pgMigrationsDest = join(outDir, "migrations");
|
||||||
|
|
||||||
|
function stageMigrations(): void {
|
||||||
|
if (!existsSync(pgMigrationsSrc)) {
|
||||||
|
console.warn(
|
||||||
|
`WARNING: PostgreSQL migrations source not found at ${pgMigrationsSrc}; the standalone binary will fail to apply schema migrations.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existsSync(pgMigrationsDest)) {
|
||||||
|
rmSync(pgMigrationsDest, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
cpSync(pgMigrationsSrc, pgMigrationsDest, { recursive: true });
|
||||||
|
console.log(` → ${pgMigrationsDest}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Embedded PostgreSQL runtime staging ───────────────────────────────
|
||||||
|
/*
|
||||||
|
FNXC:StandaloneExeEmbeddedPg 2026-07-17-14:20:
|
||||||
|
core's embedded-lifecycle loads `embedded-postgres` via createRequire at
|
||||||
|
runtime (deliberately outside the bundler graph). Inside the compiled binary
|
||||||
|
that resolution fails: bun --compile binaries perform NO node_modules
|
||||||
|
bare-specifier resolution at runtime — not even through a createRequire
|
||||||
|
anchored at a real on-disk directory (verified empirically: requiring an
|
||||||
|
absolute path works, but any bare import like "pg" from that file then fails).
|
||||||
|
A staged node_modules tree therefore cannot work. Instead, stage a fully
|
||||||
|
self-contained esbuild CJS bundle of embedded-postgres (pg, async-exit-hook,
|
||||||
|
and the matching @embedded-postgres/<platform> entry inlined) at
|
||||||
|
dist/runtime/<platform>/embedded-postgres/dist/index.cjs
|
||||||
|
plus the native initdb/pg_ctl/postgres payload at
|
||||||
|
dist/runtime/<platform>/embedded-postgres/native/
|
||||||
|
The platform package resolves its binaries via import.meta.url ("../native/
|
||||||
|
bin/..."), so import.meta.url is defined to the bundle's own file URL and the
|
||||||
|
native tree is staged one level up — the same relative layout the package
|
||||||
|
expects. embedded-lifecycle probes this execPath-relative dir (or
|
||||||
|
FUSION_EMBEDDED_PG_RUNTIME_DIR) only when normal resolution fails.
|
||||||
|
pnpm-workspace.yaml supportedArchitectures limits local installs to the host
|
||||||
|
OS, so targets whose platform payload is absent on the build host get a
|
||||||
|
warning and no embedded payload (DATABASE_URL mode is unaffected), mirroring
|
||||||
|
the spirit of verifyEmbeddedPostgresPayloads in
|
||||||
|
packages/desktop/scripts/workspace-tools.ts.
|
||||||
|
*/
|
||||||
|
const coreRequire = createRequire(join(workspaceRoot, "packages", "core", "package.json"));
|
||||||
|
|
||||||
|
const ALL_EMBEDDED_PG_PLATFORM_PACKAGES = [
|
||||||
|
"@embedded-postgres/darwin-arm64",
|
||||||
|
"@embedded-postgres/darwin-x64",
|
||||||
|
"@embedded-postgres/linux-arm64",
|
||||||
|
"@embedded-postgres/linux-x64",
|
||||||
|
"@embedded-postgres/linux-arm",
|
||||||
|
"@embedded-postgres/linux-ia32",
|
||||||
|
"@embedded-postgres/linux-ppc64",
|
||||||
|
"@embedded-postgres/windows-x64",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Map a runtime prebuild name (e.g. "darwin-arm64", "windows-x64") to the platform package. */
|
||||||
|
function embeddedPgPlatformPackageFor(prebuildName: string): string | null {
|
||||||
|
const [plat, arch] = prebuildName.split("-");
|
||||||
|
const os = plat === "windows" || plat === "win32" ? "windows" : plat;
|
||||||
|
const name = `@embedded-postgres/${os}-${arch}`;
|
||||||
|
return (ALL_EMBEDDED_PG_PLATFORM_PACKAGES as readonly string[]).includes(name) ? name : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stageEmbeddedPostgresRuntime(target?: BunTarget): boolean {
|
||||||
|
const prebuildName = target ? targetToPrebuildName(target) : hostPrebuildName();
|
||||||
|
const destRoot = join(runtimeDir, prebuildName, "embedded-postgres");
|
||||||
|
try {
|
||||||
|
if (existsSync(destRoot)) {
|
||||||
|
rmSync(destRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
mkdirSync(join(destRoot, "dist"), { recursive: true });
|
||||||
|
|
||||||
|
let embeddedPgJsonPath: string;
|
||||||
|
try {
|
||||||
|
embeddedPgJsonPath = coreRequire.resolve("embedded-postgres/package.json");
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
` WARNING: embedded-postgres is not resolvable from @fusion/core; the ${prebuildName} binary will not support the default embedded database mode.`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const embeddedPgRoot = dirname(embeddedPgJsonPath);
|
||||||
|
const embeddedPgEntry = join(embeddedPgRoot, "dist", "index.js");
|
||||||
|
const embeddedPgRequire = createRequire(embeddedPgJsonPath);
|
||||||
|
|
||||||
|
// Resolve the target's native payload (an optionalDependency of
|
||||||
|
// embedded-postgres, resolved from its own location). Absent payloads are
|
||||||
|
// a warning, not a failure — pnpm only installs the host OS's packages.
|
||||||
|
const platformPkg = embeddedPgPlatformPackageFor(prebuildName);
|
||||||
|
let nativeSrc: string | null = null;
|
||||||
|
if (platformPkg) {
|
||||||
|
try {
|
||||||
|
const platformEntry = embeddedPgRequire.resolve(platformPkg);
|
||||||
|
const candidate = join(dirname(platformEntry), "..", "native");
|
||||||
|
if (existsSync(join(candidate, "bin"))) nativeSrc = candidate;
|
||||||
|
} catch {
|
||||||
|
nativeSrc = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!platformPkg || !nativeSrc) {
|
||||||
|
console.warn(
|
||||||
|
` WARNING: embedded-postgres native payload (${platformPkg ?? "unmapped platform"}) is not installed on this host for target ${prebuildName}. ` +
|
||||||
|
`Embedded database mode will be unavailable in this build (DATABASE_URL mode is unaffected).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bundle embedded-postgres + deps into one self-contained CJS file. The
|
||||||
|
// target's platform package is inlined; the other platforms' dynamic
|
||||||
|
// imports stay external (their branches never execute at runtime).
|
||||||
|
const esbuildBin = join(workspaceRoot, "node_modules", ".bin", process.platform === "win32" ? "esbuild.cmd" : "esbuild");
|
||||||
|
if (!existsSync(esbuildBin)) {
|
||||||
|
console.warn(` WARNING: esbuild not found at ${esbuildBin}; cannot stage embedded-postgres runtime.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const externals = ALL_EMBEDDED_PG_PLATFORM_PACKAGES.filter(
|
||||||
|
(name) => !(nativeSrc && name === platformPkg),
|
||||||
|
);
|
||||||
|
const outFile = join(destRoot, "dist", "index.cjs");
|
||||||
|
const esbuildArgs = [
|
||||||
|
embeddedPgEntry,
|
||||||
|
"--bundle",
|
||||||
|
"--platform=node",
|
||||||
|
"--format=cjs",
|
||||||
|
`--outfile=${outFile}`,
|
||||||
|
// The inlined platform package computes native binary paths from
|
||||||
|
// import.meta.url; point it at the bundle's own real file location.
|
||||||
|
"--define:import.meta.url=__fusionEmbeddedPgBundleUrl",
|
||||||
|
"--banner:js=const __fusionEmbeddedPgBundleUrl = require('node:url').pathToFileURL(__filename).href;",
|
||||||
|
"--external:pg-native",
|
||||||
|
...externals.map((name) => `--external:${name}`),
|
||||||
|
"--log-level=warning",
|
||||||
|
];
|
||||||
|
const bundleProc = Bun.spawnSync({ cmd: [esbuildBin, ...esbuildArgs], cwd: workspaceRoot, stdout: "inherit", stderr: "inherit" });
|
||||||
|
if (bundleProc.exitCode !== 0) {
|
||||||
|
console.error(` ERROR: esbuild bundling of embedded-postgres failed for ${prebuildName} (exit ${bundleProc.exitCode}).`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nativeSrc) {
|
||||||
|
// Preserve symlinks (macOS dylib ABI-compat links) and executable bits.
|
||||||
|
cpSync(nativeSrc, join(destRoot, "native"), { recursive: true });
|
||||||
|
}
|
||||||
|
console.log(` → ${destRoot} (embedded-postgres runtime bundle${nativeSrc ? " + native payload" : ", JS only"})`);
|
||||||
|
return nativeSrc !== null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` ERROR: Failed to stage embedded-postgres runtime for ${prebuildName}:`, err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Copy native terminal assets for a specific target ─────────────────
|
// ── Copy native terminal assets for a specific target ─────────────────
|
||||||
/**
|
/**
|
||||||
* Stage @homebridge/node-pty-prebuilt-multiarch native assets for the given target.
|
* Stage @homebridge/node-pty-prebuilt-multiarch native assets for the given target.
|
||||||
@@ -325,6 +484,10 @@ function compileBinary(outFile: string, target: string, isCrossCompile: boolean)
|
|||||||
? target.replace(/^bun-/, "")
|
? target.replace(/^bun-/, "")
|
||||||
: hostPrebuildName();
|
: hostPrebuildName();
|
||||||
copyNativeAssets(isCrossCompile ? target as BunTarget : undefined);
|
copyNativeAssets(isCrossCompile ? target as BunTarget : undefined);
|
||||||
|
// FNXC:StandaloneExeEmbeddedPg 2026-07-17-13:40:
|
||||||
|
// Must run AFTER copyNativeAssets — that function recreates runtime/<plat>/
|
||||||
|
// and would wipe a previously staged embedded-postgres tree.
|
||||||
|
stageEmbeddedPostgresRuntime(isCrossCompile ? target as BunTarget : undefined);
|
||||||
|
|
||||||
// Prepare asset paths for embedding
|
// Prepare asset paths for embedding
|
||||||
const nativeAssetDir = join(runtimeDir, prebuildName);
|
const nativeAssetDir = join(runtimeDir, prebuildName);
|
||||||
@@ -382,6 +545,9 @@ const { targets } = parseArgs();
|
|||||||
|
|
||||||
// Stage assets once (shared across all binaries)
|
// Stage assets once (shared across all binaries)
|
||||||
const clientAssetMode = ensureClientAssets();
|
const clientAssetMode = ensureClientAssets();
|
||||||
|
// FNXC:StandaloneExeMigrations 2026-07-17-13:40:
|
||||||
|
// PostgreSQL migrations ship next to the binary (platform-independent, staged once).
|
||||||
|
stageMigrations();
|
||||||
|
|
||||||
if (targets === null) {
|
if (targets === null) {
|
||||||
// Default: build for current platform → dist/fn
|
// Default: build for current platform → dist/fn
|
||||||
|
|||||||
@@ -84,6 +84,107 @@ export { isWindowsElevatedAdmin } from "./embedded-windows-admin.js";
|
|||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:StandaloneExeEmbeddedPg 2026-07-17-14:25:
|
||||||
|
Inside the bun-compiled standalone `fn` binary, `createRequire(import.meta.url)`
|
||||||
|
is anchored at the virtual /$bunfs/root filesystem, so the deliberate
|
||||||
|
out-of-bundle `require("embedded-postgres")` fails with "Cannot find package
|
||||||
|
'embedded-postgres'" and default embedded-PG boot dies. Worse, bun --compile
|
||||||
|
binaries perform NO node_modules bare-specifier resolution at runtime at all
|
||||||
|
(verified: requiring an absolute file path works, but that file's own bare
|
||||||
|
imports like "pg" then fail), so a staged node_modules tree cannot help.
|
||||||
|
The standalone build (packages/cli/build.ts) therefore stages a fully
|
||||||
|
self-contained CJS bundle of embedded-postgres (pg and the
|
||||||
|
@embedded-postgres/<platform> entry inlined) plus the native
|
||||||
|
initdb/pg_ctl/postgres payload at
|
||||||
|
<dir-of-binary>/runtime/<platform>-<arch>/embedded-postgres/
|
||||||
|
dist/index.cjs — the bundle, loaded below by absolute path
|
||||||
|
native/bin/... — binaries the bundle resolves via ../native/
|
||||||
|
These helpers locate that staged bundle (or FUSION_EMBEDDED_PG_RUNTIME_DIR
|
||||||
|
when the operator relocates the payload). Resolution order is strictly
|
||||||
|
additive: the normal createRequire path is tried first, so npm/tsup and
|
||||||
|
desktop installs are untouched; the staged bundle is consulted only when the
|
||||||
|
primary resolution fails.
|
||||||
|
*/
|
||||||
|
function embeddedPostgresStagedRootCandidates(): string[] {
|
||||||
|
const roots: string[] = [];
|
||||||
|
const envRoot = process.env.FUSION_EMBEDDED_PG_RUNTIME_DIR;
|
||||||
|
if (envRoot) roots.push(envRoot);
|
||||||
|
try {
|
||||||
|
const execDir = dirname(process.execPath);
|
||||||
|
// build.ts stages per-target dirs named after the bun target suffix. For
|
||||||
|
// darwin/linux that equals `${process.platform}-${process.arch}`; Windows
|
||||||
|
// cross-compiled release assets use "windows-x64" while process.platform is
|
||||||
|
// "win32", so probe both spellings.
|
||||||
|
roots.push(join(execDir, "runtime", `${process.platform}-${process.arch}`, "embedded-postgres"));
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
roots.push(join(execDir, "runtime", `windows-${process.arch}`, "embedded-postgres"));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// process.execPath is always defined in practice; keep the fallback silent.
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stagedEmbeddedPgBundleCache: string | null | undefined;
|
||||||
|
/** Absolute path of the staged self-contained embedded-postgres bundle, or null. */
|
||||||
|
function getStagedEmbeddedPgBundlePath(): string | null {
|
||||||
|
if (stagedEmbeddedPgBundleCache !== undefined) return stagedEmbeddedPgBundleCache;
|
||||||
|
stagedEmbeddedPgBundleCache = null;
|
||||||
|
for (const root of embeddedPostgresStagedRootCandidates()) {
|
||||||
|
try {
|
||||||
|
const bundle = join(root, "dist", "index.cjs");
|
||||||
|
if (existsSync(bundle)) {
|
||||||
|
stagedEmbeddedPgBundleCache = bundle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep probing the remaining candidates.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stagedEmbeddedPgBundleCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* require("embedded-postgres") with the staged-standalone fallback (loaded by
|
||||||
|
* absolute path — the only require form a compiled bun binary can resolve
|
||||||
|
* outside its own bundle). Primary resolution always wins.
|
||||||
|
*/
|
||||||
|
function requireEmbeddedPostgresModule(): unknown {
|
||||||
|
try {
|
||||||
|
return require("embedded-postgres");
|
||||||
|
} catch (primaryError) {
|
||||||
|
const bundle = getStagedEmbeddedPgBundlePath();
|
||||||
|
if (bundle) {
|
||||||
|
try {
|
||||||
|
return require(bundle);
|
||||||
|
} catch {
|
||||||
|
// Fall through and surface the primary (normal-install) error.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw primaryError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* require.resolve() for embedded-postgres-family specifiers with the
|
||||||
|
* staged-standalone fallback. The staged bundle inlines the platform package,
|
||||||
|
* so every family specifier resolves to the bundle path — which preserves the
|
||||||
|
* layout contract callers rely on: join(dirname(entrypoint), "..", "native")
|
||||||
|
* lands on the staged native/ tree.
|
||||||
|
*/
|
||||||
|
function resolveEmbeddedPostgresSpecifier(specifier: string): string {
|
||||||
|
try {
|
||||||
|
return require.resolve(specifier);
|
||||||
|
} catch (primaryError) {
|
||||||
|
const bundle = getStagedEmbeddedPgBundlePath();
|
||||||
|
if (bundle && (specifier === "embedded-postgres" || specifier.startsWith("@embedded-postgres/"))) {
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
throw primaryError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const EMBEDDED_PG_BIN_NAMES = new Set([
|
const EMBEDDED_PG_BIN_NAMES = new Set([
|
||||||
"postgres",
|
"postgres",
|
||||||
"initdb",
|
"initdb",
|
||||||
@@ -505,7 +606,10 @@ function getEmbeddedPostgresCtor(): EmbeddedPostgresCtor {
|
|||||||
installElectronAsarNativePathPatch();
|
installElectronAsarNativePathPatch();
|
||||||
// Use require() so the bundler leaves this as a runtime resolution (esbuild
|
// Use require() so the bundler leaves this as a runtime resolution (esbuild
|
||||||
// keeps createRequire'd specifiers out of the static import graph).
|
// keeps createRequire'd specifiers out of the static import graph).
|
||||||
const mod = require("embedded-postgres") as { default: EmbeddedPostgresCtor };
|
// FNXC:StandaloneExeEmbeddedPg 2026-07-17-14:25:
|
||||||
|
// requireEmbeddedPostgresModule adds the execPath-relative staged-bundle
|
||||||
|
// fallback used by the bun standalone binary; normal installs resolve as before.
|
||||||
|
const mod = requireEmbeddedPostgresModule() as { default: EmbeddedPostgresCtor };
|
||||||
embeddedPostgresCtorCache = mod.default ?? (mod as unknown as EmbeddedPostgresCtor);
|
embeddedPostgresCtorCache = mod.default ?? (mod as unknown as EmbeddedPostgresCtor);
|
||||||
return embeddedPostgresCtorCache;
|
return embeddedPostgresCtorCache;
|
||||||
}
|
}
|
||||||
@@ -684,7 +788,7 @@ function findPnpmVirtualStore(start: string): string | null {
|
|||||||
|
|
||||||
function resolvePnpmPlatformPackageNativeRoot(packageName: string): string | null {
|
function resolvePnpmPlatformPackageNativeRoot(packageName: string): string | null {
|
||||||
try {
|
try {
|
||||||
const embeddedEntrypoint = require.resolve("embedded-postgres");
|
const embeddedEntrypoint = resolveEmbeddedPostgresSpecifier("embedded-postgres");
|
||||||
const virtualStore = findPnpmVirtualStore(dirname(embeddedEntrypoint));
|
const virtualStore = findPnpmVirtualStore(dirname(embeddedEntrypoint));
|
||||||
if (!virtualStore) return null;
|
if (!virtualStore) return null;
|
||||||
const encodedName = packageName.replace("/", "+");
|
const encodedName = packageName.replace("/", "+");
|
||||||
@@ -722,7 +826,10 @@ function resolveGenericEmbeddedPostgresNativeRoot(): string | null {
|
|||||||
const packageName = embeddedPostgresPlatformPackageName();
|
const packageName = embeddedPostgresPlatformPackageName();
|
||||||
if (!packageName) return null;
|
if (!packageName) return null;
|
||||||
try {
|
try {
|
||||||
const entrypoint = require.resolve(packageName);
|
// FNXC:StandaloneExeEmbeddedPg 2026-07-17-13:35:
|
||||||
|
// Staged-standalone fallback included so the native initdb/pg_ctl/postgres
|
||||||
|
// payload resolves from the runtime dir shipped next to the fn binary.
|
||||||
|
const entrypoint = resolveEmbeddedPostgresSpecifier(packageName);
|
||||||
return resolveElectronAsarUnpackedPath(join(dirname(entrypoint), "..", "native"));
|
return resolveElectronAsarUnpackedPath(join(dirname(entrypoint), "..", "native"));
|
||||||
} catch {
|
} catch {
|
||||||
return resolvePnpmPlatformPackageNativeRoot(packageName);
|
return resolvePnpmPlatformPackageNativeRoot(packageName);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
||||||
@@ -107,94 +108,107 @@ export const BULK_COMPLETION_REFUSAL_AT_VERSION = "0018";
|
|||||||
export const MIGRATION_BOOKKEEPING_TABLE = "fusion_schema_migrations";
|
export const MIGRATION_BOOKKEEPING_TABLE = "fusion_schema_migrations";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const BASELINE_MIGRATION_PATH = join(__dirname, "migrations", "0000_initial.sql");
|
|
||||||
|
/*
|
||||||
|
FNXC:StandaloneExeMigrations 2026-07-17-13:30:
|
||||||
|
The bun-compiled standalone `fn` binary runs its bundled code from the virtual
|
||||||
|
/$bunfs/root filesystem, so the historical `join(__dirname, "migrations")`
|
||||||
|
resolution points at a path that does not exist on disk (bun --compile does not
|
||||||
|
embed readFile assets) and every DATABASE_URL boot died with
|
||||||
|
ENOENT /$bunfs/root/migrations/0000_initial.sql. Resolution order:
|
||||||
|
1. FUSION_MIGRATIONS_DIR env override — always wins when set (operator escape hatch).
|
||||||
|
2. join(__dirname, "migrations") — the npm/tsup and desktop layout; kept first
|
||||||
|
among the probes so nothing changes for existing installs.
|
||||||
|
3. join(dirname(process.execPath), "migrations") — the standalone-exe layout,
|
||||||
|
where build.ts / the release tarball stage migrations/ next to the binary.
|
||||||
|
The existsSync probe (not a runtime-detection heuristic) picks between (2) and
|
||||||
|
(3): inside the compiled binary the module-relative dir simply does not exist,
|
||||||
|
while for node-based installs it always does, so npm/desktop behavior is untouched.
|
||||||
|
*/
|
||||||
|
function resolveMigrationsDir(): string {
|
||||||
|
const envDir = process.env.FUSION_MIGRATIONS_DIR;
|
||||||
|
if (envDir) return envDir;
|
||||||
|
const moduleDir = join(__dirname, "migrations");
|
||||||
|
if (existsSync(join(moduleDir, "0000_initial.sql"))) return moduleDir;
|
||||||
|
const execDir = join(dirname(process.execPath), "migrations");
|
||||||
|
if (existsSync(join(execDir, "0000_initial.sql"))) return execDir;
|
||||||
|
// Preserve the historical default (and its historical error message) when
|
||||||
|
// neither location exists — the readFile ENOENT remains the diagnostic.
|
||||||
|
return moduleDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIGRATIONS_DIR = resolveMigrationsDir();
|
||||||
|
const BASELINE_MIGRATION_PATH = join(MIGRATIONS_DIR, "0000_initial.sql");
|
||||||
const AUTOMATION_ISOLATION_MIGRATION_PATH = join(
|
const AUTOMATION_ISOLATION_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0001_automation_project_isolation.sql",
|
"0001_automation_project_isolation.sql",
|
||||||
);
|
);
|
||||||
const ANALYTICS_ISOLATION_MIGRATION_PATH = join(
|
const ANALYTICS_ISOLATION_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0002_analytics_project_isolation.sql",
|
"0002_analytics_project_isolation.sql",
|
||||||
);
|
);
|
||||||
const MONITOR_APPROVAL_ISOLATION_MIGRATION_PATH = join(
|
const MONITOR_APPROVAL_ISOLATION_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0003_monitor_approval_project_isolation.sql",
|
"0003_monitor_approval_project_isolation.sql",
|
||||||
);
|
);
|
||||||
const LEGACY_CUTOVER_PRESERVATION_MIGRATION_PATH = join(
|
const LEGACY_CUTOVER_PRESERVATION_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0004_legacy_cutover_preservation.sql",
|
"0004_legacy_cutover_preservation.sql",
|
||||||
);
|
);
|
||||||
const MULTI_PROJECT_CUTOVER_MIGRATION_PATH = join(
|
const MULTI_PROJECT_CUTOVER_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0005_multi_project_cutover.sql",
|
"0005_multi_project_cutover.sql",
|
||||||
);
|
);
|
||||||
const PROJECT_OWNERSHIP_MIGRATION_PATH = join(
|
const PROJECT_OWNERSHIP_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0006_project_ownership.sql",
|
"0006_project_ownership.sql",
|
||||||
);
|
);
|
||||||
const SQLITE_SCHEMA_PARITY_MIGRATION_PATH = join(
|
const SQLITE_SCHEMA_PARITY_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0007_sqlite_schema_parity.sql",
|
"0007_sqlite_schema_parity.sql",
|
||||||
);
|
);
|
||||||
const SESSION_ADVISOR_ENABLED_MIGRATION_PATH = join(
|
const SESSION_ADVISOR_ENABLED_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0008_session_advisor_enabled.sql",
|
"0008_session_advisor_enabled.sql",
|
||||||
);
|
);
|
||||||
const MISSION_FIX_IDEMPOTENCY_MIGRATION_PATH = join(
|
const MISSION_FIX_IDEMPOTENCY_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0009_mission_fix_idempotency.sql",
|
"0009_mission_fix_idempotency.sql",
|
||||||
);
|
);
|
||||||
const IMPORT_TRANSLATION_CACHE_MIGRATION_PATH = join(
|
const IMPORT_TRANSLATION_CACHE_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0010_import_translation_cache.sql",
|
"0010_import_translation_cache.sql",
|
||||||
);
|
);
|
||||||
const IMPORT_TRANSLATION_CACHE_SCOPE_FIX_MIGRATION_PATH = join(
|
const IMPORT_TRANSLATION_CACHE_SCOPE_FIX_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0016_import_translation_cache_scope_fix.sql",
|
"0016_import_translation_cache_scope_fix.sql",
|
||||||
);
|
);
|
||||||
const IMPORT_TRANSLATION_CACHE_LEGACY_PARTITION_BACKFILL_MIGRATION_PATH = join(
|
const IMPORT_TRANSLATION_CACHE_LEGACY_PARTITION_BACKFILL_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0019_import_translation_cache_legacy_partition_backfill.sql",
|
"0019_import_translation_cache_legacy_partition_backfill.sql",
|
||||||
);
|
);
|
||||||
const OWNER_PROJECT_ID_SPLIT_MIGRATION_PATH = join(
|
const OWNER_PROJECT_ID_SPLIT_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0011_owner_project_id.sql",
|
"0011_owner_project_id.sql",
|
||||||
);
|
);
|
||||||
const CHAT_SESSION_PINS_MIGRATION_PATH = join(
|
const CHAT_SESSION_PINS_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0012_chat_session_pins.sql",
|
"0012_chat_session_pins.sql",
|
||||||
);
|
);
|
||||||
const EXECUTOR_TOOL_FAILURE_RETRY_MIGRATION_PATH = join(
|
const EXECUTOR_TOOL_FAILURE_RETRY_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0013_executor_tool_failure_retry.sql",
|
"0013_executor_tool_failure_retry.sql",
|
||||||
);
|
);
|
||||||
const EXECUTOR_ESCALATION_ATTEMPT_MIGRATION_PATH = join(
|
const EXECUTOR_ESCALATION_ATTEMPT_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0014_executor_escalation_attempt.sql",
|
"0014_executor_escalation_attempt.sql",
|
||||||
);
|
);
|
||||||
const GLOBAL_ROUTINES_MIGRATION_PATH = join(
|
const GLOBAL_ROUTINES_MIGRATION_PATH = join(
|
||||||
__dirname,
|
MIGRATIONS_DIR,
|
||||||
"migrations",
|
|
||||||
"0015_global_routines.sql",
|
"0015_global_routines.sql",
|
||||||
);
|
);
|
||||||
const TASK_MERGER_MODEL_LANE_MIGRATION_PATH = join(__dirname, "migrations", "0017_task_merger_model_lane.sql");
|
const TASK_MERGER_MODEL_LANE_MIGRATION_PATH = join(MIGRATIONS_DIR, "0017_task_merger_model_lane.sql");
|
||||||
const BULK_COMPLETION_REFUSAL_AT_MIGRATION_PATH = join(__dirname, "migrations", "0018_bulk_completion_refusal_at.sql");
|
const BULK_COMPLETION_REFUSAL_AT_MIGRATION_PATH = join(MIGRATIONS_DIR, "0018_bulk_completion_refusal_at.sql");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the migration bookkeeping table exists. Lives in the public schema so
|
* Ensure the migration bookkeeping table exists. Lives in the public schema so
|
||||||
|
|||||||
Reference in New Issue
Block a user