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 ${{ 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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -117,6 +158,9 @@ jobs:
|
||||
path: |
|
||||
packages/cli/dist/${{ matrix.binary }}
|
||||
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/**/*
|
||||
|
||||
# ── Build Windows desktop EXE artifacts ──────────────────────────────
|
||||
@@ -568,7 +612,14 @@ jobs:
|
||||
id: collect
|
||||
run: |
|
||||
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/
|
||||
count=$(find release-files -type f | wc -l | tr -d ' ')
|
||||
echo "count=$count" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -212,6 +212,165 @@ function ensureClientAssets(): ClientAssetMode {
|
||||
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 ─────────────────
|
||||
/**
|
||||
* 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-/, "")
|
||||
: hostPrebuildName();
|
||||
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
|
||||
const nativeAssetDir = join(runtimeDir, prebuildName);
|
||||
@@ -382,6 +545,9 @@ const { targets } = parseArgs();
|
||||
|
||||
// Stage assets once (shared across all binaries)
|
||||
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) {
|
||||
// Default: build for current platform → dist/fn
|
||||
|
||||
@@ -84,6 +84,107 @@ export { isWindowsElevatedAdmin } from "./embedded-windows-admin.js";
|
||||
|
||||
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([
|
||||
"postgres",
|
||||
"initdb",
|
||||
@@ -505,7 +606,10 @@ function getEmbeddedPostgresCtor(): EmbeddedPostgresCtor {
|
||||
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 };
|
||||
// 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);
|
||||
return embeddedPostgresCtorCache;
|
||||
}
|
||||
@@ -684,7 +788,7 @@ function findPnpmVirtualStore(start: string): string | null {
|
||||
|
||||
function resolvePnpmPlatformPackageNativeRoot(packageName: string): string | null {
|
||||
try {
|
||||
const embeddedEntrypoint = require.resolve("embedded-postgres");
|
||||
const embeddedEntrypoint = resolveEmbeddedPostgresSpecifier("embedded-postgres");
|
||||
const virtualStore = findPnpmVirtualStore(dirname(embeddedEntrypoint));
|
||||
if (!virtualStore) return null;
|
||||
const encodedName = packageName.replace("/", "+");
|
||||
@@ -722,7 +826,10 @@ function resolveGenericEmbeddedPostgresNativeRoot(): string | null {
|
||||
const packageName = embeddedPostgresPlatformPackageName();
|
||||
if (!packageName) return null;
|
||||
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"));
|
||||
} catch {
|
||||
return resolvePnpmPlatformPackageNativeRoot(packageName);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
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";
|
||||
|
||||
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(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0001_automation_project_isolation.sql",
|
||||
);
|
||||
const ANALYTICS_ISOLATION_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0002_analytics_project_isolation.sql",
|
||||
);
|
||||
const MONITOR_APPROVAL_ISOLATION_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0003_monitor_approval_project_isolation.sql",
|
||||
);
|
||||
const LEGACY_CUTOVER_PRESERVATION_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0004_legacy_cutover_preservation.sql",
|
||||
);
|
||||
const MULTI_PROJECT_CUTOVER_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0005_multi_project_cutover.sql",
|
||||
);
|
||||
const PROJECT_OWNERSHIP_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0006_project_ownership.sql",
|
||||
);
|
||||
const SQLITE_SCHEMA_PARITY_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0007_sqlite_schema_parity.sql",
|
||||
);
|
||||
const SESSION_ADVISOR_ENABLED_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0008_session_advisor_enabled.sql",
|
||||
);
|
||||
const MISSION_FIX_IDEMPOTENCY_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0009_mission_fix_idempotency.sql",
|
||||
);
|
||||
const IMPORT_TRANSLATION_CACHE_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0010_import_translation_cache.sql",
|
||||
);
|
||||
const IMPORT_TRANSLATION_CACHE_SCOPE_FIX_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0016_import_translation_cache_scope_fix.sql",
|
||||
);
|
||||
const IMPORT_TRANSLATION_CACHE_LEGACY_PARTITION_BACKFILL_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0019_import_translation_cache_legacy_partition_backfill.sql",
|
||||
);
|
||||
const OWNER_PROJECT_ID_SPLIT_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0011_owner_project_id.sql",
|
||||
);
|
||||
const CHAT_SESSION_PINS_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0012_chat_session_pins.sql",
|
||||
);
|
||||
const EXECUTOR_TOOL_FAILURE_RETRY_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0013_executor_tool_failure_retry.sql",
|
||||
);
|
||||
const EXECUTOR_ESCALATION_ATTEMPT_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0014_executor_escalation_attempt.sql",
|
||||
);
|
||||
const GLOBAL_ROUTINES_MIGRATION_PATH = join(
|
||||
__dirname,
|
||||
"migrations",
|
||||
MIGRATIONS_DIR,
|
||||
"0015_global_routines.sql",
|
||||
);
|
||||
const TASK_MERGER_MODEL_LANE_MIGRATION_PATH = join(__dirname, "migrations", "0017_task_merger_model_lane.sql");
|
||||
const BULK_COMPLETION_REFUSAL_AT_MIGRATION_PATH = join(__dirname, "migrations", "0018_bulk_completion_refusal_at.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(MIGRATIONS_DIR, "0018_bulk_completion_refusal_at.sql");
|
||||
|
||||
/**
|
||||
* Ensure the migration bookkeeping table exists. Lives in the public schema so
|
||||
|
||||
Reference in New Issue
Block a user