Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ jobs:
- name: Install dependencies
run: bun install

- name: Build dbt-tools (bundled with CLI)
run: bun run build
working-directory: packages/dbt-tools

- name: Free disk space for artifact download + npm publish
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
Expand Down
1,184 changes: 978 additions & 206 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/opencode/bin/altimate
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ function run(target) {
// 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 }

// Export bin directory so the compiled binary can add it to PATH when
// spawning bash commands. This makes bundled tools (e.g. altimate-dbt)
// available to agents without manual PATH configuration.
env.ALTIMATE_BIN_DIR = scriptDir

try {
const resolvedTarget = fs.realpathSync(target)
const targetDir = path.dirname(path.dirname(resolvedTarget))
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/bin/altimate-code
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ function run(target) {
// 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 }

// Export bin directory so the compiled binary can add it to PATH when
// spawning bash commands. This makes bundled tools (e.g. altimate-dbt)
// available to agents without manual PATH configuration.
env.ALTIMATE_BIN_DIR = scriptDir

try {
const resolvedTarget = fs.realpathSync(target)
const targetDir = path.dirname(path.dirname(resolvedTarget))
Expand Down
22 changes: 22 additions & 0 deletions packages/opencode/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,27 @@ const migrations = await Promise.all(
)
console.log(`Loaded ${migrations.length} migrations`)

// Load builtin skills from .opencode/skills/ directory for embedding in binary.
// This ensures skills are available in ALL distribution channels (npm, Homebrew, AUR, Docker)
// without relying on postinstall filesystem copies.
const skillsRoot = path.resolve(dir, "../../.opencode/skills")
const skillEntries = fs.existsSync(skillsRoot)
? (await fs.promises.readdir(skillsRoot, { withFileTypes: true })).filter((e) => e.isDirectory())
: []

const builtinSkills: { name: string; content: string }[] = []
for (const entry of skillEntries) {
const skillFile = path.join(skillsRoot, entry.name, "SKILL.md")
if (!fs.existsSync(skillFile)) continue
const content = await Bun.file(skillFile).text()
builtinSkills.push({ name: entry.name, content })
}
console.log(`Loaded ${builtinSkills.length} builtin skills`)
if (Script.release && builtinSkills.length === 0) {
console.error("No builtin skills were loaded from ../../.opencode/skills; aborting release build.")
process.exit(1)
}

const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
Expand Down Expand Up @@ -242,6 +263,7 @@ for (const item of targets) {
// ALTIMATE_ENGINE_VERSION removed — Python engine eliminated
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
OPENCODE_BUILTIN_SKILLS: JSON.stringify(builtinSkills),
OPENCODE_CHANGELOG: JSON.stringify(changelog),
OPENCODE_WORKER_PATH: workerPath,
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
Expand Down
42 changes: 42 additions & 0 deletions packages/opencode/script/postinstall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,46 @@ function copyDirRecursive(src, dst) {
}
}

/**
* Link bundled dbt-tools binary into the package's bin/ directory so it's
* available alongside the main CLI binary. The wrapper script exports
* ALTIMATE_BIN_DIR pointing to this directory.
*/
function setupDbtTools() {
try {
const dbtBinSrc = path.join(__dirname, "dbt-tools", "bin", "altimate-dbt")
if (!fs.existsSync(dbtBinSrc)) {
console.warn(`Bundled altimate-dbt entrypoint missing: ${dbtBinSrc}`)
return
}

const binDir = path.join(__dirname, "bin")
if (!fs.existsSync(binDir)) fs.mkdirSync(binDir, { recursive: true })

const target = path.join(binDir, "altimate-dbt")
if (fs.existsSync(target)) fs.unlinkSync(target)

// Prefer symlink (preserves original relative imports), fall back to
// writing a new wrapper with the correct path from bin/ → dbt-tools/dist/.
try {
fs.symlinkSync(dbtBinSrc, target)
} catch {
// Direct copy would break the `import("../dist/index.js")` resolution
// since the script moves from dbt-tools/bin/ → bin/. Write a wrapper instead.
fs.writeFileSync(target, '#!/usr/bin/env node\nimport("../dbt-tools/dist/index.js")\n')
}
fs.chmodSync(target, 0o755)

// Windows: create .cmd shim since cmd.exe doesn't understand shebangs
if (os.platform() === "win32") {
const cmdTarget = path.join(binDir, "altimate-dbt.cmd")
fs.writeFileSync(cmdTarget, '@echo off\r\nnode "%~dp0\\..\\dbt-tools\\dist\\index.js" %*\r\n')
}
} catch (error) {
console.warn("Failed to setup bundled altimate-dbt:", error)
}
Comment on lines +136 to +168
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Surface altimate-dbt setup failures instead of silently succeeding.

If the bundled dbt-tools entrypoint is missing, or any of the file operations here fail, install completes without exposing that altimate-dbt was never wired into bin/. That leaves npm users in the exact half-configured state this PR is trying to eliminate.

💡 Minimal fix
 function setupDbtTools() {
   try {
     const dbtBinSrc = path.join(__dirname, "dbt-tools", "bin", "altimate-dbt")
-    if (!fs.existsSync(dbtBinSrc)) return
+    if (!fs.existsSync(dbtBinSrc)) {
+      console.warn(`Bundled altimate-dbt entrypoint missing: ${dbtBinSrc}`)
+      return
+    }
@@
-  } catch {
-    // Non-fatal — dbt-tools is optional
+  } catch (error) {
+    console.warn("Failed to setup bundled altimate-dbt:", error)
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/script/postinstall.mjs` around lines 136 - 165, The
setupDbtTools function currently swallows all errors; change it to surface
failures by removing the broad outer try/catch (or at minimum logging the caught
error and rethrowing it) so any missing dbt-tools entrypoint or file operation
error is visible to the installer. Specifically update setupDbtTools (and
references to dbtBinSrc, target, cmdTarget) to let exceptions propagate (or call
console.error with the caught error and throw) instead of silently returning,
and ensure any error message includes the actual error details so npm install
fails loudly when wiring altimate-dbt into bin/ fails.

}

/**
* Copy bundled skills to ~/.altimate/builtin/ on every install/upgrade.
* The entire directory is wiped and replaced so each release is the single
Expand Down Expand Up @@ -178,6 +218,7 @@ async function main() {
// No postinstall setup needed
if (version) writeUpgradeMarker(version)
copySkillsToAltimate()
setupDbtTools()
return
}

Expand All @@ -196,6 +237,7 @@ async function main() {
// The CLI picks up the marker and shows the welcome box on first run.
if (version) writeUpgradeMarker(version)
copySkillsToAltimate()
setupDbtTools()
} catch (error) {
console.error("Failed to setup altimate-code binary:", error.message)
process.exit(1)
Expand Down
31 changes: 23 additions & 8 deletions packages/opencode/script/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,29 @@ 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 alongside the CLI
console.log("Building dbt-tools...")
await $`bun run build`.cwd("../dbt-tools")
console.log("dbt-tools built successfully")

/**
* Copy common assets (bin, skills, dbt-tools, postinstall, license, changelog)
* into a target dist directory. Shared by scoped and unscoped packages.
*/
async function copyAssets(targetDir: string) {
await $`cp -r ./bin ${targetDir}/bin`
await $`cp -r ../../.opencode/skills ${targetDir}/skills`
await $`cp ./script/postinstall.mjs ${targetDir}/postinstall.mjs`
// Bundle dbt-tools: copy its bin wrapper + built dist
await $`mkdir -p ${targetDir}/dbt-tools/bin`
await $`cp ../dbt-tools/bin/altimate-dbt ${targetDir}/dbt-tools/bin/altimate-dbt`
await $`cp -r ../dbt-tools/dist ${targetDir}/dbt-tools/dist`
await Bun.file(`${targetDir}/LICENSE`).write(await Bun.file("../../LICENSE").text())
await Bun.file(`${targetDir}/CHANGELOG.md`).write(await Bun.file("../../CHANGELOG.md").text())
}

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 copyAssets(`./dist/${pkg.name}`)
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())

Expand Down Expand Up @@ -88,11 +107,7 @@ const unscopedName = "altimate-code"
const unscopedDir = `./dist/${unscopedName}`
try {
await $`mkdir -p ${unscopedDir}`
await $`cp -r ./bin ${unscopedDir}/bin`
await $`cp -r ../../.opencode/skills ${unscopedDir}/skills`
await $`cp ./script/postinstall.mjs ${unscopedDir}/postinstall.mjs`
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 copyAssets(unscopedDir)
await Bun.file(`${unscopedDir}/README.md`).write(await Bun.file("../../README.md").text())
await Bun.file(`${unscopedDir}/package.json`).write(
JSON.stringify(
Expand Down
47 changes: 41 additions & 6 deletions packages/opencode/src/skill/skill.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import z from "zod"
import path from "path"
import os from "os"
import matter from "gray-matter"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { NamedError } from "@opencode-ai/util/error"
Expand All @@ -17,6 +18,13 @@ import { pathToFileURL } from "url"
import type { Agent } from "@/agent/agent"
import { PermissionNext } from "@/permission/next"

// Builtin skills embedded at build time — available in ALL distribution channels
// (npm, Homebrew, AUR, Docker) without relying on postinstall filesystem copies.
// Falls back to filesystem scan in dev mode when the global is undefined.
declare const OPENCODE_BUILTIN_SKILLS:
| { name: string; content: string }[]
| undefined

export namespace Skill {
const log = Log.create({ service: "skill" })
export const Info = z.object({
Expand Down Expand Up @@ -104,10 +112,13 @@ export namespace Skill {
})
}

// altimate_change start - scan ~/.altimate/builtin/ for release-managed builtin skills
// This path is fully owned by postinstall (wipe-and-replace on every release).
// Kept separate from user-editable skill dirs so users never accidentally modify builtins.
// Load builtin skills — prefer filesystem (supports @references), fall back
// to binary-embedded data (works without postinstall for Homebrew/AUR/Docker).
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
let loadedFromFs = false

// Try filesystem first — postinstall copies skills (including references/) to
// ~/.altimate/builtin/. Filesystem paths are required for @references resolution.
const builtinDir = path.join(Global.Path.home, ".altimate", "builtin")
if (await Filesystem.isDir(builtinDir)) {
const matches = await Glob.scan(SKILL_PATTERN, {
Expand All @@ -116,10 +127,34 @@ export namespace Skill {
include: "file",
symlink: true,
})
await Promise.all(matches.map(addSkill))
if (matches.length > 0) {
await Promise.all(matches.map(addSkill))
loadedFromFs = true
}
}

// Fallback: load from binary-embedded data when filesystem is unavailable
// (e.g. Homebrew, AUR, Docker installs that skip npm postinstall).
// Note: @references won't resolve for embedded skills, but core functionality works.
if (!loadedFromFs && typeof OPENCODE_BUILTIN_SKILLS !== "undefined") {
for (const entry of OPENCODE_BUILTIN_SKILLS) {
try {
const md = matter(entry.content)
const meta = Info.pick({ name: true, description: true }).safeParse(md.data)
if (!meta.success) continue
skills[meta.data.name] = {
name: meta.data.name,
description: meta.data.description,
location: `builtin:${entry.name}/SKILL.md`,
content: md.content,
}
} catch (err) {
log.error("failed to parse embedded skill", { skill: entry.name, err })
}
}
log.info("loaded embedded builtin skills", { count: OPENCODE_BUILTIN_SKILLS.length })
}
}
// altimate_change end

// Scan external skill directories (.claude/skills/, .agents/skills/, etc.)
// Load global (home) first, then project-level (so project-level overwrites)
Expand Down Expand Up @@ -224,7 +259,7 @@ export namespace Skill {
` <skill>`,
` <name>${skill.name}</name>`,
` <description>${skill.description}</description>`,
` <location>${pathToFileURL(skill.location).href}</location>`,
` <location>${skill.location.startsWith("builtin:") ? skill.location : pathToFileURL(skill.location).href}</location>`,
` </skill>`,
]),
"</available_skills>",
Expand Down
19 changes: 15 additions & 4 deletions packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,24 @@ export const BashTool = Tool.define("bash", async () => {
{ cwd, sessionID: ctx.sessionID, callID: ctx.callID },
{ env: {} },
)

// Merge process.env + shell plugin env, then prepend bundled tools dir.
// shellEnv.env may contain PATH additions from user's shell profile.
const mergedEnv: Record<string, string | undefined> = { ...process.env, ...shellEnv.env }
const binDir = process.env.ALTIMATE_BIN_DIR
if (binDir) {
const sep = process.platform === "win32" ? ";" : ":"
const basePath = mergedEnv.PATH ?? mergedEnv.Path ?? ""
const pathEntries = basePath.split(sep).filter(Boolean)
if (!pathEntries.some((entry) => entry === binDir)) {
mergedEnv.PATH = basePath ? `${binDir}${sep}${basePath}` : binDir
}
}

const proc = spawn(params.command, {
shell,
cwd,
env: {
...process.env,
...shellEnv.env,
},
env: mergedEnv,
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
windowsHide: process.platform === "win32",
Expand Down
43 changes: 23 additions & 20 deletions packages/opencode/src/tool/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,28 +112,31 @@ export const SkillTool = Tool.define("skill", async (ctx) => {
metadata: {},
})

const dir = path.dirname(skill.location)
const base = pathToFileURL(dir).href
const isBuiltin = skill.location.startsWith("builtin:")
const dir = isBuiltin ? "" : path.dirname(skill.location)
const base = isBuiltin ? skill.location : pathToFileURL(dir).href

const limit = 10
const files = await iife(async () => {
const arr = []
for await (const file of Ripgrep.files({
cwd: dir,
follow: false,
hidden: true,
signal: ctx.abort,
})) {
if (file.includes("SKILL.md")) {
continue
}
arr.push(path.resolve(dir, file))
if (arr.length >= limit) {
break
}
}
return arr
}).then((f) => f.map((file) => `<file>${file}</file>`).join("\n"))
const files = isBuiltin
? ""
: await iife(async () => {
const arr = []
for await (const file of Ripgrep.files({
cwd: dir,
follow: false,
hidden: true,
signal: ctx.abort,
})) {
if (file.includes("SKILL.md")) {
continue
}
arr.push(path.resolve(dir, file))
if (arr.length >= limit) {
break
}
}
return arr
}).then((f) => f.map((file) => `<file>${file}</file>`).join("\n"))

// altimate_change start — telemetry instrumentation for skill loading
try {
Expand Down
Loading
Loading