From af3a4b79a554e7d0f93f446efe22e70932f25580 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Thu, 19 Mar 2026 17:05:54 -0700 Subject: [PATCH 1/2] feat: ship `dbt-tools` with `altimate-code` npm package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `dbtToolsBin` resolver in `bash.ts` that finds `altimate-dbt` and injects it into PATH for spawned bash commands - Resolver checks: env var → dev source tree → scoped npm wrapper → unscoped npm wrapper → walk-up fallback - Build and bundle dbt-tools in `publish.ts` for both scoped (`@altimateai/altimate-code`) and unscoped (`altimate-code`) packages - Register `altimate-dbt` in published `bin` field so npm creates global symlinks on install - Bundle is ~3 MB (JS + Python packages), not 200 MB (native .node files excluded — provided by `@altimateai/altimate-core` runtime dep) Co-Authored-By: Claude Opus 4.6 --- packages/opencode/script/publish.ts | 24 ++++++++++++ packages/opencode/src/tool/bash.ts | 60 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 95a4486fd..3d8163b43 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -1,5 +1,7 @@ #!/usr/bin/env bun import { $ } from "bun" +import fs from "fs" +import path from "path" import pkg from "../package.json" import { Script } from "@opencode-ai/script" import { fileURLToPath } from "url" @@ -43,12 +45,31 @@ for (const filepath of new Bun.Glob("**/package.json").scanSync({ cwd: "./dist" console.log("binaries", binaries) const version = Object.values(binaries)[0] +// Build dbt-tools so we can bundle it in the published package. +// dbt-tools provides the `altimate-dbt` CLI used by the builder agent. +const dbtToolsDir = "../dbt-tools" +await $`bun run build`.cwd(dbtToolsDir) + +// Bundle dbt-tools into the wrapper package: bin shim + bundled JS + Python packages. +// Native .node files are NOT copied — @altimateai/altimate-core is already a runtime +// dependency and provides them. This keeps the package ~200 MB lighter. +async function copyDbtTools(destRoot: string) { + await $`mkdir -p ${destRoot}/dbt-tools/bin` + await $`mkdir -p ${destRoot}/dbt-tools/dist` + await $`cp ${dbtToolsDir}/bin/altimate-dbt ${destRoot}/dbt-tools/bin/` + await $`cp ${dbtToolsDir}/dist/index.js ${destRoot}/dbt-tools/dist/` + if (fs.existsSync(path.join(dbtToolsDir, "dist/altimate_python_packages"))) { + await $`cp -r ${dbtToolsDir}/dist/altimate_python_packages ${destRoot}/dbt-tools/dist/` + } +} + await $`mkdir -p ./dist/${pkg.name}` await $`cp -r ./bin ./dist/${pkg.name}/bin` await $`cp -r ../../.opencode/skills ./dist/${pkg.name}/skills` await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs` await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text()) await Bun.file(`./dist/${pkg.name}/CHANGELOG.md`).write(await Bun.file("../../CHANGELOG.md").text()) +await copyDbtTools(`./dist/${pkg.name}`) await Bun.file(`./dist/${pkg.name}/package.json`).write( JSON.stringify( @@ -57,6 +78,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write( bin: { altimate: "./bin/altimate", "altimate-code": "./bin/altimate-code", + "altimate-dbt": "./dbt-tools/bin/altimate-dbt", }, scripts: { postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs", @@ -94,6 +116,7 @@ try { await Bun.file(`${unscopedDir}/LICENSE`).write(await Bun.file("../../LICENSE").text()) await Bun.file(`${unscopedDir}/CHANGELOG.md`).write(await Bun.file("../../CHANGELOG.md").text()) await Bun.file(`${unscopedDir}/README.md`).write(await Bun.file("../../README.md").text()) + await copyDbtTools(unscopedDir) await Bun.file(`${unscopedDir}/package.json`).write( JSON.stringify( { @@ -108,6 +131,7 @@ try { bin: { altimate: "./bin/altimate", "altimate-code": "./bin/altimate-code", + "altimate-dbt": "./dbt-tools/bin/altimate-dbt", }, scripts: { postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs", diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 109a66536..b12866dac 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -17,8 +17,63 @@ import { Shell } from "@/shell/shell" import { BashArity } from "@/permission/arity" import { Truncate } from "./truncation" import { Plugin } from "@/plugin" +import { existsSync } from "fs" const MAX_METADATA_LENGTH = 30_000 + +const DBT_BINARY_NAMES = + process.platform === "win32" ? ["altimate-dbt.exe", "altimate-dbt.cmd", "altimate-dbt"] : ["altimate-dbt"] + +function hasDbtBinary(dir: string): boolean { + return DBT_BINARY_NAMES.some((name) => existsSync(path.join(dir, name))) +} + +// Resolve dbt-tools/bin so `altimate-dbt` is on PATH when spawning bash commands. +// Checks: env var → dev source tree → npm-installed wrapper package → walk-up fallback. +const dbtToolsBin = lazy(() => { + // 1. Explicit env var override + if (process.env.ALTIMATE_DBT_TOOLS_BIN && hasDbtBinary(process.env.ALTIMATE_DBT_TOOLS_BIN)) { + return process.env.ALTIMATE_DBT_TOOLS_BIN + } + + // 2. Dev mode: resolve from source tree + // import.meta.dirname = packages/opencode/src/tool → ../../../../dbt-tools/bin + if (import.meta.dirname && !import.meta.dirname.startsWith("/$bunfs")) { + const devPath = path.resolve(import.meta.dirname, "../../../../dbt-tools/bin") + if (hasDbtBinary(devPath)) return devPath + } + + // 3. npm installed: compiled binary lives in a platform-specific package, + // dbt-tools ships in the wrapper package alongside it. + // Binary: node_modules/@altimateai/altimate-code-/bin/altimate + // Scoped: node_modules/@altimateai/altimate-code/dbt-tools/bin/altimate-dbt + // Unscoped: node_modules/altimate-code/dbt-tools/bin/altimate-dbt + try { + const binDir = path.dirname(process.execPath) + // scopeDir = node_modules/@altimateai (for scoped wrapper lookup) + const scopeDir = path.resolve(binDir, "../..") + // nodeModulesDir = node_modules (for unscoped wrapper lookup) + const nodeModulesDir = path.dirname(scopeDir) + for (const wrapper of ["altimate-code", "opencode"]) { + const scoped = path.join(scopeDir, wrapper, "dbt-tools", "bin") + if (hasDbtBinary(scoped)) return scoped + const unscoped = path.join(nodeModulesDir, wrapper, "dbt-tools", "bin") + if (hasDbtBinary(unscoped)) return unscoped + } + // Walk up for other layouts (global installs, pnpm, monorepos) + let dir = binDir + for (let i = 0; i < 8; i++) { + if (hasDbtBinary(path.join(dir, "dbt-tools", "bin"))) return path.join(dir, "dbt-tools", "bin") + const parent = path.dirname(dir) + if (parent === dir) break // reached filesystem root + dir = parent + } + } catch (e) { + log.debug("dbtToolsBin: failed to resolve from execPath", { error: String(e) }) + } + + return undefined +}) const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 export const log = Log.create({ service: "bash-tool" }) @@ -164,12 +219,17 @@ export const BashTool = Tool.define("bash", async () => { { cwd, sessionID: ctx.sessionID, callID: ctx.callID }, { env: {} }, ) + const extraPath = dbtToolsBin() + const envPATH = extraPath + ? `${extraPath}${path.delimiter}${process.env.PATH ?? ""}` + : process.env.PATH const proc = spawn(params.command, { shell, cwd, env: { ...process.env, ...shellEnv.env, + ...(extraPath ? { PATH: envPATH } : {}), }, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32", From ab44f676be28997aac1e0b0d92948bae7810a384 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Thu, 19 Mar 2026 17:44:36 -0700 Subject: [PATCH 2/2] fix: correct `dbtToolsBin` resolver path and PATH merge order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix dev-mode path: `../../../../dbt-tools/bin` → `../../../dbt-tools/bin` (was resolving to repo root instead of `packages/dbt-tools/bin`, meaning `altimate-dbt` was never found in development) - Fix PATH merge order: compute `envPATH` from merged `baseEnv` (process.env + shellEnv.env) so plugin PATH entries are preserved - Move `log` declaration above `dbtToolsBin` resolver to eliminate temporal dependency on lazy execution order - Add blank line between resolver closure and `DEFAULT_TIMEOUT` Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/tool/bash.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index b12866dac..92057fc22 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -21,6 +21,8 @@ import { existsSync } from "fs" const MAX_METADATA_LENGTH = 30_000 +export const log = Log.create({ service: "bash-tool" }) + const DBT_BINARY_NAMES = process.platform === "win32" ? ["altimate-dbt.exe", "altimate-dbt.cmd", "altimate-dbt"] : ["altimate-dbt"] @@ -37,9 +39,9 @@ const dbtToolsBin = lazy(() => { } // 2. Dev mode: resolve from source tree - // import.meta.dirname = packages/opencode/src/tool → ../../../../dbt-tools/bin + // import.meta.dirname = packages/opencode/src/tool → ../../../dbt-tools/bin if (import.meta.dirname && !import.meta.dirname.startsWith("/$bunfs")) { - const devPath = path.resolve(import.meta.dirname, "../../../../dbt-tools/bin") + const devPath = path.resolve(import.meta.dirname, "../../../dbt-tools/bin") if (hasDbtBinary(devPath)) return devPath } @@ -74,9 +76,8 @@ const dbtToolsBin = lazy(() => { return undefined }) -const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 -export const log = Log.create({ service: "bash-tool" }) +const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 const resolveWasm = (asset: string) => { if (asset.startsWith("file://")) return fileURLToPath(asset) @@ -219,17 +220,17 @@ export const BashTool = Tool.define("bash", async () => { { cwd, sessionID: ctx.sessionID, callID: ctx.callID }, { env: {} }, ) + const baseEnv: Record = { ...process.env, ...shellEnv.env } const extraPath = dbtToolsBin() const envPATH = extraPath - ? `${extraPath}${path.delimiter}${process.env.PATH ?? ""}` - : process.env.PATH + ? `${extraPath}${path.delimiter}${baseEnv.PATH ?? ""}` + : baseEnv.PATH const proc = spawn(params.command, { shell, cwd, env: { - ...process.env, - ...shellEnv.env, - ...(extraPath ? { PATH: envPATH } : {}), + ...baseEnv, + ...(envPATH ? { PATH: envPATH } : {}), }, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32",