diff --git a/packages/opencode/bin/altimate b/packages/opencode/bin/altimate index c842b49deb..dea3eecea6 100755 --- a/packages/opencode/bin/altimate +++ b/packages/opencode/bin/altimate @@ -5,9 +5,55 @@ const fs = require("fs") const path = require("path") const os = require("os") +// Resolve script location early — needed by both run() and findBinary(). +const scriptPath = fs.realpathSync(__filename) +const scriptDir = path.dirname(scriptPath) + +// Collect ALL node_modules directories walking upward from startDir. +// Bun's single-file executable uses a virtual filesystem (/$bunfs/root/) without +// node_modules. External packages are resolved via NODE_PATH instead. +// We collect every node_modules in the hierarchy (not just the first) to handle +// pnpm strict layouts, hoisted monorepos, and npm flat installs alike. +function findAllNodeModules(startDir) { + const paths = [] + let current = startDir + for (;;) { + const modules = path.join(current, "node_modules") + if (fs.existsSync(modules)) paths.push(modules) + const parent = path.dirname(current) + if (parent === current) break + current = parent + } + return paths +} + function run(target) { + // Resolve NODE_PATH so the compiled Bun binary can find external packages + // installed alongside the wrapper (e.g. @altimateai/altimate-core NAPI module). + // Search from BOTH the binary's location AND the wrapper script's location + // to cover npm flat installs, pnpm isolated stores, and hoisted monorepos. + const env = { ...process.env } + try { + const resolvedTarget = fs.realpathSync(target) + const targetDir = path.dirname(path.dirname(resolvedTarget)) + + const targetModules = findAllNodeModules(targetDir) + const scriptModules = findAllNodeModules(scriptDir) + const allPaths = [...new Set([...scriptModules, ...targetModules])] + + if (allPaths.length > 0) { + const sep = process.platform === "win32" ? ";" : ":" + const joined = allPaths.join(sep) + env.NODE_PATH = env.NODE_PATH ? joined + sep + env.NODE_PATH : joined + } + } catch { + // realpathSync failed (e.g. target doesn't exist) — continue without + // NODE_PATH; spawnSync will report the missing binary via result.error. + } + const result = childProcess.spawnSync(target, process.argv.slice(2), { stdio: "inherit", + env, }) if (result.error) { console.error(result.error.message) @@ -22,9 +68,6 @@ if (envPath) { run(envPath) } -const scriptPath = fs.realpathSync(__filename) -const scriptDir = path.dirname(scriptPath) - // const cached = path.join(scriptDir, ".altimate-code") if (fs.existsSync(cached)) { diff --git a/packages/opencode/bin/altimate-code b/packages/opencode/bin/altimate-code index c842b49deb..dea3eecea6 100755 --- a/packages/opencode/bin/altimate-code +++ b/packages/opencode/bin/altimate-code @@ -5,9 +5,55 @@ const fs = require("fs") const path = require("path") const os = require("os") +// Resolve script location early — needed by both run() and findBinary(). +const scriptPath = fs.realpathSync(__filename) +const scriptDir = path.dirname(scriptPath) + +// Collect ALL node_modules directories walking upward from startDir. +// Bun's single-file executable uses a virtual filesystem (/$bunfs/root/) without +// node_modules. External packages are resolved via NODE_PATH instead. +// We collect every node_modules in the hierarchy (not just the first) to handle +// pnpm strict layouts, hoisted monorepos, and npm flat installs alike. +function findAllNodeModules(startDir) { + const paths = [] + let current = startDir + for (;;) { + const modules = path.join(current, "node_modules") + if (fs.existsSync(modules)) paths.push(modules) + const parent = path.dirname(current) + if (parent === current) break + current = parent + } + return paths +} + function run(target) { + // Resolve NODE_PATH so the compiled Bun binary can find external packages + // installed alongside the wrapper (e.g. @altimateai/altimate-core NAPI module). + // Search from BOTH the binary's location AND the wrapper script's location + // to cover npm flat installs, pnpm isolated stores, and hoisted monorepos. + const env = { ...process.env } + try { + const resolvedTarget = fs.realpathSync(target) + const targetDir = path.dirname(path.dirname(resolvedTarget)) + + const targetModules = findAllNodeModules(targetDir) + const scriptModules = findAllNodeModules(scriptDir) + const allPaths = [...new Set([...scriptModules, ...targetModules])] + + if (allPaths.length > 0) { + const sep = process.platform === "win32" ? ";" : ":" + const joined = allPaths.join(sep) + env.NODE_PATH = env.NODE_PATH ? joined + sep + env.NODE_PATH : joined + } + } catch { + // realpathSync failed (e.g. target doesn't exist) — continue without + // NODE_PATH; spawnSync will report the missing binary via result.error. + } + const result = childProcess.spawnSync(target, process.argv.slice(2), { stdio: "inherit", + env, }) if (result.error) { console.error(result.error.message) @@ -22,9 +68,6 @@ if (envPath) { run(envPath) } -const scriptPath = fs.realpathSync(__filename) -const scriptDir = path.dirname(scriptPath) - // const cached = path.join(scriptDir, ".altimate-code") if (fs.existsSync(cached)) { diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 802f475104..2206a093a9 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -204,17 +204,26 @@ for (const item of targets) { tsconfig: "./tsconfig.json", plugins: [solidPlugin], sourcemap: "external", - // Packages excluded from the compiled binary — loaded lazily at runtime. - // NOTE: @altimateai/altimate-core is intentionally NOT external — it's a - // napi binary that must be bundled for the CLI to work out of the box. + // Packages excluded from the compiled binary — resolved from node_modules + // at runtime. Bun compiled binaries resolve externals via standard Node + // resolution from the binary's location, walking up to the wrapper + // package's node_modules. + // + // IMPORTANT: Without code splitting, Bun inlines dynamic import() targets + // into the main chunk. Any external require() in those targets will fail + // at startup — not when the import() is called. Only mark packages as + // external when they truly cannot be bundled (e.g. NAPI native addons). external: [ - // dbt integration — heavy transitive deps, loaded on first dbt operation - "@altimateai/dbt-integration", - // Database drivers — users install on demand per warehouse + // NAPI native module — cannot be embedded in Bun single-file executable. + // The JS loader dynamically require()s platform-specific .node binaries + // (e.g. @altimateai/altimate-core-darwin-arm64). + // Must be installed as a dependency of the published wrapper package. + "@altimateai/altimate-core", + // Database drivers — native addons, users install on demand per warehouse "pg", "snowflake-sdk", "@google-cloud/bigquery", "@databricks/sql", "mysql2", "mssql", "oracledb", "duckdb", "better-sqlite3", - // Optional infra packages - "keytar", "ssh2", "dockerode", "yaml", + // Optional infra packages — native addons or heavy optional deps + "keytar", "ssh2", "dockerode", ], compile: { autoloadBunfig: false, diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 73d46e4a0f..29525a10f0 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -7,6 +7,18 @@ import { fileURLToPath } from "url" const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) +// NAPI native modules that must be installed alongside the CLI binary. +// These cannot be embedded in Bun's single-file executable — the JS loader +// dynamically require()s platform-specific .node binaries at runtime. +const altimateCoreDep = pkg.dependencies["@altimateai/altimate-core"] +if (!altimateCoreDep) { + console.error("Missing required dependency: @altimateai/altimate-core in package.json") + process.exit(1) +} +const runtimeDependencies: Record = { + "@altimateai/altimate-core": altimateCoreDep, +} + const binaries: Record = {} for (const filepath of new Bun.Glob("**/package.json").scanSync({ cwd: "./dist" })) { const pkg = await Bun.file(`./dist/${filepath}`).json() @@ -34,6 +46,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write( }, version: version, license: pkg.license, + dependencies: runtimeDependencies, optionalDependencies: binaries, }, null, @@ -81,6 +94,7 @@ try { }, version: version, license: pkg.license, + dependencies: runtimeDependencies, optionalDependencies: binaries, }, null,