Skip to content

Commit af3a4b7

Browse files
suryaiyer95claude
andcommitted
feat: ship dbt-tools with altimate-code npm package
- 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 <noreply@anthropic.com>
1 parent 4cee25f commit af3a4b7

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

packages/opencode/script/publish.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!/usr/bin/env bun
22
import { $ } from "bun"
3+
import fs from "fs"
4+
import path from "path"
35
import pkg from "../package.json"
46
import { Script } from "@opencode-ai/script"
57
import { fileURLToPath } from "url"
@@ -43,12 +45,31 @@ for (const filepath of new Bun.Glob("**/package.json").scanSync({ cwd: "./dist"
4345
console.log("binaries", binaries)
4446
const version = Object.values(binaries)[0]
4547

48+
// Build dbt-tools so we can bundle it in the published package.
49+
// dbt-tools provides the `altimate-dbt` CLI used by the builder agent.
50+
const dbtToolsDir = "../dbt-tools"
51+
await $`bun run build`.cwd(dbtToolsDir)
52+
53+
// Bundle dbt-tools into the wrapper package: bin shim + bundled JS + Python packages.
54+
// Native .node files are NOT copied — @altimateai/altimate-core is already a runtime
55+
// dependency and provides them. This keeps the package ~200 MB lighter.
56+
async function copyDbtTools(destRoot: string) {
57+
await $`mkdir -p ${destRoot}/dbt-tools/bin`
58+
await $`mkdir -p ${destRoot}/dbt-tools/dist`
59+
await $`cp ${dbtToolsDir}/bin/altimate-dbt ${destRoot}/dbt-tools/bin/`
60+
await $`cp ${dbtToolsDir}/dist/index.js ${destRoot}/dbt-tools/dist/`
61+
if (fs.existsSync(path.join(dbtToolsDir, "dist/altimate_python_packages"))) {
62+
await $`cp -r ${dbtToolsDir}/dist/altimate_python_packages ${destRoot}/dbt-tools/dist/`
63+
}
64+
}
65+
4666
await $`mkdir -p ./dist/${pkg.name}`
4767
await $`cp -r ./bin ./dist/${pkg.name}/bin`
4868
await $`cp -r ../../.opencode/skills ./dist/${pkg.name}/skills`
4969
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
5070
await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text())
5171
await Bun.file(`./dist/${pkg.name}/CHANGELOG.md`).write(await Bun.file("../../CHANGELOG.md").text())
72+
await copyDbtTools(`./dist/${pkg.name}`)
5273

5374
await Bun.file(`./dist/${pkg.name}/package.json`).write(
5475
JSON.stringify(
@@ -57,6 +78,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
5778
bin: {
5879
altimate: "./bin/altimate",
5980
"altimate-code": "./bin/altimate-code",
81+
"altimate-dbt": "./dbt-tools/bin/altimate-dbt",
6082
},
6183
scripts: {
6284
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
@@ -94,6 +116,7 @@ try {
94116
await Bun.file(`${unscopedDir}/LICENSE`).write(await Bun.file("../../LICENSE").text())
95117
await Bun.file(`${unscopedDir}/CHANGELOG.md`).write(await Bun.file("../../CHANGELOG.md").text())
96118
await Bun.file(`${unscopedDir}/README.md`).write(await Bun.file("../../README.md").text())
119+
await copyDbtTools(unscopedDir)
97120
await Bun.file(`${unscopedDir}/package.json`).write(
98121
JSON.stringify(
99122
{
@@ -108,6 +131,7 @@ try {
108131
bin: {
109132
altimate: "./bin/altimate",
110133
"altimate-code": "./bin/altimate-code",
134+
"altimate-dbt": "./dbt-tools/bin/altimate-dbt",
111135
},
112136
scripts: {
113137
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",

packages/opencode/src/tool/bash.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,63 @@ import { Shell } from "@/shell/shell"
1717
import { BashArity } from "@/permission/arity"
1818
import { Truncate } from "./truncation"
1919
import { Plugin } from "@/plugin"
20+
import { existsSync } from "fs"
2021

2122
const MAX_METADATA_LENGTH = 30_000
23+
24+
const DBT_BINARY_NAMES =
25+
process.platform === "win32" ? ["altimate-dbt.exe", "altimate-dbt.cmd", "altimate-dbt"] : ["altimate-dbt"]
26+
27+
function hasDbtBinary(dir: string): boolean {
28+
return DBT_BINARY_NAMES.some((name) => existsSync(path.join(dir, name)))
29+
}
30+
31+
// Resolve dbt-tools/bin so `altimate-dbt` is on PATH when spawning bash commands.
32+
// Checks: env var → dev source tree → npm-installed wrapper package → walk-up fallback.
33+
const dbtToolsBin = lazy(() => {
34+
// 1. Explicit env var override
35+
if (process.env.ALTIMATE_DBT_TOOLS_BIN && hasDbtBinary(process.env.ALTIMATE_DBT_TOOLS_BIN)) {
36+
return process.env.ALTIMATE_DBT_TOOLS_BIN
37+
}
38+
39+
// 2. Dev mode: resolve from source tree
40+
// import.meta.dirname = packages/opencode/src/tool → ../../../../dbt-tools/bin
41+
if (import.meta.dirname && !import.meta.dirname.startsWith("/$bunfs")) {
42+
const devPath = path.resolve(import.meta.dirname, "../../../../dbt-tools/bin")
43+
if (hasDbtBinary(devPath)) return devPath
44+
}
45+
46+
// 3. npm installed: compiled binary lives in a platform-specific package,
47+
// dbt-tools ships in the wrapper package alongside it.
48+
// Binary: node_modules/@altimateai/altimate-code-<platform>/bin/altimate
49+
// Scoped: node_modules/@altimateai/altimate-code/dbt-tools/bin/altimate-dbt
50+
// Unscoped: node_modules/altimate-code/dbt-tools/bin/altimate-dbt
51+
try {
52+
const binDir = path.dirname(process.execPath)
53+
// scopeDir = node_modules/@altimateai (for scoped wrapper lookup)
54+
const scopeDir = path.resolve(binDir, "../..")
55+
// nodeModulesDir = node_modules (for unscoped wrapper lookup)
56+
const nodeModulesDir = path.dirname(scopeDir)
57+
for (const wrapper of ["altimate-code", "opencode"]) {
58+
const scoped = path.join(scopeDir, wrapper, "dbt-tools", "bin")
59+
if (hasDbtBinary(scoped)) return scoped
60+
const unscoped = path.join(nodeModulesDir, wrapper, "dbt-tools", "bin")
61+
if (hasDbtBinary(unscoped)) return unscoped
62+
}
63+
// Walk up for other layouts (global installs, pnpm, monorepos)
64+
let dir = binDir
65+
for (let i = 0; i < 8; i++) {
66+
if (hasDbtBinary(path.join(dir, "dbt-tools", "bin"))) return path.join(dir, "dbt-tools", "bin")
67+
const parent = path.dirname(dir)
68+
if (parent === dir) break // reached filesystem root
69+
dir = parent
70+
}
71+
} catch (e) {
72+
log.debug("dbtToolsBin: failed to resolve from execPath", { error: String(e) })
73+
}
74+
75+
return undefined
76+
})
2277
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
2378

2479
export const log = Log.create({ service: "bash-tool" })
@@ -164,12 +219,17 @@ export const BashTool = Tool.define("bash", async () => {
164219
{ cwd, sessionID: ctx.sessionID, callID: ctx.callID },
165220
{ env: {} },
166221
)
222+
const extraPath = dbtToolsBin()
223+
const envPATH = extraPath
224+
? `${extraPath}${path.delimiter}${process.env.PATH ?? ""}`
225+
: process.env.PATH
167226
const proc = spawn(params.command, {
168227
shell,
169228
cwd,
170229
env: {
171230
...process.env,
172231
...shellEnv.env,
232+
...(extraPath ? { PATH: envPATH } : {}),
173233
},
174234
stdio: ["ignore", "pipe", "pipe"],
175235
detached: process.platform !== "win32",

0 commit comments

Comments
 (0)