Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/opencode/script/publish.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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(
Expand All @@ -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",
Expand Down Expand Up @@ -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(
{
Expand All @@ -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",
Expand Down
67 changes: 64 additions & 3 deletions packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,68 @@ 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 DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000

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"]

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-<platform>/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

const resolveWasm = (asset: string) => {
if (asset.startsWith("file://")) return fileURLToPath(asset)
if (asset.startsWith("/") || /^[a-z]:/i.test(asset)) return asset
Expand Down Expand Up @@ -164,12 +220,17 @@ export const BashTool = Tool.define("bash", async () => {
{ cwd, sessionID: ctx.sessionID, callID: ctx.callID },
{ env: {} },
)
const baseEnv: Record<string, string | undefined> = { ...process.env, ...shellEnv.env }
const extraPath = dbtToolsBin()
const envPATH = extraPath
? `${extraPath}${path.delimiter}${baseEnv.PATH ?? ""}`
: baseEnv.PATH
const proc = spawn(params.command, {
shell,
cwd,
env: {
...process.env,
...shellEnv.env,
...baseEnv,
...(envPATH ? { PATH: envPATH } : {}),
},
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
Expand Down
Loading