import { cp, mkdir, rm, writeFile, chmod } from "node:fs/promises";import { existsSync } from "node:fs";import { createRequire } from "node:module";import { resolve, join, dirname } from "node:path";import { fileURLToPath } from "node:url";import { parseArgs } from "node:util";import { $ } from "bun"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");const cliRoot = resolve(repoRoot, "apps/cli");const webRoot = resolve(repoRoot, "apps/local");const distDir = resolve(cliRoot, "dist"); const resolveQuickJsWasmPath = (): string => { const req = createRequire(join(repoRoot, "packages/kernel/runtime-quickjs/package.json")); const quickJsPkg = req.resolve("quickjs-emscripten/package.json"); const wasmPath = resolve( dirname(quickJsPkg), "../@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.wasm", ); if (!existsSync(wasmPath)) throw new Error(`QuickJS WASM not found at ${wasmPath}`); return wasmPath;}; // ---------------------------------------------------------------------------// Metadata// --------------------------------------------------------------------------- const readMetadata = async () => { const rootPkg = await Bun.file(join(repoRoot, "package.json")).json(); const cliPkg = await Bun.file(join(cliRoot, "package.json")).json(); return { name: "executor", version: process.env.EXECUTOR_VERSION ?? cliPkg.version ?? rootPkg.version ?? "0.0.0", description: rootPkg.description ?? "Local AI executor with a CLI, local API server, and web UI.", keywords: rootPkg.keywords ?? [], homepage: rootPkg.homepage, bugs: rootPkg.bugs, repository: rootPkg.repository, license: rootPkg.license ?? "MIT", };}; // ---------------------------------------------------------------------------// Targets// --------------------------------------------------------------------------- type Target = { os: "linux" | "darwin" | "win32"; arch: "x64" | "arm64"; abi?: "musl";}; const ALL_TARGETS: Target[] = [ { os: "linux", arch: "x64" }, { os: "linux", arch: "arm64" }, { os: "linux", arch: "x64", abi: "musl" }, { os: "linux", arch: "arm64", abi: "musl" }, { os: "darwin", arch: "x64" }, { os: "darwin", arch: "arm64" }, { os: "win32", arch: "x64" }, { os: "win32", arch: "arm64" },]; const platformName = (t: Target) => (t.os === "win32" ? "windows" : t.os); /** Per-platform suffix used in dist directory names, npm dist-tags, and the * semver prerelease segment of variant package versions. e.g. "linux-x64", * "linux-x64-musl", "darwin-arm64". */const platformTag = (t: Target) => [platformName(t), t.arch, t.abi].filter(Boolean).join("-"); /** Dist directory name (e.g. dist/executor-linux-x64). Only used as a build * artifact convention; the actual npm package name inside is `executor`. */const targetPackageName = (t: Target) => `executor-${platformTag(t)}`; const bunTargetKeys = [ "linux-x64", "linux-arm64", "linux-x64-musl", "linux-arm64-musl", "darwin-x64", "darwin-arm64", "win32-x64", "win32-arm64",] as const;type BunTargetKey = (typeof bunTargetKeys)[number]; const bunTargets = { "linux-x64": "bun-linux-x64", "linux-arm64": "bun-linux-arm64", "linux-x64-musl": "bun-linux-x64-musl", "linux-arm64-musl": "bun-linux-arm64-musl", "darwin-x64": "bun-darwin-x64", "darwin-arm64": "bun-darwin-arm64", "win32-x64": "bun-windows-x64", "win32-arm64": "bun-windows-arm64",} satisfies Record<BunTargetKey, Bun.Build.CompileTarget>; const isBunTargetKey = (key: string): key is BunTargetKey => bunTargetKeys.includes(key as BunTargetKey); const bunTarget = (t: Target): Bun.Build.CompileTarget => { const key = [t.os, t.arch, t.abi].filter(Boolean).join("-"); if (!isBunTargetKey(key)) throw new Error(`Unsupported Bun compile target: ${key}`); const target = bunTargets[key]; return target;}; const binaryName = (t: Target) => (t.os === "win32" ? "executor.exe" : "executor"); const isCurrentPlatform = (t: Target) => t.os === process.platform && t.arch === process.arch && !t.abi; /** * Resolve the platform-specific @napi-rs/keyring native binding for a target. * * `bun build --compile` doesn't include `.node` modules in bunfs, so the * keyring loader's dynamic `require('@napi-rs/keyring-<plat>-<arch>')` fails * at runtime. We copy the `.node` file next to the executor and main.ts sets * `NAPI_RS_NATIVE_LIBRARY_PATH` so the loader's env-var escape hatch picks it * up instead of trying to walk node_modules. */const resolveKeyringNative = (t: Target): string | null => { const platformMap: Record<string, { pkg: string; node: string }> = { "darwin-arm64": { pkg: "@napi-rs/keyring-darwin-arm64", node: "keyring.darwin-arm64.node" }, "darwin-x64": { pkg: "@napi-rs/keyring-darwin-x64", node: "keyring.darwin-x64.node" }, "linux-arm64": { pkg: "@napi-rs/keyring-linux-arm64-gnu", node: "keyring.linux-arm64-gnu.node", }, "linux-x64": { pkg: "@napi-rs/keyring-linux-x64-gnu", node: "keyring.linux-x64-gnu.node" }, "linux-arm64-musl": { pkg: "@napi-rs/keyring-linux-arm64-musl", node: "keyring.linux-arm64-musl.node", }, "linux-x64-musl": { pkg: "@napi-rs/keyring-linux-x64-musl", node: "keyring.linux-x64-musl.node", }, "win32-arm64": { pkg: "@napi-rs/keyring-win32-arm64-msvc", node: "keyring.win32-arm64-msvc.node", }, "win32-x64": { pkg: "@napi-rs/keyring-win32-x64-msvc", node: "keyring.win32-x64-msvc.node" }, }; const key = [t.os, t.arch, t.abi].filter(Boolean).join("-"); const entry = platformMap[key]; if (!entry) return null; try { const req = createRequire(join(repoRoot, "node_modules", "@napi-rs/keyring", "package.json")); const pkgJson = req.resolve(`${entry.pkg}/package.json`); return join(dirname(pkgJson), entry.node); } catch { const bunPath = join( repoRoot, `node_modules/.bun/${entry.pkg.replace("/", "+")}@1.2.0/node_modules/${entry.pkg}/${entry.node}`, ); if (existsSync(bunPath)) return bunPath; return null; }}; /** * Resolve the platform-specific `@libsql/<target>` native binding for a target. * * The local server's SQLite driver (libSQL) loads its `.node` via a dynamic * `require('@libsql/<target>')`, which `bun build --compile` can't bundle into * bunfs (same limitation as keyring). We copy the right `.node` next to the * executor as `libsql.node`; main.ts redirects the bare require to it. */const LIBSQL_NATIVE_VERSION = "0.5.29";const resolveLibsqlNative = (t: Target): string | null => { const platformMap: Record<string, string> = { "darwin-arm64": "darwin-arm64", "darwin-x64": "darwin-x64", // The compiled binary runs on Bun, which libSQL's loader treats as glibc // (its musl->gnu workaround), so non-musl linux targets need the -gnu binding. "linux-arm64": "linux-arm64-gnu", "linux-x64": "linux-x64-gnu", "linux-arm64-musl": "linux-arm64-musl", "linux-x64-musl": "linux-x64-musl", "win32-arm64": "win32-arm64-msvc", "win32-x64": "win32-x64-msvc", }; const key = [t.os, t.arch, t.abi].filter(Boolean).join("-"); const target = platformMap[key]; if (!target) return null; const pkg = `@libsql/${target}`; try { const req = createRequire(join(repoRoot, "apps/local", "package.json")); const pkgJson = req.resolve(`${pkg}/package.json`); return join(dirname(pkgJson), "index.node"); } catch { const bunPath = join( repoRoot, `node_modules/.bun/${pkg.replace("/", "+")}@${LIBSQL_NATIVE_VERSION}/node_modules/${pkg}/index.node`, ); return existsSync(bunPath) ? bunPath : null; }}; // ---------------------------------------------------------------------------// Build mode// --------------------------------------------------------------------------- type BuildMode = "production" | "development"; // ---------------------------------------------------------------------------// Build web app// --------------------------------------------------------------------------- const buildWeb = async (mode: BuildMode) => { const webDist = join(webRoot, "dist"); await rm(webDist, { recursive: true, force: true }); console.log(`Building web app (${mode})...`); const proc = Bun.spawn(["bun", "run", "build", "--mode", mode], { cwd: webRoot, stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, NODE_ENV: mode }, }); if ((await proc.exited) !== 0) throw new Error("Web build failed"); return webDist;}; // ---------------------------------------------------------------------------// Embedded web UI — generates a virtual module that imports all web assets// using `with { type: "file" }` so Bun bakes them into the compiled binary.// --------------------------------------------------------------------------- const createEmbeddedWebUISource = async (mode: BuildMode) => { const webDist = await buildWeb(mode); const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: webDist }))) .map((f) => f.replaceAll("\\", "/")) .sort(); const imports = files.map((file, i) => { const spec = join(webDist, file).replaceAll("\\", "/"); return `import file_${i} from ${JSON.stringify(spec)} with { type: "file" };`; }); const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`); return [ "// Auto-generated — maps web UI paths to embedded file references", ...imports, "export default {", ...entries, "} as Record<string, string>;", ].join("\n");}; // ---------------------------------------------------------------------------// Embedded legacy v1 drizzle migrations — inlined as text imports so drizzle's// `migrate()` (which reads a folder from disk) can be given a tmpdir// populated from the inlined contents at runtime. The v1→v2 data migration// replays this chain to bring an older v1 database up to v1-final before// reading it (v2's own schema is created from FumaDB DDL, not this folder).// --------------------------------------------------------------------------- const createEmbeddedMigrationsSource = async () => { const migrationsDir = resolve(webRoot, "drizzle-legacy-v1"); const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: migrationsDir }))) .map((f) => f.replaceAll("\\", "/")) .sort(); const imports = files.map((file, i) => { const spec = join(migrationsDir, file).replaceAll("\\", "/"); return `import file_${i} from ${JSON.stringify(spec)} with { type: "text" };`; }); const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`); return [ "// Auto-generated — maps migration paths to inlined file contents", ...imports, "export default {", ...entries, "} as Record<string, string>;", ].join("\n");}; // ---------------------------------------------------------------------------// Build platform binaries// --------------------------------------------------------------------------- const EMBEDDED_WEB_UI_STUB = `const files: Record<string, string> | null = null;\n\nexport default files;\n`;const EMBEDDED_MIGRATIONS_STUB = `const migrations: Record<string, string> | null = null;\n\nexport default migrations;\n`; const buildBinaries = async (targets: Target[], mode: BuildMode) => { const meta = await readMetadata(); const binaries: Record<string, string> = {}; const embeddedWebUIPath = join(cliRoot, "src/embedded-web-ui.gen.ts"); const embeddedMigrationsPath = join(webRoot, "src/db/embedded-migrations.gen.ts"); await rm(distDir, { recursive: true, force: true }); // Cross-platform builds need every target's optional native packages // (e.g. @napi-rs/keyring-darwin-arm64) so we can copy the right .node next // to each target's executor. `bun install --frozen-lockfile --cpu=* --os=*` // extracts them all without modifying the lockfile. const needsCrossPlatform = targets.some((t) => !isCurrentPlatform(t)); if (needsCrossPlatform) { console.log("Installing optional native deps for all platforms..."); const proc = Bun.spawn(["bun", "install", "--frozen-lockfile", "--cpu=*", "--os=*"], { cwd: repoRoot, stdio: ["ignore", "inherit", "inherit"], }); if ((await proc.exited) !== 0) { throw new Error("bun install --cpu=* --os=* failed"); } } console.log(`Generating embedded web UI bundle (${mode})...`); const embeddedWebUI = await createEmbeddedWebUISource(mode); await writeFile(embeddedWebUIPath, `${embeddedWebUI}\n`); console.log("Generating embedded drizzle migrations..."); const embeddedMigrations = await createEmbeddedMigrationsSource(); await writeFile(embeddedMigrationsPath, `${embeddedMigrations}\n`); const quickJsWasmPath = resolveQuickJsWasmPath(); try { for (const target of targets) { const name = targetPackageName(target); const outDir = join(distDir, name); const binDir = join(outDir, "bin"); await mkdir(binDir, { recursive: true }); console.log(`Building ${name}...`); await Bun.build({ entrypoints: [join(cliRoot, "src/main.ts")], minify: mode === "production", compile: { target: bunTarget(target), outfile: join(binDir, binaryName(target)), }, }); // Copy QuickJS WASM next to binary — loaded at runtime by the server await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm")); // Copy @napi-rs/keyring native binding next to executor — bun --compile // doesn't bundle .node files, so the loader needs to find it on disk // via NAPI_RS_NATIVE_LIBRARY_PATH (set in main.ts). const keyringNative = resolveKeyringNative(target); if (keyringNative && existsSync(keyringNative)) { await cp(keyringNative, join(binDir, "keyring.node")); } // Copy the libSQL native binding next to executor — same bunfs limitation // as keyring; main.ts redirects `require('@libsql/<plat>')` to it. const libsqlNative = resolveLibsqlNative(target); if (libsqlNative && existsSync(libsqlNative)) { await cp(libsqlNative, join(binDir, "libsql.node")); } // Smoke test on current platform if (isCurrentPlatform(target)) { const bin = join(binDir, binaryName(target)); console.log(` Smoke test: ${bin} --version`); const version = await $`${bin} --version`.text(); console.log(` OK: ${version.trim()}`); } // Variant package.json. All variants publish to the SAME npm package // name (`executor`) under platform-tagged versions (e.g. // `1.4.14-linux-x64`). The wrapper references each variant via an // `npm:` alias in its optionalDependencies. This mirrors codex's // pattern and avoids ever having to claim a new npm package name when // a new platform is added — a single trusted-publishing config on the // `executor` package covers everything. // // No `bin` field on purpose — the wrapper's launcher resolves the // platform binary via require.resolve on the alias name and execs it. const tag = platformTag(target); const variantVersion = `${meta.version}-${tag}`; await writeFile( join(outDir, "package.json"), JSON.stringify( { name: meta.name, version: variantVersion, description: `${meta.description} (${tag})`, os: [target.os], cpu: [target.arch], homepage: meta.homepage, bugs: meta.bugs, repository: meta.repository, license: meta.license, }, null, 2, ) + "\n", ); // The local alias name (`executor-linux-x64`, ...) is what the // wrapper's launcher passes to require.resolve at runtime. It's // bound at install time by npm's `npm:executor@<variant-version>` // alias spec in optionalDependencies. const aliasName = `${meta.name}-${tag}`; binaries[aliasName] = `npm:${meta.name}@${variantVersion}`; } return binaries; } finally { await writeFile(embeddedWebUIPath, EMBEDDED_WEB_UI_STUB); await writeFile(embeddedMigrationsPath, EMBEDDED_MIGRATIONS_STUB); }}; // ---------------------------------------------------------------------------// Build wrapper npm package// --------------------------------------------------------------------------- const buildWrapperPackage = async (binaries: Record<string, string>) => { const meta = await readMetadata(); const wrapperDir = join(distDir, meta.name); const binDir = join(wrapperDir, "bin"); await mkdir(binDir, { recursive: true }); // Node.js launcher — resolves the platform binary via require.resolve // against optionalDependencies and execs it. No postinstall: the binary // ships as an os/cpu-filtered optional dep, the launcher resolves it at // runtime. This works whether or not the package manager runs postinstalls // (bun blocks them by default). await writeFile(join(binDir, "executor"), NODE_SHIM); await chmod(join(binDir, "executor"), 0o755); await writeFile( join(wrapperDir, "package.json"), JSON.stringify( { name: meta.name, version: meta.version, description: meta.description, keywords: meta.keywords, homepage: meta.homepage, bugs: meta.bugs, repository: meta.repository, license: meta.license, bin: { executor: "bin/executor" }, // Per-platform compiled binaries published as platform-tagged // versions of `executor` itself, referenced via npm:alias specs: // "executor-linux-x64": "npm:executor@1.4.14-linux-x64" // npm/bun fetch only the variant matching the current os/cpu (set // on each variant's package.json); everything else is a no-op. // The local alias name on the left is what the launcher passes to // require.resolve at runtime — npm puts the variant at // `node_modules/executor-<plat>-<arch>/` so resolution just works. optionalDependencies: binaries, engines: { node: ">=20", }, }, null, 2, ) + "\n", ); const readmePath = join(repoRoot, "README.md"); if (existsSync(readmePath)) { await cp(readmePath, join(wrapperDir, "README.md")); } console.log(`\nWrapper package: ${wrapperDir}`); console.log(` ${meta.name}@${meta.version}`); console.log(` optionalDependencies: ${Object.keys(binaries).join(", ")}`);}; // ---------------------------------------------------------------------------// Preview wrapper — a slim npm package for pkg.pr.new previews that fetches// the platform binary from our R2 bucket at install time.//// The release wrapper points postinstall at GitHub Releases. For previews// there's no release, so we upload per-PR tarballs to R2 (keyed by commit// SHA) and have a dedicated postinstall download from there.//// CI splits this in two:// 1. A matrix job per platform runs `buildPreviewTarballs` to produce// dist/previews/<platform>.tar.gz, which it uploads to R2.// 2. A single publish job runs `buildPreviewWrapperPackage` with an// explicit list of platforms and hands the wrapper to pkg.pr.new.//// Both paths require EXECUTOR_PREVIEW_CDN_URL and EXECUTOR_PREVIEW_SHA so// the postinstall URL embeds the right commit.// --------------------------------------------------------------------------- const buildPreviewTarballs = async (binaries: Record<string, string>) => { const previewDir = join(distDir, "previews"); await mkdir(previewDir, { recursive: true }); for (const platformPkg of Object.keys(binaries)) { const srcBinDir = join(distDir, platformPkg, "bin"); const tarPath = join(previewDir, `${platformPkg}.tar.gz`); await $`tar -czf ${tarPath} .`.cwd(srcBinDir).quiet(); console.log(`Preview tarball: ${tarPath}`);Lines 1–500 of 970. Load more
import { cp, mkdir, rm, writeFile, chmod } from "node:fs/promises"; import { existsSync } from "node:fs"; import { createRequire } from "node:module"; import { resolve, join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; import { $ } from "bun"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); const cliRoot = resolve(repoRoot, "apps/cli"); const webRoot = resolve(repoRoot, "apps/local"); const distDir = resolve(cliRoot, "dist"); const resolveQuickJsWasmPath = (): string => { const req = createRequire(join(repoRoot, "packages/kernel/runtime-quickjs/package.json")); const quickJsPkg = req.resolve("quickjs-emscripten/package.json"); const wasmPath = resolve( dirname(quickJsPkg), "../@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.wasm", ); if (!existsSync(wasmPath)) throw new Error(`QuickJS WASM not found at ${wasmPath}`); return wasmPath; }; // --------------------------------------------------------------------------- // Metadata // --------------------------------------------------------------------------- const readMetadata = async () => { const rootPkg = await Bun.file(join(repoRoot, "package.json")).json(); const cliPkg = await Bun.file(join(cliRoot, "package.json")).json(); return { name: "executor", version: process.env.EXECUTOR_VERSION ?? cliPkg.version ?? rootPkg.version ?? "0.0.0", description: rootPkg.description ?? "Local AI executor with a CLI, local API server, and web UI.", keywords: rootPkg.keywords ?? [], homepage: rootPkg.homepage, bugs: rootPkg.bugs, repository: rootPkg.repository, license: rootPkg.license ?? "MIT", }; }; // --------------------------------------------------------------------------- // Targets // --------------------------------------------------------------------------- type Target = { os: "linux" | "darwin" | "win32"; arch: "x64" | "arm64"; abi?: "musl"; }; const ALL_TARGETS: Target[] = [ { os: "linux", arch: "x64" }, { os: "linux", arch: "arm64" }, { os: "linux", arch: "x64", abi: "musl" }, { os: "linux", arch: "arm64", abi: "musl" }, { os: "darwin", arch: "x64" }, { os: "darwin", arch: "arm64" }, { os: "win32", arch: "x64" }, { os: "win32", arch: "arm64" }, ]; const platformName = (t: Target) => (t.os === "win32" ? "windows" : t.os); /** Per-platform suffix used in dist directory names, npm dist-tags, and the * semver prerelease segment of variant package versions. e.g. "linux-x64", * "linux-x64-musl", "darwin-arm64". */ const platformTag = (t: Target) => [platformName(t), t.arch, t.abi].filter(Boolean).join("-"); /** Dist directory name (e.g. dist/executor-linux-x64). Only used as a build * artifact convention; the actual npm package name inside is `executor`. */ const targetPackageName = (t: Target) => `executor-${platformTag(t)}`; const bunTargetKeys = [ "linux-x64", "linux-arm64", "linux-x64-musl", "linux-arm64-musl", "darwin-x64", "darwin-arm64", "win32-x64", "win32-arm64", ] as const; type BunTargetKey = (typeof bunTargetKeys)[number]; const bunTargets = { "linux-x64": "bun-linux-x64", "linux-arm64": "bun-linux-arm64", "linux-x64-musl": "bun-linux-x64-musl", "linux-arm64-musl": "bun-linux-arm64-musl", "darwin-x64": "bun-darwin-x64", "darwin-arm64": "bun-darwin-arm64", "win32-x64": "bun-windows-x64", "win32-arm64": "bun-windows-arm64", } satisfies Record<BunTargetKey, Bun.Build.CompileTarget>; const isBunTargetKey = (key: string): key is BunTargetKey => bunTargetKeys.includes(key as BunTargetKey); const bunTarget = (t: Target): Bun.Build.CompileTarget => { const key = [t.os, t.arch, t.abi].filter(Boolean).join("-"); if (!isBunTargetKey(key)) throw new Error(`Unsupported Bun compile target: ${key}`); const target = bunTargets[key]; return target; }; const binaryName = (t: Target) => (t.os === "win32" ? "executor.exe" : "executor"); const isCurrentPlatform = (t: Target) => t.os === process.platform && t.arch === process.arch && !t.abi; /** * Resolve the platform-specific @napi-rs/keyring native binding for a target. * * `bun build --compile` doesn't include `.node` modules in bunfs, so the * keyring loader's dynamic `require('@napi-rs/keyring-<plat>-<arch>')` fails * at runtime. We copy the `.node` file next to the executor and main.ts sets * `NAPI_RS_NATIVE_LIBRARY_PATH` so the loader's env-var escape hatch picks it * up instead of trying to walk node_modules. */ const resolveKeyringNative = (t: Target): string | null => { const platformMap: Record<string, { pkg: string; node: string }> = { "darwin-arm64": { pkg: "@napi-rs/keyring-darwin-arm64", node: "keyring.darwin-arm64.node" }, "darwin-x64": { pkg: "@napi-rs/keyring-darwin-x64", node: "keyring.darwin-x64.node" }, "linux-arm64": { pkg: "@napi-rs/keyring-linux-arm64-gnu", node: "keyring.linux-arm64-gnu.node", }, "linux-x64": { pkg: "@napi-rs/keyring-linux-x64-gnu", node: "keyring.linux-x64-gnu.node" }, "linux-arm64-musl": { pkg: "@napi-rs/keyring-linux-arm64-musl", node: "keyring.linux-arm64-musl.node", }, "linux-x64-musl": { pkg: "@napi-rs/keyring-linux-x64-musl", node: "keyring.linux-x64-musl.node", }, "win32-arm64": { pkg: "@napi-rs/keyring-win32-arm64-msvc", node: "keyring.win32-arm64-msvc.node", }, "win32-x64": { pkg: "@napi-rs/keyring-win32-x64-msvc", node: "keyring.win32-x64-msvc.node" }, }; const key = [t.os, t.arch, t.abi].filter(Boolean).join("-"); const entry = platformMap[key]; if (!entry) return null; try { const req = createRequire(join(repoRoot, "node_modules", "@napi-rs/keyring", "package.json")); const pkgJson = req.resolve(`${entry.pkg}/package.json`); return join(dirname(pkgJson), entry.node); } catch { const bunPath = join( repoRoot, `node_modules/.bun/${entry.pkg.replace("/", "+")}@1.2.0/node_modules/${entry.pkg}/${entry.node}`, ); if (existsSync(bunPath)) return bunPath; return null; } }; /** * Resolve the platform-specific `@libsql/<target>` native binding for a target. * * The local server's SQLite driver (libSQL) loads its `.node` via a dynamic * `require('@libsql/<target>')`, which `bun build --compile` can't bundle into * bunfs (same limitation as keyring). We copy the right `.node` next to the * executor as `libsql.node`; main.ts redirects the bare require to it. */ const LIBSQL_NATIVE_VERSION = "0.5.29"; const resolveLibsqlNative = (t: Target): string | null => { const platformMap: Record<string, string> = { "darwin-arm64": "darwin-arm64", "darwin-x64": "darwin-x64", // The compiled binary runs on Bun, which libSQL's loader treats as glibc // (its musl->gnu workaround), so non-musl linux targets need the -gnu binding. "linux-arm64": "linux-arm64-gnu", "linux-x64": "linux-x64-gnu", "linux-arm64-musl": "linux-arm64-musl", "linux-x64-musl": "linux-x64-musl", "win32-arm64": "win32-arm64-msvc", "win32-x64": "win32-x64-msvc", }; const key = [t.os, t.arch, t.abi].filter(Boolean).join("-"); const target = platformMap[key]; if (!target) return null; const pkg = `@libsql/${target}`; try { const req = createRequire(join(repoRoot, "apps/local", "package.json")); const pkgJson = req.resolve(`${pkg}/package.json`); return join(dirname(pkgJson), "index.node"); } catch { const bunPath = join( repoRoot, `node_modules/.bun/${pkg.replace("/", "+")}@${LIBSQL_NATIVE_VERSION}/node_modules/${pkg}/index.node`, ); return existsSync(bunPath) ? bunPath : null; } }; // --------------------------------------------------------------------------- // Build mode // --------------------------------------------------------------------------- type BuildMode = "production" | "development"; // --------------------------------------------------------------------------- // Build web app // --------------------------------------------------------------------------- const buildWeb = async (mode: BuildMode) => { const webDist = join(webRoot, "dist"); await rm(webDist, { recursive: true, force: true }); console.log(`Building web app (${mode})...`); const proc = Bun.spawn(["bun", "run", "build", "--mode", mode], { cwd: webRoot, stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, NODE_ENV: mode }, }); if ((await proc.exited) !== 0) throw new Error("Web build failed"); return webDist; }; // --------------------------------------------------------------------------- // Embedded web UI — generates a virtual module that imports all web assets // using `with { type: "file" }` so Bun bakes them into the compiled binary. // --------------------------------------------------------------------------- const createEmbeddedWebUISource = async (mode: BuildMode) => { const webDist = await buildWeb(mode); const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: webDist }))) .map((f) => f.replaceAll("\\", "/")) .sort(); const imports = files.map((file, i) => { const spec = join(webDist, file).replaceAll("\\", "/"); return `import file_${i} from ${JSON.stringify(spec)} with { type: "file" };`; }); const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`); return [ "// Auto-generated — maps web UI paths to embedded file references", ...imports, "export default {", ...entries, "} as Record<string, string>;", ].join("\n"); }; // --------------------------------------------------------------------------- // Embedded legacy v1 drizzle migrations — inlined as text imports so drizzle's // `migrate()` (which reads a folder from disk) can be given a tmpdir // populated from the inlined contents at runtime. The v1→v2 data migration // replays this chain to bring an older v1 database up to v1-final before // reading it (v2's own schema is created from FumaDB DDL, not this folder). // --------------------------------------------------------------------------- const createEmbeddedMigrationsSource = async () => { const migrationsDir = resolve(webRoot, "drizzle-legacy-v1"); const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: migrationsDir }))) .map((f) => f.replaceAll("\\", "/")) .sort(); const imports = files.map((file, i) => { const spec = join(migrationsDir, file).replaceAll("\\", "/"); return `import file_${i} from ${JSON.stringify(spec)} with { type: "text" };`; }); const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`); return [ "// Auto-generated — maps migration paths to inlined file contents", ...imports, "export default {", ...entries, "} as Record<string, string>;", ].join("\n"); }; // --------------------------------------------------------------------------- // Build platform binaries // --------------------------------------------------------------------------- const EMBEDDED_WEB_UI_STUB = `const files: Record<string, string> | null = null;\n\nexport default files;\n`; const EMBEDDED_MIGRATIONS_STUB = `const migrations: Record<string, string> | null = null;\n\nexport default migrations;\n`; const buildBinaries = async (targets: Target[], mode: BuildMode) => { const meta = await readMetadata(); const binaries: Record<string, string> = {}; const embeddedWebUIPath = join(cliRoot, "src/embedded-web-ui.gen.ts"); const embeddedMigrationsPath = join(webRoot, "src/db/embedded-migrations.gen.ts"); await rm(distDir, { recursive: true, force: true }); // Cross-platform builds need every target's optional native packages // (e.g. @napi-rs/keyring-darwin-arm64) so we can copy the right .node next // to each target's executor. `bun install --frozen-lockfile --cpu=* --os=*` // extracts them all without modifying the lockfile. const needsCrossPlatform = targets.some((t) => !isCurrentPlatform(t)); if (needsCrossPlatform) { console.log("Installing optional native deps for all platforms..."); const proc = Bun.spawn(["bun", "install", "--frozen-lockfile", "--cpu=*", "--os=*"], { cwd: repoRoot, stdio: ["ignore", "inherit", "inherit"], }); if ((await proc.exited) !== 0) { throw new Error("bun install --cpu=* --os=* failed"); } } console.log(`Generating embedded web UI bundle (${mode})...`); const embeddedWebUI = await createEmbeddedWebUISource(mode); await writeFile(embeddedWebUIPath, `${embeddedWebUI}\n`); console.log("Generating embedded drizzle migrations..."); const embeddedMigrations = await createEmbeddedMigrationsSource(); await writeFile(embeddedMigrationsPath, `${embeddedMigrations}\n`); const quickJsWasmPath = resolveQuickJsWasmPath(); try { for (const target of targets) { const name = targetPackageName(target); const outDir = join(distDir, name); const binDir = join(outDir, "bin"); await mkdir(binDir, { recursive: true }); console.log(`Building ${name}...`); await Bun.build({ entrypoints: [join(cliRoot, "src/main.ts")], minify: mode === "production", compile: { target: bunTarget(target), outfile: join(binDir, binaryName(target)), }, }); // Copy QuickJS WASM next to binary — loaded at runtime by the server await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm")); // Copy @napi-rs/keyring native binding next to executor — bun --compile // doesn't bundle .node files, so the loader needs to find it on disk // via NAPI_RS_NATIVE_LIBRARY_PATH (set in main.ts). const keyringNative = resolveKeyringNative(target); if (keyringNative && existsSync(keyringNative)) { await cp(keyringNative, join(binDir, "keyring.node")); } // Copy the libSQL native binding next to executor — same bunfs limitation // as keyring; main.ts redirects `require('@libsql/<plat>')` to it. const libsqlNative = resolveLibsqlNative(target); if (libsqlNative && existsSync(libsqlNative)) { await cp(libsqlNative, join(binDir, "libsql.node")); } // Smoke test on current platform if (isCurrentPlatform(target)) { const bin = join(binDir, binaryName(target)); console.log(` Smoke test: ${bin} --version`); const version = await $`${bin} --version`.text(); console.log(` OK: ${version.trim()}`); } // Variant package.json. All variants publish to the SAME npm package // name (`executor`) under platform-tagged versions (e.g. // `1.4.14-linux-x64`). The wrapper references each variant via an // `npm:` alias in its optionalDependencies. This mirrors codex's // pattern and avoids ever having to claim a new npm package name when // a new platform is added — a single trusted-publishing config on the // `executor` package covers everything. // // No `bin` field on purpose — the wrapper's launcher resolves the // platform binary via require.resolve on the alias name and execs it. const tag = platformTag(target); const variantVersion = `${meta.version}-${tag}`; await writeFile( join(outDir, "package.json"), JSON.stringify( { name: meta.name, version: variantVersion, description: `${meta.description} (${tag})`, os: [target.os], cpu: [target.arch], homepage: meta.homepage, bugs: meta.bugs, repository: meta.repository, license: meta.license, }, null, 2, ) + "\n", ); // The local alias name (`executor-linux-x64`, ...) is what the // wrapper's launcher passes to require.resolve at runtime. It's // bound at install time by npm's `npm:executor@<variant-version>` // alias spec in optionalDependencies. const aliasName = `${meta.name}-${tag}`; binaries[aliasName] = `npm:${meta.name}@${variantVersion}`; } return binaries; } finally { await writeFile(embeddedWebUIPath, EMBEDDED_WEB_UI_STUB); await writeFile(embeddedMigrationsPath, EMBEDDED_MIGRATIONS_STUB); } }; // --------------------------------------------------------------------------- // Build wrapper npm package // --------------------------------------------------------------------------- const buildWrapperPackage = async (binaries: Record<string, string>) => { const meta = await readMetadata(); const wrapperDir = join(distDir, meta.name); const binDir = join(wrapperDir, "bin"); await mkdir(binDir, { recursive: true }); // Node.js launcher — resolves the platform binary via require.resolve // against optionalDependencies and execs it. No postinstall: the binary // ships as an os/cpu-filtered optional dep, the launcher resolves it at // runtime. This works whether or not the package manager runs postinstalls // (bun blocks them by default). await writeFile(join(binDir, "executor"), NODE_SHIM); await chmod(join(binDir, "executor"), 0o755); await writeFile( join(wrapperDir, "package.json"), JSON.stringify( { name: meta.name, version: meta.version, description: meta.description, keywords: meta.keywords, homepage: meta.homepage, bugs: meta.bugs, repository: meta.repository, license: meta.license, bin: { executor: "bin/executor" }, // Per-platform compiled binaries published as platform-tagged // versions of `executor` itself, referenced via npm:alias specs: // "executor-linux-x64": "npm:executor@1.4.14-linux-x64" // npm/bun fetch only the variant matching the current os/cpu (set // on each variant's package.json); everything else is a no-op. // The local alias name on the left is what the launcher passes to // require.resolve at runtime — npm puts the variant at // `node_modules/executor-<plat>-<arch>/` so resolution just works. optionalDependencies: binaries, engines: { node: ">=20", }, }, null, 2, ) + "\n", ); const readmePath = join(repoRoot, "README.md"); if (existsSync(readmePath)) { await cp(readmePath, join(wrapperDir, "README.md")); } console.log(`\nWrapper package: ${wrapperDir}`); console.log(` ${meta.name}@${meta.version}`); console.log(` optionalDependencies: ${Object.keys(binaries).join(", ")}`); }; // --------------------------------------------------------------------------- // Preview wrapper — a slim npm package for pkg.pr.new previews that fetches // the platform binary from our R2 bucket at install time. // // The release wrapper points postinstall at GitHub Releases. For previews // there's no release, so we upload per-PR tarballs to R2 (keyed by commit // SHA) and have a dedicated postinstall download from there. // // CI splits this in two: // 1. A matrix job per platform runs `buildPreviewTarballs` to produce // dist/previews/<platform>.tar.gz, which it uploads to R2. // 2. A single publish job runs `buildPreviewWrapperPackage` with an // explicit list of platforms and hands the wrapper to pkg.pr.new. // // Both paths require EXECUTOR_PREVIEW_CDN_URL and EXECUTOR_PREVIEW_SHA so // the postinstall URL embeds the right commit. // --------------------------------------------------------------------------- const buildPreviewTarballs = async (binaries: Record<string, string>) => { const previewDir = join(distDir, "previews"); await mkdir(previewDir, { recursive: true }); for (const platformPkg of Object.keys(binaries)) { const srcBinDir = join(distDir, platformPkg, "bin"); const tarPath = join(previewDir, `${platformPkg}.tar.gz`); await $`tar -czf ${tarPath} .`.cwd(srcBinDir).quiet(); console.log(`Preview tarball: ${tarPath}`);